Skip to content

Commit 884bc4e

Browse files
authored
Verify oci images with SBOM attachments (#1799)
Signed-off-by: Prabhu Subramanian <[email protected]>
1 parent 1a74c0f commit 884bc4e

File tree

8 files changed

+99
-7
lines changed

8 files changed

+99
-7
lines changed

.github/workflows/image-build.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ jobs:
9393
SBOM_SIGN_PRIVATE_KEY: ${{ secrets.SBOM_SIGN_PRIVATE_KEY }}
9494
- name: Attach cdx sbom to base
9595
run: |
96+
mkdir -p $RUNNER_TEMP/cdxgen-sboms
9697
corepack pnpm install --config.strict-dep-builds=true --package-import-method copy --frozen-lockfile
9798
node bin/cdxgen.js -t docker -o sbom-oci-base-image.cdx.json ${{ fromJSON(steps.base-metadata.outputs.json).tags[0] }}
9899
node bin/verify.js -i sbom-oci-base-image.cdx.json --public-key contrib/bom-signer/public.key
@@ -103,15 +104,18 @@ jobs:
103104
env:
104105
SBOM_SIGN_ALGORITHM: RS512
105106
SBOM_SIGN_PRIVATE_KEY: ${{ github.workspace }}/private.key
107+
CDXGEN_TEMP_DIR: ${{ runner.temp }}/cdxgen-sboms
106108
- name: Attach cdx sbom
107109
run: |
110+
mkdir -p $RUNNER_TEMP/cdxgen-sboms
108111
corepack pnpm install --config.strict-dep-builds=true --package-import-method copy --frozen-lockfile
109112
node bin/cdxgen.js -t docker -o sbom-oci-image.cdx.json ${{ fromJSON(steps.cdxgen-metadata.outputs.json).tags[0] }}
110113
node bin/verify.js -i sbom-oci-image.cdx.json --public-key contrib/bom-signer/public.key
111114
oras attach --artifact-type sbom/cyclonedx ${{ fromJSON(steps.cdxgen-metadata.outputs.json).tags[0] }} ./sbom-oci-image.cdx.json:application/json
112115
oras discover --format tree ${{ fromJSON(steps.cdxgen-metadata.outputs.json).tags[0] }}
113116
continue-on-error: true
114-
if: ${{ startsWith(github.ref, 'refs/tags/') && ! fromJSON(inputs.image).cdxgen-image.skip-tags }}
117+
if: ${{ ! fromJSON(inputs.image).cdxgen-image.skip-tags }}
115118
env:
116119
SBOM_SIGN_ALGORITHM: RS512
117120
SBOM_SIGN_PRIVATE_KEY: ${{ github.workspace }}/private.key
121+
CDXGEN_TEMP_DIR: ${{ runner.temp }}/cdxgen-sboms

bin/verify.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import fs from "node:fs";
44
import { join } from "node:path";
55
import process from "node:process";
6-
import { URL } from "node:url";
76
import jws from "jws";
87
import yargs from "yargs";
98
import { hideBin } from "yargs/helpers";
109
import { dirNameStr } from "../lib/helpers/utils.js";
10+
import { getBomWithOras } from "../lib/managers/oci.js";
1111

1212
const dirName = dirNameStr;
1313

@@ -26,6 +26,7 @@ const args = yargs(hideBin(process.argv))
2626
.scriptName("cdx-verify")
2727
.version()
2828
.help("h")
29+
.alias("h", "help")
2930
.wrap(Math.min(120, yargs().terminalWidth())).argv;
3031

3132
if (args.version) {
@@ -43,7 +44,25 @@ if (process.env?.CDXGEN_NODE_OPTIONS) {
4344
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS || ""} ${process.env.CDXGEN_NODE_OPTIONS}`;
4445
}
4546

46-
const bomJson = JSON.parse(fs.readFileSync(args.input, "utf8"));
47+
function getBom(args) {
48+
if (fs.existsSync(args.input)) {
49+
return JSON.parse(fs.readFileSync(args.input, "utf8"));
50+
}
51+
if (
52+
args.input.includes(":") ||
53+
args.input.includes("docker") ||
54+
args.input.includes("ghcr")
55+
) {
56+
return getBomWithOras(args.input);
57+
}
58+
return undefined;
59+
}
60+
61+
const bomJson = getBom(args);
62+
if (!bomJson) {
63+
console.log(`${args.input} is invalid!`);
64+
process.exit(1);
65+
}
4766
let hasInvalidComp = false;
4867
// Validate any component signature
4968
for (const comp of bomJson.components) {
@@ -77,7 +96,7 @@ if (!bomSignature) {
7796
if (validationResult) {
7897
console.log("Signature is valid!");
7998
} else {
80-
console.log("SBOM signature is invalid!");
99+
console.log("BOM signature is invalid!");
81100
process.exit(1);
82101
}
83102
}

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "11.3.1",
3+
"version": "11.3.2",
44
"exports": "./lib/cli/index.js",
55
"compilerOptions": {
66
"lib": ["deno.window"],

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "11.3.1",
3+
"version": "11.3.2",
44
"exports": "./lib/cli/index.js",
55
"include": ["*.js", "lib/**", "bin/**", "data/**", "types/**"],
66
"exclude": [

lib/managers/oci.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Buffer } from "node:buffer";
2+
import { spawnSync } from "node:child_process";
3+
import fs from "node:fs";
4+
import { MAX_BUFFER, getAllFiles, getTmpDir, isWin } from "../helpers/utils.js";
5+
6+
export function getBomWithOras(image) {
7+
let result = spawnSync(
8+
"oras",
9+
[
10+
"discover",
11+
"--format",
12+
"json",
13+
"--artifact-type",
14+
"sbom/cyclonedx",
15+
image,
16+
],
17+
{
18+
encoding: "utf-8",
19+
shell: isWin,
20+
maxBuffer: MAX_BUFFER,
21+
},
22+
);
23+
if (result.status !== 0 || result.error) {
24+
console.log(
25+
"Install oras by following the instructions at: https://oras.land/docs/installation",
26+
);
27+
if (result.stderr) {
28+
console.log(result.stderr);
29+
}
30+
return undefined;
31+
}
32+
if (result.stdout) {
33+
const out = Buffer.from(result.stdout).toString();
34+
try {
35+
const manifestObj = JSON.parse(out);
36+
if (
37+
manifestObj?.manifests?.length &&
38+
Array.isArray(manifestObj.manifests) &&
39+
manifestObj.manifests[0]?.reference
40+
) {
41+
const imageRef = manifestObj.manifests[0].reference;
42+
const tmpDir = getTmpDir();
43+
result = spawnSync("oras", ["pull", imageRef, "-o", tmpDir], {
44+
encoding: "utf-8",
45+
shell: isWin,
46+
maxBuffer: MAX_BUFFER,
47+
});
48+
if (result.status !== 0 || result.error) {
49+
console.log(
50+
`Unable to pull the SBOM attachment for ${imageRef} with oras!`,
51+
);
52+
return undefined;
53+
}
54+
const bomFiles = getAllFiles(tmpDir, "**/*.{bom,cdx}.json");
55+
if (bomFiles.length) {
56+
return JSON.parse(fs.readFileSync(bomFiles.pop(), "utf8"));
57+
}
58+
} else {
59+
console.log(`${image} does not contain any SBOM attachment!`);
60+
}
61+
} catch (e) {
62+
console.log(e);
63+
}
64+
}
65+
return undefined;
66+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "11.3.1",
3+
"version": "11.3.2",
44
"description": "Creates CycloneDX Software Bill of Materials (SBOM) from source or container image",
55
"homepage": "http://github.com/cyclonedx/cdxgen",
66
"author": "Prabhu Subramanian <[email protected]>",

types/lib/managers/oci.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export function getBomWithOras(image: any): any;
2+
//# sourceMappingURL=oci.d.ts.map

types/lib/managers/oci.d.ts.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)