Skip to content

Commit 67c3f4f

Browse files
author
Lukas Fischer
committed
#1838 Fix image name and version recognition
The previous way of detecting the name and version to supply to Dependency-Track was very brittle, it already failed for image references including a hash, resulting in names like hello-world@sha256, because it would only split on ':' and then select the first and last component. This version uses a regex to as accurately as possible match the individual components of a docker image reference. The regex comes from the [official implementation] on GitHub, but is actually taken from a [pull request], which adds named capture groups and fixes an issue with domain recognition being too eager. Yes the regex looks pretty wild, yes there are tests. I don't think it makes sense to build the regex from the individual components like the docker library does it. Unfortunately this does not solve the problem of actually getting the reference from somewhere, for images it works with getting it from the name of the main component, but this part stays brittle. Annotations to the scans might be a possible solution for that. [official implementation]: https://github.com/distribution/reference [pull request]: distribution/distribution#3803 Signed-off-by: Lukas Fischer <lukas.fischer@iteratec.com>
1 parent dccdeac commit 67c3f4f

2 files changed

Lines changed: 87 additions & 3 deletions

File tree

hooks/persistence-dependencytrack/hook/hook.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,26 @@ async function handle({
2727
// Get the project name and version from the name attribute of the main component
2828
// This might be a bit brittle, but there is not really a better way to get this information
2929
// Neither Trivy's nor Syft's SBOM contains a useful version attribute (none or sha256)
30-
const components = result.metadata.component.name.split(':');
31-
const name = components[0];
32-
const version = components.length > 1 ? components.pop() : "latest";
30+
31+
// Get the components of a docker image reference, the regex is a direct JavaScript adaption of
32+
// the official Go-implementation available at https://github.com/distribution/reference/blob/main/regexp.go
33+
// but taken from pull request https://github.com/distribution/distribution/pull/3803 which
34+
// introduces the named groups and fixes the issue that in "bkimminich/juice-shop" the regex
35+
// detects "bkimminich" as part of the domain/host.
36+
const imageRegex = new RegExp([
37+
'^(?<name>(?:(?<domain>(?:localhost|(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])',
38+
'(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+|',
39+
'(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])',
40+
'(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*',
41+
'(?::[0-9]+)|\\[(?:[a-fA-F0-9:]+)\\](?::[0-9]+)?)(?::[0-9]+)?)\\/)?',
42+
'(?<repository>[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*',
43+
'(?:\\/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*))',
44+
'(?::(?<tag>[\\w][\\w.-]{0,127}))?',
45+
'(?:@(?<digest>[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,}))?$',
46+
].join(''));
47+
const groups = imageRegex.exec(result.metadata.component.name).groups
48+
const name = groups.name
49+
const version = groups.tag || groups.digest || "latest"
3350

3451
// The POST endpoint expects multipart/form-data
3552
// Alternatively the PUT endpoint could be used, which requires base64-encoding the SBOM

hooks/persistence-dependencytrack/hook/hook.test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,70 @@ test("should send a post request to the url when fired", async () => {
111111

112112
expect(fetch.mock.calls[0][1].body.get("bom")).toBe(JSON.stringify(result));
113113
});
114+
115+
// Make sure that the crazy regex to parse the reference parts actually works
116+
test.each([
117+
{
118+
reference: "bkimminich/juice-shop:v15.0.0",
119+
name: "bkimminich/juice-shop",
120+
version: "v15.0.0"
121+
},
122+
{
123+
reference: "ubuntu@sha256:b492494d8e0113c4ad3fe4528a4b5ff89faa5331f7d52c5c138196f69ce176a6",
124+
name: "ubuntu",
125+
version: "sha256:b492494d8e0113c4ad3fe4528a4b5ff89faa5331f7d52c5c138196f69ce176a6"
126+
},
127+
{
128+
reference: "hello-world",
129+
name: "hello-world",
130+
version: "latest"
131+
},
132+
{
133+
reference: "gcr.io/distroless/cc-debian12:debug-nonroot",
134+
name: "gcr.io/distroless/cc-debian12",
135+
version: "debug-nonroot"
136+
},
137+
{
138+
reference: "myawesomedockerhub.example.org:8080/notthetag",
139+
name: "myawesomedockerhub.example.org:8080/notthetag",
140+
version: "latest"
141+
},
142+
])("should detect image reference components accurately", async ({ reference, name, version }) => {
143+
const result = {
144+
bomFormat: "CycloneDX",
145+
metadata: {
146+
component: {
147+
name: reference
148+
}
149+
}
150+
};
151+
152+
const getRawResults = async () => result;
153+
154+
const scan = {
155+
metadata: {
156+
uid: "a30122a6-7f1a-4e37-ae81-2c25ed7fb8f5",
157+
name: "demo-sbom",
158+
},
159+
status: {
160+
rawResultType: "sbom-cyclonedx"
161+
}
162+
};
163+
164+
const apiKey = "verysecretgitleaksplsignore"
165+
const baseUrl = "http://example.com/foo/bar";
166+
const url = baseUrl + "/api/v1/bom"
167+
168+
await handle({ getRawResults, scan, apiKey, baseUrl, fetch });
169+
170+
expect(fetch).toBeCalledTimes(1);
171+
expect(fetch).toBeCalledWith(url, expect.objectContaining({
172+
method: "POST",
173+
headers: {
174+
"X-API-Key": apiKey,
175+
},
176+
}));
177+
178+
expect(fetch.mock.calls[0][1].body.get("projectName")).toBe(name);
179+
expect(fetch.mock.calls[0][1].body.get("projectVersion")).toBe(version);
180+
});

0 commit comments

Comments
 (0)