@cyclonedx/cdxgen 12.2.0 → 12.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 (181) hide show
  1. package/README.md +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. package/types/bin/licenses.d.ts.map +0 -1
@@ -54,10 +54,32 @@ import { parse as _load, parseAllDocuments } from "yaml";
54
54
  import { getTreeWithPlugin } from "../managers/piptree.js";
55
55
  import { IriValidationStrategy, validateIri } from "../parsers/iri.js";
56
56
  import Arborist from "../third-party/arborist/lib/index.js";
57
+ import { analyzeSuspiciousJsFile } from "./analyzer.js";
57
58
  import { parseWorkflowFile } from "./ciParsers/githubActions.js";
58
59
  import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
59
60
  import { thoughtLog, traceLog } from "./logger.js";
61
+ import { createLolbasProperties } from "./lolbas.js";
62
+ import {
63
+ createOsQueryPurl,
64
+ deriveOsQueryDescription,
65
+ deriveOsQueryName,
66
+ deriveOsQueryPublisher,
67
+ deriveOsQueryVersion,
68
+ sanitizeOsQueryIdentity,
69
+ } from "./osqueryTransform.js";
70
+ import {
71
+ collectPyLockFileComponents,
72
+ collectPyLockPackageProperties,
73
+ collectPyLockTopLevelProperties,
74
+ getPyLockPackages,
75
+ isDefaultPypiRegistry,
76
+ isPyLockObject,
77
+ } from "./pylockutils.js";
60
78
  import { get_python_command_from_env, getVenvMetadata } from "./pythonutils.js";
79
+ import {
80
+ collectNpmRegistryProvenanceProperties,
81
+ collectPypiRegistryProvenanceProperties,
82
+ } from "./registryProvenance.js";
61
83
 
62
84
  let url = import.meta?.url;
