@cyclonedx/cdxgen 12.2.1 → 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 (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  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 +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  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/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. 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,8 @@ 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
+
340
391
  // Table border style for console output.
341
392
  export const TABLE_BORDER_STYLE = ["ascii", "unicode", "auto"].includes(
342
393
  `${process.env.CDXGEN_TABLE_BORDER || ""}`.toLowerCase(),
@@ -390,6 +441,19 @@ export function shouldFetchLicense() {
390
441
  );
391
442
  }
392
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
+
393
457
  /**
394
458
  * Determines whether VCS (version control system) information should be fetched
395
459
  * for Go packages, based on the GO_FETCH_VCS environment variable.
@@ -686,6 +750,12 @@ export const PROJECT_TYPE_ALIASES = {
686
750
  "vscode-extensions",
687
751
  "ide-extensions",
688
752
  ],
753
+ "chrome-extension": [
754
+ "chrome-extension",
755
+ "chrome-extensions",
756
+ "chromium-extension",
757
+ "chromium-extensions",
758
+ ],
689
759
  };
690
760
 
691
761
  // Package manager aliases
@@ -1375,6 +1445,10 @@ export async function getNpmMetadata(pkgList) {
1375
1445
  if (body.homepage) {
1376
1446
  p.homepage = { url: body.homepage };
1377
1447
  }
1448
+ p.properties = p.properties || [];
1449
+ p.properties.push(
1450
+ ...collectNpmRegistryProvenanceProperties(body, p.version),
1451
+ );
1378
1452
  cdepList.push(p);
1379
1453
  } catch (_err) {
1380
1454
  cdepList.push(p);
@@ -1393,6 +1467,382 @@ export async function getNpmMetadata(pkgList) {
1393
1467
  * @param {boolean} simple Return a simpler representation of the component by skipping extended attributes and license fetch.
1394
1468
  * @param {boolean} securityProps Collect security-related properties
1395
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
+
1396
1846
  export async function parsePkgJson(
1397
1847
  pkgJsonFile,
1398
1848
  simple = false,
@@ -1482,24 +1932,70 @@ export async function parsePkgJson(
1482
1932
  // Track lifecycle scripts (preinstall, postinstall, etc. - code execution risk)
1483
1933
  if (pkgData.scripts && Object.keys(pkgData.scripts).length) {
1484
1934
  const scriptNames = Object.keys(pkgData.scripts).join(", ");
1935
+ const lifecycleAnalysis = analyzeNpmLifecycleScripts(
1936
+ pkgJsonFile,
1937
+ pkgData.scripts,
1938
+ );
1485
1939
  apkg.properties.push({
1486
1940
  name: "cdx:npm:scripts",
1487
1941
  value: scriptNames,
1488
1942
  });
1489
1943
  // Flag high-risk scripts specifically
1490
- const riskyScripts = [
1491
- "preinstall",
1492
- "install",
1493
- "postinstall",
1494
- "prepublish",
1495
- "prepare",
1496
- ].filter((script) => pkgData.scripts[script]);
1944
+ const riskyScripts = lifecycleAnalysis.riskyScripts;
1497
1945
  if (riskyScripts.length) {
1946
+ apkg.properties.push({
1947
+ name: "cdx:npm:hasInstallScript",
1948
+ value: "true",
1949
+ });
1498
1950
  apkg.properties.push({
1499
1951
  name: "cdx:npm:risky_scripts",
1500
1952
  value: riskyScripts.join(", "),
1501
1953
  });
1502
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
+ }
1503
1999
  }
1504
2000
  // Track platform/architecture constraints
1505
2001
  if (pkgData.cpu && Array.isArray(pkgData.cpu) && pkgData.cpu.length) {
@@ -1566,10 +2062,10 @@ export async function parsePkgJson(
1566
2062
  // continue regardless of error
1567
2063
  }
1568
2064
  }
1569
- if (!simple && shouldFetchLicense() && pkgList?.length) {
2065
+ if (!simple && shouldFetchPackageMetadata() && pkgList?.length) {
1570
2066
  if (DEBUG_MODE) {
1571
2067
  console.log(
1572
- `About to fetch license information for ${pkgList.length} packages in parsePkgJson`,
2068
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgJson`,
1573
2069
  );
1574
2070
  }
1575
2071
  return await getNpmMetadata(pkgList);
@@ -1616,8 +2112,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1616
2112
  const srcFilePath = node.path.includes(`${_sep}node_modules`)
1617
2113
  ? node.path.split(`${_sep}node_modules`)[0]
1618
2114
  : node.path;
2115
+ const isDevelopmentNode = node.dev === true || node.devOptional === true;
1619
2116
  const scope =
1620
- node.dev === true || node.optional === true ? "optional" : undefined;
2117
+ isDevelopmentNode || node.optional === true ? "optional" : undefined;
1621
2118
  const integrity = node.integrity ? node.integrity : undefined;
1622
2119
 
1623
2120
  let pkg;
@@ -1690,7 +2187,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1690
2187
  purl: purlString,
1691
2188
  "bom-ref": decodeURIComponent(purlString),
1692
2189
  };
1693
- if (node.dev === true) {
2190
+ if (isDevelopmentNode) {
1694
2191
  _setNpmDevelopmentProperty(pkg);
1695
2192
  }
1696
2193
  if (node.resolved) {
@@ -2128,10 +2625,10 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
2128
2625
  options,
2129
2626
  ));
2130
2627
 
2131
- if (shouldFetchLicense() && pkgList?.length) {
2628
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
2132
2629
  if (DEBUG_MODE) {
2133
2630
  console.log(
2134
- `About to fetch license information for ${pkgList.length} packages in parsePkgLock`,
2631
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgLock`,
2135
2632
  );
2136
2633
  }
2137
2634
  pkgList = await getNpmMetadata(pkgList);
@@ -2761,10 +3258,10 @@ export async function parseYarnLock(
2761
3258
  }
2762
3259
  }
2763
3260
 
2764
- if (shouldFetchLicense() && pkgList?.length) {
3261
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
2765
3262
  if (DEBUG_MODE) {
2766
3263
  console.log(
2767
- `About to fetch license information for ${pkgList.length} packages in parseYarnLock`,
3264
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parseYarnLock`,
2768
3265
  );
2769
3266
  }
2770
3267
  pkgList = await getNpmMetadata(pkgList);
@@ -2838,10 +3335,10 @@ export async function parseNodeShrinkwrap(swFile) {
2838
3335
  }
2839
3336
  }
2840
3337
  }
2841
- if (shouldFetchLicense() && pkgList?.length) {
3338
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
2842
3339
  if (DEBUG_MODE) {
2843
3340
  console.log(
2844
- `About to fetch license information for ${pkgList.length} packages in parseNodeShrinkwrap`,
3341
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parseNodeShrinkwrap`,
2845
3342
  );
2846
3343
  }
2847
3344
  return await getNpmMetadata(pkgList);
@@ -4157,10 +4654,10 @@ export async function parsePnpmLock(
4157
4654
  pkgList = await pnpmMetadata(pkgList, pnpmLock);
4158
4655
  }
4159
4656
 
4160
- if (shouldFetchLicense() && pkgList?.length) {
4657
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
4161
4658
  if (DEBUG_MODE) {
4162
4659
  console.log(
4163
- `About to fetch license information for ${pkgList.length} packages in parsePnpmLock`,
4660
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parsePnpmLock`,
4164
4661
  );
4165
4662
  }
4166
4663
  pkgList = await getNpmMetadata(pkgList);
@@ -4218,10 +4715,10 @@ export async function parseBowerJson(bowerJsonFile) {
4218
4715
  // continue regardless of error
4219
4716
  }
4220
4717
  }
4221
- if (shouldFetchLicense() && pkgList?.length) {
4718
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
4222
4719
  if (DEBUG_MODE) {
4223
4720
  console.log(
4224
- `About to fetch license information for ${pkgList.length} packages in parseBowerJson`,
4721
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parseBowerJson`,
4225
4722
  );
4226
4723
  }
4227
4724
  return await getNpmMetadata(pkgList);
@@ -4316,10 +4813,10 @@ export async function parseMinJs(minJsFile) {
4316
4813
  // continue regardless of error
4317
4814
  }
4318
4815
  }
4319
- if (shouldFetchLicense() && pkgList?.length) {
4816
+ if (shouldFetchPackageMetadata() && pkgList?.length) {
4320
4817
  if (DEBUG_MODE) {
4321
4818
  console.log(
4322
- `About to fetch license information for ${pkgList.length} packages in parseMinJs`,
4819
+ `About to fetch npm registry metadata for ${pkgList.length} packages in parseMinJs`,
4323
4820
  );
4324
4821
  }
4325
4822
  return await getNpmMetadata(pkgList);
@@ -5811,7 +6308,7 @@ export function guessPypiMatchingVersion(versionsList, versionSpecifiers) {
5811
6308
  * @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
5812
6309
  */
5813
6310
  export async function getPyMetadata(pkgList, fetchDepsInfo) {
5814
- if (!shouldFetchLicense() && !fetchDepsInfo) {
6311
+ if (!shouldFetchPackageMetadata() && !fetchDepsInfo) {
5815
6312
  return pkgList;
5816
6313
  }
5817
6314
  const PYPI_URL = process.env.PYPI_URL || "https://pypi.org/pypi/";
@@ -5987,6 +6484,10 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
5987
6484
  null,
5988
6485
  null,
5989
6486
  ).toString();
6487
+ p.properties = p.properties || [];
6488
+ p.properties.push(
6489
+ ...collectPypiRegistryProvenanceProperties(body, p.version),
6490
+ );
5990
6491
  p.purl = purlString;
5991
6492
  p["bom-ref"] = decodeURIComponent(purlString);
5992
6493
  cdepList.push(p);
@@ -6389,9 +6890,9 @@ export function parsePyProjectTomlFile(tomlFile) {
6389
6890
  }
6390
6891
 
6391
6892
  /**
6392
- * 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.
6393
6894
  *
6394
- * @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
6395
6896
  * @param {string} lockFile Lock file name for evidence
6396
6897
  * @param {string} pyProjectFile pyproject.toml file
6397
6898
  */
@@ -6408,6 +6909,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6408
6909
  let workspacePaths;
6409
6910
  let workspaceWarningShown = false;
6410
6911
  let hasWorkspaces = false;
6912
+ let pyLockProperties = [];
6411
6913
  // Keep track of any workspace components to be added to the parent component
6412
6914
  const workspaceComponentMap = {};
6413
6915
  const workspacePyProjMap = {};
@@ -6536,7 +7038,17 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6536
7038
  }
6537
7039
  }
