@blamejs/exceptd-skills 0.19.33 → 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 (119) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +2 -2
  4. package/lib/auto-discovery.js +56 -286
  5. package/lib/canonical-eq.js +7 -40
  6. package/lib/citation-resolve.js +22 -70
  7. package/lib/collectors/ai-api.js +20 -54
  8. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  9. package/lib/collectors/citation-hygiene.js +72 -210
  10. package/lib/collectors/containers.js +41 -130
  11. package/lib/collectors/cred-stores.js +31 -115
  12. package/lib/collectors/crypto-codebase.js +55 -138
  13. package/lib/collectors/crypto.js +24 -54
  14. package/lib/collectors/hardening.js +20 -78
  15. package/lib/collectors/kernel.js +16 -46
  16. package/lib/collectors/library-author.js +57 -206
  17. package/lib/collectors/mcp.js +24 -70
  18. package/lib/collectors/runtime.js +24 -86
  19. package/lib/collectors/sbom.js +34 -106
  20. package/lib/collectors/scan-excludes.js +31 -138
  21. package/lib/collectors/secrets.js +62 -178
  22. package/lib/cross-ref-api.js +39 -123
  23. package/lib/currency-severity.js +8 -27
  24. package/lib/cve-batch.js +13 -21
  25. package/lib/cve-cli.js +13 -20
  26. package/lib/cve-curation.js +72 -239
  27. package/lib/cve-regression-watcher.js +29 -152
  28. package/lib/cvss.js +13 -54
  29. package/lib/doctor-bucketing.js +3 -19
  30. package/lib/exit-codes.js +10 -42
  31. package/lib/flag-suggest.js +7 -25
  32. package/lib/framework-gap.js +35 -114
  33. package/lib/gap-detectors.js +37 -159
  34. package/lib/id-validation.js +9 -30
  35. package/lib/job-queue.js +13 -36
  36. package/lib/lint-skills.js +64 -232
  37. package/lib/playbook-runner.js +693 -2095
  38. package/lib/prefetch.js +100 -376
  39. package/lib/refresh-external.js +199 -627
  40. package/lib/refresh-network.js +75 -307
  41. package/lib/rfc-cli.js +23 -68
  42. package/lib/scoring.js +77 -145
  43. package/lib/sign.js +43 -229
  44. package/lib/source-advisories.js +43 -194
  45. package/lib/source-ghsa.js +37 -120
  46. package/lib/source-osv.js +94 -266
  47. package/lib/ttp-mapper.js +14 -24
  48. package/lib/upstream-check-cli.js +10 -28
  49. package/lib/upstream-check.js +19 -44
  50. package/lib/validate-catalog-meta.js +17 -61
  51. package/lib/validate-cve-catalog.js +43 -119
  52. package/lib/validate-indexes.js +25 -76
  53. package/lib/validate-package.js +16 -62
  54. package/lib/validate-playbooks.js +69 -275
  55. package/lib/validate-vendor.js +16 -49
  56. package/lib/verify.js +56 -286
  57. package/lib/version-pins.js +5 -34
  58. package/lib/worker-pool.js +11 -30
  59. package/lib/xml-tokenizer.js +47 -152
  60. package/manifest.json +53 -53
  61. package/orchestrator/dispatcher.js +17 -68
  62. package/orchestrator/event-bus.js +11 -74
  63. package/orchestrator/index.js +138 -412
  64. package/orchestrator/pipeline.js +28 -85
  65. package/orchestrator/scanner.js +34 -138
  66. package/orchestrator/scheduler.js +20 -84
  67. package/package.json +1 -1
  68. package/sbom.cdx.json +241 -241
  69. package/scripts/audit-catalog-gaps.js +9 -62
  70. package/scripts/audit-cross-skill.js +5 -31
  71. package/scripts/audit-perf.js +6 -16
  72. package/scripts/backfill-theater-test.js +7 -64
  73. package/scripts/bootstrap.js +12 -44
  74. package/scripts/build-indexes.js +40 -154
  75. package/scripts/builders/activity-feed.js +4 -14
  76. package/scripts/builders/catalog-summaries.js +3 -10
  77. package/scripts/builders/currency.js +7 -20
  78. package/scripts/builders/cwe-chains.js +7 -30
  79. package/scripts/builders/did-ladders.js +6 -13
  80. package/scripts/builders/frequency.js +5 -19
  81. package/scripts/builders/jurisdiction-clocks.js +6 -25
  82. package/scripts/builders/recipes.js +6 -14
  83. package/scripts/builders/section-offsets.js +13 -51
  84. package/scripts/builders/stale-content.js +7 -28
  85. package/scripts/builders/summary-cards.js +8 -29
  86. package/scripts/builders/theater-fingerprints.js +12 -27
  87. package/scripts/builders/token-budget.js +4 -31
  88. package/scripts/check-agents-md-collectors.js +11 -54
  89. package/scripts/check-catalog-gap-budget.js +15 -32
  90. package/scripts/check-changelog-extract.js +18 -48
  91. package/scripts/check-codebase-patterns-currency.js +6 -22
  92. package/scripts/check-codebase-patterns.js +50 -143
  93. package/scripts/check-epss-consistency.js +9 -64
  94. package/scripts/check-framework-gap-coverage.js +13 -31
  95. package/scripts/check-manifest-snapshot.js +13 -73
  96. package/scripts/check-sbom-currency.js +44 -142
  97. package/scripts/check-test-count.js +15 -52
  98. package/scripts/check-test-coverage.js +66 -197
  99. package/scripts/check-test-subjects.js +21 -62
  100. package/scripts/check-ttp-references.js +14 -38
  101. package/scripts/check-ttp-upstream.js +8 -40
  102. package/scripts/check-version-bump.js +9 -61
  103. package/scripts/check-version-tags.js +20 -121
  104. package/scripts/predeploy.js +38 -184
  105. package/scripts/refresh-manifest-snapshot.js +16 -38
  106. package/scripts/refresh-mitre-atlas.js +3 -8
  107. package/scripts/refresh-mitre-attack.js +1 -8
  108. package/scripts/refresh-mitre-d3fend.js +3 -9
  109. package/scripts/refresh-mitre-ics-attack.js +3 -8
  110. package/scripts/refresh-reverse-refs.js +27 -94
  111. package/scripts/refresh-rfc-index.js +2 -10
  112. package/scripts/refresh-sbom.js +31 -161
  113. package/scripts/refresh-upstream-catalogs.js +40 -137
  114. package/scripts/release.js +69 -232
  115. package/scripts/run-e2e-scenarios.js +24 -71
  116. package/scripts/sync-manifest-metadata.js +10 -34
  117. package/scripts/sync-package-description.js +8 -17
  118. package/scripts/validate-vendor-online.js +13 -44
  119. package/scripts/verify-shipped-tarball.js +35 -140
