@cyclonedx/cdxgen 12.3.1 → 12.3.3

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 (87) hide show
  1. package/README.md +6 -0
  2. package/bin/cdxgen.js +1 -2
  3. package/data/rules/ai-agent-governance.yaml +43 -0
  4. package/data/rules/ci-permissions.yaml +132 -0
  5. package/data/rules/dependency-sources.yaml +65 -5
  6. package/data/rules/mcp-servers.yaml +36 -2
  7. package/data/rules/package-integrity.yaml +22 -0
  8. package/lib/cli/index.js +436 -56
  9. package/lib/cli/index.poku.js +875 -2
  10. package/lib/helpers/agentFormulationParser.js +10 -3
  11. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  12. package/lib/helpers/aiInventory.js +262 -0
  13. package/lib/helpers/aiInventory.poku.js +111 -0
  14. package/lib/helpers/analyzer.js +413 -54
  15. package/lib/helpers/analyzer.poku.js +117 -0
  16. package/lib/helpers/auditCategories.js +76 -0
  17. package/lib/helpers/chromextutils.js +25 -3
  18. package/lib/helpers/chromextutils.poku.js +68 -0
  19. package/lib/helpers/ciParsers/githubActions.js +79 -0
  20. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  21. package/lib/helpers/communityAiConfigParser.js +15 -5
  22. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  23. package/lib/helpers/depsUtils.js +5 -0
  24. package/lib/helpers/depsUtils.poku.js +55 -0
  25. package/lib/helpers/display.js +50 -24
  26. package/lib/helpers/display.poku.js +70 -58
  27. package/lib/helpers/formulationParsers.js +26 -6
  28. package/lib/helpers/jsonLike.js +21 -20
  29. package/lib/helpers/jsonLike.poku.js +34 -0
  30. package/lib/helpers/mcpConfigParser.js +32 -16
  31. package/lib/helpers/mcpConfigParser.poku.js +104 -0
  32. package/lib/helpers/mcpDiscovery.js +13 -23
  33. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  34. package/lib/helpers/propertySanitizer.js +121 -0
  35. package/lib/helpers/utils.js +953 -41
  36. package/lib/helpers/utils.poku.js +901 -1
  37. package/lib/managers/binary.js +16 -0
  38. package/lib/managers/binary.poku.js +1 -0
  39. package/lib/managers/docker.js +240 -16
  40. package/lib/managers/docker.poku.js +1142 -2
  41. package/lib/server/server.js +7 -4
  42. package/lib/server/server.poku.js +36 -1
  43. package/lib/stages/postgen/annotator.js +2 -1
  44. package/lib/stages/postgen/annotator.poku.js +15 -0
  45. package/lib/stages/postgen/auditBom.js +12 -6
  46. package/lib/stages/postgen/auditBom.poku.js +755 -6
  47. package/lib/stages/postgen/postgen.js +229 -6
  48. package/lib/stages/postgen/postgen.poku.js +180 -0
  49. package/package.json +2 -1
  50. package/types/lib/cli/index.d.ts +1 -0
  51. package/types/lib/cli/index.d.ts.map +1 -1
  52. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  53. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  54. package/types/lib/helpers/aiInventory.d.ts +23 -0
  55. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  56. package/types/lib/helpers/analyzer.d.ts +5 -0
  57. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  58. package/types/lib/helpers/auditCategories.d.ts +12 -0
  59. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  60. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  61. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  62. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  63. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  64. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  65. package/types/lib/helpers/display.d.ts +1 -0
  66. package/types/lib/helpers/display.d.ts.map +1 -1
  67. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  68. package/types/lib/helpers/jsonLike.d.ts +4 -0
  69. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  70. package/types/lib/helpers/mcp.d.ts +29 -0
  71. package/types/lib/helpers/mcp.d.ts.map +1 -0
  72. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  73. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  74. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  75. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  76. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  77. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  78. package/types/lib/helpers/utils.d.ts +31 -0
  79. package/types/lib/helpers/utils.d.ts.map +1 -1
  80. package/types/lib/managers/binary.d.ts.map +1 -1
  81. package/types/lib/managers/docker.d.ts +3 -0
  82. package/types/lib/managers/docker.d.ts.map +1 -1
  83. package/types/lib/server/server.d.ts +1 -0
  84. package/types/lib/server/server.d.ts.map +1 -1
  85. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  86. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  87. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -1,10 +1,12 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import {
3
+ chmodSync,
3
4
  existsSync,
4
5
  mkdirSync,
5
6
  mkdtempSync,
6
7
  readFileSync,
7
8
  rmSync,
9
+ symlinkSync,
8
10
  unlinkSync,
9
11
  writeFileSync,
10
12
  } from "node:fs";
@@ -21,10 +23,13 @@ import { parse as loadYaml } from "yaml";
21
23
 
22
24
  import { validateRefs } from "../validator/bomValidator.js";
23
25
  import {
26
+ attachIdentityTools,
24
27
  buildObjectForCocoaPod,
25
28
  buildObjectForGradleModule,
29
+ collectExecutables,
26
30
  convertOSQueryResults,
27
31
  encodeForPurl,
32
+ extractToolRefs,
28
33
  findLicenseId,
29
34
  findPnpmPackagePath,
30
35
  getCratesMetadata,
@@ -59,6 +64,7 @@ import {
59
64
  parseCmakeDotFile,
60
65
  parseCmakeLikeFile,
61
66
  parseCocoaDependency,
67
+ parseColliderLockData,
62
68
  parseComposerJson,
63
69
  parseComposerLock,
64
70
  parseConanData,
@@ -1276,6 +1282,84 @@ it("get py metadata", async () => {
1276
1282
  ]);
1277
1283
  }, 240000);
1278
1284
 