6538
7040
  }
6539
- 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) {
6540
7052
  // This avoids validation errors with uv.lock
6541
7053
  if (parentComponent?.name && parentComponent.name === apkg.name) {
6542
7054
  continue;
@@ -6556,10 +7068,19 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6556
7068
  if (apkg.optional) {
6557
7069
  pkg.scope = "optional";
6558
7070
  }
6559
- 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) {
6560
7075
  pkg.properties.push({
6561
7076
  name: "cdx:pypi:requiresPython",
6562
- 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,
6563
7084
  });
6564
7085
  }
6565
7086
  if (apkg?.source) {
@@ -6585,6 +7106,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6585
7106
  });
6586
7107
  }
6587
7108
  }
7109
+ if (pyLockMode) {
7110
+ pkg.properties = pkg.properties.concat(
7111
+ collectPyLockPackageProperties(apkg),
7112
+ );
7113
+ }
6588
7114
  // Is this component a module?
6589
7115
  if (workspaceComponentMap[pkg.name]) {
6590
7116
  pkg.properties.push({
@@ -6672,6 +7198,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6672
7198
  });
6673
7199
  }
6674
7200
  }
7201
+ if (pyLockMode) {
7202
+ const pylockFileComponents = collectPyLockFileComponents(apkg, lockFile);
7203
+ if (pylockFileComponents.length) {
7204
+ pkg.components = (pkg.components || []).concat(pylockFileComponents);
7205
+ }
7206
+ }
6675
7207
  if (
6676
7208
  directDepsKeys[pkg.name] ||
6677
7209
  (hasWorkspaces && !Object.keys(workspaceComponentMap).length)
@@ -6728,13 +7260,14 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6728
7260
  // Example: "msgpack>=0.5.2"
6729
7261
  const nameStr =
6730
7262
  apkgDep.name || apkgDep.split(/(==|<=|~=|>=)/)[0].split(" ")[0];
6731
- 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);
6732
7268
  // Propagate the workspace properties to the child components