@@ -1,31 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * lib/collectors/library-author.js
5
- *
6
- * Companion collector for the `library-author` playbook. Audits a
7
- * publisher-side repository for supply-chain posture markers:
8
- * SECURITY.md / security.txt presence, sbom + signature pair,
9
- * publish-workflow shape (id-token write, action-ref pinning, frozen
10
- * install), package.json provenance opt-in, vendor-tree provenance,
11
- * lockfile integrity.
12
- *
13
- * Skipped indicators (require external API or runtime data, left
14
- * unflipped so the runner returns inconclusive rather than a forced
15
- * miss):
16
- *
17
- * tag-protection-absent GitHub branch-protection API
18
- * private-vuln-reporting-disabled GitHub repo-settings API
19
- * no-rekor-entry-for-latest-release sigstore lookup
20
- * release-tag-not-signed git tag --verify on each release
21
- * release-signed-with-personal-gpg-key GPG identity policy
22
- * sbom-regenerated-at-request-time timing-based; needs build
23
- * pipeline trace
24
- * skill-signing-but-verification-not-gated project-internal CI
25
- * gate inspection
26
- * ssdf-claimed-cra-not-ready operator interview shape
27
- * gha-workflow-script-injection-sink complex pattern; future pass
28
- *
4
+ * Companion collector for the `library-author` playbook: audits a publisher-side
5
+ * repository for supply-chain posture markers. Indicators needing an external API
6
+ * or a build-pipeline trace are left unflipped, so the runner returns
7
+ * inconclusive rather than a forced miss.
29
8
  * Interface: see lib/collectors/README.md
30
9
  */
31
10
 