1285
+ it("get py metadata adds distribution external references", async () => {
1286
+ const agentGetStub = sinon.stub().resolves({
1287
+ body: {
1288
+ info: {
1289
+ author: "",
1290
+ author_email: "",
1291
+ classifiers: [],
1292
+ license: "",
1293
+ license_expression: "",
1294
+ name: "requests",
1295
+ summary: "HTTP client",
1296
+ version: "2.31.0",
1297
+ },
1298
+ releases: {
1299
+ "2.31.0": [
1300
+ {
1301
+ digests: { sha256: "abc123" },
1302
+ filename: "requests-2.31.0-py3-none-any.whl",
1303
+ packagetype: "bdist_wheel",
1304
+ url: "https://files.pythonhosted.org/packages/example/requests-2.31.0-py3-none-any.whl",
1305
+ },
1306
+ {
1307
+ digests: { sha256: "def456" },
1308
+ filename: "requests-2.31.0.tar.gz",
1309
+ packagetype: "sdist",
1310
+ url: "https://files.pythonhosted.org/packages/example/requests-2.31.0.tar.gz",
1311
+ },
1312
+ ],
1313
+ },
1314
+ },
1315
+ });
1316
+ const { getPyMetadata: mockedGetPyMetadata } = await esmock("./utils.js", {
1317
+ got: {
1318
+ default: {
1319
+ extend: sinon.stub().returns({ get: agentGetStub }),
1320
+ },
1321
+ },
1322
+ });
1323
+ const data = await mockedGetPyMetadata(
1324
+ [
1325
+ {
1326
+ externalReferences: [
1327
+ {
1328
+ type: "website",
1329
+ url: "https://example.com/requests",
1330
+ },
1331
+ ],
1332
+ group: "",
1333
+ name: "requests",
1334
+ version: "2.31.0",
1335
+ },
1336
+ ],
1337
+ true,
1338
+ );
1339
+ assert.strictEqual(data.length, 1);
1340
+ assert.ok(
1341
+ data[0].externalReferences?.some(
1342
+ (reference) => reference.type === "website",
1343
+ ),
1344
+ );
1345
+ assert.ok(
1346
+ data[0].externalReferences?.some(
1347
+ (reference) =>
1348
+ reference.type === "distribution" &&
1349
+ reference.url.endsWith(".whl") &&
1350
+ reference.comment === "requests-2.31.0-py3-none-any.whl",
1351
+ ),
1352
+ );
1353
+ assert.ok(
1354
+ data[0].externalReferences?.some(
1355
+ (reference) =>
1356
+ reference.type === "distribution" &&
1357
+ reference.url.endsWith(".tar.gz") &&
1358
+ reference.comment === "requests-2.31.0.tar.gz",
1359
+ ),
1360
+ );
1361
+ });
1362
+
1279
1363
  it("parseGoModData", async () => {
1280
1364
  let retMap = await parseGoModData(null);
1281
1365
  assert.deepStrictEqual(retMap, {});
@@ -2305,6 +2389,49 @@ bindgen = { version = "0.70.0", default-features = false }
2305
2389
  }
2306
2390
  });
2307
2391
 
2392
+ it("parse cargo toml captures git dependency metadata", async () => {
2393
+ const tmpDir = mkdtempSync(path.join(tmpdir(), "cdxgen-cargo-git-"));
2394
+ const cargoTomlFile = path.join(tmpDir, "Cargo.toml");
2395
+ writeFileSync(
2396
+ cargoTomlFile,
2397
+ `[package]
2398
+ name = "demo"
2399
+ version = "1.0.0"
2400
+
2401
+ [dependencies]
2402
+ git-crate = { git = "https://github.com/acme/git-crate.git", branch = "main" }
2403
+ `,
2404
+ );
2405
+ try {
2406
+ const depList = await parseCargoTomlData(cargoTomlFile);
2407
+ const gitDep = depList.find((pkg) => pkg.name === "git-crate");
2408
+ assert.ok(gitDep);
2409
+ assert.strictEqual(
2410
+ gitDep.version,
2411
+ "git+https://github.com/acme/git-crate.git",
2412
+ );
2413
+ assert.strictEqual(
2414
+ gitDep.properties.find((property) => property.name === "cdx:cargo:git")
2415
+ ?.value,
2416
+ "https://github.com/acme/git-crate.git",
2417
+ );
2418
+ assert.strictEqual(
2419
+ gitDep.properties.find(
2420
+ (property) => property.name === "cdx:cargo:gitBranch",
2421
+ )?.value,
2422
+ "main",
2423
+ );
2424
+ assert.strictEqual(
2425
+ gitDep.properties.find(
2426
+ (property) => property.name === "cdx:cargo:dependencyKind",
2427
+ )?.value,
2428
+ "runtime",
2429
+ );
2430
+ } finally {
2431
+ rmSync(tmpDir, { force: true, recursive: true });
2432
+ }
2433
+ });
2434
+
2308
2435
  it("parse cargo virtual workspace with inherited package and dependency metadata", async () => {
2309
2436
  const workspaceDir = "./test/data/cargo-workspace-repotest";
2310
2437
  const workspaceToml = path.join(workspaceDir, "Cargo.toml");
@@ -2825,6 +2952,221 @@ it("parse conan data", () => {
2825
2952
  });
2826
2953
  });
2827
2954
 
