Skip to content

Commit b65c2f6

Browse files
agatakrajewskatommyknows
authored andcommitted
feat: add purl to PkgInfo
The validation hooks will make sure that the name and version in the package URL also match the package's "actual" name and version.
1 parent a27eded commit b65c2f6

12 files changed

+185
-16
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,29 @@ export interface DepGraph {
5050
readonly rootPkg: {
5151
name: string;
5252
version?: string;
53+
purl?: string;
5354
};
5455
// all unique packages in the graph (including root package)
5556
getPkgs(): Array<{
5657
name: string;
5758
version?: string;
59+
purl?: string;
5860
}>;
5961
// all unique packages in the graph, except the root package
6062
getDepPkgs(): Array<{
6163
name: string;
6264
version?: string;
65+
purl?: string;
6366
}>;
6467
pkgPathsToRoot(pkg: Pkg): Array<Array<{
6568
name: string;
6669
version?: string;
70+
purl?: string;
6771
}>>;
6872
directDepsLeadingTo(pkg: Pkg): Array<{
6973
name: string;
7074
version?: string;
75+
purl?: string;
7176
}>;
7277
countPathsToRoot(pkg: Pkg): number;
7378
toJSON(): DepGraphData;
@@ -94,6 +99,7 @@ export interface DepGraphData {
9499
info: {
95100
name: string;
96101
version?: string;
102+
purl?: string;
97103
};
98104
}>;
99105
graph: {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"lodash.union": "^4.6.0",
6565
"lodash.values": "^4.3.0",
6666
"object-hash": "^3.0.0",
67+
"packageurl-js": "^1.0.0",
6768
"semver": "^7.0.0",
6869
"tslib": "^2"
6970
}

src/core/builder.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as graphlib from '../graphlib';
22
import * as types from './types';
33
import { DepGraphImpl } from './dep-graph';
4+
import { validatePackageURL } from './validate-graph';
45

56
export { DepGraphBuilder };
67

@@ -60,6 +61,8 @@ class DepGraphBuilder {
6061
throw new Error('DepGraphBuilder.addPkgNode() cant override root node');
6162
}
6263

64+
validatePackageURL(pkgInfo);
65+
6366
const pkgId = DepGraphBuilder._getPkgId(pkgInfo);
6467

6568
this._pkgs[pkgId] = pkgInfo;

src/core/dep-graph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface GraphNode {
1313
}
1414

1515
class DepGraphImpl implements types.DepGraphInternal {
16-
public static SCHEMA_VERSION = '1.2.0';
16+
public static SCHEMA_VERSION = '1.3.0';
1717

1818
public static getPkgId(pkg: types.Pkg): string {
1919
return `${pkg.name}@${pkg.version || ''}`;

src/core/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ export interface Pkg {
88
version?: string;
99
}
1010

11+
export type PurlString = string;
12+
1113
export interface PkgInfo {
1214
name: string;
1315
version?: string;
16+
purl?: PurlString;
17+
1418
// NOTE: consider adding in the future
1519
// requires?: {
1620
// name: string;

src/core/validate-graph.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as graphlib from '../graphlib';
2+
import { PackageURL } from 'packageurl-js';
3+
import * as types from './types';
24
import { ValidationError } from './errors';
35

46
function assert(condition: boolean, msg: string) {
@@ -30,4 +32,45 @@ export function validateGraph(
3032
(pkgId) => !pkgNodes[pkgId] || pkgNodes[pkgId].size === 0,
3133
);
3234
assert(pkgsWithoutInstances.length === 0, 'not all pkgs have instance nodes');
35+
36+
for (const pkgId in pkgs) {
37+
try {
38+
validatePackageURL(pkgs[pkgId] as types.PkgInfo);
39+
} catch (e) {
40+
throw new ValidationError(`invalid pkg ${pkgId}: ${e}`);
41+
}
42+
}
43+
}
44+
45+
export function validatePackageURL(pkg: types.PkgInfo): void {
46+
if (!pkg.purl) {
47+
return;
48+
}
49+
50+
try {
51+
const purlPkg = PackageURL.fromString(pkg.purl);
52+
53+
switch (purlPkg.type) {
54+
// Within Snyk, maven packages use <namespace>:<name> as their *name*, but
55+
// we expect those to be separated correctly in the PackageURL.
56+
case 'maven':
57+
assert(
58+
pkg.name === purlPkg.namespace + ':' + purlPkg.name,
59+
`name and packageURL name do not match`,
60+
);
61+
break;
62+
63+
default:
64+
assert(
65+
pkg.name === purlPkg.name,
66+
`name and packageURL name do not match`,
67+
);
68+
}
69+
assert(
70+
pkg.version === purlPkg.version,
71+
`version and packageURL version do not match`,
72+
);
73+
} catch (e) {
74+
throw new ValidationError(`packageURL validation failed: ${e}`);
75+
}
3376
}

test/core/__snapshots__/filter-from-graph.test.ts.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7310,7 +7310,7 @@ Object {
73107310
},
73117311
},
73127312
],
7313-
"schemaVersion": "1.2.0",
7313+
"schemaVersion": Any<String>,
73147314
}
73157315
`;
73167316

@@ -14624,7 +14624,7 @@ Object {
1462414624
},
1462514625
},
1462614626
],
14627-
"schemaVersion": "1.2.0",
14627+
"schemaVersion": Any<String>,
1462814628
}
1462914629
`;
1463014630

@@ -21938,7 +21938,7 @@ Object {
2193821938
},
2193921939
},
2194021940
],
21941-
"schemaVersion": "1.2.0",
21941+
"schemaVersion": Any<String>,
2194221942
}
2194321943
`;
2194421944

@@ -21966,7 +21966,7 @@ Object {
2196621966
},
2196721967
},
2196821968
],
21969-
"schemaVersion": "1.2.0",
21969+
"schemaVersion": Any<String>,
2197021970
}
2197121971
`;
2197221972

