@blamejs/exceptd-skills 0.19.32 → 0.19.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +8 -8
  4. package/data/_indexes/activity-feed.json +2 -2
  5. package/data/_indexes/catalog-summaries.json +7 -7
  6. package/data/_indexes/chains.json +60118 -0
  7. package/data/attack-techniques.json +267 -7
  8. package/data/cve-catalog.json +9991 -3
  9. package/data/cwe-catalog.json +109 -2
  10. package/data/framework-control-gaps.json +578 -3
  11. package/data/zeroday-lessons.json +8330 -1
  12. package/lib/auto-discovery.js +56 -286
  13. package/lib/canonical-eq.js +7 -40
  14. package/lib/citation-resolve.js +22 -70
  15. package/lib/collectors/ai-api.js +20 -54
  16. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  17. package/lib/collectors/citation-hygiene.js +72 -210
  18. package/lib/collectors/containers.js +41 -130
  19. package/lib/collectors/cred-stores.js +31 -115
  20. package/lib/collectors/crypto-codebase.js +55 -138
  21. package/lib/collectors/crypto.js +24 -54
  22. package/lib/collectors/hardening.js +20 -78
  23. package/lib/collectors/kernel.js +16 -46
  24. package/lib/collectors/library-author.js +57 -206
  25. package/lib/collectors/mcp.js +24 -70
  26. package/lib/collectors/runtime.js +24 -86
  27. package/lib/collectors/sbom.js +34 -106
  28. package/lib/collectors/scan-excludes.js +31 -138
  29. package/lib/collectors/secrets.js +62 -178
  30. package/lib/cross-ref-api.js +39 -123
  31. package/lib/currency-severity.js +8 -27
  32. package/lib/cve-batch.js +13 -21
  33. package/lib/cve-cli.js +13 -20
  34. package/lib/cve-curation.js +72 -239
  35. package/lib/cve-regression-watcher.js +29 -152
  36. package/lib/cvss.js +13 -54
  37. package/lib/doctor-bucketing.js +3 -19
  38. package/lib/exit-codes.js +10 -42
  39. package/lib/flag-suggest.js +7 -25
  40. package/lib/framework-gap.js +35 -114
  41. package/lib/gap-detectors.js +37 -159
  42. package/lib/id-validation.js +9 -30
  43. package/lib/job-queue.js +13 -36
  44. package/lib/lint-skills.js +64 -232
  45. package/lib/playbook-runner.js +693 -2095
  46. package/lib/prefetch.js +100 -376
  47. package/lib/refresh-external.js +199 -627
  48. package/lib/refresh-network.js +75 -307
  49. package/lib/rfc-cli.js +23 -68
  50. package/lib/scoring.js +77 -145
  51. package/lib/sign.js +43 -229
  52. package/lib/source-advisories.js +43 -194
  53. package/lib/source-ghsa.js +37 -120
  54. package/lib/source-osv.js +94 -266
  55. package/lib/ttp-mapper.js +14 -24
  56. package/lib/upstream-check-cli.js +10 -28
  57. package/lib/upstream-check.js +19 -44
  58. package/lib/validate-catalog-meta.js +17 -61
  59. package/lib/validate-cve-catalog.js +43 -119
  60. package/lib/validate-indexes.js +25 -76
  61. package/lib/validate-package.js +16 -62
  62. package/lib/validate-playbooks.js +69 -275
  63. package/lib/validate-vendor.js +16 -49
  64. package/lib/verify.js +56 -286
  65. package/lib/version-pins.js +5 -34
  66. package/lib/worker-pool.js +11 -30
  67. package/lib/xml-tokenizer.js +47 -152
  68. package/manifest.json +53 -53
  69. package/orchestrator/dispatcher.js +17 -68
  70. package/orchestrator/event-bus.js +11 -74
  71. package/orchestrator/index.js +138 -412
  72. package/orchestrator/pipeline.js +28 -85
  73. package/orchestrator/scanner.js +34 -138
  74. package/orchestrator/scheduler.js +20 -84
  75. package/package.json +2 -2
  76. package/sbom.cdx.json +253 -253
  77. package/scripts/audit-catalog-gaps.js +9 -62
  78. package/scripts/audit-cross-skill.js +5 -31
  79. package/scripts/audit-perf.js +6 -16
  80. package/scripts/backfill-theater-test.js +7 -64
  81. package/scripts/bootstrap.js +12 -44
  82. package/scripts/build-indexes.js +40 -154
  83. package/scripts/builders/activity-feed.js +4 -14
  84. package/scripts/builders/catalog-summaries.js +3 -10
  85. package/scripts/builders/currency.js +7 -20
  86. package/scripts/builders/cwe-chains.js +7 -30
  87. package/scripts/builders/did-ladders.js +6 -13
  88. package/scripts/builders/frequency.js +5 -19
  89. package/scripts/builders/jurisdiction-clocks.js +6 -25
  90. package/scripts/builders/recipes.js +6 -14
  91. package/scripts/builders/section-offsets.js +13 -51
  92. package/scripts/builders/stale-content.js +7 -28
  93. package/scripts/builders/summary-cards.js +8 -29
  94. package/scripts/builders/theater-fingerprints.js +12 -27
  95. package/scripts/builders/token-budget.js +4 -31
  96. package/scripts/check-agents-md-collectors.js +11 -54
  97. package/scripts/check-catalog-gap-budget.js +15 -32
  98. package/scripts/check-changelog-extract.js +18 -48
  99. package/scripts/check-codebase-patterns-currency.js +6 -22
  100. package/scripts/check-codebase-patterns.js +50 -143
  101. package/scripts/check-epss-consistency.js +9 -64
  102. package/scripts/check-framework-gap-coverage.js +13 -31
  103. package/scripts/check-manifest-snapshot.js +13 -73
  104. package/scripts/check-sbom-currency.js +44 -142
  105. package/scripts/check-test-count.js +15 -52
  106. package/scripts/check-test-coverage.js +66 -197
  107. package/scripts/check-test-subjects.js +21 -62
  108. package/scripts/check-ttp-references.js +14 -38
  109. package/scripts/check-ttp-upstream.js +8 -40
  110. package/scripts/check-version-bump.js +9 -61
  111. package/scripts/check-version-tags.js +20 -121
  112. package/scripts/predeploy.js +38 -184
  113. package/scripts/refresh-manifest-snapshot.js +16 -38
  114. package/scripts/refresh-mitre-atlas.js +3 -8
  115. package/scripts/refresh-mitre-attack.js +1 -8
  116. package/scripts/refresh-mitre-d3fend.js +3 -9
  117. package/scripts/refresh-mitre-ics-attack.js +3 -8
  118. package/scripts/refresh-reverse-refs.js +27 -94
  119. package/scripts/refresh-rfc-index.js +2 -10
  120. package/scripts/refresh-sbom.js +31 -161
  121. package/scripts/refresh-upstream-catalogs.js +40 -137
  122. package/scripts/release.js +69 -232
  123. package/scripts/run-e2e-scenarios.js +24 -71
  124. package/scripts/sync-manifest-metadata.js +10 -34
  125. package/scripts/sync-package-description.js +8 -17
  126. package/scripts/validate-vendor-online.js +13 -44
  127. package/scripts/verify-shipped-tarball.js +35 -140
