@cdxgen/cdxgen 13.2.0 → 13.3.0
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/README.md +28 -12
- package/bin/cdxgen.js +227 -119
- package/bin/evinse.js +5 -0
- package/bin/sign.js +88 -14
- package/bin/tracebom.js +64 -4
- package/bin/validate.js +29 -7
- package/bin/verify.js +79 -39
- package/data/remediations.json +24 -0
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +43 -4
- package/lib/cli/jsBom.js +49 -24
- package/lib/cli/jvmBom.js +46 -12
- package/lib/cli/managedBom.js +112 -41
- package/lib/cli/nativeBom.js +125 -14
- package/lib/core/env.js +5 -6
- package/lib/core/fs.js +174 -6
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/ecosystems/bunutils.js +269 -101
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +66 -0
- package/lib/ecosystems/cppEvidence.js +64 -31
- package/lib/ecosystems/ecosystems.js +19 -8
- package/lib/ecosystems/gradleutils.js +42 -7
- package/lib/ecosystems/jsEvidence.js +19 -20
- package/lib/ecosystems/kosi.js +10 -3
- package/lib/ecosystems/npmutils.js +110 -9
- package/lib/ecosystems/parsers-dotnet.js +51 -10
- package/lib/ecosystems/parsers-go.js +47 -9
- package/lib/ecosystems/parsers-js.js +171 -50
- package/lib/ecosystems/parsers-misc.js +747 -161
- package/lib/ecosystems/parsers-python.js +26 -14
- package/lib/ecosystems/parsers-rust.js +24 -18
- package/lib/ecosystems/rubyutils.js +1426 -166
- package/lib/ecosystems/rusi.js +32 -1
- package/lib/evinser/evinser.js +436 -128
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/bomSigner.js +652 -221
- package/lib/inventory/atomUtils.js +568 -31
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +550 -13
- package/lib/inventory/dynamic.js +39 -1
- package/lib/inventory/evidenceUtils.js +14 -6
- package/lib/inventory/fetchBatch.js +116 -17
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/source.js +37 -19
- package/lib/inventory/traceRunner.js +210 -21
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +440 -20
- package/lib/managers/oci.js +39 -1
- package/lib/parsers/cmakeLists.js +452 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/stages/postgen/postgen.js +68 -15
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +48 -10
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +1 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +20 -20
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +20 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +13 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +42 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +10 -0
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +10 -4
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +143 -2
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +59 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +106 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +145 -3
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +64 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +47 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +14 -0
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +76 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
package/lib/inventory/cdxrs.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module is the ONLY place that spawns the `cdxrs` binary. It enforces
|
|
5
5
|
* the governing invariant: **no failure mode may abort SBOM generation.**
|
|
6
|
-
* Every failure (missing binary, non-zero exit, timeout,
|
|
7
|
-
* version-major mismatch, CDXGEN_RS_DISABLE) logs once at
|
|
8
|
-
* a sentinel that makes the caller take the JS path.
|
|
6
|
+
* Every failure (missing binary, non-zero exit, timeout, oversized stdout,
|
|
7
|
+
* malformed stdout, version-major mismatch, CDXGEN_RS_DISABLE) logs once at
|
|
8
|
+
* `warn` and returns a sentinel that makes the caller take the JS path.
|
|
9
9
|
*
|
|
10
10
|
* Protocol:
|
|
11
11
|
* cdxrs <subcommand> --input <file|-> --output <file|-> --format json
|
|
@@ -42,6 +42,45 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
42
42
|
*/
|
|
43
43
|
const MAX_STDIN_BYTES = 32 * 1024 * 1024;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* V8's maximum string length on 64-bit builds (2^29 − 24). Node exposes no
|
|
47
|
+
* API for it, so it is stated exactly as the runtime's own ERR_STRING_TOO_LONG
|
|
48
|
+
* message does. `Buffer#toString()` throws above it — the crash from issue
|
|
49
|
+
* 4393, where a pnpm workspace's registry fetch crossed the ceiling.
|
|
50
|
+
*/
|
|
51
|
+
const V8_MAX_STRING_CHARS = 0x1fffffe8;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ceiling on the stdout bytes collected from a cdxrs child. stdout must end up
|
|
55
|
+
* as a single JS string, so a payload above V8's string limit can never be
|
|
56
|
+
* delivered no matter how long the child runs: buffering it only burns memory
|
|
57
|
+
* until the concat throws. Past the ceiling the child is killed and the caller
|
|
58
|
+
* takes the JS path. Counting bytes (never smaller than the decoded character
|
|
59
|
+
* count) keeps the boundary safe for multibyte output.
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_MAX_STDOUT_BYTES = V8_MAX_STRING_CHARS;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the stdout ceiling for a run. CDXGEN_RS_MAX_STDOUT_BYTES lowers (or
|
|
65
|
+
* raises) it — used by tests and by hosts that want the failover earlier.
|
|
66
|
+
*
|
|
67
|
+
* @returns {number} Maximum stdout bytes to collect before failing over.
|
|
68
|
+
*/
|
|
69
|
+
function maxStdoutBytes() {
|
|
70
|
+
const raw = readEnvironmentVariable("CDXGEN_RS_MAX_STDOUT_BYTES");
|
|
71
|
+
if (!raw) {
|
|
72
|
+
return DEFAULT_MAX_STDOUT_BYTES;
|
|
73
|
+
}
|
|
74
|
+
const parsed = Number.parseInt(raw, 10);
|
|
75
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
console.warn(
|
|
79
|
+
`cdxrs: ignoring malformed CDXGEN_RS_MAX_STDOUT_BYTES "${raw}"; using the default ${DEFAULT_MAX_STDOUT_BYTES} bytes.`,
|
|
80
|
+
);
|
|
81
|
+
return DEFAULT_MAX_STDOUT_BYTES;
|
|
82
|
+
}
|
|
83
|
+
|
|
45
84
|
/** Sentinel returned when the Rust path is unavailable. Callers take JS path. */
|
|
46
85
|
export const CDXRS_FALLBACK = Object.freeze({
|
|
47
86
|
ok: false,
|
|
@@ -164,6 +203,11 @@ function probeVersion(binPath) {
|
|
|
164
203
|
*
|
|
165
204
|
* Passing `content` is the normal case, since cdxgen holds BOMs in memory.
|
|
166
205
|
*
|
|
206
|
+
* Collected stdout is bounded: it must become a single JS string, so a child
|
|
207
|
+
* that would exceed V8's string limit (by default; see
|
|
208
|
+
* CDXGEN_RS_MAX_STDOUT_BYTES) is killed and reported as a fallback instead of
|
|
209
|
+
* crashing the process at the concat.
|
|
210
|
+
*
|
|
167
211
|
* @param {string} subcommand The subcommand to run (e.g. "info").
|
|
168
212
|
* @param {Object} opts
|
|
169
213
|
* @param {string|Buffer} [opts.content] BOM data to feed over stdin.
|
|
@@ -284,6 +328,9 @@ export async function runCdxrs(subcommand, opts = {}) {
|
|
|
284
328
|
let timedOut = false;
|
|
285
329
|
const stdoutChunks = [];
|
|
286
330
|
const stderrChunks = [];
|
|
331
|
+
const stdoutLimit = maxStdoutBytes();
|
|
332
|
+
let stdoutBytes = 0;
|
|
333
|
+
let stdoutOverflow = false;
|
|
287
334
|
|
|
288
335
|
const pluginRuntime = getDefaultPluginRuntime();
|
|
289
336
|
const env = { ...process.env };
|
|
@@ -313,6 +360,17 @@ export async function runCdxrs(subcommand, opts = {}) {
|
|
|
313
360
|
child.stdin.end(stdinContent);
|
|
314
361
|
}
|
|
315
362
|
|
|
363
|
+
// Kill the process group (-pid) so any child processes of cdxrs die too.
|
|
364
|
+
// Best effort: the process may have already exited, in which case the
|
|
365
|
+
// close/error handlers take over.
|
|
366
|
+
const killProcessGroup = () => {
|
|
367
|
+
try {
|
|
368
|
+
process.kill(-child.pid, "SIGKILL");
|
|
369
|
+
} catch {
|
|
370
|
+
child.kill("SIGKILL");
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
316
374
|
const timer = setTimeout(() => {
|
|
317
375
|
timedOut = true;
|
|
318
376
|
if (DEBUG_MODE) {
|
|
@@ -320,16 +378,21 @@ export async function runCdxrs(subcommand, opts = {}) {
|
|
|
320
378
|
`cdxrs: timeout after ${timeoutMs}ms, killing process group.`,
|
|
321
379
|
);
|
|
322
380
|
}
|
|
323
|
-
|
|
324
|
-
// Kill the process group (-pid) so any child processes of cdxrs die too.
|
|
325
|
-
process.kill(-child.pid, "SIGKILL");
|
|
326
|
-
} catch {
|
|
327
|
-
// Process may have already exited; fall through to error handling.
|
|
328
|
-
child.kill("SIGKILL");
|
|
329
|
-
}
|
|
381
|
+
killProcessGroup();
|
|
330
382
|
}, timeoutMs);
|
|
331
383
|
|
|
332
384
|
child.stdout?.on("data", (chunk) => {
|
|
385
|
+
if (stdoutOverflow) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
stdoutBytes += chunk.length;
|
|
389
|
+
if (stdoutBytes > stdoutLimit) {
|
|
390
|
+
// The payload can never become a deliverable string, so stop
|
|
391
|
+
// buffering, kill the child, and let the close handler fall back.
|
|
392
|
+
stdoutOverflow = true;
|
|
393
|
+
killProcessGroup();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
333
396
|
stdoutChunks.push(chunk);
|
|
334
397
|
});
|
|
335
398
|
|
|
@@ -365,8 +428,36 @@ export async function runCdxrs(subcommand, opts = {}) {
|
|
|
365
428
|
return;
|
|
366
429
|
}
|
|
367
430
|
|
|
368
|
-
|
|
369
|
-
|
|
431
|
+
// Checked before the exit code: the child was killed mid-stream for
|
|
432
|
+
// producing more stdout than can ever be delivered as a string, so even
|
|
433
|
+
// an exit-0 result is an undeliverable truncation (issue 4393).
|
|
434
|
+
if (stdoutOverflow) {
|
|
435
|
+
console.warn(
|
|
436
|
+
`cdxrs: stdout passed ${stdoutLimit} bytes (a string cannot exceed ${V8_MAX_STRING_CHARS} characters), so the child was stopped.`,
|
|
437
|
+
);
|
|
438
|
+
resolve({ ...CDXRS_FALLBACK, reason: "stdout-too-large" });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let stdout;
|
|
443
|
+
try {
|
|
444
|
+
stdout = Buffer.concat(stdoutChunks).toString();
|
|
445
|
+
} catch (err) {
|
|
446
|
+
// The byte cap above should make this unreachable; it is kept so that
|
|
447
|
+
// no failure inside this handler can abort SBOM generation.
|
|
448
|
+
console.warn(
|
|
449
|
+
`cdxrs: could not collect child stdout (${err.message}), falling back to JS path.`,
|
|
450
|
+
);
|
|
451
|
+
resolve({ ...CDXRS_FALLBACK, reason: "stdout-collect-failed" });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
let stderr = "";
|
|
455
|
+
try {
|
|
456
|
+
stderr = Buffer.concat(stderrChunks).toString();
|
|
457
|
+
} catch {
|
|
458
|
+
// stderr carries NDJSON logs, already forwarded as they arrived;
|
|
459
|
+
// dropping the raw tail must not fail a run with valid stdout.
|
|
460
|
+
}
|
|
370
461
|
|
|
371
462
|
// For the validate subcommand, exit code 3 means "validation failure
|
|
372
463
|
// with findings on stdout" — the operation succeeded, the BOM just has
|
|
@@ -215,6 +215,112 @@ export function propagateRequiredScopeFromDependencies(
|
|
|
215
215
|
return components;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
function isTypeOnlyExcludedComponent(component) {
|
|
219
|
+
if (component?.scope !== "excluded") {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return (
|
|
223
|
+
isNpmTypesOnlyComponent(component) ||
|
|
224
|
+
(component.properties || []).some(
|
|
225
|
+
(property) =>
|
|
226
|
+
property.name === "cdx:npm:package:type-only" &&
|
|
227
|
+
property.value === "true",
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Scopes the dependencies of type-only npm packages as optional.
|
|
234
|
+
*
|
|
235
|
+
* A package imported only for its TypeScript types, like an `@types` package,
|
|
236
|
+
* is erased at compile time, and so is what it depends on unless a runtime
|
|
237
|
+
* package depends on it too. A component below a type-only package becomes
|
|
238
|
+
* optional when no dependency path from a root of the graph reaches it without
|
|
239
|
+
* passing through a type-only package. A component that already has a scope,
|
|
240
|
+
* from its manifest or from usage evidence, keeps it.
|
|
241
|
+
*
|
|
242
|
+
* @param {Object[]} components CycloneDX component objects
|
|
243
|
+
* @param {Object[]} dependencies CycloneDX dependency entries
|
|
244
|
+
* @returns {Object[]} The same component array with scopes updated in place
|
|
245
|
+
*/
|
|
246
|
+
export function markTypeOnlyDependenciesOptional(
|
|
247
|
+
components = [],
|
|
248
|
+
dependencies = [],
|
|
249
|
+
) {
|
|
250
|
+
if (!components?.length || !dependencies?.length) {
|
|
251
|
+
return components;
|
|
252
|
+
}
|
|
253
|
+
const componentByRef = new Map();
|
|
254
|
+
const typeOnlyRefs = new Set();
|
|
255
|
+
for (const component of components) {
|
|
256
|
+
for (const ref of [component?.["bom-ref"], component?.purl]) {
|
|
257
|
+
if (!ref) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const normalizedRef = normalizeBomRef(ref);
|
|
261
|
+
componentByRef.set(normalizedRef, component);
|
|
262
|
+
if (isTypeOnlyExcludedComponent(component)) {
|
|
263
|
+
typeOnlyRefs.add(normalizedRef);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (!typeOnlyRefs.size) {
|
|
268
|
+
return components;
|
|
269
|
+
}
|
|
270
|
+
const childrenByRef = new Map();
|
|
271
|
+
const dependentRefs = new Set();
|
|
272
|
+
for (const dependency of dependencies) {
|
|
273
|
+
const ref = normalizeBomRef(dependency?.ref);
|
|
274
|
+
if (!ref) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (!childrenByRef.has(ref)) {
|
|
278
|
+
childrenByRef.set(ref, new Set());
|
|
279
|
+
}
|
|
280
|
+
for (const dependsOnRef of dependency.dependsOn || []) {
|
|
281
|
+
const childRef = normalizeBomRef(dependsOnRef);
|
|
282
|
+
if (childRef && childRef !== ref) {
|
|
283
|
+
childrenByRef.get(ref).add(childRef);
|
|
284
|
+
dependentRefs.add(childRef);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const walk = (startRefs) => {
|
|
289
|
+
const seen = new Set();
|
|
290
|
+
const stack = [...startRefs];
|
|
291
|
+
while (stack.length) {
|
|
292
|
+
const ref = stack.pop();
|
|
293
|
+
if (seen.has(ref)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
seen.add(ref);
|
|
297
|
+
for (const childRef of childrenByRef.get(ref) || []) {
|
|
298
|
+
if (!typeOnlyRefs.has(childRef)) {
|
|
299
|
+
stack.push(childRef);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return seen;
|
|
304
|
+
};
|
|
305
|
+
// Every ref nothing depends on is a root, so a partial graph errs towards
|
|
306
|
+
// keeping a component rather than scoping it optional.
|
|
307
|
+
const runtimeRefs = walk(
|
|
308
|
+
[...childrenByRef.keys()].filter(
|
|
309
|
+
(ref) => !dependentRefs.has(ref) && !typeOnlyRefs.has(ref),
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
for (const ref of walk(typeOnlyRefs)) {
|
|
313
|
+
if (typeOnlyRefs.has(ref) || runtimeRefs.has(ref)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const component = componentByRef.get(ref);
|
|
317
|
+
if (component && !component.scope) {
|
|
318
|
+
component.scope = "optional";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return components;
|
|
322
|
+
}
|
|
323
|
+
|
|
218
324
|
function serviceIdentityKey(service) {
|
|
219
325
|
if (service?.["bom-ref"]) {
|
|
220
326
|
return service["bom-ref"].toLowerCase();
|
|
@@ -395,6 +501,71 @@ export function mergeServices(services, newServices) {
|
|
|
395
501
|
return Array.from(serviceMap.values());
|
|
396
502
|
}
|
|
397
503
|
|
|
504
|
+
// Descriptive fields, each listed with the raw spellings that collectors set
|
|
505
|
+
// before component conversion. Conversion prefers the converted `licenses`
|
|
506
|
+
// over a raw `license`, and a raw `author` over `authors`, so a field is only
|
|
507
|
+
// filled in, under the spelling the duplicate uses, when the retained
|
|
508
|
+
// component has no spelling of it.
|
|
509
|
+
const DESCRIPTIVE_FIELDS = [
|
|
510
|
+
["description"],
|
|
511
|
+
["publisher"],
|
|
512
|
+
["copyright"],
|
|
513
|
+
["authors", "author"],
|
|
514
|
+
["licenses", "license"],
|
|
515
|
+
];
|
|
516
|
+
|
|
517
|
+
const hasFieldValue = (value) =>
|
|
518
|
+
value !== undefined &&
|
|
519
|
+
value !== null &&
|
|
520
|
+
value !== "" &&
|
|
521
|
+
!(Array.isArray(value) && !value.length);
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Carry the descriptive metadata of a duplicate sighting over to the retained
|
|
525
|
+
* component. A lockfile is usually parsed first and knows only the identity,
|
|
526
|
+
* while a manifest seen later, such as a gemspec, knows the description,
|
|
527
|
+
* authors, licenses, and project links (discussion 4389).
|
|
528
|
+
*
|
|
529
|
+
* The retained sighting keeps its own values. Lists are not unioned because
|
|
530
|
+
* two sightings spell the same fact differently: one license as an SPDX id and
|
|
531
|
+
* as a name, or as an expression that must never share the `licenses` array
|
|
532
|
+
* with license objects. External references are unioned, since each one is a
|
|
533
|
+
* distinct link.
|
|
534
|
+
*
|
|
535
|
+
* @param {object} existingComponent Retained component, updated in place
|
|
536
|
+
* @param {object} comp Duplicate sighting of the same component
|
|
537
|
+
*/
|
|
538
|
+
function mergeDescriptiveFields(existingComponent, comp) {
|
|
539
|
+
for (const spellings of DESCRIPTIVE_FIELDS) {
|
|
540
|
+
if (
|
|
541
|
+
spellings.some((spelling) => hasFieldValue(existingComponent[spelling]))
|
|
542
|
+
) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const spelling = spellings.find((aspelling) =>
|
|
546
|
+
hasFieldValue(comp[aspelling]),
|
|
547
|
+
);
|
|
548
|
+
if (spelling) {
|
|
549
|
+
existingComponent[spelling] = comp[spelling];
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (!comp.externalReferences?.length) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// A copy, as the array may be shared with another sighting
|
|
556
|
+
const existingRefs = [...(existingComponent.externalReferences || [])];
|
|
557
|
+
for (const newref of comp.externalReferences) {
|
|
558
|
+
if (
|
|
559
|
+
!existingRefs.some(
|
|
560
|
+
(ref) => ref.type === newref.type && ref.url === newref.url,
|
|
561
|
+
)
|
|
562
|
+
) {
|
|
563
|
+
existingRefs.push(newref);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
existingComponent.externalReferences = existingRefs;
|
|
567
|
+
}
|
|
568
|
+
|
|
398
569
|
/**
|
|
399
570
|
* Trim duplicate components by retaining all the properties
|
|
400
571
|
*
|
|
@@ -403,18 +574,21 @@ export function mergeServices(services, newServices) {
|
|
|
403
574
|
* @returns {Array} Filtered components
|
|
404
575
|
*/
|
|
405
576
|
export function trimComponents(components) {
|
|
406
|
-
|
|
407
|
-
|
|
577
|
+
// The keys come from the scanned manifests. In a plain object, a key such as
|
|
578
|
+
// `__proto__` would resolve to Object.prototype and the merge below would
|
|
579
|
+
// write to it.
|
|
580
|
+
const keyCache = new Map();
|
|
408
581
|
for (const comp of components) {
|
|
409
582
|
const key = (
|
|
410
583
|
comp.purl ||
|
|
411
584
|
comp["bom-ref"] ||
|
|
412
585
|
comp.name + comp.version
|
|
413
586
|
).toLowerCase();
|
|
414
|
-
if (!keyCache
|
|
415
|
-
keyCache
|
|
587
|
+
if (!keyCache.has(key)) {
|
|
588
|
+
keyCache.set(key, comp);
|
|
416
589
|
} else {
|
|
417
|
-
const existingComponent = keyCache
|
|
590
|
+
const existingComponent = keyCache.get(key);
|
|
591
|
+
mergeDescriptiveFields(existingComponent, comp);
|
|
418
592
|
// We need to retain any properties that differ
|
|
419
593
|
if (comp.properties) {
|
|
420
594
|
if (existingComponent.properties) {
|
|
@@ -530,10 +704,7 @@ export function trimComponents(components) {
|
|
|
530
704
|
}
|
|
531
705
|
}
|
|
532
706
|
}
|
|
533
|
-
|
|
534
|
-
filteredComponents.push(keyCache[akey]);
|
|
535
|
-
}
|
|
536
|
-
return filteredComponents;
|
|
707
|
+
return Array.from(keyCache.values());
|
|
537
708
|
}
|
|
538
709
|
|
|
539
710
|
/**
|
|
@@ -603,7 +774,8 @@ export function isPartialTree(dependencies, componentsCount = 1) {
|
|
|
603
774
|
let isCbom = false;
|
|
604
775
|
let parentsWithChildsCount = 0;
|
|
605
776
|
for (const adep of dependencies) {
|
|
606
|
-
|
|
777
|
+
// Producers such as protobuf omit empty dependsOn lists
|
|
778
|
+
if (adep?.dependsOn?.length > 0) {
|
|
607
779
|
parentsWithChildsCount++;
|
|
608
780
|
}
|
|
609
781
|
if (!isCbom && adep?.provides?.length > 0) {
|
|
@@ -640,7 +812,7 @@ export function recomputeScope(pkgList, dependencies) {
|
|
|
640
812
|
}
|
|
641
813
|
for (const adep of dependencies) {
|
|
642
814
|
if (requiredPkgs[adep.ref]) {
|
|
643
|
-
for (const ado of adep.dependsOn) {
|
|
815
|
+
for (const ado of adep.dependsOn || []) {
|
|
644
816
|
requiredPkgs[ado] = true;
|
|
645
817
|
}
|
|
646
818
|
}
|
package/lib/inventory/display.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import process from "node:process";
|
|
4
3
|
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
import { safeExistsSync } from "../core/fs.js";
|
|
12
11
|
import { toCamel } from "../core/paths.js";
|
|
13
12
|
import { detectAiModelVariants } from "./aiModelVariants.js";
|
|
13
|
+
import { readReachablesSlices } from "./atomUtils.js";
|
|
14
14
|
import { formatOccurrenceEvidence } from "./evidenceUtils.js";
|
|
15
15
|
import {
|
|
16
16
|
formatHbomHardwareClassSummary,
|
|
@@ -1407,12 +1407,11 @@ export function printReachables(sliceArtefacts) {
|
|
|
1407
1407
|
return;
|
|
1408
1408
|
}
|
|
1409
1409
|
const purlCounts = {};
|
|
1410
|
-
const reachablesSlices =
|
|
1411
|
-
|
|
1412
|
-
);
|
|
1410
|
+
const reachablesSlices = readReachablesSlices(reachablesSlicesFile);
|
|
1411
|
+
// undefined when the slices file was unreadable; readReachablesSlices warned.
|
|
1413
1412
|
const rflows = Array.isArray(reachablesSlices)
|
|
1414
1413
|
? reachablesSlices
|
|
1415
|
-
: reachablesSlices
|
|
1414
|
+
: reachablesSlices?.reachables || [];
|
|
1416
1415
|
for (const areachable of rflows) {
|
|
1417
1416
|
const purls = areachable.purls || [];
|
|
1418
1417
|
for (const apurl of purls) {
|