6733
- if (
6734
- existingPkgMap[nameStr] &&
6735
- pkgBomRefMap[existingPkgMap[nameStr]]
6736
- ) {
6737
- const dependentPkg = pkgBomRefMap[existingPkgMap[nameStr]];
7269
+ if (depPkgRef && pkgBomRefMap[depPkgRef]) {
7270
+ const dependentPkg = pkgBomRefMap[depPkgRef];
6738
7271
  dependentPkg.properties = dependentPkg.properties || [];
6739
7272
  const addedValue = {};
6740
7273
  // Is the parent a workspace
@@ -6776,6 +7309,8 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6776
7309
  depRef = adep;
6777
7310
  } else if (existingPkgMap[adep]) {
6778
7311
  depRef = existingPkgMap[adep];
7312
+ } else if (existingPkgMap[adep.toLowerCase()]) {
7313
+ depRef = existingPkgMap[adep.toLowerCase()];
6779
7314
  } else if (existingPkgMap[`py${adep}`]) {
6780
7315
  depRef = existingPkgMap[`py${adep}`];
6781
7316
  } else if (existingPkgMap[adep.replace(/-/g, "_")]) {
@@ -6845,6 +7380,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6845
7380
  pkgList,
6846
7381
  rootList,
6847
7382
  dependenciesList,
7383
+ pyLockProperties,
6848
7384
  workspaceWarningShown,
6849
7385
  };
6850
7386
  }