@@ -1,19 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * lib/collectors/secrets.js
5
- *
6
- * Companion collector for the `secrets` playbook. Walks the cwd
7
- * tree, identifies the artifact files (env / auth-config / ssh-keys /
8
- * iac-credential-bearers), runs the catalogued regex set against text
9
- * file contents, and stats permission posture on secret-carrier
10
- * files. Emits a submission with deterministic signal_overrides per
11
- * indicator that fired.
12
- *
13
- * Scope: any cwd. Cross-platform (Windows / macOS / Linux).
14
- * Permission-posture indicator only meaningful on POSIX hosts.
15
- *
16
- * Interface: see lib/collectors/README.md
4
+ * Companion collector for the `secrets` playbook: walks the cwd tree, classifies
5
+ * artifact files, regex-scans text content, and stats permission posture on the
6
+ * carriers (POSIX only). Interface: lib/collectors/README.md
17
7
  */
18
8
 
19
9
  const fs = require("node:fs");
@@ -22,19 +12,13 @@ const { codeExcludeSet, walkTree, buildEvidenceLocations, lineFromOffset } = req
22
12
 
23
13
  const COLLECTOR_ID = "secrets";
24
14
 
25
- // Walk depth + exclusion list mirrors the secrets playbook's
26
- // `look.artifacts[repo-tree].source` declaration. Exclusions come from
27
- // the shared code-scope policy (dependency caches, build output, VCS +
28
- // agent/editor scratch including `.claude/`); no secrets-specific extras.
15
+ // Depth and exclusions mirror the secrets playbook's `look.artifacts[repo-tree]`;
16
+ // the exclusions come from the shared code-scope policy.
29
17
  const DEFAULT_MAX_DEPTH = 6;
30
18
  const DEFAULT_EXCLUDES = codeExcludeSet();
