| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { CodeAlert, SecretFinding } from "@g1t/contracts"; |
| 5 | |
| 6 | import { |
| 7 | codeFilters, |
| 8 | codeScanningBranch, |
| 9 | codeScanningPullBody, |
| 10 | codeScanningWorkflow, |
| 11 | countStates, |
| 12 | keepCode, |
| 13 | keepSecret, |
| 14 | legacySecurityTarget, |
| 15 | secretFilters, |
| 16 | secretTypes, |
| 17 | severityCounts, |
| 18 | total, |
| 19 | trendMax, |
| 20 | } from "./security-suite.ts"; |
| 21 | |
| 22 | const secret = (over: Partial<SecretFinding> = {}): SecretFinding => ({ |
| 23 | id: "sec_1", |
| 24 | repoId: "r", |
| 25 | kind: "github_token", |
| 26 | label: "a GitHub token", |
| 27 | path: "scripts/release.sh", |
| 28 | line: 12, |
| 29 | commit: "abcdef1234", |
| 30 | preview: "ghp_X7…", |
| 31 | status: "open", |
| 32 | source: "push", |
| 33 | foundBy: "ana", |
| 34 | foundAt: "2026-10-06T09:00:00.000Z", |
| 35 | decidedBy: null, |
| 36 | reason: null, |
| 37 | decidedAt: null, |
| 38 | state: "open", |
| 39 | ...over, |
| 40 | }); |
| 41 | |
| 42 | const code = (over: Partial<CodeAlert> = {}): CodeAlert => ({ |
| 43 | id: "cod_1", |
| 44 | number: 1, |
| 45 | repoId: "r", |
| 46 | tool: "Semgrep OSS", |
| 47 | category: "Semgrep OSS", |
| 48 | ruleId: "rule", |
| 49 | ruleName: null, |
| 50 | ruleDescription: null, |
| 51 | help: null, |
| 52 | helpUri: null, |
| 53 | tags: [], |
| 54 | level: "error", |
| 55 | securitySeverity: null, |
| 56 | severity: "high", |
| 57 | message: "m", |
| 58 | path: "a.js", |
| 59 | startLine: 1, |
| 60 | endLine: 1, |
| 61 | startColumn: null, |
| 62 | endColumn: null, |
| 63 | state: "open", |
| 64 | fingerprint: "f", |
| 65 | firstCommit: "c", |
| 66 | lastCommit: "c", |
| 67 | createdAt: "", |
| 68 | updatedAt: "", |
| 69 | fixedAt: null, |
| 70 | dismissedBy: null, |
| 71 | dismissedReason: null, |
| 72 | dismissedComment: null, |
| 73 | dismissedAt: null, |
| 74 | issue: null, |
| 75 | ...over, |
| 76 | }); |
| 77 | |
| 78 | test("secret filters are read from the address and narrow the list", () => { |
| 79 | const filters = secretFilters(new URLSearchParams("state=open&validity=active&bypassed=true")); |
| 80 | assert.deepEqual(filters, { state: "open", type: null, validity: "active", bypassed: true }); |
| 81 | const bypassed = secret({ validity: "active", bypass: { reason: "will_fix_later", comment: null, by: "ana", at: "x", approvedBy: null } }); |
| 82 | assert.equal(keepSecret(bypassed, filters), true); |
| 83 | assert.equal(keepSecret(secret({ validity: "active" }), filters), false); |
| 84 | // Unknown words fall back to the defaults. |
| 85 | assert.deepEqual(secretFilters(new URLSearchParams("state=everything&validity=maybe")), { state: "open", type: null, validity: null, bypassed: null }); |
| 86 | // Never asked counts as unknown. |
| 87 | assert.equal(keepSecret(secret(), { state: "open", type: null, validity: "unknown", bypassed: null }), true); |
| 88 | }); |
| 89 | |
| 90 | test("secret types are listed once each, custom patterns by name", () => { |
| 91 | const types = secretTypes([secret(), secret({ id: "sec_2" }), secret({ kind: "custom_pattern", patternName: "Acme key", label: "a match for the custom pattern \"Acme key\"" })]); |
| 92 | assert.deepEqual(types, [["github_token", "GitHub token"], ["custom_pattern", "Custom: Acme key"]].sort((a, b) => a[1].localeCompare(b[1]))); |
| 93 | }); |
| 94 | |
| 95 | test("code filters and counts", () => { |
| 96 | const filters = codeFilters(new URLSearchParams("severity=high&tool=Semgrep OSS")); |
| 97 | assert.equal(keepCode(code(), filters), true); |
| 98 | assert.equal(keepCode(code({ severity: "low" }), filters), false); |
| 99 | assert.equal(keepCode(code({ state: "fixed" }), filters), false); |
| 100 | assert.deepEqual(countStates([code(), code({ state: "fixed" }), code({ state: "fixed" })]), { open: 1, dismissed: 0, fixed: 2 }); |
| 101 | const counts = severityCounts([code(), code({ severity: "critical" }), code({ state: "dismissed" })]); |
| 102 | assert.deepEqual(counts, { critical: 1, high: 1, medium: 0, low: 0, unknown: 0 }); |
| 103 | assert.equal(total(counts), 2); |
| 104 | }); |
| 105 | |
| 106 | test("old Security links go to the sections that replaced their tabs", () => { |
| 107 | const base = "/acme/rocket"; |
| 108 | assert.equal(legacySecurityTarget(base, new URLSearchParams("tab=secrets&finding=sec_9")), "/acme/rocket/security/secret-scanning/sec_9"); |
| 109 | assert.equal(legacySecurityTarget(base, new URLSearchParams("finding=vul_3")), "/acme/rocket/security/vulnerabilities?finding=vul_3"); |
| 110 | assert.equal(legacySecurityTarget(base, new URLSearchParams("tab=dependencies&state=fixed")), "/acme/rocket/security/vulnerabilities?state=fixed"); |
| 111 | assert.equal(legacySecurityTarget(base, new URLSearchParams("")), null); |
| 112 | }); |
| 113 | |
| 114 | test("the starter workflow scans each language it finds, then uploads SARIF for the right ref", () => { |
| 115 | const yaml = codeScanningWorkflow("main"); |
| 116 | assert.match(yaml, /branches: \["main"\]/); |
| 117 | // A scanner per language, each run only when the repository has it. |
| 118 | assert.match(yaml, /if: steps\.languages\.outputs\.python == 'true'/); |
| 119 | assert.match(yaml, /bandit --recursive \. .*\n.*--format sarif --output \/tmp\/sarif\/python\.sarif/); |
| 120 | assert.match(yaml, /if: steps\.languages\.outputs\.go == 'true'/); |
| 121 | assert.match(yaml, /-fmt sarif -out "\$out" \.\/\.\.\./); |
| 122 | assert.match(yaml, /if: steps\.languages\.outputs\.javascript == 'true'/); |
| 123 | assert.match(yaml, /eslint-plugin-security/); |
| 124 | assert.match(yaml, /@microsoft\/eslint-formatter-sarif/); |
| 125 | assert.match(yaml, /if: steps\.languages\.outputs\.rust == 'true'/); |
| 126 | assert.match(yaml, /cargo clippy --all-targets --message-format=json/); |
| 127 | assert.match(yaml, /clippy-sarif/); |
| 128 | assert.ok(!/semgrep/i.test(yaml), "no Semgrep"); |
| 129 | // Each language's results under their own category. |
| 130 | for (const category of ["python", "javascript", '"go"', '"rust"']) assert.ok(yaml.includes(category), category); |
| 131 | assert.match(yaml, /--arg category "\$category"/); |
| 132 | assert.match(yaml, /refs\/pull\/\$\(jq -r \.number "\$GITHUB_EVENT_PATH"\)\/head/); |
| 133 | assert.match(yaml, /--rawfile sarif "\$file\.b64"/); |
| 134 | assert.match(yaml, /\/code-scanning\/sarifs/); |
| 135 | assert.match(yaml, /\$\{\{ secrets\.G1T_TOKEN \}\}/); |
| 136 | assert.ok(!yaml.includes("\t"), "YAML takes no tabs"); |
| 137 | assert.match(codeScanningPullBody("main"), /Code scanning\*\* check/); |
| 138 | assert.equal(codeScanningBranch([]), "add-code-scanning"); |
| 139 | assert.equal(codeScanningBranch(["add-code-scanning", "add-code-scanning-2"]), "add-code-scanning-3"); |
| 140 | }); |
| 141 | |
| 142 | test("a trend is scaled to its tallest day", () => { |
| 143 | assert.equal(trendMax([]), 1); |
| 144 | assert.equal(trendMax([{ day: "d", secretScanning: 1, codeScanning: 2, vulnerability: 3 }]), 6); |
| 145 | }); |