@cdxgen/cdxgen 13.2.0 → 13.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +28 -12
  2. package/bin/cdxgen.js +227 -119
  3. package/bin/evinse.js +5 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/remediations.json +24 -0
  9. package/lib/cli/bomAssembly.js +105 -15
  10. package/lib/cli/cliOptions.js +93 -1
  11. package/lib/cli/index.js +43 -4
  12. package/lib/cli/jsBom.js +49 -24
  13. package/lib/cli/jvmBom.js +46 -12
  14. package/lib/cli/managedBom.js +112 -41
  15. package/lib/cli/nativeBom.js +125 -14
  16. package/lib/core/env.js +5 -6
  17. package/lib/core/fs.js +174 -6
  18. package/lib/core/metadataProperties.js +97 -0
  19. package/lib/core/projectConfig.js +31 -4
  20. package/lib/ecosystems/bunutils.js +269 -101
  21. package/lib/ecosystems/caxa.js +434 -2
  22. package/lib/ecosystems/cmakeResolver.js +66 -0
  23. package/lib/ecosystems/cppEvidence.js +64 -31
  24. package/lib/ecosystems/ecosystems.js +19 -8
  25. package/lib/ecosystems/gradleutils.js +42 -7
  26. package/lib/ecosystems/jsEvidence.js +19 -20
  27. package/lib/ecosystems/kosi.js +10 -3
  28. package/lib/ecosystems/npmutils.js +110 -9
  29. package/lib/ecosystems/parsers-dotnet.js +51 -10
  30. package/lib/ecosystems/parsers-go.js +47 -9
  31. package/lib/ecosystems/parsers-js.js +171 -50
  32. package/lib/ecosystems/parsers-misc.js +747 -161
  33. package/lib/ecosystems/parsers-python.js +26 -14
  34. package/lib/ecosystems/parsers-rust.js +24 -18
  35. package/lib/ecosystems/rubyutils.js +1426 -166
  36. package/lib/ecosystems/rusi.js +32 -1
  37. package/lib/evinser/evinser.js +436 -128
  38. package/lib/evinser/swiftsem.js +1914 -302
  39. package/lib/helpers/bomSigner.js +652 -221
  40. package/lib/inventory/atomUtils.js +568 -31
  41. package/lib/inventory/cdxrs.js +103 -12
  42. package/lib/inventory/depsUtils.js +183 -11
  43. package/lib/inventory/display.js +4 -5
  44. package/lib/inventory/dosai.js +550 -13
  45. package/lib/inventory/dynamic.js +39 -1
  46. package/lib/inventory/evidenceUtils.js +14 -6
  47. package/lib/inventory/fetchBatch.js +116 -17
  48. package/lib/inventory/protobom.js +8 -1
  49. package/lib/inventory/purl.js +152 -3
  50. package/lib/inventory/sbomDocument.js +2 -70
  51. package/lib/inventory/source.js +37 -19
  52. package/lib/inventory/traceRunner.js +210 -21
  53. package/lib/managers/binary.js +115 -24
  54. package/lib/managers/docker.js +440 -20
  55. package/lib/managers/oci.js +39 -1
  56. package/lib/parsers/cmakeLists.js +452 -0
  57. package/lib/parsers/largeJson.js +454 -0
  58. package/lib/parsers/npmrc.js +19 -1
  59. package/lib/stages/postgen/postgen.js +68 -15
  60. package/lib/validator/bomValidator.js +6 -16
  61. package/lib/validator/index.js +48 -10
  62. package/lib/validator/reporters/console.js +3 -0
  63. package/lib/validator/reporters/json.js +1 -0
  64. package/lib/validator/reporters/sarif.js +7 -0
  65. package/package.json +20 -20
  66. package/types/lib/cli/bomAssembly.d.ts +2 -1
  67. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  68. package/types/lib/cli/cliOptions.d.ts +33 -1
  69. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  70. package/types/lib/cli/index.d.ts.map +1 -1
  71. package/types/lib/cli/jsBom.d.ts.map +1 -1
  72. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  73. package/types/lib/cli/managedBom.d.ts.map +1 -1
  74. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  75. package/types/lib/core/env.d.ts.map +1 -1
  76. package/types/lib/core/fs.d.ts +55 -0
  77. package/types/lib/core/fs.d.ts.map +1 -1
  78. package/types/lib/core/metadataProperties.d.ts +39 -0
  79. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  80. package/types/lib/core/projectConfig.d.ts +6 -2
  81. package/types/lib/core/projectConfig.d.ts.map +1 -1
  82. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  83. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  84. package/types/lib/ecosystems/caxa.d.ts +14 -0
  85. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  86. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  87. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  88. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  89. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  90. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  91. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  92. package/types/lib/ecosystems/gradleutils.d.ts +20 -4
  93. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  94. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  95. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  96. package/types/lib/ecosystems/kosi.d.ts +13 -0
  97. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  98. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  99. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  102. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  103. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  104. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  105. package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
  106. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  109. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  110. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  111. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  112. package/types/lib/ecosystems/rusi.d.ts +13 -0
  113. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  114. package/types/lib/evinser/evinser.d.ts +59 -9
  115. package/types/lib/evinser/evinser.d.ts.map +1 -1
  116. package/types/lib/evinser/swiftsem.d.ts +280 -15
  117. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  118. package/types/lib/helpers/bomSigner.d.ts +106 -13
  119. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  120. package/types/lib/inventory/atomUtils.d.ts +145 -3
  121. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  122. package/types/lib/inventory/cdxrs.d.ts +8 -3
  123. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts +15 -0
  125. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  126. package/types/lib/inventory/display.d.ts.map +1 -1
  127. package/types/lib/inventory/dosai.d.ts +64 -1
  128. package/types/lib/inventory/dosai.d.ts.map +1 -1
  129. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  130. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  131. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  132. package/types/lib/inventory/plugins.d.ts.map +1 -1
  133. package/types/lib/inventory/protobom.d.ts.map +1 -1
  134. package/types/lib/inventory/purl.d.ts +71 -2
  135. package/types/lib/inventory/purl.d.ts.map +1 -1
  136. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  137. package/types/lib/inventory/source.d.ts.map +1 -1
  138. package/types/lib/inventory/traceRunner.d.ts +47 -1
  139. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  140. package/types/lib/managers/binary.d.ts +30 -1
  141. package/types/lib/managers/binary.d.ts.map +1 -1
  142. package/types/lib/managers/docker.d.ts +29 -0
  143. package/types/lib/managers/docker.d.ts.map +1 -1
  144. package/types/lib/managers/oci.d.ts +14 -0
  145. package/types/lib/managers/oci.d.ts.map +1 -1
  146. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  147. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  148. package/types/lib/parsers/largeJson.d.ts +52 -0
  149. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  150. package/types/lib/parsers/npmrc.d.ts +5 -0
  151. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  152. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  153. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  154. package/types/lib/validator/index.d.ts +10 -1
  155. package/types/lib/validator/index.d.ts.map +1 -1
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  157. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  158. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -25,7 +25,10 @@ import {
25
25
  CDXGEN_VERSION,
26
26
  frameworksList,
27
27
  } from "../core/state.js";
