@cdxgen/cdxgen 13.0.1 → 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 (155) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +125 -4
  4. package/bin/evinse.js +1 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/frameworks-list.json +86 -1
  7. package/data/remediations.json +1144 -0
  8. package/data/rules/asar-archives.yaml +1 -1
  9. package/data/rules/build-fidelity.yaml +782 -0
  10. package/data/rules/package-integrity.yaml +13 -13
  11. package/lib/audit/index.js +48 -4
  12. package/lib/audit/scoring.js +2 -8
  13. package/lib/cli/bomAssembly.js +20 -1
  14. package/lib/cli/cliOptions.js +94 -7
  15. package/lib/cli/index.js +76 -48
  16. package/lib/cli/jsBom.js +32 -3
  17. package/lib/cli/jvmBom.js +1494 -1315
  18. package/lib/cli/managedBom.js +97 -28
  19. package/lib/cli/nativeBom.js +189 -10
  20. package/lib/core/buildLedger.js +799 -0
  21. package/lib/core/deferredExit.js +141 -0
  22. package/lib/core/env.js +69 -1
  23. package/lib/core/fs.js +60 -0
  24. package/lib/core/logger.js +12 -99
  25. package/lib/core/severity.js +16 -0
  26. package/lib/core/syncFileWriter.js +106 -0
  27. package/lib/ecosystems/ecosystems.js +9 -0
  28. package/lib/ecosystems/gradleutils.js +355 -15
  29. package/lib/ecosystems/parsers-js.js +26 -0
  30. package/lib/ecosystems/parsers-jvm.js +9 -0
  31. package/lib/ecosystems/parsers-misc.js +16 -2
  32. package/lib/ecosystems/parsers-python.js +171 -35
  33. package/lib/ecosystems/parsers-rust.js +12 -2
  34. package/lib/ecosystems/pypiClassifiers.js +326 -0
  35. package/lib/ecosystems/pythonutils.js +49 -1
  36. package/lib/ecosystems/rubyutils.js +13 -0
  37. package/lib/evinser/evinser.js +30 -12
  38. package/lib/helpers/remote/dependency-track.js +81 -14
  39. package/lib/inventory/auditCategories.js +9 -0
  40. package/lib/inventory/depsUtils.js +72 -0
  41. package/lib/inventory/dosai.js +431 -22
  42. package/lib/inventory/envcontext.js +206 -18
  43. package/lib/inventory/fetchBatch.js +57 -0
  44. package/lib/inventory/jvmToolEnv.js +593 -0
  45. package/lib/inventory/osinfo.js +1 -0
  46. package/lib/inventory/toolRequirements.js +903 -0
  47. package/lib/managers/binary.js +24 -0
  48. package/lib/managers/docker.js +26 -5
  49. package/lib/server/server.js +1 -1
  50. package/lib/stages/postgen/annotationBuilder.js +97 -0
  51. package/lib/stages/postgen/auditBom.js +76 -79
  52. package/lib/stages/postgen/introspection/annotate.js +320 -0
  53. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  54. package/lib/stages/postgen/introspection/emit.js +192 -0
  55. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  56. package/lib/stages/postgen/introspection/markers.js +227 -0
  57. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  58. package/lib/stages/postgen/introspection/report.js +1208 -0
  59. package/lib/stages/postgen/introspection/score.js +1067 -0
  60. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  61. package/lib/stages/postgen/parentEdge.js +156 -0
  62. package/lib/stages/postgen/postgen.js +115 -2
  63. package/lib/stages/postgen/ruleEngine.js +17 -0
  64. package/lib/stages/pregen/envAudit.js +42 -2
  65. package/lib/stages/pregen/pregen.js +1171 -6
  66. package/lib/validator/bomValidator.js +11 -2
  67. package/package.json +14 -14
  68. package/types/lib/audit/index.d.ts.map +1 -1
  69. package/types/lib/audit/scoring.d.ts +2 -8
  70. package/types/lib/audit/scoring.d.ts.map +1 -1
  71. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  72. package/types/lib/cli/cliOptions.d.ts +2 -1
  73. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  74. package/types/lib/cli/index.d.ts +8 -0
  75. package/types/lib/cli/index.d.ts.map +1 -1
  76. package/types/lib/cli/jsBom.d.ts.map +1 -1
  77. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  78. package/types/lib/cli/managedBom.d.ts.map +1 -1
  79. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  80. package/types/lib/core/buildLedger.d.ts +272 -0
  81. package/types/lib/core/buildLedger.d.ts.map +1 -0
  82. package/types/lib/core/deferredExit.d.ts +84 -0
  83. package/types/lib/core/deferredExit.d.ts.map +1 -0
  84. package/types/lib/core/env.d.ts +22 -1
  85. package/types/lib/core/env.d.ts.map +1 -1
  86. package/types/lib/core/fs.d.ts +21 -0
  87. package/types/lib/core/fs.d.ts.map +1 -1
  88. package/types/lib/core/logger.d.ts +2 -11
  89. package/types/lib/core/logger.d.ts.map +1 -1
  90. package/types/lib/core/severity.d.ts +11 -0
  91. package/types/lib/core/severity.d.ts.map +1 -0
  92. package/types/lib/core/syncFileWriter.d.ts +33 -0
  93. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  94. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  95. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  96. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  97. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  98. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  99. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  102. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  103. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  104. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  105. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  107. package/types/lib/evinser/evinser.d.ts +5 -1
  108. package/types/lib/evinser/evinser.d.ts.map +1 -1
  109. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  110. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  111. package/types/lib/inventory/auditCategories.d.ts +8 -0
  112. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  113. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  114. package/types/lib/inventory/dosai.d.ts +20 -0
  115. package/types/lib/inventory/dosai.d.ts.map +1 -1
  116. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  117. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  118. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  119. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  120. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  121. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  122. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  123. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  124. package/types/lib/managers/binary.d.ts.map +1 -1
  125. package/types/lib/managers/docker.d.ts.map +1 -1
  126. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  127. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  128. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  129. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  130. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  131. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  132. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  133. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  134. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  135. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  136. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  137. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  138. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  139. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  140. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  141. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  142. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  144. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  145. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  146. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  147. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  148. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  149. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  150. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  151. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  152. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  153. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  154. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  155. 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"];