2955
+ it("parse collider lock data", () => {
2956
+ let colliderLockData = parseColliderLockData(null);
2957
+ assert.deepStrictEqual(colliderLockData.pkgList.length, 0);
2958
+ assert.deepStrictEqual(Object.keys(colliderLockData.dependencies).length, 0);
2959
+ assert.deepStrictEqual(
2960
+ colliderLockData.parentComponentDependencies.length,
2961
+ 0,
2962
+ );
2963
+
2964
+ colliderLockData = parseColliderLockData(
2965
+ readFileSync("./test/data/collider.lock", { encoding: "utf-8" }),
2966
+ "./test/data/collider.lock",
2967
+ );
2968
+ assert.deepStrictEqual(colliderLockData.pkgList.length, 3);
2969
+ assert.deepStrictEqual(colliderLockData.pkgList[0], {
2970
+ name: "fmt",
2971
+ version: "11.0.2",
2972
+ "bom-ref": "pkg:generic/fmt@11.0.2",
2973
+ externalReferences: [
2974
+ {
2975
+ type: "distribution",
2976
+ url: "https://packages.example.com/collider/v2/",
2977
+ },
2978
+ ],
2979
+ hashes: [
2980
+ {
2981
+ alg: "SHA-256",
2982
+ content:
2983
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2984
+ },
2985
+ ],
2986
+ properties: [
2987
+ { name: "SrcFile", value: "./test/data/collider.lock" },
2988
+ { name: "cdx:collider:dependencyKind", value: "direct" },
2989
+ {
2990
+ name: "cdx:collider:wrapHash",
2991
+ value:
2992
+ "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2993
+ },
2994
+ { name: "cdx:collider:hasWrapHash", value: "true" },
2995
+ {
2996
+ name: "cdx:collider:origin",
2997
+ value: "https://packages.example.com/collider/v2/",
2998
+ },
2999
+ { name: "cdx:collider:originScheme", value: "https" },
3000
+ { name: "cdx:collider:originHost", value: "packages.example.com" },
3001
+ ],
3002
+ purl: "pkg:generic/fmt@11.0.2",
3003
+ scope: "required",
3004
+ });
3005
+ assert.deepStrictEqual(colliderLockData.pkgList[1], {
3006
+ name: "spdlog",
3007
+ version: "1.15.0",
3008
+ "bom-ref": "pkg:generic/spdlog@1.15.0",
3009
+ externalReferences: [
3010
+ {
3011
+ type: "distribution",
3012
+ url: "https://packages.example.com/collider/v2/",
3013
+ },
3014
+ ],
3015
+ hashes: [
3016
+ {
3017
+ alg: "SHA-256",
3018
+ content:
3019
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
3020
+ },
3021
+ ],
3022
+ properties: [
3023
+ { name: "SrcFile", value: "./test/data/collider.lock" },
3024
+ { name: "cdx:collider:dependencyKind", value: "direct" },
3025
+ {
3026
+ name: "cdx:collider:wrapHash",
3027
+ value:
3028
+ "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
3029
+ },
3030
+ { name: "cdx:collider:hasWrapHash", value: "true" },
3031
+ {
3032
+ name: "cdx:collider:origin",
3033
+ value: "https://packages.example.com/collider/v2/",
3034
+ },
3035
+ { name: "cdx:collider:originScheme", value: "https" },
3036
+ { name: "cdx:collider:originHost", value: "packages.example.com" },
3037
+ ],
3038
+ purl: "pkg:generic/spdlog@1.15.0",
3039
+ scope: "required",
3040
+ });
3041
+ assert.deepStrictEqual(colliderLockData.pkgList[2], {
3042
+ name: "fast_float",
3043
+ version: "8.0.2",
3044
+ "bom-ref": "pkg:generic/fast_float@8.0.2",
3045
+ externalReferences: [
3046
+ {
3047
+ type: "distribution",
3048
+ url: "https://wrapdb.mesonbuild.com/v2/",
3049
+ },
3050
+ ],
3051
+ hashes: [
3052
+ {
3053
+ alg: "SHA-256",
3054
+ content:
3055
+ "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
3056
+ },
3057
+ ],
3058
+ properties: [
3059
+ { name: "SrcFile", value: "./test/data/collider.lock" },
3060
+ { name: "cdx:collider:dependencyKind", value: "transitive" },
3061
+ {
3062
+ name: "cdx:collider:wrapHash",
3063
+ value:
3064
+ "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
3065
+ },
3066
+ { name: "cdx:collider:hasWrapHash", value: "true" },
3067
+ {
3068
+ name: "cdx:collider:origin",
3069
+ value: "https://wrapdb.mesonbuild.com/v2/",
3070
+ },
3071
+ { name: "cdx:collider:originScheme", value: "https" },
3072
+ { name: "cdx:collider:originHost", value: "wrapdb.mesonbuild.com" },
3073
+ ],
3074
+ purl: "pkg:generic/fast_float@8.0.2",
3075
+ });
3076
+ assert.deepStrictEqual(colliderLockData.dependencies, {
3077
+ "pkg:generic/fmt@11.0.2": [],
3078
+ "pkg:generic/spdlog@1.15.0": [],
3079
+ "pkg:generic/fast_float@8.0.2": [],
3080
+ });
3081
+ assert.deepStrictEqual(colliderLockData.parentComponentDependencies, [
3082
+ "pkg:generic/fmt@11.0.2",
3083
+ "pkg:generic/spdlog@1.15.0",
3084
+ ]);
3085
+ });
3086
+
3087
+ it("parse collider lock data sanitizes origin metadata and tracks invalid wrap hashes", () => {
3088
+ const colliderLockData = parseColliderLockData(
3089
+ JSON.stringify({
3090
+ version: 1,
3091
+ dependencies: {
3092
+ "unsafe-origin": {
3093
+ version: "1.0.0",
3094
+ wrap_hash:
3095
+ "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
3096
+ origin: "https://user:pass@example.com/private/v2/?token=secret#frag",
3097
+ },
3098
+ },
3099
+ packages: {
3100
+ malformed: {
3101
+ version: "2.0.0",
3102
+ wrap_hash: "not-a-sha256",
3103
+ origin: "http://mirror.example.com/collider/v2/?sig=123",
3104
+ },
3105
+ "bad-origin": {
3106
+ version: "3.0.0",
3107
+ wrap_hash:
3108
+ "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
3109
+ origin: "://not a url",
3110
+ },
3111
+ },
3112
+ }),
3113
+ "/repo/collider.lock",
3114
+ );
3115
+ assert.strictEqual(colliderLockData.pkgList.length, 3);
3116
+ const unsafeOrigin = colliderLockData.pkgList.find(
3117
+ (pkg) => pkg.name === "unsafe-origin",
3118
+ );
3119
+ const malformed = colliderLockData.pkgList.find(
3120
+ (pkg) => pkg.name === "malformed",
3121
+ );
3122
+ const badOrigin = colliderLockData.pkgList.find(
3123
+ (pkg) => pkg.name === "bad-origin",
3124
+ );
3125
+ assert.deepStrictEqual(
3126
+ unsafeOrigin.properties.find(
3127
+ (property) => property.name === "cdx:collider:origin",
3128
+ )?.value,
3129
+ "https://example.com/private/v2/",
3130
+ );
3131
+ assert.deepStrictEqual(
3132
+ unsafeOrigin.properties.find(
3133
+ (property) => property.name === "cdx:collider:originSanitized",
3134
+ )?.value,
3135
+ "true",
3136
+ );
3137
+ assert.deepStrictEqual(unsafeOrigin.externalReferences, [
3138
+ {
3139
+ type: "distribution",
3140
+ url: "https://example.com/private/v2/",
3141
+ },
3142
+ ]);
3143
+ assert.deepStrictEqual(
3144
+ malformed.properties.find(
3145
+ (property) => property.name === "cdx:collider:hasWrapHash",
3146
+ )?.value,
3147
+ "false",
3148
+ );
3149
+ assert.deepStrictEqual(
3150
+ malformed.properties.find(
3151
+ (property) => property.name === "cdx:collider:wrapHashInvalid",
3152
+ )?.value,
3153
+ "true",
3154
+ );
3155
+ assert.deepStrictEqual(
3156
+ malformed.properties.find(
3157
+ (property) => property.name === "cdx:collider:origin",
3158
+ )?.value,
3159
+ "http://mirror.example.com/collider/v2/",
3160
+ );
3161
+ assert.ok(!malformed.hashes);
3162
+ assert.ok(
3163
+ !badOrigin.properties.some(
3164
+ (property) => property.name === "cdx:collider:origin",
3165
+ ),
3166
+ );
3167
+ assert.ok(!badOrigin.externalReferences);
3168
+ });
3169
+
2828
3170
  it("conan package reference mapper to pURL", () => {
2829
3171
  const checkParseResult = (inputPkgRef, expectedPurl) => {
2830
3172
  const [purl, name, version] =
@@ -3133,7 +3475,25 @@ it("parse github actions workflow data", () => {
3133
3475
  dep_list = parseGitHubWorkflowData("./test/data/github-actions-tj.yaml");
3134
3476
  assert.deepStrictEqual(dep_list.length, 4);
3135
3477
  dep_list = parseGitHubWorkflowData("./.github/workflows/repotests.yml");
3136
- assert.deepStrictEqual(dep_list.length, 92);
3478
+ assert.ok(dep_list.length > 0);
3479
+ assert.ok(
3480
+ dep_list.every((component) =>
3481
+ component.properties?.some(
3482
+ (property) =>
3483
+ property.name === "cdx:github:workflow:file" &&
3484
+ property.value === "./.github/workflows/repotests.yml",
3485
+ ),
3486
+ ),
3487
+ );
3488
+ assert.ok(
3489
+ dep_list.some((component) =>
3490
+ component.properties?.some(
3491
+ (property) =>
3492
+ property.name === "cdx:github:checkout:repository" &&
3493
+ property.value === "AppThreat/vulnerability-db",
3494
+ ),
3495
+ ),
3496
+ );
3137
3497
  });
3138
3498
  // biome-ignore-end lint/suspicious/noTemplateCurlyInString: fp
3139
3499
 
@@ -4805,6 +5165,181 @@ it("parsePkgLock marks devOptional entries as development", async () => {
4805
5165
  );
4806
5166
  });
4807
5167
 
5168
+ it("parsePkgLock captures manifest-declared npm direct sources", async () => {
5169
+ const rootNode = {
5170
+ path: "/virtual/project",
5171
+ package: {
5172
+ author: "",
5173
+ license: "MIT",
5174
+ },
5175
+ packageName: "virtual-project",
5176
+ version: "1.0.0",
5177
+ edgesOut: new Map(),
5178
+ fsChildren: new Set(),
5179
+ children: new Map(),
5180
+ };
5181
+ const gitNode = {
5182
+ path: "/virtual/project/node_modules/git-dep",
5183
+ package: {
5184
+ author: "",
5185
+ license: "MIT",
5186
+ },
5187
+ packageName: "git-dep",
5188
+ version: "2.0.0",
5189
+ hasInstallScript: true,
5190
+ integrity: "sha512-gitdep",
5191
+ edgesIn: new Set([
5192
+ {
5193
+ name: "git-dep",
5194
+ spec: "git+https://github.com/acme/git-dep.git",
5195
+ },
5196
+ ]),
5197
+ edgesOut: new Map(),
5198
+ fsChildren: new Set(),
5199
+ children: new Map(),
5200
+ };
5201
+ rootNode.children.set("node_modules/git-dep", gitNode);
5202
+ rootNode.edgesOut.set("git-dep", {
5203
+ name: "git-dep",
5204
+ spec: "git+https://github.com/acme/git-dep.git",
5205
+ to: gitNode,
5206
+ });
5207
+ const { parsePkgLock: parsePkgLockWithMockedArborist } = await esmock(
5208
+ "./utils.js",
5209
+ {
5210
+ "../third-party/arborist/lib/index.js": {
5211
+ default: class MockArborist {
5212
+ async loadVirtual() {
5213
+ return rootNode;
5214
+ }
5215
+ },
5216
+ },
5217
+ },
5218
+ );
5219
+ const parsedList = await parsePkgLockWithMockedArborist(
5220
+ "./test/data/package-json/v3/package-lock.json",
5221
+ {},
5222
+ );
5223
+ const gitDepPkg = parsedList.pkgList.find(
5224
+ (pkg) => pkg["bom-ref"] === "pkg:npm/git-dep@2.0.0",
5225
+ );
5226
+ assert.ok(gitDepPkg);
5227
+ assert.ok(
5228
+ gitDepPkg.properties.some(
5229
+ (property) =>
5230
+ property.name === "cdx:npm:manifestSourceType" &&
5231
+ property.value === "git",
5232
+ ),
5233
+ );
5234
+ assert.ok(
5235
+ gitDepPkg.properties.some(
5236
+ (property) =>
5237
+ property.name === "cdx:npm:manifestSource" &&
5238
+ property.value === "git+https://github.com/acme/git-dep.git",
5239
+ ),
5240
+ );
5241
+ });
5242
+
5243
+ it("parsePkgLock captures supported npm manifest source syntaxes", async () => {
5244
+ const rootNode = {
5245
+ path: "/virtual/project",
5246
+ package: {
5247
+ author: "",
5248
+ license: "MIT",
5249
+ },
5250
+ packageName: "virtual-project",
5251
+ version: "1.0.0",
5252
+ edgesOut: new Map(),
5253
+ fsChildren: new Set(),
5254
+ children: new Map(),
5255
+ };
5256
+ const sourceCases = [
5257
+ ["git-plus", "git+https://github.com/acme/git-plus.git", "git"],
5258
+ ["git-protocol", "git://github.com/acme/git-protocol.git", "git"],
5259
+ ["github-shortcut", "github:acme/github-shortcut", "git"],
5260
+ ["gitlab-shortcut", "gitlab:acme/gitlab-shortcut", "git"],
5261
+ ["bitbucket-shortcut", "bitbucket:acme/bitbucket-shortcut", "git"],
5262
+ ["gist-shortcut", "gist:1234567890abcdef", "git"],
5263
+ ["http-archive", "http://example.com/http-archive.tgz", "url"],
5264
+ ["https-archive", "https://example.com/https-archive.tgz", "url"],
5265
+ ["file-source", "file:../libs/file-source", "path"],
5266
+ ["link-source", "link:../libs/link-source", "path"],
5267
+ ["workspace-source", "workspace:*", "path"],
5268
+ ["relative-source", "./libs/relative-source", "path"],
5269
+ ["parent-source", "../libs/parent-source", "path"],
5270
+ ["absolute-source", "/opt/libs/absolute-source", "path"],
5271
+ ["windows-source", "C:\\libs\\windows-source", "path"],
5272
+ ];
5273
+
5274
+ for (const [packageName, spec] of sourceCases) {
5275
+ const childPath = `/virtual/project/node_modules/${packageName}`;
5276
+ const childNode = {
5277
+ path: childPath,
5278
+ package: {
5279
+ author: "",
5280
+ license: "MIT",
5281
+ },
5282
+ packageName,
5283
+ version: "1.0.0",
5284
+ integrity: `sha512-${packageName}`,
5285
+ edgesIn: new Set([
5286
+ {
5287
+ name: packageName,
5288
+ spec,
5289
+ },
5290
+ ]),
5291
+ edgesOut: new Map(),
5292
+ fsChildren: new Set(),
5293
+ children: new Map(),
5294
+ };
5295
+ rootNode.children.set(`node_modules/${packageName}`, childNode);
5296
+ rootNode.edgesOut.set(packageName, {
5297
+ name: packageName,
5298
+ spec,
5299
+ to: childNode,
5300
+ });
5301
+ }
5302
+
5303
+ const { parsePkgLock: parsePkgLockWithMockedArborist } = await esmock(
5304
+ "./utils.js",
5305
+ {
5306
+ "../third-party/arborist/lib/index.js": {
5307
+ default: class MockArborist {
5308
+ async loadVirtual() {
5309
+ return rootNode;
5310
+ }
5311
+ },
5312
+ },
5313
+ },
5314
+ );
5315
+ const parsedList = await parsePkgLockWithMockedArborist(
5316
+ "./test/data/package-json/v3/package-lock.json",
5317
+ {},
5318
+ );
5319
+
5320
+ for (const [packageName, spec, expectedType] of sourceCases) {
5321
+ const pkg = parsedList.pkgList.find(
5322
+ (parsedPkg) => parsedPkg.name === packageName,
5323
+ );
5324
+ assert.ok(pkg, `expected ${packageName} to be parsed`);
5325
+ assert.ok(
5326
+ pkg.properties.some(
5327
+ (property) =>
5328
+ property.name === "cdx:npm:manifestSourceType" &&
5329
+ property.value === expectedType,
5330
+ ),
5331
+ `expected ${packageName} manifest source type ${expectedType}`,
5332
+ );
5333
+ assert.ok(
5334
+ pkg.properties.some(
5335
+ (property) =>
5336
+ property.name === "cdx:npm:manifestSource" && property.value === spec,
5337
+ ),
5338
+ `expected ${packageName} manifest source ${spec}`,
5339
+ );
5340
+ }
5341
+ });
5342
+
4808
5343
  it("parsePkgLock theia", async () => {
4809
5344
  const parsedList = await parsePkgLock(
4810
5345
  "./test/data/package-json/theia/package-lock.json",
@@ -7447,6 +7982,112 @@ it("parse requirements.txt", async () => {
7447
7982
  }
7448
7983
  });
7449
7984
 
7985
+ it("parse requirements.txt enriches distribution references when package metadata fetch is enabled", async () => {
7986
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-req-pypi-"));
7987
+ const reqFile = path.join(tempDir, "requirements.txt");
7988
+ const agentGetStub = sinon.stub().resolves({
7989
+ body: {
7990
+ info: {
7991
+ author: "",
7992
+ author_email: "",
7993
+ classifiers: [],
7994
+ license: "",
7995
+ license_expression: "",
7996
+ name: "requests",
7997
+ summary: "HTTP client",
7998
+ version: "2.31.0",
7999
+ },
8000
+ releases: {
8001
+ "2.31.0": [
8002
+ {
8003
+ digests: { sha256: "abc123" },
8004
+ filename: "requests-2.31.0-py3-none-any.whl",
8005
+ packagetype: "bdist_wheel",
8006
+ url: "https://files.pythonhosted.org/packages/example/requests-2.31.0-py3-none-any.whl",
8007
+ },
8008
+ ],
8009
+ },
8010
+ },
8011
+ });
8012
+ writeFileSync(reqFile, "requests==2.31.0\n", "utf-8");
8013
+ const { parseReqFile: mockedParseReqFile } = await esmock("./utils.js", {
8014
+ got: {
8015
+ default: {
8016
+ extend: sinon.stub().returns({ get: agentGetStub }),
8017
+ },
8018
+ },
8019
+ });
8020
+ try {
8021
+ const deps = await mockedParseReqFile(reqFile, true);
8022
+ assert.strictEqual(deps.length, 1);
8023
+ assert.ok(
8024
+ deps[0].externalReferences?.some(
8025
+ (reference) =>
8026
+ reference.type === "distribution" && reference.url.endsWith(".whl"),
8027
+ ),
8028
+ );
8029
+ } finally {
8030
+ rmSync(tempDir, { recursive: true, force: true });
8031
+ }
8032
+ });
8033
+
8034
+ it("parse requirements.txt captures direct manifest sources", async () => {
8035
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-req-source-"));
8036
+ const reqFile = path.join(tempDir, "requirements.txt");
8037
+ writeFileSync(
8038
+ reqFile,
8039
+ [
8040
+ "requests @ https://example.com/packages/requests-2.31.0.whl # MIT",
8041
+ "-e git+https://github.com/acme/private-lib.git#egg=private-lib",
8042
+ "",
8043
+ ].join("\n"),
8044
+ "utf-8",
8045
+ );
8046
+ try {
8047
+ const deps = await parseReqFile(reqFile, false);
8048
+ const requestsPkg = deps.find((pkg) => pkg.name === "requests");
8049
+ assert.ok(requestsPkg);
8050
+ assert.ok(
8051
+ requestsPkg.properties.some(
8052
+ (property) =>
8053
+ property.name === "cdx:pypi:manifestSourceType" &&
8054
+ property.value === "url",
8055
+ ),
8056
+ );
8057
+ assert.ok(
8058
+ requestsPkg.properties.some(
8059
+ (property) =>
8060
+ property.name === "cdx:pypi:manifestSource" &&
8061
+ property.value === "https://example.com/packages/requests-2.31.0.whl",
8062
+ ),
8063
+ );
8064
+ assert.deepStrictEqual(requestsPkg.licenses, [
8065
+ {
8066
+ license: {
8067
+ id: "MIT",
8068
+ },
8069
+ },
8070
+ ]);
8071
+ const privateLibPkg = deps.find((pkg) => pkg.name === "private-lib");
8072
+ assert.ok(privateLibPkg);
8073
+ assert.ok(
8074
+ privateLibPkg.properties.some(
8075
+ (property) =>
8076
+ property.name === "cdx:pypi:manifestSourceType" &&
8077
+ property.value === "git",
8078
+ ),
8079
+ );
8080
+ assert.ok(
8081
+ privateLibPkg.properties.some(
8082
+ (property) =>
8083
+ property.name === "cdx:pypi:editable" && property.value === "true",
8084
+ ),
8085
+ );
8086
+ } finally {
8087
+ rmSync(tempDir, { recursive: true, force: true });
8088
+ }
8089
+ });
8090
+
7450
8091
  it("parse pyproject.toml", () => {
7451
8092
  let retMap = parsePyProjectTomlFile("./test/data/pyproject.toml");
7452
8093
  assert.deepStrictEqual(retMap.parentComponent, {
@@ -7651,6 +8292,120 @@ it("parse pyproject.toml with custom poetry source", () => {
7651
8292
  assert.deepStrictEqual(Object.keys(retMap.directDepsKeys).length, 6);
7652
8293
  });
7653
8294
 
8295
+ it("parse pyproject.toml captures dependency manifest sources", async () => {
8296
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-pyproject-source-"));
8297
+ const pyProjectFile = path.join(tempDir, "pyproject.toml");
8298
+ writeFileSync(
8299
+ pyProjectFile,
8300
+ `
8301
+ [project]
8302
+ name = "demo-app"
8303
+ version = "0.1.0"
8304
+ dependencies = ["anyio[http2] @ https://example.com/packages/anyio.whl"]
8305
+
8306
+ [tool.poetry.dependencies]
8307
+ python = ">=3.11"
8308
+ poetry-git = { git = "https://github.com/acme/poetry-git.git" }
8309
+
8310
+ [tool.uv.sources]
8311
+ uv-path = { path = "../libs/uv-path" }
8312
+ `.trim(),
8313
+ "utf-8",
8314
+ );
8315
+ try {
8316
+ const pyProjectData = parsePyProjectTomlFile(pyProjectFile);
8317
+ assert.deepStrictEqual(pyProjectData.dependencySourceMap.anyio, {
8318
+ type: "url",
8319
+ value: "https://example.com/packages/anyio.whl",
8320
+ });
8321
+ assert.strictEqual(pyProjectData.directDepsKeys.anyio, true);
8322
+ assert.deepStrictEqual(pyProjectData.dependencySourceMap["poetry-git"], {
8323
+ type: "git",
8324
+ value: "https://github.com/acme/poetry-git.git",
8325
+ });
8326
+ assert.deepStrictEqual(pyProjectData.dependencySourceMap["uv-path"], {
8327
+ type: "path",
8328
+ value: "../libs/uv-path",
8329
+ });
8330
+
8331
+ const retMap = await parsePyLockData(
8332
+ readFileSync("./test/data/uv.lock", { encoding: "utf-8" }),
8333
+ "./test/data/uv.lock",
8334
+ pyProjectFile,
8335
+ );
8336
+ const anyioPkg = retMap.pkgList.find((pkg) => pkg.name === "anyio");
8337
+ assert.ok(anyioPkg);
8338
+ assert.ok(
8339
+ anyioPkg.properties.some(
8340
+ (property) =>
8341
+ property.name === "cdx:pypi:manifestSourceType" &&
8342
+ property.value === "url",
8343
+ ),
8344
+ );
8345
+ assert.ok(
8346
+ anyioPkg.properties.some(
8347
+ (property) =>
8348
+ property.name === "cdx:pypi:manifestSource" &&
8349
+ property.value === "https://example.com/packages/anyio.whl",
8350
+ ),
8351
+ );
8352
+ } finally {
8353
+ rmSync(tempDir, { recursive: true, force: true });
8354
+ }
8355
+ });
8356
+
8357
+ it("normalizes pyproject direct dependency keys when matching pylock packages", async () => {
8358
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-pylock-normalize-"));
8359
+ const pyProjectFile = path.join(tempDir, "pyproject.toml");
8360
+ const pyLockFile = path.join(tempDir, "pylock.toml");
8361
+ writeFileSync(
8362
+ pyProjectFile,
8363
+ `
8364
+ [project]
8365
+ name = "normalize-demo"
8366
+ version = "1.0.0"
8367
+ dependencies = [
8368
+ "demo_pkg @ https://example.com/packages/demo-pkg-1.0.0.whl",
8369
+ ]
8370
+ `.trim(),
8371
+ "utf-8",
8372
+ );
8373
+ writeFileSync(
8374
+ pyLockFile,
8375
+ `
8376
+ lock-version = "1.0"
8377
+ created-by = "poku"
8378
+
8379
+ [[packages]]
8380
+ name = "demo-pkg"
8381
+ version = "1.0.0"
8382
+ index = "https://pypi.org/simple/"
8383
+ wheels = [
8384
+ { name = "demo_pkg-1.0.0-py3-none-any.whl", url = "https://example.com/packages/demo-pkg-1.0.0.whl", size = 1234, upload-time = 2026-01-01T00:00:00+00:00, hashes = { sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } },
8385
+ ]
8386
+ `.trim(),
8387
+ "utf-8",
8388
+ );
8389
+ try {
8390
+ const retMap = await parsePyLockData(
8391
+ readFileSync(pyLockFile, { encoding: "utf-8" }),
8392
+ pyLockFile,
8393
+ pyProjectFile,
8394
+ );
8395
+ assert.strictEqual(retMap.rootList.length, 1);
8396
+ assert.strictEqual(retMap.rootList[0].name, "demo-pkg");
8397
+ assert.ok(
8398
+ retMap.rootList[0].properties.some(
8399
+ (property) =>
8400
+ property.name === "cdx:pypi:manifestSourceType" &&
8401
+ property.value === "url",
8402
+ ),
8403
+ );
8404
+ } finally {
8405
+ rmSync(tempDir, { recursive: true, force: true });
8406
+ }
8407
+ });
8408
+
7654
8409
  it("parse python lock files", async () => {
7655
8410
  let retMap = await parsePyLockData(
7656
8411
  readFileSync("./test/data/poetry.lock", { encoding: "utf-8" }),
@@ -7677,12 +8432,28 @@ it("parse python lock files", async () => {
7677
8432
  );
7678
8433
  assert.deepStrictEqual(retMap.pkgList.length, 39);
7679
8434
  assert.deepStrictEqual(retMap.dependenciesList.length, 37);
8435
+ const pdmBlinkerPkg = retMap.pkgList.find((p) => p.name === "blinker");
8436
+ assert.ok(
8437
+ pdmBlinkerPkg.externalReferences?.some(
8438
+ (reference) =>
8439
+ reference.type === "distribution" && reference.url.endsWith(".whl"),
8440
+ ),
8441
+ "Expected pdm.lock metadata files to populate distribution externalReferences",
8442
+ );
7680
8443
  retMap = await parsePyLockData(
7681
8444
  readFileSync("./test/data/uv.lock", { encoding: "utf-8" }),
7682
8445
  "./test/data/uv.lock",
7683
8446
  );
7684
8447
  assert.deepStrictEqual(retMap.pkgList.length, 63);
7685
8448
  assert.deepStrictEqual(retMap.dependenciesList.length, 63);
8449
+ const uvAnyioPkg = retMap.pkgList.find((p) => p.name === "anyio");
8450
+ assert.ok(
8451
+ uvAnyioPkg.externalReferences?.some(
8452
+ (reference) =>
8453
+ reference.type === "distribution" && reference.url.endsWith(".whl"),
8454
+ ),
8455
+ "Expected uv.lock packages to populate distribution externalReferences",
8456
+ );
7686
8457
  retMap = await parsePyLockData(
7687
8458
  readFileSync("./test/data/uv-workspace.lock", { encoding: "utf-8" }),
7688
8459
  "./test/data/uv-workspace.lock",
@@ -7709,6 +8480,13 @@ it("parse python lock files", async () => {
7709
8480
  attrsPkg.components?.length,
7710
8481
  "Expected pylock wheel entry to produce file component",
7711
8482
  );
8483
+ assert.ok(
8484
+ attrsPkg.externalReferences?.some(
8485
+ (reference) =>
8486
+ reference.type === "distribution" && reference.url.endsWith(".whl"),
8487
+ ),
8488
+ "Expected pylock package to retain distribution externalReferences",
8489
+ );
7712
8490
  const cattrsPkg = retMap.pkgList.find((p) => p.name === "cattrs");
7713
8491
  assert.ok(
7714
8492
  cattrsPkg.properties.some(
@@ -8863,6 +9641,80 @@ it("parsePackageJsonName tests", () => {
8863
9641
  });
8864
9642
  });
8865
9643
 
9644
+ it("extractToolRefs collects unique bom-refs from metadata.tools", () => {
9645
+ assert.deepStrictEqual(
9646
+ extractToolRefs(
9647
+ {
9648
+ components: [
9649
+ { name: "trivy", "bom-ref": "pkg:generic/trivy@0.1.0" },
9650
+ { name: "trivy", "bom-ref": "pkg:generic/trivy@0.1.0" },
9651
+ { name: "cdxgen" },
9652
+ ],
9653
+ services: [{ name: "blint", "bom-ref": "urn:tool:blint" }],
9654
+ },
9655
+ (tool) => tool.name !== "cdxgen",
9656
+ ),
9657
+ ["pkg:generic/trivy@0.1.0", "urn:tool:blint"],
9658
+ );
9659
+ });
9660
+
9661
+ it("extractToolRefs derives and persists bom-refs for external tools", () => {
9662
+ const tools = {
9663
+ components: [
9664
+ {
9665
+ group: "aquasecurity",
9666
+ name: "trivy",
9667
+ version: "dev",
9668
+ },
9669
+ ],
9670
+ };
9671
+ assert.deepStrictEqual(extractToolRefs(tools), [
9672
+ "pkg:generic/aquasecurity/trivy@dev",
9673
+ ]);
9674
+ assert.strictEqual(
9675
+ tools.components[0]["bom-ref"],
9676
+ "pkg:generic/aquasecurity/trivy@dev",
9677
+ );
9678
+ });
9679
+
9680
+ it("attachIdentityTools adds tool references to object and array identities", () => {
9681
+ const subjects = [
9682
+ {
9683
+ evidence: {
9684
+ identity: {
9685
+ field: "purl",
9686
+ tools: ["pkg:generic/existing-tool@1.0.0"],
9687
+ },
9688
+ },
9689
+ },
9690
+ {
9691
+ evidence: {
9692
+ identity: [
9693
+ { field: "purl" },
9694
+ { field: "hash", tools: ["urn:tool:hash"] },
9695
+ ],
9696
+ },
9697
+ },
9698
+ ];
9699
+ attachIdentityTools(subjects, [
9700
+ "pkg:generic/existing-tool@1.0.0",
9701
+ "pkg:generic/trivy@0.1.0",
9702
+ ]);
9703
+ assert.deepStrictEqual(subjects[0].evidence.identity.tools, [
9704
+ "pkg:generic/existing-tool@1.0.0",
9705
+ "pkg:generic/trivy@0.1.0",
9706
+ ]);
9707
+ assert.deepStrictEqual(subjects[1].evidence.identity[0].tools, [
9708
+ "pkg:generic/existing-tool@1.0.0",
9709
+ "pkg:generic/trivy@0.1.0",
9710
+ ]);
9711
+ assert.deepStrictEqual(subjects[1].evidence.identity[1].tools, [
9712
+ "urn:tool:hash",
9713
+ "pkg:generic/existing-tool@1.0.0",
9714
+ "pkg:generic/trivy@0.1.0",
9715
+ ]);
9716
+ });
9717
+
8866
9718
  it("parseDot tests", () => {
8867
9719
  const retMap = parseCmakeDotFile("./test/data/tslite.dot", "conan");
8868
9720
  assert.deepStrictEqual(retMap.parentComponent, {
@@ -9037,6 +9889,20 @@ it("hasAnyProjectType tests", () => {
9037
9889
  }),
9038
9890
  true,
9039
9891
  );
9892
+ assert.deepStrictEqual(
9893
+ hasAnyProjectType(["oci"], {
9894
+ projectType: ["rootfs"],
9895
+ excludeType: undefined,
9896
+ }),
9897
+ true,
9898
+ );
9899
+ assert.deepStrictEqual(
9900
+ hasAnyProjectType(["docker"], {
9901
+ projectType: ["rootfs"],
9902
+ excludeType: undefined,
9903
+ }),
9904
+ true,
9905
+ );
9040
9906
 
9041
9907
  assert.deepStrictEqual(
9042
9908
  hasAnyProjectType(["js"], {
@@ -9837,4 +10703,38 @@ describe("convertOSQueryResults", () => {
9837
10703
  assert.ok(propertyMap["cdx:lolbas:functions"].includes("download"));
9838
10704
  assert.ok(propertyMap["cdx:lolbas:attackTechniques"].includes("T1059.001"));
9839
10705
  });
10706
+
10707
+ it("collectExecutables() prefers usr-merged executable paths", () => {
10708
+ if (process.platform === "win32") {
10709
+ return;
10710
+ }
10711
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-executables-"));
10712
+ try {
10713
+ mkdirSync(path.join(tempDir, "usr", "bin"), { recursive: true });
10714
+ mkdirSync(path.join(tempDir, "usr", "sbin"), { recursive: true });
10715
+ writeFileSync(path.join(tempDir, "usr", "bin", "which"), "#!/bin/sh\n");
10716
+ writeFileSync(
10717
+ path.join(tempDir, "usr", "sbin", "zramctl"),
10718
+ "#!/bin/sh\n",
10719
+ );
10720
+ chmodSync(path.join(tempDir, "usr", "bin", "which"), 0o755);
10721
+ chmodSync(path.join(tempDir, "usr", "sbin", "zramctl"), 0o755);
10722
+ symlinkSync(path.join(tempDir, "usr", "bin"), path.join(tempDir, "bin"));
10723
+ symlinkSync(
10724
+ path.join(tempDir, "usr", "sbin"),
10725
+ path.join(tempDir, "sbin"),
10726
+ );
10727
+
10728
+ const result = collectExecutables(tempDir, [
10729
+ "/bin",
10730
+ "/usr/bin",
10731
+ "/sbin",
10732
+ "/usr/sbin",
10733
+ ]);
10734
+
10735
+ assert.deepStrictEqual(result, ["usr/bin/which", "usr/sbin/zramctl"]);
10736
+ } finally {
10737
+ rmSync(tempDir, { recursive: true, force: true });
10738
+ }
10739
+ });
9840
10740
  });