28
- import { isPkgTypeOnlyImport } from "../ecosystems/npmutils.js";
28
+ import {
29
+ isPkgTypeOnlyImport,
30
+ setNpmCleanedVersionProperty,
31
+ } from "../ecosystems/npmutils.js";
29
32
  import { parsePackageJsonName } from "../ecosystems/parsers-js.js";
30
33
  import {
31
34
  AI_INSTRUCTION_FILE_KINDS,
@@ -50,8 +53,7 @@ import {
50
53
  import { enrichComponentWithMcpMetadata } from "../inventory/mcp.js";
51
54
  import {
52
55
  applyPurl,
53
- encodeForPurl,
54
- isValidPurl,
56
+ canonicalPurlFromLooseString,
55
57
  npmPurl,
56
58
  tryBuildPurl,
57
59
  } from "../inventory/purl.js";
@@ -182,6 +184,31 @@ export const determineParentComponent = (options) => {
182
184
  return parentComponent;
183
185
  };
184
186
 
187
+ /**
188
+ * Tells whether a `--project-name`/`--project-version` override names the very
189
+ * project the generator detected, rather than a larger project containing it.
190
+ *
191
+ * The override is typed `application` because it can name anything. When its
192
+ * name, group, and version match the detected parent, the two describe the same
193
+ * project, and the detected parent is the better description of it: it carries
194
+ * the ecosystem purl and the manifest metadata. Treating it as a subproject
195
+ * would list the project as a dependency of itself under a second purl.
196
+ *
197
+ * @param {object|undefined} overrideParent Parent built from the options
198
+ * @param {object|undefined} detectedParent Parent the generator derived
199
+ * @returns {boolean} true when both describe the same project
200
+ */
201
+ const overrideNamesDetectedParent = (overrideParent, detectedParent) => {
202
+ if (!overrideParent || !detectedParent?.purl) {
203
+ return false;
204
+ }
205
+ return (
206
+ overrideParent.name === detectedParent.name &&
207
+ (overrideParent.group || "") === (detectedParent.group || "") &&
208
+ `${overrideParent.version}` === `${detectedParent.version}`
209
+ );
210
+ };
211
+
185
212
  /**
186
213
  * Re-anchor a dependency graph onto a caller-supplied parent component.
187
214
  *
@@ -338,7 +365,8 @@ export const componentToSimpleFullName = (comp) => {
338
365
  /**
339
366
  * Strips transient keys (`evidence`, `_integrity`, `license`, `qualifiers`,
340
367
  * `repository`, `homepage`) from a parent component while preserving licenses
341
- * and external references (bug #1519).
368
+ * and external references (bug #1519). External references whose URL is not a
369
+ * valid IRI reference are dropped.
342
370
  *
343
371
  * @param {object} comp Parent component to clean in place
344
372
  * @returns {object} The cleaned parent component
@@ -356,10 +384,15 @@ export const cleanParentComponent = (comp) => {
356
384
  }
357
385
  delete comp.license;
358
386
  delete comp.qualifiers;
359
- if (comp.repository || comp.homepage) {
387
+ // Components get their references validated in listComponents. The parent
388
+ // skips that conversion, so validate here too: an unusable URL taken from a
389
+ // manifest must not make the whole document invalid.
390
+ if (comp.repository || comp.homepage || comp.externalReferences?.length) {
360
391
  const externalReferences = addExternalReferences(comp);
361
392
  if (externalReferences?.length) {
362
393
  comp.externalReferences = externalReferences;
394
+ } else {
395
+ delete comp.externalReferences;
363
396
  }
364
397
  }
365
398
  delete comp.repository;
@@ -454,12 +487,18 @@ export function addMetadata(parentComponent = {}, options = {}, context = {}) {
454
487
  // name-based bom-ref, and blindly copying that here manufactured invalid
455
488
  // purls such as `"purl": "swift-smoke"`. CycloneDX requires `purl` to be
456
489
  // a valid Package URL when present, so absent is the only correct answer.
490
+ // A bom-ref holds the decoded purl (`pkg:npm/app@1.0.0+build`), so it is
491
+ // split on its separators and rebuilt rather than escaped wholesale,
492
+ // which would also escape the `@` before the version.
457
493
  if (!parentComponent["purl"] && parentComponent["bom-ref"]) {
458
- const candidate = encodeForPurl(parentComponent["bom-ref"]);
459
- if (isValidPurl(candidate)) {
494
+ const candidate = canonicalPurlFromLooseString(
495
+ parentComponent["bom-ref"],
496
+ );
497
+ if (candidate) {
460
498
  parentComponent["purl"] = candidate;
461
499
  }
462
500
  }
501
+ setNpmCleanedVersionProperty(parentComponent);
463
502
  }
464
503
  if (parentComponent?.components) {
465
504
  parentComponent.components = listComponents(
@@ -841,6 +880,7 @@ export function addExternalReferences(opkg) {
841
880
  }
842
881
  }
843
882
  return externalReferences
883
+ .filter((reference) => typeof reference?.url === "string")
844
884
  .map((reference) => ({ ...reference, url: reference.url.trim() }))
845
885
  .filter((reference) => isValidIriReference(reference.url));
846
886
  }
@@ -1014,13 +1054,16 @@ export function addComponent(
1014
1054
  let purl = pkg.purl;
1015
1055
  if (!purl && ptype) {
1016
1056
  try {
1057
+ // Raw values: cdx-purl escapes each component itself, so escaping
1058
+ // them here first turned `@scope` into `%2540scope` and
1059
+ // `libstdc++6` into `libstdc%252B%252B6`.
1017
1060
  purl = new Purl({
1018
1061
  type: ptype,
1019
- namespace: encodeForPurl(group) || null,
1020
- name: encodeForPurl(name),
1062
+ namespace: group || null,
1063
+ name,
1021
1064
  version: version || null,
1022
1065
  qualifiers: pkg.qualifiers || null,
1023
- subpath: encodeForPurl(pkg.subpath) || null,
1066
+ subpath: pkg.subpath || null,
1024
1067
  });
1025
1068
  } catch {
1026
1069
  // purl construction may fail for types requiring a namespace (maven,
@@ -1071,9 +1114,10 @@ export function addComponent(
1071
1114
  } else {
1072
1115
  compScope = "required";
1073
1116
  }
1074
- } else if (impPkgs.length && compScope !== "excluded") {
1075
- compScope = "optional";
1076
1117
  }
1118
+ // A package missing from the imports keeps its scope. Usage analysis
1119
+ // only promotes: the ecosystem passes have already scoped the package
1120
+ // list, and a missing import is not evidence that a package is optional.
1077
1121
  }
1078
1122
  let component = {
1079
1123
  author,
@@ -1089,6 +1133,11 @@ export function addComponent(
1089
1133
  purl: purlString,
1090
1134
  externalReferences: addExternalReferences(pkg),
1091
1135
  };
1136
+ // A CPE from the collector (caxa records one for the bundled Node.js
1137
+ // runtime) is how NVD-based scanners match the component.
1138
+ if (typeof pkg.cpe === "string" && pkg.cpe.length) {
1139
+ component.cpe = pkg.cpe;
1140
+ }
1092
1141
  if (options.specVersion >= 1.5) {
1093
1142
  component.pedigree = pkg.pedigree || undefined;
1094
1143
  }
@@ -1102,6 +1151,13 @@ export function addComponent(
1102
1151
  // than bundled with the product. The schema defaults it to false, so it
1103
1152
  // is only worth emitting when true.
1104
1153
  component.isExternal = pkg.isExternal ? true : undefined;
1154
+ // A range describes an external dependency named without a resolved
1155
+ // version, such as the Ruby a gem requires. The schema allows it only
1156
+ // on an external component, and never next to a version.
1157
+ component.versionRange =
1158
+ pkg.isExternal && !pkg.version
1159
+ ? pkg.versionRange || undefined
1160
+ : undefined;
1105
1161
  }
1106
1162
  component["type"] = determinePackageType(pkg);
1107
1163
  // `version` is optional in every spec version cdxgen emits. An empty string
@@ -1131,9 +1187,16 @@ export function addComponent(
1131
1187
  if (options.specVersion >= 1.6 && component.author) {
1132
1188
  const authorsList = [];
1133
1189
  for (const aauthor of component.author.split(",")) {
1134
- authorsList.push({ name: aauthor });
1190
+ // Registries hand out comma separated lists without a consistent
1191
+ // spacing, so `A, B` and `A,B` must produce the same names
1192
+ const name = aauthor.trim();
1193
+ if (name.length) {
1194
+ authorsList.push({ name });
1195
+ }
1196
+ }
1197
+ if (authorsList.length) {
1198
+ component.authors = authorsList;
1135
1199
  }
1136
- component.authors = authorsList;
1137
1200
  delete component.author;
1138
1201
  }
1139
1202
  // Downgrade authors section for < 1.5 :(
@@ -1169,6 +1232,7 @@ export function addComponent(
1169
1232
  if (pkg.properties?.length) {
1170
1233
  component.properties = pkg.properties;
1171
1234
  }
1235
+ setNpmCleanedVersionProperty(component);
1172
1236
  if (isTypeOnlyPkg) {
1173
1237
  component.properties = component.properties || [];
1174
1238
  if (
@@ -1285,6 +1349,25 @@ export function determinePackageType(pkg) {
1285
1349
  ) {
1286
1350
  return "library";
1287
1351
  }
1352
+ // A gem is a framework only when the curated list names that very gem:
1353
+ // the prefix matches below took rails-html-sanitizer and sinatra-contrib
1354
+ // for their frameworks, and the bare words meant for other ecosystems
1355
+ // turned childprocess ("chi") and jquery-rails ("jquery") into
1356
+ // frameworks. Any other gem is a library, unless it ships executables,
1357
+ // which makes it an application (discussions 4405 and 4408). A
1358
+ // description or keyword that merely mentions a framework is no evidence
1359
+ // of one: every gem whose summary says "tracing framework" would
1360
+ // otherwise become a framework component.
1361
+ if (purl.type === "gem") {
1362
+ if (frameworksList.all.includes(`pkg:gem/${purl.name}`)) {
1363
+ return "framework";
1364
+ }
1365
+ const hasExecutables = pkg.properties?.some(
1366
+ (prop) =>
1367
+ prop.name === "cdx:gem:executables" && prop.value?.trim().length,
1368
+ );
1369
+ return hasExecutables ? "application" : "library";
1370
+ }
1288
1371
  for (const cf of frameworksList.all) {
1289
1372
  if (
1290
1373
  pkg.purl.startsWith(cf) ||
@@ -1466,7 +1549,14 @@ export const buildBomNSData = (options, pkgInfo, ptype, context) => {
1466
1549
  }
1467
1550
  const nsMapping = context.nsMapping || {};
1468
1551
  const services = context.services || [];
1469
- const overrideParent = determineParentComponent(options);
1552
+ let overrideParent = determineParentComponent(options);
1553
+ // A caller-supplied parentComponent is always honoured as given
1554
+ if (
1555
+ overrideParent !== options.parentComponent &&
1556
+ overrideNamesDetectedParent(overrideParent, context.parentComponent)
1557
+ ) {
1558
+ overrideParent = undefined;
1559
+ }
1470
1560
  const parentComponent = overrideParent || context.parentComponent;
1471
1561
  const metadata = addMetadata(parentComponent, options, context);
1472
1562
  const components = listComponents(options, allImports, pkgInfo, ptype);
@@ -15,6 +15,10 @@ import { join, resolve } from "node:path";
15
15
 
16
16
  import { isSecureMode, readEnvironmentVariable } from "../core/activity.js";
17
17
  import { PROJECT_TYPE_ALIASES } from "../core/env.js";
18
+ import {
19
+ METADATA_PROPERTY_CONFIG_KEYS,
20
+ parseMetadataProperties,
21
+ } from "../core/metadataProperties.js";
18
22
  import {
19
23
  DEFAULT_CDX_SPEC_VERSION,
20
24
  normalizeCycloneDxComponentTypeFilter,
@@ -120,6 +124,80 @@ export function isUserProvided(value, yaDefault) {
120
124
  return value !== undefined && value !== yaDefault;
121
125
  }
122
126
 
127
+ /**
128
+ * Combine the custom metadata properties of every source (discussion 4391):
129
+ * the `--metadata-property` flag, the CDXGEN_METADATA_PROPERTY and
130
+ * CDXGEN_METADATA_PROPERTIES environment variables, and a config file's
131
+ * `metadata-property` or `metadataProperties` entry.
132
+ *
133
+ * yargs would let the highest-precedence source replace the others outright,
134
+ * so each is read on its own and they are merged instead. A name set by more
135
+ * than one source takes its values from the one that wins in the usual order
136
+ * — the command line, then the environment, then the config file — while a
137
+ * source may repeat a name for a multi-valued property.
138
+ *
139
+ * @param {object} args Parsed yargs argv
140
+ * @param {object} [context]
141
+ * @param {string[]} [context.argv] Raw command-line arguments, which tell a
142
+ * flag on the command line from one yargs filled in from elsewhere
143
+ * @param {object} [context.projectConfig] The sanitized config file contents.
144
+ * Without it, a programmatic caller's `args` are taken as the config.
145
+ * @returns {{name: string, value: string}[]} The merged properties
146
+ */
147
+ export function resolveMetadataProperties(
148
+ args,
149
+ { argv = process.argv, projectConfig } = {},
150
+ ) {
151
+ const flagGiven = argv.some((argument) =>
152
+ ["--metadata-property", "--metadataProperty"].includes(
153
+ `${argument}`.split("=")[0],
154
+ ),
155
+ );
156
+ const fromConfig = projectConfig
157
+ ? METADATA_PROPERTY_CONFIG_KEYS.flatMap((key) =>
158
+ parseMetadataProperties(projectConfig[key]),
159
+ )
160
+ : flagGiven
161
+ ? []
162
+ : [
163
+ ...parseMetadataProperties(args?.metadataProperty),
164
+ ...parseMetadataProperties(args?.metadataProperties),
165
+ ];
166
+ const sources = [
167
+ flagGiven ? parseMetadataProperties(args?.metadataProperty) : [],
168
+ [
169
+ ...parseMetadataProperties(
170
+ readEnvironmentVariable("CDXGEN_METADATA_PROPERTY"),
171
+ ),
172
+ ...parseMetadataProperties(
173
+ readEnvironmentVariable("CDXGEN_METADATA_PROPERTIES"),
174
+ ),
175
+ ],
176
+ fromConfig,
177
+ ];
178
+ const properties = [];
179
+ const claimedNames = new Set();
180
+ for (const source of sources) {
181
+ const sourceNames = new Set();
182
+ for (const aprop of source) {
183
+ if (
184
+ claimedNames.has(aprop.name) ||
185
+ properties.some(
186
+ (prop) => prop.name === aprop.name && prop.value === aprop.value,
187
+ )
188
+ ) {
189
+ continue;
190
+ }
191
+ properties.push(aprop);
192
+ sourceNames.add(aprop.name);
193
+ }
194
+ for (const name of sourceNames) {
195
+ claimedNames.add(name);
196
+ }
197
+ }
198
+ return properties;
199
+ }
200
+
123
201
  /**
124
202
  * Apply command-name-based defaults to the raw parsed args. When cdxgen is
125
203
  * invoked via a symlink or alias (e.g. `obom`, `cbom`, `spdxgen`, `aibom`),
@@ -162,6 +240,8 @@ export function applyCommandNameDefaults(args, invokedCommandName) {
162
240
  * @param {object} context
163
241
  * @param {string} context.filePath Resolved source path
164
242
  * @param {boolean} context.isRemoteOrPurl True when source is a URL or purl
243
+ * @param {string[]} [context.argv] Raw command-line arguments
244
+ * @param {object} [context.projectConfig] Sanitized config file contents
165
245
  * @returns {object} The initial options object
166
246
  */
167
247
  /**
@@ -221,8 +301,12 @@ function resolveIntrospectOption(args, argv) {
221
301
 
222
302
  export function buildInitialOptions(
223
303
  args,
224
- { filePath, isRemoteOrPurl, argv = process.argv },
304
+ { filePath, isRemoteOrPurl, argv = process.argv, projectConfig },
225
305
  ) {
306
+ const metadataProperties = resolveMetadataProperties(args, {
307
+ argv,
308
+ projectConfig,
309
+ });
226
310
  const res = Object.assign({}, args, {
227
311
  projectType: args.type,
228
312
  multiProject: args.recurse,
@@ -235,6 +319,9 @@ export function buildInitialOptions(
235
319
  include: args.include || args.includeRegex,
236
320
  noIgnore: args.noIgnore,
237
321
  gitAiNotesRef: args.gitAiNotesRef || "refs/notes/ai",
322
+ metadataProperties: metadataProperties.length
323
+ ? metadataProperties
324
+ : undefined,
238
325
  // Introspection: the flag or the documented environment opt-in. An
239
326
  // explicit --no-introspect stays `false` and beats both, while an
240
327
  // unmentioned flag stays undefined so a profile can still enable it.
@@ -598,6 +685,9 @@ export function applyAdvancedOptions(options, context = {}) {
598
685
  * passed `--spec-version`
599
686
  * @param {boolean} [context.isDryRun=false] Dry-run mode
600
687
  * @param {boolean} [context.isSecureMode] Secure mode
688
+ * @param {object} [context.projectConfig] Sanitized config file contents,
689
+ * whose metadata properties merge
690
+ * with the other sources'
601
691
  * @returns {{ options: object, warnings: OptionsWarning[] }}
602
692
  */
603
693
  export function buildOptionsFromArgs(args, context) {
@@ -607,12 +697,14 @@ export function buildOptionsFromArgs(args, context) {
607
697
  isRemoteOrPurl,
608
698
  userSetSpecVersion,
609
699
  isDryRun = false,
700
+ projectConfig,
610
701
  } = context;
611
702
 
612
703
  const expandedArgs = applyCommandNameDefaults(args, invokedCommandName);
613
704
  const options = buildInitialOptions(expandedArgs, {
614
705
  filePath,
615
706
  isRemoteOrPurl,
707
+ projectConfig,
616
708
  });
617
709
  const phase3Warnings = applyPostConstructionOverrides(options, {
618
710
  invokedCommandName,
package/lib/cli/index.js CHANGED
@@ -84,6 +84,7 @@ import {
84
84
  exportArchive,
85
85
  exportImage,
86
86
  getPkgPathList,
87
+ isImageReference,
87
88
  parseImageName,
88
89
  } from "../managers/docker.js";
89
90
  import {
@@ -1900,17 +1901,49 @@ export async function createDynamicBom(path, options) {
1900
1901
  blockExec: options.traceBlockExec,
1901
1902
  traceCrypto: options.traceCrypto,
1902
1903
  cbom: options.cbom,
1904
+ proxyEgress: options.traceProxyEgress,
1905
+ allowLoopback: options.traceAllowLoopback,
1906
+ dryRun: options.traceDryRun,
1907
+ policy: options.tracePolicy,
1908
+ policyFile: options.tracePolicyFile,
1909
+ blockInterpreters: options.traceBlockInterpreters,
1910
+ denyPersistenceWrites: options.traceDenyPersistenceWrites,
1911
+ privateTmp: options.tracePrivateTmp,
1912
+ protectHome: options.traceProtectHome,
1903
1913
  };
1904
1914
 
1905
- const { components: pkgList, services: traceServices } =
1906
- await buildDynamicComponents(commandStr, workingDir, traceOptions);
1915
+ const {
1916
+ components: pkgList,
1917
+ services: traceServices,
1918
+ dryRunSummary,
1919
+ } = await buildDynamicComponents(commandStr, workingDir, traceOptions);
1907
1920
 
1908
- return buildBomNSData(options, pkgList, "generic", {
1921
+ const bomNSData = buildBomNSData(options, pkgList, "generic", {
1909
1922
  src: workingDir,
1910
1923
  projectType: "dynamic",
1911
1924
  filename: commandStr,
1912
1925
  services: traceServices,
1913
1926
  });
1927
+ // A dry run denies every side effect, so the attempted-operation counts are
1928
+ // the result. Only counts are recorded, never the paths or targets.
1929
+ if (dryRunSummary && bomNSData?.bomJson?.metadata) {
1930
+ const metadata = bomNSData.bomJson.metadata;
1931
+ metadata.properties = metadata.properties || [];
1932
+ metadata.properties.push({ name: "cdx:dynamic:dryRun", value: "true" });
1933
+ metadata.properties.push({
1934
+ name: "cdx:dynamic:dryRun:captured",
1935
+ value: dryRunSummary.captured === false ? "false" : "true",
1936
+ });
1937
+ for (const [key, value] of Object.entries(dryRunSummary)) {
1938
+ if (Number.isInteger(value)) {
1939
+ metadata.properties.push({
1940
+ name: `cdx:dynamic:dryRun:${key}`,
1941
+ value: String(value),
1942
+ });
1943
+ }
1944
+ }
1945
+ }
1946
+ return bomNSData;
1914
1947
  }
1915
1948
 
1916
1949
  /**
@@ -2561,7 +2594,13 @@ async function createBomForPath(path, options) {
2561
2594
  } else if (
2562
2595
  (options.projectType &&
2563
2596
  !options.projectType?.includes("universal") &&
2564
- hasAnyProjectType(["oci"], options, false)) ||
2597
+ hasAnyProjectType(["oci"], options, false) &&
2598
+ // Explicit container types accept every documented image reference
2599
+ // style (postgres:15-alpine, shiftleft/scan-slim, registry:5000/ns/img),
2600
+ // but only for inputs that cannot be a local path: existing files and
2601
+ // directories keep flowing to the tar, oci-dir and source scanners.
2602
+ !safeExistsSync(path) &&
2603
+ isImageReference(path)) ||
2565
2604
  path.startsWith("docker.io") ||
2566
2605
  path.startsWith("quay.io") ||
2567
2606
  path.startsWith("ghcr.io") ||
package/lib/cli/jsBom.js CHANGED
@@ -24,7 +24,10 @@ import {
24
24
  rewriteExtractedArchivePaths,
25
25
  } from "../ecosystems/asarutils.js";
26
26
  import { parseBunLock } from "../ecosystems/bunutils.js";
27
- import { parseCaxaMetadata } from "../ecosystems/caxa.js";
27
+ import {
28
+ enrichCaxaMetadataFromApp,
29
+ parseCaxaMetadata,
30
+ } from "../ecosystems/caxa.js";
28
31
  import {
29
32
  CHROME_EXTENSION_PURL_TYPE,
30
33
  collectChromeExtensionsFromPath,
@@ -77,6 +80,7 @@ import {
77
80
  } from "../inventory/analyzer.js";
78
81
  import {
79
82
  markNpmTypesPackagesAsExcluded,
83
+ markTypeOnlyDependenciesOptional,
80
84
  mergeDependencies,
81
85
  mergeServices,
82
86
  propagateRequiredScopeFromDependencies,
@@ -567,7 +571,16 @@ export async function createNodejsBom(path, options) {
567
571
  recordSensitiveFileRead(npmrcFile, {
568
572
  label: "npm registry configuration",
569
573
  });
570
- const npmrcObj = parseNpmrc(npmrcData);
574
+ let npmrcObj = {};
575
+ try {
576
+ npmrcObj = parseNpmrc(npmrcData);
577
+ } catch (e) {
578
+ // A malformed .npmrc must degrade to a warning, never abort the scan.
579
+ console.warn(
580
+ `Unable to parse ${npmrcFile}. Continuing without it. ${e?.message || ""}`,
581
+ );
582
+ npmrcObj = {};
583
+ }
571
584
  for (const [key, value] of Object.entries(npmrcObj)) {
572
585
  const baseKey = key.replace(/^(?:\/\/[^/]+\/|@[^:]+:)/, "");
573
586
  if (
@@ -957,8 +970,12 @@ export async function createNodejsBom(path, options) {
957
970
  ) {
958
971
  // Rush.js creates node_modules inside common/temp directory
959
972
  const nmDir = join(path, "common", "temp", "node_modules");
960
- // Do rush install if we don't have node_modules directory
961
- if (!safeExistsSync(nmDir)) {
973
+ // Do rush install if we don't have node_modules directory. `rush install`
974
+ // drives pnpm without cdxgen's --ignore-scripts hardening and
975
+ // --bypass-policy disables rush's own policy gate, so it must respect
976
+ // --no-install-deps and the pre-build lifecycle like every other
977
+ // installer.
978
+ if (!safeExistsSync(nmDir) && options.installDeps !== false) {
962
979
  console.log("Executing 'rush install --no-link'", path);
963
980
  const result = safeSpawnSync(
964
981
  "rush",
@@ -1036,6 +1053,7 @@ export async function createNodejsBom(path, options) {
1036
1053
  }
1037
1054
  pkgList = propagateRequiredScopeFromDependencies(pkgList, dependencies);
1038
1055
  pkgList = markNpmTypesPackagesAsExcluded(pkgList);
1056
+ pkgList = markTypeOnlyDependenciesOptional(pkgList, dependencies);
1039
1057
  return buildBomNSData(options, pkgList, "npm", {
1040
1058
  allImports,
1041
1059
  allExports,
@@ -1120,25 +1138,25 @@ export async function createNodejsBom(path, options) {
1120
1138
  );
1121
1139
  workspacePackages.push(workspaceRef);
1122
1140
  workspaceSrcFiles[workspaceRef] = relativePkgJsonFile;
1123
- if (pkgData?.dependencies || pkgData?.devDependencies) {
1124
- const dlist = new Set();
1125
- for (const [k, v] of Object.entries({
1126
- ...(pkgData.dependencies || {}),
1127
- ...(pkgData.devDependencies || {}),
1128
- })) {
1129
- if (v.match(/^\d/)) {
1130
- dlist.add(`pkg:npm/${k}@${v}`);
1131
- } else {
1132
- dlist.add(`pkg:npm/${k}`);
1133
- }
1134
- }
1135
- workspaceDirectDeps[workspaceRef] = Array.from(dlist);
1136
- for (const dep of workspaceDirectDeps[workspaceRef]) {
1137
- if (!depsWorkspaceRefs[dep]) {
1138
- depsWorkspaceRefs[dep] = [];
1139
- }
1140
- depsWorkspaceRefs[dep].push(workspaceRef);
1141
+ // The declared name and range of each direct dependency, which
1142
+ // parseYarnLock resolves against the lock file to link the
1143
+ // member into the dependency tree.
1144
+ const declaredDeps = {
1145
+ ...(pkgData.dependencies || {}),
1146
+ ...(pkgData.optionalDependencies || {}),
1147
+ ...(pkgData.devDependencies || {}),
1148
+ };
1149
+ if (Object.keys(declaredDeps).length) {
1150
+ workspaceDirectDeps[workspaceRef] = declaredDeps;
1151
+ }
1152
+ for (const [k, v] of Object.entries(declaredDeps)) {
1153
+ const dep = /^\d/.test(String(v))
1154
+ ? `pkg:npm/${k}@${v}`
1155
+ : `pkg:npm/${k}`;
1156
+ if (!depsWorkspaceRefs[dep]) {
1157
+ depsWorkspaceRefs[dep] = [];
1141
1158
  }
1159
+ depsWorkspaceRefs[dep].push(workspaceRef);
1142
1160
  }
1143
1161
  }
1144
1162
  }
@@ -1495,6 +1513,7 @@ export async function createNodejsBom(path, options) {
1495
1513
  );
1496
1514
  pkgList = propagateRequiredScopeFromDependencies(pkgList, dependencies);
1497
1515
  pkgList = markNpmTypesPackagesAsExcluded(pkgList);
1516
+ pkgList = markTypeOnlyDependenciesOptional(pkgList, dependencies);
1498
1517
  if (exactAiInventoryType === "mcp") {
1499
1518
  pkgList = trimComponents(filterInventorySubjectsByTypes(pkgList, ["mcp"]));
1500
1519
  dependencies = filterInventoryDependencies(
@@ -1624,8 +1643,14 @@ export async function createCaxaBom(path, options) {
1624
1643
  if (DEBUG_MODE) {
1625
1644
  console.log(`Parsing ${mfile}`);
1626
1645
  }
1627
- const { parentComponent, components, dependencies } =
1628
- await parseCaxaMetadata(mfile);
1646
+ let mdata = await parseCaxaMetadata(mfile);
1647
+ if (options.caxaAppDir) {
1648
+ mdata = await enrichCaxaMetadataFromApp(
1649
+ mdata,
1650
+ resolve(options.caxaAppDir),
1651
+ );
1652
+ }
1653
+ const { parentComponent, components, dependencies } = mdata;
1629
1654
  if (parentComponent) {
1630
1655
  if (!finalParentComponent) {
1631
1656
  finalParentComponent = parentComponent;