@@ -29262,7 +29262,7 @@ Object {
2926229262
},
2926329263
},
2926429264
],
29265-
"schemaVersion": "1.2.0",
29265+
"schemaVersion": Any<String>,
2926629266
}
2926729267
`;
2926829268

@@ -36558,6 +36558,6 @@ Object {
3655836558
},
3655936559
},
3656036560
],
36561-
"schemaVersion": "1.2.0",
36561+
"schemaVersion": Any<String>,
3656236562
}
3656336563
`;

test/core/builder.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DepGraphBuilder } from '../../src';
2+
import { ValidationError } from '../../src/core/errors';
23

34
describe('builder', () => {
45
let builder;
@@ -35,6 +36,89 @@ describe('builder', () => {
3536
);
3637
expect(packageAdded).toBeDefined();
3738
});
39+
40+
it('should throw error if invalid package URL is defined', () => {
41+
const pkgToAdd = {
42+
name: 'json',
43+
version: '1.0.0',
44+
purl: 'this-is:not/a-/purl',
45+
};
46+
expect(() => {
47+
builder.addPkgNode(pkgToAdd, pkgToAdd.name);
48+
}).toThrow(ValidationError);
49+
});
50+
it('should throw error if package URL defines different name', () => {
51+
const pkgToAdd = {
52+
name: 'json',
53+
version: '1.0.0',
54+
purl: 'pkg:rpm/[email protected]',
55+
};
56+
expect(() => {
57+
builder.addPkgNode(pkgToAdd, pkgToAdd.name);
58+
}).toThrow(ValidationError);
59+
});
60+
it('should throw error if package URL defines different version', () => {
61+
const pkgToAdd = {
62+
name: 'json',
63+
version: '1.0.0',
64+
purl: 'pkg:rpm/[email protected]',
65+
};
66+
expect(() => {
67+
builder.addPkgNode(pkgToAdd, pkgToAdd.name);
68+
}).toThrow(ValidationError);
69+
});
70+
it('successfully adds package with package URL', () => {
71+
const pkgToAdd = {
72+
name: 'json',
73+
version: '1.0.0',
74+
purl: 'pkg:rpm/rhel/[email protected]?repositories=a,b,c',
75+
};
76+
builder.addPkgNode(pkgToAdd, pkgToAdd.name);
77+
const packageAdded = builder
78+
.getPkgs()
79+
.find(
80+
(pkg) =>
81+
pkg.name === pkgToAdd.name && pkg.version === pkgToAdd.version,
82+
);
83+
expect(packageAdded).toEqual(pkgToAdd);
84+
});
85+
it('successfully handles maven special case', () => {
86+
const pkgToAdd = {
87+
name: 'com.namespace:foo',
88+
version: '1.0.0',
89+
purl: 'pkg:maven/com.namespace/[email protected]',
90+
};
91+
builder.addPkgNode(pkgToAdd, pkgToAdd.name);
92+
const packageAdded = builder
93+
.getPkgs()
94+
.find(
95+
(pkg) =>
96+
pkg.name === pkgToAdd.name && pkg.version === pkgToAdd.version,
97+
);
98+
expect(packageAdded).toEqual(pkgToAdd);
99+
});
100+
it('fails on missing group id on maven package', () => {
101+
// the groupId in maven is the namespace in purl.
102+
const pkgToAdd = {
103+
name: 'foo',
104+
version: '1.0.0',
105+
purl: 'pkg:maven/com.namespace/[email protected]',
106+
};
107+
expect(() => {
108+
builder.addPkgNode(pkgToAdd, pkgToAdd.name);
109+
}).toThrow(ValidationError);
110+
});
111+
it('fails on different group id on maven package', () => {
112+
// the groupId in maven is the namespace in purl.
113+
const pkgToAdd = {
114+
name: 'com.namespace:foo',
115+
version: '1.0.0',
116+
purl: 'pkg:maven/com.other/[email protected]',
117+
};
118+
expect(() => {
119+
builder.addPkgNode(pkgToAdd, pkgToAdd.name);
120+
}).toThrow(ValidationError);
121+
});
38122
});
39123

