@cdxgen/cdxgen 13.0.0 → 13.1.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 (170) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +140 -8
  4. package/bin/convert.js +7 -0
  5. package/bin/evinse.js +1 -1
  6. package/bin/hbom.js +13 -4
  7. package/bin/tracebom.js +9 -1
  8. package/data/component-tags.json +8 -1
  9. package/data/frameworks-list.json +86 -1
  10. package/data/remediations.json +1144 -0
  11. package/data/rules/asar-archives.yaml +1 -1
  12. package/data/rules/build-fidelity.yaml +782 -0
  13. package/data/rules/package-integrity.yaml +13 -13
  14. package/lib/audit/index.js +48 -4
  15. package/lib/audit/scoring.js +2 -8
  16. package/lib/cli/bomAssembly.js +83 -15
  17. package/lib/cli/cliOptions.js +124 -7
  18. package/lib/cli/index.js +76 -48
  19. package/lib/cli/jsBom.js +32 -3
  20. package/lib/cli/jvmBom.js +1494 -1315
  21. package/lib/cli/managedBom.js +97 -28
  22. package/lib/cli/nativeBom.js +189 -10
  23. package/lib/core/buildLedger.js +799 -0
  24. package/lib/core/deferredExit.js +141 -0
  25. package/lib/core/env.js +69 -1
  26. package/lib/core/fs.js +60 -0
  27. package/lib/core/logger.js +76 -25
  28. package/lib/core/severity.js +16 -0
  29. package/lib/core/syncFileWriter.js +106 -0
  30. package/lib/core/ui.js +46 -2
  31. package/lib/ecosystems/ecosystems.js +9 -0
  32. package/lib/ecosystems/gradleutils.js +355 -15
  33. package/lib/ecosystems/parsers-js.js +26 -0
  34. package/lib/ecosystems/parsers-jvm.js +9 -0
  35. package/lib/ecosystems/parsers-misc.js +16 -2
  36. package/lib/ecosystems/parsers-python.js +171 -35
  37. package/lib/ecosystems/parsers-rust.js +12 -2
  38. package/lib/ecosystems/pypiClassifiers.js +326 -0
  39. package/lib/ecosystems/pythonutils.js +49 -1
  40. package/lib/ecosystems/rubyutils.js +13 -0
  41. package/lib/ecosystems/utils.js +9 -1
  42. package/lib/evinser/evinser.js +30 -12
  43. package/lib/helpers/remote/dependency-track.js +81 -14
  44. package/lib/inventory/auditCategories.js +9 -0
  45. package/lib/inventory/depsUtils.js +72 -0
  46. package/lib/inventory/dosai.js +431 -22
  47. package/lib/inventory/envcontext.js +211 -19
  48. package/lib/inventory/fetchBatch.js +57 -0
  49. package/lib/inventory/jvmToolEnv.js +593 -0
  50. package/lib/inventory/osinfo.js +123 -16
  51. package/lib/inventory/table.js +9 -5
  52. package/lib/inventory/toolRequirements.js +903 -0
  53. package/lib/managers/binary.js +216 -40
  54. package/lib/managers/docker.js +26 -5
  55. package/lib/server/server.js +1 -1
  56. package/lib/stages/postgen/annotationBuilder.js +97 -0
  57. package/lib/stages/postgen/auditBom.js +76 -79
  58. package/lib/stages/postgen/introspection/annotate.js +320 -0
  59. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  60. package/lib/stages/postgen/introspection/emit.js +192 -0
  61. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  62. package/lib/stages/postgen/introspection/markers.js +227 -0
  63. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  64. package/lib/stages/postgen/introspection/report.js +1208 -0
  65. package/lib/stages/postgen/introspection/score.js +1067 -0
  66. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  67. package/lib/stages/postgen/parentEdge.js +156 -0
  68. package/lib/stages/postgen/postgen.js +115 -2
  69. package/lib/stages/postgen/ruleEngine.js +17 -0
  70. package/lib/stages/pregen/envAudit.js +42 -2
  71. package/lib/stages/pregen/pregen.js +1171 -6
  72. package/lib/validator/bomValidator.js +11 -2
  73. package/package.json +14 -14
  74. package/types/lib/audit/index.d.ts.map +1 -1
  75. package/types/lib/audit/scoring.d.ts +2 -8
  76. package/types/lib/audit/scoring.d.ts.map +1 -1
  77. package/types/lib/cli/bomAssembly.d.ts +14 -0
  78. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  79. package/types/lib/cli/cliOptions.d.ts +2 -1
  80. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  81. package/types/lib/cli/index.d.ts +8 -0
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/cli/jsBom.d.ts.map +1 -1
  84. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  85. package/types/lib/cli/managedBom.d.ts.map +1 -1
  86. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  87. package/types/lib/core/buildLedger.d.ts +272 -0
  88. package/types/lib/core/buildLedger.d.ts.map +1 -0
  89. package/types/lib/core/deferredExit.d.ts +84 -0
  90. package/types/lib/core/deferredExit.d.ts.map +1 -0
  91. package/types/lib/core/env.d.ts +22 -1
  92. package/types/lib/core/env.d.ts.map +1 -1
  93. package/types/lib/core/fs.d.ts +21 -0
  94. package/types/lib/core/fs.d.ts.map +1 -1
  95. package/types/lib/core/logger.d.ts +14 -1
  96. package/types/lib/core/logger.d.ts.map +1 -1
  97. package/types/lib/core/severity.d.ts +11 -0
  98. package/types/lib/core/severity.d.ts.map +1 -0
  99. package/types/lib/core/syncFileWriter.d.ts +33 -0
  100. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  101. package/types/lib/core/ui.d.ts +1 -1
  102. package/types/lib/core/ui.d.ts.map +1 -1
  103. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  104. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  105. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  109. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  110. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  111. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  112. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  113. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  114. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  115. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  116. package/types/lib/ecosystems/utils.d.ts +4 -4
  117. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  118. package/types/lib/evinser/evinser.d.ts +5 -1
  119. package/types/lib/evinser/evinser.d.ts.map +1 -1
  120. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  121. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  122. package/types/lib/inventory/auditCategories.d.ts +8 -0
  123. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  125. package/types/lib/inventory/dosai.d.ts +20 -0
  126. package/types/lib/inventory/dosai.d.ts.map +1 -1
  127. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  128. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  129. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  130. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  131. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  132. package/types/lib/inventory/osinfo.d.ts +83 -2
  133. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  134. package/types/lib/inventory/table.d.ts +4 -1
  135. package/types/lib/inventory/table.d.ts.map +1 -1
  136. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  137. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  138. package/types/lib/managers/binary.d.ts +71 -0
  139. package/types/lib/managers/binary.d.ts.map +1 -1
  140. package/types/lib/managers/docker.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  142. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  144. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  145. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  146. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  147. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  148. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  149. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  150. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  151. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  152. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  153. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  154. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  155. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  156. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  157. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  158. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  159. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  160. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  161. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  162. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  163. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  164. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  165. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  166. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  167. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  168. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  169. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  170. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -250,7 +250,7 @@
