@cyclonedx/cdxgen 12.3.0 → 12.3.2

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 (121) hide show
  1. package/README.md +15 -5
  2. package/bin/audit.js +7 -0
  3. package/bin/cdxgen.js +241 -81
  4. package/bin/repl.js +138 -0
  5. package/data/rules/ai-agent-governance.yaml +249 -0
  6. package/data/rules/dependency-sources.yaml +41 -0
  7. package/data/rules/mcp-servers.yaml +304 -0
  8. package/data/rules/package-integrity.yaml +123 -0
  9. package/lib/audit/index.js +353 -29
  10. package/lib/audit/index.poku.js +247 -7
  11. package/lib/audit/reporters.js +26 -0
  12. package/lib/audit/scoring.js +262 -13
  13. package/lib/audit/scoring.poku.js +179 -0
  14. package/lib/audit/targets.js +391 -2
  15. package/lib/audit/targets.poku.js +416 -3
  16. package/lib/cli/index.js +588 -45
  17. package/lib/cli/index.poku.js +735 -1
  18. package/lib/evinser/evinser.js +8 -5
  19. package/lib/helpers/agentFormulationParser.js +318 -0
  20. package/lib/helpers/aiInventory.js +262 -0
  21. package/lib/helpers/aiInventory.poku.js +111 -0
  22. package/lib/helpers/analyzer.js +1769 -0
  23. package/lib/helpers/analyzer.poku.js +284 -3
  24. package/lib/helpers/auditCategories.js +76 -0
  25. package/lib/helpers/ciParsers/githubActions.js +140 -16
  26. package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
  27. package/lib/helpers/communityAiConfigParser.js +672 -0
  28. package/lib/helpers/communityAiConfigParser.poku.js +63 -0
  29. package/lib/helpers/depsUtils.js +108 -0
  30. package/lib/helpers/depsUtils.poku.js +72 -1
  31. package/lib/helpers/display.js +325 -3
  32. package/lib/helpers/display.poku.js +301 -0
  33. package/lib/helpers/formulationParsers.js +28 -0
  34. package/lib/helpers/formulationParsers.poku.js +504 -1
  35. package/lib/helpers/jsonLike.js +102 -0
  36. package/lib/helpers/jsonLike.poku.js +34 -0
  37. package/lib/helpers/mcp.js +248 -0
  38. package/lib/helpers/mcp.poku.js +101 -0
  39. package/lib/helpers/mcpConfigParser.js +656 -0
  40. package/lib/helpers/mcpConfigParser.poku.js +126 -0
  41. package/lib/helpers/mcpDiscovery.js +84 -0
  42. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  43. package/lib/helpers/protobom.js +3 -3
  44. package/lib/helpers/provenanceUtils.js +29 -4
  45. package/lib/helpers/provenanceUtils.poku.js +29 -3
  46. package/lib/helpers/registryProvenance.js +210 -0
  47. package/lib/helpers/registryProvenance.poku.js +144 -0
  48. package/lib/helpers/rustFormulationParser.js +330 -0
  49. package/lib/helpers/source.js +21 -2
  50. package/lib/helpers/source.poku.js +38 -0
  51. package/lib/helpers/utils.js +1331 -83
  52. package/lib/helpers/utils.poku.js +599 -188
  53. package/lib/helpers/vsixutils.js +12 -4
  54. package/lib/helpers/vsixutils.poku.js +34 -0
  55. package/lib/managers/binary.js +36 -12
  56. package/lib/managers/binary.poku.js +68 -0
  57. package/lib/managers/docker.js +59 -9
  58. package/lib/managers/docker.poku.js +61 -0
  59. package/lib/managers/piptree.js +12 -7
  60. package/lib/managers/piptree.poku.js +44 -0
  61. package/lib/stages/postgen/annotator.js +2 -1
  62. package/lib/stages/postgen/annotator.poku.js +15 -0
  63. package/lib/stages/postgen/auditBom.js +20 -6
  64. package/lib/stages/postgen/auditBom.poku.js +694 -1
  65. package/lib/stages/postgen/postgen.js +262 -11
  66. package/lib/stages/postgen/postgen.poku.js +306 -2
  67. package/lib/stages/postgen/ruleEngine.js +49 -1
  68. package/lib/stages/postgen/spdxConverter.poku.js +70 -0
  69. package/lib/stages/pregen/pregen.js +6 -4
  70. package/package.json +1 -1
  71. package/types/bin/repl.d.ts.map +1 -1
  72. package/types/lib/audit/index.d.ts.map +1 -1
  73. package/types/lib/audit/reporters.d.ts.map +1 -1
  74. package/types/lib/audit/scoring.d.ts.map +1 -1
  75. package/types/lib/audit/targets.d.ts +12 -0
  76. package/types/lib/audit/targets.d.ts.map +1 -1
  77. package/types/lib/cli/index.d.ts +2 -8
  78. package/types/lib/cli/index.d.ts.map +1 -1
  79. package/types/lib/evinser/evinser.d.ts.map +1 -1
  80. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  81. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  82. package/types/lib/helpers/aiInventory.d.ts +23 -0
  83. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  84. package/types/lib/helpers/analyzer.d.ts +10 -0
  85. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  86. package/types/lib/helpers/auditCategories.d.ts +12 -0
  87. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  88. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  89. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  90. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  91. package/types/lib/helpers/depsUtils.d.ts +8 -0
  92. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  93. package/types/lib/helpers/display.d.ts +17 -1
  94. package/types/lib/helpers/display.d.ts.map +1 -1
  95. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  96. package/types/lib/helpers/jsonLike.d.ts +4 -0
  97. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  98. package/types/lib/helpers/mcp.d.ts +29 -0
  99. package/types/lib/helpers/mcp.d.ts.map +1 -0
  100. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  101. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  102. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  103. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  104. package/types/lib/helpers/provenanceUtils.d.ts +5 -3
  105. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
  106. package/types/lib/helpers/registryProvenance.d.ts +9 -0
  107. package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
  108. package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
  109. package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
  110. package/types/lib/helpers/source.d.ts.map +1 -1
  111. package/types/lib/helpers/utils.d.ts +31 -1
  112. package/types/lib/helpers/utils.d.ts.map +1 -1
  113. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  114. package/types/lib/managers/binary.d.ts.map +1 -1
  115. package/types/lib/managers/docker.d.ts.map +1 -1
  116. package/types/lib/managers/piptree.d.ts.map +1 -1
  117. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  118. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  119. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  120. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  121. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
