@cdxgen/cdxgen 13.5.0 → 13.6.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 +6 -6
- package/bin/cdxgen.js +4 -0
- package/bin/evinse.js +3 -0
- package/bin/validate.js +6 -0
- package/data/license-aliases.json +24 -2
- package/data/remediations.json +124 -0
- package/data/rules/ai-bom.yaml +1 -1
- package/data/rules/brotli-decoder.yaml +107 -0
- package/data/rules/vscode-extensions.yaml +75 -0
- package/data/spdx-license-list.json +121 -7
- package/data/spdx-licenses.json +69 -0
- package/data/spdx.schema.json +845 -2
- package/lib/cli/bomAssembly.js +92 -3
- package/lib/cli/index.js +108 -1
- package/lib/cli/jsBom.js +153 -64
- package/lib/cli/jvmBom.js +237 -46
- package/lib/cli/managedBom.js +142 -16
- package/lib/cli/nativeBom.js +42 -5
- package/lib/core/boundedFile.js +344 -0
- package/lib/core/brotliProbe.js +182 -0
- package/lib/core/env.js +8 -0
- package/lib/core/fs.js +12 -0
- package/lib/core/httpClient.js +98 -1
- package/lib/core/lockPaths.js +38 -0
- package/lib/core/paths.js +24 -1
- package/lib/core/processTree.js +5 -0
- package/lib/core/toolRegistry.js +170 -0
- package/lib/ecosystems/cppEvidence.js +263 -106
- package/lib/ecosystems/cppIncludeResolver.js +16 -5
- package/lib/ecosystems/ecosystems.js +25 -10
- package/lib/ecosystems/gemHostProbe.js +301 -0
- package/lib/ecosystems/gradleutils.js +828 -224
- package/lib/ecosystems/parsers-bazel.js +107 -6
- package/lib/ecosystems/parsers-js.js +505 -22
- package/lib/ecosystems/parsers-misc.js +209 -20
- package/lib/ecosystems/parsers-python.js +89 -2
- package/lib/ecosystems/pythonutils.js +128 -2
- package/lib/ecosystems/rubyutils.js +538 -31
- package/lib/ecosystems/scalacliutils.js +2 -0
- package/lib/ecosystems/scalasem.js +796 -151
- package/lib/ecosystems/vendoredCode.js +237 -3
- package/lib/ecosystems/venvProbe.js +360 -0
- package/lib/evinser/evinser.js +346 -43
- package/lib/helpers/agenticToolutils.js +413 -93
- package/lib/helpers/vsixutils.js +1574 -315
- package/lib/inventory/atomUtils.js +77 -0
- package/lib/inventory/brotliDecoder.js +142 -0
- package/lib/inventory/buildToolRateLimit.js +1 -1
- package/lib/inventory/cbomutils.js +56 -7
- package/lib/inventory/deps.js +35 -0
- package/lib/inventory/dosai.js +139 -5
- package/lib/inventory/dotnetRuntime.js +217 -0
- package/lib/inventory/envcontext.js +307 -13
- package/lib/inventory/evidenceUtils.js +11 -0
- package/lib/inventory/fetchBatch.js +1 -1
- package/lib/inventory/formulationParsers.js +31 -3
- package/lib/inventory/gtfobins.js +13 -0
- package/lib/inventory/hostRuntimeProbe.js +686 -0
- package/lib/inventory/jvmLocalRepos.js +9 -2
- package/lib/inventory/jvmToolEnv.js +29 -0
- package/lib/inventory/licenseEnhancer.js +12 -0
- package/lib/inventory/mavenCentralMirror.js +534 -0
- package/lib/inventory/nodeBuildInfo.js +218 -0
- package/lib/inventory/nodeHostProbe.js +202 -0
- package/lib/inventory/osPackageResolver.js +182 -0
- package/lib/inventory/phpBuildInfo.js +10 -59
- package/lib/inventory/phpHostProbe.js +32 -488
- package/lib/inventory/rubyBuildInfo.js +972 -0
- package/lib/inventory/rubyHostProbe.js +218 -0
- package/lib/inventory/scalaCoords.js +4 -3
- package/lib/inventory/scalaDedupe.js +440 -0
- package/lib/inventory/source.js +49 -2
- package/lib/managers/binary.js +337 -3
- package/lib/managers/docker.js +79 -0
- package/lib/parsers/largeJson.js +28 -11
- package/lib/parsers/vscodeExtensionId.js +183 -0
- package/lib/server/server.js +12 -1
- package/lib/stages/postgen/ruleEngine.js +13 -5
- package/lib/stages/pregen/envAudit.js +1 -0
- package/lib/stages/pregen/pregen.js +114 -57
- package/lib/validator/bomValidator.js +6 -1
- package/lib/validator/generated/validate-1.6.mjs +1 -1
- package/lib/validator/generated/validate-1.7.mjs +1 -1
- package/lib/validator/generated/validate-2.0.mjs +1 -1
- package/lib/validator/index.js +1 -1
- package/package.json +3 -2
- package/types/lib/cli/bomAssembly.d.ts +13 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +1 -17
- 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/boundedFile.d.ts +70 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -0
- package/types/lib/core/brotliProbe.d.ts +37 -0
- package/types/lib/core/brotliProbe.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +4 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +24 -0
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/lockPaths.d.ts +19 -0
- package/types/lib/core/lockPaths.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +2 -0
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts +36 -0
- package/types/lib/core/toolRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
- package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
- package/types/lib/ecosystems/gradleutils.d.ts +47 -13
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +17 -20
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +10 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +20 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +144 -12
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
- package/types/lib/ecosystems/venvProbe.d.ts +40 -0
- package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +19 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/brotliDecoder.d.ts +58 -0
- package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
- package/types/lib/inventory/cbomutils.d.ts +17 -0
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +17 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +52 -6
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/gtfobins.d.ts.map +1 -1
- package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
- package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
- package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
- package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
- package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
- package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +38 -0
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
- package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
- package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +10 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +12 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/largeJson.d.ts +4 -3
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +12 -8
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
|
@@ -183,6 +183,7 @@ export function findCompileCommands(src, options = {}) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
const atomFrontendArgKeysCache = new Map();
|
|
186
|
+
const atomOptionsCache = new Map();
|
|
186
187
|
|
|
187
188
|
/**
|
|
188
189
|
* Split an atom command that may carry a leading argument (`node
|
|
@@ -271,6 +272,43 @@ export function atomFrontendArgKeys(language) {
|
|
|
271
272
|
return keys;
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The options the installed atom accepts, as `atom --help` lists them. Asked
|
|
277
|
+
* once per atom command. An atom that fails to start accepts none.
|
|
278
|
+
*
|
|
279
|
+
* @returns {Set<string>} Supported options, such as `--bom-file`
|
|
280
|
+
*/
|
|
281
|
+
export function atomOptions() {
|
|
282
|
+
const command = getAtomCommand();
|
|
283
|
+
if (atomOptionsCache.has(command)) {
|
|
284
|
+
return atomOptionsCache.get(command);
|
|
285
|
+
}
|
|
286
|
+
const [bin, leadingArg] = splitAtomCommand(command);
|
|
287
|
+
const args = ["--help"];
|
|
288
|
+
if (leadingArg !== undefined) {
|
|
289
|
+
args.unshift(leadingArg);
|
|
290
|
+
}
|
|
291
|
+
const env = { ...process.env };
|
|
292
|
+
if (readEnvironmentVariable("ATOM_JAVA_HOME")) {
|
|
293
|
+
env.JAVA_HOME = readEnvironmentVariable("ATOM_JAVA_HOME");
|
|
294
|
+
}
|
|
295
|
+
const options = new Set();
|
|
296
|
+
const result = safeSpawnSync(bin, args, {
|
|
297
|
+
shell: isWin,
|
|
298
|
+
env,
|
|
299
|
+
timeout: 120000,
|
|
300
|
+
});
|
|
301
|
+
if (result?.status === 0 && result.stdout) {
|
|
302
|
+
for (const match of `${result.stdout}`.matchAll(
|
|
303
|
+
/(?:^|\s)(--[a-z][a-z0-9-]*)/g,
|
|
304
|
+
)) {
|
|
305
|
+
options.add(match[1]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
atomOptionsCache.set(command, options);
|
|
309
|
+
return options;
|
|
310
|
+
}
|
|
311
|
+
|
|
274
312
|
/**
|
|
275
313
|
* The `--frontend-args` atom takes for a C/C++ language: the project's
|
|
276
314
|
* compilation database, when there is one and the installed atom reads it
|
|
@@ -315,6 +353,45 @@ export function atomCompileCommandsArgs(src, language, options = {}) {
|
|
|
315
353
|
return ["--frontend-args", `compile-commands=${database}`];
|
|
316
354
|
}
|
|
317
355
|
|
|
356
|
+
/**
|
|
357
|
+
* The `--bom-file` a reachables slice takes package purls from: the BOM evinse
|
|
358
|
+
* annotates. atom would otherwise only find a BOM saved as `bom.json` inside
|
|
359
|
+
* the scanned directory, so a BOM written anywhere else gave every reachable
|
|
360
|
+
* flow no purl. An atom without `--bom-file` is given the arguments it has
|
|
361
|
+
* always been given, and still reads a `bom.json` in the scanned directory.
|
|
362
|
+
*
|
|
363
|
+
* @param {string} sliceType atom slice command
|
|
364
|
+
* @param {Object} options CLI options, for `input`
|
|
365
|
+
* @returns {string[]} Arguments to add to the atom command
|
|
366
|
+
*/
|
|
367
|
+
export function atomBomFileArgs(sliceType, options = {}) {
|
|
368
|
+
if (
|
|
369
|
+
sliceType !== "reachables" ||
|
|
370
|
+
!options?.input ||
|
|
371
|
+
!safeExistsSync(options.input)
|
|
372
|
+
) {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
const bomFile = resolve(options.input);
|
|
376
|
+
// The argument reaches a Windows shell command line through `shell: isWin`
|
|
377
|
+
// in executeAtom, so a path a shell would interpret is never passed on.
|
|
378
|
+
if (UNSAFE_SHELL_CHARS_RE.test(bomFile)) {
|
|
379
|
+
console.warn(
|
|
380
|
+
`The BOM path ${bomFile} contains characters a shell would interpret. Reachable flows will carry purls only if a bom.json is in the scanned directory.`,
|
|
381
|
+
);
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
if (!atomOptions().has("--bom-file")) {
|
|
385
|
+
if (DEBUG_MODE) {
|
|
386
|
+
console.log(
|
|
387
|
+
"The installed atom does not take --bom-file. Reachable flows will carry purls only if a bom.json is in the scanned directory.",
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
return ["--bom-file", bomFile];
|
|
393
|
+
}
|
|
394
|
+
|
|
318
395
|
/**
|
|
319
396
|
* The arguments that stop atom's Java frontend from running the scanned
|
|
320
397
|
* project's build. In its default delombok mode, atom runs
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// BOM components for the Brotli decoders compiled into native files. The
|
|
2
|
+
// detection itself, and why its markers can be trusted, is in
|
|
3
|
+
// lib/core/brotliProbe.js.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
MAX_BROTLI_PROBE_BYTES,
|
|
7
|
+
MAX_BROTLI_PROBES,
|
|
8
|
+
readBrotliDecoder,
|
|
9
|
+
} from "../core/brotliProbe.js";
|
|
10
|
+
import { applyPurl, tryBuildPurl } from "./purl.js";
|
|
11
|
+
|
|
12
|
+
export { MAX_BROTLI_PROBE_BYTES, MAX_BROTLI_PROBES, readBrotliDecoder };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The properties a file component carries once it has been probed: the fix
|
|
16
|
+
* state of each decoder found in it, or why it could not be read.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} decoder result of readBrotliDecoder
|
|
19
|
+
* @returns {{name: string, value: string}[]} properties to append
|
|
20
|
+
*/
|
|
21
|
+
export function brotliDecoderProperties(decoder) {
|
|
22
|
+
const properties = [];
|
|
23
|
+
if (decoder?.skipped) {
|
|
24
|
+
properties.push({ name: "cdx:brotli:probe", value: decoder.skipped });
|
|
25
|
+
}
|
|
26
|
+
if (decoder?.c) {
|
|
27
|
+
properties.push(
|
|
28
|
+
{ name: "cdx:brotli:c:blockSwitchFix", value: decoder.c.blockSwitchFix },
|
|
29
|
+
{ name: "cdx:brotli:c:evidence", value: decoder.c.evidence },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (decoder?.go) {
|
|
33
|
+
properties.push({
|
|
34
|
+
name: "cdx:brotli:go:blockSwitchFix",
|
|
35
|
+
value: decoder.go.blockSwitchFix,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return properties;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The library component every embedded decoder links to. It has no version:
|
|
43
|
+
* no build records one, and only the per-file cdx:brotli:*:blockSwitchFix
|
|
44
|
+
* properties say whether a copy is vulnerable. The `vcs_url` qualifier names
|
|
45
|
+
* the project and keeps the identity apart from an unpackaged `brotli`
|
|
46
|
+
* executable, which is `pkg:generic/brotli`. The purl has no namespace: a
|
|
47
|
+
* `pkg:github` purl is typed as an application and `pkg:generic/google` as a
|
|
48
|
+
* framework when the BOM is assembled, and this is neither.
|
|
49
|
+
*
|
|
50
|
+
* @returns {Object} component with `purl` and `bom-ref`
|
|
51
|
+
*/
|
|
52
|
+
export function createBrotliComponent() {
|
|
53
|
+
return applyPurl(
|
|
54
|
+
{
|
|
55
|
+
type: "library",
|
|
56
|
+
name: "brotli",
|
|
57
|
+
description:
|
|
58
|
+
"Brotli decoder compiled into native files; see the cdx:brotli properties on each file",
|
|
59
|
+
properties: [],
|
|
60
|
+
},
|
|
61
|
+
tryBuildPurl({
|
|
62
|
+
type: "generic",
|
|
63
|
+
name: "brotli",
|
|
64
|
+
qualifiers: { vcs_url: "git+https://github.com/google/brotli" },
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Start collecting the decoder copies of one scan.
|
|
71
|
+
*
|
|
72
|
+
* @returns {{component: Object|undefined, dependencies: Object[], copies: number, unpatched: number, unknown: number}}
|
|
73
|
+
*/
|
|
74
|
+
export function createBrotliInventory() {
|
|
75
|
+
return {
|
|
76
|
+
component: undefined,
|
|
77
|
+
dependencies: [],
|
|
78
|
+
copies: 0,
|
|
79
|
+
unpatched: 0,
|
|
80
|
+
unknown: 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Tag a probed file component and, when it holds a decoder, link it to the
|
|
86
|
+
* shared brotli component, creating that component on first use. Mutates the
|
|
87
|
+
* file component in place.
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} inventory accumulator from createBrotliInventory
|
|
90
|
+
* @param {Object} fileComponent file component with a `properties` array
|
|
91
|
+
* @param {Object} decoder result of readBrotliDecoder
|
|
92
|
+
*/
|
|
93
|
+
export function attachBrotliDecoder(inventory, fileComponent, decoder) {
|
|
94
|
+
const properties = brotliDecoderProperties(decoder);
|
|
95
|
+
if (!properties.length) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
fileComponent.properties.push(...properties);
|
|
99
|
+
if (!decoder.c && !decoder.go) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!inventory.component) {
|
|
103
|
+
inventory.component = createBrotliComponent();
|
|
104
|
+
}
|
|
105
|
+
const states = [decoder.c?.blockSwitchFix, decoder.go?.blockSwitchFix];
|
|
106
|
+
inventory.copies += 1;
|
|
107
|
+
if (states.includes("false")) {
|
|
108
|
+
inventory.unpatched += 1;
|
|
109
|
+
} else if (states.includes("unknown")) {
|
|
110
|
+
inventory.unknown += 1;
|
|
111
|
+
}
|
|
112
|
+
if (fileComponent["bom-ref"] && inventory.component["bom-ref"]) {
|
|
113
|
+
inventory.dependencies.push({
|
|
114
|
+
ref: fileComponent["bom-ref"],
|
|
115
|
+
dependsOn: [inventory.component["bom-ref"]],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Finish an inventory: record the copy counts on the brotli component.
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} inventory accumulator from createBrotliInventory
|
|
124
|
+
* @returns {{components: Object[], dependencies: Object[]}} what to add to the BOM
|
|
125
|
+
*/
|
|
126
|
+
export function finishBrotliInventory(inventory) {
|
|
127
|
+
if (!inventory?.component) {
|
|
128
|
+
return { components: [], dependencies: [] };
|
|
129
|
+
}
|
|
130
|
+
inventory.component.properties = [
|
|
131
|
+
{ name: "cdx:brotli:embeddedCopies", value: String(inventory.copies) },
|
|
132
|
+
{ name: "cdx:brotli:unpatchedCopies", value: String(inventory.unpatched) },
|
|
133
|
+
{ name: "cdx:brotli:unknownCopies", value: String(inventory.unknown) },
|
|
134
|
+
];
|
|
135
|
+
return {
|
|
136
|
+
components: [inventory.component],
|
|
137
|
+
dependencies: [
|
|
138
|
+
{ ref: inventory.component["bom-ref"], dependsOn: [] },
|
|
139
|
+
...inventory.dependencies,
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -129,7 +129,7 @@ export function noteBuildToolRateLimit(tool, output, context = {}) {
|
|
|
129
129
|
? `, so cdxgen runs no further ${tool} resolution in this scan and`
|
|
130
130
|
: ", so";
|
|
131
131
|
console.warn(
|
|
132
|
-
`The repository ${tool} downloads from is rate limiting this machine (HTTP 429)${stopped} its dependency tree is incomplete. ${MIRROR_SETTINGS[tool] || MIRROR_SETTINGS[TOOL_MATCHER[tool]]}
|
|
132
|
+
`The repository ${tool} downloads from is rate limiting this machine (HTTP 429)${stopped} its dependency tree is incomplete. ${MIRROR_SETTINGS[tool] || MIRROR_SETTINGS[TOOL_MATCHER[tool]]} Set MAVEN_CENTRAL_MIRROR=true to point every build tool at Google's Maven Central mirror, or MAVEN_CENTRAL_URL for cdxgen's own lookups.`,
|
|
133
133
|
);
|
|
134
134
|
recordDegradation("build.rate-limited", {
|
|
135
135
|
ecosystem: "java",
|
|
@@ -601,19 +601,68 @@ export async function collectDosaiCryptoComponents(src, options = {}) {
|
|
|
601
601
|
/**
|
|
602
602
|
* Find crypto algorithm in the given code snippet
|
|
603
603
|
*
|
|
604
|
+
* cleanStr fuses the snippet's identifiers into one lowercase string
|
|
605
|
+
* (`prepare_for_save(iv` becomes `prepareforsaveiv`), so a plain substring
|
|
606
|
+
* test reads short algorithm names inside unrelated identifiers (`rsa` in
|
|
607
|
+
* `for_save`). A name therefore matches only where a cleanStr word begins:
|
|
608
|
+
* the snippet's start, right after any character cleanStr strips or keeps as
|
|
609
|
+
* a separator, or at a lowercase-to-uppercase change in the original text
|
|
610
|
+
* (`SHA256withRSA`, `HmacSHA256`, `PBKDF2WithHmacSHA256`,
|
|
611
|
+
* `generateRsaKeyPair`). Boundaries are computed from the original string
|
|
612
|
+
* before it is lowercased, so case changes survive the cleanStr fusion.
|
|
613
|
+
* Letter/digit changes do not start a word: they added no match on any
|
|
614
|
+
* reviewed form or corpus snippet, while splitting every digit-bearing
|
|
615
|
+
* identifier (`md5`, `base64`) into false word starts. Fused forms the names
|
|
616
|
+
* themselves are written in still match: `algorithms.AES(key)` and
|
|
617
|
+
* `AESGCM(key)` yield `aes`, `AES-256-GCM` yields `aes256-GCM`,
|
|
618
|
+
* `createHash("sha256")` yields `sha-256`. All-uppercase fused forms
|
|
619
|
+
* (`HMACSHA256`) still hide every name but the run's first.
|
|
620
|
+
*
|
|
604
621
|
* @param {string} code Code snippet
|
|
605
622
|
* @returns {Array} Arary of crypto algorithm objects with oid and description
|
|
606
623
|
*/
|
|
607
624
|
export function findCryptoAlgos(code) {
|
|
608
|
-
|
|
625
|
+
if (typeof code !== "string" || !code) {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
const cleanChars = [];
|
|
629
|
+
// canStart[i] is true when cleanChars[i] begins a maximal [0-9a-z] word,
|
|
630
|
+
// either in the original text or at a lower-to-uppercase change in it
|
|
631
|
+
const canStart = [];
|
|
632
|
+
let inWord = false;
|
|
633
|
+
for (let i = 0; i < code.length; i++) {
|
|
634
|
+
const ch = code[i];
|
|
635
|
+
const lower = ch.toLowerCase();
|
|
636
|
+
const isWordChar =
|
|
637
|
+
lower.length === 1 &&
|
|
638
|
+
((lower >= "0" && lower <= "9") || (lower >= "a" && lower <= "z"));
|
|
639
|
+
if (isWordChar) {
|
|
640
|
+
cleanChars.push(lower);
|
|
641
|
+
const prev = i > 0 ? code[i - 1] : "";
|
|
642
|
+
const caseStart = /[a-z]/.test(prev) && /[A-Z]/.test(ch);
|
|
643
|
+
canStart.push(!inWord || caseStart);
|
|
644
|
+
inWord = true;
|
|
645
|
+
} else {
|
|
646
|
+
inWord = false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const cleanCode = cleanChars.join("");
|
|
609
650
|
const cryptoAlgos = [];
|
|
610
651
|
for (const algoName of Object.keys(cbomCryptoOids)) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
652
|
+
const needle = cleanStr(algoName);
|
|
653
|
+
if (!needle) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
let pos = cleanCode.indexOf(needle);
|
|
657
|
+
for (; pos !== -1; pos = cleanCode.indexOf(needle, pos + 1)) {
|
|
658
|
+
if (canStart[pos]) {
|
|
659
|
+
cryptoAlgos.push({
|
|
660
|
+
...cbomCryptoOids[algoName],
|
|
661
|
+
name: algoName,
|
|
662
|
+
ref: `crypto/algorithm/${algoName}@${cbomCryptoOids[algoName].oid}`,
|
|
663
|
+
});
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
617
666
|
}
|
|
618
667
|
}
|
|
619
668
|
return cryptoAlgos;
|
package/lib/inventory/deps.js
CHANGED
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
mavenLocalRepositories,
|
|
26
26
|
splitArtifactFileName,
|
|
27
27
|
} from "./jvmLocalRepos.js";
|
|
28
|
+
import {
|
|
29
|
+
mavenMirrorSettingsArgs,
|
|
30
|
+
mavenWrapperMirrorEnv,
|
|
31
|
+
} from "./mavenCentralMirror.js";
|
|
28
32
|
import { pypiBomRef } from "./purl.js";
|
|
29
33
|
import { findLicenseId, spdxLicenses } from "./spdx.js";
|
|
30
34
|
|
|
@@ -136,11 +140,13 @@ export async function collectMvnDependencies(
|
|
|
136
140
|
const addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
|
|
137
141
|
copyArgs = copyArgs.concat(addArgs);
|
|
138
142
|
}
|
|
143
|
+
copyArgs = copyArgs.concat(mavenMirrorSettingsArgs());
|
|
139
144
|
if (basePath && basePath !== MAVEN_CACHE_DIR) {
|
|
140
145
|
console.log(`Executing '${mavenCmd} in ${basePath}`);
|
|
141
146
|
const result = safeSpawnSync(mavenCmd, copyArgs, {
|
|
142
147
|
cwd: basePath,
|
|
143
148
|
shell: isWin,
|
|
149
|
+
env: { ...process.env, ...mavenWrapperMirrorEnv() },
|
|
144
150
|
});
|
|
145
151
|
const rateLimited = noteBuildToolRateLimit(
|
|
146
152
|
"maven",
|
|
@@ -336,6 +342,35 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
|
|
|
336
342
|
return jarNSMapping;
|
|
337
343
|
}
|
|
338
344
|
|
|
345
|
+
/**
|
|
346
|
+
* The jar namespace map entries of components a jar scan produced, in the
|
|
347
|
+
* shape {@link collectJarNS} writes (`{ namespaces: string[] }`), from their
|
|
348
|
+
* `internal:Namespaces` properties. The scan reads each jar from a temporary
|
|
349
|
+
* copy, so no `jarFile` is recorded. Purls `existing` already maps keep the
|
|
350
|
+
* entry they have, which names the jar it was read from.
|
|
351
|
+
*
|
|
352
|
+
* @param {Object[]} components Components with `internal:Namespaces` properties
|
|
353
|
+
* @param {Object} [existing] Jar namespace map already collected
|
|
354
|
+
* @returns {Object} New entries keyed by purl
|
|
355
|
+
*/
|
|
356
|
+
export function namespaceMappingOfComponents(components, existing = {}) {
|
|
357
|
+
const mapping = {};
|
|
358
|
+
for (const component of components || []) {
|
|
359
|
+
if (!component?.purl || existing[component.purl]) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const namespaces = (component.properties || [])
|
|
363
|
+
.filter((property) => property.name === "internal:Namespaces")
|
|
364
|
+
.flatMap((property) => String(property.value || "").split("\n"))
|
|
365
|
+
.map((namespace) => namespace.trim())
|
|
366
|
+
.filter(Boolean);
|
|
367
|
+
if (namespaces.length) {
|
|
368
|
+
mapping[component.purl] = { namespaces: [...new Set(namespaces)] };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return mapping;
|
|
372
|
+
}
|
|
373
|
+
|
|
339
374
|
/**
|
|
340
375
|
* Convert a JAR namespace mapping (produced by {@link collectJarNS}) into an array
|
|
341
376
|
* of CycloneDX package component objects.
|
package/lib/inventory/dosai.js
CHANGED
|
@@ -727,6 +727,105 @@ function referencedIds(entries, field) {
|
|
|
727
727
|
return ids;
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
+
// The fields of a MethodCalls row cdxgen reads: the component it names (Purl
|
|
731
|
+
// or Module), where the call site is (Path, FileName, LineNumber) and what it
|
|
732
|
+
// calls (ClassName, CalledMethod). Everything else a report row carries -
|
|
733
|
+
// arguments, evidence kinds, caller identities - no cdxgen consumer reads,
|
|
734
|
+
// and a report with millions of calls cannot afford to hold them.
|
|
735
|
+
const DOSAI_METHOD_CALL_FIELDS = [
|
|
736
|
+
"Purl",
|
|
737
|
+
"Module",
|
|
738
|
+
"Path",
|
|
739
|
+
"FileName",
|
|
740
|
+
"LineNumber",
|
|
741
|
+
"ClassName",
|
|
742
|
+
"CalledMethod",
|
|
743
|
+
];
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* The largest number of distinct method calls cdxgen keeps from one report.
|
|
747
|
+
*
|
|
748
|
+
* Reports in the millions of calls repeat the same call sites heavily, so the
|
|
749
|
+
* aggregated rows stay far below this; the cap exists so a report whose calls
|
|
750
|
+
* are all distinct degrades the occurrence evidence instead of the process.
|
|
751
|
+
*
|
|
752
|
+
* @returns {number} Cap on distinct kept rows
|
|
753
|
+
*/
|
|
754
|
+
export function maxDosaiMethodCalls() {
|
|
755
|
+
const configured = Number.parseInt(
|
|
756
|
+
readEnvironmentVariable("CDXGEN_DOSAI_MAX_METHOD_CALLS") || "",
|
|
757
|
+
10,
|
|
758
|
+
);
|
|
759
|
+
if (Number.isInteger(configured) && configured > 0) {
|
|
760
|
+
return configured;
|
|
761
|
+
}
|
|
762
|
+
return 1_000_000;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Build the `map` that turns report MethodCalls rows into what cdxgen keeps:
|
|
767
|
+
* rows the caller rejects are dropped, the rest are reduced to
|
|
768
|
+
* {@link DOSAI_METHOD_CALL_FIELDS} and aggregated by those fields, so a call
|
|
769
|
+
* site reached a million times is held once with a `CallCount`. Past
|
|
770
|
+
* {@link maxDosaiMethodCalls} distinct rows, further rows are dropped and one
|
|
771
|
+
* warning names the cap.
|
|
772
|
+
*
|
|
773
|
+
* @param {string} reportFile Report the rows come from, named in the warning
|
|
774
|
+
* @param {Object} [options] Options
|
|
775
|
+
* @param {Function} [options.keepMethodCall] Predicate choosing the rows to keep
|
|
776
|
+
* @param {number} [options.maxMethodCalls] Cap override, for tests
|
|
777
|
+
* @returns {{map: Function, finish: Function}} The row mapper, and the
|
|
778
|
+
* `finish` that reports a cap reached
|
|
779
|
+
*/
|
|
780
|
+
function dosaiMethodCallCollector(reportFile, options = {}) {
|
|
781
|
+
const keep = options.keepMethodCall;
|
|
782
|
+
const max = options.maxMethodCalls ?? maxDosaiMethodCalls();
|
|
783
|
+
const keptByKey = new Map();
|
|
784
|
+
let dropped = 0;
|
|
785
|
+
return {
|
|
786
|
+
map(methodCall) {
|
|
787
|
+
if (keep && !keep(methodCall)) {
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
const compact = {};
|
|
791
|
+
for (const field of DOSAI_METHOD_CALL_FIELDS) {
|
|
792
|
+
if (methodCall?.[field] !== undefined) {
|
|
793
|
+
compact[field] = methodCall[field];
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const key = DOSAI_METHOD_CALL_FIELDS.map((field) => compact[field]).join(
|
|
797
|
+
"\u0000",
|
|
798
|
+
);
|
|
799
|
+
const kept = keptByKey.get(key);
|
|
800
|
+
if (kept) {
|
|
801
|
+
kept.CallCount = (kept.CallCount ?? 1) + 1;
|
|
802
|
+
return undefined;
|
|
803
|
+
}
|
|
804
|
+
if (keptByKey.size >= max) {
|
|
805
|
+
dropped++;
|
|
806
|
+
return undefined;
|
|
807
|
+
}
|
|
808
|
+
keptByKey.set(key, compact);
|
|
809
|
+
return compact;
|
|
810
|
+
},
|
|
811
|
+
finish() {
|
|
812
|
+
if (!dropped) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
console.warn(
|
|
816
|
+
`The dosai report "${reportFile}" holds more distinct method calls than cdxgen keeps (${max}); ${dropped} call sites were dropped, so occurrence evidence may be incomplete. Raise the cap with CDXGEN_DOSAI_MAX_METHOD_CALLS.`,
|
|
817
|
+
);
|
|
818
|
+
recordDegradation("dosai.method-calls-capped", {
|
|
819
|
+
ecosystem: "csharp",
|
|
820
|
+
tool: "dosai",
|
|
821
|
+
impact: "components",
|
|
822
|
+
command: reportFile,
|
|
823
|
+
detail: `The dosai report holds more distinct method calls than the cap of ${max}, so ${dropped} call sites were dropped from the occurrence evidence.`,
|
|
824
|
+
});
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
730
829
|
function readDosaiReport(reportFile, options, readTrimmed) {
|
|
731
830
|
if (!reportFile || !safeExistsSync(reportFile)) {
|
|
732
831
|
return undefined;
|
|
@@ -761,7 +860,13 @@ function readDosaiReport(reportFile, options, readTrimmed) {
|
|
|
761
860
|
}
|
|
762
861
|
return report;
|
|
763
862
|
} catch (err) {
|
|
764
|
-
if (
|
|
863
|
+
if (err instanceof RangeError) {
|
|
864
|
+
// A heap or string limit is a report-size problem the user can act on,
|
|
865
|
+
// unlike a malformed report, so it is named even without debug output.
|
|
866
|
+
console.warn(
|
|
867
|
+
`The dosai report "${reportFile}" is too large to read: ${err.message}. Raise the Node.js heap, for example NODE_OPTIONS="--max-old-space-size=8192", or write the report with dosai's --sections so it holds less.`,
|
|
868
|
+
);
|
|
869
|
+
} else if (DEBUG_MODE) {
|
|
765
870
|
console.log(
|
|
766
871
|
`Unable to read the dosai report "${reportFile}": ${err.message}`,
|
|
767
872
|
);
|
|
@@ -776,25 +881,34 @@ function readDosaiReport(reportFile, options, readTrimmed) {
|
|
|
776
881
|
* A report that fits in one JavaScript string is parsed whole. A larger one is
|
|
777
882
|
* read in bounded runs, keeping the requested sections, the CallGraph nodes and
|
|
778
883
|
* edges that PackageReachability references, and the MethodCalls accepted by
|
|
779
|
-
* `keepMethodCall` (none when it is omitted).
|
|
884
|
+
* `keepMethodCall` (none when it is omitted). A kept MethodCalls row is
|
|
885
|
+
* reduced to the fields cdxgen reads and aggregated by them, so a call site
|
|
886
|
+
* reached many times is held once with a `CallCount`; distinct rows past
|
|
887
|
+
* {@link maxDosaiMethodCalls} are dropped with one warning. Nothing cdxgen
|
|
888
|
+
* reads is lost.
|
|
780
889
|
*
|
|
781
890
|
* @param {string} reportFile Path to the dosai methods JSON
|
|
782
891
|
* @param {Object} [options] Options
|
|
783
892
|
* @param {string[]} [options.sections] Sections to keep from a large report (default: every section cdxgen reads)
|
|
784
893
|
* @param {Function} [options.keepMethodCall] Predicate choosing the MethodCalls to keep from a large report
|
|
894
|
+
* @param {number} [options.maxMethodCalls] Cap on distinct kept MethodCalls, for tests
|
|
785
895
|
* @param {number} [options.maxTextBytes] Largest report parsed whole, for tests
|
|
786
896
|
* @returns {Object|undefined} Parsed report, or undefined when missing or invalid
|
|
787
897
|
*/
|
|
788
898
|
export function readDosaiMethodsReport(reportFile, options = {}) {
|
|
789
899
|
const sections = options.sections || DOSAI_METHODS_SECTIONS;
|
|
790
|
-
|
|
900
|
+
const collector =
|
|
901
|
+
sections.includes("MethodCalls") && options.keepMethodCall
|
|
902
|
+
? dosaiMethodCallCollector(reportFile, options)
|
|
903
|
+
: undefined;
|
|
904
|
+
const report = readDosaiReport(reportFile, options, (report, read) => {
|
|
791
905
|
for (const section of sections) {
|
|
792
906
|
if (section !== "CallGraph" && section !== "MethodCalls") {
|
|
793
907
|
read(section);
|
|
794
908
|
}
|
|
795
909
|
}
|
|
796
|
-
if (
|
|
797
|
-
read("MethodCalls", {
|
|
910
|
+
if (collector) {
|
|
911
|
+
read("MethodCalls", { map: collector.map });
|
|
798
912
|
}
|
|
799
913
|
if (sections.includes("CallGraph")) {
|
|
800
914
|
// CallGraph precedes PackageReachability in the file, which is why the
|
|
@@ -809,6 +923,26 @@ export function readDosaiMethodsReport(reportFile, options = {}) {
|
|
|
809
923
|
});
|
|
810
924
|
}
|
|
811
925
|
});
|
|
926
|
+
// A report parsed whole kept every row, as it always has: reduce them to
|
|
927
|
+
// the fields cdxgen reads and aggregate repeats, without the caller's
|
|
928
|
+
// predicate, which the whole parse has never applied. A report that took
|
|
929
|
+
// the trimmed path already went through the collector above.
|
|
930
|
+
if (report && !dosaiReportSources.get(report)?.trimmed) {
|
|
931
|
+
const wholeCollector = dosaiMethodCallCollector(reportFile, {
|
|
932
|
+
maxMethodCalls: options.maxMethodCalls,
|
|
933
|
+
});
|
|
934
|
+
const reduced = [];
|
|
935
|
+
for (const methodCall of report.MethodCalls || []) {
|
|
936
|
+
const kept = wholeCollector.map(methodCall);
|
|
937
|
+
if (kept !== undefined) {
|
|
938
|
+
reduced.push(kept);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
report.MethodCalls = reduced;
|
|
942
|
+
wholeCollector.finish();
|
|
943
|
+
}
|
|
944
|
+
collector?.finish();
|
|
945
|
+
return report;
|
|
812
946
|
}
|
|
813
947
|
|
|
814
948
|
/**
|