250
250
  and $prop($, 'cdx:cargo:hasNativeBuild') = 'true'
251
251
  and $count($$.components[
252
252
  $prop($, 'cdx:github:action:ecosystem') = 'cargo'
253
- and $contains($prop($, 'cdx:github:action:role'), 'toolchain')
253
+ and $contains($nullSafeProp($, 'cdx:github:action:role'), 'toolchain')
254
254
  and $prop($, 'cdx:github:action:versionPinningType') != 'sha'
255
255
  ]) > 0
256
256
  ]
@@ -267,7 +267,7 @@
267
267
  "buildScriptCapabilities": $prop($, 'cdx:cargo:buildScriptCapabilities'),
268
268
  "toolchainActions": $$.components[
269
269
  $prop($, 'cdx:github:action:ecosystem') = 'cargo'
270
- and $contains($prop($, 'cdx:github:action:role'), 'toolchain')
270
+ and $contains($nullSafeProp($, 'cdx:github:action:role'), 'toolchain')
271
271
  and $prop($, 'cdx:github:action:versionPinningType') != 'sha'
272
272
  ].$prop($, 'cdx:github:action:uses')
273
273
  }
@@ -283,17 +283,17 @@
283
283
  $prop($, 'cdx:rust:buildTool') = 'cargo'