@@ -33,9 +33,11 @@ import {
33
33
  getMvnMetadata,
34
34
  getPropertyGroupTextNodes,
35
35
  getPyMetadata,
36
+ getRecordedActivities,
36
37
  guessPypiMatchingVersion,
37
38
  hasAnyProjectType,
38
39
  inferJarGroupFromManifest,
40
+ isDryRunError,
39
41
  isPackageManagerAllowed,
40
42
  isPartialTree,
41
43
  isValidIriReference,
@@ -50,6 +52,7 @@ import {
50
52
  parseCargoAuditableData,
51
53
  parseCargoData,
52
54
  parseCargoDependencyData,
55
+ parseCargoManifestDependencyData,
53
56
  parseCargoTomlData,
54
57
  parseCljDep,
55
58
  parseCloudBuildData,
@@ -125,7 +128,13 @@ import {
125
128
  pnpmMetadata,
126
129
  purlFromUrlString,
127
130
  readZipEntry,
131
+ resetRecordedActivities,
132
+ safeMkdtempSync,
133
+ safeRmSync,
128
134
  safeSpawnSync,
135
+ safeUnlinkSync,
136
+ safeWriteSync,
137
+ setDryRunMode,
129
138
  splitOutputByGradleProjects,
130
139
  toGemModuleNames,
131
140
  trimJarGroupSuffix,
@@ -259,6 +268,177 @@ it("safeSpawnSync() resets ANSI color state for host pip warnings", () => {
259
268
  }
260
269
  });
261
270
 
271
+ it("safeSpawnSync() returns a dry-run sentinel result when dry run mode is enabled", () => {
272
+ setDryRunMode(true);
273
+ resetRecordedActivities();
274
+ try {
275
+ const result = safeSpawnSync("node", ["--version"], {});
276
+ assert.strictEqual(result.status, 1);
277
+ assert.ok(isDryRunError(result.error));
278
+ const activities = getRecordedActivities();
279
+ assert.strictEqual(activities[0].kind, "execute");
280
+ assert.strictEqual(activities[0].status, "blocked");
281
+ } finally {
282
+ setDryRunMode(false);
283
+ resetRecordedActivities();
284
+ }
285
+ });
286
+
287
+ it("dry-run filesystem wrappers do not mutate the filesystem", () => {
288
+ const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-"));
289
+ const fileToKeep = path.join(tmpRoot, "keep.txt");
290
+ const fileToSkip = path.join(tmpRoot, "skip.txt");
291
+ const dirToKeep = path.join(tmpRoot, "keep-dir");
292
+ writeFileSync(fileToKeep, "hello");
293
+ mkdirSync(dirToKeep, { recursive: true });
294
+ setDryRunMode(true);
295
+ resetRecordedActivities();
296
+ try {
297
+ const tempPath = safeMkdtempSync(path.join(tmpRoot, "temp-"));
298
+ safeWriteSync(fileToSkip, "world");
299
+ safeUnlinkSync(fileToKeep);
300
+ safeRmSync(dirToKeep, { recursive: true, force: true });
301
+ assert.ok(!existsSync(fileToSkip));
302
+ assert.ok(existsSync(fileToKeep));
303
+ assert.ok(existsSync(dirToKeep));
304
+ assert.ok(!existsSync(tempPath));
305
+ const activities = getRecordedActivities();
306
+ assert.deepStrictEqual(
307
+ activities.map((activity) => activity.kind),
308
+ ["temp-dir", "write", "cleanup", "cleanup"],
309
+ );
310
+ assert.ok(activities.every((activity) => activity.status === "blocked"));
311
+ } finally {
312
+ setDryRunMode(false);
313
+ resetRecordedActivities();
314
+ rmSync(tmpRoot, { force: true, recursive: true });
315
+ }
316
+ });
317
+
318
+ it("safeWriteSync() honors explicit fs.write permission for output files", async () => {
319
+ const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-secure-write-"));
320
+ const outputFile = path.join(tmpRoot, "bom.json");
321
+ const originalDebugMode = process.env.CDXGEN_DEBUG_MODE;
322
+ const originalSecureMode = process.env.CDXGEN_SECURE_MODE;
323
+ const originalPermissionDescriptor = Object.getOwnPropertyDescriptor(
324
+ process,
325
+ "permission",
326
+ );
327
+ const hasPermissionStub = sinon.stub().callsFake((scope, filePath) => {
328
+ return (
329
+ scope === "fs.read" || (scope === "fs.write" && filePath === outputFile)
330
+ );
331
+ });
332
+ process.env.CDXGEN_DEBUG_MODE = "debug";
333
+ process.env.CDXGEN_SECURE_MODE = "true";
334
+ Object.defineProperty(process, "permission", {
335
+ configurable: true,
336
+ value: {
337
+ has: hasPermissionStub,
338
+ },
339
+ });
340
+ try {
341
+ const {
342
+ getRecordedActivities: getRecordedActivitiesMocked,
343
+ resetRecordedActivities: resetRecordedActivitiesMocked,
344
+ safeWriteSync: safeWriteSyncMocked,
345
+ } = await import(
346
+ new URL(`./utils.js?secure-write-test=${Date.now()}`, import.meta.url)
347
+ );
348
+ resetRecordedActivitiesMocked();
349
+ safeWriteSyncMocked(outputFile, "{}");
350
+ assert.strictEqual(readFileSync(outputFile, "utf-8"), "{}");
351
+ assert.strictEqual(getRecordedActivitiesMocked()[0].status, "completed");
352
+ } finally {
353
+ if (originalDebugMode === undefined) {
354
+ delete process.env.CDXGEN_DEBUG_MODE;
355
+ } else {
356
+ process.env.CDXGEN_DEBUG_MODE = originalDebugMode;
357
+ }
358
+ if (originalSecureMode === undefined) {
359
+ delete process.env.CDXGEN_SECURE_MODE;
360
+ } else {
361
+ process.env.CDXGEN_SECURE_MODE = originalSecureMode;
362
+ }
363
+ if (originalPermissionDescriptor) {
364
+ Object.defineProperty(
365
+ process,
366
+ "permission",
367
+ originalPermissionDescriptor,
368
+ );
369
+ } else {
370
+ delete process.permission;
371
+ }
372
+ rmSync(tmpRoot, { force: true, recursive: true });
373
+ }
374
+ });
375
+
376
+ it("cdxgenAgent records completed and failed network activity outcomes", async () => {
377
+ let setDryRunModeMocked;
378
+ try {
379
+ const {
380
+ cdxgenAgent,
381
+ getRecordedActivities: getRecordedActivitiesMocked,
382
+ resetRecordedActivities: resetRecordedActivitiesMocked,
383
+ setDryRunMode: mockedSetDryRunMode,
384
+ } = await esmock("./utils.js", {
385
+ got: {
386
+ default: {
387
+ extend: sinon.stub().callsFake((options) => {
388
+ return {
389
+ hooks: options.hooks,
390
+ };
391
+ }),
392
+ },
393
+ },
394
+ });
395
+ setDryRunModeMocked = mockedSetDryRunMode;
396
+ const afterResponseHook = cdxgenAgent.hooks.afterResponse[0];
397
+ const beforeErrorHook = cdxgenAgent.hooks.beforeError[0];
398
+
399
+ setDryRunModeMocked(true);
400
+ resetRecordedActivitiesMocked();
401
+ const successUrl = "https://example.com/success";
402
+ const successOptions = {
403
+ context: {
404
+ activityTarget: successUrl,
405
+ },
406
+ url: new URL(successUrl),
407
+ };
408
+ afterResponseHook({
409
+ request: {
410
+ options: successOptions,
411
+ },
412
+ statusCode: 200,
413
+ url: successUrl,
414
+ });
415
+ assert.strictEqual(getRecordedActivitiesMocked()[0].status, "completed");
416
+ assert.strictEqual(getRecordedActivitiesMocked()[0].target, successUrl);
417
+
418
+ resetRecordedActivitiesMocked();
419
+ const failureUrl = "https://example.com/failure";
420
+ const failureOptions = {
421
+ context: {
422
+ activityTarget: failureUrl,
423
+ },
424
+ url: new URL(failureUrl),
425
+ };
426
+ const returnedError = beforeErrorHook({
427
+ message: "Request failed with status code 500",
428
+ options: failureOptions,
429
+ });
430
+ assert.match(returnedError.message, /status code 500/);
431
+ const failureActivities = getRecordedActivitiesMocked();
432
+ assert.strictEqual(failureActivities.length, 1);
433
+ assert.strictEqual(failureActivities[0].status, "failed");
434
+ assert.strictEqual(failureActivities[0].target, failureUrl);
435
+ } finally {
436
+ if (setDryRunModeMocked) {
437
+ setDryRunModeMocked(false);
438
+ }
439
+ }
440
+ });
441
+
262
442
  it("safeSpawnSync() logs container python notices to stdout", () => {
263
443
  const originalConsoleLog = console.log;
264
444
  const originalConsoleWarn = console.warn;
@@ -278,10 +458,12 @@ it("safeSpawnSync() logs container python notices to stdout", () => {
278
458
  try {
279
459
  safeSpawnSync("python-cdxgen-test", ["-c", "pass"], {});
280
460
  safeSpawnSync("python-cdxgen-test", ["-c", "pass"], {});
281
- assert.deepStrictEqual(logs, [
282
- "Running python command without '-S' argument.",
283
- ]);
284
- assert.deepStrictEqual(warnings, []);
461
+ assert.strictEqual(logs.length + warnings.length, 1);
462
+ assert.ok(
463
+ [...logs, ...warnings].some((message) =>
464
+ message.includes("Running python command without '-S' argument."),
465
+ ),
466
+ );
285
467
  } finally {
286
468
  console.log = originalConsoleLog;
287
469
  console.warn = originalConsoleWarn;
@@ -1798,7 +1980,7 @@ it("parse cargo lock", async () => {
1798
1980
  version: "0.5.2",
1799
1981
  hashes: [
1800
1982
  {
1801
- alg: "SHA-384",
1983
+ alg: "SHA-256",
1802
1984
  content:
1803
1985
  "6a07677093120a02583717b6dd1ef81d8de1e8d01bd226c83f0f9bdf3e56bb3a",
1804
1986
  },
@@ -1835,7 +2017,7 @@ it("parse cargo lock", async () => {
1835
2017
  version: "0.3.0",
1836
2018
  hashes: [
1837
2019
  {
1838
- alg: "SHA-384",
2020
+ alg: "SHA-256",
1839
2021
  content:
1840
2022
  "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570",
1841
2023
  },
@@ -1998,167 +2180,305 @@ dependencies = ["does-not-exist"]
1998
2180
  it("parse cargo toml", async () => {
1999
2181
  assert.deepStrictEqual(await parseCargoTomlData(null), []);
2000
2182
  let dep_list = await parseCargoTomlData("./test/data/Cargo1.toml");
2001
- assert.deepStrictEqual(dep_list.length, 4);
2002
- assert.deepStrictEqual(dep_list, [
2003
- {
2004
- author: "The Rust Project Developers",
2005
- group: "",
2006
- name: "unwind",
2007
- version: "0.0.0",
2008
- properties: [{ name: "SrcFile", value: "./test/data/Cargo1.toml" }],
2009
- evidence: {
2010
- identity: {
2011
- field: "purl",
2012
- confidence: 0.5,
2013
- methods: [
2014
- {
2015
- technique: "manifest-analysis",
2016
- confidence: 0.5,
2017
- value: "./test/data/Cargo1.toml",
2018
- },
2019
- ],
2020
- },
2021
- },
2022
- purl: "pkg:cargo/unwind@0.0.0",
2023
- "bom-ref": "pkg:cargo/unwind@0.0.0",
2024
- type: "library",
2025
- },
2026
- {
2027
- name: "libc",
2028
- version: "0.2.79",
2029
- properties: [{ name: "SrcFile", value: "./test/data/Cargo1.toml" }],
2030
- evidence: {
2031
- identity: {
2032
- field: "purl",
2033
- confidence: 0.5,
2034
- methods: [
2035
- {
2036
- technique: "manifest-analysis",
2037
- confidence: 0.5,
2038
- value: "./test/data/Cargo1.toml",
2039
- },
2040
- ],
2041
- },
2042
- },
2043
- purl: "pkg:cargo/libc@0.2.79",
2044
- "bom-ref": "pkg:cargo/libc@0.2.79",
2045
- type: "library",
2046
- },
2047
- {
2048
- name: "compiler_builtins",
2049
- version: "0.1.0",
2050
- properties: [{ name: "SrcFile", value: "./test/data/Cargo1.toml" }],
2051
- evidence: {
2052
- identity: {
2053
- field: "purl",
2054
- confidence: 0.5,
2055
- methods: [
2056
- {
2057
- technique: "manifest-analysis",
2058
- confidence: 0.5,
2059
- value: "./test/data/Cargo1.toml",
2060
- },
2061
- ],
2062
- },
2063
- },
2064
- purl: "pkg:cargo/compiler_builtins@0.1.0",
2065
- "bom-ref": "pkg:cargo/compiler_builtins@0.1.0",
2066
- type: "library",
2067
- },
2068
- {
2069
- name: "cfg-if",
2070
- version: "0.1.8",
2071
- properties: [{ name: "SrcFile", value: "./test/data/Cargo1.toml" }],
2072
- evidence: {
2073
- identity: {
2074
- field: "purl",
2075
- confidence: 0.5,
2076
- methods: [
2077
- {
2078
- technique: "manifest-analysis",
2079
- confidence: 0.5,
2080
- value: "./test/data/Cargo1.toml",
2081
- },
2082
- ],
2083
- },
2084
- },
2085
- purl: "pkg:cargo/cfg-if@0.1.8",
2086
- "bom-ref": "pkg:cargo/cfg-if@0.1.8",
2087
- type: "library",
2088
- },
2089
- ]);
2183
+ assert.ok(dep_list.length >= 6);
2184
+ assert.strictEqual(dep_list[0].name, "unwind");
2185
+ const coreDep = dep_list.find((pkg) => pkg.name === "core");
2186
+ assert.ok(coreDep);
2187
+ assert.strictEqual(coreDep.version, "path+../core");
2188
+ assert.strictEqual(
2189
+ coreDep.properties.find((property) => property.name === "cdx:cargo:path")
2190
+ ?.value,
2191
+ "../core",
2192
+ );
2193
+ assert.strictEqual(
2194
+ coreDep.properties.find(
2195
+ (property) => property.name === "cdx:cargo:dependencyKind",
2196
+ )?.value,
2197
+ "runtime",
2198
+ );
2199
+ const libcDep = dep_list.find((pkg) => pkg.name === "libc");
2200
+ assert.ok(libcDep);
2201
+ assert.strictEqual(
2202
+ libcDep.properties.find(
2203
+ (property) => property.name === "cdx:cargo:defaultFeatures",
2204
+ )?.value,
2205
+ "false",
2206
+ );
2207
+ assert.strictEqual(
2208
+ libcDep.properties.find(
2209
+ (property) => property.name === "cdx:cargo:dependencyFeatures",
2210
+ )?.value,
2211
+ '["rustc-dep-of-std"]',
2212
+ );
2213
+ const ccDep = dep_list.find((pkg) => pkg.name === "cc");
2214
+ assert.ok(ccDep);
2215
+ assert.strictEqual(
2216
+ ccDep.properties.find(
2217
+ (property) => property.name === "cdx:cargo:dependencyKind",
2218
+ )?.value,
2219
+ "build",
2220
+ );
2090
2221
  dep_list = await parseCargoTomlData("./test/data/Cargo2.toml");
2091
- assert.deepStrictEqual(dep_list.length, 3);
2092
- assert.deepStrictEqual(dep_list, [
2093
- {
2094
- group: "",
2095
- name: "quiche-fuzz",
2096
- version: "0.1.0",
2097
- author: "Alessandro Ghedini <alessandro@ghedini.me>",
2098
- properties: [{ name: "SrcFile", value: "./test/data/Cargo2.toml" }],
2099
- evidence: {
2100
- identity: {
2101
- field: "purl",
2102
- confidence: 0.5,
2103
- methods: [
2104
- {
2105
- technique: "manifest-analysis",
2106
- confidence: 0.5,
2107
- value: "./test/data/Cargo2.toml",
2108
- },
2109
- ],
2110
- },
2111
- },
2112
- purl: "pkg:cargo/quiche-fuzz@0.1.0",
2113
- "bom-ref": "pkg:cargo/quiche-fuzz@0.1.0",
2114
- type: "library",
2115
- },
2222
+ assert.deepStrictEqual(dep_list.length, 4);
2223
+ assert.strictEqual(dep_list[0].name, "quiche-fuzz");
2224
+ const quicheDep = dep_list.find((pkg) => pkg.name === "quiche");
2225
+ assert.ok(quicheDep);
2226
+ assert.strictEqual(quicheDep.version, "path+../quiche");
2227
+ assert.strictEqual(
2228
+ quicheDep.properties.find((property) => property.name === "cdx:cargo:path")
2229
+ ?.value,
2230
+ "../quiche",
2231
+ );
2232
+ assert.strictEqual(
2233
+ quicheDep.properties.find(
2234
+ (property) => property.name === "cdx:cargo:dependencyFeatures",
2235
+ )?.value,
2236
+ '["fuzzing"]',
2237
+ );
2238
+ const libfuzzerDep = dep_list.find((pkg) => pkg.name === "libfuzzer-sys");
2239
+ assert.ok(libfuzzerDep);
2240
+ assert.strictEqual(
2241
+ libfuzzerDep.properties.find(
2242
+ (property) => property.name === "cdx:cargo:git",
2243
+ )?.value,
2244
+ "https://github.com/rust-fuzz/libfuzzer-sys.git",
2245
+ );
2246
+ dep_list = await parseCargoTomlData("./test/data/Cargo3.toml", true);
2247
+ assert.ok(dep_list.length >= 10);
2248
+ });
2249
+
2250
+ it("parse cargo toml target and dev dependency metadata", async () => {
2251
+ const tmpDir = mkdtempSync(path.join(tmpdir(), "cdxgen-cargo-"));
2252
+ const cargoTomlFile = path.join(tmpDir, "Cargo.toml");
2253
+ writeFileSync(
2254
+ cargoTomlFile,
2255
+ `[package]
2256
+ name = "demo"
2257
+ version = "1.0.0"
2258
+
2259
+ [dependencies]
2260
+ serde = { version = "1.0.0", optional = true }
2261
+
2262
+ [dev-dependencies]
2263
+ insta = "1.0.0"
2264
+
2265
+ [target.'cfg(target_os = "linux")'.dependencies]
2266
+ nix = "0.29.0"
2267
+
2268
+ [target.'cfg(target_os = "linux")'.build-dependencies]
2269
+ bindgen = { version = "0.70.0", default-features = false }
2270
+ `,
2271
+ );
2272
+ try {
2273
+ const depList = await parseCargoTomlData(cargoTomlFile);
2274
+ const serdeDep = depList.find((pkg) => pkg.name === "serde");
2275
+ const instaDep = depList.find((pkg) => pkg.name === "insta");
2276
+ const bindgenDep = depList.find((pkg) => pkg.name === "bindgen");
2277
+ assert.strictEqual(serdeDep.scope, "optional");
2278
+ assert.strictEqual(
2279
+ serdeDep.properties.find(
2280
+ (property) => property.name === "cdx:cargo:optional",
2281
+ )?.value,
2282
+ "true",
2283
+ );
2284
+ assert.strictEqual(instaDep.scope, "excluded");
2285
+ assert.strictEqual(
2286
+ bindgenDep.properties.find(
2287
+ (property) => property.name === "cdx:cargo:dependencyKind",
2288
+ )?.value,
2289
+ "build",
2290
+ );
2291
+ assert.strictEqual(
2292
+ bindgenDep.properties.find(
2293
+ (property) => property.name === "cdx:cargo:target",
2294
+ )?.value,
2295
+ 'cfg(target_os = "linux")',
2296
+ );
2297
+ assert.strictEqual(
2298
+ bindgenDep.properties.find(
2299
+ (property) => property.name === "cdx:cargo:defaultFeatures",
2300
+ )?.value,
2301
+ "false",
2302
+ );
2303
+ } finally {
2304
+ rmSync(tmpDir, { force: true, recursive: true });
2305
+ }
2306
+ });
2307
+
2308
+ it("parse cargo virtual workspace with inherited package and dependency metadata", async () => {
2309
+ const workspaceDir = "./test/data/cargo-workspace-repotest";
2310
+ const workspaceToml = path.join(workspaceDir, "Cargo.toml");
2311
+ const normalizePathForAssertion = (value) => value?.replaceAll("\\", "/");
2312
+ const depList = await parseCargoTomlData(workspaceToml);
2313
+ assert.strictEqual(depList[0].name, "cargo-workspace-repotest");
2314
+ assert.strictEqual(depList[0].version, "workspace");
2315
+ assert.strictEqual(
2316
+ depList[0].properties.find(
2317
+ (property) => property.name === "cdx:cargo:manifestMode",
2318
+ )?.value,
2319
+ "virtual-workspace",
2320
+ );
2321
+ const memberPackage = depList.find((pkg) => pkg.name === "cli");
2322
+ assert.ok(memberPackage);
2323
+ assert.strictEqual(memberPackage.version, "1.2.3");
2324
+ assert.strictEqual(memberPackage.license, "Apache-2.0");
2325
+ assert.strictEqual(
2326
+ memberPackage.repository?.url,
2327
+ "https://github.com/example/cargo-workspace-repotest",
2328
+ );
2329
+ const coreDep = depList.find(
2330
+ (pkg) =>
2331
+ pkg.name === "core" &&
2332
+ pkg.properties.some(
2333
+ (property) => property.name === "cdx:cargo:workspaceDependency",
2334
+ ),
2335
+ );
2336
+ assert.ok(coreDep);
2337
+ assert.strictEqual(coreDep.version, "path+crates/core");
2338
+ assert.strictEqual(
2339
+ coreDep.properties.find(
2340
+ (property) => property.name === "cdx:cargo:workspaceDependency",
2341
+ )?.value,
2342
+ "true",
2343
+ );
2344
+ assert.strictEqual(
2345
+ coreDep.properties.find(
2346
+ (property) => property.name === "cdx:cargo:workspaceDependencyResolved",
2347
+ )?.value,
2348
+ "true",
2349
+ );
2350
+ assert.strictEqual(
2351
+ coreDep.properties.find(
2352
+ (property) => property.name === "cdx:cargo:resolvedWorkspaceMember",
2353
+ )?.value,
2354
+ "core",
2355
+ );
2356
+ assert.ok(
2357
+ normalizePathForAssertion(
2358
+ coreDep.properties.find(
2359
+ (property) => property.name === "cdx:cargo:resolvedMemberPath",
2360
+ )?.value,
2361
+ )?.endsWith("/test/data/cargo-workspace-repotest/crates/core/Cargo.toml"),
2362
+ );
2363
+ const buildHelperDep = depList.find(
2364
+ (pkg) =>
2365
+ pkg.name === "build-helper" &&
2366
+ pkg.properties.some(
2367
+ (property) => property.name === "cdx:cargo:workspaceDependency",
2368
+ ),
2369
+ );
2370
+ assert.ok(buildHelperDep);
2371
+ assert.strictEqual(buildHelperDep.version, "path+crates/build-helper");
2372
+ assert.strictEqual(
2373
+ buildHelperDep.properties.find(
2374
+ (property) => property.name === "cdx:cargo:dependencyKind",
2375
+ )?.value,
2376
+ "build",
2377
+ );
2378
+ assert.strictEqual(
2379
+ buildHelperDep.properties.find(
2380
+ (property) => property.name === "cdx:cargo:workspaceDependencyResolved",
2381
+ )?.value,
2382
+ "true",
2383
+ );
2384
+ assert.strictEqual(
2385
+ buildHelperDep.properties.find(
2386
+ (property) => property.name === "cdx:cargo:resolvedWorkspaceMember",
2387
+ )?.value,
2388
+ "build-helper",
2389
+ );
2390
+ assert.ok(
2391
+ normalizePathForAssertion(
2392
+ buildHelperDep.properties.find(
2393
+ (property) => property.name === "cdx:cargo:resolvedMemberPath",
2394
+ )?.value,
2395
+ )?.endsWith(
2396
+ "/test/data/cargo-workspace-repotest/crates/build-helper/Cargo.toml",
2397
+ ),
2398
+ );
2399
+ const ccDep = depList.find((pkg) => pkg.name === "cc");
2400
+ assert.ok(ccDep);
2401
+ assert.strictEqual(ccDep.version, "1.2.0");
2402
+ });
2403
+
2404
+ it("normalize visited cargo manifest paths across relative and absolute inputs", async () => {
2405
+ const workspaceToml = "./test/data/cargo-workspace-repotest/Cargo.toml";
2406
+ const absoluteWorkspaceToml = path.resolve(workspaceToml);
2407
+ const parsedPackages = await parseCargoTomlData(
2408
+ absoluteWorkspaceToml,
2409
+ false,
2410
+ {},
2116
2411
  {
2117
- name: "lazy_static",
2118
- version: "1",
2119
- properties: [{ name: "SrcFile", value: "./test/data/Cargo2.toml" }],
2120
- evidence: {
2121
- identity: {
2122
- field: "purl",
2123
- confidence: 0.5,
2124
- methods: [
2125
- {
2126
- technique: "manifest-analysis",
2127
- confidence: 0.5,
2128
- value: "./test/data/Cargo2.toml",
2129
- },
2130
- ],
2131
- },
2132
- },
2133
- purl: "pkg:cargo/lazy_static@1",
2134
- "bom-ref": "pkg:cargo/lazy_static@1",
2135
- type: "library",
2412
+ visitedCargoTomlFiles: new Set([workspaceToml]),
2136
2413
  },
2414
+ );
2415
+ assert.deepStrictEqual(parsedPackages, []);
2416
+ const dependencyGraph = parseCargoManifestDependencyData(
2417
+ absoluteWorkspaceToml,
2137
2418
  {
2138
- name: "libfuzzer-sys",
2139
- version: "git+https://github.com/rust-fuzz/libfuzzer-sys.git",
2140
- properties: [{ name: "SrcFile", value: "./test/data/Cargo2.toml" }],
2141
- evidence: {
2142
- identity: {
2143
- field: "purl",
2144
- confidence: 0.5,
2145
- methods: [
2146
- {
2147
- technique: "manifest-analysis",
2148
- confidence: 0.5,
2149
- value: "./test/data/Cargo2.toml",
2150
- },
2151
- ],
2152
- },
2153
- },
2154
- purl: "pkg:cargo/libfuzzer-sys@git%2Bhttps:%2F%2Fgithub.com%2Frust-fuzz%2Flibfuzzer-sys.git",
2155
- "bom-ref":
2156
- "pkg:cargo/libfuzzer-sys@git+https://github.com/rust-fuzz/libfuzzer-sys.git",
2157
- type: "library",
2419
+ visitedCargoTomlDependencyGraphFiles: new Set([workspaceToml]),
2158
2420
  },
2159
- ]);
2160
- dep_list = await parseCargoTomlData("./test/data/Cargo3.toml", true);
2161
- assert.deepStrictEqual(dep_list.length, 10);
2421
+ );
2422
+ assert.deepStrictEqual(dependencyGraph, []);
2423
+ });
2424
+
2425
+ it("build cargo workspace manifest dependency graph with member-to-member edges", () => {
2426
+ const tmpDir = mkdtempSync(
2427
+ path.join(tmpdir(), "cdxgen-cargo-workspace-graph-"),
2428
+ );
2429
+ const workspaceToml = path.join(tmpDir, "Cargo.toml");
2430
+ const coreDir = path.join(tmpDir, "crates", "core");
2431
+ const cliDir = path.join(tmpDir, "crates", "cli");
2432
+ mkdirSync(coreDir, { recursive: true });
2433
+ mkdirSync(cliDir, { recursive: true });
2434
+ writeFileSync(
2435
+ workspaceToml,
2436
+ `[workspace]
2437
+ members = ["crates/*"]
2438
+
2439
+ [workspace.package]
2440
+ version = "1.2.3"
2441
+
2442
+ [workspace.dependencies]
2443
+ core = { path = "crates/core" }
2444
+ `,
2445
+ );
2446
+ writeFileSync(
2447
+ path.join(coreDir, "Cargo.toml"),
2448
+ `[package]
2449
+ name = "core"
2450
+ version.workspace = true
2451
+ `,
2452
+ );
2453
+ writeFileSync(
2454
+ path.join(cliDir, "Cargo.toml"),
2455
+ `[package]
2456
+ name = "cli"
2457
+ version.workspace = true
2458
+
2459
+ [dependencies]
2460
+ core = { workspace = true }
2461
+ `,
2462
+ );
2463
+
2464
+ try {
2465
+ const dependencyGraph = parseCargoManifestDependencyData(workspaceToml);
2466
+ const workspaceRef = `pkg:cargo/${path.basename(tmpDir)}@workspace`;
2467
+ const cliRef = "pkg:cargo/cli@1.2.3";
2468
+ const coreRef = "pkg:cargo/core@1.2.3";
2469
+ const workspaceNode = dependencyGraph.find(
2470
+ (dependency) => dependency.ref === workspaceRef,
2471
+ );
2472
+ const cliNode = dependencyGraph.find(
2473
+ (dependency) => dependency.ref === cliRef,
2474
+ );
2475
+ assert.ok(workspaceNode);
2476
+ assert.deepStrictEqual(workspaceNode.dependsOn, [cliRef, coreRef]);
2477
+ assert.ok(cliNode);
2478
+ assert.deepStrictEqual(cliNode.dependsOn, [coreRef]);
2479
+ } finally {
2480
+ rmSync(tmpDir, { force: true, recursive: true });
2481
+ }
2162
2482
  });
2163
2483
 
2164
2484
  it("parse cargo auditable data", async () => {
@@ -2185,33 +2505,74 @@ it("get crates metadata", async () => {
2185
2505
  },
2186
2506
  ]);
2187
2507
  assert.deepStrictEqual(dep_list.length, 1);
2188
- assert.deepStrictEqual(dep_list[0], {
2189
- group: "",
2190
- name: "abscissa_core",
2191
- version: "0.5.2",
2192
- _integrity:
2193
- "sha256-6a07677093120a02583717b6dd1ef81d8de1e8d01bd226c83f0f9bdf3e56bb3a",
2194
- description:
2195
- "Application microframework with support for command-line option parsing,\nconfiguration, error handling, logging, and terminal interactions.\nThis crate contains the framework's core functionality.\n",
2196
- distribution: {
2197
- url: "https://crates.io/api/v1/crates/abscissa_core/0.5.2/download",
2198
- },
2199
- license: "Apache-2.0",
2200
- repository: {
2201
- url: "https://github.com/iqlusioninc/abscissa/tree/main/core/",
2202
- },
2203
- homepage: { url: "https://github.com/iqlusioninc/abscissa/" },
2204
- properties: [
2205
- { name: "cdx:cargo:crate_id", value: "207912" },
2206
- { name: "cdx:cargo:latest_version", value: "0.9.0" },
2508
+ assert.strictEqual(dep_list[0].group, "");
2509
+ assert.strictEqual(dep_list[0].name, "abscissa_core");
2510
+ assert.strictEqual(dep_list[0].version, "0.5.2");
2511
+ assert.strictEqual(dep_list[0].license, "Apache-2.0");
2512
+ assert.strictEqual(
2513
+ dep_list[0].distribution?.url,
2514
+ "https://crates.io/api/v1/crates/abscissa_core/0.5.2/download",
2515
+ );
2516
+ assert.ok(
2517
+ dep_list[0].properties.find(
2518
+ (property) => property.name === "cdx:cargo:crate_id",
2519
+ ),
2520
+ );
2521
+ assert.ok(
2522
+ dep_list[0].properties.find(
2523
+ (property) => property.name === "cdx:cargo:latest_version",
2524
+ ),
2525
+ );
2526
+ assert.ok(
2527
+ dep_list[0].properties.find(
2528
+ (property) => property.name === "cdx:cargo:versionCount",
2529
+ ),
2530
+ );
2531
+ assert.ok(
2532
+ dep_list[0].properties.find(
2533
+ (property) => property.name === "cdx:cargo:publishTime",
2534
+ ),
2535
+ );
2536
+ }, 20000);
2537
+
2538
+ it("parse cargo lock integrity using matching sha256 and sha384 hash algorithms", async () => {
2539
+ const tmpDir = mkdtempSync(path.join(tmpdir(), "cdxgen-cargo-lock-"));
2540
+ const cargoLockFile = path.join(tmpDir, "Cargo.lock");
2541
+ writeFileSync(
2542
+ cargoLockFile,
2543
+ `version = 3
2544
+
2545
+ [[package]]
2546
+ name = "sha256-demo"
2547
+ version = "1.0.0"
2548
+ checksum = "${"a".repeat(64)}"
2549
+
2550
+ [[package]]
2551
+ name = "sha384-demo"
2552
+ version = "2.0.0"
2553
+ checksum = "${"b".repeat(96)}"
2554
+ `,
2555
+ );
2556
+ try {
2557
+ const depList = await parseCargoData(cargoLockFile, true);
2558
+ const sha256Demo = depList.find((pkg) => pkg.name === "sha256-demo");
2559
+ const sha384Demo = depList.find((pkg) => pkg.name === "sha384-demo");
2560
+ assert.deepStrictEqual(sha256Demo.hashes, [
2207
2561
  {
2208
- name: "cdx:cargo:features",
2209
- value:
2210
- '{"application":["config","generational-arena","trace","options","semver/serde","terminal"],"config":["secrets","serde","terminal","toml"],"default":["application","signals","secrets","testing","time"],"gimli-backtrace":["backtrace/gimli-symbolize","color-backtrace/gimli-symbolize"],"options":["gumdrop"],"secrets":["secrecy"],"signals":["libc","signal-hook"],"terminal":["color-backtrace","termcolor"],"testing":["regex","wait-timeout"],"time":["chrono"],"trace":["tracing","tracing-log","tracing-subscriber"]}',
2562
+ alg: "SHA-256",
2563
+ content: "a".repeat(64),
2211
2564
  },
2212
- ],
2213
- });
2214
- }, 20000);
2565
+ ]);
2566
+ assert.deepStrictEqual(sha384Demo.hashes, [
2567
+ {
2568
+ alg: "SHA-384",
2569
+ content: "b".repeat(96),
2570
+ },
2571
+ ]);
2572
+ } finally {
2573
+ rmSync(tmpDir, { force: true, recursive: true });
2574
+ }
2575
+ });
2215
2576
 
2216
2577
  it("parse pub lock", async () => {
2217
2578
  assert.deepStrictEqual(await parsePubLockData(null), []);
@@ -2772,10 +3133,60 @@ it("parse github actions workflow data", () => {
2772
3133
  dep_list = parseGitHubWorkflowData("./test/data/github-actions-tj.yaml");
2773
3134
  assert.deepStrictEqual(dep_list.length, 4);
2774
3135
  dep_list = parseGitHubWorkflowData("./.github/workflows/repotests.yml");
2775
- assert.deepStrictEqual(dep_list.length, 91);
3136
+ assert.ok(dep_list.length > 0);
3137
+ assert.ok(
3138
+ dep_list.every((component) =>
3139
+ component.properties?.some(
3140
+ (property) =>
3141
+ property.name === "cdx:github:workflow:file" &&
3142
+ property.value === "./.github/workflows/repotests.yml",
3143
+ ),
3144
+ ),
3145
+ );
3146
+ assert.ok(
3147
+ dep_list.some((component) =>
3148
+ component.properties?.some(
3149
+ (property) =>
3150
+ property.name === "cdx:github:checkout:repository" &&
3151
+ property.value === "AppThreat/vulnerability-db",
3152
+ ),
3153
+ ),
3154
+ );
2776
3155
  });
2777
3156
  // biome-ignore-end lint/suspicious/noTemplateCurlyInString: fp
2778
3157
 
3158
+ it("parse github actions workflow data preserves cargo run steps for audit correlation", () => {
3159
+ const tmpDir = mkdtempSync(path.join(tmpdir(), "cdxgen-gha-utils-"));
3160
+ const workflowFile = path.join(tmpDir, "cargo.yml");
3161
+ writeFileSync(
3162
+ workflowFile,
3163
+ [
3164
+ "name: Cargo CI",
3165
+ "on: push",
3166
+ "jobs:",
3167
+ " build:",
3168
+ " runs-on: ubuntu-latest",
3169
+ " steps:",
3170
+ " - uses: dtolnay/rust-toolchain@stable",
3171
+ " - run: cargo build --workspace && cargo test --workspace",
3172
+ ].join("\n"),
3173
+ );
3174
+ try {
3175
+ const depList = parseGitHubWorkflowData(workflowFile);
3176
+ assert.ok(
3177
+ depList.some((component) =>
3178
+ component.properties?.some(
3179
+ (property) =>
3180
+ property.name === "cdx:github:step:usesCargo" &&
3181
+ property.value === "true",
3182
+ ),
3183
+ ),
3184
+ );
3185
+ } finally {
3186
+ rmSync(tmpDir, { force: true, recursive: true });
3187
+ }
3188
+ });
3189
+
2779
3190
  it("parse cs pkg data", () => {
2780
3191
  assert.deepStrictEqual(parseCsPkgData(null), []);
2781
3192
  const dep_list = parseCsPkgData(