@@ -225,7 +229,20 @@ function anchorDetectedParent(
225
229
  // `metadata.component` is cleaned of. As a regular component it must go
226
230
  // through the same conversion or schema validation rejects the document
227
231
  // (issue #4320). The copy keeps the caller's context object untouched.
228
- components.push(cleanParentComponent({ ...detectedParent }));
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
+ );
229
246
  }
230
247
  // The caller owns the array it passed, and postProcess merges these edges
231
248
  // across generators, so the re-anchoring is returned rather than spliced in.
@@ -1446,6 +1463,8 @@ export const buildBomNSData = (options, pkgInfo, ptype, context) => {
1446
1463
  context.parentComponent,
1447
1464
  components,
1448
1465
  context.dependencies || [],
1466
+ options,
1467
+ ptype,
1449
1468
  );
1450
1469
  if (
1451
1470
  components &&
@@ -88,6 +88,21 @@ const POST_BUILD_SUPPORTED_PROJECT_TYPES = new Set([
88
88
  // Helpers
89
89
  // ---------------------------------------------------------------------------
90
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
+
91
106
  /**
92
107
  * Check whether a value was explicitly provided by the user (i.e., is not
93
108
  * the yargs default). Yargs always populates default values, so we cannot
@@ -175,7 +190,39 @@ function resolveOutputTarget(args, { filePath, isRemoteOrPurl }) {
175
190
  : resolve(join(filePath, args.output));
176
191
  }
177
192
 
178
- 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
+ ) {
179
226
  const res = Object.assign({}, args, {
180
227
  projectType: args.type,
181
228
  multiProject: args.recurse,
@@ -188,6 +235,10 @@ export function buildInitialOptions(args, { filePath, isRemoteOrPurl }) {
188
235
  include: args.include || args.includeRegex,
189
236
  noIgnore: args.noIgnore,
190
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),
191
242
  // License normalization and enrichment are on by default; the only escape
192
243
  // hatch is the corresponding environment variable.
193
244
  licenseEnhance:
@@ -318,6 +369,20 @@ export function applyPostConstructionOverrides(options, context) {
318
369
  options.installDeps = false;
319
370
  }
320
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
+
321
386
  // When a standard is requested, the minimum spec version is 1.6
322
387
  if (options.standard) {
323
388
  if (!userSetSpecVersion) {
@@ -381,6 +446,8 @@ export function applyAdvancedOptions(options, context = {}) {
381
446
  const secureMode = context.isSecureMode ?? isSecureMode;
382
447
  const warnings = [];
383
448
  const isHbomOnly = isHbomOnlyProjectTypes(options.projectType);
449
+ const includeFormulationBeforeProfiles = options.includeFormulation === true;
450
+ const introspectExplicitlyDisabled = options.introspect === false;
384
451
 
385
452
  // --- Profile expansion -------------------------------------------------
386
453
  switch (options.profile) {
@@ -412,6 +479,14 @@ export function applyAdvancedOptions(options, context = {}) {
412
479
  case "license-compliance":
413
480
  process.env.FETCH_LICENSE = "true";
414
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;
415
490
  case "ml-tiny":
416
491
  process.env.FETCH_LICENSE = "true";
417
492
  options.deep = false;
@@ -471,17 +546,29 @@ export function applyAdvancedOptions(options, context = {}) {
471
546
  }
472
547
  }
473
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
+
474
561
  // --- BOM audit → auto-formulation --------------------------------------
475
- if (options.bomAudit) {
562
+ const introspectProfileAddsFormulation =
563
+ options.profile === "introspect" && !includeFormulationBeforeProfiles;
564
+ if (options.bomAudit || introspectProfileAddsFormulation) {
476
565
  if (isHbomOnly) {
477
566
  // HBOM-only: skip formulation
478
567
  } else if (!options.includeFormulation) {
479
- warnings.push({
480
- level: "info",
481
- message:
482
- "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.",
483
- });
568
+ warnings.push(formulationPrivacyNotice());
484
569
  options.includeFormulation = true;
570
+ } else if (introspectProfileAddsFormulation) {
571
+ warnings.push(formulationPrivacyNotice());
485
572
  }
486
573
  }
487
574
 
package/lib/cli/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  recordActivity,
19
19
  setActivityContext,
20
20
  } from "../core/activity.js";
21
+ import { recordDegradation } from "../core/buildLedger.js";
21
22
  import { hasAnyProjectType, PROJECT_TYPE_ALIASES } from "../core/env.js";
22
23
  import {
23
24
  clearFileDiscoveryCache,
@@ -40,7 +41,7 @@ import {
40
41
  } from "../ecosystems/parsers-misc.js";
41
42
  import { isPyLockFile } from "../ecosystems/pylockutils.js";
42
43
  import {
43
- buildDependencyTrackBomPayload,
44
+ buildDependencyTrackBomRequest,
44
45
  getDependencyTrackBomApiUrl,
45
46
  } from "../helpers/remote/dependency-track.js";
46
47
  import { optionIncludesAiInventoryProjectType } from "../inventory/aiInventory.js";
@@ -173,6 +174,11 @@ export {
173
174
  createZigBom,
174
175
  } from "./nativeBom.js";
175
176
 
177
+ import {
178
+ deferFailOnError,
179
+ isDeferredFailOnError,
180
+ } from "../core/deferredExit.js";
181
+
176
182
  const dirName = dirNameStr;
177
183
 
178
184
  const isWin = _platform() === "win32";
@@ -816,6 +822,23 @@ export async function createMultiXBom(pathList, options) {
816
822
  // from here on, so repeated walks of them can share one view of the tree.
817
823
  setDirWalkCacheRoot(options.allLayersExplodedDir);
818
824
  const scanOnePath = async (path) => {
825
+ try {
826
+ return await scanOnePathInner(path);
827
+ } catch (err) {
828
+ if (!isDeferredFailOnError(err)) {
829
+ throw err;
830
+ }
831
+ // The failing path contributes nothing, and the remaining paths still
832
+ // do: fail-on-error scopes the loss to the leg that failed.
833
+ return {
834
+ components: [],
835
+ dependencies: [],
836
+ options: { ...sharedOptions },
837
+ parentSubComponents: [],
838
+ };
839
+ }
840
+ };
841
+ const scanOnePathInner = async (path) => {
819
842
  // Each path accumulates into its own lists and its own view of options, so
820
843
  // that legs which record a parent component or a tool cannot overwrite
821
844
  // another path's. The lists are merged below in pathList order.
@@ -1433,6 +1456,14 @@ export async function createMultiXBom(pathList, options) {
1433
1456
  );
1434
1457
  }
1435
1458
  setProjectTypeActivityContext("jar", path);
1459
+ recordDegradation("jvm.jar.heuristic", {
1460
+ ecosystem: "java",
1461
+ tool: "jar",
1462
+ impact: "versions",
1463
+ path,
1464
+ detail:
1465
+ "Components are being inferred by scanning jar/war/ear files from disk.",
1466
+ });
1436
1467
  bomData = await createJarBom(path, options);
1437
1468
  if (bomData?.bomJson?.components?.length) {
1438
1469
  thoughtLog(
@@ -2292,7 +2323,28 @@ export async function createHBom(_path, options) {
2292
2323
  * @param {Object} options Parse options from the cli
2293
2324
  * @returns {Promise<Object>} Promise resolving to BOM object
2294
2325
  */
2326
+ /**
2327
+ * Generate a BOM for one path, letting a deferred fail-on-error abort return
2328
+ * the empty BOM the flag promises rather than unwinding past the reports.
2329
+ *
2330
+ * @param {string} path Source path.
2331
+ * @param {Object} options CLI options.
2332
+ * @returns {Promise<Object>} BOM NS data.
2333
+ */
2295
2334
  export async function createBom(path, options) {
2335
+ try {
2336
+ return await createBomForPath(path, options);
2337
+ } catch (err) {
2338
+ if (!isDeferredFailOnError(err)) {
2339
+ throw err;
2340
+ }
2341
+ // fail-on-error forbids an incomplete result, so the extractor kept
2342
+ // nothing; the CLI still writes the reports and exits non-zero.
2343
+ return buildBomNSData(options, [], "", { src: path, filename: "" });
2344
+ }
2345
+ }
2346
+
2347
+ async function createBomForPath(path, options) {
2296
2348
  clearFileDiscoveryCache();
2297
2349
  let { projectType } = options;
2298
2350
  if (!projectType) {
@@ -2327,7 +2379,11 @@ export async function createBom(path, options) {
2327
2379
  console.log(
2328
2380
  `OS BOM generation has failed due to problems with exporting the image ${path}`,
2329
2381
  );
2330
- options.failOnError && process.exit(1);
2382
+ deferFailOnError(options, {
2383
+ ecosystem: "container",
2384
+ tool: "docker",
2385
+ detail: "exporting the image for the OS BOM failed",
2386
+ });
2331
2387
  return {};
2332
2388
  }
2333
2389
  isContainerMode = true;
@@ -2367,7 +2423,11 @@ export async function createBom(path, options) {
2367
2423
  console.log(
2368
2424
  `OCI BOM generation has failed due to problems with exporting the image ${path}.`,
2369
2425
  );
2370
- options.failOnError && process.exit(1);
2426
+ deferFailOnError(options, {
2427
+ ecosystem: "container",
2428
+ tool: "docker",
2429
+ detail: "exporting the image for the OCI BOM failed",
2430
+ });
2371
2431
  return {};
2372
2432
  }
2373
2433
  if (DEBUG_MODE) {
@@ -2660,8 +2720,8 @@ export async function submitBom(args, bomContents) {
2660
2720
  });
2661
2721
  return undefined;
2662
2722
  }
2663
- const bomPayload = buildDependencyTrackBomPayload(args, bomContents);
2664
- if (!bomPayload) {
2723
+ const bomRequest = buildDependencyTrackBomRequest(args, bomContents);
2724
+ if (!bomRequest) {
2665
2725
  console.log(
2666
2726
  "Invalid Dependency-Track submission arguments. Provide projectId or projectName (projectVersion defaults to main) and specify parent project either by UUID or by parent project name + version.",
2667
2727
  );
@@ -2674,21 +2734,21 @@ export async function submitBom(args, bomContents) {
2674
2734
  serverUrl,
2675
2735
  "params",
2676
2736
  args.projectName,
2677
- bomPayload.projectVersion,
2737
+ args.projectVersion || "main",
2678
2738
  );
2679
2739
  }
2680
2740
  try {
2681
2741
  const requestOptions = {
2682
- method: "PUT",
2742
+ method: "POST",
2683
2743
  followRedirect: false,
2684
2744
  https: {
2685
2745
  rejectUnauthorized: !args.skipDtTlsCheck,
2686
2746
  },
2687
2747
  headers: {
2688
2748
  "X-Api-Key": (args.apiKey || "").replace(/[\r\n]/g, ""),
2689
- "Content-Type": "application/json",
2749
+ "Content-Type": bomRequest.contentType,
2690
2750
  },
2691
- json: bomPayload,
2751
+ body: bomRequest.body,
2692
2752
  responseType: "json",
2693
2753
  context: {
2694
2754
  activityIntent: "bom-submit",
@@ -2711,47 +2771,15 @@ export async function submitBom(args, bomContents) {
2711
2771
  );
2712
2772
  } else if (error.response && error.response.statusCode === 405) {
2713
2773
  console.log(
2714
- "Method PUT not allowed on Dependency-Track server. Trying with POST ...",
2774
+ `The server at ${serverUrl} does not accept a multipart/form-data POST on /api/v1/bom. Check that the URL points to a Dependency-Track 4.x or later API server.`,
2775
+ );
2776
+ } else if (DEBUG_MODE) {
2777
+ console.log(
2778
+ "Unable to submit the SBOM to the Dependency-Track server",
2779
+ error,
2715
2780
  );
2716
- // Method not allowed errors
2717
- try {
2718
- return await cdxgenAgent(dependencyTrackApiUrl, {
2719
- method: "POST",
2720
- followRedirect: false,
2721
- https: {
2722
- rejectUnauthorized: !args.skipDtTlsCheck,
2723
- },
2724
- headers: {
2725
- "X-Api-Key": (args.apiKey || "").replace(/[\r\n]/g, ""),
2726
- "Content-Type": "application/json",
2727
- },
2728
- json: bomPayload,
2729
- responseType: "json",
2730
- context: {
2731
- activityIntent: "bom-submit",
2732
- },
2733
- }).json();
2734
- } catch (error) {
2735
- if (DEBUG_MODE) {
2736
- console.log(
2737
- "Unable to submit the SBOM to the Dependency-Track server using POST method",
2738
- error,
2739
- );
2740
- } else {
2741
- console.log(
2742
- "Unable to submit the SBOM to the Dependency-Track server using POST method",
2743
- );
2744
- }
2745
- }
2746
2781
  } else {
2747
- if (DEBUG_MODE) {
2748
- console.log(
2749
- "Unable to submit the SBOM to the Dependency-Track server using POST method",
2750
- error,
2751
- );
2752
- } else {
2753
- console.log("Unable to submit the SBOM to the Dependency-Track server");
2754
- }
2782
+ console.log("Unable to submit the SBOM to the Dependency-Track server");
2755
2783
  }
2756
2784
  // rethrow error as function is async and we should try to catch it in the caller
2757
2785
  throw error;
package/lib/cli/jsBom.js CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  readEnvironmentVariable,
12
12
  recordSensitiveFileRead,
13
13
  } from "../core/activity.js";
14
+ import { recordDegradation } from "../core/buildLedger.js";
15
+ import { deferFailOnError } from "../core/deferredExit.js";
14
16
  import { hasAnyProjectType, isPackageManagerAllowed } from "../core/env.js";
15
17
  import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
16
18
  import { thoughtLog } from "../core/logger.js";
@@ -529,6 +531,14 @@ export async function createNodejsBom(path, options) {
529
531
  isSecureMode &&
530
532
  !installArgs.join(" ").includes("--allow-git")
531
533
  ) {
534
+ recordDegradation("js.git-dependency", {
535
+ ecosystem: "npm",
536
+ tool: pkgMgr,
537
+ impact: "integrity",
538
+ command: `${pkgMgr} ${installArgs.join(" ")}`,
539
+ detail:
540
+ "Secure mode npm install may still fetch and install git dependencies.",
541
+ });
532
542
  console.log(
533
543
  "Consider passing '--allow-git=none' via the environment variable NPM_INSTALL_ARGS to prevent any git dependencies from being fetched and installed via npm.",
534
544
  );
@@ -580,6 +590,13 @@ export async function createNodejsBom(path, options) {
580
590
  `**PACKAGE MANAGER**: Let's run the '${pkgMgr}' command with the arguments '${installArgs.join(" ")}' to generate the needed lock files.`,
581
591
  );
582
592
  }
593
+ recordDegradation("js.no-lockfile", {
594
+ ecosystem: "npm",
595
+ tool: pkgMgr,
596
+ impact: "versions",
597
+ detail:
598
+ "No lockfile was found for this project, so the SBOM is generated without pinned dependency versions.",
599
+ });
583
600
  console.warn(
584
601
  "\x1b[1;35mNotice: Generating an SBOM without a lockfile is risky and non-deterministic. Consider generating and committing the lockfile to your repository to ensure reproducible builds and SBOMs.\x1b[0m",
585
602
  );
@@ -640,7 +657,11 @@ export async function createNodejsBom(path, options) {
640
657
  console.log("---------------");
641
658
  console.log(result.stderr);
642
659
  }
643
- options.failOnError && process.exit(1);
660
+ deferFailOnError(options, {
661
+ ecosystem: "npm",
662
+ tool: "npm",
663
+ detail: "the npm command failed",
664
+ });
644
665
  } else {
645
666
  anyInstallSuccess = true;
646
667
  }
@@ -936,7 +957,11 @@ export async function createNodejsBom(path, options) {
936
957
  );
937
958
  if (result.status === 1 || result.error) {
938
959
  console.error(result.stdout, result.stderr);
939
- options.failOnError && process.exit(1);
960
+ deferFailOnError(options, {
961
+ ecosystem: "npm",
962
+ tool: "npm",
963
+ detail: "the yarn or pnpm command failed",
964
+ });
940
965
  }
941
966
  }
942
967
  // Look for shrinkwrap file
@@ -1013,7 +1038,11 @@ export async function createNodejsBom(path, options) {
1013
1038
  pnpmLock,
1014
1039
  "was found!",
1015
1040
  );
1016
- options.failOnError && process.exit(1);
1041
+ deferFailOnError(options, {
1042
+ ecosystem: "npm",
1043
+ tool: "npm",
1044
+ detail: "no JavaScript dependency evidence could be collected",
1045
+ });
1017
1046
  }
1018
1047
  if (
1019
1048
  yarnLockFiles?.length &&