284
284
  and $prop($, 'cdx:cargo:hasNativeBuild') = 'true'
285
285
  and (
286
- $contains($prop($, 'cdx:cargo:buildScriptCapabilities'), 'process-execution')
287
- or $contains($prop($, 'cdx:cargo:buildScriptCapabilities'), 'network-access')
288
- or $contains($prop($, 'cdx:cargo:nativeBuildIndicators'), '-sys')
286
+ $contains($nullSafeProp($, 'cdx:cargo:buildScriptCapabilities'), 'process-execution')
287
+ or $contains($nullSafeProp($, 'cdx:cargo:buildScriptCapabilities'), 'network-access')
288
+ or $contains($nullSafeProp($, 'cdx:cargo:nativeBuildIndicators'), '-sys')
289
289
  )
290
290
  and $count($$.components[
291
291
  $prop($, 'cdx:github:step:usesCargo') = 'true'
292
292
  and (
293
- $contains($prop($, 'cdx:github:step:cargoSubcommands'), 'build')
294
- or $contains($prop($, 'cdx:github:step:cargoSubcommands'), 'test')
295
- or $contains($prop($, 'cdx:github:step:cargoSubcommands'), 'package')
296
- or $contains($prop($, 'cdx:github:step:cargoSubcommands'), 'publish')
293
+ $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'build')
294
+ or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'test')
295
+ or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'package')
296
+ or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'publish')
297
297
  )
298
298
  ]) > 0
299
299
  ]
@@ -311,10 +311,10 @@
311
311
  "cargoCommands": $$.components[
312
312
  $prop($, 'cdx:github:step:usesCargo') = 'true'
313
313
  and (
314
- $contains($prop($, 'cdx:github:step:cargoSubcommands'), 'build')
315
- or $contains($prop($, 'cdx:github:step:cargoSubcommands'), 'test')
316
- or $contains($prop($, 'cdx:github:step:cargoSubcommands'), 'package')
317
- or $contains($prop($, 'cdx:github:step:cargoSubcommands'), 'publish')
314
+ $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'build')
315
+ or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'test')
316
+ or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'package')
317
+ or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'publish')
318
318
  )
319
319
  ].$prop($, 'cdx:github:step:command')
320
320
  }
@@ -6,9 +6,11 @@ import process from "node:process";
6
6
  import { createBom } from "../cli/index.js";
7
7
  import {
8
8
  isDryRun,
9
+ isSecureMode,
9
10
  readEnvironmentVariable,
10
11
  recordActivity,
11
12
  } from "../core/activity.js";
13
+ import { getRemediationCatalog } from "../core/buildLedger.js";
12
14
  import {
13
15
  getTmpDir,
14
16
  safeExistsSync,
@@ -53,6 +55,12 @@ import {
53
55
  isHbomLikeBom,
54
56
  isObomLikeBom,
55
57
  } from "../stages/postgen/auditBom.js";
58
+ import {
59
+ isIntrospectionEnabled,
60
+ reflectOnRun,
61
+ } from "../stages/postgen/introspection/reflect.js";
62
+ import { buildIntrospectionJson } from "../stages/postgen/introspection/report.js";
63
+ import { scoreReflection } from "../stages/postgen/introspection/score.js";
56
64
  import { postProcess } from "../stages/postgen/postgen.js";
57
65
  import { formatTargetLabel } from "./progress.js";
58
66
  import { renderAuditReport } from "./reporters.js";
@@ -259,7 +267,7 @@ export async function runDirectBomAuditFromBoms(inputBoms, options = {}) {
259
267
  ...directAuditOptions,
260
268
  getBomWithOras,
261
269
  });
