@blamejs/exceptd-skills 0.19.33 → 0.19.35
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.
- package/CHANGELOG.md +28 -0
- package/bin/exceptd.js +895 -2828
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +170 -76
- package/lib/collectors/cicd-pipeline-compromise.js +113 -136
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +198 -211
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +130 -118
- package/lib/collectors/scan-excludes.js +33 -139
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -155
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +39 -113
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +88 -236
- package/lib/playbook-runner.js +759 -2107
- package/lib/prefetch.js +101 -376
- package/lib/refresh-external.js +199 -633
- package/lib/refresh-network.js +78 -311
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +85 -146
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +28 -27
- package/lib/upstream-check-cli.js +36 -29
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +52 -121
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +78 -286
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -413
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +242 -242
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +29 -28
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +21 -31
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +26 -57
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +63 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +62 -81
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +83 -198
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +7 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +7 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +7 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +63 -148
- package/scripts/release.js +69 -234
- package/scripts/run-e2e-scenarios.js +26 -73
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -141
package/lib/collectors/sbom.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* fingerprint of the cwd (npm / yarn / pnpm / pip / cargo / go / ruby /
|
|
8
|
-
* composer) so the runner can correlate against the SBOM-currency +
|
|
9
|
-
* supply-chain integrity indicators. Counts components per lockfile
|
|
10
|
-
* for a coarse SBOM-presence signal.
|
|
11
|
-
*
|
|
12
|
-
* Scope: any cwd with a recognizable lockfile. Multi-ecosystem repos
|
|
13
|
-
* report every detected lockfile.
|
|
4
|
+
* Companion collector for the `sbom` playbook. Fingerprints the lockfiles in the
|
|
5
|
+
* cwd (npm, yarn, pnpm, pip, cargo, go, ruby, composer) and counts components
|
|
6
|
+
* per lockfile as a coarse SBOM-presence signal.
|
|
14
7
|
*
|
|
15
8
|
* Interface: see lib/collectors/README.md
|
|
16
9
|
*/
|
|
@@ -21,8 +14,8 @@ const { buildEvidenceLocations } = require("./scan-excludes");
|
|
|
21
14
|
|
|
22
15
|
const COLLECTOR_ID = "sbom";
|
|
23
16
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
17
|
+
// Each parser takes the file content and returns
|
|
18
|
+
// { component_count, top_level_count, lockfile_version } or { error }.
|
|
26
19
|
const LOCKFILES = [
|
|
27
20
|
{
|
|
28
21
|
file: "package-lock.json",
|
|
@@ -123,30 +116,21 @@ const LOCKFILES = [
|
|
|
123
116
|
} catch (e) { return { error: e.message }; }
|
|
124
117
|
},
|
|
125
118
|
},
|
|
126
|
-
//
|
|
127
|
-
// canonical project file for modern Python projects). Counted as
|
|
128
|
-
// a Python ecosystem dependency source so projects with only a
|
|
129
|
-
// pyproject.toml + no requirements.txt are recognized.
|
|
119
|
+
// A manifest, not a lockfile: a pyproject-only project is still recognized.
|
|
130
120
|
{
|
|
131
121
|
file: "pyproject.toml",
|
|
132
122
|
ecosystem: "python",
|
|
133
123
|
parser: (content) => {
|
|
134
|
-
//
|
|
135
|
-
// dependencies.*] / [tool.poetry.dependencies] / [tool.poetry
|
|
136
|
-
// .dev-dependencies]. Coarse line-based count — the TOML parser
|
|
137
|
-
// would pull a dep into the stdlib-only contract.
|
|
124
|
+
// Line-based, not parsed: the collector contract is stdlib-only.
|
|
138
125
|
const depBlocks = content.match(/^\[(?:project\.(?:dependencies|optional-dependencies)|tool\.poetry\.(?:dependencies|dev-dependencies|group\.[a-z0-9_-]+\.dependencies))[^\]]*\][\s\S]*?(?=^\[|$)/gm) || [];
|
|
139
126
|
let count = 0;
|
|
140
127
|
for (const block of depBlocks) {
|
|
141
|
-
// count "name = ..." lines (excluding the block header)
|
|
142
128
|
const lines = block.split(/\r?\n/).slice(1);
|
|
143
129
|
for (const line of lines) {
|
|
144
130
|
if (/^\s*[A-Za-z][A-Za-z0-9._\-]*\s*=/.test(line)) count++;
|
|
145
131
|
}
|
|
146
132
|
}
|
|
147
|
-
//
|
|
148
|
-
// [project]
|
|
149
|
-
// dependencies = [ "a", "b", ... ]
|
|
133
|
+
// PEP 621 array style: `dependencies = [ "a", "b" ]` under [project].
|
|
150
134
|
const arrMatch = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
151
135
|
if (arrMatch) {
|
|
152
136
|
const entries = arrMatch[1].match(/"([^"]+)"|'([^']+)'/g) || [];
|
|
@@ -157,10 +141,7 @@ const LOCKFILES = [
|
|
|
157
141
|
},
|
|
158
142
|
];
|
|
159
143
|
|
|
160
|
-
//
|
|
161
|
-
// .txt, requirements-prod.txt, dev-requirements.txt. The
|
|
162
|
-
// requirements.txt entry above covers the canonical name; this
|
|
163
|
-
// glob extends coverage to the common variants.
|
|
144
|
+
// requirements*.txt variants; the canonical name is a LOCKFILES entry above.
|
|
164
145
|
const REQUIREMENTS_GLOB_RE = /^(?:[a-z0-9_-]+-)?requirements(?:-[a-z0-9_-]+)?\.txt$/i;
|
|
165
146
|
const REQUIREMENTS_LF = {
|
|
166
147
|
ecosystem: "pip",
|
|
@@ -170,11 +151,7 @@ const REQUIREMENTS_LF = {
|
|
|
170
151
|
},
|
|
171
152
|
};
|
|
172
153
|
|
|
173
|
-
//
|
|
174
|
-
// the walk bounded. Covers the common monorepo / docs-subdir / iac
|
|
175
|
-
// layouts: docs/ (requirements.txt for sphinx-style docs builds),
|
|
176
|
-
// packages/* (monorepo workspaces), backend/ + frontend/ +
|
|
177
|
-
// infra/ + iac/ (split-stack repos).
|
|
154
|
+
// Probed one level deep and hand-listed so the walk stays bounded.
|
|
178
155
|
const SUBDIR_PROBE_PATHS = ["docs", "packages", "backend", "frontend", "infra", "iac", "src", "app"];
|
|
179
156
|
|
|
180
157
|
const SBOM_FORMATS = [
|
|
@@ -195,24 +172,31 @@ function captureLockfile(p, ecosystem, parser, label) {
|
|
|
195
172
|
path: p,
|
|
196
173
|
size_bytes: Buffer.byteLength(content, "utf8"),
|
|
197
174
|
...stats,
|
|
175
|
+
// A parser that could not decode the structure returns `error`; the read
|
|
176
|
+
// itself succeeded, so the two failures carry different kinds downstream.
|
|
177
|
+
...(stats && stats.error ? { error_kind: "lockfile_parse_failed" } : {}),
|
|
198
178
|
};
|
|
199
179
|
} catch (e) {
|
|
200
|
-
return { file: label, ecosystem, path: p, error: e.message };
|
|
180
|
+
return { file: label, ecosystem, path: p, error: e.message, error_kind: "lockfile_read_failed" };
|
|
201
181
|
}
|
|
202
182
|
}
|
|
203
183
|
|
|
204
|
-
|
|
184
|
+
// `errors` is the collector_errors channel: a directory the walk could not read
|
|
185
|
+
// is a scan gap the operator has to see, not a silent zero. Required, not
|
|
186
|
+
// defaulted: a default array would restore the discard-into-the-void behaviour
|
|
187
|
+
// for any caller that forgets it, and neither this function nor
|
|
188
|
+
// findSbomDocuments is exported, so no test could reach that caller.
|
|
189
|
+
function findLockfiles(cwd, errors) {
|
|
190
|
+
if (!Array.isArray(errors)) {
|
|
191
|
+
throw new TypeError("findLockfiles: `errors` must be the collector_errors array — scan gaps have nowhere else to go");
|
|
192
|
+
}
|
|
205
193
|
const found = [];
|
|
206
|
-
// Canonical names at cwd root.
|
|
207
194
|
for (const lf of LOCKFILES) {
|
|
208
195
|
const p = path.join(cwd, lf.file);
|
|
209
196
|
if (fs.existsSync(p)) {
|
|
210
197
|
found.push(captureLockfile(p, lf.ecosystem, lf.parser, lf.file));
|
|
211
198
|
}
|
|
212
199
|
}
|
|
213
|
-
// requirements*.txt glob at cwd root — covers requirements-dev.txt,
|
|
214
|
-
// dev-requirements.txt, etc. The exact-name `requirements.txt`
|
|
215
|
-
// already lands via LOCKFILES; skip it here.
|
|
216
200
|
try {
|
|
217
201
|
for (const entry of fs.readdirSync(cwd)) {
|
|
218
202
|
if (entry === "requirements.txt") continue; // captured above
|
|
@@ -223,25 +207,35 @@ function findLockfiles(cwd) {
|
|
|
223
207
|
}
|
|
224
208
|
}
|
|
225
209
|
}
|
|
226
|
-
} catch {
|
|
210
|
+
} catch (e) {
|
|
211
|
+
errors.push({
|
|
212
|
+
artifact_id: "lockfile-inventory",
|
|
213
|
+
kind: "readdir_failed",
|
|
214
|
+
reason: `cwd root: ${e.message} — requirements*.txt variants were not enumerated`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
227
217
|
|
|
228
|
-
// One-level subdirectory probe for canonical-name lockfiles. The
|
|
229
|
-
// common pattern: docs/requirements.txt (sphinx builds),
|
|
230
|
-
// packages/*/package.json (monorepo workspaces), backend/Gemfile
|
|
231
|
-
// .lock (split-stack repos). Capped depth (1 level only) and
|
|
232
|
-
// pre-listed subdirs to keep the walk bounded.
|
|
233
218
|
for (const sub of SUBDIR_PROBE_PATHS) {
|
|
234
219
|
const subDir = path.join(cwd, sub);
|
|
235
220
|
let entries;
|
|
236
221
|
try {
|
|
237
222
|
if (!fs.statSync(subDir).isDirectory()) continue;
|
|
238
223
|
entries = fs.readdirSync(subDir, { withFileTypes: true });
|
|
239
|
-
} catch {
|
|
224
|
+
} catch (e) {
|
|
225
|
+
// An absent probe dir is the normal case, not a gap. A permission or I/O
|
|
226
|
+
// failure on one that IS there means the subtree went unscanned.
|
|
227
|
+
if (e.code !== "ENOENT") {
|
|
228
|
+
errors.push({
|
|
229
|
+
artifact_id: "lockfile-inventory",
|
|
230
|
+
kind: "readdir_failed",
|
|
231
|
+
reason: `${sub}/: ${e.message} — subtree not scanned for lockfiles`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
240
236
|
for (const e of entries) {
|
|
241
237
|
if (e.isDirectory()) {
|
|
242
|
-
//
|
|
243
|
-
// names. (Doesn't recurse further; monorepo workspaces are
|
|
244
|
-
// the only common case.)
|
|
238
|
+
// packages/* gets one more level for monorepo workspaces, no deeper.
|
|
245
239
|
if (sub === "packages") {
|
|
246
240
|
for (const lf of LOCKFILES) {
|
|
247
241
|
const p = path.join(subDir, e.name, lf.file);
|
|
@@ -254,10 +248,7 @@ function findLockfiles(cwd) {
|
|
|
254
248
|
continue;
|
|
255
249
|
}
|
|
256
250
|
if (!e.isFile()) continue;
|
|
257
|
-
//
|
|
258
|
-
// file is captured there. The requirements glob below catches
|
|
259
|
-
// ONLY non-canonical names (e.g. requirements-dev.txt) so
|
|
260
|
-
// exact `requirements.txt` doesn't double-fire.
|
|
251
|
+
// A canonical name wins, so `requirements.txt` cannot double-fire below.
|
|
261
252
|
let captured = false;
|
|
262
253
|
for (const lf of LOCKFILES) {
|
|
263
254
|
if (e.name === lf.file) {
|
|
@@ -279,37 +270,53 @@ function findLockfiles(cwd) {
|
|
|
279
270
|
return found;
|
|
280
271
|
}
|
|
281
272
|
|
|
282
|
-
|
|
273
|
+
// `errors` is required for the same reason it is on findLockfiles above.
|
|
274
|
+
function findSbomDocuments(cwd, errors) {
|
|
275
|
+
if (!Array.isArray(errors)) {
|
|
276
|
+
throw new TypeError("findSbomDocuments: `errors` must be the collector_errors array — scan gaps have nowhere else to go");
|
|
277
|
+
}
|
|
283
278
|
const found = [];
|
|
284
279
|
for (const s of SBOM_FORMATS) {
|
|
285
280
|
const p = path.join(cwd, s.file);
|
|
286
281
|
let fd;
|
|
287
|
-
// Open once and fstat the descriptor
|
|
288
|
-
//
|
|
289
|
-
// but there is no TOCTOU window between the check, the size stat, and the read.
|
|
282
|
+
// Open once and fstat the descriptor rather than existsSync→statSync→read:
|
|
283
|
+
// ENOENT still skips, and there is no TOCTOU window.
|
|
290
284
|
try { fd = fs.openSync(p, "r"); }
|
|
291
|
-
catch (e) { if (e.code === "ENOENT") continue; found.push({ file: s.file, format: s.format, error: e.message }); continue; }
|
|
285
|
+
catch (e) { if (e.code === "ENOENT") continue; found.push({ file: s.file, format: s.format, error: e.message, error_kind: "open_failed" }); continue; }
|
|
292
286
|
try {
|
|
293
287
|
const stat = fs.fstatSync(fd);
|
|
294
288
|
let content;
|
|
295
|
-
// Read from the
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
289
|
+
// Read from the open descriptor, never by path again. readFileSync(fd) loops
|
|
290
|
+
// to EOF; a single readSync can return short on a network or FUSE mount,
|
|
291
|
+
// NUL-padding the buffer and truncating otherwise-valid JSON.
|
|
292
|
+
try { content = fs.readFileSync(fd, "utf8"); } catch (e) {
|
|
293
|
+
content = null;
|
|
294
|
+
errors.push({
|
|
295
|
+
artifact_id: "sbom-document",
|
|
296
|
+
kind: "read_failed",
|
|
297
|
+
reason: `${s.file}: ${e.message} — component count not derived`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
303
300
|
let component_count = null;
|
|
304
301
|
if (content && s.format === "cyclonedx-1.x") {
|
|
305
302
|
try {
|
|
306
303
|
const j = JSON.parse(content);
|
|
307
304
|
component_count = (j.components || []).length;
|
|
308
|
-
} catch {
|
|
305
|
+
} catch (e) {
|
|
306
|
+
// A present-but-unparseable SBOM reported `null components` with no
|
|
307
|
+
// explanation, which reads as "no components" rather than "not read".
|
|
308
|
+
errors.push({
|
|
309
|
+
artifact_id: "sbom-document",
|
|
310
|
+
kind: "parse_failed",
|
|
311
|
+
reason: `${s.file}: ${e.message} — component count unavailable`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
309
314
|
}
|
|
310
315
|
found.push({ file: s.file, format: s.format, size_bytes: stat.size, component_count });
|
|
311
316
|
} catch (e) {
|
|
312
|
-
|
|
317
|
+
// The descriptor opened; fstat is what failed here. Reporting it as an
|
|
318
|
+
// open failure sends the operator after the wrong thing.
|
|
319
|
+
found.push({ file: s.file, format: s.format, error: e.message, error_kind: "stat_failed" });
|
|
313
320
|
} finally {
|
|
314
321
|
if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } }
|
|
315
322
|
}
|
|
@@ -322,13 +329,32 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
322
329
|
const startTime = Date.now();
|
|
323
330
|
const root = path.resolve(cwd);
|
|
324
331
|
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const lockfiles = findLockfiles(root);
|
|
329
|
-
const sbomDocuments = findSbomDocuments(root);
|
|
332
|
+
// sbom-tool-available: a lockfile or an SBOM document is a sufficient proxy.
|
|
333
|
+
const lockfiles = findLockfiles(root, errors);
|
|
334
|
+
const sbomDocuments = findSbomDocuments(root, errors);
|
|
330
335
|
const hasAnything = lockfiles.length > 0 || sbomDocuments.length > 0;
|
|
331
336
|
|
|
337
|
+
// A lockfile that was found but not read contributes no component count, so
|
|
338
|
+
// the inventory under-reports; the operator sees which one and why.
|
|
339
|
+
for (const l of lockfiles) {
|
|
340
|
+
if (!l.error) continue;
|
|
341
|
+
errors.push({
|
|
342
|
+
artifact_id: "lockfile-inventory",
|
|
343
|
+
kind: l.error_kind || "lockfile_read_failed",
|
|
344
|
+
reason: `${l.file}: ${l.error}`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
for (const s of sbomDocuments) {
|
|
348
|
+
if (!s.error) continue;
|
|
349
|
+
errors.push({
|
|
350
|
+
artifact_id: "sbom-document",
|
|
351
|
+
// Carried from the push site, mirroring the lockfile loop above: an
|
|
352
|
+
// fstat failure and an open failure are different operator actions.
|
|
353
|
+
kind: s.error_kind || "open_failed",
|
|
354
|
+
reason: `${s.file}: ${s.error}`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
332
358
|
const artifacts = {
|
|
333
359
|
"lockfile-inventory": {
|
|
334
360
|
value: lockfiles.length
|
|
@@ -344,22 +370,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
344
370
|
},
|
|
345
371
|
};
|
|
346
372
|
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
// catalog cross-referencing the collector does not have — that's
|
|
351
|
-
// the runner's job. The collector's role here is to surface the
|
|
352
|
-
// artifacts (lockfile-inventory + sbom-document) and let the
|
|
353
|
-
// runner evaluate the indicators against them. Emitting
|
|
354
|
-
// signal_overrides for keys that don't exist in the playbook
|
|
355
|
-
// would be silently ignored; surfacing the artifacts honestly
|
|
356
|
-
// is the contract.
|
|
357
|
-
//
|
|
358
|
-
// One indicator the collector CAN decide deterministically:
|
|
359
|
-
// lockfile-no-integrity — true when an npm package-lock.json
|
|
360
|
-
// exists but has zero `integrity` entries (lockfileVersion 1
|
|
361
|
-
// legacy) OR when the dependency list contains entries lacking
|
|
362
|
-
// `integrity` strings.
|
|
373
|
+
// lockfile-no-integrity is the one indicator decidable here: an npm
|
|
374
|
+
// package-lock.json with entries carrying no `integrity`. The rest need catalog
|
|
375
|
+
// cross-referencing, which the runner does against the artifacts above.
|
|
363
376
|
const npmLockfile = lockfiles.find(l => l.file === "package-lock.json");
|
|
364
377
|
const signal_overrides = {};
|
|
365
378
|
if (npmLockfile && !npmLockfile.error) {
|
|
@@ -367,19 +380,15 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
367
380
|
const j = JSON.parse(fs.readFileSync(npmLockfile.path, "utf8"));
|
|
368
381
|
let withIntegrity = 0;
|
|
369
382
|
let withoutIntegrity = 0;
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// have no registry integrity hash. A remote-registry tarball without
|
|
373
|
-
// integrity is the genuine finding.
|
|
383
|
+
// A local-path, workspace or git ref legitimately has no integrity hash, so
|
|
384
|
+
// FP check [0] demotes those; a remote-registry tarball is the finding.
|
|
374
385
|
let withoutIntegrityLocalOnly = true;
|
|
375
386
|
const LOCAL_REF_RE = /^(?:file:|link:|workspace:|git\+ssh:|git\+https:|git:|github:|portal:)/i;
|
|
376
387
|
const walk = (obj) => {
|
|
377
388
|
if (!obj || typeof obj !== "object") return;
|
|
378
|
-
// Only
|
|
379
|
-
//
|
|
380
|
-
// `
|
|
381
|
-
// `integrity`, so keying off `version` would false-positive on
|
|
382
|
-
// every clean lockfile. Mirror library-author.js's guard.
|
|
389
|
+
// Only entries with a `resolved` URL are expected to carry `integrity`.
|
|
390
|
+
// The npm 7+ root entry `"": { name, version }` has neither, so keying off
|
|
391
|
+
// `version` false-positives. library-author.js guards the same way.
|
|
383
392
|
if (obj.resolved != null) {
|
|
384
393
|
if (obj.integrity != null) {
|
|
385
394
|
withIntegrity++;
|
|
@@ -391,15 +400,12 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
391
400
|
for (const v of Object.values(obj)) if (v && typeof v === "object") walk(v);
|
|
392
401
|
};
|
|
393
402
|
walk(j.packages || j.dependencies || {});
|
|
394
|
-
//
|
|
395
|
-
// resolves to a remote tarball — the indicator captures the
|
|
396
|
-
// class, not full coverage.
|
|
403
|
+
// Any single integrity-less resolved entry fires it: the class, not coverage.
|
|
397
404
|
if (withoutIntegrity > 0) {
|
|
398
405
|
signal_overrides["lockfile-no-integrity"] = "hit";
|
|
399
|
-
// __fp_checks attestation
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// build consumes, not a stale copy under archive/ pre-migration/.
|
|
406
|
+
// __fp_checks attestation — [0]: an integrity-less entry is a remote
|
|
407
|
+
// tarball, not only local, workspace or git refs. [1]: this is the root
|
|
408
|
+
// lockfile the build consumes, not a stale archived copy.
|
|
403
409
|
const att = {};
|
|
404
410
|
if (!withoutIntegrityLocalOnly) att["0"] = true;
|
|
405
411
|
const rel = (npmLockfile.path || "").replace(/\\/g, "/");
|
|
@@ -408,19 +414,19 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
408
414
|
} else if (withIntegrity > 0) {
|
|
409
415
|
signal_overrides["lockfile-no-integrity"] = "miss";
|
|
410
416
|
}
|
|
411
|
-
// Stash diagnostic counts on collector_meta further below.
|
|
412
417
|
npmLockfile.integrity_present_count = withIntegrity;
|
|
413
418
|
npmLockfile.integrity_missing_count = withoutIntegrity;
|
|
414
|
-
} catch {
|
|
415
|
-
//
|
|
416
|
-
|
|
419
|
+
} catch (e) {
|
|
420
|
+
// A malformed lockfile leaves the indicator unflipped: inconclusive, not miss.
|
|
421
|
+
errors.push({
|
|
422
|
+
artifact_id: "lockfile-inventory",
|
|
423
|
+
kind: "lockfile_parse_failed",
|
|
424
|
+
reason: `${npmLockfile.file}: ${e.message} — lockfile-no-integrity left undecided (inconclusive, not miss)`,
|
|
425
|
+
});
|
|
417
426
|
}
|
|
418
427
|
}
|
|
419
428
|
|
|
420
|
-
//
|
|
421
|
-
// indicator: a lockfile-no-integrity hit points at the npm lockfile that
|
|
422
|
-
// carries integrity-less entries. File-level (the gap is spread across
|
|
423
|
-
// many entries, not one line).
|
|
429
|
+
// File-level: the gap is spread across entries rather than sitting on one line.
|
|
424
430
|
const evidence_locations = {};
|
|
425
431
|
if (signal_overrides["lockfile-no-integrity"] === "hit" && npmLockfile) {
|
|
426
432
|
const locs = buildEvidenceLocations([{ file: npmLockfile.file }]);
|
|
@@ -430,11 +436,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
430
436
|
return {
|
|
431
437
|
precondition_checks: {
|
|
432
438
|
"sbom-tool-available": hasAnything,
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
// the scanned --cwd (it sees the run process cwd, not the collected repo),
|
|
436
|
-
// so a lockfile we found here would otherwise surface a spurious
|
|
437
|
-
// precondition_unverified warning on a repo that clearly has one.
|
|
439
|
+
// Attested from what was collected: autoDetectPreconditions sees the run
|
|
440
|
+
// process cwd, not the scanned --cwd, so this would warn as unverified.
|
|
438
441
|
"any-package-manager-present": lockfiles.length > 0,
|
|
439
442
|
},
|
|
440
443
|
artifacts,
|
|
@@ -450,6 +453,15 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
450
453
|
lockfiles_found: lockfiles.length,
|
|
451
454
|
sbom_documents_found: sbomDocuments.length,
|
|
452
455
|
ecosystems_detected: [...new Set(lockfiles.map(l => l.ecosystem))],
|
|
456
|
+
// Magnitude behind the lockfile-no-integrity verdict: "hit" alone does not
|
|
457
|
+
// say whether one entry or nine hundred resolve without a hash. Present
|
|
458
|
+
// only when an npm lockfile was found AND parsed.
|
|
459
|
+
...(npmLockfile && npmLockfile.integrity_present_count != null
|
|
460
|
+
? {
|
|
461
|
+
npm_integrity_present_count: npmLockfile.integrity_present_count,
|
|
462
|
+
npm_integrity_missing_count: npmLockfile.integrity_missing_count,
|
|
463
|
+
}
|
|
464
|
+
: {}),
|
|
453
465
|
},
|
|
454
466
|
collector_errors: errors,
|
|
455
467
|
};
|