@@ -36,10 +15,8 @@ const { codeExcludeSet, isLinkedWorktreeDir, buildEvidenceLocations } = require(
36
15
  const COLLECTOR_ID = "library-author";
37
16
 
38
17
  const DEFAULT_MAX_DEPTH = 6;
39
- // Shared code-scope exclusions (dependency caches, build output, VCS +
40
- // agent/editor scratch including `.claude/`). Applied to the vendor-tree
41
- // walk so a linked worktree or dependency cache nested under `vendor/`
42
- // isn't mistaken for vendored provenance state.
18
+ // Applied to the vendor-tree walk, so a cache nested under `vendor/` is not
19
+ // mistaken for vendored provenance state.
43
20
  const DEFAULT_EXCLUDES = codeExcludeSet();
44
21
 
45
22
  function readSafe(full, max = 512 * 1024) {
@@ -48,10 +25,8 @@ function readSafe(full, max = 512 * 1024) {
48
25
  fd = fs.openSync(full, "r");
49
26
  const s = fs.fstatSync(fd);
50
27
  if (s.size > max) return null;
51
- // readFileSync(fd) loops read() to EOF — a single readSync may return
52
- // fewer than s.size bytes on network/FUSE/sync-backed fds, which would
53
- // leave the buffer tail NUL-filled and silently drop trailing content.
54
- // Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
28
+ // readFileSync(fd), never a single readSync: a short read on a network fd
29
+ // NUL-fills the tail. Reading the open fd is also TOCTOU-free.
55
30
  return fs.readFileSync(fd, "utf8");
56
31
  } catch { return null; }
57
32
  finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
@@ -74,53 +49,34 @@ function walkWorkflows(root) {
74
49
  }
75
50
 
76
51
  function looksLikePublishWorkflow(name, content) {
77
- // Heuristic: filename starts with a publish-prefix, OR the body
78
- // invokes a publish command. Conservative — broad signals like
79
- // `id-token: write`, bare `sigstore/cosign-installer`, or `cosign
80
- // sign-blob` (commonly used in test workflows) DO NOT count; they
81
- // produced false positives on verification + e2e workflows that
82
- // happen to share signing infrastructure.
52
+ // Broad signals — `id-token: write`, a cosign installer — must not count:
53
+ // verification and e2e workflows share the same signing infrastructure.
83
54
  const lower = name.toLowerCase();
84
55
 
85
- // Verification / test / e2e workflows are never publish, regardless
86
- // of body content. Hard demotion.
87
56
  if (/^(test|verify|validate|e2e|kind|check|conformance|coverage)/.test(lower)) return false;
88
57
 
89
- // Filename prefix signals: only the canonical publish words.
90
58
  if (/^(release|publish|deploy|promote|tag-and-release)/.test(lower)) return true;
91
59
 
92
- // Strip YAML line-comments before the content-shape probes so a comment that
93
- // merely MENTIONS a publish verb (e.g. a ci.yml line "# … matches the npm
94
- // publish workflow's depth.") can't masquerade as a publish command. The
95
- // filename-prefix checks above are unaffected. A `#` inside a quoted string
96
- // is rare in workflow YAML and not load-bearing for these command probes
97
- // (stripping can only REMOVE comment text, never create a false match).
60
+ // Comment-stripped, so a `#` line mentioning a publish verb cannot masquerade
61
+ // as one. Stripping can only remove text, never create a match.
98
62
  const code = stripYamlComments(content);
99
63
 
100
- // Explicit publish-shape commands — these are commitments to push
101
- // artifacts, not setup / scaffolding.
102
64
  if (/\bnpm\s+publish\b/.test(code)) return true;
103
65
  if (/pypa\/gh-action-pypi-publish/.test(code)) return true;
104
66
  if (/\bcargo\s+publish\b/.test(code)) return true;
105
67
  if (/goreleaser/.test(code)) return true;
106
68
  if (/softprops\/action-gh-release/.test(code)) return true;
107
- // `ko build` builds AND pushes container images by default (unless
108
- // --push=false). All four sub-commands push to a registry.
69
+ // All four `ko` sub-commands push to a registry by default, `build` included.
109
70
  if (/\bko\s+(?:publish|build|apply|resolve)\b/.test(code)) return true;
110
- // `cosign sign` (container signing — publish-shape). Exclude
111
- // `cosign sign-blob` which signs arbitrary artifacts and shows
112
- // up in verification / e2e tests as often as in publishes.
71
+ // The `(?!-)` keeps out `cosign sign-blob`, as common in verification workflows.
113
72
  if (/\bcosign\s+sign\b(?!-)/.test(code)) return true;
114
73
  if (/\bcrane\s+(?:push|copy|append)\b/.test(code)) return true;
115
74
  if (/\boras\s+(?:push|copy)\b/.test(code)) return true;
116
75
  if (/docker\/build-push-action/.test(code)) return true;
117
76
  if (/\bdocker\s+push\b/.test(code)) return true;
118
77
 
119
- // Registry login actions — when a workflow logs into a registry,
120
- // it's almost certainly publishing. Picks up Makefile / opaque
121
- // publish paths (cosign's build.yaml uses make sign-ci-containers
122
- // after docker/login-action). Does NOT match verification tests
123
- // because those don't authenticate to a registry.
78
+ // Registry login stands in for opaque publish paths (a Makefile target run
79
+ // after docker/login-action); verification workflows never authenticate.
124
80
  if (/docker\/login-action/.test(code)) return true;
125
81
  if (/google-github-actions\/auth/.test(code) && /gcloud auth configure-docker/.test(code)) return true;
126
82
  if (/aws-actions\/configure-aws-credentials/.test(code) && /amazon-ecr/.test(code)) return true;
@@ -128,33 +84,20 @@ function looksLikePublishWorkflow(name, content) {
128
84
  return false;
129
85
  }
130
86
 
131
- // `id-token: write` grants the OIDC token used for npm provenance and
132
- // sigstore keyless signing. It is valid at workflow-level permissions or at
133
- // a specific job's `permissions:` block, so any occurrence of the
134
- // `id-token: write` token within the publish workflow file counts as the
135
- // capability being present — this is what lets a job-scoped declaration
136
- // (not just a workflow-level one) satisfy the check. The match is
137
- // deliberately scoped to the file being scanned, not repo-wide: a sibling
138
- // workflow's OIDC does not grant it to the publish job.
87
+ // `id-token: write` is valid at workflow OR job scope, so any occurrence counts.
88
+ // Scoped to the one file passed in: a sibling workflow's OIDC does not grant it.
139
89
  function hasIdTokenWriteAnyScope(content) {
140
90
  return /\bid-token:\s*write\b/.test(content);
141
91
  }
142
92
 
143
- // Strip YAML line-comments so a `#`-commented MENTION of a publish-shape
144
- // token / command / runner is not read as the real thing. The classifier
145
- // (looksLikePublishWorkflow) already does this; the indicator probes — and the
146
- // provenance / SBOM-capability probes in collect() — must use the same view,
147
- // or a comment produces a false (often deterministic) hit, and in the
148
- // provenance direction a commented `--provenance` would suppress a real gap
149
- // (a false negative on a security-relevant posture check).
93
+ // Every probe that reads workflow bodies must use this view: a commented publish
94
+ // token otherwise produces a false hit, and a commented `--provenance` hides a gap.
150
95
  function stripYamlComments(content) {
151
96
  return content.replace(/#.*$/gm, "");
152
97
  }
153
98
 
154
99
  function scanPublishWorkflow(content, rel) {
155
- // Whole-content probes below run against a comment-stripped view. The
156
- // `uses:` line scan stays on the raw lines — its anchored regex already
157
- // rejects `#`-prefixed lines, so a commented `uses:` cannot match.
100
+ // Whole-content probes read `code`; the `uses:` scan below stays on raw lines.
158
101
  const code = stripYamlComments(content);
159
102
  const hits = {
160
103
  "publish-workflow-uses-static-token": [],
@@ -164,18 +107,10 @@ function scanPublishWorkflow(content, rel) {
164
107
  "publish-workflow-runs-on-self-hosted": [],
165
108
  };
166
109
 
167
- // static-token: workflow references a publish-credential secret
168
- // without a corresponding `id-token: write` permission (at any scope).
169
- // The predicate (per data/playbooks/library-author.json) lists
170
- // NPM_TOKEN / PYPI_TOKEN / CARGO_TOKEN / RUBYGEMS_API_KEY /
171
- // GEM_HOST_API_KEY; expand to cover the common variants for each
172
- // ecosystem.
110
+ // Widens the predicate in data/playbooks/library-author.json to the common
111
+ // token-name variant per ecosystem.
173
112
  const usesStaticToken = /\bsecrets\.(NPM_TOKEN|PYPI_TOKEN|PYPI_API_TOKEN|CARGO_TOKEN|CARGO_REGISTRY_TOKEN|RUBYGEMS_API_KEY|GEM_HOST_API_KEY|MAVEN_TOKEN|MAVEN_CENTRAL_TOKEN|GH_TOKEN)\b/.test(code);
174
- // OIDC is available when THIS publish file declares `id-token: write` at
175
- // any scope (workflow or job). Scoped to the file by design — a sibling
176
- // workflow's OIDC does not authenticate this publish job. Read from the
177
- // comment-stripped view so a commented `id-token: write` cannot falsely
178
- // satisfy the capability (which would suppress the static-token finding).
113
+ // Comment-stripped: a commented `id-token: write` would suppress the finding.
179
114
  const hasIdTokenWrite = hasIdTokenWriteAnyScope(code);
180
115
  if (usesStaticToken && !hasIdTokenWrite) {
181
116
  hits["publish-workflow-uses-static-token"].push({ file: rel, line: 0, snippet: "publish workflow uses a static long-lived token (NPM_TOKEN / PYPI / Cargo / Maven) without id-token: write for OIDC" });
@@ -184,20 +119,15 @@ function scanPublishWorkflow(content, rel) {
184
119
  hits["publish-workflow-no-id-token-write"].push({ file: rel, line: 0, snippet: "no id-token: write at any scope (workflow or job) — npm provenance / sigstore signing unavailable" });
185
120
  }
186
121
 
187
- // action-refs-mutable: any `uses: <action>@<ref>` where ref is NOT
188
- // a 40-char hex sha. Excludes `uses: ./local-action`.
122
+ // Mutable = any `uses: <action>@<ref>` whose ref is not a 40-char hex sha.
189
123
  const lines = content.split(/\r?\n/);
190
124
  for (let i = 0; i < lines.length; i++) {
191
125
  const line = lines[i];
192
- // A real `uses:` line is never multiple KB. Skip overlong lines so a
193
- // crafted whitespace run can't drive regex backtracking.
126
+ // A real `uses:` line is never KBs long; skipping overlong ones keeps a
127
+ // crafted whitespace run from driving regex backtracking.
194
128
  if (line.length > 4096) continue;
195
- // Allow optional leading `-` from a YAML list item: `- uses: ...`.
196
- // `^[ \t]*(?:-[ \t]*)?` anchors the indentation once, then an optional
197
- // `- ` list marker — no overlapping `\s*` runs that backtrack. Exclude `#`
198
- // from the capture and drop the end anchor (mirroring the cicd collector) so
199
- // a `uses: x@v1 # comment` line is still matched — the `$`-anchored pattern
200
- // silently missed every floating ref that carried a trailing YAML comment.
129
+ // No end anchor and `#` excluded from the capture, so `uses: x@v1 # comment`
130
+ // still matches. The indent and `- ` marker are each anchored once.
201
131
  const m = line.match(/^[ \t]*(?:-[ \t]*)?uses:\s*['"]?([^'"\s#]+)['"]?/);
202
132
  if (!m) continue;
203
133
  const ref = m[1];
@@ -210,9 +140,6 @@ function scanPublishWorkflow(content, rel) {
210
140
  }
211
141
  }
212
142
 
213
- // non-frozen-install: workflow uses `npm install` instead of `npm ci`,
214
- // or `pip install <pkg>` without `--require-hashes`, or `cargo
215
- // install` without `--locked`.
216
143
  if (/\bnpm\s+install\b/.test(code) && !/\bnpm\s+ci\b/.test(code)) {
217
144
  hits["release-workflow-non-frozen-install"].push({ file: rel, line: 0, snippet: "publish workflow uses `npm install` rather than `npm ci` — lockfile is not enforced" });
218
145
  }
@@ -220,7 +147,6 @@ function scanPublishWorkflow(content, rel) {
220
147
  hits["release-workflow-non-frozen-install"].push({ file: rel, line: 0, snippet: "cargo build/install without --locked / --frozen" });
221
148
  }
222
149
 
223
- // runs-on-self-hosted: any `runs-on: self-hosted` line.
224
150
  if (/runs-on:\s*['"]?(?:self-hosted|\[?\s*self-hosted)/i.test(code)) {
225
151
  hits["publish-workflow-runs-on-self-hosted"].push({ file: rel, line: 0, snippet: "publish workflow runs on a self-hosted runner — non-ephemeral execution context" });
226
152
  }
@@ -233,7 +159,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
233
159
  const startTime = Date.now();
234
160
  const root = path.resolve(cwd);
235
161
 
236
- // package-manifest detection.
237
162
  const manifests = [];
238
163
  const manifestFiles = [
239
164
  "package.json", "pyproject.toml", "Cargo.toml", "go.mod",
@@ -246,7 +171,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
246
171
  manifests.push({ file: f, path: p, content: readSafe(p) });
247
172
  }
248
173
  }
249
- // *.gemspec — glob at root only.
250
174
  try {
251
175
  for (const e of fs.readdirSync(root)) {
252
176
  if (e.endsWith(".gemspec")) {
@@ -256,11 +180,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
256
180
  }
257
181
  } catch {}
258
182
 
259
- // Release / publish workflows.
260
183
  const workflows = walkWorkflows(root).map(w => ({ ...w, content: readSafe(w.full) || "" }));
261
184
  const publishWorkflows = workflows.filter(w => looksLikePublishWorkflow(w.name, w.content));
262
185
 
263
- // Aggregate publish-workflow indicator hits.
264
186
  const workflowHits = {
265
187
  "publish-workflow-uses-static-token": [],
266
188
  "publish-workflow-no-id-token-write": [],
@@ -268,24 +190,15 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
268
190
  "release-workflow-non-frozen-install": [],
269
191
  "publish-workflow-runs-on-self-hosted": [],
270
192
  };
271
- // OIDC capability is evaluated per publish workflow, NOT repo-wide: a
272
- // sibling docs/deploy workflow declaring `id-token: write` does not give
273
- // OIDC to a release job that still publishes with a long-lived
274
- // `secrets.NPM_TOKEN`. Treating it repo-wide would mask exactly the
275
- // static-token publisher-takeover case this indicator exists to catch.
276
- // Job-scoped `id-token: write` inside the publish file still counts —
277
- // hasIdTokenWriteAnyScope matches any scope within the scanned file.
193
+ // OIDC capability is evaluated per publish workflow, never repo-wide: a sibling
194
+ // workflow's `id-token: write` gives nothing to a job using a static token.
278
195
  for (const w of publishWorkflows) {
279
196
  const h = scanPublishWorkflow(w.content, w.rel);
280
197
  for (const [id, list] of Object.entries(h)) workflowHits[id].push(...list);
281
198
  }
282
199
 
283
- // package.json provenance: per playbook, the indicator fires
284
- // when EITHER the manifest opts in (`publishConfig.provenance:
285
- // true`) is missing AND the publish workflow does not invoke
286
- // `npm publish --provenance`. Repos that flip provenance on at
287
- // the workflow level (the modern path) should not be flagged.
288
- // We need the workflow inventory above this point.
200
+ // Fires only when neither `publishConfig.provenance: true` nor a workflow
201
+ // `npm publish --provenance` opts in. Needs the workflow inventory above.
289
202
  let provenanceMissing = "miss";
290
203
  const pkgManifest = manifests.find(m => m.file === "package.json");
291
204
  if (pkgManifest && pkgManifest.content) {
@@ -299,16 +212,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
299
212
  }
300
213
  }
301
214
 
302
- // lockfile-missing-integrity (per playbook): "any pinned entry
303
- // in walked lockfiles lacks an integrity field (sha512/sha384/
304
- // sha256, sri-integrity, go.sum h1: hash)". The predicate
305
- // covers every ecosystem's lockfile shape, not just npm. Walk
306
- // each supported lockfile and flip the indicator if any one of
307
- // them has a missing-integrity entry. When NO lockfile is
308
- // present, leave the indicator unflipped (undefined) so the
309
- // runner returns inconclusive — a no-lockfile repo is not
310
- // evidence of "no missing integrity"; it's evidence we can't
311
- // tell, and the playbook covers that case as inconclusive.
215
+ // Any one lockfile with a missing-integrity entry flips the indicator. With no
216
+ // lockfile it stays undefined: a repo without one is not evidence of integrity.
312
217
  let lockfileMissingIntegrity = undefined;
313
218
  const lockfilesChecked = [];
314
219
  const lockfileScans = [
@@ -317,15 +222,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
317
222
  scan: (text) => {
318
223
  const j = JSON.parse(text);
319
224
  let missing = 0;
320
- // Only a REMOTE-tarball entry (`resolved` is an http(s)/git URL) is
321
- // expected to carry an integrity hash. Workspace links — npm v3 emits
322
- // them as `{ "resolved": "<relative-path>", "link": true }` — and
323
- // file:/link:/workspace:/git: refs legitimately have NO integrity (a
324
- // local symlink is not a registry artifact). Counting them flips
325
- // lockfile-missing-integrity to a false hit on every npm-workspaces
326
- // monorepo even when every registry dep is sha512-pinned. Mirrors
327
- // sbom.js's local-ref demotion; the bare-relative-path + `link:true`
328
- // shape is the canonical workspace link.
225
+ // Only a remote-tarball entry (`resolved` is an http(s)/git URL) owes an
226
+ // integrity hash; a workspace link and file:/link:/workspace: refs have
227
+ // none by nature, and counting them false-hits every workspaces monorepo.
329
228
  const walkObj = (obj) => {
330
229
  if (!obj || typeof obj !== "object") return;
331
230
  if (
@@ -343,8 +242,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
343
242
  {
344
243
  file: "yarn.lock",
345
244
  scan: (text) => {
346
- // yarn.lock entries: a `resolved "https://..."` line should be
347
- // followed within ~5 lines by an `integrity sha512-...` line.
245
+ // A `resolved "https://..."` line owes an `integrity sha512-...` nearby.
348
246
  const lines = text.split(/\r?\n/);
349
247
  let missing = 0;
350
248
  for (let i = 0; i < lines.length; i++) {
@@ -358,9 +256,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
358
256
  {
359
257
  file: "pnpm-lock.yaml",
360
258
  scan: (text) => {
361
- // pnpm entries have an `integrity:` field. Count entries
362
- // with `resolution: { tarball: ... }` lines whose nearby
363
- // block lacks `integrity:`.
259
+ // A pnpm block carrying `resolution:` owes an `integrity:` field.
364
260
  const blocks = text.split(/\n(?=\s+\/)/);
365
261
  let missing = 0;
366
262
  for (const b of blocks) {
@@ -373,14 +269,10 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
373
269
  {
374
270
  file: "Cargo.lock",
375
271
  scan: (text) => {
376
- // Cargo.lock entries have `checksum = "..."` per [[package]]
377
- // (registry deps only — path / git deps legitimately have
378
- // none, so count only registry packages).
272
+ // `checksum` is owed per [[package]] by registry deps only.
379
273
  const packages = text.split(/^\[\[package\]\]/m).slice(1);
380
274
  let missing = 0;
381
275
  for (const p of packages) {
382
- // Skip path / git deps — they have a `source = "<...>"`
383
- // line; registry deps have `source = "registry+..."`.
384
276
  const sourceMatch = p.match(/^source\s*=\s*"([^"]+)"/m);
385
277
  if (!sourceMatch) continue; // path dep, no integrity expected
386
278
  if (!sourceMatch[1].startsWith("registry+")) continue;
@@ -392,8 +284,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
392
284
  {
393
285
  file: "go.sum",
394
286
  scan: (text) => {
395
- // go.sum entries are `<module> <version> h1:<hash>=`.
396
- // Missing-integrity = a line without `h1:` (rare; malformed).
287
+ // A go.sum line is `<module> <version> h1:<hash>=`; one without `h1:` is malformed.
397
288
  const lines = text.split(/\r?\n/).filter(Boolean);
398
289
  let missing = 0;
399
290
  for (const l of lines) {
@@ -414,8 +305,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
414
305
  lockfileMissingIntegrity = "hit";
415
306
  break;
416
307
  }
417
- // If at least one lockfile scanned cleanly, the indicator
418
- // moves to miss — we have evidence of integrity coverage.
419
308
  if (lockfileMissingIntegrity === undefined) lockfileMissingIntegrity = "miss";
420
309
  } catch (e) {
421
310
  errors.push({
@@ -425,55 +314,37 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
425
314
  });
426
315
  }
427
316
  }
428
- // If no supported lockfile was present, leave the indicator
429
- // unflipped (undefined). The runner returns inconclusive.
430
-
431
- // SECURITY.md presence (root + .github/SECURITY.md).
432
317
  const securityMdPresent =
433
318
  fs.existsSync(path.join(root, "SECURITY.md")) ||
434
319
  fs.existsSync(path.join(root, ".github", "SECURITY.md"));
435
320
 
436
- // security.txt presence (.well-known/security.txt + security.txt).
437
321
  const securityTxtPresent =
438
322
  fs.existsSync(path.join(root, ".well-known", "security.txt")) ||
439
323
  fs.existsSync(path.join(root, "security.txt"));
440
324
 
441
- // sbom-absent-or-unsigned: no sbom file at root, OR sbom file
442
- // present but no matching .sig sidecar.
325
+ // Fires when there is no root SBOM, or one with no matching .sig sidecar.
443
326
  let sbomFile = null;
444
327
  for (const f of ["sbom.cdx.json", "sbom.json", "bom.json", "sbom.spdx.json", "sbom.cdx.xml"]) {
445
328
  if (fs.existsSync(path.join(root, f))) { sbomFile = f; break; }
446
329
  }
447
- // Many publishers don't commit a static SBOM; they generate a
448
- // cosign-signed CycloneDX/SPDX SBOM (and provenance attestation) at
449
- // release time inside the publish workflow. Those assets never land in
450
- // the committed tree, so a repo-state-only check can't see them. Detect
451
- // the *capability* in the publish workflows: an SBOM-generation step
452
- // (cyclonedx / syft / anchore-sbom-action / trivy / `npm sbom`), npm
453
- // provenance (`--provenance` flag or `publishConfig.provenance: true`),
454
- // or a sigstore/cosign signing step. If any is present, the SBOM /
455
- // signed-attestation capability exists at release and the indicator
456
- // should not fire on the absence of a committed artifact.
330
+ // An SBOM generated and signed at release time never lands in the committed
331
+ // tree, so the capability is detected in the publish workflows instead.
457
332
  const releaseSbomCapable = publishWorkflows.some(w => {
458
333
  const c = stripYamlComments(w.content);
459
334
  return (
460
- // SBOM-generation tooling invoked in the workflow.
461
335
  /cyclonedx/i.test(c) ||
462
336
  /\bsyft\b/i.test(c) ||
463
337
  /anchore\/sbom-action/i.test(c) ||
464
338
  /\btrivy\b[^\n]*\bsbom\b/i.test(c) ||
465
339
  /\bnpm\s+sbom\b/.test(c) ||
466
340
  /spdx-sbom-generator/i.test(c) ||
467
- // npm provenance (signed build provenance attestation at publish).
468
341
  /npm\s+publish[^\n]*--provenance\b/.test(c) ||
469
- // sigstore / cosign signing of release artifacts.
470
342
  /\bcosign\s+(?:sign|attest)\b/.test(c) ||
471
343
  /sigstore\//i.test(c) ||
472
344
  /gh-action-sigstore-python/i.test(c)
473
345
  );
474
346
  });
475
- // package.json publishConfig.provenance opt-in also signals a signed
476
- // provenance attestation is produced at publish time.
347
+ // The manifest opt-in likewise produces a signed provenance attestation.
477
348
  const manifestProvenanceOptIn = (() => {
478
349
  if (!pkgManifest || !pkgManifest.content) return false;
479
350
  try { return JSON.parse(pkgManifest.content)?.publishConfig?.provenance === true; }
@@ -484,14 +355,10 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
484
355
  const sigPath = path.join(root, `${sbomFile}.sig`);
485
356
  sbomAbsentOrUnsigned = fs.existsSync(sigPath) ? "miss" : "hit";
486
357
  } else if (releaseSbomCapable || manifestProvenanceOptIn) {
487
- // No committed SBOM, but the release pipeline generates / signs one
488
- // (or emits a signed provenance attestation). Treat the capability
489
- // as present-at-release rather than reporting it absent.
490
358
  sbomAbsentOrUnsigned = "miss";
491
359
  }
492
360
 
493
- // vendored-no-provenance: vendor/ directory exists without a
494
- // _PROVENANCE.json at any level inside it.
361
+ // Fires when vendor/ exists with no _PROVENANCE.json at any level inside it.
495
362
  let vendoredNoProvenance = "miss";
496
363
  const vendorDir = path.join(root, "vendor");
497
364
  if (fs.existsSync(vendorDir)) {
@@ -505,10 +372,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
505
372
  if (e.isDirectory()) {
506
373
  if (DEFAULT_EXCLUDES.has(e.name)) continue;
507
374
  const sub = path.join(dir, e.name);
508
- // Don't descend into a linked git worktree nested under
509
- // vendor/ — its `.git` is a gitdir pointer file, and a repo
510
- // copy stamped by agent tooling carries unrelated provenance
511
- // state that shouldn't count toward this tree's vendoring.
375
+ // A linked worktree under vendor/ is a separate repo copy.
512
376
  if (isLinkedWorktreeDir(sub)) continue;
513
377
  walkVendor(sub, depth + 1);
514
378
  }
@@ -530,19 +394,14 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
530
394
  "no-security-txt": securityTxtPresent ? "miss" : "hit",
531
395
  "vendored-no-provenance": vendoredNoProvenance,
532
396
  };
533
- // Conditionally include lockfile-missing-integrity — leave it
534
- // out (so the runner returns inconclusive) when no supported
535
- // lockfile was present to scan.
397
+ // Omitted when no lockfile was there to scan, so the runner reads inconclusive.
536
398
  if (lockfileMissingIntegrity !== undefined) {
537
399
  signal_overrides["lockfile-missing-integrity"] = lockfileMissingIntegrity;
538
400
  }
539
401
 
540
- // __fp_checks attestation for publish-workflow-action-refs-mutable. Both
541
- // false_positive_checks_required entries are deterministic from the repo:
542
- // [0] Dependabot configured for github-actions on a weekly+ schedule
543
- // demotes the finding — attest survival when no such config exists.
544
- // [1] every mutable ref pointing to a github-owned action is lower risk —
545
- // attest survival when at least one mutable ref is third-party.
402
+ // The keys "0" and "1" are positional indexes into the indicator's
403
+ // false_positive_checks_required list: [0] Dependabot on github-actions demotes,
404
+ // [1] all-github-owned refs are lower risk. Reordering that list mis-attests.
546
405
  if (signal_overrides["publish-workflow-action-refs-mutable"] === "hit") {
547
406
  let dependabotActions = false;
548
407
  try {
@@ -567,12 +426,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
567
426
  if (Object.keys(att).length) signal_overrides["publish-workflow-action-refs-mutable__fp_checks"] = att;
568
427
  }
569
428
 
570
- // Per-indicator file locations for the publish-workflow indicators
571
- // flipped to "hit", so a SARIF result points at the workflow file (and,
572
- // for mutable action refs, the offending `uses:` line). The other
573
- // indicators (provenance / sbom / security.md / security.txt / vendor /
574
- // lockfile) reflect a whole-repo presence-or-absence state with no single
575
- // offending file, so they carry no file-level location here.
429
+ // Only the workflow indicators get locations, so a SARIF result can point at the
430
+ // offending `uses:` line; the rest are whole-repo presence or absence.
576
431
  const evidence_locations = {};
577
432
  for (const id of Object.keys(workflowHits)) {
578
433
  if (signal_overrides[id] === "hit") {
@@ -610,13 +465,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
610
465
  return {
611
466
  precondition_checks: {
612
467
  "publisher-context": manifests.length > 0,
613
- // Auto-attest the playbook's publishable-artifact-evidence gate from what
614
- // we collected (a package manifest or a publish workflow): the runner
615
- // can't probe the scanned --cwd, so otherwise a repo that clearly has a
616
- // manifest surfaces a spurious precondition_unverified warning. (The
617
- // repo-walk-access HALT gate is intentionally NOT attested here — it is
618
- // resolved host-side and attesting it from the collector would turn a
619
- // currently-passing run on a non-git dir into a fail-closed halt.)
468
+ // Attested here because the runner cannot probe the scanned --cwd. The
469
+ // repo-walk-access HALT gate must not be attested from the collector: it is
470
+ // resolved host-side, and attesting it turns a passing run into a halt.
620
471
  "publishable-artifact-evidence": manifests.length > 0 || publishWorkflows.length > 0,
621
472
  },
622
473
  artifacts,