31
19
 
32
- // Path segments that denote test / fixture / example material. Hits
33
- // scoped exclusively to these paths are downgraded — a private-key
34
- // block in `cosign-test.key` or a JWT literal in `_test.go` is
35
- // expected test material, not a real secret. If at least one hit
36
- // exists outside these paths, the indicator still fires. Mirrors
37
- // the crypto-codebase collector's isTestPath shape.
20
+ // Hits confined to these paths are downgraded; one hit outside them still fires.
21
+ // Mirrors the crypto-codebase collector's isTestPath.
38
22
  const TEST_PATH_SEGMENTS = [
39
23
  "/test/", "/tests/", "/spec/", "/specs/", "/__tests__/",
40
24
  "/fixtures/", "/fixture/", "/examples/", "/example/",
@@ -47,12 +31,9 @@ function isTestPath(rel) {
47
31
  for (const seg of TEST_PATH_SEGMENTS) {
48
32
  if (norm.includes(seg)) return true;
49
33
  }
50
- // foo.test.js / bar.spec.py — dot-separated convention.
51
34
  if (/\.(test|spec)\.[a-z]+$/i.test(rel)) return true;
52
- // foo_test.go (Go) / bar_test.py (some Python) — underscore convention.
53
35
  if (/(?:^|[\\/])[^\\/]+_test\.[a-z]+$/i.test(rel)) return true;
54
- // Files whose name itself includes the substring "test" before a
55
- // key extension (e.g. cosign-test.key, github-test-token.json).
36
+ // "test" inside the name itself: cosign-test.key, github-test-token.json.
56
37
  if (/-test[-.][^\\/]*$/i.test(rel)) return true;
57
38
  return false;
58
39
  }
@@ -73,11 +54,8 @@ const AUTH_CONFIG_FILES = new Set([
73
54
 
74
55
  const SSH_PRIVATE_KEY_FILES = new Set(["id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"]);
75
56
  const SSH_PRIVATE_KEY_EXTS = new Set([".pem", ".key", ".p12", ".pfx"]);
76
- // `.pem` / `.key` are ambiguous: a certificate chain (`fullchain.pem`) or a
77
- // public trust anchor (`bimi-trust-anchors.pem`) is conventionally `.pem` and
78
- // carries no private key. A name-only classifier flags these as private keys.
79
- // `.p12` / `.pfx` are binary keystores (private material by nature) and the
80
- // id_* names are private SSH keys, so only the PEM extensions are content-gated.
57
+ // `.pem` / `.key` are ambiguous — a cert chain is conventionally `.pem` — so only
58
+ // those two are content-gated; `.p12` / `.pfx` and id_* are private by nature.
81
59
  const PEM_PRIVATE_KEY_MARKER = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/;
82
60
  const CONTENT_GATED_KEY_EXTS = new Set([".pem", ".key"]);
83
61
  function carriesPrivateKey(file) {
@@ -88,15 +66,9 @@ function carriesPrivateKey(file) {
88
66
  fd = fs.openSync(file.full, "r");
89
67
  const st = fs.fstatSync(fd); // fstat on the open fd — no stat-then-read race
90
68
  if (st.size > MAX_FILE_BYTES) return true; // too big to scan → conservative
91
- // Read the whole descriptor to EOF. A single readSync may return fewer
92
- // bytes than requested (a short read on a network / FUSE / interrupted fd),
93
- // which would leave the buffer's tail NUL-padded; if the BEGIN marker sat
94
- // past that boundary — e.g. a .pem with a leading `Bag Attributes` /
95
- // subject= header block, the default OpenSSL pkcs12→PEM layout — the marker
96
- // test would return false and silently drop a real private key. readFileSync
97
- // on the open fd loops internally until EOF, so the test always sees the
98
- // complete content. Reading from the already-open fd keeps the no-TOCTOU
99
- // open→fstat ordering (no second path lookup).
69
+ // readFileSync on the open fd, never readSync: a short read NUL-pads the tail,
70
+ // and a BEGIN marker past that boundary (a .pem with a leading `Bag Attributes`
71
+ // header) would test false and drop a real key. Also preserves open→fstat order.
100
72
  const content = fs.readFileSync(fd, "utf8");
101
73
  return PEM_PRIVATE_KEY_MARKER.test(content);
102
74
  } catch {
@@ -110,37 +82,21 @@ const IAC_EXTS = new Set([".tf", ".tfvars", ".bicep"]);
110
82
  const IAC_EXACT = new Set(["terraform.tfstate", "values.yaml", "secret.yaml"]);
111
83
  const IAC_GLOB_PREFIX = ["pulumi.", "arm."];
112
84
 
113
- // Indicator regex set — must mirror data/playbooks/secrets.json's
114
- // detect.indicators[].value embedded patterns. The playbook is the
115
- // source of truth for what counts as a hit; the collector
116
- // implements the same patterns so its signal_overrides match what
117
- // the runner would compute.
118
- // AWS-published documentation/example access-key IDs. These appear verbatim
119
- // throughout AWS docs, SDK samples, and countless READMEs, so a literal match
120
- // is example material, not a leaked credential. `cred-stores` demotes the same
121
- // value (its FP[0]); secrets.js must too or it false-positives on any README
122
- // that quotes the AWS docs. The 40-char example secret
123
- // (`wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`) carries the literal `EXAMPLE`
124
- // token, which the AWS-secret-access-key pattern already requires elsewhere;
125
- // the access-key ID is the one that needs an explicit allowlist.
85
+ // The patterns and demotions below mirror data/playbooks/secrets.json's
86
+ // detect.indicators[] — the playbook is the source of truth for what counts as a hit.
87
+ // AWS's published example access-key ID appears verbatim across docs and READMEs;
88
+ // `cred-stores` demotes the same value in its FP[0].
126
89
  const AWS_EXAMPLE_ACCESS_KEY_IDS = new Set([
127
90
  "AKIAIOSFODNN7EXAMPLE",
128
91
  ]);
129
92
 
130
- // AWS-published sample secret-access-key (paired with AKIAIOSFODNN7EXAMPLE
131
- // throughout the AWS docs). aws-secret-access-key false_positive_checks_required[1]
132
- // demotes this exact value.
93
+ // AWS's published sample secret key; aws-secret-access-key FP[1] demotes it.
133
94
  const AWS_EXAMPLE_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
134
95
 
135
- // Placeholder/fixture substrings the API-key indicators' FP[0] checks demote.
136
- // A literal `PLACEHOLDER` / `EXAMPLE` / `XXXX` run / a `dummy`/`test` infix
137
- // marks documentation material rather than a live credential.
96
+ // Substrings the API-key indicators' FP[0] checks demote as documentation material.
138
97
  const PLACEHOLDER_RE = /placeholder|example|redacted|dummy|x{4,}|0{6,}|1234567890/i;
139
98
 
140
- // Path segments the per-indicator FP path checks treat as documentation /
141
- // fixture material in addition to TEST_PATH_SEGMENTS (which already covers
142
- // /examples/, /fixtures/, /test/ etc.). The secret indicators' FP prose adds
143
- // /docs/ and quickstart/snippet paths.
99
+ // Documentation paths the FP checks treat as fixture material, atop TEST_PATH_SEGMENTS.
144
100
  const DOC_PATH_SEGMENTS = [
145
101
  "/docs/", "/doc/", "/sdk-quickstart/", "/quickstart/", "/docs-snippet/",
146
102
  ];
@@ -151,13 +107,9 @@ function isDocOrTestPath(rel) {
151
107
  return DOC_PATH_SEGMENTS.some((seg) => norm.includes(seg));
152
108
  }
153
109
 
154
- // Per-indicator deterministic false_positive_checks_required evaluation.
155
- // Returns the set of FP-check indices (as strings) the collector can attest
156
- // for a single hit — i.e. the checks it actually ran and that the hit
157
- // survives. Indices requiring network reachability or operator judgement are
158
- // deliberately omitted so the runner honestly downgrades to inconclusive.
159
- // `value` is the matched credential, `file` the relative path, `window` a
160
- // few-line context slice around the match.
110
+ // The false_positive_checks_required indices (as strings) this hit survives. Indices
111
+ // needing network reachability or operator judgement are omitted, so the runner
112
+ // downgrades to inconclusive rather than over-attesting. `window` is a context slice.
161
113
  function fpIndicesSatisfied(indicatorId, value, file, window) {
162
114
  const sat = new Set();
163
115
  const notDocPath = !isDocOrTestPath(file);
@@ -165,51 +117,37 @@ function fpIndicesSatisfied(indicatorId, value, file, window) {
165
117
  case "aws-secret-access-key": {
166
118
  // [0] co-occurrence with an AKIA*/ASIA*/AGPA*/AIDA* id in a 10-line window
167
119
  if (/\b(?:AKIA|ASIA|AGPA|AIDA)[0-9A-Z]{12,}\b/.test(window)) sat.add("0");
168
- // [1] not the AWS-published sample secret
169
120
  if (value !== AWS_EXAMPLE_SECRET_KEY) sat.add("1");
170
- // [2] not under examples/ docs/ fixtures/ / a test snapshot
171
121
  if (notDocPath) sat.add("2");
172
122
  break;
173
123
  }
174
124
  case "slack-bot-or-user-token": {
175
- // [0] not a placeholder / published doc fixture
176
125
  if (!PLACEHOLDER_RE.test(value)) sat.add("0");
177
- // [1] conforms to a current Slack token shape: at least three
178
- // dash-separated segments after the xox? prefix
126
+ // [1] current Slack shape: 3+ dash-separated segments after the prefix
179
127
  if (value.split("-").length >= 4) sat.add("1");
180
- // [2] not under examples/ docs/ fixtures/
181
128
  if (notDocPath) sat.add("2");
182
129
  break;
183
130
  }
184
131
  case "stripe-secret-key": {
185
- // [0] not a sk_test_ published sample (deterministic only for the
186
- // test prefix; live keys are handled by [2])
132
+ // [0] not a published sk_test_ sample; live keys fall to [2]
187
133
  if (!(value.startsWith("sk_test_") && PLACEHOLDER_RE.test(value))) sat.add("0");
188
- // [1] not under examples/ fixtures/ docs/ / a quickstart template
189
134
  if (notDocPath) sat.add("1");
190
- // [2] the live-validity probe applies only to sk_live_*; a sk_test_
191
- // key carries no live financial exposure, so the check is moot and
192
- // the collector can attest it deterministically. sk_live_* still
193
- // needs operator-authorised network validation — left unattested.
135
+ // [2] live-validity probe is moot for a test key; sk_live_* needs operator auth
194
136
  if (value.startsWith("sk_test_") || value.startsWith("rk_test_")) sat.add("2");
195
137
  break;
196
138
  }
197
139
  case "openai-api-key": {
198
- // [0] not a placeholder / sk-test- / sk-dummy- fixture
199
140
  if (!PLACEHOLDER_RE.test(value) && !/^sk-(?:test|dummy)-/i.test(value)) sat.add("0");
200
- // [1] post-prefix length meets the entropy floor (>= 48 chars)
141
+ // [1] post-prefix length meets the entropy floor
201
142
  if (value.replace(/^sk-(?:proj-|svcacct-|admin-)?/, "").length >= 48) sat.add("1");
202
143
  // [2] vendor disambiguation — sk-ant-* is Anthropic, not OpenAI
203
144
  if (!/^sk-ant-/i.test(value)) sat.add("2");
204
145
  break;
205
146
  }
206
147
  case "anthropic-api-key": {
207
- // [0] not a placeholder / sk-ant-test- fixture
208
148
  if (!PLACEHOLDER_RE.test(value) && !/^sk-ant-test-/i.test(value)) sat.add("0");
209
- // [1] not under examples/ fixtures/ sdk-quickstart/ docs-snippet/
210
149
  if (notDocPath) sat.add("1");
211
- // [2] post-prefix length meets the entropy floor (>= 80 chars after
212
- // sk-ant-(api03|admin01)-)
150
+ // [2] post-prefix length meets the entropy floor
213
151
  if (value.replace(/^sk-ant-(?:api03|admin01)-/, "").length >= 80) sat.add("2");
214
152
  break;
215
153
  }
@@ -222,33 +160,22 @@ function fpIndicesSatisfied(indicatorId, value, file, window) {
222
160
  const INDICATOR_PATTERNS = [
223
161
  { id: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/g },
224
162
  { id: "aws-secret-access-key", re: /\baws_secret_access_key\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi },
225
- // Require a full PEM block (header, base64 body, closing marker) — not just
226
- // the `-----BEGIN PRIVATE KEY-----` header — so a service-account JSON shown
227
- // as a doc placeholder or a redaction/DLP library's detection-pattern literal
228
- // does not register as a real embedded key. A real key is JSON-encoded with
229
- // `\n` escapes, so the body class includes backslash; `-` is excluded so the
230
- // run halts at `-----END` (no backtracking, ReDoS-safe). Mirrors
231
- // ssh-private-key-block below.
163
+ // A full PEM block, not just the BEGIN header, so a doc placeholder or a DLP
164
+ // library's own detection pattern does not register. The body class admits
165
+ // backslash for JSON `\n` escapes and excludes `-` so the run halts — ReDoS-safe.
232
166
  { id: "gcp-service-account-json", re: /"type"\s*:\s*"service_account"[\s\S]{0,1200}?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[A-Za-z0-9+/=\s\\]{40,4000}-----END/g },
233
167
  { id: "github-personal-access-token", re: /\bghp_[A-Za-z0-9]{36}\b/g },
234
168
  { id: "github-fine-grained-pat", re: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
235
169
  { id: "slack-bot-or-user-token", re: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g },
236
170
  { id: "stripe-secret-key", re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
237
171
  { id: "jwt-token-with-secret-context", re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
238
- // Require a complete PEM block — header, base64 body, closing marker —
239
- // so a bare `BEGIN ... PRIVATE KEY` header used as a detection pattern (a
240
- // redaction/DLP library's regex literal) or a doc placeholder does not
241
- // register as an embedded key. `-` is excluded from the body class so the
242
- // run halts at the first `-` of `-----END` (no backtracking, ReDoS-safe).
243
- // Private-key *files* (.pem/.key) are detected separately by file presence.
172
+ // A complete PEM block, so a bare BEGIN header used as a DLP library's own
173
+ // detection pattern does not register. `-` excluded from the body class halts
174
+ // the run at `-----END` — no backtracking. Key *files* are found by presence.
244
175
  { id: "ssh-private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED |)PRIVATE KEY-----[A-Za-z0-9+/=\s]{20,4000}-----END/g },
245
- // The `(?!ant-)` lookahead keeps an Anthropic `sk-ant-*` key from also
246
- // matching here. Without it the trailing empty alternative made the prefix
247
- // optional (`sk-<20+ chars>`), so one Anthropic key double-fired as both
248
- // openai-api-key and anthropic-api-key — inflating the hit count and
249
- // mislabeling the vendor. The optional `(?:proj-|svcacct-|admin-)?` group
250
- // still admits every real OpenAI shape (proj/svcacct/admin and the bare
251
- // legacy `sk-` key); the lookahead is anchored, so no backtracking / ReDoS.
176
+ // `(?!ant-)` stops an Anthropic key double-firing as openai-api-key too. The
177
+ // optional prefix group still admits every real OpenAI shape, including the bare
178
+ // legacy `sk-` key; the lookahead is anchored, so no ReDoS.
252
179
  { id: "openai-api-key", re: /\bsk-(?!ant-)(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}\b/g },
253
180
  { id: "anthropic-api-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
254
181
  ];
@@ -310,12 +237,8 @@ function redactMatch(literal) {
310
237
  function scanContent(full, rel) {
311
238
  let buf;
312
239
  try {
313
- // Read raw bytes first, then enforce the 1 MB cap on the buffer length.
314
- // The previous statSync-before-read doubled the per-file syscall count on
315
- // the hot path; reading the buffer and measuring it is byte-accurate (the
316
- // cap is a byte limit) without the extra stat. Reading as a Buffer rather
317
- // than decoding to a string up front means an oversized file is rejected
318
- // before any UTF-8 decode work.
240
+ // The cap is a byte limit, so measure the Buffer rather than stat first —
241
+ // byte-accurate, and oversize is rejected before any UTF-8 decode.
319
242
  const raw = fs.readFileSync(full);
320
243
  if (raw.length > MAX_FILE_BYTES) return { skipped: "file_too_large", bytes: raw.length, hits: [] };
321
244
  buf = raw.toString("utf8");
@@ -327,26 +250,21 @@ function scanContent(full, rel) {
327
250
  const matches = buf.matchAll(p.re);
328
251
  let count = 0;
329
252
  for (const m of matches) {
330
- // Demote AWS-published example access-key IDs (e.g. the docs' canonical
331
- // AKIAIOSFODNN7EXAMPLE). A README quoting the AWS docs must not hit.
253
+ // A README quoting the AWS docs must not hit.
332
254
  if (p.id === "aws-access-key-id" && AWS_EXAMPLE_ACCESS_KEY_IDS.has(m[0])) continue;
333
- // The captured credential is group 1 when the pattern brackets it
334
- // (aws_secret_access_key=<value>); otherwise the whole match.
255
+ // Group 1 when the pattern brackets the credential; else the whole match.
335
256
  const value = m[1] != null ? m[1] : m[0];
336
- // ±600-byte context window (a deterministic proxy for "nearby lines")
337
- // used by the co-occurrence FP check.
257
+ // ±600 bytes — a deterministic proxy for "nearby lines" in the FP check.
338
258
  const winStart = Math.max(0, m.index - 600);
339
259
  const window = buf.slice(winStart, m.index + 600);
340
260
  hits.push({
341
261
  indicator_id: p.id,
342
262
  file: rel,
343
263
  offset: m.index,
344
- // 1-based line of the match so buildEvidenceLocations emits a region
345
- // (SARIF startLine) instead of a bare file-level location.
264
+ // 1-based, so buildEvidenceLocations emits a SARIF startLine region.
346
265
  line: lineFromOffset(buf, m.index),
347
266
  redacted_match: redactMatch(m[0]),
348
- // The false_positive_checks_required indices this hit deterministically
349
- // survives — attested so the runner doesn't downgrade hit → inconclusive.
267
+ // Attested so the runner does not downgrade hit → inconclusive.
350
268
  fp_satisfied: fpIndicesSatisfied(p.id, value, rel, window),
351
269
  });
352
270
  if (++count >= 5) break; // cap per-indicator-per-file
@@ -368,10 +286,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
368
286
  errors.push({ kind: "walk_failed", reason: e.message });
369
287
  files = [];
370
288
  }
371
- // Subtrees pruned for exceeding the depth cap are NOT scanned. Surface them
372
- // so an unscanned deep .env / id_rsa / api-key file is an observable gap,
373
- // not a silent miss indistinguishable from "scanned, found nothing" —
374
- // symmetric with the per-file `file_too_large_skipped` reporting below.
289
+ // Subtrees pruned at the depth cap are NOT scanned; surface them so an unscanned
290
+ // deep .env or id_rsa is an observable gap, not a silent miss.
375
291
  if (truncations.length) {
376
292
  errors.push({
377
293
  artifact_id: "repo-tree",
@@ -423,9 +339,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
423
339
  if (r.skipped === "read_error") {
424
340
  errors.push({ artifact_id: "secret-regex-scan-text-files", kind: "read_failed", reason: `${f.rel}: ${r.reason}` });
425
341
  } else if (r.skipped === "file_too_large") {
426
- // A secret in the first bytes of a large file would otherwise be
427
- // dropped silently. Record the skip so the operator knows this file
428
- // was NOT scanned (mirrors crypto-codebase's >1 MB read_failed entry).
342
+ // Record the skip, or a secret in an oversized file is dropped silently.
429
343
  errors.push({
430
344
  artifact_id: "secret-regex-scan-text-files",
431
345
  kind: "file_too_large_skipped",
@@ -434,9 +348,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
434
348
  }
435
349
  }
436
350
 
437
- // Split hits into production vs test-path. The indicator fires
438
- // only when at least one PROD hit exists. Test-only hits stay in
439
- // the artifact for operator inspection but don't flip the signal.
351
+ // The indicator fires only on a PROD hit; test-path hits stay in the artifact.
440
352
  const hitsByIndicator = {};
441
353
  const prodHitsByIndicator = {};
442
354
  for (const h of allHits) {
@@ -445,23 +357,16 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
445
357
  (prodHitsByIndicator[h.indicator_id] = prodHitsByIndicator[h.indicator_id] || []).push(h);
446
358
  }
447
359
  }
448
- // Same split for the file-presence indicators (ssh-private-keys
449
- // artifact backs the ssh-private-key-block content scan AND the
450
- // ssh-key-bad-perms posture check below). Filter out test-named
451
- // private-key files (e.g. cosign-test.key) for the signal too.
360
+ // The same split for the file-presence indicators, so a fixture key raises neither.
452
361
  const prodSshPrivateKeys = sshPrivateKeys.filter(f => !isTestPath(f.rel));
453
362
 
454
363
  const signal_overrides = {};
455
364
  for (const p of INDICATOR_PATTERNS) {
456
365
  signal_overrides[p.id] = prodHitsByIndicator[p.id] && prodHitsByIndicator[p.id].length > 0 ? "hit" : "miss";
457
366
  }
458
- // Per-indicator __fp_checks attestation. For each FP-gated indicator that
459
- // fired, attest the false_positive_checks_required indices the collector
460
- // deterministically ran AND that EVERY surviving hit satisfies (the
461
- // intersection — an index is only universally true if no hit fails it).
462
- // Network / operator-judgement indices are never in the set, so the runner
463
- // still downgrades indicators that carry one. Without this, a real secret
464
- // surfaced by `collect` is downgraded to inconclusive after `run`.
367
+ // For each fired indicator, attest the FP indices EVERY surviving hit satisfies —
368
+ // the intersection, since an index is universally true only if no hit fails it.
369
+ // Without this, a real secret found by `collect` is downgraded after `run`.
465
370
  for (const p of INDICATOR_PATTERNS) {
466
371
  if (signal_overrides[p.id] !== "hit") continue;
467
372
  const hits = prodHitsByIndicator[p.id] || [];
@@ -478,46 +383,26 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
478
383
  signal_overrides[`${p.id}__fp_checks`] = att;
479
384
  }
480
385
  }
481
- // ssh-private-key-block is also flipped by file presence (a private
482
- // key file with the matching magic bytes counts even without a
483
- // content scan match — e.g. binary-only key formats). Re-flip when
484
- // any non-test private-key file was discovered.
386
+ // Also flipped by file presence: a binary-only key format has no content match.
485
387
  if (prodSshPrivateKeys.length > 0) signal_overrides["ssh-private-key-block"] = "hit";
486
- // world-writable-env-file predicate (per data/playbooks/secrets.json):
487
- // restricted to env-files artifact entries
488
- // any .env / .env.* / .envrc with mode 0666 or 0664 (group/world writable)
489
- // i.e. group-write OR world-write bit set (mode & 0o022).
388
+ // Per the playbook: any env-files entry with the group-write or world-write bit set.
490
389
  const envFilePostures = process.platform === "win32" ? [] : envFiles.map(f => ({ file: f.rel, ...statPosture(f.full) }));
491
- // POSIX mode bits are unreadable on win32, so this posture cannot be checked
492
- // there. OMIT the signal on Windows (mirroring cred-stores' credentials-file-
493
- // bad-perms) so the runner returns `inconclusive` rather than a deterministic
494
- // false `miss` for a check that physically cannot run. (The posture array is
495
- // still declared — empty on win32 — for the SARIF per-indicator locations.)
390
+ // POSIX mode bits are unreadable on win32, so the signal is OMITTED there — the
391
+ // runner returns `inconclusive`, not a false `miss`. Same as cred-stores' bad-perms.
496
392
  if (process.platform !== "win32") {
497
393
  signal_overrides["world-writable-env-file"] = envFilePostures.some(p => p.error == null && (p.mode & 0o022) !== 0) ? "hit" : "miss";
498
394
  }
499
395
 
500
- // ssh-key-bad-perms predicate (per playbook):
501
- // restricted to ssh-private-keys artifact + ~/.ssh/id_* paths
502
- // any private-key file with mode != 0600
503
- // The collector scope is the cwd; ~/.ssh enumeration is outside this
504
- // walk root. Within cwd, flag any discovered private key whose mode
505
- // is anything other than 0600 (strict). Use the test-path-filtered set
506
- // (prodSshPrivateKeys) — matching ssh-private-key-block — so a fixture
507
- // key checked in under a test/ path doesn't raise a bad-perms posture.
396
+ // Per the playbook: any private key whose mode is not exactly 0600. ~/.ssh lies
397
+ // outside the cwd walk root, so only keys discovered here are checked.
508
398
  const sshKeyPostures = process.platform === "win32" ? [] : prodSshPrivateKeys.map(f => ({ file: f.rel, ...statPosture(f.full) }));
509
- // Same as world-writable-env-file: omit on win32 (mode bits unreadable) so the
510
- // runner returns inconclusive instead of a forced false `miss`.
399
+ // Omitted on win32 for the same reason as world-writable-env-file.
511
400
  if (process.platform !== "win32") {
512
401
  signal_overrides["ssh-key-bad-perms"] = sshKeyPostures.some(p => p.error == null && p.mode !== 0o600) ? "hit" : "miss";
513
402
  }
514
403
 
515
- // Per-indicator file locations for every indicator flipped to "hit", so
516
- // a SARIF result points at the file carrying the secret / bad posture.
517
- // Content-regex hits carry a 1-based `line` (derived from the match offset),
518
- // so these locations include a startLine region. The file-presence and
519
- // posture indicators contribute the carrier file path directly (file-level,
520
- // no line).
404
+ // Locations for every indicator flipped to "hit", so a SARIF result points at the
405
+ // carrying file. A content hit carries a line and becomes a region.
521
406
  const evidence_locations = {};
522
407
  for (const p of INDICATOR_PATTERNS) {
523
408
  if (signal_overrides[p.id] === "hit") {
@@ -525,8 +410,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
525
410
  if (locs.length) evidence_locations[p.id] = locs;
526
411
  }
527
412
  }
528
- // ssh-private-key-block also fires on private-key file presence — fold in
529
- // the discovered key files alongside any content-scan hits, de-duplicated.
413
+ // Fold the discovered key files in alongside content hits, de-duplicated.
530
414
  if (signal_overrides["ssh-private-key-block"] === "hit") {
531
415
  const locs = buildEvidenceLocations([
532
416
  ...(prodHitsByIndicator["ssh-private-key-block"] || []),