@cyclonedx/cdxgen 12.2.0 → 12.3.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.
- package/README.md +242 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +532 -168
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +276 -68
- package/lib/cli/index.poku.js +368 -0
- package/lib/helpers/analyzer.js +1052 -5
- package/lib/helpers/analyzer.poku.js +301 -0
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/depsUtils.js +16 -0
- package/lib/helpers/depsUtils.poku.js +58 -1
- package/lib/helpers/display.js +245 -61
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +882 -136
- package/lib/helpers/utils.poku.js +995 -91
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +50 -0
- package/lib/server/server.js +228 -331
- package/lib/server/server.poku.js +220 -5
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +20 -5
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +13 -2
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -8
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +30 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -35
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- package/types/bin/licenses.d.ts.map +0 -1
|
@@ -1,28 +1,22 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import process from "node:process";
|
|
2
5
|
|
|
3
|
-
import
|
|
6
|
+
import esmock from "esmock";
|
|
7
|
+
import { assert, beforeEach, describe, it } from "poku";
|
|
8
|
+
import sinon from "sinon";
|
|
9
|
+
import { create as createTar } from "tar";
|
|
4
10
|
|
|
5
11
|
import {
|
|
6
12
|
addSkippedSrcFiles,
|
|
13
|
+
exportArchive,
|
|
7
14
|
exportImage,
|
|
8
|
-
|
|
9
|
-
getImage,
|
|
15
|
+
extractFromManifest,
|
|
10
16
|
isWin,
|
|
11
17
|
parseImageName,
|
|
12
|
-
removeImage,
|
|
13
18
|
} from "./docker.js";
|
|
14
19
|
|
|
15
|
-
if (process.env.CI === "true" && (isWin || process.platform === "darwin")) {
|
|
16
|
-
skip("Skipping Docker tests on Windows and Mac");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
await it("docker connection", async () => {
|
|
20
|
-
const dockerConn = await getConnection();
|
|
21
|
-
if (dockerConn) {
|
|
22
|
-
assert.ok(dockerConn);
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
20
|
it("parseImageName tests", () => {
|
|
27
21
|
if (isWin && process.env.CI === "true") {
|
|
28
22
|
return;
|
|
@@ -127,25 +121,330 @@ it("parseImageName tests", () => {
|
|
|
127
121
|
);
|
|
128
122
|
});
|
|
129
123
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
|
|
125
|
+
const dockerClient = sinon.stub().resolves(
|
|
126
|
+
clientResponse || {
|
|
127
|
+
Id: "sha256:hello-world",
|
|
128
|
+
RepoTags: ["hello-world:latest"],
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
dockerClient.stream = sinon.stub();
|
|
132
|
+
const gotStub = {
|
|
133
|
+
extend: sinon.stub().returns(dockerClient),
|
|
134
|
+
get: sinon.stub().resolves({ body: "OK" }),
|
|
135
|
+
};
|
|
136
|
+
const utilsStub = {
|
|
137
|
+
DEBUG_MODE: false,
|
|
138
|
+
extractPathEnv: sinon.stub().returns([]),
|
|
139
|
+
getAllFiles: sinon.stub().returns([]),
|
|
140
|
+
getTmpDir: sinon.stub().returns("/tmp"),
|
|
141
|
+
safeExistsSync: sinon.stub().returns(false),
|
|
142
|
+
safeMkdirSync: sinon.stub(),
|
|
143
|
+
safeSpawnSync: sinon.stub().returns({ status: 1, stdout: "", stderr: "" }),
|
|
144
|
+
...utilsOverrides,
|
|
145
|
+
};
|
|
146
|
+
const dockerModule = await esmock("./docker.js", {
|
|
147
|
+
got: { default: gotStub },
|
|
148
|
+
"../helpers/utils.js": utilsStub,
|
|
149
|
+
});
|
|
150
|
+
return { dockerClient, dockerModule, gotStub, utilsStub };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await it("docker connection uses the detected daemon client", async () => {
|
|
154
|
+
const { dockerModule, gotStub, dockerClient } = await loadDockerModule();
|
|
155
|
+
const dockerConn = await dockerModule.getConnection();
|
|
156
|
+
assert.strictEqual(dockerConn, dockerClient);
|
|
157
|
+
sinon.assert.calledOnce(gotStub.get);
|
|
158
|
+
sinon.assert.calledOnce(gotStub.extend);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await it("docker getImage returns inspect data from the daemon client", async () => {
|
|
162
|
+
const inspectData = {
|
|
163
|
+
Id: "sha256:hello-world",
|
|
164
|
+
RepoTags: ["hello-world:latest"],
|
|
165
|
+
};
|
|
166
|
+
const { dockerModule, dockerClient } = await loadDockerModule({
|
|
167
|
+
clientResponse: inspectData,
|
|
168
|
+
});
|
|
169
|
+
const imageData = await dockerModule.getImage("hello-world:latest");
|
|
170
|
+
assert.deepStrictEqual(imageData, inspectData);
|
|
171
|
+
sinon.assert.calledWith(
|
|
172
|
+
dockerClient,
|
|
173
|
+
"images/hello-world:latest/json",
|
|
174
|
+
sinon.match.has("method", "GET"),
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await it("docker getImage falls back to the daemon client when cli inspect fails", async () => {
|
|
179
|
+
const originalDockerUseCli = process.env.DOCKER_USE_CLI;
|
|
180
|
+
process.env.DOCKER_USE_CLI = "1";
|
|
181
|
+
try {
|
|
182
|
+
const inspectData = {
|
|
183
|
+
Id: "sha256:hello-world",
|
|
184
|
+
RepoTags: ["hello-world:latest"],
|
|
185
|
+
};
|
|
186
|
+
const { dockerModule, dockerClient } = await loadDockerModule({
|
|
187
|
+
clientResponse: inspectData,
|
|
188
|
+
});
|
|
189
|
+
const imageData = await dockerModule.getImage("hello-world:latest");
|
|
190
|
+
assert.deepStrictEqual(imageData, inspectData);
|
|
191
|
+
sinon.assert.calledWith(
|
|
192
|
+
dockerClient,
|
|
193
|
+
"images/hello-world:latest/json",
|
|
194
|
+
sinon.match.has("method", "GET"),
|
|
195
|
+
);
|
|
196
|
+
} finally {
|
|
197
|
+
if (originalDockerUseCli === undefined) {
|
|
198
|
+
delete process.env.DOCKER_USE_CLI;
|
|
199
|
+
} else {
|
|
200
|
+
process.env.DOCKER_USE_CLI = originalDockerUseCli;
|
|
201
|
+
}
|
|
133
202
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await it("docker getImage uses nerdctl when DOCKER_CMD is configured", async () => {
|
|
206
|
+
const originalDockerCmd = process.env.DOCKER_CMD;
|
|
207
|
+
const originalDockerUseCli = process.env.DOCKER_USE_CLI;
|
|
208
|
+
process.env.DOCKER_CMD = "nerdctl";
|
|
209
|
+
delete process.env.DOCKER_USE_CLI;
|
|
210
|
+
try {
|
|
211
|
+
const inspectData = {
|
|
212
|
+
Id: "sha256:hello-world",
|
|
213
|
+
RepoTags: ["hello-world:latest"],
|
|
214
|
+
};
|
|
215
|
+
const safeSpawnSync = sinon.stub();
|
|
216
|
+
safeSpawnSync
|
|
217
|
+
.onCall(0)
|
|
218
|
+
.returns({
|
|
219
|
+
status: 0,
|
|
220
|
+
stdout: '{"Repository":"hello-world","Tag":"latest"}\n',
|
|
221
|
+
stderr: "",
|
|
222
|
+
})
|
|
223
|
+
.onCall(1)
|
|
224
|
+
.returns({
|
|
225
|
+
status: 0,
|
|
226
|
+
stdout: JSON.stringify([inspectData]),
|
|
227
|
+
stderr: "",
|
|
228
|
+
});
|
|
229
|
+
const { dockerModule, utilsStub } = await loadDockerModule({
|
|
230
|
+
clientResponse: inspectData,
|
|
231
|
+
utilsOverrides: {
|
|
232
|
+
safeSpawnSync,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
const imageData = await dockerModule.getImage("hello-world:latest");
|
|
236
|
+
assert.deepStrictEqual(imageData, inspectData);
|
|
237
|
+
sinon.assert.calledWithExactly(safeSpawnSync, "nerdctl", [
|
|
238
|
+
"images",
|
|
239
|
+
"--format=json",
|
|
240
|
+
]);
|
|
241
|
+
sinon.assert.calledWithExactly(safeSpawnSync, "nerdctl", [
|
|
242
|
+
"inspect",
|
|
243
|
+
"hello-world:latest",
|
|
244
|
+
]);
|
|
245
|
+
sinon.assert.notCalled(utilsStub.safeMkdirSync);
|
|
246
|
+
} finally {
|
|
247
|
+
if (originalDockerCmd === undefined) {
|
|
248
|
+
delete process.env.DOCKER_CMD;
|
|
249
|
+
} else {
|
|
250
|
+
process.env.DOCKER_CMD = originalDockerCmd;
|
|
251
|
+
}
|
|
252
|
+
if (originalDockerUseCli === undefined) {
|
|
253
|
+
delete process.env.DOCKER_USE_CLI;
|
|
254
|
+
} else {
|
|
255
|
+
process.env.DOCKER_USE_CLI = originalDockerUseCli;
|
|
139
256
|
}
|
|
140
257
|
}
|
|
141
258
|
});
|
|
142
259
|
|
|
143
|
-
await it("docker
|
|
144
|
-
|
|
145
|
-
|
|
260
|
+
await it("docker exportImage ignores local directories", async () => {
|
|
261
|
+
const imageData = await exportImage(".");
|
|
262
|
+
assert.strictEqual(imageData, undefined);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await it("extractFromManifest derives PATH metadata from archive config", async () => {
|
|
266
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-docker-"));
|
|
267
|
+
try {
|
|
268
|
+
const allLayersExplodedDir = join(tempDir, "all-layers");
|
|
269
|
+
const manifestFile = join(tempDir, "manifest.json");
|
|
270
|
+
mkdirSync(allLayersExplodedDir, { recursive: true });
|
|
271
|
+
writeFileSync(
|
|
272
|
+
manifestFile,
|
|
273
|
+
JSON.stringify([
|
|
274
|
+
{
|
|
275
|
+
Config: "blobs/sha256/config.json",
|
|
276
|
+
Layers: ["blobs/sha256/layer.tar"],
|
|
277
|
+
},
|
|
278
|
+
]),
|
|
279
|
+
);
|
|
280
|
+
mkdirSync(join(tempDir, "blobs", "sha256"), { recursive: true });
|
|
281
|
+
writeFileSync(
|
|
282
|
+
join(tempDir, "blobs", "sha256", "config.json"),
|
|
283
|
+
JSON.stringify({
|
|
284
|
+
config: {
|
|
285
|
+
Env: [
|
|
286
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
287
|
+
],
|
|
288
|
+
WorkingDir: "/work",
|
|
289
|
+
},
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
writeFileSync(join(tempDir, "blobs", "sha256", "layer.tar"), "");
|
|
293
|
+
|
|
294
|
+
const exportData = await extractFromManifest(
|
|
295
|
+
manifestFile,
|
|
296
|
+
{},
|
|
297
|
+
tempDir,
|
|
298
|
+
allLayersExplodedDir,
|
|
299
|
+
{},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
assert.deepStrictEqual(exportData.binPaths, [
|
|
303
|
+
"/usr/local/sbin",
|
|
304
|
+
"/usr/local/bin",
|
|
305
|
+
"/usr/sbin",
|
|
306
|
+
"/usr/bin",
|
|
307
|
+
"/sbin",
|
|
308
|
+
"/bin",
|
|
309
|
+
]);
|
|
310
|
+
assert.deepStrictEqual(exportData.inspectData?.Config?.Env, [
|
|
311
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
312
|
+
]);
|
|
313
|
+
assert.strictEqual(
|
|
314
|
+
exportData.lastWorkingDir,
|
|
315
|
+
join(allLayersExplodedDir, "/work"),
|
|
316
|
+
);
|
|
317
|
+
} finally {
|
|
318
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
await it("extractFromManifest resolves OCI index manifests", async () => {
|
|
323
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-docker-"));
|
|
324
|
+
try {
|
|
325
|
+
const allLayersExplodedDir = join(tempDir, "all-layers");
|
|
326
|
+
const manifestFile = join(tempDir, "index.json");
|
|
327
|
+
mkdirSync(allLayersExplodedDir, { recursive: true });
|
|
328
|
+
mkdirSync(join(tempDir, "blobs", "sha256"), { recursive: true });
|
|
329
|
+
writeFileSync(
|
|
330
|
+
manifestFile,
|
|
331
|
+
JSON.stringify({
|
|
332
|
+
schemaVersion: 2,
|
|
333
|
+
manifests: [
|
|
334
|
+
{
|
|
335
|
+
digest: "sha256:manifest-blob",
|
|
336
|
+
mediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
writeFileSync(
|
|
342
|
+
join(tempDir, "blobs", "sha256", "manifest-blob"),
|
|
343
|
+
JSON.stringify({
|
|
344
|
+
schemaVersion: 2,
|
|
345
|
+
config: {
|
|
346
|
+
digest: "sha256:config-blob",
|
|
347
|
+
},
|
|
348
|
+
layers: [
|
|
349
|
+
{
|
|
350
|
+
digest: "sha256:layer-blob",
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
writeFileSync(
|
|
356
|
+
join(tempDir, "blobs", "sha256", "config-blob"),
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
config: {
|
|
359
|
+
Env: ["PATH=/usr/local/bin:/usr/bin:/bin"],
|
|
360
|
+
WorkingDir: "/workspace",
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
writeFileSync(join(tempDir, "blobs", "sha256", "layer-blob"), "");
|
|
365
|
+
|
|
366
|
+
const exportData = await extractFromManifest(
|
|
367
|
+
manifestFile,
|
|
368
|
+
{},
|
|
369
|
+
tempDir,
|
|
370
|
+
allLayersExplodedDir,
|
|
371
|
+
{},
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
assert.deepStrictEqual(exportData.binPaths, [
|
|
375
|
+
"/usr/local/bin",
|
|
376
|
+
"/usr/bin",
|
|
377
|
+
"/bin",
|
|
378
|
+
]);
|
|
379
|
+
assert.deepStrictEqual(exportData.inspectData?.Config?.Env, [
|
|
380
|
+
"PATH=/usr/local/bin:/usr/bin:/bin",
|
|
381
|
+
]);
|
|
382
|
+
assert.strictEqual(
|
|
383
|
+
exportData.lastWorkingDir,
|
|
384
|
+
join(allLayersExplodedDir, "/workspace"),
|
|
385
|
+
);
|
|
386
|
+
} finally {
|
|
387
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await it("exportArchive derives PATH metadata from blobs-only podman archives", async () => {
|
|
392
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-docker-"));
|
|
393
|
+
try {
|
|
394
|
+
const archiveDir = join(tempDir, "archive");
|
|
395
|
+
const archiveFile = join(tempDir, "podman-archive.tar");
|
|
396
|
+
mkdirSync(join(archiveDir, "blobs", "sha256"), { recursive: true });
|
|
397
|
+
writeFileSync(
|
|
398
|
+
join(archiveDir, "blobs", "sha256", "manifest-blob"),
|
|
399
|
+
JSON.stringify({
|
|
400
|
+
schemaVersion: 2,
|
|
401
|
+
config: {
|
|
402
|
+
digest: "sha256:config-blob",
|
|
403
|
+
},
|
|
404
|
+
layers: [
|
|
405
|
+
{
|
|
406
|
+
digest: "sha256:layer-blob",
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
}),
|
|
410
|
+
);
|
|
411
|
+
writeFileSync(
|
|
412
|
+
join(archiveDir, "blobs", "sha256", "config-blob"),
|
|
413
|
+
JSON.stringify({
|
|
414
|
+
config: {
|
|
415
|
+
Env: ["PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/bin"],
|
|
416
|
+
WorkingDir: "/app",
|
|
417
|
+
},
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
writeFileSync(join(archiveDir, "blobs", "sha256", "layer-blob"), "");
|
|
421
|
+
await createTar(
|
|
422
|
+
{
|
|
423
|
+
cwd: archiveDir,
|
|
424
|
+
file: archiveFile,
|
|
425
|
+
portable: true,
|
|
426
|
+
},
|
|
427
|
+
["blobs"],
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const exportData = await exportArchive(archiveFile, {});
|
|
431
|
+
|
|
432
|
+
assert.deepStrictEqual(exportData.binPaths, [
|
|
433
|
+
"/usr/local/sbin",
|
|
434
|
+
"/usr/local/bin",
|
|
435
|
+
"/usr/bin",
|
|
436
|
+
"/bin",
|
|
437
|
+
]);
|
|
438
|
+
assert.deepStrictEqual(exportData.inspectData?.Config?.Env, [
|
|
439
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/bin",
|
|
440
|
+
]);
|
|
441
|
+
assert.strictEqual(
|
|
442
|
+
exportData.lastWorkingDir,
|
|
443
|
+
join(exportData.allLayersExplodedDir, "app"),
|
|
444
|
+
);
|
|
445
|
+
} finally {
|
|
446
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
146
447
|
}
|
|
147
|
-
const imageData = await exportImage("hello-world:latest");
|
|
148
|
-
assert.ok(imageData);
|
|
149
448
|
});
|
|
150
449
|
|
|
151
450
|
describe("addSkippedSrcFiles tests", () => {
|
package/lib/managers/oci.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { arch } from "node:os";
|
|
4
4
|
|
|
5
|
+
import { isCycloneDxBom } from "../helpers/bomUtils.js";
|
|
5
6
|
import {
|
|
6
7
|
getAllFiles,
|
|
7
8
|
getTmpDir,
|
|
@@ -9,6 +10,100 @@ import {
|
|
|
9
10
|
safeSpawnSync,
|
|
10
11
|
} from "../helpers/utils.js";
|
|
11
12
|
|
|
13
|
+
const ORAS_CREATED_ANNOTATION = "org.opencontainers.image.created";
|
|
14
|
+
|
|
15
|
+
function getManifestDescriptors(manifestObj) {
|
|
16
|
+
if (Array.isArray(manifestObj?.manifests)) {
|
|
17
|
+
return manifestObj.manifests;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(manifestObj?.referrers)) {
|
|
20
|
+
return manifestObj.referrers;
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getRepositoryRef(image) {
|
|
26
|
+
let repositoryRef = image;
|
|
27
|
+
const digestIndex = repositoryRef.indexOf("@");
|
|
28
|
+
if (digestIndex !== -1) {
|
|
29
|
+
repositoryRef = repositoryRef.slice(0, digestIndex);
|
|
30
|
+
}
|
|
31
|
+
const lastSlashIndex = repositoryRef.lastIndexOf("/");
|
|
32
|
+
const tagIndex = repositoryRef.lastIndexOf(":");
|
|
33
|
+
if (tagIndex > lastSlashIndex) {
|
|
34
|
+
repositoryRef = repositoryRef.slice(0, tagIndex);
|
|
35
|
+
}
|
|
36
|
+
return repositoryRef;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getManifestImageRef(image, manifest) {
|
|
40
|
+
if (manifest?.reference) {
|
|
41
|
+
return manifest.reference;
|
|
42
|
+
}
|
|
43
|
+
if (manifest?.digest) {
|
|
44
|
+
return `${getRepositoryRef(image)}@${manifest.digest}`;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getManifestCreatedAt(manifest) {
|
|
50
|
+
const createdAt = manifest?.annotations?.[ORAS_CREATED_ANNOTATION];
|
|
51
|
+
if (!createdAt) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const createdAtTimestamp = Date.parse(createdAt);
|
|
55
|
+
if (Number.isNaN(createdAtTimestamp)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return createdAtTimestamp;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function selectManifestImageRef(image, manifestObj) {
|
|
62
|
+
const manifestDescriptors = getManifestDescriptors(manifestObj);
|
|
63
|
+
const candidates = manifestDescriptors
|
|
64
|
+
.map((manifest, index) => {
|
|
65
|
+
const imageRef = getManifestImageRef(image, manifest);
|
|
66
|
+
if (!imageRef) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
createdAt: getManifestCreatedAt(manifest),
|
|
71
|
+
imageRef,
|
|
72
|
+
index,
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
if (!candidates.length) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
candidates.sort((a, b) => {
|
|
80
|
+
if (a.createdAt !== undefined || b.createdAt !== undefined) {
|
|
81
|
+
if (a.createdAt === undefined) {
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
if (b.createdAt === undefined) {
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
if (b.createdAt !== a.createdAt) {
|
|
88
|
+
return b.createdAt - a.createdAt;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return b.index - a.index;
|
|
92
|
+
});
|
|
93
|
+
return candidates[0]?.imageRef;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getBomFiles(tmpDir) {
|
|
97
|
+
let bomFiles = getAllFiles(tmpDir, "**/*.{bom,cdx}.json");
|
|
98
|
+
if (!bomFiles.length) {
|
|
99
|
+
bomFiles = getAllFiles(tmpDir, "**/bom.json");
|
|
100
|
+
}
|
|
101
|
+
if (!bomFiles.length) {
|
|
102
|
+
bomFiles = getAllFiles(tmpDir, "**/*.json");
|
|
103
|
+
}
|
|
104
|
+
return bomFiles;
|
|
105
|
+
}
|
|
106
|
+
|
|
12
107
|
/**
|
|
13
108
|
* Retrieves a CycloneDX BOM attached to an OCI image using the `oras` CLI tool.
|
|
14
109
|
* Discovers SBOM attachments via `oras discover`, pulls the first matching
|
|
@@ -50,26 +145,8 @@ export function getBomWithOras(image, platform = undefined) {
|
|
|
50
145
|
const out = Buffer.from(result.stdout).toString();
|
|
51
146
|
try {
|
|
52
147
|
const manifestObj = JSON.parse(out);
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
if (
|
|
56
|
-
manifestObj?.manifests?.length &&
|
|
57
|
-
Array.isArray(manifestObj.manifests) &&
|
|
58
|
-
manifestObj.manifests[0]?.reference
|
|
59
|
-
) {
|
|
60
|
-
manifest = manifestObj.manifests[0];
|
|
61
|
-
}
|
|
62
|
-
} else if (manifestObj?.referrers) {
|
|
63
|
-
if (
|
|
64
|
-
manifestObj?.referrers?.length &&
|
|
65
|
-
Array.isArray(manifestObj.referrers) &&
|
|
66
|
-
manifestObj.referrers[0]?.reference
|
|
67
|
-
) {
|
|
68
|
-
manifest = manifestObj.referrers[0];
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (manifest != null) {
|
|
72
|
-
const imageRef = manifest.reference;
|
|
148
|
+
const imageRef = selectManifestImageRef(image, manifestObj);
|
|
149
|
+
if (imageRef) {
|
|
73
150
|
const tmpDir = getTmpDir();
|
|
74
151
|
result = safeSpawnSync("oras", ["pull", imageRef, "-o", tmpDir], {
|
|
75
152
|
shell: isWin,
|
|
@@ -80,9 +157,16 @@ export function getBomWithOras(image, platform = undefined) {
|
|
|
80
157
|
);
|
|
81
158
|
return undefined;
|
|
82
159
|
}
|
|
83
|
-
const bomFiles =
|
|
84
|
-
|
|
85
|
-
|
|
160
|
+
const bomFiles = getBomFiles(tmpDir);
|
|
161
|
+
for (const bomFile of bomFiles) {
|
|
162
|
+
try {
|
|
163
|
+
const bomJson = JSON.parse(fs.readFileSync(bomFile, "utf8"));
|
|
164
|
+
if (isCycloneDxBom(bomJson)) {
|
|
165
|
+
return bomJson;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Ignore unrelated or malformed JSON files pulled alongside the SBOM.
|
|
169
|
+
}
|
|
86
170
|
}
|
|
87
171
|
} else {
|
|
88
172
|
console.log(`${image} does not contain any SBOM attachment!`);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import esmock from "esmock";
|
|
6
|
+
import { assert, describe, it } from "poku";
|
|
7
|
+
import sinon from "sinon";
|
|
8
|
+
|
|
9
|
+
async function loadOciModule({ getAllFiles, getTmpDir, safeSpawnSync }) {
|
|
10
|
+
return esmock("./oci.js", {
|
|
11
|
+
"../helpers/utils.js": {
|
|
12
|
+
getAllFiles,
|
|
13
|
+
getTmpDir,
|
|
14
|
+
isWin: false,
|
|
15
|
+
safeSpawnSync,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("getBomWithOras()", () => {
|
|
21
|
+
it("pulls the newest digest-only SBOM referrer", async () => {
|
|
22
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-oci-poku-"));
|
|
23
|
+
const bomFile = path.join(tmpDir, "sbom-oci-image.cdx.json");
|
|
24
|
+
const bomJson = {
|
|
25
|
+
bomFormat: "CycloneDX",
|
|
26
|
+
specVersion: "1.6",
|
|
27
|
+
signature: {
|
|
28
|
+
algorithm: "RS512",
|
|
29
|
+
value: "signed",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const safeSpawnSync = sinon.stub();
|
|
33
|
+
const getAllFiles = sinon.stub();
|
|
34
|
+
try {
|
|
35
|
+
writeFileSync(bomFile, JSON.stringify(bomJson), "utf8");
|
|
36
|
+
safeSpawnSync
|
|
37
|
+
.onCall(0)
|
|
38
|
+
.returns({
|
|
39
|
+
status: 0,
|
|
40
|
+
stdout: JSON.stringify({
|
|
41
|
+
referrers: [
|
|
42
|
+
{
|
|
43
|
+
digest: "sha256:older",
|
|
44
|
+
annotations: {
|
|
45
|
+
"org.opencontainers.image.created": "2026-04-29T01:26:38Z",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
digest: "sha256:newer",
|
|
50
|
+
annotations: {
|
|
51
|
+
"org.opencontainers.image.created": "2026-04-29T02:00:20Z",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
}),
|
|
56
|
+
})
|
|
57
|
+
.onCall(1)
|
|
58
|
+
.returns({
|
|
59
|
+
status: 0,
|
|
60
|
+
stdout: "",
|
|
61
|
+
});
|
|
62
|
+
getAllFiles.withArgs(tmpDir, "**/*.{bom,cdx}.json").returns([bomFile]);
|
|
63
|
+
const { getBomWithOras } = await loadOciModule({
|
|
64
|
+
getAllFiles,
|
|
65
|
+
getTmpDir: sinon.stub().returns(tmpDir),
|
|
66
|
+
safeSpawnSync,
|
|
67
|
+
});
|
|
68
|
+
const result = getBomWithOras("ghcr.io/cdxgen/alpine-python313:master");
|
|
69
|
+
assert.deepStrictEqual(result, bomJson);
|
|
70
|
+
sinon.assert.calledWith(
|
|
71
|
+
safeSpawnSync,
|
|
72
|
+
"oras",
|
|
73
|
+
["pull", "ghcr.io/cdxgen/alpine-python313@sha256:newer", "-o", tmpDir],
|
|
74
|
+
{ shell: false },
|
|
75
|
+
);
|
|
76
|
+
} finally {
|
|
77
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("falls back to bom.json when oras pulls a plain BOM filename", async () => {
|
|
82
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-oci-poku-"));
|
|
83
|
+
const bomFile = path.join(tmpDir, "bom.json");
|
|
84
|
+
const bomJson = {
|
|
85
|
+
bomFormat: "CycloneDX",
|
|
86
|
+
specVersion: "1.6",
|
|
87
|
+
signature: {
|
|
88
|
+
algorithm: "RS512",
|
|
89
|
+
value: "signed",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const safeSpawnSync = sinon.stub();
|
|
93
|
+
const getAllFiles = sinon.stub();
|
|
94
|
+
try {
|
|
95
|
+
writeFileSync(bomFile, JSON.stringify(bomJson), "utf8");
|
|
96
|
+
safeSpawnSync
|
|
97
|
+
.onCall(0)
|
|
98
|
+
.returns({
|
|
99
|
+
status: 0,
|
|
100
|
+
stdout: JSON.stringify({
|
|
101
|
+
manifests: [
|
|
102
|
+
{
|
|
103
|
+
reference: "ghcr.io/cdxgen/demo@sha256:latest",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
})
|
|
108
|
+
.onCall(1)
|
|
109
|
+
.returns({
|
|
110
|
+
status: 0,
|
|
111
|
+
stdout: "",
|
|
112
|
+
});
|
|
113
|
+
getAllFiles.withArgs(tmpDir, "**/*.{bom,cdx}.json").returns([]);
|
|
114
|
+
getAllFiles.withArgs(tmpDir, "**/bom.json").returns([bomFile]);
|
|
115
|
+
const { getBomWithOras } = await loadOciModule({
|
|
116
|
+
getAllFiles,
|
|
117
|
+
getTmpDir: sinon.stub().returns(tmpDir),
|
|
118
|
+
safeSpawnSync,
|
|
119
|
+
});
|
|
120
|
+
const result = getBomWithOras("ghcr.io/cdxgen/demo:master");
|
|
121
|
+
assert.deepStrictEqual(result, bomJson);
|
|
122
|
+
sinon.assert.calledWith(
|
|
123
|
+
safeSpawnSync,
|
|
124
|
+
"oras",
|
|
125
|
+
["pull", "ghcr.io/cdxgen/demo@sha256:latest", "-o", tmpDir],
|
|
126
|
+
{ shell: false },
|
|
127
|
+
);
|
|
128
|
+
} finally {
|
|
129
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|