63
85
  if (url && !url.startsWith("file://")) {
@@ -229,16 +251,37 @@ export function safeSpawnSync(command, args, options) {
229
251
  if (!options.timeout) {
230
252
  options.timeout = TIMEOUT_MS;
231
253
  }
254
+ // Emit certain operational warnings only once per process to keep audit logs readable.
255
+ const emitNoticeOnce = (noticeKey, message, level = "warn") => {
256
+ if (!globalThis.__cdxgenNoticeCache) {
257
+ globalThis.__cdxgenNoticeCache = new Set();
258
+ }
259
+ if (globalThis.__cdxgenNoticeCache.has(noticeKey)) {
260
+ return;
261
+ }
262
+ globalThis.__cdxgenNoticeCache.add(noticeKey);
263
+ if (level === "log") {
264
+ console.log(message);
265
+ return;
266
+ }
267
+ console.warn(message);
268
+ };
232
269
  // Check for -S for python invocations in secure mode
233
270
  if (command.includes("python") && (!args?.length || args[0] !== "-S")) {
234
271
  if (isSecureMode) {
235
- console.warn(
272
+ emitNoticeOnce(
273
+ "python-without-S-secure",
236
274
  "\x1b[1;35mNotice: Running python command without '-S' argument. This is a bug in cdxgen. Please report with an example repo here https://github.com/cdxgen/cdxgen/issues.\x1b[0m",
237
275
  );
238
276
  } else if (process.env?.CDXGEN_IN_CONTAINER === "true") {
239
- console.log("Running python command without '-S' argument.");
277
+ emitNoticeOnce(
278
+ "python-without-S-container",
279
+ "Running python command without '-S' argument.",
280
+ "log",
281
+ );
240
282
  } else {
241
- console.warn(
283
+ emitNoticeOnce(
284
+ "python-without-S-host",
242
285
  "\x1b[1;35mNotice: Running python command without '-S' argument. Only run cdxgen in trusted directories to prevent auto-executing local scripts.\x1b[0m",
243
286
  );
244
287
  }
@@ -265,14 +308,20 @@ export function safeSpawnSync(command, args, options) {
265
308
  );
266
309
  if (!hasOnlyBinary) {
267
310
  if (isSecureMode) {
268
- console.warn(
311
+ emitNoticeOnce(
312
+ "pip-without-only-binary-secure",
269
313
  "\x1b[1;31mSecurity Alert: pip/uv install invoked without '--only-binary' argument in secure mode. This is a bug in cdxgen and introduces Arbitrary Code Execution (ACE) risks. Please report with an example repo here https://github.com/cdxgen/cdxgen/issues.\x1b[0m",
270
314
  );
271
315
  } else if (process.env?.CDXGEN_IN_CONTAINER === "true") {
272
- console.log("Running pip/uv install without '--only-binary' argument.");
316
+ emitNoticeOnce(
317
+ "pip-without-only-binary-container",
318
+ "Running pip/uv install without '--only-binary' argument.",
319
+ "log",
320
+ );
273
321
  } else {
274
- console.warn(
275
- "\x1b[1;35mNotice: pip/uv install invoked without '--only-binary'. This allows executing untrusted setup.py scripts. Only run cdxgen in trusted directories.\x1b",
322
+ emitNoticeOnce(
323
+ "pip-without-only-binary-host",
324
+ "\x1b[1;35mNotice: pip/uv install invoked without '--only-binary'. This allows executing untrusted setup.py scripts. Only run cdxgen in trusted directories.\x1b[0m",
276
325
  );
277
326
  }
278
327
  }
@@ -337,6 +386,15 @@ export const DEBUG_MODE =
337
386
  ["debug", "verbose"].includes(process.env.CDXGEN_DEBUG_MODE) ||
338
387
  process.env.SCAN_DEBUG_MODE === "debug";
339
388
 
389
+ export const CDXGEN_SPDX_CREATED_BY = process.env.CDXGEN_SPDX_CREATED_BY;
390
+
391
+ // Table border style for console output.
392
+ export const TABLE_BORDER_STYLE = ["ascii", "unicode", "auto"].includes(
393
+ `${process.env.CDXGEN_TABLE_BORDER || ""}`.toLowerCase(),
394
+ )
395
+ ? `${process.env.CDXGEN_TABLE_BORDER}`.toLowerCase()
396
+ : "auto";
397
+
340
398
  // Timeout milliseconds. Default 20 mins
341
399
  export const TIMEOUT_MS =
342
400
  Number.parseInt(process.env.CDXGEN_TIMEOUT_MS, 10) || 20 * 60 * 1000;
@@ -383,6 +441,19 @@ export function shouldFetchLicense() {
383
441
  );
384
442
  }
385
443
 
444
+ /**
445
+ * Determines whether remote package metadata should be fetched for enrichment.
446
+ *
447
+ * @returns {boolean} True when registry metadata enrichment is enabled.
448
+ */
449
+ export function shouldFetchPackageMetadata() {
450
+ return (
451
+ shouldFetchLicense() ||
452
+ (process.env.CDXGEN_FETCH_PKG_METADATA &&
453
+ ["true", "1"].includes(process.env.CDXGEN_FETCH_PKG_METADATA))
454
+ );
455
+ }
456
+
386
457
  /**
387
458
  * Determines whether VCS (version control system) information should be fetched
388
459
  * for Go packages, based on the GO_FETCH_VCS environment variable.
@@ -679,6 +750,12 @@ export const PROJECT_TYPE_ALIASES = {
679
750
  "vscode-extensions",
680
751
  "ide-extensions",
681
752
  ],
753
+ "chrome-extension": [
754
+ "chrome-extension",
755
+ "chrome-extensions",
756
+ "chromium-extension",
757
+ "chromium-extensions",
758
+ ],
682
759
  };
683
760
 
684
761
  // Package manager aliases
@@ -1120,7 +1197,44 @@ export function getLicenses(pkg) {
1120
1197
  licenseContent.name = l;
1121
1198
  }
1122
1199
  } else if (Object.keys(l).length) {
1123
- licenseContent = l;
1200
+ licenseContent = { ...l };
1201
+ if (
1202
+ licenseContent.type &&
1203
+ !licenseContent.id &&
1204
+ !licenseContent.name &&
1205
+ !licenseContent.expression
1206
+ ) {
1207
+ if (spdxLicenses.includes(licenseContent.type)) {
1208
+ licenseContent.id = licenseContent.type;
1209
+ } else if (isSpdxLicenseExpression(licenseContent.type)) {
1210
+ licenseContent.expression = licenseContent.type;
1211
+ } else {
1212
+ licenseContent.name = licenseContent.type;
1213
+ }
1214
+ }
1215
+ if (
1216
+ !licenseContent.id &&
1217
+ !licenseContent.name &&
1218
+ !licenseContent.expression &&
1219
+ licenseContent.url?.startsWith("http")
1220
+ ) {
1221
+ const knownLicense = getKnownLicense(licenseContent.url, pkg);
1222
+ if (knownLicense) {
1223
+ if (knownLicense.id) {
1224
+ licenseContent.id = knownLicense.id;
1225
+ } else if (knownLicense.name) {
1226
+ licenseContent.name = knownLicense.name;
1227
+ }
1228
+ }
1229
+ }
1230
+ if (
1231
+ !licenseContent.id &&
1232
+ !licenseContent.name &&
1233
+ !licenseContent.expression
1234
+ ) {
1235
+ licenseContent.name = "CUSTOM";
1236
+ }
1237
+ delete licenseContent.type;
1124
1238
  } else {
1125
1239
  return undefined;
1126
1240
  }
@@ -1331,6 +1445,10 @@ export async function getNpmMetadata(pkgList) {
1331
1445
  if (body.homepage) {
1332
1446
  p.homepage = { url: body.homepage };
1333
1447
  }
1448
+ p.properties = p.properties || [];
1449
+ p.properties.push(
1450
+ ...collectNpmRegistryProvenanceProperties(body, p.version),
1451
+ );
1334
1452
  cdepList.push(p);
1335
1453
  } catch (_err) {
1336
1454
  cdepList.push(p);
@@ -1349,6 +1467,382 @@ export async function getNpmMetadata(pkgList) {
1349
1467
  * @param {boolean} simple Return a simpler representation of the component by skipping extended attributes and license fetch.
1350
1468
  * @param {boolean} securityProps Collect security-related properties
1351
1469
  */
1470
+ const NPM_INSTALL_HOOK_NAMES = [
1471
+ "preinstall",
1472
+ "install",
1473
+ "postinstall",
1474
+ "prepublish",
1475
+ "prepare",
1476
+ ];
1477
+
1478
+ const NPM_LIFECYCLE_OBFUSCATION_PATTERNS = [
1479
+ [
1480
+ "base64-decode",
1481
+ /\b(?:base64(?:\s+--decode|\s+-d)?|openssl\s+enc\s+-base64\s+-d)\b/i,
1482
+ ],
1483
+ ["buffer-base64", /Buffer\.from\s*\([^)]*,\s*["']base64["']\s*\)/i],
1484
+ ["atob", /\batob\s*\(/i],
1485
+ ["string-from-char-code", /\bString\.fromCharCode\s*\(/i],
1486
+ ["long-base64-literal", /\b[A-Za-z0-9+/]{80,}={0,2}\b/],
1487
+ ];
1488
+
1489
+ const NPM_LIFECYCLE_EXECUTION_PATTERNS = [
1490
+ ["node-eval", /\bnode\b[^\n]*\s-[ep]\b/i],
1491
+ ["eval", /\beval\s*\(/i],
1492
+ ["function-constructor", /\b(?:new\s+Function|Function\s*\()/i],
1493
+ [
1494
+ "child-process",
1495
+ /\b(?:child_process|node:child_process|execSync|execFileSync|spawnSync|execFile|spawn|exec)\b/i,
1496
+ ],
1497
+ [
1498
+ "shell-inline",
1499
+ /\b(?:sh|bash|cmd|powershell|pwsh)\b[^\n]*\s-(?:c|Command|EncodedCommand)\b/i,
1500
+ ],
1501
+ ];
1502
+
1503
+ const NPM_LIFECYCLE_NETWORK_PATTERNS = [
1504
+ ["curl", /\bcurl\b/i],
1505
+ ["wget", /\bwget\b/i],
1506
+ ["invoke-webrequest", /\b(?:invoke-webrequest|iwr)\b/i],
1507
+ ["http-url", /https?:\/\//i],
1508
+ ];
1509
+
1510
+ const NPM_LIFECYCLE_JS_RUNNERS = new Set([
1511
+ "babel-node",
1512
+ "node",
1513
+ "ts-node",
1514
+ "tsx",
1515
+ "bun",
1516
+ "deno",
1517
+ ]);
1518
+
1519
+ const NPM_LIFECYCLE_JS_RUNNER_VALUE_OPTIONS = new Set([
1520
+ "-c",
1521
+ "-e",
1522
+ "-p",
1523
+ "-r",
1524
+ "--config",
1525
+ "--conditions",
1526
+ "--cwd-file",
1527
+ "--compilerOptions",
1528
+ "--cwd",
1529
+ "--env-file",
1530
+ "--env-file-if-exists",
1531
+ "--eval",
1532
+ "--experimental-loader",
1533
+ "--ignore",
1534
+ "--import",
1535
+ "--import-map",
1536
+ "--input-type",
1537
+ "--inspect",
1538
+ "--inspect-brk",
1539
+ "--inspect-port",
1540
+ "--loader",
1541
+ "--print",
1542
+ "--preload",
1543
+ "--project",
1544
+ "--require",
1545
+ "--test-name-pattern",
1546
+ "--test-reporter",
1547
+ "--test-reporter-destination",
1548
+ "--test-shard",
1549
+ "--title",
1550
+ "--tsconfig",
1551
+ "--watch-path",
1552
+ ]);
1553
+
1554
+ const NPM_LIFECYCLE_JS_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/i;
1555
+ const SHELL_ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
1556
+
1557
+ function splitLifecycleScriptCommands(scriptValue) {
1558
+ const commands = [];
1559
+ let current = "";
1560
+ let escaped = false;
1561
+ let quoteChar = "";
1562
+
1563
+ for (let index = 0; index < scriptValue.length; index++) {
1564
+ const character = scriptValue[index];
1565
+ if (escaped) {
1566
+ current += character;
1567
+ escaped = false;
1568
+ continue;
1569
+ }
1570
+ if (character === "\\") {
1571
+ current += character;
1572
+ escaped = true;
1573
+ continue;
1574
+ }
1575
+ if (quoteChar) {
1576
+ current += character;
1577
+ if (character === quoteChar) {
1578
+ quoteChar = "";
1579
+ }
1580
+ continue;
1581
+ }
1582
+ if (character === '"' || character === "'") {
1583
+ current += character;
1584
+ quoteChar = character;
1585
+ continue;
1586
+ }
1587
+ if (character === ";" || character === "|") {
1588
+ if (current.trim()) {
1589
+ commands.push(current.trim());
1590
+ }
1591
+ current = "";
1592
+ continue;
1593
+ }
1594
+ if (character === "&") {
1595
+ if (current.trim()) {
1596
+ commands.push(current.trim());
1597
+ }
1598
+ current = "";
1599
+ if (scriptValue[index + 1] === "&") {
1600
+ index += 1;
1601
+ }
1602
+ continue;
1603
+ }
1604
+ current += character;
1605
+ }
1606
+ if (current.trim()) {
1607
+ commands.push(current.trim());
1608
+ }
1609
+ return commands;
1610
+ }
1611
+
1612
+ function lifecycleRunnerOptionConsumesValue(token) {
1613
+ if (!token?.startsWith("-") || token === "--") {
1614
+ return false;
1615
+ }
1616
+ if (token.includes("=")) {
1617
+ return false;
1618
+ }
1619
+ if (token.startsWith("--")) {
1620
+ return NPM_LIFECYCLE_JS_RUNNER_VALUE_OPTIONS.has(token);
1621
+ }
1622
+ if (token.length > 2) {
1623
+ return false;
1624
+ }
1625
+ return NPM_LIFECYCLE_JS_RUNNER_VALUE_OPTIONS.has(token);
1626
+ }
1627
+
1628
+ function isLifecycleScriptSourceFile(token) {
1629
+ if (!token || token.startsWith("-")) {
1630
+ return false;
1631
+ }
1632
+ const fileToken = token.split(/[?#]/u, 1)[0];
1633
+ return NPM_LIFECYCLE_JS_SOURCE_FILE_PATTERN.test(fileToken);
1634
+ }
1635
+
1636
+ function findLifecycleScriptSourceArg(tokens, startIndex) {
1637
+ for (let index = startIndex; index < tokens.length; index++) {
1638
+ const token = tokens[index]?.trim();
1639
+ if (!token) {
1640
+ continue;
1641
+ }
1642
+ if (token === "--" || SHELL_ENV_ASSIGNMENT_PATTERN.test(token)) {
1643
+ continue;
1644
+ }
1645
+ if (token.startsWith("-")) {
1646
+ if (
1647
+ lifecycleRunnerOptionConsumesValue(token) &&
1648
+ index + 1 < tokens.length
1649
+ ) {
1650
+ index += 1;
1651
+ }
1652
+ continue;
1653
+ }
1654
+ if (isLifecycleScriptSourceFile(token)) {
1655
+ return token;
1656
+ }
1657
+ break;
1658
+ }
1659
+ return undefined;
1660
+ }
1661
+
1662
+ function findLifecycleScriptSourceStartIndex(tokens, runnerIndex) {
1663
+ const runnerToken = tokens[runnerIndex];
1664
+ for (let index = runnerIndex + 1; index < tokens.length; index++) {
1665
+ const token = tokens[index]?.trim();
1666
+ if (!token || token === "--" || SHELL_ENV_ASSIGNMENT_PATTERN.test(token)) {
1667
+ continue;
1668
+ }
1669
+ if (token.startsWith("-")) {
1670
+ if (
1671
+ lifecycleRunnerOptionConsumesValue(token) &&
1672
+ index + 1 < tokens.length
1673
+ ) {
1674
+ index += 1;
1675
+ }
1676
+ continue;
1677
+ }
1678
+ if (runnerToken === "deno") {
1679
+ return token === "run" ? index + 1 : undefined;
1680
+ }
1681
+ if (runnerToken === "bun" && token === "run") {
1682
+ return index + 1;
1683
+ }
1684
+ return index;
1685
+ }
1686
+ return undefined;
1687
+ }
1688
+
1689
+ function isResolvedPathWithinDirectory(baseDir, resolvedFile) {
1690
+ const relativePath = relative(baseDir, resolvedFile);
1691
+ if (!relativePath) {
1692
+ return true;
1693
+ }
1694
+ return (
1695
+ !relativePath.startsWith(`..${path.sep}`) &&
1696
+ relativePath !== ".." &&
1697
+ !path.isAbsolute(relativePath)
1698
+ );
1699
+ }
1700
+
1701
+ function collectLifecyclePatternIndicators(scriptValue, patterns) {
1702
+ const indicators = [];
1703
+ patterns.forEach(([name, pattern]) => {
1704
+ if (pattern.test(scriptValue)) {
1705
+ indicators.push(name);
1706
+ }
1707
+ });
1708
+ return indicators;
1709
+ }
1710
+
1711
+ function extractLifecycleScriptSourceFiles(pkgJsonFile, scriptValue) {
1712
+ const sourceFiles = [];
1713
+ const seen = new Set();
1714
+ if (!scriptValue || typeof scriptValue !== "string") {
1715
+ return sourceFiles;
1716
+ }
1717
+ const pkgJsonDir = resolve(dirname(pkgJsonFile));
1718
+ const commandSegments = splitLifecycleScriptCommands(scriptValue);
1719
+ for (const commandSegment of commandSegments) {
1720
+ const tokens = splitCommandArgs(commandSegment);
1721
+ for (let index = 0; index < tokens.length; index++) {
1722
+ if (!NPM_LIFECYCLE_JS_RUNNERS.has(tokens[index])) {
1723
+ continue;
1724
+ }
1725
+ const scriptStartIndex = findLifecycleScriptSourceStartIndex(
1726
+ tokens,
1727
+ index,
1728
+ );
1729
+ if (scriptStartIndex === undefined) {
1730
+ continue;
1731
+ }
1732
+ const relativeFile = findLifecycleScriptSourceArg(
1733
+ tokens,
1734
+ scriptStartIndex,
1735
+ );
1736
+ if (!relativeFile) {
1737
+ continue;
1738
+ }
1739
+ const resolvedFile = resolve(pkgJsonDir, relativeFile);
1740
+ if (
1741
+ !isResolvedPathWithinDirectory(pkgJsonDir, resolvedFile) ||
1742
+ !safeExistsSync(resolvedFile) ||
1743
+ seen.has(resolvedFile)
1744
+ ) {
1745
+ continue;
1746
+ }
1747
+ seen.add(resolvedFile);
1748
+ sourceFiles.push(resolvedFile);
1749
+ }
1750
+ }
1751
+ return sourceFiles;
1752
+ }
1753
+
1754
+ function analyzeNpmLifecycleScripts(pkgJsonFile, scripts) {
1755
+ const executionIndicators = new Set();
1756
+ const networkIndicators = new Set();
1757
+ const obfuscationIndicators = new Set();
1758
+ const obfuscatedScripts = [];
1759
+ const suspiciousScripts = [];
1760
+ const scriptIndicatorMap = [];
1761
+ const riskyScripts = NPM_INSTALL_HOOK_NAMES.filter(
1762
+ (scriptName) => scripts?.[scriptName],
1763
+ );
1764
+ riskyScripts.forEach((scriptName) => {
1765
+ const scriptValue = String(scripts[scriptName] || "");
1766
+ const scriptExecutionIndicators = new Set(
1767
+ collectLifecyclePatternIndicators(
1768
+ scriptValue,
1769
+ NPM_LIFECYCLE_EXECUTION_PATTERNS,
1770
+ ),
1771
+ );
1772
+ const scriptNetworkIndicators = new Set(
1773
+ collectLifecyclePatternIndicators(
1774
+ scriptValue,
1775
+ NPM_LIFECYCLE_NETWORK_PATTERNS,
1776
+ ),
1777
+ );
1778
+ const scriptObfuscationIndicators = new Set(
1779
+ collectLifecyclePatternIndicators(
1780
+ scriptValue,
1781
+ NPM_LIFECYCLE_OBFUSCATION_PATTERNS,
1782
+ ),
1783
+ );
1784
+ extractLifecycleScriptSourceFiles(pkgJsonFile, scriptValue).forEach(
1785
+ (sourceFile) => {
1786
+ const astIndicators = analyzeSuspiciousJsFile(sourceFile);
1787
+ astIndicators.executionIndicators.forEach((indicator) => {
1788
+ scriptExecutionIndicators.add(`ast:${indicator}`);
1789
+ });
1790
+ astIndicators.networkIndicators.forEach((indicator) => {
1791
+ scriptNetworkIndicators.add(`ast:${indicator}`);
1792
+ });
1793
+ astIndicators.obfuscationIndicators.forEach((indicator) => {
1794
+ scriptObfuscationIndicators.add(`ast:${indicator}`);
1795
+ });
1796
+ },
1797
+ );
1798
+ if (scriptObfuscationIndicators.size) {
1799
+ scriptExecutionIndicators.forEach((indicator) => {
1800
+ executionIndicators.add(indicator);
1801
+ });
1802
+ scriptObfuscationIndicators.forEach((indicator) => {
1803
+ obfuscationIndicators.add(indicator);
1804
+ });
1805
+ }
1806
+ if (
1807
+ scriptObfuscationIndicators.size &&
1808
+ (scriptExecutionIndicators.size || scriptNetworkIndicators.size)
1809
+ ) {
1810
+ obfuscatedScripts.push(scriptName);
1811
+ }
1812
+ if (
1813
+ scriptObfuscationIndicators.size ||
1814
+ (scriptExecutionIndicators.size && scriptNetworkIndicators.size)
1815
+ ) {
1816
+ suspiciousScripts.push(scriptName);
1817
+ }
1818
+ scriptNetworkIndicators.forEach((indicator) => {
1819
+ networkIndicators.add(indicator);
1820
+ });
1821
+ if (
1822
+ scriptExecutionIndicators.size ||
1823
+ scriptNetworkIndicators.size ||
1824
+ scriptObfuscationIndicators.size
1825
+ ) {
1826
+ scriptIndicatorMap.push(
1827
+ `${scriptName}:${[
1828
+ ...scriptObfuscationIndicators,
1829
+ ...scriptExecutionIndicators,
1830
+ ...scriptNetworkIndicators,
1831
+ ].join("+")}`,
1832
+ );
1833
+ }
1834
+ });
1835
+ return {
1836
+ executionIndicators: Array.from(executionIndicators).sort(),
1837
+ networkIndicators: Array.from(networkIndicators).sort(),
1838
+ obfuscatedScripts: [...new Set(obfuscatedScripts)].sort(),
1839
+ obfuscationIndicators: Array.from(obfuscationIndicators).sort(),
1840
+ riskyScripts,
1841
+ scriptIndicatorMap: scriptIndicatorMap.sort(),
1842
+ suspiciousScripts: [...new Set(suspiciousScripts)].sort(),
1843
+ };
1844
+ }
1845
+
1352
1846
  export async function parsePkgJson(
1353
1847
  pkgJsonFile,
1354
1848
  simple = false,
@@ -1438,24 +1932,70 @@ export async function parsePkgJson(
1438
1932
  // Track lifecycle scripts (preinstall, postinstall, etc. - code execution risk)
1439
1933
  if (pkgData.scripts && Object.keys(pkgData.scripts).length) {
1440
1934
  const scriptNames = Object.keys(pkgData.scripts).join(", ");
1935
+ const lifecycleAnalysis = analyzeNpmLifecycleScripts(
1936
+ pkgJsonFile,
1937
+ pkgData.scripts,
1938
+ );
1441
1939
  apkg.properties.push({
1442
1940
  name: "cdx:npm:scripts",
1443
1941
  value: scriptNames,
1444
1942
  });
1445
1943
  // Flag high-risk scripts specifically
1446
- const riskyScripts = [
1447
- "preinstall",
1448
- "install",
1449
- "postinstall",
1450
- "prepublish",
1451
- "prepare",
1452
- ].filter((script) => pkgData.scripts[script]);
1944
+ const riskyScripts = lifecycleAnalysis.riskyScripts;
1453
1945
  if (riskyScripts.length) {
1946
+ apkg.properties.push({
1947
+ name: "cdx:npm:hasInstallScript",
1948
+ value: "true",
1949
+ });
1454
1950
  apkg.properties.push({
1455
1951
  name: "cdx:npm:risky_scripts",
1456
1952
  value: riskyScripts.join(", "),
1457
1953
  });
1458
1954
  }
1955
+ if (lifecycleAnalysis.suspiciousScripts.length) {
1956
+ apkg.properties.push({
1957
+ name: "cdx:npm:hasSuspiciousLifecycleScript",
1958
+ value: "true",
1959
+ });
1960
+ apkg.properties.push({
1961
+ name: "cdx:npm:suspiciousLifecycleScripts",
1962
+ value: lifecycleAnalysis.suspiciousScripts.join(", "),
1963
+ });
1964
+ }
1965
+ if (lifecycleAnalysis.obfuscatedScripts.length) {
1966
+ apkg.properties.push({
1967
+ name: "cdx:npm:hasObfuscatedLifecycleScript",
1968
+ value: "true",
1969
+ });
1970
+ apkg.properties.push({
1971
+ name: "cdx:npm:obfuscatedLifecycleScripts",
1972
+ value: lifecycleAnalysis.obfuscatedScripts.join(", "),
1973
+ });
1974
+ }
1975
+ if (lifecycleAnalysis.obfuscationIndicators.length) {
1976
+ apkg.properties.push({
1977
+ name: "cdx:npm:lifecycleObfuscationIndicators",
1978
+ value: lifecycleAnalysis.obfuscationIndicators.join(", "),
1979
+ });
1980
+ }
1981
+ if (lifecycleAnalysis.executionIndicators.length) {
1982
+ apkg.properties.push({
1983
+ name: "cdx:npm:lifecycleExecutionIndicators",
1984
+ value: lifecycleAnalysis.executionIndicators.join(", "),
1985
+ });
1986
+ }
1987
+ if (lifecycleAnalysis.networkIndicators.length) {
1988
+ apkg.properties.push({
1989
+ name: "cdx:npm:lifecycleNetworkIndicators",
1990
+ value: lifecycleAnalysis.networkIndicators.join(", "),
1991
+ });
1992
+ }
1993
+ if (lifecycleAnalysis.scriptIndicatorMap.length) {
1994
+ apkg.properties.push({
1995
+ name: "cdx:npm:lifecycleIndicatorMap",
1996
+ value: lifecycleAnalysis.scriptIndicatorMap.join(" | "),
1997
+ });
1998
+ }
1459
1999
  }
1460
2000
  // Track platform/architecture constraints
1461
2001
  if (pkgData.cpu && Array.isArray(pkgData.cpu) && pkgData.cpu.length) {
@@ -1522,10 +2062,10 @@ export async function parsePkgJson(
1522
2062
  // continue regardless of error
1523
2063
  }
1524
2064
  }
1525
- if (!simple && shouldFetchLicense() && pkgList?.length) {
2065
+ if (!simple && shouldFetchPackageMetadata() && pkgList?.length) {
1526
2066
  if (DEBUG_MODE) {
1527
2067
  console.log(
1528
- `About to fetch license information for ${pkgList.length} packages in parsePkgJson`,
2068
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgJson`,
1529
2069
  );
1530
2070
  }
1531
2071
  return await getNpmMetadata(pkgList);
@@ -1572,7 +2112,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1572
2112
  const srcFilePath = node.path.includes(`${_sep}node_modules`)
1573
2113
  ? node.path.split(`${_sep}node_modules`)[0]
1574
2114
  : node.path;
1575
- const scope = node.dev === true ? "optional" : undefined;
2115
+ const isDevelopmentNode = node.dev === true || node.devOptional === true;
2116
+ const scope =
2117
+ isDevelopmentNode || node.optional === true ? "optional" : undefined;
1576
2118
  const integrity = node.integrity ? node.integrity : undefined;
1577
2119
 
1578
2120
  let pkg;
@@ -1645,6 +2187,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1645
2187
  purl: purlString,
1646
2188
  "bom-ref": decodeURIComponent(purlString),
1647
2189
  };
2190
+ if (isDevelopmentNode) {
2191
+ _setNpmDevelopmentProperty(pkg);
2192
+ }
1648
2193
  if (node.resolved) {
1649
2194
  if (node.resolved.startsWith("file:")) {
1650
2195
  pkg.properties.push({
@@ -2080,10 +2625,10 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
2080
2625
  options,
2081
2626
  ));
2082
2627
 
2083
- if (shouldFetchLicense() && pkgList?.length) {
2628
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
2084
2629
  if (DEBUG_MODE) {
2085
2630
  console.log(
2086
- `About to fetch license information for ${pkgList.length} packages in parsePkgLock`,
2631
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgLock`,
2087
2632
  );
2088
2633
  }
2089
2634
  pkgList = await getNpmMetadata(pkgList);
@@ -2713,10 +3258,10 @@ export async function parseYarnLock(
2713
3258
  }
2714
3259
  }
2715
3260
 
2716
- if (shouldFetchLicense() && pkgList?.length) {
3261
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
2717
3262
  if (DEBUG_MODE) {
2718
3263
  console.log(
2719
- `About to fetch license information for ${pkgList.length} packages in parseYarnLock`,
3264
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parseYarnLock`,
2720
3265
  );
2721
3266
  }
2722
3267
  pkgList = await getNpmMetadata(pkgList);
@@ -2790,10 +3335,10 @@ export async function parseNodeShrinkwrap(swFile) {
2790
3335
  }
2791
3336
  }
2792
3337
  }
2793
- if (shouldFetchLicense() && pkgList?.length) {
3338
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
2794
3339
  if (DEBUG_MODE) {
2795
3340
  console.log(
2796
- `About to fetch license information for ${pkgList.length} packages in parseNodeShrinkwrap`,
3341
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parseNodeShrinkwrap`,
2797
3342
  );
2798
3343
  }
2799
3344
  return await getNpmMetadata(pkgList);
@@ -2826,6 +3371,51 @@ function _markTreeOptional(
2826
3371
  }
2827
3372
  }
2828
3373
 
3374
+ function _markTreeDevelopment(
3375
+ dbomRef,
3376
+ dependenciesMap,
3377
+ possibleDevelopmentDeps,
3378
+ visited,
3379
+ ) {
3380
+ // Production-required packages set this map entry to false, and that wins
3381
+ // over any later attempt to propagate a development-only marking.
3382
+ if (possibleDevelopmentDeps[dbomRef] === undefined) {
3383
+ possibleDevelopmentDeps[dbomRef] = true;
3384
+ }
3385
+ if (dependenciesMap[dbomRef] && !visited[dbomRef]) {
3386
+ visited[dbomRef] = true;
3387
+ for (const eachDep of dependenciesMap[dbomRef]) {
3388
+ // Undefined means we have not classified this dependency yet, so we
3389
+ // continue propagating the dev-only marking unless it was already proven
3390
+ // to be non-development via a false entry.
3391
+ if (possibleDevelopmentDeps[eachDep] !== false) {
3392
+ _markTreeDevelopment(
3393
+ eachDep,
3394
+ dependenciesMap,
3395
+ possibleDevelopmentDeps,
3396
+ visited,
3397
+ );
3398
+ }
3399
+ }
3400
+ }
3401
+ }
3402
+
3403
+ function _setNpmDevelopmentProperty(pkg) {
3404
+ if (!pkg.properties) {
3405
+ pkg.properties = [];
3406
+ }
3407
+ if (
3408
+ !pkg.properties.some((property) => {
3409
+ return property.name === "cdx:npm:package:development";
3410
+ })
3411
+ ) {
3412
+ pkg.properties.push({
3413
+ name: "cdx:npm:package:development",
3414
+ value: "true",
3415
+ });
3416
+ }
3417
+ }
3418
+
2829
3419
  function _setTreeWorkspaceRef(
2830
3420
  dependenciesMap,
2831
3421
  depref,
@@ -3201,6 +3791,7 @@ export async function parsePnpmLock(
3201
3791
  // See: #1163
3202
3792
  // Moreover, we have changed >= 9 for >= 6
3203
3793
  // See: discussion #1359
3794
+ const possibleDevelopmentDeps = {};
3204
3795
  const possibleOptionalDeps = {};
3205
3796
  const dependenciesMap = {};
3206
3797
  let ppurl = "";
@@ -3293,7 +3884,7 @@ export async function parsePnpmLock(
3293
3884
  null,
3294
3885
  null,
3295
3886
  ).toString();
3296
- possibleOptionalDeps[decodeURIComponent(dpurl)] = true;
3887
+ possibleDevelopmentDeps[decodeURIComponent(dpurl)] = true;
3297
3888
  }
3298
3889
  // Find the root optional and peer dependencies
3299
3890
  for (const rdk of Object.keys({ ...rootOptionalDeps, ...rootPeerDeps })) {
@@ -3317,6 +3908,7 @@ export async function parsePnpmLock(
3317
3908
  null,
3318
3909
  ).toString();
3319
3910
  possibleOptionalDeps[decodeURIComponent(dpurl)] = true;
3911
+ possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
3320
3912
  }
3321
3913
  // Find the root direct dependencies
3322
3914
  for (const dk of Object.keys(rootDirectDeps)) {
@@ -3344,6 +3936,7 @@ export async function parsePnpmLock(
3344
3936
  // These are direct dependencies so cannot be optional
3345
3937
  possibleOptionalDeps[decodeURIComponent(dpurl)] = false;
3346
3938
  }
3939
+ possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
3347
3940
  }
3348
3941
  // pnpm-lock.yaml contains more than root dependencies in importers
3349
3942
  // we do what we did above but for all the other components
@@ -3469,6 +4062,7 @@ export async function parsePnpmLock(
3469
4062
  // This is a definite dependency of this component
3470
4063
  comDepList.add(depRef);
3471
4064
  possibleOptionalDeps[depRef] = false;
4065
+ possibleDevelopmentDeps[depRef] = false;
3472
4066
  // Track the package.json files
3473
4067
  if (pkgSrcFile) {
3474
4068
  if (!srcFilesMap[depRef]) {
@@ -3488,7 +4082,7 @@ export async function parsePnpmLock(
3488
4082
  null,
3489
4083
  ).toString();
3490
4084
  const devDpRef = decodeURIComponent(dpurl);
3491
- possibleOptionalDeps[devDpRef] = true;
4085
+ possibleDevelopmentDeps[devDpRef] = true;
3492
4086
  // This is also a dependency of this component
3493
4087
  comDepList.add(devDpRef);
3494
4088
  }
@@ -3506,6 +4100,7 @@ export async function parsePnpmLock(
3506
4100
  null,
3507
4101
  ).toString();
3508
4102
  possibleOptionalDeps[decodeURIComponent(dpurl)] = true;
4103
+ possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
3509
4104
  }
3510
4105
  dependenciesList.push({
3511
4106
  ref: decodeURIComponent(compPurl),
@@ -3708,7 +4303,21 @@ export async function parsePnpmLock(
3708
4303
  null,
3709
4304
  ).toString();
3710
4305
  const bomRef = decodeURIComponent(purlString);
4306
+ if (
4307
+ packageNode.dev === true &&
4308
+ possibleDevelopmentDeps[bomRef] === undefined
4309
+ ) {
4310
+ possibleDevelopmentDeps[bomRef] = true;
4311
+ }
3711
4312
  const isBaseOptional = possibleOptionalDeps[bomRef];
4313
+ // optionalDependencies are tracked separately because they may still
4314
+ // be runtime-relevant and should keep the CycloneDX optional scope.
4315
+ // packageNode.dev captures explicit dev-only packages from the lock
4316
+ // entry, while possibleDevelopmentDeps lets that marking propagate to
4317
+ // transitive dependencies discovered through the dependency graph.
4318
+ const isBaseDevelopment =
4319
+ packageNode.dev === true ||
4320
+ possibleDevelopmentDeps[bomRef] === true;
3712
4321
  const deplist = [];
3713
4322
  for (let dpkgName of Object.keys(deps)) {
3714
4323
  let vers = deps[dpkgName];
@@ -3751,6 +4360,18 @@ export async function parsePnpmLock(
3751
4360
  {},
3752
4361
  );
3753
4362
  }
4363
+ if (
4364
+ isBaseDevelopment &&
4365
+ possibleDevelopmentDeps[dbomRef] === undefined
4366
+ ) {
4367
+ possibleDevelopmentDeps[dbomRef] = true;
4368
+ _markTreeDevelopment(
4369
+ dbomRef,
4370
+ dependenciesMap,
4371
+ possibleDevelopmentDeps,
4372
+ {},
4373
+ );
4374
+ }
3754
4375
  }
3755
4376
  if (!dependenciesMap[bomRef]) {
3756
4377
  dependenciesMap[bomRef] = [];
@@ -3910,6 +4531,19 @@ export async function parsePnpmLock(
3910
4531
  }
3911
4532
  }
3912
4533
  }
4534
+ // Repeat development dependency detection after the dependency graph is fully
4535
+ // built, since a single package iteration can encounter a dev-only component
4536
+ // before its own dependency list has been captured in dependenciesMap.
4537
+ for (const dependencyRef of Object.keys(possibleDevelopmentDeps)) {
4538
+ if (possibleDevelopmentDeps[dependencyRef] === true) {
4539
+ _markTreeDevelopment(
4540
+ dependencyRef,
4541
+ dependenciesMap,
4542
+ possibleDevelopmentDeps,
4543
+ {},
4544
+ );
4545
+ }
4546
+ }
3913
4547
 
3914
4548
  // Problem: We might have over aggressively marked a package as optional even it is both required and optional
3915
4549
  // The below loops ensure required packages continue to stay required
@@ -3940,6 +4574,15 @@ export async function parsePnpmLock(
3940
4574
  if (requiredDependencies[apkg["bom-ref"]]) {
3941
4575
  apkg.scope = undefined;
3942
4576
  }
4577
+ if (
4578
+ !requiredDependencies[apkg["bom-ref"]] &&
4579
+ possibleDevelopmentDeps[apkg["bom-ref"]]
4580
+ ) {
4581
+ if (!apkg.scope) {
4582
+ apkg.scope = "optional";
4583
+ }
4584
+ _setNpmDevelopmentProperty(apkg);
4585
+ }
3943
4586
  if (possibleAliasesRefs[apkg["bom-ref"]]) {
3944
4587
  apkg.properties.push({
3945
4588
  name: "cdx:pnpm:alias",
@@ -4011,10 +4654,10 @@ export async function parsePnpmLock(
4011
4654
  pkgList = await pnpmMetadata(pkgList, pnpmLock);
4012
4655
  }
4013
4656
 
4014
- if (shouldFetchLicense() && pkgList?.length) {
4657
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
4015
4658
  if (DEBUG_MODE) {
4016
4659
  console.log(
4017
- `About to fetch license information for ${pkgList.length} packages in parsePnpmLock`,
4660
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parsePnpmLock`,
4018
4661
  );
4019
4662
  }
4020
4663
  pkgList = await getNpmMetadata(pkgList);
@@ -4072,10 +4715,10 @@ export async function parseBowerJson(bowerJsonFile) {
4072
4715
  // continue regardless of error
4073
4716
  }
4074
4717
  }
4075
- if (shouldFetchLicense() && pkgList?.length) {
4718
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
4076
4719
  if (DEBUG_MODE) {
4077
4720
  console.log(
4078
- `About to fetch license information for ${pkgList.length} packages in parseBowerJson`,
4721
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parseBowerJson`,
4079
4722
  );
4080
4723
  }
4081
4724
  return await getNpmMetadata(pkgList);
@@ -4170,10 +4813,10 @@ export async function parseMinJs(minJsFile) {
4170
4813
  // continue regardless of error
4171
4814
  }
4172
4815
  }
4173
- if (shouldFetchLicense() && pkgList?.length) {
4816
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
4174
4817
  if (DEBUG_MODE) {
4175
4818
  console.log(
4176
- `About to fetch license information for ${pkgList.length} packages in parseMinJs`,
4819
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parseMinJs`,
4177
4820
  );
4178
4821
  }
4179
4822
  return await getNpmMetadata(pkgList);
@@ -5665,7 +6308,7 @@ export function guessPypiMatchingVersion(versionsList, versionSpecifiers) {
5665
6308
  * @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
5666
6309
  */
5667
6310
  export async function getPyMetadata(pkgList, fetchDepsInfo) {
5668
- if (!shouldFetchLicense() && !fetchDepsInfo) {
6311
+ if (!shouldFetchPackageMetadata() && !fetchDepsInfo) {
5669
6312
  return pkgList;
5670
6313
  }
5671
6314
  const PYPI_URL = process.env.PYPI_URL || "https://pypi.org/pypi/";
@@ -5841,6 +6484,10 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
5841
6484
  null,
5842
6485
  null,
5843
6486
  ).toString();
6487
+ p.properties = p.properties || [];
6488
+ p.properties.push(
6489
+ ...collectPypiRegistryProvenanceProperties(body, p.version),
6490
+ );
5844
6491
  p.purl = purlString;
5845
6492
  p["bom-ref"] = decodeURIComponent(purlString);
5846
6493
  cdepList.push(p);
@@ -6243,9 +6890,9 @@ export function parsePyProjectTomlFile(tomlFile) {
6243
6890
  }
6244
6891
 
6245
6892
  /**
6246
- * Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock.
6893
+ * Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock, and pylock.toml.
6247
6894
  *
6248
- * @param {Object} lockData JSON data from poetry.lock, pdm.lock, or uv.lock file
6895
+ * @param {string} lockData Raw TOML text from poetry.lock, pdm.lock, uv.lock, or pylock.toml
6249
6896
  * @param {string} lockFile Lock file name for evidence
6250
6897
  * @param {string} pyProjectFile pyproject.toml file
6251
6898
  */
@@ -6262,6 +6909,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6262
6909
  let workspacePaths;
6263
6910
  let workspaceWarningShown = false;
6264
6911
  let hasWorkspaces = false;
6912
+ let pyLockProperties = [];
6265
6913
  // Keep track of any workspace components to be added to the parent component
6266
6914
  const workspaceComponentMap = {};
6267
6915
  const workspacePyProjMap = {};
@@ -6390,7 +7038,17 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6390
7038
  }
6391
7039
  }
6392
7040
  }
6393
- for (const apkg of lockTomlObj.package || []) {
7041
+ const pyLockMode = isPyLockObject(lockTomlObj);
7042
+ if (pyLockMode) {
7043
+ pyLockProperties = collectPyLockTopLevelProperties(lockTomlObj);
7044
+ if (parentComponent) {
7045
+ parentComponent.properties = parentComponent.properties || [];
7046
+ parentComponent.properties =
7047
+ parentComponent.properties.concat(pyLockProperties);
7048
+ }
7049
+ }
7050
+ const packageEntries = getPyLockPackages(lockTomlObj);
7051
+ for (const apkg of packageEntries) {
6394
7052
  // This avoids validation errors with uv.lock
6395
7053
  if (parentComponent?.name && parentComponent.name === apkg.name) {
6396
7054
  continue;
@@ -6410,10 +7068,19 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6410
7068
  if (apkg.optional) {
6411
7069
  pkg.scope = "optional";
6412
7070
  }
6413
- if (apkg["python-versions"]) {
7071
+ // poetry/pdm/uv use "python-versions", while pylock (PEP 751) uses "requires-python".
7072
+ // Prefer the existing lock-family field when both are present.
7073
+ const requiresPython = apkg["python-versions"] || apkg["requires-python"];
7074
+ if (requiresPython) {
6414
7075
  pkg.properties.push({
6415
7076
  name: "cdx:pypi:requiresPython",
6416
- value: apkg["python-versions"],
7077
+ value: requiresPython,
7078
+ });
7079
+ }
7080
+ if (apkg.index && !isDefaultPypiRegistry(apkg.index)) {
7081
+ pkg.properties.push({
7082
+ name: "cdx:pypi:registry",
7083
+ value: apkg.index,
6417
7084
  });
6418
7085
  }
6419
7086
  if (apkg?.source) {
@@ -6439,6 +7106,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6439
7106
  });
6440
7107
  }
6441
7108
  }
7109
+ if (pyLockMode) {
7110
+ pkg.properties = pkg.properties.concat(
7111
+ collectPyLockPackageProperties(apkg),
7112
+ );
7113
+ }
6442
7114
  // Is this component a module?
6443
7115
  if (workspaceComponentMap[pkg.name]) {
6444
7116
  pkg.properties.push({
@@ -6526,6 +7198,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6526
7198
  });
6527
7199
  }
6528
7200
  }
7201
+ if (pyLockMode) {
7202
+ const pylockFileComponents = collectPyLockFileComponents(apkg, lockFile);
7203
+ if (pylockFileComponents.length) {
7204
+ pkg.components = (pkg.components || []).concat(pylockFileComponents);
7205
+ }
7206
+ }
6529
7207
  if (
6530
7208
  directDepsKeys[pkg.name] ||
6531
7209
  (hasWorkspaces && !Object.keys(workspaceComponentMap).length)
@@ -6582,13 +7260,14 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6582
7260
  // Example: "msgpack>=0.5.2"
6583
7261
  const nameStr =
6584
7262
  apkgDep.name || apkgDep.split(/(==|<=|~=|>=)/)[0].split(" ")[0];
6585
- depsMap[pkg["bom-ref"]].add(existingPkgMap[nameStr] || nameStr);
7263
+ // Python package names are normalized/case-insensitive; support both forms for lookup.
7264
+ const nameLower = nameStr.toLowerCase();
7265
+ const depPkgRef =
7266
+ existingPkgMap[nameLower] || existingPkgMap[nameStr];
7267
+ depsMap[pkg["bom-ref"]].add(depPkgRef || nameStr);
6586
7268
  // Propagate the workspace properties to the child components
6587
- if (
6588
- existingPkgMap[nameStr] &&
6589
- pkgBomRefMap[existingPkgMap[nameStr]]
6590
- ) {
6591
- const dependentPkg = pkgBomRefMap[existingPkgMap[nameStr]];
7269
+ if (depPkgRef && pkgBomRefMap[depPkgRef]) {
7270
+ const dependentPkg = pkgBomRefMap[depPkgRef];
6592
7271
  dependentPkg.properties = dependentPkg.properties || [];
6593
7272
  const addedValue = {};
6594
7273
  // Is the parent a workspace
@@ -6630,6 +7309,8 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6630
7309
  depRef = adep;
6631
7310
  } else if (existingPkgMap[adep]) {
6632
7311
  depRef = existingPkgMap[adep];
7312
+ } else if (existingPkgMap[adep.toLowerCase()]) {
7313
+ depRef = existingPkgMap[adep.toLowerCase()];
6633
7314
  } else if (existingPkgMap[`py${adep}`]) {
6634
7315
  depRef = existingPkgMap[`py${adep}`];
6635
7316
  } else if (existingPkgMap[adep.replace(/-/g, "_")]) {
@@ -6699,6 +7380,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6699
7380
  pkgList,
6700
7381
  rootList,
6701
7382
  dependenciesList,
7383
+ pyLockProperties,
6702
7384
  workspaceWarningShown,
6703
7385
  };
6704
7386
  }
@@ -12810,38 +13492,14 @@ export function convertOSQueryResults(
12810
13492
  const pkgList = [];
12811
13493
  if (results?.length) {
12812
13494
  for (const res of results) {
12813
- const version =
12814
- res.version ||
12815
- res.hotfix_id ||
12816
- res.hardware_version ||
12817
- res.port ||
12818
- res.pid ||
12819
- res.subject_key_id ||
12820
- res.interface ||
12821
- res.instance_id;
12822
- let name =
12823
- res.name ||
12824
- res.device_id ||
12825
- res.hotfix_id ||
12826
- res.uuid ||
12827
- res.serial ||
12828
- res.pid ||
12829
- res.address ||
12830
- res.ami_id ||
12831
- res.interface ||
12832
- res.client_app_id;
13495
+ const version = deriveOsQueryVersion(res);
13496
+ let name = deriveOsQueryName(res, results.length === 1, queryObj.name);
13497
+ if (queryObj.purlType === "chrome-extension") {
13498
+ name = (res.identifier || res.extension_id || name || "").toLowerCase();
13499
+ }
12833
13500
  let group = "";
12834
13501
  const subpath = res.path || res.admindir || res.source;
12835
- let publisher =
12836
- res.publisher ||
12837
- res.maintainer ||
12838
- res.creator ||
12839
- res.manufacturer ||
12840
- res.provider ||
12841
- "";
12842
- if (publisher === "null") {
12843
- publisher = "";
12844
- }
13502
+ const publisher = deriveOsQueryPublisher(res);
12845
13503
  // For vscode-extension purl type, the publisher is used as the namespace
12846
13504
  if (queryObj.purlType === "vscode-extension" && publisher) {
12847
13505
  group = publisher.toLowerCase();
@@ -12852,20 +13510,8 @@ export function convertOSQueryResults(
12852
13510
  scope = compScope;
12853
13511
  }
12854
13512
  const description =
12855
- res.description ||
12856
- res.summary ||
12857
- res.arguments ||
12858
- res.device ||
12859
- res.codename ||
12860
- res.section ||
12861
- res.status ||
12862
- res.identifier ||
12863
- res.components ||
12864
- "";
12865
- // Re-use the name from query obj
12866
- if (!name && results.length === 1 && queryObj.name) {
12867
- name = queryObj.name;
12868
- }
13513
+ deriveOsQueryDescription(res) ||
13514
+ (queryObj.purlType === "chrome-extension" ? res.name || "" : "");
12869
13515
  let qualifiers;
12870
13516
  if (res.identifying_number?.length) {
12871
13517
  qualifiers = {
@@ -12873,25 +13519,18 @@ export function convertOSQueryResults(
12873
13519
  };
12874
13520
  }
12875
13521
  if (name) {
12876
- name = name
12877
- .replace(/ /g, "+")
12878
- .replace(/[:%]/g, "-")
12879
- .replace(/^[@{]/g, "")
12880
- .replace(/[}]$/g, "");
12881
- group = group
12882
- .replace(/ /g, "+")
12883
- .replace(/[:%]/g, "-")
12884
- .replace(/^[@{]/g, "")
12885
- .replace(/[}]$/g, "");
12886
- const purl = new PackageURL(
12887
- queryObj.purlType || "swid",
13522
+ name = sanitizeOsQueryIdentity(name);
13523
+ group = sanitizeOsQueryIdentity(group);
13524
+ const purl = createOsQueryPurl(
13525
+ queryObj.purlType,
12888
13526
  group,
12889
13527
  name,
12890
- version || "",
13528
+ version,
12891
13529
  qualifiers,
12892
13530
  subpath,
12893
- ).toString();
13531
+ );
12894
13532
  const props = [{ name: "cdx:osquery:category", value: queryCategory }];
13533
+ props.push(...createLolbasProperties(queryCategory, res));
12895
13534
  let providesList;
12896
13535
  if (enhance) {
12897
13536
  switch (queryObj.purlType) {
@@ -12928,9 +13567,15 @@ export function convertOSQueryResults(
12928
13567
  scope,
12929
13568
  type: queryObj.componentType,
12930
13569
  };
12931
- for (const k of Object.keys(res).filter(
12932
- (p) => !["name", "version", "description", "publisher"].includes(p),
12933
- )) {
13570
+ for (const k of Object.keys(res).filter((p) => {
13571
+ if (["version", "description", "publisher"].includes(p)) {
13572
+ return false;
13573
+ }
13574
+ if (queryObj.purlType !== "chrome-extension" && p === "name") {
13575
+ return false;
13576
+ }
13577
+ return true;
13578
+ })) {
12934
13579
  if (res[k] && res[k] !== "null") {
12935
13580
  props.push({
12936
13581
  name: k,
@@ -13612,6 +14257,76 @@ export function parseJarManifest(jarMetadata) {
13612
14257
  return metadata;
13613
14258
  }
13614
14259
 
14260
+ /**
14261
+ * Determine whether a manifest candidate looks like a namespace-qualified identifier.
14262
+ *
14263
+ * @param {string} candidate Manifest field value
14264
+ * @returns {boolean} True when candidate appears namespace-qualified
14265
+ */
14266
+ function isQualifiedJarNamespace(candidate) {
14267
+ return (
14268
+ !!candidate &&
14269
+ !candidate.includes(" ") &&
14270
+ (candidate.includes(".") || candidate.includes("-"))
14271
+ );
14272
+ }
14273
+
14274
+ /**
14275
+ * Select the most reliable group candidate from JAR manifest metadata.
14276
+ *
14277
+ * @param {Object} jarMetadata Parsed MANIFEST.MF key-value map
14278
+ * @returns {string} Best group candidate, or empty string if none exists
14279
+ */
14280
+ export function inferJarGroupFromManifest(jarMetadata = {}) {
14281
+ // Keep this ordered from most to least namespace-qualified manifest fields.
14282
+ // Extension-Name is intentionally lower priority due to inconsistent usage.
14283
+ const qualifiedCandidates = [
14284
+ jarMetadata["Bundle-SymbolicName"],
14285
+ jarMetadata["Automatic-Module-Name"],
14286
+ jarMetadata["Implementation-Title"],
14287
+ jarMetadata["Extension-Name"],
14288
+ ];
14289
+ for (const candidate of qualifiedCandidates) {
14290
+ if (isQualifiedJarNamespace(candidate)) {
14291
+ return candidate;
14292
+ }
14293
+ }
14294
+ return (
14295
+ jarMetadata["Implementation-Vendor-Id"] ||
14296
+ jarMetadata["Bundle-Vendor"] ||
14297
+ jarMetadata["Extension-Name"] ||
14298
+ ""
14299
+ );
14300
+ }
14301
+
14302
+ /**
14303
+ * Trim group suffix that duplicates the artifact name for compound artifact names.
14304
+ *
14305
+ * @param {string} group Group candidate
14306
+ * @param {string} name Artifact name candidate
14307
+ * @returns {string} Adjusted group
14308
+ */
14309
+ export function trimJarGroupSuffix(group, name) {
14310
+ if (!group || !name || group.startsWith("javax")) {
14311
+ return group;
14312
+ }
14313
+ // Only trim when the artifact name contains a separator (hyphen or dot).
14314
+ if (!name.includes("-") && !name.includes(".")) {
14315
+ return group;
14316
+ }
14317
+ const lowerName = name.toLowerCase();
14318
+ const dottedName = lowerName.replace(/-/g, ".");
14319
+ const dottedSuffix = `.${dottedName}`;
14320
+ if (group.endsWith(dottedSuffix)) {
14321
+ return group.slice(0, -dottedSuffix.length);
14322
+ }
14323
+ const lowerSuffix = `.${lowerName}`;
14324
+ if (group.endsWith(lowerSuffix)) {
14325
+ return group.slice(0, -lowerSuffix.length);
14326
+ }
14327
+ return group;
14328
+ }
14329
+
13615
14330
  /**
13616
14331
  * Parse a Maven pom.properties file and return its key-value pairs as an object.
13617
14332
  *
@@ -13902,14 +14617,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
13902
14617
  .split(";")[0]
13903
14618
  .trim();
13904
14619
  }
13905
- group =
13906
- group ||
13907
- jarMetadata["Extension-Name"] ||
13908
- jarMetadata["Implementation-Vendor-Id"] ||
13909
- jarMetadata["Bundle-SymbolicName"] ||
13910
- jarMetadata["Bundle-Vendor"] ||
13911
- jarMetadata["Automatic-Module-Name"] ||
13912
- "";
14620
+ group = group || inferJarGroupFromManifest(jarMetadata);
13913
14621
  version =
13914
14622
  version ||
13915
14623
  jarMetadata["Bundle-Version"] ||
@@ -13965,16 +14673,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
13965
14673
  }
13966
14674
  // Sometimes the group might already contain the name
13967
14675
  // Eg: group: org.checkerframework.checker.qual name: checker-qual
13968
- if (name && group && !group.startsWith("javax")) {
13969
- if (group.includes(`.${name.toLowerCase().replace(/-/g, ".")}`)) {
13970
- group = group.replace(
13971
- new RegExp(`.${name.toLowerCase().replace(/-/g, ".")}$`),
13972
- "",
13973
- );
13974
- } else if (group.includes(`.${name.toLowerCase()}`)) {
13975
- group = group.replace(new RegExp(`.${name.toLowerCase()}$`), "");
13976
- }
13977
- }
14676
+ group = trimJarGroupSuffix(group, name);
13978
14677
  // Patch the group string
13979
14678
  if (vendorAliases[name]) {
13980
14679
  group = vendorAliases[name];
@@ -16447,6 +17146,7 @@ export async function addEvidenceForImports(
16447
17146
  : [name];
16448
17147
  let isImported = false;
16449
17148
  for (const alias of aliases) {
17149
+ const isWasmAlias = /\.wasm([?#].*)?$/i.test(alias);
16450
17150
  const all_includes = impPkgs.filter(
16451
17151
  (find_pkg) =>
16452
17152
  find_pkg.startsWith(alias) &&
@@ -16456,12 +17156,20 @@ export async function addEvidenceForImports(
16456
17156
  find_pkg.startsWith(alias),
16457
17157
  );
16458
17158
  if (all_exports?.length) {
16459
- let exportedModules = new Set(all_exports);
17159
+ let exportedModules = new Set(isWasmAlias ? [] : all_exports);
16460
17160
  pkg.properties = pkg.properties || [];
16461
17161
  for (const subevidence of all_exports) {
16462
17162
  const evidences = allExports[subevidence];
16463
17163
  for (const evidence of evidences) {
16464
17164
  if (evidence && Object.keys(evidence).length) {
17165
+ if (isWasmAlias) {
17166
+ for (const wasmImportedModule of evidence.importedModules ||
17167
+ []) {
17168
+ if (wasmImportedModule?.length) {
17169
+ exportedModules.add(wasmImportedModule);
17170
+ }
17171
+ }
17172
+ }
16465
17173
  if (evidence.exportedModules.length > 1) {
16466
17174
  for (const aexpsubm of evidence.exportedModules) {
16467
17175
  // Be selective on the submodule names
@@ -16496,6 +17204,8 @@ export async function addEvidenceForImports(
16496
17204
  if (impPkgs.includes(alias) || all_includes.length) {
16497
17205
  isImported = true;
16498
17206
  let importedModules = new Set();
17207
+ let wasmExportedModules = new Set();
17208
+ const seenOccurrenceLocations = new Set();
16499
17209
  pkg.scope = "required";
16500
17210
  for (const subevidence of all_includes) {
16501
17211
  const evidences = allImports[subevidence];
@@ -16503,16 +17213,23 @@ export async function addEvidenceForImports(
16503
17213
  if (evidence && Object.keys(evidence).length && evidence.fileName) {
16504
17214
  pkg.evidence = pkg.evidence || {};
16505
17215
  pkg.evidence.occurrences = pkg.evidence.occurrences || [];
16506
- pkg.evidence.occurrences.push({
16507
- location: `${evidence.fileName}${
16508
- evidence.lineNumber ? `#${evidence.lineNumber}` : ""
16509
- }`,
16510
- });
17216
+ const occurrenceLocation = `${evidence.fileName}${
17217
+ evidence.lineNumber ? `#${evidence.lineNumber}` : ""
17218
+ }`;
17219
+ if (!seenOccurrenceLocations.has(occurrenceLocation)) {
17220
+ pkg.evidence.occurrences.push({
17221
+ location: occurrenceLocation,
17222
+ });
17223
+ seenOccurrenceLocations.add(occurrenceLocation);
17224
+ }
16511
17225
  importedModules.add(evidence.importedAs);
16512
17226
  for (const importedSm of evidence.importedModules || []) {
16513
17227
  if (!importedSm) {
16514
17228
  continue;
16515
17229
  }
17230
+ if (isWasmAlias) {
17231
+ wasmExportedModules.add(importedSm);
17232
+ }
16516
17233
  // Store both the short and long form of the imported sub modules
16517
17234
  if (importedSm.length > 3) {
16518
17235
  importedModules.add(importedSm);
@@ -16523,6 +17240,7 @@ export async function addEvidenceForImports(
16523
17240
  }
16524
17241
  }
16525
17242
  importedModules = Array.from(importedModules);
17243
+ wasmExportedModules = Array.from(wasmExportedModules);
16526
17244
  if (importedModules.length) {
16527
17245
  pkg.properties = pkg.properties || [];
16528
17246
  pkg.properties.push({
@@ -16530,6 +17248,15 @@ export async function addEvidenceForImports(
16530
17248
  value: importedModules.join(","),
16531
17249
  });
16532
17250
  }
17251
+ if (isWasmAlias && wasmExportedModules.length) {
17252
+ pkg.properties = pkg.properties || [];
17253
+ if (!pkg.properties.some((p) => p.name === "ExportedModules")) {
17254
+ pkg.properties.push({
17255
+ name: "ExportedModules",
17256
+ value: wasmExportedModules.join(","),
17257
+ });
17258
+ }
17259
+ }
16533
17260
  break;
16534
17261
  }
16535
17262
  if (
@@ -17774,6 +18501,29 @@ export function parseMakeDFile(dfile) {
17774
18501
  return pkgFilesMap;
17775
18502
  }
17776
18503
 
18504
+ const isAsciiHexCode = (code) => {
18505
+ return (
18506
+ (code >= 0x30 && code <= 0x39) ||
18507
+ (code >= 0x41 && code <= 0x46) ||
18508
+ (code >= 0x61 && code <= 0x66)
18509
+ );
18510
+ };
18511
+
18512
+ const hasValidPercentEncoding = (value) => {
18513
+ for (let index = 0; index < value.length; index++) {
18514
+ if (value.charCodeAt(index) !== 0x25) {
18515
+ continue;
18516
+ }
18517
+ const firstHex = value.charCodeAt(index + 1);
18518
+ const secondHex = value.charCodeAt(index + 2);
18519
+ if (!isAsciiHexCode(firstHex) || !isAsciiHexCode(secondHex)) {
18520
+ return false;
18521
+ }
18522
+ index += 2;
18523
+ }
18524
+ return true;
18525
+ };
18526
+
17777
18527
  /**
17778
18528
  * Function to validate an externalReference URL for conforming to the JSON schema or bomLink
17779
18529
  * https://github.com/CycloneDX/cyclonedx-core-java/blob/75575318b268dda9e2a290761d7db11b4f414255/src/main/resources/bom-1.5.schema.json#L1140
@@ -17798,13 +18548,9 @@ export function isValidIriReference(iri) {
17798
18548
  return false;
17799
18549
  }
17800
18550
 
17801
- // Check for malformed percent-encoding sequences more robustly
17802
- // This regex looks for a % that is NOT followed by:
17803
- // - Exactly two hex digits that are then either:
17804
- // - The end of the string ($)
17805
- // - Or a character that is NOT a hex digit ([^0-9A-Fa-f])
17806
- // This catches %ab, %ab%, %abZ, %abc, etc.
17807
- if (/%(?!([0-9A-Fa-f]{2})($|[^0-9A-Fa-f]))/.test(iri)) {
18551
+ // Validate percent-encoding with a linear scan to avoid regex backtracking
18552
+ // issues on very long attacker-controlled inputs.
18553
+ if (!hasValidPercentEncoding(iri)) {
17808
18554
  return false;
17809
18555
  }
17810
18556