262
- results.push({
270
+ const result = {
263
271
  auditOptions: directAuditOptions,
264
272
  bomFormat: getCycloneDxFormat(inputBom.bomJson),
265
273
  findings,
@@ -268,7 +276,43 @@ export async function runDirectBomAuditFromBoms(inputBoms, options = {}) {
268
276
  specVersion: inputBom.bomJson?.specVersion,
269
277
  status: "audited",
270
278
  summary: summarizeDirectAuditFindings(findings),
271
- });
279
+ };
280
+ if (isIntrospectionEnabled(options)) {
281
+ // The audited BOM was not generated by this run, so there is no ledger
282
+ // of its own; the verdict rests on BOM structure alone and says so.
283
+ try {
284
+ const reflection = await reflectOnRun(inputBom.bomJson, options, {
285
+ ledgerEvents: [],
286
+ ledgerSource: "none",
287
+ projectPath: inputBom.source || "",
288
+ });
289
+ reflection.scoring = scoreReflection(
290
+ reflection,
291
+ getRemediationCatalog(),
292
+ {
293
+ secureMode: isSecureMode,
294
+ inContainer:
295
+ readEnvironmentVariable("CDXGEN_IN_CONTAINER") === "true",
296
+ },
297
+ );
298
+ reflection.bom = {
299
+ serialNumber: inputBom.bomJson?.serialNumber,
300
+ componentCount: Array.isArray(inputBom.bomJson?.components)
301
+ ? inputBom.bomJson.components.length
302
+ : 0,
303
+ };
304
+ result.introspection = buildIntrospectionJson(
305
+ reflection,
306
+ reflection.scoring,
307
+ options,
308
+ );
309
+ } catch (err) {
310
+ console.warn(
311
+ `Build introspection failed to complete and was skipped: ${err?.message}`,
312
+ );
313
+ }
314
+ }
315
+ results.push(result);
272
316
  }