@@ -12956,38 +13492,14 @@ export function convertOSQueryResults(
12956
13492
  const pkgList = [];
12957
13493
  if (results?.length) {
12958
13494
  for (const res of results) {
12959
- const version =
12960
- res.version ||
12961
- res.hotfix_id ||
12962
- res.hardware_version ||
12963
- res.port ||
12964
- res.pid ||
12965
- res.subject_key_id ||
12966
- res.interface ||
12967
- res.instance_id;
12968
- let name =
12969
- res.name ||
12970
- res.device_id ||
12971
- res.hotfix_id ||
12972
- res.uuid ||
12973
- res.serial ||
12974
- res.pid ||
12975
- res.address ||
12976
- res.ami_id ||
12977
- res.interface ||
12978
- 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
+ }
12979
13500
  let group = "";
12980
13501
  const subpath = res.path || res.admindir || res.source;
12981
- let publisher =
12982
- res.publisher ||
12983
- res.maintainer ||
12984
- res.creator ||
12985
- res.manufacturer ||
12986
- res.provider ||
12987
- "";
12988
- if (publisher === "null") {
12989
- publisher = "";
12990
- }
13502
+ const publisher = deriveOsQueryPublisher(res);
12991
13503
  // For vscode-extension purl type, the publisher is used as the namespace
12992
13504
  if (queryObj.purlType === "vscode-extension" && publisher) {
12993
13505
  group = publisher.toLowerCase();
@@ -12998,20 +13510,8 @@ export function convertOSQueryResults(
12998
13510
  scope = compScope;
12999
13511
  }
13000
13512
  const description =
13001
- res.description ||
13002
- res.summary ||
13003
- res.arguments ||
13004
- res.device ||
13005
- res.codename ||
13006
- res.section ||
13007
- res.status ||
13008
- res.identifier ||
13009
- res.components ||
13010
- "";
13011
- // Re-use the name from query obj
13012
- if (!name && results.length === 1 && queryObj.name) {
13013
- name = queryObj.name;
13014
- }
13513
+ deriveOsQueryDescription(res) ||
13514
+ (queryObj.purlType === "chrome-extension" ? res.name || "" : "");
13015
13515
  let qualifiers;
13016
13516
  if (res.identifying_number?.length) {
13017
13517
  qualifiers = {
@@ -13019,25 +13519,18 @@ export function convertOSQueryResults(
13019
13519
  };
13020
13520
  }
13021
13521
  if (name) {
13022
- name = name
13023
- .replace(/ /g, "+")
13024
- .replace(/[:%]/g, "-")
13025
- .replace(/^[@{]/g, "")
13026
- .replace(/[}]$/g, "");
13027
- group = group
13028
- .replace(/ /g, "+")
13029
- .replace(/[:%]/g, "-")
13030
- .replace(/^[@{]/g, "")
13031
- .replace(/[}]$/g, "");
13032
- const purl = new PackageURL(
13033
- queryObj.purlType || "swid",
13522
+ name = sanitizeOsQueryIdentity(name);
13523
+ group = sanitizeOsQueryIdentity(group);
13524
+ const purl = createOsQueryPurl(
13525
+ queryObj.purlType,
13034
13526
  group,
13035
13527
  name,
13036
- version || "",
13528
+ version,
13037
13529
  qualifiers,
13038
13530
  subpath,
13039
- ).toString();
13531
+ );
13040
13532
  const props = [{ name: "cdx:osquery:category", value: queryCategory }];
13533
+ props.push(...createLolbasProperties(queryCategory, res));
13041
13534
  let providesList;
13042
13535
  if (enhance) {
13043
13536
  switch (queryObj.purlType) {
@@ -13074,9 +13567,15 @@ export function convertOSQueryResults(
13074
13567
  scope,
13075
13568
  type: queryObj.componentType,
13076
13569
  };
13077
- for (const k of Object.keys(res).filter(
13078
- (p) => !["name", "version", "description", "publisher"].includes(p),
13079
- )) {
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
+ })) {
13080
13579
  if (res[k] && res[k] !== "null") {
13081
13580
  props.push({
13082
13581
  name: k,
@@ -13758,6 +14257,76 @@ export function parseJarManifest(jarMetadata) {
13758
14257
  return metadata;
13759
14258
  }
13760
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
+
13761
14330
  /**
13762
14331
  * Parse a Maven pom.properties file and return its key-value pairs as an object.
13763
14332
  *
@@ -14048,14 +14617,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
14048
14617
  .split(";")[0]
14049
14618
  .trim();
14050
14619
  }
14051
- group =
14052
- group ||
14053
- jarMetadata["Extension-Name"] ||
14054
- jarMetadata["Implementation-Vendor-Id"] ||
14055
- jarMetadata["Bundle-SymbolicName"] ||
14056
- jarMetadata["Bundle-Vendor"] ||
14057
- jarMetadata["Automatic-Module-Name"] ||
14058
- "";
14620
+ group = group || inferJarGroupFromManifest(jarMetadata);
14059
14621
  version =
14060
14622
  version ||
14061
14623
  jarMetadata["Bundle-Version"] ||
@@ -14111,16 +14673,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
14111
14673
  }
14112
14674
  // Sometimes the group might already contain the name
14113
14675
  // Eg: group: org.checkerframework.checker.qual name: checker-qual
14114
- if (name && group && !group.startsWith("javax")) {
14115
- if (group.includes(`.${name.toLowerCase().replace(/-/g, ".")}`)) {
14116
- group = group.replace(
14117
- new RegExp(`.${name.toLowerCase().replace(/-/g, ".")}$`),
14118
- "",
14119
- );
14120
- } else if (group.includes(`.${name.toLowerCase()}`)) {
14121
- group = group.replace(new RegExp(`.${name.toLowerCase()}$`), "");
14122
- }
14123
- }
14676
+ group = trimJarGroupSuffix(group, name);
14124
14677
  // Patch the group string
14125
14678
  if (vendorAliases[name]) {
14126
14679
  group = vendorAliases[name];
@@ -17948,6 +18501,29 @@ export function parseMakeDFile(dfile) {
17948
18501
  return pkgFilesMap;
17949
18502
  }
17950
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
+
17951
18527
  /**
17952
18528
  * Function to validate an externalReference URL for conforming to the JSON schema or bomLink
17953
18529
  * https://github.com/CycloneDX/cyclonedx-core-java/blob/75575318b268dda9e2a290761d7db11b4f414255/src/main/resources/bom-1.5.schema.json#L1140
@@ -17972,13 +18548,9 @@ export function isValidIriReference(iri) {
17972
18548
  return false;
17973
18549
  }
17974
18550
 
17975
- // Check for malformed percent-encoding sequences more robustly
17976
- // This regex looks for a % that is NOT followed by:
17977
- // - Exactly two hex digits that are then either:
17978
- // - The end of the string ($)
17979
- // - Or a character that is NOT a hex digit ([^0-9A-Fa-f])
17980
- // This catches %ab, %ab%, %abZ, %abc, etc.
17981
- 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)) {
17982
18554
  return false;
17983
18555
  }
17984
18556