g1t/apps/web/app/lib/security-alerts.test.ts

170 lines6,002 bytesCodeBlame
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { AlertActivity, SecretFinding, Vulnerability } from "@g1t/contracts";
5
6import {
7 alertActivity,
8 alertCapability,
9 compareVersions,
10 countByState,
11 groupByPackage,
12 highestFix,
13 latestUpdate,
14 parseAlertState,
15 splitSecrets,
16 tabOf,
17 worstSeverity,
18} from "./security-alerts.ts";
19
20const secret = (over: Partial<SecretFinding> = {}): SecretFinding => ({
21 id: "sec_1",
22 repoId: "r",
23 kind: "aws_access_key",
24 label: "an AWS access key",
25 path: "src/config.ts",
26 line: 4,
27 commit: "abcdef1234",
28 preview: "AKIA…WXYZ",
29 status: "open",
30 source: "history",
31 foundBy: null,
32 foundAt: "2026-10-01T10:00:00Z",
33 decidedBy: null,
34 reason: null,
35 decidedAt: null,
36 state: "open",
37 ...over,
38});
39
40const vuln = (over: Partial<Vulnerability> = {}): Vulnerability => ({
41 id: "vul_1",
42 repoId: "r",
43 ecosystem: "npm",
44 package: "lodash",
45 version: "4.17.20",
46 manifest: "package-lock.json",
47 advisory: "GHSA-1",
48 osvId: "GHSA-1",
49 summary: "Prototype pollution",
50 severity: "high",
51 fixedVersion: "4.17.21",
52 status: "open",
53 issue: null,
54 foundAt: "2026-10-01T10:00:00Z",
55 fixedAt: null,
56 state: "open",
57 ...over,
58});
59
60test("the state parameter falls back to open", () => {
61 assert.equal(parseAlertState(null), "open");
62 assert.equal(parseAlertState("dismissed"), "dismissed");
63 assert.equal(parseAlertState("fixed"), "fixed");
64 assert.equal(parseAlertState("nonsense"), "open");
65});
66
67test("ids say which tab and which role an alert takes", () => {
68 assert.equal(tabOf("sec_9"), "secrets");
69 assert.equal(tabOf("vul_9"), "dependencies");
70 assert.equal(alertCapability("sec_9"), "manage_integrations");
71 assert.equal(alertCapability("vul_9"), "push");
72});
73
74test("alerts are counted by state", () => {
75 assert.deepEqual(countByState([secret(), secret({ state: "dismissed" }), secret({ state: "fixed" }), secret()]), {
76 open: 2,
77 dismissed: 1,
78 fixed: 1,
79 });
80});
81
82test("likely test values are listed apart from real secrets", () => {
83 const real = secret({ id: "sec_real" });
84 const fake = secret({ id: "sec_fake", testValue: "AWS's documented example key" });
85 const { real: shown, tests } = splitSecrets([fake, real]);
86 assert.deepEqual(shown.map((s) => s.id), ["sec_real"]);
87 assert.deepEqual(tests.map((s) => s.id), ["sec_fake"]);
88});
89
90test("packages group their alerts and take the worst severity", () => {
91 const groups = groupByPackage([
92 vuln({ id: "vul_1", severity: "medium" }),
93 vuln({ id: "vul_2", package: "express" }),
94 vuln({ id: "vul_3", severity: "critical", fixedVersion: "4.17.3" }),
95 ]);
96 assert.deepEqual(groups.map((g) => g.name), ["lodash", "express"]);
97 assert.equal(worstSeverity(groups[0].vulns), "critical");
98 assert.equal(highestFix(groups[0].vulns), "4.17.21");
99});
100
101test("versions compare number by number", () => {
102 assert.ok(compareVersions("4.17.10", "4.17.9") > 0);
103 assert.ok(compareVersions("1.2.0", "1.10.0") < 0);
104 assert.equal(highestFix([vuln({ fixedVersion: null })]), null);
105});
106
107test("the newest security update wins", () => {
108 const older = { state: "superseded" as const, target: "4.17.20", branch: null, pull: 3, issue: null, error: null, updatedAt: "2026-10-01T00:00:00Z" };
109 const newer = { ...older, state: "open" as const, target: "4.17.21", pull: 14, updatedAt: "2026-10-02T00:00:00Z" };
110 assert.equal(latestUpdate([vuln({ update: older }), vuln({ update: newer }), vuln()])?.pull, 14);
111 assert.equal(latestUpdate([vuln()]), null);
112});
113
114const row = (over: Partial<AlertActivity>): AlertActivity => ({
115 id: "act_1",
116 alertId: "sec_1",
117 action: "dismissed",
118 actor: "syntaqx",
119 reason: "used_in_tests",
120 comment: "A fixture.",
121 number: null,
122 at: "2026-10-03T00:00:00Z",
123 ...over,
124});
125
126test("a secret's activity starts with when it was found", () => {
127 const entries = alertActivity(secret({ source: "push", status: "blocked", foundBy: "ana" }), [
128 row({}),
129 row({ id: "act_2", alertId: "sec_other" }),
130 ]);
131 assert.deepEqual(
132 entries.map((e) => [e.actor, e.text]),
133 [
134 ["ana", "pushed it, and the push was refused"],
135 ["syntaqx", "dismissed it"],
136 ],
137 );
138 assert.equal(entries[1].reason, "used_in_tests");
139});
140
141test("an older decision without a row is shown from the alert itself", () => {
142 const entries = alertActivity(
143 secret({ status: "allowed", state: "dismissed", decidedBy: "ana", decidedAt: "2026-10-02T00:00:00Z", reason: "Docs example." }),
144 [],
145 );
146 assert.equal(entries.length, 2);
147 assert.equal(entries[0].text, "Found in the history");
148 assert.equal(entries[0].actor, null);
149 assert.deepEqual([entries[1].actor, entries[1].comment, entries[1].reason], ["ana", "Docs example.", "false_positive"]);
150 // A row for the dismissal replaces it.
151 const covered = alertActivity(
152 secret({ status: "allowed", state: "dismissed", decidedBy: "ana", decidedAt: "2026-10-02T00:00:00Z" }),
153 [row({ actor: "ana" })],
154 );
155 assert.equal(covered.filter((e) => e.text === "dismissed it").length, 1);
156});
157
158test("a dependency's activity links its pull request and issue", () => {
159 const entries = alertActivity(vuln({ state: "fixed", status: "fixed", fixedAt: "2026-10-05T00:00:00Z" }), [
160 row({ id: "a", alertId: "vul_1", action: "update_opened", actor: "g1t", reason: null, comment: null, number: 14, at: "2026-10-02T00:00:00Z" }),
161 row({ id: "b", alertId: "vul_1", action: "update_needs_code", actor: "g1t", reason: null, comment: null, number: 15, at: "2026-10-03T00:00:00Z" }),
162 row({ id: "c", alertId: "vul_1", action: "update_merged", actor: "g1t", reason: null, comment: null, number: 14, at: "2026-10-04T00:00:00Z" }),
163 ]);
164 assert.deepEqual(
165 entries.map((e) => e.ref),
166 [null, { kind: "pull", number: 14 }, { kind: "issue", number: 15 }, { kind: "pull", number: 14 }],
167 );
168 // A merged update already says it was fixed.
169 assert.ok(!entries.some((e) => e.text === "found it no longer vulnerable"));
170});