@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.
Files changed (182) hide show
  1. package/README.md +6 -6
  2. package/bin/cdxgen.js +4 -0
  3. package/bin/evinse.js +3 -0
  4. package/bin/validate.js +6 -0
  5. package/data/license-aliases.json +24 -2
  6. package/data/remediations.json +124 -0
  7. package/data/rules/ai-bom.yaml +1 -1
  8. package/data/rules/brotli-decoder.yaml +107 -0
  9. package/data/rules/vscode-extensions.yaml +75 -0
  10. package/data/spdx-license-list.json +121 -7
  11. package/data/spdx-licenses.json +69 -0
  12. package/data/spdx.schema.json +845 -2
  13. package/lib/cli/bomAssembly.js +92 -3
  14. package/lib/cli/index.js +108 -1
  15. package/lib/cli/jsBom.js +153 -64
  16. package/lib/cli/jvmBom.js +237 -46
  17. package/lib/cli/managedBom.js +142 -16
  18. package/lib/cli/nativeBom.js +42 -5
  19. package/lib/core/boundedFile.js +344 -0
  20. package/lib/core/brotliProbe.js +182 -0
  21. package/lib/core/env.js +8 -0
  22. package/lib/core/fs.js +12 -0
  23. package/lib/core/httpClient.js +98 -1
  24. package/lib/core/lockPaths.js +38 -0
  25. package/lib/core/paths.js +24 -1
  26. package/lib/core/processTree.js +5 -0
  27. package/lib/core/toolRegistry.js +170 -0
  28. package/lib/ecosystems/cppEvidence.js +263 -106
  29. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  30. package/lib/ecosystems/ecosystems.js +25 -10
  31. package/lib/ecosystems/gemHostProbe.js +301 -0
  32. package/lib/ecosystems/gradleutils.js +828 -224
  33. package/lib/ecosystems/parsers-bazel.js +107 -6
  34. package/lib/ecosystems/parsers-js.js +505 -22
  35. package/lib/ecosystems/parsers-misc.js +209 -20
  36. package/lib/ecosystems/parsers-python.js +89 -2
  37. package/lib/ecosystems/pythonutils.js +128 -2
  38. package/lib/ecosystems/rubyutils.js +538 -31
  39. package/lib/ecosystems/scalacliutils.js +2 -0
  40. package/lib/ecosystems/scalasem.js +796 -151
  41. package/lib/ecosystems/vendoredCode.js +237 -3
  42. package/lib/ecosystems/venvProbe.js +360 -0
  43. package/lib/evinser/evinser.js +346 -43
  44. package/lib/helpers/agenticToolutils.js +413 -93
  45. package/lib/helpers/vsixutils.js +1574 -315
  46. package/lib/inventory/atomUtils.js +77 -0
  47. package/lib/inventory/brotliDecoder.js +142 -0
  48. package/lib/inventory/buildToolRateLimit.js +1 -1
  49. package/lib/inventory/cbomutils.js +56 -7
  50. package/lib/inventory/deps.js +35 -0
  51. package/lib/inventory/dosai.js +139 -5
  52. package/lib/inventory/dotnetRuntime.js +217 -0
  53. package/lib/inventory/envcontext.js +307 -13
  54. package/lib/inventory/evidenceUtils.js +11 -0
  55. package/lib/inventory/fetchBatch.js +1 -1
  56. package/lib/inventory/formulationParsers.js +31 -3
  57. package/lib/inventory/gtfobins.js +13 -0
  58. package/lib/inventory/hostRuntimeProbe.js +686 -0
  59. package/lib/inventory/jvmLocalRepos.js +9 -2
  60. package/lib/inventory/jvmToolEnv.js +29 -0
  61. package/lib/inventory/licenseEnhancer.js +12 -0
  62. package/lib/inventory/mavenCentralMirror.js +534 -0
  63. package/lib/inventory/nodeBuildInfo.js +218 -0
  64. package/lib/inventory/nodeHostProbe.js +202 -0
  65. package/lib/inventory/osPackageResolver.js +182 -0
  66. package/lib/inventory/phpBuildInfo.js +10 -59
  67. package/lib/inventory/phpHostProbe.js +32 -488
  68. package/lib/inventory/rubyBuildInfo.js +972 -0
  69. package/lib/inventory/rubyHostProbe.js +218 -0
  70. package/lib/inventory/scalaCoords.js +4 -3
  71. package/lib/inventory/scalaDedupe.js +440 -0
  72. package/lib/inventory/source.js +49 -2
  73. package/lib/managers/binary.js +337 -3
  74. package/lib/managers/docker.js +79 -0
  75. package/lib/parsers/largeJson.js +28 -11
  76. package/lib/parsers/vscodeExtensionId.js +183 -0
  77. package/lib/server/server.js +12 -1
  78. package/lib/stages/postgen/ruleEngine.js +13 -5
  79. package/lib/stages/pregen/envAudit.js +1 -0
  80. package/lib/stages/pregen/pregen.js +114 -57
  81. package/lib/validator/bomValidator.js +6 -1
  82. package/lib/validator/generated/validate-1.6.mjs +1 -1
  83. package/lib/validator/generated/validate-1.7.mjs +1 -1
  84. package/lib/validator/generated/validate-2.0.mjs +1 -1
  85. package/lib/validator/index.js +1 -1
  86. package/package.json +3 -2
  87. package/types/lib/cli/bomAssembly.d.ts +13 -1
  88. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/cli/jsBom.d.ts +1 -17
  91. package/types/lib/cli/jsBom.d.ts.map +1 -1
  92. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  93. package/types/lib/cli/managedBom.d.ts.map +1 -1
  94. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  95. package/types/lib/core/boundedFile.d.ts +70 -0
  96. package/types/lib/core/boundedFile.d.ts.map +1 -0
  97. package/types/lib/core/brotliProbe.d.ts +37 -0
  98. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  99. package/types/lib/core/env.d.ts +4 -0
  100. package/types/lib/core/env.d.ts.map +1 -1
  101. package/types/lib/core/fs.d.ts.map +1 -1
  102. package/types/lib/core/httpClient.d.ts +24 -0
  103. package/types/lib/core/httpClient.d.ts.map +1 -1
  104. package/types/lib/core/lockPaths.d.ts +19 -0
  105. package/types/lib/core/lockPaths.d.ts.map +1 -0
  106. package/types/lib/core/processTree.d.ts +2 -0
  107. package/types/lib/core/processTree.d.ts.map +1 -1
  108. package/types/lib/core/toolRegistry.d.ts +36 -0
  109. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  110. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  111. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  112. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  113. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  114. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  115. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  116. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  117. package/types/lib/ecosystems/gradleutils.d.ts +47 -13
  118. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  121. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
  123. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-python.d.ts +10 -0
  125. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  126. package/types/lib/ecosystems/pythonutils.d.ts +20 -2
  127. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  128. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  129. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  131. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  132. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  133. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  134. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  135. package/types/lib/evinser/evinser.d.ts +63 -8
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/inventory/atomUtils.d.ts +19 -0
  138. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  139. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  140. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  141. package/types/lib/inventory/cbomutils.d.ts +17 -0
  142. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  143. package/types/lib/inventory/deps.d.ts.map +1 -1
  144. package/types/lib/inventory/dosai.d.ts +17 -1
  145. package/types/lib/inventory/dosai.d.ts.map +1 -1
  146. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  147. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  148. package/types/lib/inventory/envcontext.d.ts +52 -6
  149. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  150. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  151. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  152. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  153. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  154. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  155. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  156. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  157. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  158. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  159. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  160. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  161. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  162. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  163. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  164. package/types/lib/inventory/osPackageResolver.d.ts +38 -0
  165. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  166. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
  167. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
  168. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  169. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  170. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  171. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  172. package/types/lib/inventory/source.d.ts +10 -0
  173. package/types/lib/inventory/source.d.ts.map +1 -1
  174. package/types/lib/managers/binary.d.ts.map +1 -1
  175. package/types/lib/managers/docker.d.ts +12 -0
  176. package/types/lib/managers/docker.d.ts.map +1 -1
  177. package/types/lib/parsers/largeJson.d.ts +4 -3
  178. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  179. package/types/lib/server/server.d.ts.map +1 -1
  180. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/pregen.d.ts +12 -8
  182. 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]]} cdxgen's own lookups can be moved with MAVEN_CENTRAL_URL.`,
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
- const cleanCode = cleanStr(code);
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
- if (cleanCode.includes(cleanStr(algoName))) {
612
- cryptoAlgos.push({
613
- ...cbomCryptoOids[algoName],
614
- name: algoName,
615
- ref: `crypto/algorithm/${algoName}@${cbomCryptoOids[algoName].oid}`,
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;
@@ -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.
@@ -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 (DEBUG_MODE) {
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). Nothing cdxgen reads is lost.
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
- return readDosaiReport(reportFile, options, (report, read) => {
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 (sections.includes("MethodCalls") && options.keepMethodCall) {
797
- read("MethodCalls", { filter: options.keepMethodCall });
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
  /**