@cyclonedx/cdxgen 12.3.2 → 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 (53) hide show
  1. package/README.md +6 -0
  2. package/data/rules/ci-permissions.yaml +132 -0
  3. package/data/rules/dependency-sources.yaml +65 -5
  4. package/data/rules/package-integrity.yaml +22 -0
  5. package/lib/cli/index.js +141 -39
  6. package/lib/cli/index.poku.js +579 -1
  7. package/lib/helpers/agentFormulationParser.js +6 -2
  8. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  9. package/lib/helpers/analyzer.js +38 -9
  10. package/lib/helpers/analyzer.poku.js +67 -0
  11. package/lib/helpers/chromextutils.js +25 -3
  12. package/lib/helpers/chromextutils.poku.js +68 -0
  13. package/lib/helpers/ciParsers/githubActions.js +79 -0
  14. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  15. package/lib/helpers/communityAiConfigParser.js +15 -5
  16. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  17. package/lib/helpers/depsUtils.js +5 -0
  18. package/lib/helpers/depsUtils.poku.js +55 -0
  19. package/lib/helpers/display.js +45 -22
  20. package/lib/helpers/display.poku.js +47 -60
  21. package/lib/helpers/mcpConfigParser.js +21 -5
  22. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  23. package/lib/helpers/propertySanitizer.js +121 -0
  24. package/lib/helpers/utils.js +951 -40
  25. package/lib/helpers/utils.poku.js +882 -0
  26. package/lib/managers/binary.js +16 -0
  27. package/lib/managers/binary.poku.js +1 -0
  28. package/lib/managers/docker.js +240 -16
  29. package/lib/managers/docker.poku.js +1142 -2
  30. package/lib/server/server.js +7 -4
  31. package/lib/server/server.poku.js +36 -1
  32. package/lib/stages/postgen/auditBom.poku.js +644 -2
  33. package/package.json +2 -1
  34. package/types/lib/cli/index.d.ts.map +1 -1
  35. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  36. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  37. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  38. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  39. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  40. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  41. package/types/lib/helpers/display.d.ts +1 -0
  42. package/types/lib/helpers/display.d.ts.map +1 -1
  43. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  44. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  45. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  46. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  47. package/types/lib/helpers/utils.d.ts +29 -0
  48. package/types/lib/helpers/utils.d.ts.map +1 -1
  49. package/types/lib/managers/binary.d.ts.map +1 -1
  50. package/types/lib/managers/docker.d.ts +3 -0
  51. package/types/lib/managers/docker.d.ts.map +1 -1
  52. package/types/lib/server/server.d.ts +1 -0
  53. package/types/lib/server/server.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] =
@@ -4823,6 +5165,181 @@ it("parsePkgLock marks devOptional entries as development", async () => {
4823
5165
  );
4824
5166
  });
4825
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
+
4826
5343
  it("parsePkgLock theia", async () => {
4827
5344
  const parsedList = await parsePkgLock(
4828
5345
  "./test/data/package-json/theia/package-lock.json",
@@ -7465,6 +7982,112 @@ it("parse requirements.txt", async () => {
7465
7982
  }
7466
7983
  });
7467
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
+
7468
8091
  it("parse pyproject.toml", () => {
7469
8092
  let retMap = parsePyProjectTomlFile("./test/data/pyproject.toml");
7470
8093
  assert.deepStrictEqual(retMap.parentComponent, {
@@ -7669,6 +8292,120 @@ it("parse pyproject.toml with custom poetry source", () => {
7669
8292
  assert.deepStrictEqual(Object.keys(retMap.directDepsKeys).length, 6);
7670
8293
  });
7671
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
+
7672
8409
  it("parse python lock files", async () => {
7673
8410
  let retMap = await parsePyLockData(
7674
8411
  readFileSync("./test/data/poetry.lock", { encoding: "utf-8" }),
@@ -7695,12 +8432,28 @@ it("parse python lock files", async () => {
7695
8432
  );
7696
8433
  assert.deepStrictEqual(retMap.pkgList.length, 39);
7697
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
+ );
7698
8443
  retMap = await parsePyLockData(
7699
8444
  readFileSync("./test/data/uv.lock", { encoding: "utf-8" }),
7700
8445
  "./test/data/uv.lock",
7701
8446
  );
7702
8447
  assert.deepStrictEqual(retMap.pkgList.length, 63);
7703
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
+ );
7704
8457
  retMap = await parsePyLockData(
7705
8458
  readFileSync("./test/data/uv-workspace.lock", { encoding: "utf-8" }),
7706
8459
  "./test/data/uv-workspace.lock",
@@ -7727,6 +8480,13 @@ it("parse python lock files", async () => {
7727
8480
  attrsPkg.components?.length,
7728
8481
  "Expected pylock wheel entry to produce file component",
7729
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
+ );
7730
8490
  const cattrsPkg = retMap.pkgList.find((p) => p.name === "cattrs");
7731
8491
  assert.ok(
7732
8492
  cattrsPkg.properties.some(
@@ -8881,6 +9641,80 @@ it("parsePackageJsonName tests", () => {
8881
9641
  });
8882
9642
  });
8883
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
+
8884
9718
  it("parseDot tests", () => {
8885
9719
  const retMap = parseCmakeDotFile("./test/data/tslite.dot", "conan");
8886
9720
  assert.deepStrictEqual(retMap.parentComponent, {
@@ -9055,6 +9889,20 @@ it("hasAnyProjectType tests", () => {
9055
9889
  }),
9056
9890
  true,
9057
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
+ );
9058
9906
 
9059
9907
  assert.deepStrictEqual(
9060
9908
  hasAnyProjectType(["js"], {
@@ -9855,4 +10703,38 @@ describe("convertOSQueryResults", () => {
9855
10703
  assert.ok(propertyMap["cdx:lolbas:functions"].includes("download"));
9856
10704
  assert.ok(propertyMap["cdx:lolbas:attackTechniques"].includes("T1059.001"));
9857
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
+ });
9858
10740
  });