@cyclonedx/cdxgen 11.4.4 → 11.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import { afterAll, beforeAll, describe, expect, test } from "@jest/globals";
@@ -11,6 +11,7 @@ import {
11
11
  buildObjectForGradleModule,
12
12
  encodeForPurl,
13
13
  findLicenseId,
14
+ findPnpmPackagePath,
14
15
  getCratesMetadata,
15
16
  getDartMetadata,
16
17
  getLicenses,
@@ -39,6 +40,7 @@ import {
39
40
  parseCmakeDotFile,
40
41
  parseCmakeLikeFile,
41
42
  parseCocoaDependency,
43
+ parseComposerJson,
42
44
  parseComposerLock,
43
45
  parseConanData,
44
46
  parseConanLockData,
@@ -49,6 +51,8 @@ import {
49
51
  parseCsProjAssetsData,
50
52
  parseCsProjData,
51
53
  parseEdnData,
54
+ parseFlakeLock,
55
+ parseFlakeNix,
52
56
  parseGemfileLockData,
53
57
  parseGemspecData,
54
58
  parseGitHubWorkflowData,
@@ -98,6 +102,7 @@ import {
98
102
  parseSwiftJsonTree,
99
103
  parseSwiftResolved,
100
104
  parseYarnLock,
105
+ pnpmMetadata,
101
106
  readZipEntry,
102
107
  splitOutputByGradleProjects,
103
108
  toGemModuleNames,
@@ -1634,6 +1639,64 @@ test("parse go mod why dependencies", () => {
1634
1639
  expect(pkg_name).toBeUndefined();
1635
1640
  });
1636
1641
 
1642
+ test("multimodule go.mod file ordering", async () => {
1643
+ // Test that simulates the file ordering logic from createGoBom
1644
+ const mockPath = "/workspace/project";
1645
+ const mockGomodFiles = [
1646
+ "/workspace/project/deep/nested/go.mod",
1647
+ "/workspace/project/go.mod",
1648
+ "/workspace/project/submodule/go.mod",
1649
+ ];
1650
+
1651
+ // Sort files by depth (shallowest first) - this is the fix we implemented
1652
+ const sortedFiles = mockGomodFiles.sort((a, b) => {
1653
+ const relativePathA = a.replace(`${mockPath}/`, "");
1654
+ const relativePathB = b.replace(`${mockPath}/`, "");
1655
+ const depthA = relativePathA.split("/").length;
1656
+ const depthB = relativePathB.split("/").length;
1657
+ return depthA - depthB;
1658
+ });
1659
+
1660
+ // The root go.mod should be first (shallowest)
1661
+ expect(sortedFiles[0]).toEqual("/workspace/project/go.mod");
1662
+ expect(sortedFiles[1]).toEqual("/workspace/project/submodule/go.mod");
1663
+ expect(sortedFiles[2]).toEqual("/workspace/project/deep/nested/go.mod");
1664
+ });
1665
+
1666
+ test("parseGoModData for multiple modules with root priority", async () => {
1667
+ // Test parsing multiple go.mod files to ensure proper component hierarchy
1668
+ const rootModData = readFileSync("./test/data/multimodule-root.mod", {
1669
+ encoding: "utf-8",
1670
+ });
1671
+ const subModData = readFileSync("./test/data/multimodule-sub.mod", {
1672
+ encoding: "utf-8",
1673
+ });
1674
+ const deepModData = readFileSync("./test/data/multimodule-deep.mod", {
1675
+ encoding: "utf-8",
1676
+ });
1677
+
1678
+ const rootResult = await parseGoModData(rootModData, {});
1679
+ const subResult = await parseGoModData(subModData, {});
1680
+ const deepResult = await parseGoModData(deepModData, {});
1681
+
1682
+ // Root module should be identified correctly
1683
+ expect(rootResult.parentComponent.name).toEqual(
1684
+ "github.com/example/root-project",
1685
+ );
1686
+ expect(rootResult.parentComponent.type).toEqual("application");
1687
+
1688
+ // Sub modules should also be parsed correctly
1689
+ expect(subResult.parentComponent.name).toEqual(
1690
+ "github.com/example/root-project/submodule",
1691
+ );
1692
+ expect(deepResult.parentComponent.name).toEqual(
1693
+ "github.com/example/root-project/deep/nested",
1694
+ );
1695
+
1696
+ // In the fixed logic, the root should take priority over sub-modules
1697
+ // This test verifies the parsing works correctly for each individual module
1698
+ }, 10000);
1699
+
1637
1700
  test("parseGopkgData", async () => {
1638
1701
  let dep_list = await parseGopkgData(null);
1639
1702
  expect(dep_list).toEqual([]);
@@ -2197,7 +2260,7 @@ test("parse cabal freeze", () => {
2197
2260
  test("parse conan data", () => {
2198
2261
  expect(parseConanLockData(null)).toEqual([]);
2199
2262
  let dep_list = parseConanLockData(
2200
- readFileSync("./test/data/conan.lock", { encoding: "utf-8" }),
2263
+ readFileSync("./test/data/conan-v1.lock", { encoding: "utf-8" }),
2201
2264
  );
2202
2265
  expect(dep_list.length).toEqual(3);
2203
2266
  expect(dep_list[0]).toEqual({
@@ -2206,6 +2269,16 @@ test("parse conan data", () => {
2206
2269
  "bom-ref": "pkg:conan/zstd@1.4.4",
2207
2270
  purl: "pkg:conan/zstd@1.4.4",
2208
2271
  });
2272
+ dep_list = parseConanLockData(
2273
+ readFileSync("./test/data/conan-v2.lock", { encoding: "utf-8" }),
2274
+ );
2275
+ expect(dep_list.length).toEqual(2);
2276
+ expect(dep_list[0]).toEqual({
2277
+ name: "opensta",
2278
+ version: "4.0.0",
2279
+ "bom-ref": "pkg:conan/opensta@4.0.0?rrev=765a7eed989e624c762a73291d712b14",
2280
+ purl: "pkg:conan/opensta@4.0.0?rrev=765a7eed989e624c762a73291d712b14",
2281
+ });
2209
2282
  dep_list = parseConanData(
2210
2283
  readFileSync("./test/data/conanfile.txt", { encoding: "utf-8" }),
2211
2284
  );
@@ -3844,6 +3917,149 @@ test("parsePnpmLock", async () => {
3844
3917
  expect(parsedList.dependenciesList.length).toEqual(1189);
3845
3918
  });
3846
3919
 
3920
+ test("findPnpmPackagePath", () => {
3921
+ // Test with non-existent base directory
3922
+ expect(
3923
+ findPnpmPackagePath("/nonexistent", "test-package", "1.0.0"),
3924
+ ).toBeNull();
3925
+
3926
+ // Test with null/undefined inputs
3927
+ expect(findPnpmPackagePath(null, "test-package", "1.0.0")).toBeNull();
3928
+ expect(findPnpmPackagePath("/tmp", null, "1.0.0")).toBeNull();
3929
+ expect(findPnpmPackagePath("/tmp", "", "1.0.0")).toBeNull();
3930
+
3931
+ // Test with actual cdxgen project structure - should find packages in node_modules
3932
+ const packagePath = findPnpmPackagePath(".", "chalk", "4.1.2");
3933
+ if (packagePath) {
3934
+ expect(packagePath).toMatch(/node_modules.*chalk/);
3935
+ // Verify package.json exists at the found path
3936
+ expect(existsSync(path.join(packagePath, "package.json"))).toBe(true);
3937
+ }
3938
+
3939
+ // Test with scoped package
3940
+ const scopedPackagePath = findPnpmPackagePath(".", "@babel/core", "7.22.5");
3941
+ if (scopedPackagePath) {
3942
+ expect(scopedPackagePath).toMatch(/node_modules.*@babel.*core/);
3943
+ }
3944
+ });
3945
+
3946
+ test("pnpmMetadata enhancement", async () => {
3947
+ // Test with empty/null inputs
3948
+ expect(await pnpmMetadata([], "./pnpm-lock.yaml")).toEqual([]);
3949
+ expect(await pnpmMetadata(null, "./pnpm-lock.yaml")).toEqual(null);
3950
+ expect(await pnpmMetadata(undefined, "./pnpm-lock.yaml")).toEqual(undefined);
3951
+
3952
+ // Test with non-existent lockfile path
3953
+ const mockPkgList = [
3954
+ {
3955
+ name: "test-package",
3956
+ version: "1.0.0",
3957
+ properties: [],
3958
+ },
3959
+ ];
3960
+ const result = await pnpmMetadata(mockPkgList, "/nonexistent/pnpm-lock.yaml");
3961
+ expect(result).toEqual(mockPkgList);
3962
+ expect(result[0].description).toBeUndefined();
3963
+
3964
+ // Test with actual project that has node_modules
3965
+ const testPkgList = [
3966
+ {
3967
+ name: "chalk",
3968
+ version: "4.1.2",
3969
+ properties: [],
3970
+ },
3971
+ {
3972
+ name: "nonexistent-package",
3973
+ version: "1.0.0",
3974
+ properties: [],
3975
+ },
3976
+ ];
3977
+
3978
+ const enhancedResult = await pnpmMetadata(testPkgList, "./pnpm-lock.yaml");
3979
+
3980
+ const chalkPkg = enhancedResult.find((p) => p.name === "chalk");
3981
+ if (chalkPkg) {
3982
+ const localPath = chalkPkg.properties?.find(
3983
+ (p) => p.name === "LocalNodeModulesPath",
3984
+ );
3985
+ if (localPath) {
3986
+ expect(localPath.value).toMatch(/node_modules.*chalk/);
3987
+ expect(chalkPkg.description).toBeDefined();
3988
+ expect(chalkPkg.license).toBeDefined();
3989
+ }
3990
+ }
3991
+
3992
+ // Non-existent package should remain unchanged
3993
+ const nonExistentPkg = enhancedResult.find(
3994
+ (p) => p.name === "nonexistent-package",
3995
+ );
3996
+ expect(nonExistentPkg.description).toBeUndefined();
3997
+ expect(
3998
+ nonExistentPkg.properties.find((p) => p.name === "LocalNodeModulesPath"),
3999
+ ).toBeUndefined();
4000
+ });
4001
+
4002
+ test("pnpmMetadata preserves existing metadata", async () => {
4003
+ const testPkgList = [
4004
+ {
4005
+ name: "test-package",
4006
+ version: "1.0.0",
4007
+ description: "Existing description",
4008
+ author: "Existing author",
4009
+ license: "Existing license",
4010
+ properties: [],
4011
+ },
4012
+ ];
4013
+
4014
+ const result = await pnpmMetadata(testPkgList, "./pnpm-lock.yaml");
4015
+
4016
+ // Should preserve existing metadata
4017
+ expect(result[0].description).toBe("Existing description");
4018
+ expect(result[0].author).toBe("Existing author");
4019
+ expect(result[0].license).toBe("Existing license");
4020
+ });
4021
+
4022
+ test("pnpmMetadata with scoped packages", async () => {
4023
+ const testPkgList = [
4024
+ {
4025
+ name: "@babel/core",
4026
+ version: "7.22.5",
4027
+ properties: [],
4028
+ },
4029
+ ];
4030
+
4031
+ const result = await pnpmMetadata(testPkgList, "./pnpm-lock.yaml");
4032
+
4033
+ // Check if scoped package was processed
4034
+ const babelPkg = result.find((p) => p.name === "@babel/core");
4035
+ expect(babelPkg).toBeDefined();
4036
+ expect(babelPkg.name).toBe("@babel/core");
4037
+ });
4038
+
4039
+ test("pnpmMetadata integration with parsePnpmLock", async () => {
4040
+ // Test that the integration works by parsing a real pnpm lock file
4041
+ const parsedList = await parsePnpmLock("./pnpm-lock.yaml");
4042
+
4043
+ // Check that some packages have been enhanced with LocalNodeModulesPath
4044
+ const enhancedPackages = parsedList.pkgList.filter((pkg) =>
4045
+ pkg.properties?.some((p) => p.name === "LocalNodeModulesPath"),
4046
+ );
4047
+
4048
+ if (enhancedPackages.length > 0) {
4049
+ expect(enhancedPackages.length).toBeGreaterThan(0);
4050
+
4051
+ const examplePkg = enhancedPackages[0];
4052
+ expect(
4053
+ examplePkg.properties.find((p) => p.name === "LocalNodeModulesPath"),
4054
+ ).toBeDefined();
4055
+
4056
+ const packagesWithMetadata = enhancedPackages.filter(
4057
+ (pkg) => pkg.description || pkg.license || pkg.author,
4058
+ );
4059
+ expect(packagesWithMetadata.length).toBeGreaterThan(0);
4060
+ }
4061
+ });
4062
+
3847
4063
  test("parseYarnLock", async () => {
3848
4064
  let identMap = yarnLockToIdentMap(readFileSync("./test/yarn.lock", "utf8"));
3849
4065
  expect(Object.keys(identMap).length).toEqual(62);
@@ -4466,6 +4682,14 @@ test("parseComposerLock", () => {
4466
4682
  });
4467
4683
  });
4468
4684
 
4685
+ test("parseComposerJson", () => {
4686
+ let retMap = parseComposerJson("./test/data/composer.json");
4687
+ expect(Object.keys(retMap.rootRequires).length).toEqual(1);
4688
+
4689
+ retMap = parseComposerJson("./test/data/composer-2.json");
4690
+ expect(Object.keys(retMap.rootRequires).length).toEqual(31);
4691
+ });
4692
+
4469
4693
  test("parseGemfileLockData", async () => {
4470
4694
  let retMap = await parseGemfileLockData(
4471
4695
  readFileSync("./test/data/Gemfile.lock", { encoding: "utf-8" }),
@@ -4780,37 +5004,58 @@ test("parseGemspecData", async () => {
4780
5004
  });
4781
5005
 
4782
5006
  test("parse requirements.txt", async () => {
4783
- let deps = await parseReqFile(
4784
- readFileSync("./test/data/requirements.comments.txt", {
4785
- encoding: "utf-8",
4786
- }),
4787
- false,
4788
- );
5007
+ let deps = await parseReqFile("./test/data/requirements.comments.txt", false);
4789
5008
  expect(deps.length).toEqual(31);
4790
- deps = await parseReqFile(
4791
- readFileSync("./test/data/requirements.freeze.txt", {
4792
- encoding: "utf-8",
4793
- }),
4794
- false,
4795
- );
5009
+ deps = await parseReqFile("./test/data/requirements.freeze.txt", false);
4796
5010
  expect(deps.length).toEqual(113);
4797
5011
  expect(deps[0]).toEqual({
4798
5012
  name: "elasticsearch",
4799
5013
  version: "8.6.2",
4800
5014
  scope: "required",
5015
+ properties: [
5016
+ {
5017
+ name: "SrcFile",
5018
+ value: "./test/data/requirements.freeze.txt",
5019
+ },
5020
+ ],
5021
+ evidence: {
5022
+ identity: {
5023
+ field: "purl",
5024
+ confidence: 0.5,
5025
+ methods: [
5026
+ {
5027
+ technique: "manifest-analysis",
5028
+ confidence: 0.5,
5029
+ value: "./test/data/requirements.freeze.txt",
5030
+ },
5031
+ ],
5032
+ },
5033
+ },
4801
5034
  });
4802
- deps = await parseReqFile(
4803
- readFileSync("./test/data/chen-science-requirements.txt", {
4804
- encoding: "utf-8",
4805
- }),
4806
- false,
4807
- );
5035
+ deps = await parseReqFile("./test/data/chen-science-requirements.txt", false);
4808
5036
  expect(deps.length).toEqual(87);
4809
5037
  expect(deps[0]).toEqual({
4810
5038
  name: "aiofiles",
4811
5039
  version: "23.2.1",
4812
5040
  scope: undefined,
5041
+ evidence: {
5042
+ identity: {
5043
+ field: "purl",
5044
+ confidence: 0.5,
5045
+ methods: [
5046
+ {
5047
+ technique: "manifest-analysis",
5048
+ confidence: 0.5,
5049
+ value: "./test/data/chen-science-requirements.txt",
5050
+ },
5051
+ ],
5052
+ },
5053
+ },
4813
5054
  properties: [
5055
+ {
5056
+ name: "SrcFile",
5057
+ value: "./test/data/chen-science-requirements.txt",
5058
+ },
4814
5059
  {
4815
5060
  name: "cdx:pip:markers",
4816
5061
  value:
@@ -4819,9 +5064,7 @@ test("parse requirements.txt", async () => {
4819
5064
  ],
4820
5065
  });
4821
5066
  deps = await parseReqFile(
4822
- readFileSync("./test/data/requirements-lock.linux_py3.txt", {
4823
- encoding: "utf-8",
4824
- }),
5067
+ "./test/data/requirements-lock.linux_py3.txt",
4825
5068
  false,
4826
5069
  );
4827
5070
  expect(deps.length).toEqual(375);
@@ -4829,11 +5072,49 @@ test("parse requirements.txt", async () => {
4829
5072
  name: "adal",
4830
5073
  scope: undefined,
4831
5074
  version: "1.2.2",
5075
+ properties: [
5076
+ {
5077
+ name: "SrcFile",
5078
+ value: "./test/data/requirements-lock.linux_py3.txt",
5079
+ },
5080
+ ],
5081
+ evidence: {
5082
+ identity: {
5083
+ field: "purl",
5084
+ confidence: 0.5,
5085
+ methods: [
5086
+ {
5087
+ technique: "manifest-analysis",
5088
+ confidence: 0.5,
5089
+ value: "./test/data/requirements-lock.linux_py3.txt",
5090
+ },
5091
+ ],
5092
+ },
5093
+ },
4832
5094
  });
4833
5095
  expect(deps[deps.length - 1]).toEqual({
4834
5096
  name: "zipp",
4835
5097
  scope: undefined,
4836
5098
  version: "0.6.0",
5099
+ properties: [
5100
+ {
5101
+ name: "SrcFile",
5102
+ value: "./test/data/requirements-lock.linux_py3.txt",
5103
+ },
5104
+ ],
5105
+ evidence: {
5106
+ identity: {
5107
+ field: "purl",
5108
+ confidence: 0.5,
5109
+ methods: [
5110
+ {
5111
+ technique: "manifest-analysis",
5112
+ confidence: 0.5,
5113
+ value: "./test/data/requirements-lock.linux_py3.txt",
5114
+ },
5115
+ ],
5116
+ },
5117
+ },
4837
5118
  });
4838
5119
  });
4839
5120
 
@@ -6266,3 +6547,143 @@ test("parseMillDependency test", () => {
6266
6547
  expect(dependencies.size).toEqual(15);
6267
6548
  expect(relations.size).toEqual(15);
6268
6549
  });
6550
+
6551
+ test("parse flake.nix file", () => {
6552
+ const result = parseFlakeNix("./test/data/test-flake.nix");
6553
+ expect(result.pkgList).toBeDefined();
6554
+ expect(result.dependencies).toBeDefined();
6555
+ expect(result.pkgList.length).toEqual(3);
6556
+
6557
+ // Check nixpkgs input
6558
+ const nixpkgs = result.pkgList.find((pkg) => pkg.name === "nixpkgs");
6559
+ expect(nixpkgs).toBeDefined();
6560
+ expect(nixpkgs.version).toEqual("latest");
6561
+ expect(nixpkgs.purl).toEqual("pkg:nix/nixpkgs@latest");
6562
+ expect(nixpkgs["bom-ref"]).toEqual("pkg:nix/nixpkgs@latest");
6563
+ expect(nixpkgs.scope).toEqual("required");
6564
+ expect(nixpkgs.description).toEqual("Nix flake input: nixpkgs");
6565
+ expect(nixpkgs.properties).toBeDefined();
6566
+
6567
+ // Check properties
6568
+ const srcFileProperty = nixpkgs.properties.find((p) => p.name === "SrcFile");
6569
+ expect(srcFileProperty.value).toEqual("./test/data/test-flake.nix");
6570
+
6571
+ const urlProperty = nixpkgs.properties.find(
6572
+ (p) => p.name === "cdx:nix:input_url",
6573
+ );
6574
+ expect(urlProperty.value).toEqual("github:NixOS/nixpkgs/release-23.11");
6575
+
6576
+ // Check flake-utils input
6577
+ const flakeUtils = result.pkgList.find((pkg) => pkg.name === "flake-utils");
6578
+ expect(flakeUtils).toBeDefined();
6579
+ expect(flakeUtils.version).toEqual("latest");
6580
+
6581
+ // Check rust-overlay input
6582
+ const rustOverlay = result.pkgList.find((pkg) => pkg.name === "rust-overlay");
6583
+ expect(rustOverlay).toBeDefined();
6584
+ expect(rustOverlay.version).toEqual("latest");
6585
+
6586
+ const rustOverlayUrlProperty = rustOverlay.properties.find(
6587
+ (p) => p.name === "cdx:nix:input_url",
6588
+ );
6589
+ expect(rustOverlayUrlProperty.value).toEqual("github:oxalica/rust-overlay");
6590
+
6591
+ // Check evidence
6592
+ expect(nixpkgs.evidence).toBeDefined();
6593
+ expect(nixpkgs.evidence.identity.field).toEqual("purl");
6594
+ expect(nixpkgs.evidence.identity.confidence).toEqual(0.8);
6595
+ expect(nixpkgs.evidence.identity.methods[0].technique).toEqual(
6596
+ "manifest-analysis",
6597
+ );
6598
+ });
6599
+
6600
+ test("parse flake.lock file", () => {
6601
+ const result = parseFlakeLock("./test/data/test-flake.lock");
6602
+ expect(result.pkgList).toBeDefined();
6603
+ expect(result.dependencies).toBeDefined();
6604
+ expect(result.pkgList.length).toEqual(4);
6605
+
6606
+ // Check nixpkgs package
6607
+ const nixpkgs = result.pkgList.find((pkg) => pkg.name === "nixpkgs");
6608
+ expect(nixpkgs).toBeDefined();
6609
+ expect(nixpkgs.version).toEqual("bd645e8"); // Short commit hash
6610
+ expect(nixpkgs.purl).toEqual("pkg:nix/nixpkgs@bd645e8");
6611
+ expect(nixpkgs["bom-ref"]).toEqual("pkg:nix/nixpkgs@bd645e8");
6612
+ expect(nixpkgs.scope).toEqual("required");
6613
+ expect(nixpkgs.description).toEqual("Nix flake dependency: nixpkgs");
6614
+
6615
+ // Check properties for nixpkgs
6616
+ const nixpkgsProperties = nixpkgs.properties;
6617
+ expect(nixpkgsProperties).toBeDefined();
6618
+
6619
+ const srcFileProperty = nixpkgsProperties.find((p) => p.name === "SrcFile");
6620
+ expect(srcFileProperty.value).toEqual("./test/data/test-flake.lock");
6621
+
6622
+ const narHashProperty = nixpkgsProperties.find(
6623
+ (p) => p.name === "cdx:nix:nar_hash",
6624
+ );
6625
+ expect(narHashProperty.value).toEqual(
6626
+ "sha256-RtDKd8Mynhe5CFnVT8s0/0yqtWFMM9LmCzXv/YKxnq4=",
6627
+ );
6628
+
6629
+ const lastModifiedProperty = nixpkgsProperties.find(
6630
+ (p) => p.name === "cdx:nix:last_modified",
6631
+ );
6632
+ expect(lastModifiedProperty.value).toEqual("1704194953");
6633
+
6634
+ const revisionProperty = nixpkgsProperties.find(
6635
+ (p) => p.name === "cdx:nix:revision",
6636
+ );
6637
+ expect(revisionProperty.value).toEqual(
6638
+ "bd645e8668ec6612439a9ee7e71f7eac4099d4f6",
6639
+ );
6640
+
6641
+ // Check flake-utils package
6642
+ const flakeUtils = result.pkgList.find((pkg) => pkg.name === "flake-utils");
6643
+ expect(flakeUtils).toBeDefined();
6644
+ expect(flakeUtils.version).toEqual("1ef2e67");
6645
+
6646
+ // Check rust-overlay package
6647
+ const rustOverlay = result.pkgList.find((pkg) => pkg.name === "rust-overlay");
6648
+ expect(rustOverlay).toBeDefined();
6649
+ expect(rustOverlay.version).toEqual("9a8a835");
6650
+
6651
+ // Check systems package
6652
+ const systems = result.pkgList.find((pkg) => pkg.name === "systems");
6653
+ expect(systems).toBeDefined();
6654
+ expect(systems.version).toEqual("da67096");
6655
+
6656
+ // Check dependencies
6657
+ expect(result.dependencies.length).toEqual(1);
6658
+ const rootDep = result.dependencies[0];
6659
+ expect(rootDep.ref).toEqual("pkg:nix/flake@latest");
6660
+ expect(rootDep.dependsOn).toBeDefined();
6661
+ expect(rootDep.dependsOn.length).toEqual(3); // flake-utils, nixpkgs, rust-overlay
6662
+ expect(rootDep.dependsOn).toContain("pkg:nix/flake-utils@1ef2e67");
6663
+ expect(rootDep.dependsOn).toContain("pkg:nix/nixpkgs@bd645e8");
6664
+ expect(rootDep.dependsOn).toContain("pkg:nix/rust-overlay@9a8a835");
6665
+
6666
+ // Check evidence
6667
+ expect(nixpkgs.evidence).toBeDefined();
6668
+ expect(nixpkgs.evidence.identity.field).toEqual("purl");
6669
+ expect(nixpkgs.evidence.identity.confidence).toEqual(1.0);
6670
+ expect(nixpkgs.evidence.identity.methods[0].technique).toEqual(
6671
+ "manifest-analysis",
6672
+ );
6673
+ });
6674
+
6675
+ test("parse flake.nix file with missing file", () => {
6676
+ const result = parseFlakeNix("./test/data/missing-flake.nix");
6677
+ expect(result.pkgList).toBeDefined();
6678
+ expect(result.dependencies).toBeDefined();
6679
+ expect(result.pkgList.length).toEqual(0);
6680
+ expect(result.dependencies.length).toEqual(0);
6681
+ });
6682
+
6683
+ test("parse flake.lock file with missing file", () => {
6684
+ const result = parseFlakeLock("./test/data/missing-flake.lock");
6685
+ expect(result.pkgList).toBeDefined();
6686
+ expect(result.dependencies).toBeDefined();
6687
+ expect(result.pkgList.length).toEqual(0);
6688
+ expect(result.dependencies.length).toEqual(0);
6689
+ });
@@ -185,6 +185,16 @@ export const validatePurls = (bomJson) => {
185
185
  `purl does not include namespace but includes encoded slash in name for npm type. ${comp.purl}`,
186
186
  );
187
187
  }
188
+ // Catch the trivy version hack that removes the epoch from version
189
+ const qualifiers = purlObj.qualifiers || {};
190
+ if (
191
+ qualifiers.epoch &&
192
+ !comp.version.startsWith(`${qualifiers.epoch}:`)
193
+ ) {
194
+ errorList.push(
195
+ `'${comp.name}' version '${comp.version}' doesn't include epoch '${qualifiers.epoch}'.`,
196
+ );
197
+ }
188
198
  } catch (_ex) {
189
199
  errorList.push(`Invalid purl ${comp.purl}`);
190
200
  }