40124
describe('when using connectDep', () => {

test/core/equals.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,18 @@ describe('equals', () => {
125125
expect(a.equals(b, { compareRoot: false })).toBe(true);
126126
expect(b.equals(a, { compareRoot: false })).toBe(true);
127127
});
128+
129+
test('same graphs with different minor schema version', async () => {
130+
const a = depGraphLib.createFromJSON({
131+
...helpers.loadFixture('equals/simple.json'),
132+
schemaVersion: '1.2.0',
133+
});
134+
const b = depGraphLib.createFromJSON({
135+
...helpers.loadFixture('equals/simple.json'),
136+
schemaVersion: '1.3.0',
137+
});
138+
139+
expect(a.equals(b)).toBe(true);
140+
expect(b.equals(a)).toBe(true);
141+
});
128142
});

test/core/filter-from-graph.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ describe('filter-from-graph', function () {
3636
depGraph.getDepPkgs().length - 1,
3737
);
3838

39-
expect(result).toMatchSnapshot();
39+
expect(result.toJSON()).toMatchSnapshot({
40+
schemaVersion: expect.any(String),
41+
});
4042
});
4143

4244
it('should not mutate original depGraph', async () => {
@@ -70,15 +72,19 @@ describe('filter-from-graph', function () {
7072
expect(result.getDepPkgs().length).toEqual(
7173
depGraph.getDepPkgs().length - pkgs.length,
7274
);
73-
expect(result).toMatchSnapshot();
75+
expect(result.toJSON()).toMatchSnapshot({
76+
schemaVersion: expect.any(String),
77+
});
7478
});
7579

7680
it('should return an empty graph', async () => {
7781
const pkgs = depGraph.getDepPkgs();
7882
const result = await filterPackagesFromGraph(depGraph, pkgs);
7983

8084
expect(result.getDepPkgs().length).toBe(0);
81-
expect(result).toMatchSnapshot();
85+
expect(result.toJSON()).toMatchSnapshot({
86+
schemaVersion: expect.any(String),
87+
});
8288
});
8389
});
8490
});

test/legacy/__snapshots__/from-dep-tree.test.ts.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ Object {
140140
},
141141
},
142142
],
143-
"schemaVersion": "1.2.0",
143+
"schemaVersion": Any<String>,
144144
}
145145
`;
146146
@@ -659,7 +659,7 @@ Object {
659659
},
660660
},
661661
],
662-
"schemaVersion": "1.2.0",
662+
"schemaVersion": Any<String>,
663663
}
664664
`;
665665
@@ -7988,6 +7988,6 @@ Object {
79887988
},
79897989
},
79907990
],
7991-
"schemaVersion": "1.2.0",
7991+
"schemaVersion": Any<String>,
79927992
}
79937993
`;

0 commit comments

Comments
 (0)