@cyclonedx/cdxgen 12.3.3 → 12.4.1
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 +69 -25
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +270 -127
- package/bin/convert.js +34 -15
- package/bin/hbom.js +495 -0
- package/bin/repl.js +592 -37
- package/bin/validate.js +31 -4
- package/bin/verify.js +18 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -0
- package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
- package/data/predictive-audit-allowlist.json +11 -0
- package/data/queries-darwin.json +12 -1
- package/data/queries-win.json +7 -1
- package/data/queries.json +39 -2
- package/data/rules/ai-agent-governance.yaml +16 -0
- package/data/rules/asar-archives.yaml +150 -0
- package/data/rules/chrome-extensions.yaml +8 -0
- package/data/rules/ci-permissions.yaml +42 -18
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +11 -0
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +18 -3
- package/data/rules/obom-runtime.yaml +907 -22
- package/data/rules/package-integrity.yaml +14 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +210 -8
- package/lib/audit/index.poku.js +332 -0
- package/lib/audit/reporters.js +222 -0
- package/lib/audit/targets.js +146 -1
- package/lib/audit/targets.poku.js +186 -0
- package/lib/cli/asar.poku.js +328 -0
- package/lib/cli/index.js +527 -99
- package/lib/cli/index.poku.js +1469 -212
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/analyzer.js +1406 -29
- package/lib/helpers/analyzer.poku.js +342 -0
- package/lib/helpers/analyzerScope.js +712 -0
- package/lib/helpers/asarutils.js +1556 -0
- package/lib/helpers/asarutils.poku.js +443 -0
- package/lib/helpers/auditCategories.js +12 -0
- package/lib/helpers/auditCategories.poku.js +32 -0
- package/lib/helpers/bomUtils.js +155 -1
- package/lib/helpers/bomUtils.poku.js +79 -1
- package/lib/helpers/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/display.js +291 -1
- package/lib/helpers/display.poku.js +149 -0
- package/lib/helpers/evidenceUtils.js +58 -0
- package/lib/helpers/evidenceUtils.poku.js +54 -0
- package/lib/helpers/exportUtils.js +9 -0
- package/lib/helpers/gtfobins.js +142 -8
- package/lib/helpers/gtfobins.poku.js +24 -1
- package/lib/helpers/hbom.js +710 -0
- package/lib/helpers/hbom.poku.js +496 -0
- package/lib/helpers/hbomAnalysis.js +268 -0
- package/lib/helpers/hbomAnalysis.poku.js +249 -0
- package/lib/helpers/hbomLoader.js +35 -0
- package/lib/helpers/hostTopology.js +803 -0
- package/lib/helpers/hostTopology.poku.js +363 -0
- package/lib/helpers/inventoryStats.js +69 -0
- package/lib/helpers/inventoryStats.poku.js +86 -0
- package/lib/helpers/lolbas.js +19 -1
- package/lib/helpers/lolbas.poku.js +23 -0
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +350 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/protobom.js +209 -45
- package/lib/helpers/protobom.poku.js +183 -5
- package/lib/helpers/protobomLoader.js +43 -0
- package/lib/helpers/protobomLoader.poku.js +31 -0
- package/lib/helpers/remote/dependency-track.js +36 -3
- package/lib/helpers/remote/dependency-track.poku.js +44 -0
- package/lib/helpers/source.js +24 -0
- package/lib/helpers/source.poku.js +32 -0
- package/lib/helpers/utils.js +1438 -93
- package/lib/helpers/utils.poku.js +846 -4
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2293 -353
- package/lib/managers/binary.poku.js +1699 -1
- package/lib/managers/docker.js +201 -79
- package/lib/managers/docker.poku.js +337 -12
- package/lib/server/server.js +4 -28
- package/lib/stages/postgen/annotator.js +38 -0
- package/lib/stages/postgen/annotator.poku.js +107 -1
- package/lib/stages/postgen/auditBom.js +121 -18
- package/lib/stages/postgen/auditBom.poku.js +1366 -31
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +406 -8
- package/lib/stages/postgen/postgen.poku.js +484 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/lib/validator/bomValidator.js +90 -38
- package/lib/validator/bomValidator.poku.js +90 -0
- package/lib/validator/complianceRules.js +4 -2
- package/lib/validator/index.poku.js +14 -0
- package/package.json +23 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +44 -0
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts +16 -0
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +16 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +4 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +33 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/analyzerScope.d.ts +11 -0
- package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
- package/types/lib/helpers/asarutils.d.ts +34 -0
- package/types/lib/helpers/asarutils.d.ts.map +1 -0
- package/types/lib/helpers/auditCategories.d.ts +5 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -1
- package/types/lib/helpers/bomUtils.d.ts +10 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/evidenceUtils.d.ts +8 -0
- package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +8 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -1
- package/types/lib/helpers/hbom.d.ts +49 -0
- package/types/lib/helpers/hbom.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
- package/types/lib/helpers/hbomLoader.d.ts +7 -0
- package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
- package/types/lib/helpers/hostTopology.d.ts +12 -0
- package/types/lib/helpers/hostTopology.d.ts.map +1 -0
- package/types/lib/helpers/inventoryStats.d.ts +11 -0
- package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -1
- package/types/lib/helpers/osqueryTransform.d.ts +3 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts +58 -0
- package/types/lib/helpers/plugins.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +5 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/protobomLoader.d.ts +17 -0
- package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +45 -8
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +5 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -1
- 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 +26 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +2 -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/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
- package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/data/spdx-model-v3.0.1.jsonld +0 -15999
package/lib/cli/index.poku.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
3
5
|
mkdirSync,
|
|
4
6
|
mkdtempSync,
|
|
5
7
|
readFileSync,
|
|
6
8
|
rmSync,
|
|
7
9
|
writeFileSync,
|
|
8
10
|
} from "node:fs";
|
|
11
|
+
import { createServer } from "node:http";
|
|
9
12
|
import { tmpdir } from "node:os";
|
|
10
13
|
import { dirname, join, normalize, sep } from "node:path";
|
|
11
14
|
import process from "node:process";
|
|
@@ -15,6 +18,12 @@ import esmock from "esmock";
|
|
|
15
18
|
import { assert, describe, it } from "poku";
|
|
16
19
|
import sinon from "sinon";
|
|
17
20
|
|
|
21
|
+
import { readBinary } from "../helpers/protobom.js";
|
|
22
|
+
import {
|
|
23
|
+
getRecordedActivities,
|
|
24
|
+
resetRecordedActivities,
|
|
25
|
+
setDryRunMode,
|
|
26
|
+
} from "../helpers/utils.js";
|
|
18
27
|
import { auditBom } from "../stages/postgen/auditBom.js";
|
|
19
28
|
import { postProcess } from "../stages/postgen/postgen.js";
|
|
20
29
|
import {
|
|
@@ -22,7 +31,10 @@ import {
|
|
|
22
31
|
createChromeExtensionBom,
|
|
23
32
|
createNodejsBom,
|
|
24
33
|
createPHPBom,
|
|
34
|
+
createPythonBom,
|
|
25
35
|
createRustBom,
|
|
36
|
+
listComponents,
|
|
37
|
+
submitBom,
|
|
26
38
|
} from "./index.js";
|
|
27
39
|
|
|
28
40
|
const fixtureDir = join(
|
|
@@ -60,6 +72,14 @@ const mcpFixtureDir = join(
|
|
|
60
72
|
"data",
|
|
61
73
|
"mcp-repotest",
|
|
62
74
|
);
|
|
75
|
+
const cbomFixtureDir = join(
|
|
76
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
77
|
+
"..",
|
|
78
|
+
"..",
|
|
79
|
+
"test",
|
|
80
|
+
"data",
|
|
81
|
+
"cbom-js-repotest",
|
|
82
|
+
);
|
|
63
83
|
const cacheDisableFixtureDir = join(
|
|
64
84
|
dirname(fileURLToPath(import.meta.url)),
|
|
65
85
|
"..",
|
|
@@ -149,7 +169,125 @@ function getNpmPackFilePaths() {
|
|
|
149
169
|
return packSummary.files.map((file) => toPortablePath(file.path));
|
|
150
170
|
}
|
|
151
171
|
|
|
172
|
+
function buildMinimalCliEnv(extraEnv = {}) {
|
|
173
|
+
const baseEnv = {
|
|
174
|
+
HOME: process.env.HOME,
|
|
175
|
+
PATH: process.env.PATH,
|
|
176
|
+
TMPDIR: process.env.TMPDIR,
|
|
177
|
+
};
|
|
178
|
+
if (process.platform === "win32") {
|
|
179
|
+
baseEnv.SystemRoot = process.env.SystemRoot;
|
|
180
|
+
baseEnv.TEMP = process.env.TEMP;
|
|
181
|
+
baseEnv.TMP = process.env.TMP;
|
|
182
|
+
baseEnv.USERPROFILE = process.env.USERPROFILE;
|
|
183
|
+
}
|
|
184
|
+
return Object.fromEntries(
|
|
185
|
+
Object.entries({
|
|
186
|
+
...baseEnv,
|
|
187
|
+
...extraEnv,
|
|
188
|
+
}).filter(([, value]) => value !== undefined),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function startSubmitBomTestServer(requestHandler) {
|
|
193
|
+
const requests = [];
|
|
194
|
+
const server = createServer((req, res) => {
|
|
195
|
+
let body = "";
|
|
196
|
+
req.setEncoding("utf8");
|
|
197
|
+
req.on("data", (chunk) => {
|
|
198
|
+
body += chunk;
|
|
199
|
+
});
|
|
200
|
+
req.on("end", async () => {
|
|
201
|
+
const request = {
|
|
202
|
+
body,
|
|
203
|
+
headers: req.headers,
|
|
204
|
+
rawHeaders: req.rawHeaders,
|
|
205
|
+
method: req.method,
|
|
206
|
+
url: req.url,
|
|
207
|
+
};
|
|
208
|
+
requests.push(request);
|
|
209
|
+
const response = (await requestHandler(request, requests.length)) || {};
|
|
210
|
+
if (res.writableEnded) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
res.writeHead(response.statusCode || 200, {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
});
|
|
216
|
+
res.end(JSON.stringify(response.body || { success: true }));
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
await new Promise((resolve) => {
|
|
220
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
221
|
+
});
|
|
222
|
+
const address = server.address();
|
|
223
|
+
const serverUrl = `http://127.0.0.1:${address.port}`;
|
|
224
|
+
return {
|
|
225
|
+
close: () =>
|
|
226
|
+
new Promise((resolve, reject) => {
|
|
227
|
+
server.close((error) => {
|
|
228
|
+
if (error) {
|
|
229
|
+
reject(error);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
resolve();
|
|
233
|
+
});
|
|
234
|
+
}),
|
|
235
|
+
requests,
|
|
236
|
+
serverUrl,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getRequestHeader(request, headerName) {
|
|
241
|
+
const normalizedHeaderName = headerName.toLowerCase();
|
|
242
|
+
const directValue = request?.headers?.[normalizedHeaderName];
|
|
243
|
+
if (directValue !== undefined) {
|
|
244
|
+
return Array.isArray(directValue) ? directValue[0] : directValue;
|
|
245
|
+
}
|
|
246
|
+
const rawHeaders = request?.rawHeaders;
|
|
247
|
+
if (!Array.isArray(rawHeaders)) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
251
|
+
if (rawHeaders[index]?.toLowerCase() === normalizedHeaderName) {
|
|
252
|
+
return rawHeaders[index + 1];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
152
258
|
describe("CLI tests", () => {
|
|
259
|
+
describe("component creation", () => {
|
|
260
|
+
it("keeps readable OBOM bom-refs when no package purl type is available", () => {
|
|
261
|
+
const components = listComponents(
|
|
262
|
+
{ specVersion: 1.7 },
|
|
263
|
+
undefined,
|
|
264
|
+
[
|
|
265
|
+
{
|
|
266
|
+
"bom-ref":
|
|
267
|
+
"osquery:authorized_keys_snapshot:data:root@ssh-ed25519[key_file=/root/.ssh/authorized_keys]",
|
|
268
|
+
name: "root",
|
|
269
|
+
properties: [
|
|
270
|
+
{
|
|
271
|
+
name: "cdx:osquery:category",
|
|
272
|
+
value: "authorized_keys_snapshot",
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
type: "data",
|
|
276
|
+
version: "ssh-ed25519",
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
"",
|
|
280
|
+
);
|
|
281
|
+
assert.strictEqual(components.length, 1);
|
|
282
|
+
assert.strictEqual(components[0].purl, undefined);
|
|
283
|
+
assert.strictEqual(
|
|
284
|
+
components[0]["bom-ref"],
|
|
285
|
+
"osquery:authorized_keys_snapshot:data:root@ssh-ed25519[key_file=/root/.ssh/authorized_keys]",
|
|
286
|
+
);
|
|
287
|
+
assert.strictEqual(components[0].type, "data");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
153
291
|
describe("distribution filters", () => {
|
|
154
292
|
it("keeps npm types while excluding poku tests from npm pack output", () => {
|
|
155
293
|
const packedPaths = getNpmPackFilePaths();
|
|
@@ -169,6 +307,560 @@ describe("CLI tests", () => {
|
|
|
169
307
|
});
|
|
170
308
|
});
|
|
171
309
|
|
|
310
|
+
describe("dry-run tracing", () => {
|
|
311
|
+
it("captures sensitive file reads and environment reads for private registry style Docker inputs", () => {
|
|
312
|
+
const fixtureRoot = mkdtempSync(
|
|
313
|
+
join(tmpdir(), "cdxgen-dry-run-registry-"),
|
|
314
|
+
);
|
|
315
|
+
const dockerConfigDir = join(fixtureRoot, "docker-config");
|
|
316
|
+
mkdirSync(dockerConfigDir, { recursive: true });
|
|
317
|
+
writeFileSync(
|
|
318
|
+
join(dockerConfigDir, "config.json"),
|
|
319
|
+
JSON.stringify({
|
|
320
|
+
credHelpers: {
|
|
321
|
+
"docker.io": "osxkeychain",
|
|
322
|
+
},
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
try {
|
|
326
|
+
const output = execFileSync(
|
|
327
|
+
process.execPath,
|
|
328
|
+
[
|
|
329
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
330
|
+
"--dry-run",
|
|
331
|
+
"-t",
|
|
332
|
+
"oci",
|
|
333
|
+
"docker.io/library/alpine:3.20",
|
|
334
|
+
"--no-banner",
|
|
335
|
+
],
|
|
336
|
+
{
|
|
337
|
+
cwd: repoDir,
|
|
338
|
+
encoding: "utf8",
|
|
339
|
+
env: buildMinimalCliEnv({
|
|
340
|
+
DOCKER_CONFIG: dockerConfigDir,
|
|
341
|
+
}),
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
assert.match(output, /cdxgen dry-run activity summary/);
|
|
345
|
+
assert.match(output, /process\.env:DOCKER_CONFIG/);
|
|
346
|
+
} finally {
|
|
347
|
+
rmSync(fixtureRoot, { force: true, recursive: true });
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("supports bom audit in dry-run mode while skipping predictive dependency analysis", () => {
|
|
352
|
+
const result = spawnSync(
|
|
353
|
+
process.execPath,
|
|
354
|
+
[
|
|
355
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
356
|
+
"--dry-run",
|
|
357
|
+
"--bom-audit",
|
|
358
|
+
"--bom-audit-categories",
|
|
359
|
+
"mcp-server",
|
|
360
|
+
"-t",
|
|
361
|
+
"js",
|
|
362
|
+
mcpFixtureDir,
|
|
363
|
+
"--no-banner",
|
|
364
|
+
],
|
|
365
|
+
{
|
|
366
|
+
cwd: repoDir,
|
|
367
|
+
encoding: "utf8",
|
|
368
|
+
env: buildMinimalCliEnv(),
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
assert.strictEqual(result.status, 0);
|
|
372
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
373
|
+
|
|
374
|
+
assert.match(output, /BOM Audit Findings/);
|
|
375
|
+
assert.match(output, /MCP-001/);
|
|
376
|
+
assert.match(
|
|
377
|
+
output,
|
|
378
|
+
/Dry-run mode only planned predictive audit targets/i,
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("enforces CDXGEN_ALLOWED_HOSTS for Dependency-Track submission in secure CLI mode", () => {
|
|
383
|
+
const result = spawnSync(
|
|
384
|
+
process.execPath,
|
|
385
|
+
[
|
|
386
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
387
|
+
"--dry-run",
|
|
388
|
+
"-t",
|
|
389
|
+
"js",
|
|
390
|
+
mcpFixtureDir,
|
|
391
|
+
"--server-url",
|
|
392
|
+
"https://blocked.example.com",
|
|
393
|
+
"--api-key",
|
|
394
|
+
"test-api-key",
|
|
395
|
+
"--no-banner",
|
|
396
|
+
],
|
|
397
|
+
{
|
|
398
|
+
cwd: repoDir,
|
|
399
|
+
encoding: "utf8",
|
|
400
|
+
env: buildMinimalCliEnv({
|
|
401
|
+
CDXGEN_ALLOWED_HOSTS: "allowed.example.com",
|
|
402
|
+
CDXGEN_SECURE_MODE: "true",
|
|
403
|
+
}),
|
|
404
|
+
},
|
|
405
|
+
);
|
|
406
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
407
|
+
|
|
408
|
+
assert.strictEqual(result.status, 1);
|
|
409
|
+
assert.match(
|
|
410
|
+
output,
|
|
411
|
+
/Dependency-Track server host 'blocked\.example\.com' is not allowed/i,
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("protobuf CLI round-trip", () => {
|
|
417
|
+
it("generates, converts, and validates protobuf BOMs for CycloneDX 1.6 and 1.7", () => {
|
|
418
|
+
const fixtureRoot = mkdtempSync(
|
|
419
|
+
join(tmpdir(), "cdxgen-proto-roundtrip-"),
|
|
420
|
+
);
|
|
421
|
+
try {
|
|
422
|
+
for (const specVersion of ["1.6", "1.7"]) {
|
|
423
|
+
const jsonPath = join(fixtureRoot, `bom-${specVersion}.json`);
|
|
424
|
+
const protoPath = join(fixtureRoot, `bom-${specVersion}.cdx`);
|
|
425
|
+
const spdxPath = join(fixtureRoot, `bom-${specVersion}.spdx.json`);
|
|
426
|
+
const generateResult = spawnSync(
|
|
427
|
+
process.execPath,
|
|
428
|
+
[
|
|
429
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
430
|
+
"-t",
|
|
431
|
+
"js",
|
|
432
|
+
mcpFixtureDir,
|
|
433
|
+
"-o",
|
|
434
|
+
jsonPath,
|
|
435
|
+
"--spec-version",
|
|
436
|
+
specVersion,
|
|
437
|
+
"--export-proto",
|
|
438
|
+
"--proto-bin-file",
|
|
439
|
+
protoPath,
|
|
440
|
+
"--no-banner",
|
|
441
|
+
],
|
|
442
|
+
{
|
|
443
|
+
cwd: repoDir,
|
|
444
|
+
encoding: "utf8",
|
|
445
|
+
env: buildMinimalCliEnv(),
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
assert.strictEqual(generateResult.status, 0);
|
|
449
|
+
assert.ok(existsSync(jsonPath));
|
|
450
|
+
assert.ok(existsSync(protoPath));
|
|
451
|
+
|
|
452
|
+
const convertResult = spawnSync(
|
|
453
|
+
process.execPath,
|
|
454
|
+
[
|
|
455
|
+
join(repoDir, "bin", "convert.js"),
|
|
456
|
+
"-i",
|
|
457
|
+
protoPath,
|
|
458
|
+
"-o",
|
|
459
|
+
spdxPath,
|
|
460
|
+
],
|
|
461
|
+
{
|
|
462
|
+
cwd: repoDir,
|
|
463
|
+
encoding: "utf8",
|
|
464
|
+
env: buildMinimalCliEnv(),
|
|
465
|
+
},
|
|
466
|
+
);
|
|
467
|
+
assert.strictEqual(convertResult.status, 0);
|
|
468
|
+
assert.ok(existsSync(spdxPath));
|
|
469
|
+
|
|
470
|
+
const validateResult = spawnSync(
|
|
471
|
+
process.execPath,
|
|
472
|
+
[
|
|
473
|
+
join(repoDir, "bin", "validate.js"),
|
|
474
|
+
"-i",
|
|
475
|
+
protoPath,
|
|
476
|
+
"--fail-severity",
|
|
477
|
+
"critical",
|
|
478
|
+
"--no-deep",
|
|
479
|
+
"--report",
|
|
480
|
+
"json",
|
|
481
|
+
],
|
|
482
|
+
{
|
|
483
|
+
cwd: repoDir,
|
|
484
|
+
encoding: "utf8",
|
|
485
|
+
env: buildMinimalCliEnv(),
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
assert.strictEqual(validateResult.status, 0);
|
|
489
|
+
assert.doesNotMatch(
|
|
490
|
+
`${validateResult.stdout}${validateResult.stderr}`,
|
|
491
|
+
/Failed to parse|non-CycloneDX|Unsupported CycloneDX specVersion/i,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
} finally {
|
|
495
|
+
rmSync(fixtureRoot, { force: true, recursive: true });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("preserves user output directories for research-profile protobuf exports", () => {
|
|
500
|
+
const fixtureRoot = mkdtempSync(
|
|
501
|
+
join(tmpdir(), "cdxgen-proto-research-roundtrip-"),
|
|
502
|
+
);
|
|
503
|
+
try {
|
|
504
|
+
const jsonPath = join(fixtureRoot, "research.json");
|
|
505
|
+
const protoPath = join(fixtureRoot, "research.cdx");
|
|
506
|
+
const generateResult = spawnSync(
|
|
507
|
+
process.execPath,
|
|
508
|
+
[
|
|
509
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
510
|
+
"-t",
|
|
511
|
+
"js",
|
|
512
|
+
"-t",
|
|
513
|
+
"mcp",
|
|
514
|
+
mcpFixtureDir,
|
|
515
|
+
"--profile",
|
|
516
|
+
"research",
|
|
517
|
+
"-o",
|
|
518
|
+
jsonPath,
|
|
519
|
+
"--export-proto",
|
|
520
|
+
"--proto-bin-file",
|
|
521
|
+
protoPath,
|
|
522
|
+
"--no-banner",
|
|
523
|
+
],
|
|
524
|
+
{
|
|
525
|
+
cwd: repoDir,
|
|
526
|
+
encoding: "utf8",
|
|
527
|
+
env: buildMinimalCliEnv(),
|
|
528
|
+
},
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
assert.strictEqual(generateResult.status, 0);
|
|
532
|
+
assert.ok(existsSync(fixtureRoot));
|
|
533
|
+
assert.ok(existsSync(jsonPath));
|
|
534
|
+
assert.ok(existsSync(protoPath));
|
|
535
|
+
|
|
536
|
+
const generatedBom = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
537
|
+
assert.ok((generatedBom.services || []).length >= 1);
|
|
538
|
+
|
|
539
|
+
const roundTrippedBom = readBinary(protoPath);
|
|
540
|
+
assert.ok(roundTrippedBom);
|
|
541
|
+
assert.ok((roundTrippedBom.formulation || []).length >= 1);
|
|
542
|
+
assert.ok((roundTrippedBom.services || []).length >= 1);
|
|
543
|
+
} finally {
|
|
544
|
+
rmSync(fixtureRoot, { force: true, recursive: true });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("exports standards-enabled BOMs to protobuf using canonical definitions objects", () => {
|
|
549
|
+
const fixtureRoot = mkdtempSync(
|
|
550
|
+
join(tmpdir(), "cdxgen-proto-standards-roundtrip-"),
|
|
551
|
+
);
|
|
552
|
+
try {
|
|
553
|
+
const jsonPath = join(fixtureRoot, "standards.json");
|
|
554
|
+
const protoPath = join(fixtureRoot, "standards.cdx");
|
|
555
|
+
const generateResult = spawnSync(
|
|
556
|
+
process.execPath,
|
|
557
|
+
[
|
|
558
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
559
|
+
"-t",
|
|
560
|
+
"js",
|
|
561
|
+
mcpFixtureDir,
|
|
562
|
+
"--standard",
|
|
563
|
+
"asvs-5.0",
|
|
564
|
+
"-o",
|
|
565
|
+
jsonPath,
|
|
566
|
+
"--export-proto",
|
|
567
|
+
"--proto-bin-file",
|
|
568
|
+
protoPath,
|
|
569
|
+
"--no-banner",
|
|
570
|
+
],
|
|
571
|
+
{
|
|
572
|
+
cwd: repoDir,
|
|
573
|
+
encoding: "utf8",
|
|
574
|
+
env: buildMinimalCliEnv(),
|
|
575
|
+
},
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
assert.strictEqual(generateResult.status, 0);
|
|
579
|
+
assert.ok(existsSync(jsonPath));
|
|
580
|
+
assert.ok(existsSync(protoPath));
|
|
581
|
+
|
|
582
|
+
const roundTrippedBom = readBinary(protoPath);
|
|
583
|
+
assert.ok(roundTrippedBom);
|
|
584
|
+
assert.equal(Array.isArray(roundTrippedBom.definitions), false);
|
|
585
|
+
assert.ok((roundTrippedBom.definitions?.standards || []).length >= 1);
|
|
586
|
+
} finally {
|
|
587
|
+
rmSync(fixtureRoot, { force: true, recursive: true });
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("round-trips research, standards, and CBOM protobuf exports with canonical JSON", () => {
|
|
592
|
+
const fixtureRoot = mkdtempSync(
|
|
593
|
+
join(tmpdir(), "cdxgen-proto-mode-roundtrip-"),
|
|
594
|
+
);
|
|
595
|
+
const scenarios = [
|
|
596
|
+
{
|
|
597
|
+
args: [
|
|
598
|
+
"-t",
|
|
599
|
+
"js",
|
|
600
|
+
"-t",
|
|
601
|
+
"mcp",
|
|
602
|
+
mcpFixtureDir,
|
|
603
|
+
"--profile",
|
|
604
|
+
"research",
|
|
605
|
+
],
|
|
606
|
+
assertRoundTrip: (bomJson) => {
|
|
607
|
+
assert.ok((bomJson.formulation || []).length >= 1);
|
|
608
|
+
},
|
|
609
|
+
expectedSpecVersion: (specVersion) => specVersion,
|
|
610
|
+
name: "research",
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
args: [cbomFixtureDir, "--include-crypto", "--evidence", "--deep"],
|
|
614
|
+
assertRoundTrip: (bomJson) => {
|
|
615
|
+
const cryptoComponents = (bomJson.components || []).filter(
|
|
616
|
+
(component) => component.type === "cryptographic-asset",
|
|
617
|
+
);
|
|
618
|
+
assert.ok(cryptoComponents.length >= 3);
|
|
619
|
+
assert.equal(
|
|
620
|
+
cryptoComponents.some(
|
|
621
|
+
(component) => component.purl !== undefined,
|
|
622
|
+
),
|
|
623
|
+
false,
|
|
624
|
+
);
|
|
625
|
+
},
|
|
626
|
+
isolateDepsSlicesFile: true,
|
|
627
|
+
expectedSpecVersion: (specVersion) => specVersion,
|
|
628
|
+
name: "cbom",
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
args: ["-t", "js", mcpFixtureDir, "--standard", "asvs-5.0"],
|
|
632
|
+
assertRoundTrip: (bomJson) => {
|
|
633
|
+
assert.equal(Array.isArray(bomJson.definitions), false);
|
|
634
|
+
assert.ok((bomJson.definitions?.standards || []).length >= 1);
|
|
635
|
+
},
|
|
636
|
+
expectedSpecVersion: () => "1.7",
|
|
637
|
+
name: "standards",
|
|
638
|
+
},
|
|
639
|
+
];
|
|
640
|
+
try {
|
|
641
|
+
for (const scenario of scenarios) {
|
|
642
|
+
for (const specVersion of ["1.6", "1.7"]) {
|
|
643
|
+
const jsonPath = join(
|
|
644
|
+
fixtureRoot,
|
|
645
|
+
`${scenario.name}-${specVersion}.json`,
|
|
646
|
+
);
|
|
647
|
+
const protoPath = join(
|
|
648
|
+
fixtureRoot,
|
|
649
|
+
`${scenario.name}-${specVersion}.cdx`,
|
|
650
|
+
);
|
|
651
|
+
const spdxPath = join(
|
|
652
|
+
fixtureRoot,
|
|
653
|
+
`${scenario.name}-${specVersion}.spdx.json`,
|
|
654
|
+
);
|
|
655
|
+
const depsSlicesPath = join(
|
|
656
|
+
fixtureRoot,
|
|
657
|
+
`${scenario.name}-${specVersion}.deps.slices.json`,
|
|
658
|
+
);
|
|
659
|
+
const depsSlicesArgs = scenario.isolateDepsSlicesFile
|
|
660
|
+
? ["--deps-slices-file", depsSlicesPath]
|
|
661
|
+
: [];
|
|
662
|
+
const generateResult = spawnSync(
|
|
663
|
+
process.execPath,
|
|
664
|
+
[
|
|
665
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
666
|
+
...scenario.args,
|
|
667
|
+
"-o",
|
|
668
|
+
jsonPath,
|
|
669
|
+
"--spec-version",
|
|
670
|
+
specVersion,
|
|
671
|
+
...depsSlicesArgs,
|
|
672
|
+
"--export-proto",
|
|
673
|
+
"--proto-bin-file",
|
|
674
|
+
protoPath,
|
|
675
|
+
"--no-banner",
|
|
676
|
+
],
|
|
677
|
+
{
|
|
678
|
+
cwd: repoDir,
|
|
679
|
+
encoding: "utf8",
|
|
680
|
+
env: buildMinimalCliEnv(),
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
assert.strictEqual(
|
|
684
|
+
generateResult.status,
|
|
685
|
+
0,
|
|
686
|
+
`${scenario.name} ${specVersion}: ${generateResult.stdout}${generateResult.stderr}`,
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
const generatedBom = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
690
|
+
assert.strictEqual(
|
|
691
|
+
generatedBom.specVersion,
|
|
692
|
+
scenario.expectedSpecVersion(specVersion),
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
const convertResult = spawnSync(
|
|
696
|
+
process.execPath,
|
|
697
|
+
[
|
|
698
|
+
join(repoDir, "bin", "convert.js"),
|
|
699
|
+
"-i",
|
|
700
|
+
protoPath,
|
|
701
|
+
"-o",
|
|
702
|
+
spdxPath,
|
|
703
|
+
],
|
|
704
|
+
{
|
|
705
|
+
cwd: repoDir,
|
|
706
|
+
encoding: "utf8",
|
|
707
|
+
env: buildMinimalCliEnv(),
|
|
708
|
+
},
|
|
709
|
+
);
|
|
710
|
+
assert.strictEqual(
|
|
711
|
+
convertResult.status,
|
|
712
|
+
0,
|
|
713
|
+
`${scenario.name} ${specVersion}: ${convertResult.stdout}${convertResult.stderr}`,
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
const validateResult = spawnSync(
|
|
717
|
+
process.execPath,
|
|
718
|
+
[
|
|
719
|
+
join(repoDir, "bin", "validate.js"),
|
|
720
|
+
"-i",
|
|
721
|
+
protoPath,
|
|
722
|
+
"--fail-severity",
|
|
723
|
+
"critical",
|
|
724
|
+
"--no-deep",
|
|
725
|
+
"--report",
|
|
726
|
+
"json",
|
|
727
|
+
],
|
|
728
|
+
{
|
|
729
|
+
cwd: repoDir,
|
|
730
|
+
encoding: "utf8",
|
|
731
|
+
env: buildMinimalCliEnv(),
|
|
732
|
+
},
|
|
733
|
+
);
|
|
734
|
+
assert.strictEqual(
|
|
735
|
+
validateResult.status,
|
|
736
|
+
0,
|
|
737
|
+
`${scenario.name} ${specVersion}: ${validateResult.stdout}${validateResult.stderr}`,
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
const roundTrippedBom = readBinary(protoPath);
|
|
741
|
+
assert.ok(roundTrippedBom);
|
|
742
|
+
assert.strictEqual(roundTrippedBom.bomFormat, "CycloneDX");
|
|
743
|
+
assert.strictEqual(
|
|
744
|
+
roundTrippedBom.specVersion,
|
|
745
|
+
scenario.expectedSpecVersion(specVersion),
|
|
746
|
+
);
|
|
747
|
+
scenario.assertRoundTrip(roundTrippedBom);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
assert.strictEqual(
|
|
751
|
+
existsSync(join(repoDir, "deps.slices.json")),
|
|
752
|
+
false,
|
|
753
|
+
"protobuf round-trip tests must not leave deps.slices.json in the repository root",
|
|
754
|
+
);
|
|
755
|
+
} finally {
|
|
756
|
+
rmSync(join(repoDir, "deps.slices.json"), { force: true });
|
|
757
|
+
rmSync(fixtureRoot, { force: true, recursive: true });
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
describe("CycloneDX 2.0 JSON output", () => {
|
|
763
|
+
it("generates valid experimental 2.0-dev JSON with specFormat", () => {
|
|
764
|
+
const fixtureRoot = mkdtempSync(join(tmpdir(), "cdxgen-json20-"));
|
|
765
|
+
try {
|
|
766
|
+
const jsonPath = join(fixtureRoot, "bom-2.0.json");
|
|
767
|
+
const generateResult = spawnSync(
|
|
768
|
+
process.execPath,
|
|
769
|
+
[
|
|
770
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
771
|
+
"-t",
|
|
772
|
+
"js",
|
|
773
|
+
mcpFixtureDir,
|
|
774
|
+
"-o",
|
|
775
|
+
jsonPath,
|
|
776
|
+
"--spec-version",
|
|
777
|
+
"2.0",
|
|
778
|
+
"--no-banner",
|
|
779
|
+
],
|
|
780
|
+
{
|
|
781
|
+
cwd: repoDir,
|
|
782
|
+
encoding: "utf8",
|
|
783
|
+
env: buildMinimalCliEnv(),
|
|
784
|
+
},
|
|
785
|
+
);
|
|
786
|
+
assert.strictEqual(
|
|
787
|
+
generateResult.status,
|
|
788
|
+
0,
|
|
789
|
+
`${generateResult.stdout}${generateResult.stderr}`,
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const generatedBom = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
793
|
+
assert.strictEqual(generatedBom.specFormat, "CycloneDX");
|
|
794
|
+
assert.strictEqual(generatedBom.bomFormat, undefined);
|
|
795
|
+
assert.strictEqual(generatedBom.specVersion, "2.0");
|
|
796
|
+
assert.ok(Array.isArray(generatedBom.metadata?.tools?.components));
|
|
797
|
+
|
|
798
|
+
const validateResult = spawnSync(
|
|
799
|
+
process.execPath,
|
|
800
|
+
[
|
|
801
|
+
join(repoDir, "bin", "validate.js"),
|
|
802
|
+
"-i",
|
|
803
|
+
jsonPath,
|
|
804
|
+
"--fail-severity",
|
|
805
|
+
"critical",
|
|
806
|
+
"--no-deep",
|
|
807
|
+
"--report",
|
|
808
|
+
"json",
|
|
809
|
+
],
|
|
810
|
+
{
|
|
811
|
+
cwd: repoDir,
|
|
812
|
+
encoding: "utf8",
|
|
813
|
+
env: buildMinimalCliEnv(),
|
|
814
|
+
},
|
|
815
|
+
);
|
|
816
|
+
assert.strictEqual(
|
|
817
|
+
validateResult.status,
|
|
818
|
+
0,
|
|
819
|
+
`${validateResult.stdout}${validateResult.stderr}`,
|
|
820
|
+
);
|
|
821
|
+
} finally {
|
|
822
|
+
rmSync(fixtureRoot, { force: true, recursive: true });
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it("rejects experimental 2.0-dev protobuf export until cdx-proto supports it", () => {
|
|
827
|
+
const fixtureRoot = mkdtempSync(join(tmpdir(), "cdxgen-proto20-"));
|
|
828
|
+
try {
|
|
829
|
+
const jsonPath = join(fixtureRoot, "bom-2.0.json");
|
|
830
|
+
const protoPath = join(fixtureRoot, "bom-2.0.cdx");
|
|
831
|
+
const generateResult = spawnSync(
|
|
832
|
+
process.execPath,
|
|
833
|
+
[
|
|
834
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
835
|
+
"-t",
|
|
836
|
+
"js",
|
|
837
|
+
mcpFixtureDir,
|
|
838
|
+
"-o",
|
|
839
|
+
jsonPath,
|
|
840
|
+
"--spec-version",
|
|
841
|
+
"2.0",
|
|
842
|
+
"--export-proto",
|
|
843
|
+
"--proto-bin-file",
|
|
844
|
+
protoPath,
|
|
845
|
+
"--no-banner",
|
|
846
|
+
],
|
|
847
|
+
{
|
|
848
|
+
cwd: repoDir,
|
|
849
|
+
encoding: "utf8",
|
|
850
|
+
env: buildMinimalCliEnv(),
|
|
851
|
+
},
|
|
852
|
+
);
|
|
853
|
+
assert.strictEqual(generateResult.status, 1);
|
|
854
|
+
assert.match(
|
|
855
|
+
`${generateResult.stdout}${generateResult.stderr}`,
|
|
856
|
+
/CycloneDX 2\.0 is not currently supported for protobuf export/i,
|
|
857
|
+
);
|
|
858
|
+
} finally {
|
|
859
|
+
rmSync(fixtureRoot, { force: true, recursive: true });
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
172
864
|
describe("submitBom()", () => {
|
|
173
865
|
it("should report blocked Dependency-Track submission during dry-run", async () => {
|
|
174
866
|
const recordActivity = sinon.stub();
|
|
@@ -201,18 +893,11 @@ describe("CLI tests", () => {
|
|
|
201
893
|
});
|
|
202
894
|
|
|
203
895
|
it("should successfully report the SBOM with given project id, name, version and a single tag", async () => {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
209
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
896
|
+
const server = await startSubmitBomTestServer(async () => ({
|
|
897
|
+
body: { success: true },
|
|
898
|
+
}));
|
|
210
899
|
|
|
211
|
-
const
|
|
212
|
-
got: { default: gotStub },
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const serverUrl = "https://dtrack.example.com";
|
|
900
|
+
const serverUrl = server.serverUrl;
|
|
216
901
|
const projectId = "f7cb9f02-8041-4991-9101-b01fa07a6522";
|
|
217
902
|
const projectName = "cdxgen-test-project";
|
|
218
903
|
const projectVersion = "1.0.0";
|
|
@@ -230,47 +915,44 @@ describe("CLI tests", () => {
|
|
|
230
915
|
projectTags: [{ name: projectTag }],
|
|
231
916
|
};
|
|
232
917
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// Verify got was called exactly once
|
|
247
|
-
sinon.assert.calledOnce(gotStub);
|
|
248
|
-
|
|
249
|
-
// Grab call arguments
|
|
250
|
-
const [calledUrl, options] = gotStub.firstCall.args;
|
|
918
|
+
try {
|
|
919
|
+
const response = await submitBom(
|
|
920
|
+
{
|
|
921
|
+
serverUrl,
|
|
922
|
+
projectId,
|
|
923
|
+
projectName,
|
|
924
|
+
projectVersion,
|
|
925
|
+
apiKey,
|
|
926
|
+
skipDtTlsCheck,
|
|
927
|
+
projectTag,
|
|
928
|
+
},
|
|
929
|
+
bomContent,
|
|
930
|
+
);
|
|
251
931
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
932
|
+
assert.deepEqual(response, { success: true });
|
|
933
|
+
assert.equal(server.requests.length, 1);
|
|
934
|
+
assert.equal(server.requests[0].method, "PUT");
|
|
935
|
+
assert.equal(server.requests[0].url, "/api/v1/bom");
|
|
936
|
+
assert.equal(getRequestHeader(server.requests[0], "x-api-key"), apiKey);
|
|
937
|
+
assert.equal(
|
|
938
|
+
getRequestHeader(server.requests[0], "content-type"),
|
|
939
|
+
"application/json",
|
|
940
|
+
);
|
|
941
|
+
assert.deepEqual(
|
|
942
|
+
JSON.parse(server.requests[0].body),
|
|
943
|
+
expectedRequestPayload,
|
|
944
|
+
);
|
|
945
|
+
} finally {
|
|
946
|
+
await server.close();
|
|
947
|
+
}
|
|
259
948
|
});
|
|
260
949
|
|
|
261
950
|
it("should successfully report the SBOM with given parent project, name, version and multiple tags", async () => {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
267
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
268
|
-
|
|
269
|
-
const { submitBom } = await esmock("./index.js", {
|
|
270
|
-
got: { default: gotStub },
|
|
271
|
-
});
|
|
951
|
+
const server = await startSubmitBomTestServer(async () => ({
|
|
952
|
+
body: { success: true },
|
|
953
|
+
}));
|
|
272
954
|
|
|
273
|
-
const serverUrl =
|
|
955
|
+
const serverUrl = server.serverUrl;
|
|
274
956
|
const projectName = "cdxgen-test-project";
|
|
275
957
|
const projectVersion = "1.1.0";
|
|
276
958
|
const projectTags = ["tag1", "tag2"];
|
|
@@ -290,48 +972,44 @@ describe("CLI tests", () => {
|
|
|
290
972
|
projectTags: [{ name: projectTags[0] }, { name: projectTags[1] }],
|
|
291
973
|
};
|
|
292
974
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// Verify got was called exactly once
|
|
307
|
-
sinon.assert.calledOnce(gotStub);
|
|
308
|
-
|
|
309
|
-
// Grab call arguments
|
|
310
|
-
const [calledUrl, options] = gotStub.firstCall.args;
|
|
975
|
+
try {
|
|
976
|
+
const response = await submitBom(
|
|
977
|
+
{
|
|
978
|
+
serverUrl,
|
|
979
|
+
parentProjectId,
|
|
980
|
+
projectName,
|
|
981
|
+
projectVersion,
|
|
982
|
+
apiKey,
|
|
983
|
+
skipDtTlsCheck,
|
|
984
|
+
projectTag: projectTags,
|
|
985
|
+
},
|
|
986
|
+
bomContent,
|
|
987
|
+
);
|
|
311
988
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
989
|
+
assert.deepEqual(response, { success: true });
|
|
990
|
+
assert.equal(server.requests.length, 1);
|
|
991
|
+
assert.equal(server.requests[0].method, "PUT");
|
|
992
|
+
assert.equal(server.requests[0].url, "/api/v1/bom");
|
|
993
|
+
assert.equal(getRequestHeader(server.requests[0], "x-api-key"), apiKey);
|
|
994
|
+
assert.equal(
|
|
995
|
+
getRequestHeader(server.requests[0], "content-type"),
|
|
996
|
+
"application/json",
|
|
997
|
+
);
|
|
998
|
+
assert.deepEqual(
|
|
999
|
+
JSON.parse(server.requests[0].body),
|
|
1000
|
+
expectedRequestPayload,
|
|
1001
|
+
);
|
|
1002
|
+
} finally {
|
|
1003
|
+
await server.close();
|
|
1004
|
+
}
|
|
320
1005
|
});
|
|
321
1006
|
|
|
322
1007
|
it("should include parentName and parentVersion when parent project name and version are passed", async () => {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
328
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
329
|
-
|
|
330
|
-
const { submitBom } = await esmock("./index.js", {
|
|
331
|
-
got: { default: gotStub },
|
|
332
|
-
});
|
|
1008
|
+
const server = await startSubmitBomTestServer(async () => ({
|
|
1009
|
+
body: { success: true },
|
|
1010
|
+
}));
|
|
333
1011
|
|
|
334
|
-
const serverUrl =
|
|
1012
|
+
const serverUrl = server.serverUrl;
|
|
335
1013
|
const projectName = "cdxgen-test-project";
|
|
336
1014
|
const projectVersion = "2.0.0";
|
|
337
1015
|
const parentProjectName = "parent-project";
|
|
@@ -351,77 +1029,71 @@ describe("CLI tests", () => {
|
|
|
351
1029
|
projectVersion,
|
|
352
1030
|
};
|
|
353
1031
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
sinon.assert.calledOnce(gotStub);
|
|
368
|
-
const [calledUrl, options] = gotStub.firstCall.args;
|
|
1032
|
+
try {
|
|
1033
|
+
const response = await submitBom(
|
|
1034
|
+
{
|
|
1035
|
+
serverUrl,
|
|
1036
|
+
projectName,
|
|
1037
|
+
projectVersion,
|
|
1038
|
+
parentProjectName,
|
|
1039
|
+
parentProjectVersion,
|
|
1040
|
+
apiKey,
|
|
1041
|
+
skipDtTlsCheck,
|
|
1042
|
+
},
|
|
1043
|
+
bomContent,
|
|
1044
|
+
);
|
|
369
1045
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1046
|
+
assert.deepEqual(response, { success: true });
|
|
1047
|
+
assert.equal(server.requests.length, 1);
|
|
1048
|
+
assert.equal(server.requests[0].method, "PUT");
|
|
1049
|
+
assert.equal(server.requests[0].url, "/api/v1/bom");
|
|
1050
|
+
assert.equal(getRequestHeader(server.requests[0], "x-api-key"), apiKey);
|
|
1051
|
+
assert.equal(
|
|
1052
|
+
getRequestHeader(server.requests[0], "content-type"),
|
|
1053
|
+
"application/json",
|
|
1054
|
+
);
|
|
1055
|
+
assert.deepEqual(
|
|
1056
|
+
JSON.parse(server.requests[0].body),
|
|
1057
|
+
expectedRequestPayload,
|
|
1058
|
+
);
|
|
1059
|
+
} finally {
|
|
1060
|
+
await server.close();
|
|
1061
|
+
}
|
|
377
1062
|
});
|
|
378
1063
|
|
|
379
1064
|
it("should include configurable autoCreate and isLatest values in payload", async () => {
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
385
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
386
|
-
|
|
387
|
-
const { submitBom } = await esmock("./index.js", {
|
|
388
|
-
got: { default: gotStub },
|
|
389
|
-
});
|
|
1065
|
+
const server = await startSubmitBomTestServer(async () => ({
|
|
1066
|
+
body: { success: true },
|
|
1067
|
+
}));
|
|
390
1068
|
|
|
391
|
-
const serverUrl =
|
|
1069
|
+
const serverUrl = server.serverUrl;
|
|
392
1070
|
const projectName = "cdxgen-test-project";
|
|
393
1071
|
const apiKey = "TEST_API_KEY";
|
|
394
1072
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
1073
|
+
try {
|
|
1074
|
+
const response = await submitBom(
|
|
1075
|
+
{
|
|
1076
|
+
serverUrl,
|
|
1077
|
+
projectName,
|
|
1078
|
+
apiKey,
|
|
1079
|
+
autoCreate: false,
|
|
1080
|
+
isLatest: true,
|
|
1081
|
+
},
|
|
1082
|
+
{ bom: "test4" },
|
|
1083
|
+
);
|
|
405
1084
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1085
|
+
assert.deepEqual(response, { success: true });
|
|
1086
|
+
assert.equal(server.requests.length, 1);
|
|
1087
|
+
const payload = JSON.parse(server.requests[0].body);
|
|
1088
|
+
assert.equal(payload.autoCreate, "false");
|
|
1089
|
+
assert.equal(payload.isLatest, true);
|
|
1090
|
+
assert.equal(payload.projectVersion, "main");
|
|
1091
|
+
} finally {
|
|
1092
|
+
await server.close();
|
|
1093
|
+
}
|
|
411
1094
|
});
|
|
412
1095
|
|
|
413
1096
|
it("should reject invalid mixed parent modes before making network request", async () => {
|
|
414
|
-
const fakeGotResponse = {
|
|
415
|
-
json: sinon.stub().resolves({ success: true }),
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
419
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
420
|
-
|
|
421
|
-
const { submitBom } = await esmock("./index.js", {
|
|
422
|
-
got: { default: gotStub },
|
|
423
|
-
});
|
|
424
|
-
|
|
425
1097
|
const response = await submitBom(
|
|
426
1098
|
{
|
|
427
1099
|
serverUrl: "https://dtrack.example.com",
|
|
@@ -434,42 +1106,57 @@ describe("CLI tests", () => {
|
|
|
434
1106
|
);
|
|
435
1107
|
|
|
436
1108
|
assert.equal(response, undefined);
|
|
437
|
-
sinon.assert.notCalled(gotStub);
|
|
438
1109
|
});
|
|
439
1110
|
|
|
440
|
-
it("
|
|
441
|
-
const
|
|
442
|
-
putError.response = { statusCode: 405 };
|
|
443
|
-
const gotStub = sinon.stub();
|
|
444
|
-
gotStub
|
|
445
|
-
.onFirstCall()
|
|
446
|
-
.returns({
|
|
447
|
-
json: sinon.stub().rejects(putError),
|
|
448
|
-
})
|
|
449
|
-
.onSecondCall()
|
|
450
|
-
.returns({
|
|
451
|
-
json: sinon.stub().resolves({ success: true }),
|
|
452
|
-
});
|
|
453
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
454
|
-
|
|
455
|
-
const { submitBom } = await esmock("./index.js", {
|
|
456
|
-
got: { default: gotStub },
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
await submitBom(
|
|
1111
|
+
it("rejects malformed Dependency-Track URLs before making a request", async () => {
|
|
1112
|
+
const response = await submitBom(
|
|
460
1113
|
{
|
|
461
|
-
serverUrl: "
|
|
1114
|
+
serverUrl: "file:///tmp/dtrack",
|
|
462
1115
|
projectName: "cdxgen-test-project",
|
|
463
|
-
apiKey: "TEST_API_KEY
|
|
1116
|
+
apiKey: "TEST_API_KEY",
|
|
464
1117
|
},
|
|
465
|
-
{ bom: "
|
|
1118
|
+
{ bom: "test-invalid-url" },
|
|
466
1119
|
);
|
|
467
1120
|
|
|
468
|
-
assert.equal(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
1121
|
+
assert.equal(response, undefined);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it("disables redirects for the POST fallback request too", async () => {
|
|
1125
|
+
const server = await startSubmitBomTestServer(
|
|
1126
|
+
async (_request, requestCount) => {
|
|
1127
|
+
if (requestCount === 1) {
|
|
1128
|
+
return { body: { error: "Method not allowed" }, statusCode: 405 };
|
|
1129
|
+
}
|
|
1130
|
+
return { body: { success: true }, statusCode: 200 };
|
|
1131
|
+
},
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
try {
|
|
1135
|
+
const response = await submitBom(
|
|
1136
|
+
{
|
|
1137
|
+
serverUrl: server.serverUrl,
|
|
1138
|
+
projectName: "cdxgen-test-project",
|
|
1139
|
+
apiKey: "TEST_API_KEY\r\n",
|
|
1140
|
+
},
|
|
1141
|
+
{ bom: "test6" },
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
assert.deepEqual(response, { success: true });
|
|
1145
|
+
assert.equal(server.requests.length, 2);
|
|
1146
|
+
assert.equal(server.requests[0].method, "PUT");
|
|
1147
|
+
assert.equal(server.requests[1].method, "POST");
|
|
1148
|
+
assert.equal(server.requests[1].url, "/api/v1/bom");
|
|
1149
|
+
assert.equal(
|
|
1150
|
+
getRequestHeader(server.requests[1], "x-api-key"),
|
|
1151
|
+
"TEST_API_KEY",
|
|
1152
|
+
);
|
|
1153
|
+
assert.equal(
|
|
1154
|
+
getRequestHeader(server.requests[1], "content-type"),
|
|
1155
|
+
"application/json",
|
|
1156
|
+
);
|
|
1157
|
+
} finally {
|
|
1158
|
+
await server.close();
|
|
1159
|
+
}
|
|
473
1160
|
});
|
|
474
1161
|
});
|
|
475
1162
|
|
|
@@ -654,6 +1341,136 @@ describe("CLI tests", () => {
|
|
|
654
1341
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
655
1342
|
}
|
|
656
1343
|
});
|
|
1344
|
+
|
|
1345
|
+
it("should not scan installed browser locations without explicit extension project type", async () => {
|
|
1346
|
+
const discoverChromiumExtensionDirs = sinon.stub().returns([
|
|
1347
|
+
{
|
|
1348
|
+
browser: "Google Chrome",
|
|
1349
|
+
channel: "stable",
|
|
1350
|
+
dir: join(tmpdir(), "fake-browser-dir"),
|
|
1351
|
+
},
|
|
1352
|
+
]);
|
|
1353
|
+
const collectInstalledChromeExtensions = sinon.stub().returns([
|
|
1354
|
+
{
|
|
1355
|
+
type: "application",
|
|
1356
|
+
name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
1357
|
+
version: "1.0.0",
|
|
1358
|
+
purl: "pkg:chrome-extension/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@1.0.0",
|
|
1359
|
+
"bom-ref":
|
|
1360
|
+
"pkg:chrome-extension/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@1.0.0",
|
|
1361
|
+
},
|
|
1362
|
+
]);
|
|
1363
|
+
const { createChromeExtensionBom: createChromeExtensionBomMocked } =
|
|
1364
|
+
await esmock("./index.js", {
|
|
1365
|
+
"../helpers/chromextutils.js": {
|
|
1366
|
+
CHROME_EXTENSION_PURL_TYPE: "chrome-extension",
|
|
1367
|
+
collectChromeExtensionsFromPath: sinon
|
|
1368
|
+
.stub()
|
|
1369
|
+
.returns({ components: [], extensionDirs: [] }),
|
|
1370
|
+
collectInstalledChromeExtensions,
|
|
1371
|
+
discoverChromiumExtensionDirs,
|
|
1372
|
+
},
|
|
1373
|
+
});
|
|
1374
|
+
const bomData = await createChromeExtensionBomMocked(
|
|
1375
|
+
join(tmpdir(), "generic-project"),
|
|
1376
|
+
{
|
|
1377
|
+
deep: true,
|
|
1378
|
+
multiProject: false,
|
|
1379
|
+
projectType: ["js"],
|
|
1380
|
+
},
|
|
1381
|
+
);
|
|
1382
|
+
assert.deepStrictEqual(bomData?.bomJson?.components || [], []);
|
|
1383
|
+
sinon.assert.notCalled(discoverChromiumExtensionDirs);
|
|
1384
|
+
sinon.assert.notCalled(collectInstalledChromeExtensions);
|
|
1385
|
+
});
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
describe("createVscodeExtensionBom()", () => {
|
|
1389
|
+
it("should not scan installed IDE locations without explicit extension project type", async () => {
|
|
1390
|
+
const discoverIdeExtensionDirs = sinon.stub().returns([
|
|
1391
|
+
{
|
|
1392
|
+
name: "VS Code",
|
|
1393
|
+
dir: join(tmpdir(), "fake-ide-dir"),
|
|
1394
|
+
},
|
|
1395
|
+
]);
|
|
1396
|
+
const collectInstalledExtensions = sinon.stub().returns([
|
|
1397
|
+
{
|
|
1398
|
+
type: "application",
|
|
1399
|
+
name: "sample.publisher",
|
|
1400
|
+
version: "1.0.0",
|
|
1401
|
+
purl: "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1402
|
+
"bom-ref": "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1403
|
+
},
|
|
1404
|
+
]);
|
|
1405
|
+
const { createVscodeExtensionBom: createVscodeExtensionBomMocked } =
|
|
1406
|
+
await esmock("./index.js", {
|
|
1407
|
+
"../helpers/vsixutils.js": {
|
|
1408
|
+
cleanupTempDir: sinon.stub(),
|
|
1409
|
+
collectInstalledExtensions,
|
|
1410
|
+
discoverIdeExtensionDirs,
|
|
1411
|
+
extractVsixToTempDir: sinon.stub(),
|
|
1412
|
+
parseVsixFile: sinon.stub(),
|
|
1413
|
+
VSCODE_EXTENSION_PURL_TYPE: "vscode-extension",
|
|
1414
|
+
},
|
|
1415
|
+
});
|
|
1416
|
+
const bomData = await createVscodeExtensionBomMocked(
|
|
1417
|
+
join(tmpdir(), "generic-project"),
|
|
1418
|
+
{
|
|
1419
|
+
deep: true,
|
|
1420
|
+
multiProject: false,
|
|
1421
|
+
projectType: ["js"],
|
|
1422
|
+
},
|
|
1423
|
+
);
|
|
1424
|
+
assert.deepStrictEqual(bomData?.bomJson?.components || [], []);
|
|
1425
|
+
sinon.assert.notCalled(discoverIdeExtensionDirs);
|
|
1426
|
+
sinon.assert.notCalled(collectInstalledExtensions);
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
it("should scan installed IDE locations when explicitly requested", async () => {
|
|
1430
|
+
const discoverIdeExtensionDirs = sinon.stub().returns([
|
|
1431
|
+
{
|
|
1432
|
+
name: "VS Code",
|
|
1433
|
+
dir: join(tmpdir(), "fake-ide-dir"),
|
|
1434
|
+
},
|
|
1435
|
+
]);
|
|
1436
|
+
const collectInstalledExtensions = sinon.stub().returns([
|
|
1437
|
+
{
|
|
1438
|
+
type: "application",
|
|
1439
|
+
name: "sample.publisher",
|
|
1440
|
+
version: "1.0.0",
|
|
1441
|
+
purl: "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1442
|
+
"bom-ref": "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1443
|
+
},
|
|
1444
|
+
]);
|
|
1445
|
+
const { createVscodeExtensionBom: createVscodeExtensionBomMocked } =
|
|
1446
|
+
await esmock("./index.js", {
|
|
1447
|
+
"../helpers/vsixutils.js": {
|
|
1448
|
+
cleanupTempDir: sinon.stub(),
|
|
1449
|
+
collectInstalledExtensions,
|
|
1450
|
+
discoverIdeExtensionDirs,
|
|
1451
|
+
extractVsixToTempDir: sinon.stub(),
|
|
1452
|
+
parseVsixFile: sinon.stub(),
|
|
1453
|
+
VSCODE_EXTENSION_PURL_TYPE: "vscode-extension",
|
|
1454
|
+
},
|
|
1455
|
+
});
|
|
1456
|
+
const bomData = await createVscodeExtensionBomMocked(
|
|
1457
|
+
join(tmpdir(), "generic-project"),
|
|
1458
|
+
{
|
|
1459
|
+
deep: true,
|
|
1460
|
+
multiProject: false,
|
|
1461
|
+
projectType: ["ide-extension"],
|
|
1462
|
+
},
|
|
1463
|
+
);
|
|
1464
|
+
const components = bomData?.bomJson?.components || [];
|
|
1465
|
+
assert.ok(
|
|
1466
|
+
components.some(
|
|
1467
|
+
(component) =>
|
|
1468
|
+
component.purl === "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1469
|
+
),
|
|
1470
|
+
);
|
|
1471
|
+
sinon.assert.calledOnce(discoverIdeExtensionDirs);
|
|
1472
|
+
sinon.assert.calledOnce(collectInstalledExtensions);
|
|
1473
|
+
});
|
|
657
1474
|
});
|
|
658
1475
|
|
|
659
1476
|
describe("createMultiXBom()", () => {
|
|
@@ -1088,6 +1905,291 @@ checksum = "${"a".repeat(64)}"
|
|
|
1088
1905
|
});
|
|
1089
1906
|
});
|
|
1090
1907
|
|
|
1908
|
+
if (process.platform !== "win32") {
|
|
1909
|
+
describe("HBOM support", () => {
|
|
1910
|
+
it("delegates hbom project types to the hbom helper", async () => {
|
|
1911
|
+
const actualHbomHelpers = await import("../helpers/hbom.js");
|
|
1912
|
+
const createHbomDocument = sinon.stub().resolves({
|
|
1913
|
+
bomFormat: "CycloneDX",
|
|
1914
|
+
components: [],
|
|
1915
|
+
metadata: {
|
|
1916
|
+
component: {
|
|
1917
|
+
name: "Demo Board",
|
|
1918
|
+
type: "device",
|
|
1919
|
+
version: "rev-a",
|
|
1920
|
+
},
|
|
1921
|
+
},
|
|
1922
|
+
specVersion: "1.7",
|
|
1923
|
+
});
|
|
1924
|
+
const { createBom: createBomMocked } = await esmock("./index.js", {
|
|
1925
|
+
"../helpers/hbom.js": {
|
|
1926
|
+
...actualHbomHelpers,
|
|
1927
|
+
createHbomDocument,
|
|
1928
|
+
},
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
const bomNSData = await createBomMocked(repoDir, {
|
|
1932
|
+
projectType: ["hbom"],
|
|
1933
|
+
specVersion: 1.7,
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
sinon.assert.calledOnce(createHbomDocument);
|
|
1937
|
+
assert.strictEqual(
|
|
1938
|
+
bomNSData?.bomJson?.metadata?.component?.name,
|
|
1939
|
+
"Demo Board",
|
|
1940
|
+
);
|
|
1941
|
+
assert.strictEqual(bomNSData?.parentComponent?.type, "device");
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
it("supports dry-run mode for hbom project types in the main CLI flow", async () => {
|
|
1945
|
+
setDryRunMode(true);
|
|
1946
|
+
resetRecordedActivities();
|
|
1947
|
+
|
|
1948
|
+
try {
|
|
1949
|
+
const bomNSData = await createBom(repoDir, {
|
|
1950
|
+
projectType: ["hbom"],
|
|
1951
|
+
specVersion: 1.7,
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
assert.strictEqual(bomNSData?.bomJson?.bomFormat, "CycloneDX");
|
|
1955
|
+
assert.strictEqual(bomNSData?.bomJson?.specVersion, "1.7");
|
|
1956
|
+
assert.ok(Array.isArray(bomNSData?.bomJson?.components));
|
|
1957
|
+
assert.ok(bomNSData?.bomJson?.components.length >= 1);
|
|
1958
|
+
assert.ok(Array.isArray(bomNSData?.dependencies));
|
|
1959
|
+
} finally {
|
|
1960
|
+
setDryRunMode(false);
|
|
1961
|
+
resetRecordedActivities();
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
it("shows dedicated hbom command help", () => {
|
|
1966
|
+
const result = spawnSync(
|
|
1967
|
+
process.execPath,
|
|
1968
|
+
[join(repoDir, "bin", "hbom.js"), "--help"],
|
|
1969
|
+
{
|
|
1970
|
+
cwd: repoDir,
|
|
1971
|
+
encoding: "utf8",
|
|
1972
|
+
env: buildMinimalCliEnv(),
|
|
1973
|
+
},
|
|
1974
|
+
);
|
|
1975
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
1976
|
+
|
|
1977
|
+
assert.strictEqual(result.status, 0);
|
|
1978
|
+
assert.match(output, /Output file\.\s+Default\s+hbom\.json/u);
|
|
1979
|
+
assert.match(output, /--include-runtime/u);
|
|
1980
|
+
assert.match(output, /--privileged/u);
|
|
1981
|
+
assert.match(output, /diagnostics/u);
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
it("uses the invoked hbom binary name in help output", () => {
|
|
1985
|
+
const tempDir = mkdtempSync(join(repoDir, ".cdxgen-hbom-help-name-"));
|
|
1986
|
+
try {
|
|
1987
|
+
const slimScript = join(tempDir, "hbom-slim");
|
|
1988
|
+
copyFileSync(join(repoDir, "bin", "hbom.js"), slimScript);
|
|
1989
|
+
const result = spawnSync(process.execPath, [slimScript, "--help"], {
|
|
1990
|
+
cwd: tempDir,
|
|
1991
|
+
encoding: "utf8",
|
|
1992
|
+
env: buildMinimalCliEnv(),
|
|
1993
|
+
});
|
|
1994
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
1995
|
+
|
|
1996
|
+
assert.strictEqual(result.status, 0);
|
|
1997
|
+
assert.match(output, /hbom-slim \[command\] \[options\]/u);
|
|
1998
|
+
} finally {
|
|
1999
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
it("fails early when hbom include-runtime lacks osquery support", () => {
|
|
2004
|
+
const emptyPluginsDir = mkdtempSync(
|
|
2005
|
+
join(tmpdir(), "cdxgen-empty-plugins-"),
|
|
2006
|
+
);
|
|
2007
|
+
try {
|
|
2008
|
+
const result = spawnSync(
|
|
2009
|
+
process.execPath,
|
|
2010
|
+
[join(repoDir, "bin", "hbom.js"), "--include-runtime"],
|
|
2011
|
+
{
|
|
2012
|
+
cwd: repoDir,
|
|
2013
|
+
encoding: "utf8",
|
|
2014
|
+
env: buildMinimalCliEnv({
|
|
2015
|
+
CDXGEN_PLUGINS_DIR: emptyPluginsDir,
|
|
2016
|
+
}),
|
|
2017
|
+
},
|
|
2018
|
+
);
|
|
2019
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
2020
|
+
|
|
2021
|
+
assert.strictEqual(result.status, 1);
|
|
2022
|
+
assert.match(output, /--include-runtime/u);
|
|
2023
|
+
assert.match(output, /cdxgen-plugins-bin/u);
|
|
2024
|
+
assert.match(
|
|
2025
|
+
output,
|
|
2026
|
+
/'hbom' is the bundled option required for '--include-runtime' support/u,
|
|
2027
|
+
);
|
|
2028
|
+
assert.doesNotMatch(output, /About to generate OBOM/u);
|
|
2029
|
+
} finally {
|
|
2030
|
+
rmSync(emptyPluginsDir, { force: true, recursive: true });
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
it("guides hbom-slim users to the standard binary for include-runtime", () => {
|
|
2035
|
+
const tempDir = mkdtempSync(
|
|
2036
|
+
join(repoDir, ".cdxgen-hbom-runtime-check-"),
|
|
2037
|
+
);
|
|
2038
|
+
const emptyPluginsDir = mkdtempSync(
|
|
2039
|
+
join(tmpdir(), "cdxgen-empty-plugins-"),
|
|
2040
|
+
);
|
|
2041
|
+
try {
|
|
2042
|
+
const slimScript = join(tempDir, "hbom-slim");
|
|
2043
|
+
copyFileSync(join(repoDir, "bin", "hbom.js"), slimScript);
|
|
2044
|
+
const result = spawnSync(
|
|
2045
|
+
process.execPath,
|
|
2046
|
+
[slimScript, "--include-runtime"],
|
|
2047
|
+
{
|
|
2048
|
+
cwd: tempDir,
|
|
2049
|
+
encoding: "utf8",
|
|
2050
|
+
env: buildMinimalCliEnv({
|
|
2051
|
+
CDXGEN_PLUGINS_DIR: emptyPluginsDir,
|
|
2052
|
+
}),
|
|
2053
|
+
},
|
|
2054
|
+
);
|
|
2055
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
2056
|
+
|
|
2057
|
+
assert.strictEqual(result.status, 1);
|
|
2058
|
+
assert.match(output, /'hbom-slim' is hardware-only by default/u);
|
|
2059
|
+
assert.match(
|
|
2060
|
+
output,
|
|
2061
|
+
/Use 'hbom' for bundled '--include-runtime' support/u,
|
|
2062
|
+
);
|
|
2063
|
+
} finally {
|
|
2064
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
2065
|
+
rmSync(emptyPluginsDir, { force: true, recursive: true });
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
it("supports the hbom diagnostics subcommand for existing BOM files", () => {
|
|
2070
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-hbom-diagnostics-"));
|
|
2071
|
+
try {
|
|
2072
|
+
const inputFile = join(tempDir, "hbom.json");
|
|
2073
|
+
writeFileSync(
|
|
2074
|
+
inputFile,
|
|
2075
|
+
JSON.stringify({
|
|
2076
|
+
bomFormat: "CycloneDX",
|
|
2077
|
+
components: [],
|
|
2078
|
+
metadata: {
|
|
2079
|
+
component: {
|
|
2080
|
+
name: "demo-host",
|
|
2081
|
+
properties: [
|
|
2082
|
+
{ name: "cdx:hbom:platform", value: "linux" },
|
|
2083
|
+
{ name: "cdx:hbom:architecture", value: "amd64" },
|
|
2084
|
+
],
|
|
2085
|
+
type: "device",
|
|
2086
|
+
},
|
|
2087
|
+
},
|
|
2088
|
+
properties: [
|
|
2089
|
+
{ name: "cdx:hbom:collectorProfile", value: "linux-amd64-v1" },
|
|
2090
|
+
{
|
|
2091
|
+
name: "cdx:hbom:evidence:commandDiagnosticCount",
|
|
2092
|
+
value: "2",
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
name: "cdx:hbom:evidence:commandDiagnostic",
|
|
2096
|
+
value: JSON.stringify({
|
|
2097
|
+
command: "lsusb",
|
|
2098
|
+
installHint:
|
|
2099
|
+
"Command not found: install the Linux package providing lsusb (commonly `usbutils`).",
|
|
2100
|
+
issue: "missing-command",
|
|
2101
|
+
message: "lsusb failed with missing-command",
|
|
2102
|
+
}),
|
|
2103
|
+
},
|
|
2104
|
+
{
|
|
2105
|
+
name: "cdx:hbom:evidence:commandDiagnostic",
|
|
2106
|
+
value: JSON.stringify({
|
|
2107
|
+
command: "drm_info",
|
|
2108
|
+
issue: "permission-denied",
|
|
2109
|
+
message: "drm_info failed with permission-denied",
|
|
2110
|
+
privilegeHint:
|
|
2111
|
+
"Retry with --privileged to allow a non-interactive sudo attempt for permission-sensitive Linux commands.",
|
|
2112
|
+
}),
|
|
2113
|
+
},
|
|
2114
|
+
],
|
|
2115
|
+
specVersion: "1.7",
|
|
2116
|
+
version: 1,
|
|
2117
|
+
}),
|
|
2118
|
+
);
|
|
2119
|
+
const result = spawnSync(
|
|
2120
|
+
process.execPath,
|
|
2121
|
+
[
|
|
2122
|
+
join(repoDir, "bin", "hbom.js"),
|
|
2123
|
+
"diagnostics",
|
|
2124
|
+
"--input",
|
|
2125
|
+
inputFile,
|
|
2126
|
+
],
|
|
2127
|
+
{
|
|
2128
|
+
cwd: tempDir,
|
|
2129
|
+
encoding: "utf8",
|
|
2130
|
+
env: buildMinimalCliEnv(),
|
|
2131
|
+
},
|
|
2132
|
+
);
|
|
2133
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
2134
|
+
|
|
2135
|
+
assert.strictEqual(result.status, 0);
|
|
2136
|
+
assert.match(output, /HBOM diagnostics summary/u);
|
|
2137
|
+
assert.match(output, /Missing commands:\n- lsusb/u);
|
|
2138
|
+
assert.match(output, /Permission-sensitive enrichments:/u);
|
|
2139
|
+
assert.match(output, /--privileged/u);
|
|
2140
|
+
} finally {
|
|
2141
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
it("supports dry-run mode in the dedicated hbom command", () => {
|
|
2146
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-hbom-dry-run-"));
|
|
2147
|
+
try {
|
|
2148
|
+
const outputFile = join(tempDir, "hbom.json");
|
|
2149
|
+
const result = spawnSync(
|
|
2150
|
+
process.execPath,
|
|
2151
|
+
[join(repoDir, "bin", "hbom.js"), "--dry-run"],
|
|
2152
|
+
{
|
|
2153
|
+
cwd: tempDir,
|
|
2154
|
+
encoding: "utf8",
|
|
2155
|
+
env: buildMinimalCliEnv(),
|
|
2156
|
+
},
|
|
2157
|
+
);
|
|
2158
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
2159
|
+
|
|
2160
|
+
assert.strictEqual(result.status, 0);
|
|
2161
|
+
assert.match(output, /cdxgen dry-run activity summary/u);
|
|
2162
|
+
assert.strictEqual(existsSync(outputFile), false);
|
|
2163
|
+
} finally {
|
|
2164
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
|
|
2168
|
+
it("rejects mixed hbom and sbom project types in the main CLI", () => {
|
|
2169
|
+
const result = spawnSync(
|
|
2170
|
+
process.execPath,
|
|
2171
|
+
[
|
|
2172
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
2173
|
+
"-t",
|
|
2174
|
+
"hbom",
|
|
2175
|
+
"-t",
|
|
2176
|
+
"js",
|
|
2177
|
+
"--no-banner",
|
|
2178
|
+
],
|
|
2179
|
+
{
|
|
2180
|
+
cwd: repoDir,
|
|
2181
|
+
encoding: "utf8",
|
|
2182
|
+
env: buildMinimalCliEnv(),
|
|
2183
|
+
},
|
|
2184
|
+
);
|
|
2185
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
2186
|
+
|
|
2187
|
+
assert.strictEqual(result.status, 1);
|
|
2188
|
+
assert.match(output, /HBOM project types cannot be mixed/u);
|
|
2189
|
+
});
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
|
|
1091
2193
|
describe("createBom() Collider lock support", () => {
|
|
1092
2194
|
it("preserves Collider integrity metadata and dependency nodes in the BOM", async () => {
|
|
1093
2195
|
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-collider-"));
|
|
@@ -1421,7 +2523,7 @@ checksum = "${"a".repeat(64)}"
|
|
|
1421
2523
|
);
|
|
1422
2524
|
});
|
|
1423
2525
|
|
|
1424
|
-
it("
|
|
2526
|
+
it("requires explicit opt-in for AI inventory in js and python scans", async () => {
|
|
1425
2527
|
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-ai-inventory-"));
|
|
1426
2528
|
mkdirSync(join(tmpDir, ".claude", "skills", "release"), {
|
|
1427
2529
|
recursive: true,
|
|
@@ -1539,88 +2641,158 @@ checksum = "${"a".repeat(64)}"
|
|
|
1539
2641
|
tmpDir,
|
|
1540
2642
|
).bomJson;
|
|
1541
2643
|
assert.ok(
|
|
1542
|
-
(jsBomJson.components || []).some(
|
|
1543
|
-
(
|
|
1544
|
-
getProp(component, "cdx:file:kind")
|
|
1545
|
-
|
|
2644
|
+
!(jsBomJson.components || []).some((component) =>
|
|
2645
|
+
["agent-instructions", "mcp-config", "skill-file"].includes(
|
|
2646
|
+
getProp(component, "cdx:file:kind"),
|
|
2647
|
+
),
|
|
2648
|
+
),
|
|
2649
|
+
"did not expect AI inventory components in js scan without opt-in",
|
|
2650
|
+
);
|
|
2651
|
+
assert.ok(
|
|
2652
|
+
!(jsBomJson.services || []).some((service) =>
|
|
2653
|
+
service.properties?.some((property) =>
|
|
2654
|
+
property.name.startsWith("cdx:mcp:"),
|
|
2655
|
+
),
|
|
1546
2656
|
),
|
|
1547
|
-
"
|
|
2657
|
+
"did not expect MCP services in js scan without opt-in",
|
|
1548
2658
|
);
|
|
2659
|
+
|
|
2660
|
+
const dockerOptions = {
|
|
2661
|
+
...baseOptions,
|
|
2662
|
+
projectType: ["js", "docker"],
|
|
2663
|
+
};
|
|
2664
|
+
const dockerBomJson = postProcess(
|
|
2665
|
+
await createNodejsBom(tmpDir, dockerOptions),
|
|
2666
|
+
dockerOptions,
|
|
2667
|
+
tmpDir,
|
|
2668
|
+
).bomJson;
|
|
1549
2669
|
assert.ok(
|
|
1550
|
-
(
|
|
2670
|
+
!(dockerBomJson.components || []).some((component) =>
|
|
2671
|
+
["agent-instructions", "mcp-config", "skill-file"].includes(
|
|
2672
|
+
getProp(component, "cdx:file:kind"),
|
|
2673
|
+
),
|
|
2674
|
+
),
|
|
2675
|
+
"did not expect AI inventory components in docker js scan without opt-in",
|
|
2676
|
+
);
|
|
2677
|
+
|
|
2678
|
+
const exactAiSkillOptions = {
|
|
2679
|
+
...baseOptions,
|
|
2680
|
+
projectType: ["ai-skill"],
|
|
2681
|
+
};
|
|
2682
|
+
const aiSkillBomJson = postProcess(
|
|
2683
|
+
await createBom(tmpDir, exactAiSkillOptions),
|
|
2684
|
+
exactAiSkillOptions,
|
|
2685
|
+
tmpDir,
|
|
2686
|
+
).bomJson;
|
|
2687
|
+
assert.ok(
|
|
2688
|
+
(aiSkillBomJson.components || []).some(
|
|
1551
2689
|
(component) =>
|
|
1552
2690
|
component.name === "CLAUDE.md" &&
|
|
1553
2691
|
getProp(component, "cdx:file:kind") === "agent-instructions",
|
|
1554
2692
|
),
|
|
1555
|
-
"expected CLAUDE.md in
|
|
2693
|
+
"expected CLAUDE.md in exact ai-skill scan",
|
|
1556
2694
|
);
|
|
1557
2695
|
assert.ok(
|
|
1558
|
-
(
|
|
2696
|
+
!(aiSkillBomJson.components || []).some(
|
|
1559
2697
|
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1560
2698
|
),
|
|
1561
|
-
"
|
|
2699
|
+
"did not expect MCP configs in exact ai-skill scan",
|
|
2700
|
+
);
|
|
2701
|
+
|
|
2702
|
+
const optedInJsOptions = {
|
|
2703
|
+
...baseOptions,
|
|
2704
|
+
projectType: ["js", "ai-skill", "mcp"],
|
|
2705
|
+
};
|
|
2706
|
+
const optedInJsBomJson = postProcess(
|
|
2707
|
+
await createBom(tmpDir, optedInJsOptions),
|
|
2708
|
+
optedInJsOptions,
|
|
2709
|
+
tmpDir,
|
|
2710
|
+
).bomJson;
|
|
2711
|
+
assert.ok(
|
|
2712
|
+
(optedInJsBomJson.components || []).some(
|
|
2713
|
+
(component) =>
|
|
2714
|
+
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
2715
|
+
getProp(component, "cdx:skill:name") === "release",
|
|
2716
|
+
),
|
|
2717
|
+
"expected skill file in opted-in js scan",
|
|
1562
2718
|
);
|
|
1563
2719
|
assert.ok(
|
|
1564
|
-
(
|
|
2720
|
+
(optedInJsBomJson.components || []).some(
|
|
2721
|
+
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
2722
|
+
),
|
|
2723
|
+
"expected MCP config in opted-in js scan",
|
|
2724
|
+
);
|
|
2725
|
+
assert.ok(
|
|
2726
|
+
(optedInJsBomJson.services || []).some(
|
|
1565
2727
|
(service) =>
|
|
1566
2728
|
service.name === "releaseDocs" &&
|
|
1567
2729
|
getProp(service, "cdx:mcp:inventorySource") === "config-file",
|
|
1568
2730
|
),
|
|
1569
|
-
"expected MCP config service in js scan",
|
|
2731
|
+
"expected MCP config service in opted-in js scan",
|
|
1570
2732
|
);
|
|
1571
2733
|
|
|
1572
|
-
const
|
|
2734
|
+
const auditAliasJsOptions = {
|
|
1573
2735
|
...baseOptions,
|
|
1574
|
-
|
|
2736
|
+
bomAuditCategories: "ai-inventory",
|
|
2737
|
+
projectType: ["js"],
|
|
1575
2738
|
};
|
|
1576
|
-
const
|
|
1577
|
-
await
|
|
1578
|
-
|
|
2739
|
+
const auditAliasJsBomJson = postProcess(
|
|
2740
|
+
await createBom(tmpDir, auditAliasJsOptions),
|
|
2741
|
+
auditAliasJsOptions,
|
|
1579
2742
|
tmpDir,
|
|
1580
2743
|
).bomJson;
|
|
1581
2744
|
assert.ok(
|
|
1582
|
-
(
|
|
2745
|
+
(auditAliasJsBomJson.components || []).some(
|
|
1583
2746
|
(component) =>
|
|
1584
2747
|
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
1585
2748
|
getProp(component, "cdx:skill:name") === "release",
|
|
1586
2749
|
),
|
|
1587
|
-
"expected skill file in
|
|
2750
|
+
"expected skill file in ai-inventory audit-category js scan",
|
|
1588
2751
|
);
|
|
1589
2752
|
assert.ok(
|
|
1590
|
-
(
|
|
2753
|
+
(auditAliasJsBomJson.components || []).some(
|
|
1591
2754
|
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1592
2755
|
),
|
|
1593
|
-
"expected MCP config in
|
|
2756
|
+
"expected MCP config in ai-inventory audit-category js scan",
|
|
2757
|
+
);
|
|
2758
|
+
assert.ok(
|
|
2759
|
+
(auditAliasJsBomJson.services || []).some(
|
|
2760
|
+
(service) =>
|
|
2761
|
+
service.name === "releaseDocs" &&
|
|
2762
|
+
getProp(service, "cdx:mcp:inventorySource") === "config-file",
|
|
2763
|
+
),
|
|
2764
|
+
"expected MCP config service in ai-inventory audit-category js scan",
|
|
1594
2765
|
);
|
|
1595
2766
|
|
|
1596
|
-
const
|
|
2767
|
+
const auditAgentJsOptions = {
|
|
1597
2768
|
...baseOptions,
|
|
1598
|
-
|
|
2769
|
+
bomAuditCategories: "ai-agent",
|
|
2770
|
+
projectType: ["js"],
|
|
1599
2771
|
};
|
|
1600
|
-
const
|
|
1601
|
-
await createBom(tmpDir,
|
|
1602
|
-
|
|
2772
|
+
const auditAgentJsBomJson = postProcess(
|
|
2773
|
+
await createBom(tmpDir, auditAgentJsOptions),
|
|
2774
|
+
auditAgentJsOptions,
|
|
1603
2775
|
tmpDir,
|
|
1604
2776
|
).bomJson;
|
|
1605
2777
|
assert.ok(
|
|
1606
|
-
(
|
|
2778
|
+
(auditAgentJsBomJson.components || []).some(
|
|
1607
2779
|
(component) =>
|
|
1608
|
-
component
|
|
1609
|
-
getProp(component, "cdx:
|
|
2780
|
+
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
2781
|
+
getProp(component, "cdx:skill:name") === "release",
|
|
1610
2782
|
),
|
|
1611
|
-
"expected
|
|
2783
|
+
"expected skill file in ai-agent audit-category js scan",
|
|
1612
2784
|
);
|
|
1613
2785
|
assert.ok(
|
|
1614
|
-
!(
|
|
2786
|
+
!(auditAgentJsBomJson.components || []).some(
|
|
1615
2787
|
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1616
2788
|
),
|
|
1617
|
-
"did not expect MCP
|
|
2789
|
+
"did not expect MCP config in ai-agent audit-category js scan",
|
|
1618
2790
|
);
|
|
1619
2791
|
|
|
1620
2792
|
const filteredOptions = {
|
|
1621
2793
|
...baseOptions,
|
|
1622
2794
|
excludeType: ["ai-skill", "mcp"],
|
|
1623
|
-
projectType: ["js"],
|
|
2795
|
+
projectType: ["js", "ai-skill", "mcp"],
|
|
1624
2796
|
};
|
|
1625
2797
|
const filteredBomJson = postProcess(
|
|
1626
2798
|
await createBom(tmpDir, filteredOptions),
|
|
@@ -1654,29 +2826,114 @@ checksum = "${"a".repeat(64)}"
|
|
|
1654
2826
|
tmpDir,
|
|
1655
2827
|
).bomJson;
|
|
1656
2828
|
assert.ok(
|
|
1657
|
-
(pyBomJson.components || []).some(
|
|
2829
|
+
!(pyBomJson.components || []).some((component) =>
|
|
2830
|
+
["agent-instructions", "mcp-config", "skill-file"].includes(
|
|
2831
|
+
getProp(component, "cdx:file:kind"),
|
|
2832
|
+
),
|
|
2833
|
+
),
|
|
2834
|
+
"did not expect AI inventory components in python scan without opt-in",
|
|
2835
|
+
);
|
|
2836
|
+
assert.ok(
|
|
2837
|
+
!(pyBomJson.services || []).some((service) =>
|
|
2838
|
+
service.properties?.some((property) =>
|
|
2839
|
+
property.name.startsWith("cdx:mcp:"),
|
|
2840
|
+
),
|
|
2841
|
+
),
|
|
2842
|
+
"did not expect MCP services in python scan without opt-in",
|
|
2843
|
+
);
|
|
2844
|
+
|
|
2845
|
+
const optedInPyOptions = {
|
|
2846
|
+
...baseOptions,
|
|
2847
|
+
projectType: ["py", "ai-skill", "mcp"],
|
|
2848
|
+
};
|
|
2849
|
+
const optedInPyBomJson = postProcess(
|
|
2850
|
+
await createPythonBom(tmpDir, optedInPyOptions),
|
|
2851
|
+
optedInPyOptions,
|
|
2852
|
+
tmpDir,
|
|
2853
|
+
).bomJson;
|
|
2854
|
+
assert.ok(
|
|
2855
|
+
(optedInPyBomJson.components || []).some(
|
|
1658
2856
|
(component) =>
|
|
1659
2857
|
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
1660
2858
|
getProp(component, "cdx:skill:name") === "release",
|
|
1661
2859
|
),
|
|
1662
|
-
"expected skill file in python scan",
|
|
2860
|
+
"expected skill file in opted-in python scan",
|
|
1663
2861
|
);
|
|
1664
2862
|
assert.ok(
|
|
1665
|
-
(
|
|
2863
|
+
(optedInPyBomJson.components || []).some(
|
|
1666
2864
|
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1667
2865
|
),
|
|
1668
|
-
"expected MCP config in python scan",
|
|
2866
|
+
"expected MCP config in opted-in python scan",
|
|
2867
|
+
);
|
|
2868
|
+
assert.ok(
|
|
2869
|
+
(optedInPyBomJson.services || []).some(
|
|
2870
|
+
(service) =>
|
|
2871
|
+
service.name === "python-release-docs" &&
|
|
2872
|
+
getProp(service, "cdx:mcp:inventorySource") ===
|
|
2873
|
+
"source-code-analysis",
|
|
2874
|
+
),
|
|
2875
|
+
"expected Python MCP service in opted-in python scan",
|
|
1669
2876
|
);
|
|
2877
|
+
|
|
2878
|
+
const auditMcpPyOptions = {
|
|
2879
|
+
...baseOptions,
|
|
2880
|
+
bomAuditCategories: "mcp-server",
|
|
2881
|
+
projectType: ["py"],
|
|
2882
|
+
};
|
|
2883
|
+
const auditMcpPyBomJson = postProcess(
|
|
2884
|
+
await createPythonBom(tmpDir, auditMcpPyOptions),
|
|
2885
|
+
auditMcpPyOptions,
|
|
2886
|
+
tmpDir,
|
|
2887
|
+
).bomJson;
|
|
1670
2888
|
assert.ok(
|
|
1671
|
-
(
|
|
2889
|
+
(auditMcpPyBomJson.services || []).some(
|
|
1672
2890
|
(service) =>
|
|
1673
2891
|
service.name === "python-release-docs" &&
|
|
1674
2892
|
getProp(service, "cdx:mcp:inventorySource") ===
|
|
1675
2893
|
"source-code-analysis",
|
|
1676
2894
|
),
|
|
1677
|
-
"expected Python MCP service in
|
|
2895
|
+
"expected Python MCP service in mcp-server audit-category scan",
|
|
2896
|
+
);
|
|
2897
|
+
assert.ok(
|
|
2898
|
+
!(auditMcpPyBomJson.components || []).some(
|
|
2899
|
+
(component) => getProp(component, "cdx:file:kind") === "skill-file",
|
|
2900
|
+
),
|
|
2901
|
+
"did not expect skill file in mcp-server audit-category python scan",
|
|
2902
|
+
);
|
|
2903
|
+
} finally {
|
|
2904
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2905
|
+
}
|
|
2906
|
+
});
|
|
2907
|
+
|
|
2908
|
+
it("does not trace an npm registry config read when opening .npmrc fails", async () => {
|
|
2909
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-npmrc-read-fail-"));
|
|
2910
|
+
writeFileSync(
|
|
2911
|
+
join(tmpDir, "package.json"),
|
|
2912
|
+
JSON.stringify({
|
|
2913
|
+
name: "npmrc-read-fail",
|
|
2914
|
+
version: "1.0.0",
|
|
2915
|
+
}),
|
|
2916
|
+
);
|
|
2917
|
+
mkdirSync(join(tmpDir, ".npmrc"), { recursive: true });
|
|
2918
|
+
setDryRunMode(true);
|
|
2919
|
+
resetRecordedActivities();
|
|
2920
|
+
try {
|
|
2921
|
+
await assert.rejects(() =>
|
|
2922
|
+
createNodejsBom(tmpDir, {
|
|
2923
|
+
installDeps: true,
|
|
2924
|
+
multiProject: false,
|
|
2925
|
+
projectType: ["npm"],
|
|
2926
|
+
}),
|
|
2927
|
+
);
|
|
2928
|
+
const readActivities = getRecordedActivities().filter(
|
|
2929
|
+
(activity) =>
|
|
2930
|
+
activity.kind === "read" &&
|
|
2931
|
+
activity.target === join(tmpDir, ".npmrc"),
|
|
1678
2932
|
);
|
|
2933
|
+
assert.deepStrictEqual(readActivities, []);
|
|
1679
2934
|
} finally {
|
|
2935
|
+
setDryRunMode(false);
|
|
2936
|
+
resetRecordedActivities();
|
|
1680
2937
|
rmSync(tmpDir, { force: true, recursive: true });
|
|
1681
2938
|
}
|
|
1682
2939
|
});
|