273
317
  const summary = {
274
318
  findingsBySeverity: {
@@ -1744,7 +1788,7 @@ async function recheckFindingsAgainstDefaultBranch(
1744
1788
  if (!bomNSData?.bomJson) {
1745
1789
  return undefined;
1746
1790
  }
1747
- const processedBomNSData = postProcess(
1791
+ const processedBomNSData = await postProcess(
1748
1792
  bomNSData,
1749
1793
  childOptions,
1750
1794
  defaultSourceSelection.scanDir,
@@ -1948,7 +1992,7 @@ export async function auditTarget(target, options) {
1948
1992
  options,
1949
1993
  );
1950
1994
  }
1951
- const processedBomNSData = postProcess(
1995
+ const processedBomNSData = await postProcess(
1952
1996
  bomNSData,
1953
1997
  childOptions,
1954
1998
  sourceSelection.scanDir,
@@ -1,17 +1,11 @@
1
1
  import { thoughtLog } from "../core/logger.js";
2
+ import { SEVERITY_ORDER } from "../core/severity.js";
2
3
  import {
3
4
  hasRegistryProvenanceEvidenceProperties,
4
5
  hasTrustedPublishingProperties,
5
6
  } from "../inventory/provenanceUtils.js";
6
7
 
7
- /** Map of severity names to numeric ordering used for threshold comparisons. */
8
- export const SEVERITY_ORDER = {
9
- none: -1,
10
- low: 0,
11
- medium: 1,
12
- high: 2,
13
- critical: 3,
14
- };
8
+ export { SEVERITY_ORDER };
15
9
 
16
10
  const BASE_FINDING_WEIGHT = {
17
11
  low: 4,
@@ -200,6 +200,8 @@ export const determineParentComponent = (options) => {
200
200
  * @param {object|undefined} detectedParent Parent the generator derived
201
201
  * @param {object[]} components The component list, appended to in place
202
202
  * @param {object[]} dependencies Edges the generator produced
203
+ * @param {object} options CLI options
204
+ * @param {string} ptype Package type
203
205
  * @returns {object[]} Edges to emit, re-anchored when an override applies
204
206
  */
205
207
  function anchorDetectedParent(
@@ -207,6 +209,8 @@ function anchorDetectedParent(
207
209
  detectedParent,
208
210
  components,
209
211
  dependencies,
212
+ options,
213
+ ptype,
210
214
  ) {
211
215
  const detectedRef = detectedParent?.["bom-ref"];
212
216
  const overrideRef = overrideParent?.["bom-ref"];
@@ -220,7 +224,25 @@ function anchorDetectedParent(
220
224
  Array.isArray(components) &&
221
225
  !components.some((comp) => comp["bom-ref"] === detectedRef)
222
226
  ) {
223
- components.push({ ...detectedParent });
227
+ // The detected parent carries transient keys (`license`, `evidence`,
228
+ // `_integrity`, `qualifiers`, `repository`, `homepage`) that only
229
+ // `metadata.component` is cleaned of. As a regular component it must go
230
+ // through the same conversion or schema validation rejects the document
231
+ // (issue #4320). The copy keeps the caller's context object untouched.
232
+ //
233
+ // Its workspace members go through `listComponents`, the same conversion
234
+ // `addMetadata` gives them when the detected parent is the one reaching
235
+ // `metadata.component`. Without it they keep the raw parser shape
236
+ // (`homepage: { url }`) that the nested Component schema rejects
237
+ // (issue #4326).
238
+ components.push(
239
+ cleanParentComponent({
240
+ ...detectedParent,
241
+ components: detectedParent.components?.length
242
+ ? listComponents(options, {}, detectedParent.components, ptype)
243
+ : undefined,
244
+ }),
245
+ );
224
246
  }
225
247
  // The caller owns the array it passed, and postProcess merges these edges
226
248
  // across generators, so the re-anchoring is returned rather than spliced in.
@@ -1302,8 +1324,7 @@ export function processHashes(pkg, component) {
1302
1324
  addComponentHash(ahash.alg, ahash.content, component);
1303
1325
  }
1304
1326
  } else if (pkg._shasum) {
1305
- const ahash = { alg: "SHA-1", content: pkg._shasum };
1306
- component.hashes.push(ahash);
1327
+ addComponentHash("SHA-1", pkg._shasum, component);
1307
1328
  } else if (pkg._integrity) {
1308
1329
  const integrity = parse(pkg._integrity) || {};
1309
1330
  // Components may have multiple hashes with various lengths. Check each one
@@ -1326,24 +1347,69 @@ export function processHashes(pkg, component) {
1326
1347
  }
1327
1348
  }
1328
1349
 
1350
+ // Digest length in hex characters for each algorithm CycloneDX names. A digest
1351
+ // that decodes to the wrong length for its algorithm is corrupt, whatever
1352
+ // encoding it arrived in.
1353
+ const HASH_HEX_LENGTHS = {
1354
+ MD5: 32,
1355
+ "SHA-1": 40,
1356
+ "SHA-256": 64,
1357
+ "SHA-384": 96,
1358
+ "SHA-512": 128,
1359
+ "SHA3-256": 64,
1360
+ "SHA3-384": 96,
1361
+ "SHA3-512": 128,
1362
+ "BLAKE2b-256": 64,
1363
+ "BLAKE2b-384": 96,
1364
+ "BLAKE2b-512": 128,
1365
+ };
1366
+
1367
+ // Lengths the CycloneDX hash-content pattern accepts, for algorithms not in the
1368
+ // table above.
1369
+ const HASH_HEX_LENGTHS_ANY = new Set([32, 40, 64, 96, 128]);
1370
+
1329
1371
  /**
1330
1372
  * Adds a hash to component.
1373
+ *
1374
+ * Registries hand out digests in hex or in the base64 form npm uses for
1375
+ * `integrity`, and both are accepted. A digest that is neither, or that decodes
1376
+ * to the wrong length for its algorithm, is dropped rather than copied through:
1377
+ * CycloneDX constrains `hash.content` to a hex digest, so emitting it anyway
1378
+ * fails schema validation for the whole document over one bad package.
1331
1379
  */
1332
1380
  export function addComponentHash(alg, digest, component) {
1333
- let hash;
1334
- // If it is a valid hash simply use it
1381
+ const hash = normalizeHashContent(alg, digest);
1382
+ if (!hash) {
1383
+ return;
1384
+ }
1385
+ component.hashes.push({ alg: alg, content: hash });
1386
+ }
1387
+
1388
+ /**
1389
+ * Normalize a digest to the lower-case hex form CycloneDX requires.
1390
+ *
1391
+ * @param {string} alg Hash algorithm name
1392
+ * @param {string} digest Digest in hex or base64
1393
+ * @returns {string|undefined} Hex digest, or undefined when it cannot be trusted
1394
+ */
1395
+ export function normalizeHashContent(alg, digest) {
1396
+ if (!digest || typeof digest !== "string") {
1397
+ return undefined;
1398
+ }
1399
+ const expectedLength = HASH_HEX_LENGTHS[alg];
1400
+ const isExpectedLength = (value) =>
1401
+ expectedLength
1402
+ ? value.length === expectedLength
1403
+ : HASH_HEX_LENGTHS_ANY.has(value.length);
1404
+
1335
1405
  if (new RegExp(HASH_PATTERN).test(digest)) {
1336
- hash = digest;
1337
- } else {
1338
- // Check if base64 encoded
1339
- const isBase64Encoded =
1340
- Buffer.from(digest, "base64").toString("base64") === digest;
1341
- hash = isBase64Encoded
1342
- ? Buffer.from(digest, "base64").toString("hex")
1343
- : digest;
1406
+ return isExpectedLength(digest) ? digest : undefined;
1344
1407
  }
1345
- const ahash = { alg: alg, content: hash };
1346
- component.hashes.push(ahash);
1408
+ // npm and friends carry the digest base64-encoded. Node decodes leniently, so
1409
+ // the decoded length is the check that matters: a truncated or invented value
1410
+ // decodes to something, just not to a digest of the right size.
1411
+ const decoded = Buffer.from(digest, "base64").toString("hex");
1412
+ return isExpectedLength(decoded) ? decoded : undefined;
1347
1413
  }
1348
1414
 
1349
1415
  /**
@@ -1397,6 +1463,8 @@ export const buildBomNSData = (options, pkgInfo, ptype, context) => {
1397
1463
  context.parentComponent,
1398
1464
  components,
1399
1465
  context.dependencies || [],
1466
+ options,
1467
+ ptype,
1400
1468
  );
1401
1469
  if (
1402
1470
  components &&
@@ -14,6 +14,7 @@
14
14
  import { join, resolve } from "node:path";
15
15
 
16
16
  import { isSecureMode, readEnvironmentVariable } from "../core/activity.js";
17
+ import { PROJECT_TYPE_ALIASES } from "../core/env.js";
17
18
  import {
18
19
  DEFAULT_CDX_SPEC_VERSION,
19
20
  normalizeCycloneDxComponentTypeFilter,
@@ -87,6 +88,21 @@ const POST_BUILD_SUPPORTED_PROJECT_TYPES = new Set([
87
88
  // Helpers
88
89
  // ---------------------------------------------------------------------------
89
90
 
91
+ /**
92
+ * The formulation privacy notice, shared by every path that turns formulation
93
+ * collection on automatically: the warning about emails and secrets is
94
+ * identical whichever caller adds the section.
95
+ *
96
+ * @returns {OptionsWarning} The notice.
97
+ */
98
+ function formulationPrivacyNotice() {
99
+ return {
100
+ level: "info",
101
+ message:
102
+ "NOTE: Automatically collecting formulation information. The section may include sensitive data such as emails and secrets.\nPlease review the generated SBOM before distribution or LLM training.",
103
+ };
104
+ }
105
+
90
106
  /**
91
107
  * Check whether a value was explicitly provided by the user (i.e., is not
92
108
  * the yargs default). Yargs always populates default values, so we cannot
@@ -174,7 +190,39 @@ function resolveOutputTarget(args, { filePath, isRemoteOrPurl }) {
174
190
  : resolve(join(filePath, args.output));
175
191
  }
176
192
 
177
- export function buildInitialOptions(args, { filePath, isRemoteOrPurl }) {
193
+ /**
194
+ * Resolve the introspection opt-in from the flag and the environment.
195
+ *
196
+ * The parser maps `CDXGEN_INTROSPECT` onto the same boolean the flag sets, so
197
+ * a value outside the documented opt-in vocabulary — an exported but empty
198
+ * variable most of all — arrives indistinguishable from `--no-introspect`.
199
+ * The command line is therefore the authority on refusal, and the environment
200
+ * speaks only through `true` and `1`. An unmentioned flag stays undefined so a
201
+ * profile can still enable it.
202
+ *
203
+ * @param {Object} args Parsed CLI arguments.
204
+ * @param {string[]} argv Raw command-line arguments the parser was given.
205
+ * @returns {boolean|undefined} The requested state, or undefined when nothing asked.
206
+ */
207
+ function resolveIntrospectOption(args, argv) {
208
+ const refused =
209
+ args.introspect === false &&
210
+ argv.some((argument) =>
211
+ ["--no-introspect", "--introspect=false"].includes(argument),
212
+ );
213
+ if (refused) {
214
+ return false;
215
+ }
216
+ return args.introspect === true ||
217
+ ["true", "1"].includes(readEnvironmentVariable("CDXGEN_INTROSPECT"))
218
+ ? true
219
+ : undefined;
220
+ }
221
+
222
+ export function buildInitialOptions(
223
+ args,
224
+ { filePath, isRemoteOrPurl, argv = process.argv },
225
+ ) {
178
226
  const res = Object.assign({}, args, {
179
227
  projectType: args.type,
180
228
  multiProject: args.recurse,
@@ -187,6 +235,10 @@ export function buildInitialOptions(args, { filePath, isRemoteOrPurl }) {
187
235
  include: args.include || args.includeRegex,
188
236
  noIgnore: args.noIgnore,
189
237
  gitAiNotesRef: args.gitAiNotesRef || "refs/notes/ai",
238
+ // Introspection: the flag or the documented environment opt-in. An
239
+ // explicit --no-introspect stays `false` and beats both, while an
240
+ // unmentioned flag stays undefined so a profile can still enable it.
241
+ introspect: resolveIntrospectOption(args, argv),
190
242
  // License normalization and enrichment are on by default; the only escape
191
243
  // hatch is the corresponding environment variable.
192
244
  licenseEnhance:
@@ -217,6 +269,24 @@ export function buildInitialOptions(args, { filePath, isRemoteOrPurl }) {
217
269
  return res;
218
270
  }
219
271
 
272
+ /**
273
+ * Check whether the supplied project types include an OS inventory type
274
+ * (`os` or an alias such as `linux`, `windows`, `macos`).
275
+ *
276
+ * @param {string|string[]|undefined|null} projectTypes Project types.
277
+ * @returns {boolean} True when an OS inventory type is requested.
278
+ */
279
+ function hasOsProjectType(projectTypes) {
280
+ if (!projectTypes) {
281
+ return false;
282
+ }
283
+ const values = Array.isArray(projectTypes) ? projectTypes : [projectTypes];
284
+ const osTypes = new Set(["os", ...(PROJECT_TYPE_ALIASES.os || [])]);
285
+ return values
286
+ .flatMap((projectType) => `${projectType}`.split(","))
287
+ .some((projectType) => osTypes.has(projectType.trim().toLowerCase()));
288
+ }
289
+
220
290
  /**
221
291
  * @typedef {object} OptionsWarning
222
292
  * @property {"info"|"warn"|"error"} level Severity level
@@ -263,6 +333,17 @@ export function applyPostConstructionOverrides(options, context) {
263
333
  "The cbom command does not support --component-type. Use cdxgen with --include-crypto when you need component-type filtering.",
264
334
  });
265
335
  }
336
+ // Crypto evidence collection analyses source code with atom, which is
337
+ // meaningless and extremely slow for OS inventories (discussion #4310).
338
+ // Refuse the combination and direct users to obom.
339
+ if (hasOsProjectType(options.projectType)) {
340
+ warnings.push({
341
+ level: "error",
342
+ message:
343
+ "The cbom command does not support OS inventory project types (-t os). Use the obom command instead to generate an OBOM for an operating system installation.",
344
+ });
345
+ return warnings;
346
+ }
266
347
  options.includeCrypto = true;
267
348
  }
268
349
  options.evidence = true;
@@ -288,6 +369,20 @@ export function applyPostConstructionOverrides(options, context) {
288
369
  options.installDeps = false;
289
370
  }
290
371
 
372
+ // --- Introspection CI gate threshold -----------------------------------
373
+ if (options.introspectFailBelow !== undefined) {
374
+ const threshold = Number(options.introspectFailBelow);
375
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 100) {
376
+ warnings.push({
377
+ level: "error",
378
+ message:
379
+ "--introspect-fail-below requires a number between 0 and 100, where absent means the gate never fails.",
380
+ });
381
+ } else {
382
+ options.introspectFailBelow = threshold;
383
+ }
384
+ }
385
+
291
386
  // When a standard is requested, the minimum spec version is 1.6
292
387
  if (options.standard) {
293
388
  if (!userSetSpecVersion) {
@@ -351,6 +446,8 @@ export function applyAdvancedOptions(options, context = {}) {
351
446
  const secureMode = context.isSecureMode ?? isSecureMode;
352
447
  const warnings = [];
353
448
  const isHbomOnly = isHbomOnlyProjectTypes(options.projectType);
449
+ const includeFormulationBeforeProfiles = options.includeFormulation === true;
450
+ const introspectExplicitlyDisabled = options.introspect === false;
354
451
 
355
452
  // --- Profile expansion -------------------------------------------------
356
453
  switch (options.profile) {
@@ -382,6 +479,14 @@ export function applyAdvancedOptions(options, context = {}) {
382
479
  case "license-compliance":
383
480
  process.env.FETCH_LICENSE = "true";
384
481
  break;
482
+ case "introspect":
483
+ options.introspect = true;
484
+ options.introspectAnnotate = options.introspectAnnotate !== false;
485
+ // The ledger's own record of what ran; formulation is where a consumer
486
+ // looks for the build environment the SBOM was produced in.
487
+ options.includeFormulation = !isHbomOnly;
488
+ options.evidence = true;
489
+ break;
385
490
  case "ml-tiny":
386
491
  process.env.FETCH_LICENSE = "true";
387
492
  options.deep = false;
@@ -441,17 +546,29 @@ export function applyAdvancedOptions(options, context = {}) {
441
546
  }
442
547
  }
443
548
 
549
+ // An explicit --no-introspect negates the introspect profile's headline
550
+ // capability while keeping the bundle's other effects: the user asked for
551
+ // the profile minus introspection.
552
+ if (options.profile === "introspect" && introspectExplicitlyDisabled) {
553
+ options.introspect = false;
554
+ warnings.push({
555
+ level: "info",
556
+ message:
557
+ "NOTE: Introspection stays off because --no-introspect was passed explicitly; the other introspect profile settings still apply.",
558
+ });
559
+ }
560
+
444
561
  // --- BOM audit → auto-formulation --------------------------------------
445
- if (options.bomAudit) {
562
+ const introspectProfileAddsFormulation =
563
+ options.profile === "introspect" && !includeFormulationBeforeProfiles;
564
+ if (options.bomAudit || introspectProfileAddsFormulation) {
446
565
  if (isHbomOnly) {
447
566
  // HBOM-only: skip formulation
448
567
  } else if (!options.includeFormulation) {
449
- warnings.push({
450
- level: "info",
451
- message:
452
- "NOTE: Automatically collecting formulation information. The section may include sensitive data such as emails and secrets.\nPlease review the generated SBOM before distribution or LLM training.",
453
- });
568
+ warnings.push(formulationPrivacyNotice());
454
569
  options.includeFormulation = true;
570
+ } else if (introspectProfileAddsFormulation) {
571
+ warnings.push(formulationPrivacyNotice());
455
572
  }
456
573
  }
457
574