@cyclonedx/cdxgen 12.3.2 → 12.4.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 +70 -22
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +238 -116
- package/bin/convert.js +28 -13
- package/bin/hbom.js +490 -0
- package/bin/repl.js +580 -29
- package/bin/validate.js +34 -4
- package/bin/verify.js +40 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -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 +171 -15
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +76 -5
- 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 +36 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +209 -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 +647 -127
- package/lib/cli/index.poku.js +1905 -187
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/agentFormulationParser.js +6 -2
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/analyzer.js +1444 -38
- package/lib/helpers/analyzer.poku.js +409 -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/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/chromextutils.js +25 -3
- package/lib/helpers/chromextutils.poku.js +68 -0
- package/lib/helpers/ciParsers/githubActions.js +79 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
- package/lib/helpers/communityAiConfigParser.js +15 -5
- package/lib/helpers/communityAiConfigParser.poku.js +71 -0
- package/lib/helpers/depsUtils.js +5 -0
- package/lib/helpers/depsUtils.poku.js +55 -0
- package/lib/helpers/display.js +336 -23
- package/lib/helpers/display.poku.js +179 -43
- 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/mcpConfigParser.js +21 -5
- package/lib/helpers/mcpConfigParser.poku.js +39 -2
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +349 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/protobom.js +156 -45
- package/lib/helpers/protobom.poku.js +140 -5
- 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 +2454 -198
- package/lib/helpers/utils.poku.js +1798 -74
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2306 -350
- package/lib/managers/binary.poku.js +1700 -1
- package/lib/managers/docker.js +441 -95
- package/lib/managers/docker.poku.js +1479 -14
- package/lib/server/server.js +2 -24
- package/lib/server/server.poku.js +36 -1
- 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 +2967 -990
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +192 -1
- package/lib/stages/postgen/postgen.poku.js +321 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/package.json +24 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- 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/agentFormulationParser.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/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/chromextutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +1 -0
- 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 +62 -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/mcpConfigParser.d.ts +1 -1
- package/types/lib/helpers/mcpConfigParser.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/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +3 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- 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 +74 -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 +3 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -0
- 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/data/spdx-model-v3.0.1.jsonld +0 -15999
package/lib/cli/index.poku.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
1
2
|
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
2
5
|
mkdirSync,
|
|
3
6
|
mkdtempSync,
|
|
4
7
|
readFileSync,
|
|
5
8
|
rmSync,
|
|
6
9
|
writeFileSync,
|
|
7
10
|
} from "node:fs";
|
|
11
|
+
import { createServer } from "node:http";
|
|
8
12
|
import { tmpdir } from "node:os";
|
|
9
|
-
import { dirname, join } from "node:path";
|
|
13
|
+
import { dirname, join, normalize, sep } from "node:path";
|
|
10
14
|
import process from "node:process";
|
|
11
15
|
import { fileURLToPath } from "node:url";
|
|
12
16
|
|
|
@@ -14,13 +18,23 @@ import esmock from "esmock";
|
|
|
14
18
|
import { assert, describe, it } from "poku";
|
|
15
19
|
import sinon from "sinon";
|
|
16
20
|
|
|
21
|
+
import { readBinary } from "../helpers/protobom.js";
|
|
22
|
+
import {
|
|
23
|
+
getRecordedActivities,
|
|
24
|
+
resetRecordedActivities,
|
|
25
|
+
setDryRunMode,
|
|
26
|
+
} from "../helpers/utils.js";
|
|
17
27
|
import { auditBom } from "../stages/postgen/auditBom.js";
|
|
18
28
|
import { postProcess } from "../stages/postgen/postgen.js";
|
|
19
29
|
import {
|
|
20
30
|
createBom,
|
|
21
31
|
createChromeExtensionBom,
|
|
22
32
|
createNodejsBom,
|
|
33
|
+
createPHPBom,
|
|
34
|
+
createPythonBom,
|
|
23
35
|
createRustBom,
|
|
36
|
+
listComponents,
|
|
37
|
+
submitBom,
|
|
24
38
|
} from "./index.js";
|
|
25
39
|
|
|
26
40
|
const fixtureDir = join(
|
|
@@ -58,12 +72,678 @@ const mcpFixtureDir = join(
|
|
|
58
72
|
"data",
|
|
59
73
|
"mcp-repotest",
|
|
60
74
|
);
|
|
75
|
+
const cbomFixtureDir = join(
|
|
76
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
77
|
+
"..",
|
|
78
|
+
"..",
|
|
79
|
+
"test",
|
|
80
|
+
"data",
|
|
81
|
+
"cbom-js-repotest",
|
|
82
|
+
);
|
|
83
|
+
const cacheDisableFixtureDir = join(
|
|
84
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
85
|
+
"..",
|
|
86
|
+
"..",
|
|
87
|
+
"test",
|
|
88
|
+
"data",
|
|
89
|
+
"cache-disable-repotest",
|
|
90
|
+
);
|
|
91
|
+
const composerFixtureDir = join(
|
|
92
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
93
|
+
"..",
|
|
94
|
+
"..",
|
|
95
|
+
"test",
|
|
96
|
+
"data",
|
|
97
|
+
);
|
|
98
|
+
const repoDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
61
99
|
|
|
62
100
|
function getProp(obj, name) {
|
|
63
101
|
return obj?.properties?.find((property) => property.name === name)?.value;
|
|
64
102
|
}
|
|
65
103
|
|
|
104
|
+
function createComposerNodeModulesFixture() {
|
|
105
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-composer-node-modules-"));
|
|
106
|
+
const packageDir = join(tmpDir, "node_modules", "moment-timezone");
|
|
107
|
+
mkdirSync(packageDir, { recursive: true });
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(packageDir, "composer.json"),
|
|
110
|
+
readFileSync(join(composerFixtureDir, "composer.json"), "utf-8"),
|
|
111
|
+
);
|
|
112
|
+
writeFileSync(
|
|
113
|
+
join(packageDir, "composer.lock"),
|
|
114
|
+
readFileSync(join(composerFixtureDir, "composer.lock"), "utf-8"),
|
|
115
|
+
);
|
|
116
|
+
return tmpDir;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createJarNodeModulesFixture() {
|
|
120
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-jar-node-modules-"));
|
|
121
|
+
const packageDir = join(tmpDir, "node_modules", "font-mfizz");
|
|
122
|
+
mkdirSync(packageDir, { recursive: true });
|
|
123
|
+
writeFileSync(join(packageDir, "blaze.jar"), "fake jar content");
|
|
124
|
+
return tmpDir;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const stubbedJarPackage = {
|
|
128
|
+
group: "org.slf4j",
|
|
129
|
+
name: "slf4j-simple",
|
|
130
|
+
version: "2.0.17",
|
|
131
|
+
purl: "pkg:maven/org.slf4j/slf4j-simple@2.0.17?type=jar",
|
|
132
|
+
"bom-ref": "pkg:maven/org.slf4j/slf4j-simple@2.0.17?type=jar",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
async function loadStubbedCreateJarBom() {
|
|
136
|
+
const actualUtils = await import("../helpers/utils.js");
|
|
137
|
+
const extractJarArchive = sinon.stub().resolves([stubbedJarPackage]);
|
|
138
|
+
const getMvnMetadata = sinon.stub().callsFake(async (pkgList) => pkgList);
|
|
139
|
+
const mockedIndex = await esmock("./index.js", {
|
|
140
|
+
"../helpers/utils.js": {
|
|
141
|
+
...actualUtils,
|
|
142
|
+
extractJarArchive,
|
|
143
|
+
getMvnMetadata,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
return mockedIndex.createJarBom;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toPortablePath(filePath) {
|
|
150
|
+
return normalize(filePath).split(sep).join("/");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getNpmPackFilePaths() {
|
|
154
|
+
const command =
|
|
155
|
+
process.platform === "win32"
|
|
156
|
+
? {
|
|
157
|
+
args: ["/c", "npm", "pack", "--dry-run", "--json"],
|
|
158
|
+
file: process.env.ComSpec || "cmd.exe",
|
|
159
|
+
}
|
|
160
|
+
: {
|
|
161
|
+
args: ["pack", "--dry-run", "--json"],
|
|
162
|
+
file: "npm",
|
|
163
|
+
};
|
|
164
|
+
const packOutput = execFileSync(command.file, command.args, {
|
|
165
|
+
cwd: repoDir,
|
|
166
|
+
encoding: "utf8",
|
|
167
|
+
});
|
|
168
|
+
const [packSummary] = JSON.parse(packOutput);
|
|
169
|
+
return packSummary.files.map((file) => toPortablePath(file.path));
|
|
170
|
+
}
|
|
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
|
+
|
|
66
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
|
+
|
|
291
|
+
describe("distribution filters", () => {
|
|
292
|
+
it("keeps npm types while excluding poku tests from npm pack output", () => {
|
|
293
|
+
const packedPaths = getNpmPackFilePaths();
|
|
294
|
+
|
|
295
|
+
assert.ok(
|
|
296
|
+
packedPaths.some((path) => path.startsWith("types/")),
|
|
297
|
+
"expected npm pack output to keep generated type definitions",
|
|
298
|
+
);
|
|
299
|
+
assert.ok(
|
|
300
|
+
packedPaths.every((path) => !path.endsWith(".poku.js")),
|
|
301
|
+
"expected npm pack output to exclude co-located poku tests",
|
|
302
|
+
);
|
|
303
|
+
assert.ok(
|
|
304
|
+
packedPaths.every((path) => !path.startsWith("test/")),
|
|
305
|
+
"expected npm pack output to exclude test fixtures",
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
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
|
+
expectedSpecVersion: (specVersion) => specVersion,
|
|
627
|
+
name: "cbom",
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
args: ["-t", "js", mcpFixtureDir, "--standard", "asvs-5.0"],
|
|
631
|
+
assertRoundTrip: (bomJson) => {
|
|
632
|
+
assert.equal(Array.isArray(bomJson.definitions), false);
|
|
633
|
+
assert.ok((bomJson.definitions?.standards || []).length >= 1);
|
|
634
|
+
},
|
|
635
|
+
expectedSpecVersion: () => "1.7",
|
|
636
|
+
name: "standards",
|
|
637
|
+
},
|
|
638
|
+
];
|
|
639
|
+
try {
|
|
640
|
+
for (const scenario of scenarios) {
|
|
641
|
+
for (const specVersion of ["1.6", "1.7"]) {
|
|
642
|
+
const jsonPath = join(
|
|
643
|
+
fixtureRoot,
|
|
644
|
+
`${scenario.name}-${specVersion}.json`,
|
|
645
|
+
);
|
|
646
|
+
const protoPath = join(
|
|
647
|
+
fixtureRoot,
|
|
648
|
+
`${scenario.name}-${specVersion}.cdx`,
|
|
649
|
+
);
|
|
650
|
+
const spdxPath = join(
|
|
651
|
+
fixtureRoot,
|
|
652
|
+
`${scenario.name}-${specVersion}.spdx.json`,
|
|
653
|
+
);
|
|
654
|
+
const generateResult = spawnSync(
|
|
655
|
+
process.execPath,
|
|
656
|
+
[
|
|
657
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
658
|
+
...scenario.args,
|
|
659
|
+
"-o",
|
|
660
|
+
jsonPath,
|
|
661
|
+
"--spec-version",
|
|
662
|
+
specVersion,
|
|
663
|
+
"--export-proto",
|
|
664
|
+
"--proto-bin-file",
|
|
665
|
+
protoPath,
|
|
666
|
+
"--no-banner",
|
|
667
|
+
],
|
|
668
|
+
{
|
|
669
|
+
cwd: repoDir,
|
|
670
|
+
encoding: "utf8",
|
|
671
|
+
env: buildMinimalCliEnv(),
|
|
672
|
+
},
|
|
673
|
+
);
|
|
674
|
+
assert.strictEqual(
|
|
675
|
+
generateResult.status,
|
|
676
|
+
0,
|
|
677
|
+
`${scenario.name} ${specVersion}: ${generateResult.stdout}${generateResult.stderr}`,
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const generatedBom = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
681
|
+
assert.strictEqual(
|
|
682
|
+
generatedBom.specVersion,
|
|
683
|
+
scenario.expectedSpecVersion(specVersion),
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
const convertResult = spawnSync(
|
|
687
|
+
process.execPath,
|
|
688
|
+
[
|
|
689
|
+
join(repoDir, "bin", "convert.js"),
|
|
690
|
+
"-i",
|
|
691
|
+
protoPath,
|
|
692
|
+
"-o",
|
|
693
|
+
spdxPath,
|
|
694
|
+
],
|
|
695
|
+
{
|
|
696
|
+
cwd: repoDir,
|
|
697
|
+
encoding: "utf8",
|
|
698
|
+
env: buildMinimalCliEnv(),
|
|
699
|
+
},
|
|
700
|
+
);
|
|
701
|
+
assert.strictEqual(
|
|
702
|
+
convertResult.status,
|
|
703
|
+
0,
|
|
704
|
+
`${scenario.name} ${specVersion}: ${convertResult.stdout}${convertResult.stderr}`,
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
const validateResult = spawnSync(
|
|
708
|
+
process.execPath,
|
|
709
|
+
[
|
|
710
|
+
join(repoDir, "bin", "validate.js"),
|
|
711
|
+
"-i",
|
|
712
|
+
protoPath,
|
|
713
|
+
"--fail-severity",
|
|
714
|
+
"critical",
|
|
715
|
+
"--no-deep",
|
|
716
|
+
"--report",
|
|
717
|
+
"json",
|
|
718
|
+
],
|
|
719
|
+
{
|
|
720
|
+
cwd: repoDir,
|
|
721
|
+
encoding: "utf8",
|
|
722
|
+
env: buildMinimalCliEnv(),
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
assert.strictEqual(
|
|
726
|
+
validateResult.status,
|
|
727
|
+
0,
|
|
728
|
+
`${scenario.name} ${specVersion}: ${validateResult.stdout}${validateResult.stderr}`,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const roundTrippedBom = readBinary(protoPath);
|
|
732
|
+
assert.ok(roundTrippedBom);
|
|
733
|
+
assert.strictEqual(roundTrippedBom.bomFormat, "CycloneDX");
|
|
734
|
+
assert.strictEqual(
|
|
735
|
+
roundTrippedBom.specVersion,
|
|
736
|
+
scenario.expectedSpecVersion(specVersion),
|
|
737
|
+
);
|
|
738
|
+
scenario.assertRoundTrip(roundTrippedBom);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
} finally {
|
|
742
|
+
rmSync(fixtureRoot, { force: true, recursive: true });
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
67
747
|
describe("submitBom()", () => {
|
|
68
748
|
it("should report blocked Dependency-Track submission during dry-run", async () => {
|
|
69
749
|
const recordActivity = sinon.stub();
|
|
@@ -96,18 +776,11 @@ describe("CLI tests", () => {
|
|
|
96
776
|
});
|
|
97
777
|
|
|
98
778
|
it("should successfully report the SBOM with given project id, name, version and a single tag", async () => {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
104
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
105
|
-
|
|
106
|
-
const { submitBom } = await esmock("./index.js", {
|
|
107
|
-
got: { default: gotStub },
|
|
108
|
-
});
|
|
779
|
+
const server = await startSubmitBomTestServer(async () => ({
|
|
780
|
+
body: { success: true },
|
|
781
|
+
}));
|
|
109
782
|
|
|
110
|
-
const serverUrl =
|
|
783
|
+
const serverUrl = server.serverUrl;
|
|
111
784
|
const projectId = "f7cb9f02-8041-4991-9101-b01fa07a6522";
|
|
112
785
|
const projectName = "cdxgen-test-project";
|
|
113
786
|
const projectVersion = "1.0.0";
|
|
@@ -125,46 +798,44 @@ describe("CLI tests", () => {
|
|
|
125
798
|
projectTags: [{ name: projectTag }],
|
|
126
799
|
};
|
|
127
800
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Verify got was called exactly once
|
|
142
|
-
sinon.assert.calledOnce(gotStub);
|
|
143
|
-
|
|
144
|
-
// Grab call arguments
|
|
145
|
-
const [calledUrl, options] = gotStub.firstCall.args;
|
|
801
|
+
try {
|
|
802
|
+
const response = await submitBom(
|
|
803
|
+
{
|
|
804
|
+
serverUrl,
|
|
805
|
+
projectId,
|
|
806
|
+
projectName,
|
|
807
|
+
projectVersion,
|
|
808
|
+
apiKey,
|
|
809
|
+
skipDtTlsCheck,
|
|
810
|
+
projectTag,
|
|
811
|
+
},
|
|
812
|
+
bomContent,
|
|
813
|
+
);
|
|
146
814
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
815
|
+
assert.deepEqual(response, { success: true });
|
|
816
|
+
assert.equal(server.requests.length, 1);
|
|
817
|
+
assert.equal(server.requests[0].method, "PUT");
|
|
818
|
+
assert.equal(server.requests[0].url, "/api/v1/bom");
|
|
819
|
+
assert.equal(getRequestHeader(server.requests[0], "x-api-key"), apiKey);
|
|
820
|
+
assert.equal(
|
|
821
|
+
getRequestHeader(server.requests[0], "content-type"),
|
|
822
|
+
"application/json",
|
|
823
|
+
);
|
|
824
|
+
assert.deepEqual(
|
|
825
|
+
JSON.parse(server.requests[0].body),
|
|
826
|
+
expectedRequestPayload,
|
|
827
|
+
);
|
|
828
|
+
} finally {
|
|
829
|
+
await server.close();
|
|
830
|
+
}
|
|
153
831
|
});
|
|
154
832
|
|
|
155
833
|
it("should successfully report the SBOM with given parent project, name, version and multiple tags", async () => {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
161
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
162
|
-
|
|
163
|
-
const { submitBom } = await esmock("./index.js", {
|
|
164
|
-
got: { default: gotStub },
|
|
165
|
-
});
|
|
834
|
+
const server = await startSubmitBomTestServer(async () => ({
|
|
835
|
+
body: { success: true },
|
|
836
|
+
}));
|
|
166
837
|
|
|
167
|
-
const serverUrl =
|
|
838
|
+
const serverUrl = server.serverUrl;
|
|
168
839
|
const projectName = "cdxgen-test-project";
|
|
169
840
|
const projectVersion = "1.1.0";
|
|
170
841
|
const projectTags = ["tag1", "tag2"];
|
|
@@ -184,47 +855,44 @@ describe("CLI tests", () => {
|
|
|
184
855
|
projectTags: [{ name: projectTags[0] }, { name: projectTags[1] }],
|
|
185
856
|
};
|
|
186
857
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// Verify got was called exactly once
|
|
201
|
-
sinon.assert.calledOnce(gotStub);
|
|
202
|
-
|
|
203
|
-
// Grab call arguments
|
|
204
|
-
const [calledUrl, options] = gotStub.firstCall.args;
|
|
858
|
+
try {
|
|
859
|
+
const response = await submitBom(
|
|
860
|
+
{
|
|
861
|
+
serverUrl,
|
|
862
|
+
parentProjectId,
|
|
863
|
+
projectName,
|
|
864
|
+
projectVersion,
|
|
865
|
+
apiKey,
|
|
866
|
+
skipDtTlsCheck,
|
|
867
|
+
projectTag: projectTags,
|
|
868
|
+
},
|
|
869
|
+
bomContent,
|
|
870
|
+
);
|
|
205
871
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
872
|
+
assert.deepEqual(response, { success: true });
|
|
873
|
+
assert.equal(server.requests.length, 1);
|
|
874
|
+
assert.equal(server.requests[0].method, "PUT");
|
|
875
|
+
assert.equal(server.requests[0].url, "/api/v1/bom");
|
|
876
|
+
assert.equal(getRequestHeader(server.requests[0], "x-api-key"), apiKey);
|
|
877
|
+
assert.equal(
|
|
878
|
+
getRequestHeader(server.requests[0], "content-type"),
|
|
879
|
+
"application/json",
|
|
880
|
+
);
|
|
881
|
+
assert.deepEqual(
|
|
882
|
+
JSON.parse(server.requests[0].body),
|
|
883
|
+
expectedRequestPayload,
|
|
884
|
+
);
|
|
885
|
+
} finally {
|
|
886
|
+
await server.close();
|
|
887
|
+
}
|
|
213
888
|
});
|
|
214
889
|
|
|
215
890
|
it("should include parentName and parentVersion when parent project name and version are passed", async () => {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
221
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
222
|
-
|
|
223
|
-
const { submitBom } = await esmock("./index.js", {
|
|
224
|
-
got: { default: gotStub },
|
|
225
|
-
});
|
|
891
|
+
const server = await startSubmitBomTestServer(async () => ({
|
|
892
|
+
body: { success: true },
|
|
893
|
+
}));
|
|
226
894
|
|
|
227
|
-
const serverUrl =
|
|
895
|
+
const serverUrl = server.serverUrl;
|
|
228
896
|
const projectName = "cdxgen-test-project";
|
|
229
897
|
const projectVersion = "2.0.0";
|
|
230
898
|
const parentProjectName = "parent-project";
|
|
@@ -244,76 +912,71 @@ describe("CLI tests", () => {
|
|
|
244
912
|
projectVersion,
|
|
245
913
|
};
|
|
246
914
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
sinon.assert.calledOnce(gotStub);
|
|
261
|
-
const [calledUrl, options] = gotStub.firstCall.args;
|
|
915
|
+
try {
|
|
916
|
+
const response = await submitBom(
|
|
917
|
+
{
|
|
918
|
+
serverUrl,
|
|
919
|
+
projectName,
|
|
920
|
+
projectVersion,
|
|
921
|
+
parentProjectName,
|
|
922
|
+
parentProjectVersion,
|
|
923
|
+
apiKey,
|
|
924
|
+
skipDtTlsCheck,
|
|
925
|
+
},
|
|
926
|
+
bomContent,
|
|
927
|
+
);
|
|
262
928
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
929
|
+
assert.deepEqual(response, { success: true });
|
|
930
|
+
assert.equal(server.requests.length, 1);
|
|
931
|
+
assert.equal(server.requests[0].method, "PUT");
|
|
932
|
+
assert.equal(server.requests[0].url, "/api/v1/bom");
|
|
933
|
+
assert.equal(getRequestHeader(server.requests[0], "x-api-key"), apiKey);
|
|
934
|
+
assert.equal(
|
|
935
|
+
getRequestHeader(server.requests[0], "content-type"),
|
|
936
|
+
"application/json",
|
|
937
|
+
);
|
|
938
|
+
assert.deepEqual(
|
|
939
|
+
JSON.parse(server.requests[0].body),
|
|
940
|
+
expectedRequestPayload,
|
|
941
|
+
);
|
|
942
|
+
} finally {
|
|
943
|
+
await server.close();
|
|
944
|
+
}
|
|
269
945
|
});
|
|
270
946
|
|
|
271
947
|
it("should include configurable autoCreate and isLatest values in payload", async () => {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
277
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
948
|
+
const server = await startSubmitBomTestServer(async () => ({
|
|
949
|
+
body: { success: true },
|
|
950
|
+
}));
|
|
278
951
|
|
|
279
|
-
const
|
|
280
|
-
got: { default: gotStub },
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const serverUrl = "https://dtrack.example.com";
|
|
952
|
+
const serverUrl = server.serverUrl;
|
|
284
953
|
const projectName = "cdxgen-test-project";
|
|
285
954
|
const apiKey = "TEST_API_KEY";
|
|
286
955
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
956
|
+
try {
|
|
957
|
+
const response = await submitBom(
|
|
958
|
+
{
|
|
959
|
+
serverUrl,
|
|
960
|
+
projectName,
|
|
961
|
+
apiKey,
|
|
962
|
+
autoCreate: false,
|
|
963
|
+
isLatest: true,
|
|
964
|
+
},
|
|
965
|
+
{ bom: "test4" },
|
|
966
|
+
);
|
|
297
967
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
968
|
+
assert.deepEqual(response, { success: true });
|
|
969
|
+
assert.equal(server.requests.length, 1);
|
|
970
|
+
const payload = JSON.parse(server.requests[0].body);
|
|
971
|
+
assert.equal(payload.autoCreate, "false");
|
|
972
|
+
assert.equal(payload.isLatest, true);
|
|
973
|
+
assert.equal(payload.projectVersion, "main");
|
|
974
|
+
} finally {
|
|
975
|
+
await server.close();
|
|
976
|
+
}
|
|
303
977
|
});
|
|
304
978
|
|
|
305
979
|
it("should reject invalid mixed parent modes before making network request", async () => {
|
|
306
|
-
const fakeGotResponse = {
|
|
307
|
-
json: sinon.stub().resolves({ success: true }),
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const gotStub = sinon.stub().returns(fakeGotResponse);
|
|
311
|
-
gotStub.extend = sinon.stub().returns(gotStub);
|
|
312
|
-
|
|
313
|
-
const { submitBom } = await esmock("./index.js", {
|
|
314
|
-
got: { default: gotStub },
|
|
315
|
-
});
|
|
316
|
-
|
|
317
980
|
const response = await submitBom(
|
|
318
981
|
{
|
|
319
982
|
serverUrl: "https://dtrack.example.com",
|
|
@@ -326,7 +989,57 @@ describe("CLI tests", () => {
|
|
|
326
989
|
);
|
|
327
990
|
|
|
328
991
|
assert.equal(response, undefined);
|
|
329
|
-
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it("rejects malformed Dependency-Track URLs before making a request", async () => {
|
|
995
|
+
const response = await submitBom(
|
|
996
|
+
{
|
|
997
|
+
serverUrl: "file:///tmp/dtrack",
|
|
998
|
+
projectName: "cdxgen-test-project",
|
|
999
|
+
apiKey: "TEST_API_KEY",
|
|
1000
|
+
},
|
|
1001
|
+
{ bom: "test-invalid-url" },
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
assert.equal(response, undefined);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it("disables redirects for the POST fallback request too", async () => {
|
|
1008
|
+
const server = await startSubmitBomTestServer(
|
|
1009
|
+
async (_request, requestCount) => {
|
|
1010
|
+
if (requestCount === 1) {
|
|
1011
|
+
return { body: { error: "Method not allowed" }, statusCode: 405 };
|
|
1012
|
+
}
|
|
1013
|
+
return { body: { success: true }, statusCode: 200 };
|
|
1014
|
+
},
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
try {
|
|
1018
|
+
const response = await submitBom(
|
|
1019
|
+
{
|
|
1020
|
+
serverUrl: server.serverUrl,
|
|
1021
|
+
projectName: "cdxgen-test-project",
|
|
1022
|
+
apiKey: "TEST_API_KEY\r\n",
|
|
1023
|
+
},
|
|
1024
|
+
{ bom: "test6" },
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
assert.deepEqual(response, { success: true });
|
|
1028
|
+
assert.equal(server.requests.length, 2);
|
|
1029
|
+
assert.equal(server.requests[0].method, "PUT");
|
|
1030
|
+
assert.equal(server.requests[1].method, "POST");
|
|
1031
|
+
assert.equal(server.requests[1].url, "/api/v1/bom");
|
|
1032
|
+
assert.equal(
|
|
1033
|
+
getRequestHeader(server.requests[1], "x-api-key"),
|
|
1034
|
+
"TEST_API_KEY",
|
|
1035
|
+
);
|
|
1036
|
+
assert.equal(
|
|
1037
|
+
getRequestHeader(server.requests[1], "content-type"),
|
|
1038
|
+
"application/json",
|
|
1039
|
+
);
|
|
1040
|
+
} finally {
|
|
1041
|
+
await server.close();
|
|
1042
|
+
}
|
|
330
1043
|
});
|
|
331
1044
|
});
|
|
332
1045
|
|
|
@@ -511,13 +1224,143 @@ describe("CLI tests", () => {
|
|
|
511
1224
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
512
1225
|
}
|
|
513
1226
|
});
|
|
514
|
-
});
|
|
515
1227
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1228
|
+
it("should not scan installed browser locations without explicit extension project type", async () => {
|
|
1229
|
+
const discoverChromiumExtensionDirs = sinon.stub().returns([
|
|
1230
|
+
{
|
|
1231
|
+
browser: "Google Chrome",
|
|
1232
|
+
channel: "stable",
|
|
1233
|
+
dir: join(tmpdir(), "fake-browser-dir"),
|
|
1234
|
+
},
|
|
1235
|
+
]);
|
|
1236
|
+
const collectInstalledChromeExtensions = sinon.stub().returns([
|
|
1237
|
+
{
|
|
1238
|
+
type: "application",
|
|
1239
|
+
name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
1240
|
+
version: "1.0.0",
|
|
1241
|
+
purl: "pkg:chrome-extension/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@1.0.0",
|
|
1242
|
+
"bom-ref":
|
|
1243
|
+
"pkg:chrome-extension/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@1.0.0",
|
|
1244
|
+
},
|
|
1245
|
+
]);
|
|
1246
|
+
const { createChromeExtensionBom: createChromeExtensionBomMocked } =
|
|
1247
|
+
await esmock("./index.js", {
|
|
1248
|
+
"../helpers/chromextutils.js": {
|
|
1249
|
+
CHROME_EXTENSION_PURL_TYPE: "chrome-extension",
|
|
1250
|
+
collectChromeExtensionsFromPath: sinon
|
|
1251
|
+
.stub()
|
|
1252
|
+
.returns({ components: [], extensionDirs: [] }),
|
|
1253
|
+
collectInstalledChromeExtensions,
|
|
1254
|
+
discoverChromiumExtensionDirs,
|
|
1255
|
+
},
|
|
1256
|
+
});
|
|
1257
|
+
const bomData = await createChromeExtensionBomMocked(
|
|
1258
|
+
join(tmpdir(), "generic-project"),
|
|
1259
|
+
{
|
|
1260
|
+
deep: true,
|
|
1261
|
+
multiProject: false,
|
|
1262
|
+
projectType: ["js"],
|
|
1263
|
+
},
|
|
1264
|
+
);
|
|
1265
|
+
assert.deepStrictEqual(bomData?.bomJson?.components || [], []);
|
|
1266
|
+
sinon.assert.notCalled(discoverChromiumExtensionDirs);
|
|
1267
|
+
sinon.assert.notCalled(collectInstalledChromeExtensions);
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
describe("createVscodeExtensionBom()", () => {
|
|
1272
|
+
it("should not scan installed IDE locations without explicit extension project type", async () => {
|
|
1273
|
+
const discoverIdeExtensionDirs = sinon.stub().returns([
|
|
1274
|
+
{
|
|
1275
|
+
name: "VS Code",
|
|
1276
|
+
dir: join(tmpdir(), "fake-ide-dir"),
|
|
1277
|
+
},
|
|
1278
|
+
]);
|
|
1279
|
+
const collectInstalledExtensions = sinon.stub().returns([
|
|
1280
|
+
{
|
|
1281
|
+
type: "application",
|
|
1282
|
+
name: "sample.publisher",
|
|
1283
|
+
version: "1.0.0",
|
|
1284
|
+
purl: "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1285
|
+
"bom-ref": "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1286
|
+
},
|
|
1287
|
+
]);
|
|
1288
|
+
const { createVscodeExtensionBom: createVscodeExtensionBomMocked } =
|
|
1289
|
+
await esmock("./index.js", {
|
|
1290
|
+
"../helpers/vsixutils.js": {
|
|
1291
|
+
cleanupTempDir: sinon.stub(),
|
|
1292
|
+
collectInstalledExtensions,
|
|
1293
|
+
discoverIdeExtensionDirs,
|
|
1294
|
+
extractVsixToTempDir: sinon.stub(),
|
|
1295
|
+
parseVsixFile: sinon.stub(),
|
|
1296
|
+
VSCODE_EXTENSION_PURL_TYPE: "vscode-extension",
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
const bomData = await createVscodeExtensionBomMocked(
|
|
1300
|
+
join(tmpdir(), "generic-project"),
|
|
1301
|
+
{
|
|
1302
|
+
deep: true,
|
|
1303
|
+
multiProject: false,
|
|
1304
|
+
projectType: ["js"],
|
|
1305
|
+
},
|
|
1306
|
+
);
|
|
1307
|
+
assert.deepStrictEqual(bomData?.bomJson?.components || [], []);
|
|
1308
|
+
sinon.assert.notCalled(discoverIdeExtensionDirs);
|
|
1309
|
+
sinon.assert.notCalled(collectInstalledExtensions);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
it("should scan installed IDE locations when explicitly requested", async () => {
|
|
1313
|
+
const discoverIdeExtensionDirs = sinon.stub().returns([
|
|
1314
|
+
{
|
|
1315
|
+
name: "VS Code",
|
|
1316
|
+
dir: join(tmpdir(), "fake-ide-dir"),
|
|
1317
|
+
},
|
|
1318
|
+
]);
|
|
1319
|
+
const collectInstalledExtensions = sinon.stub().returns([
|
|
1320
|
+
{
|
|
1321
|
+
type: "application",
|
|
1322
|
+
name: "sample.publisher",
|
|
1323
|
+
version: "1.0.0",
|
|
1324
|
+
purl: "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1325
|
+
"bom-ref": "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1326
|
+
},
|
|
1327
|
+
]);
|
|
1328
|
+
const { createVscodeExtensionBom: createVscodeExtensionBomMocked } =
|
|
1329
|
+
await esmock("./index.js", {
|
|
1330
|
+
"../helpers/vsixutils.js": {
|
|
1331
|
+
cleanupTempDir: sinon.stub(),
|
|
1332
|
+
collectInstalledExtensions,
|
|
1333
|
+
discoverIdeExtensionDirs,
|
|
1334
|
+
extractVsixToTempDir: sinon.stub(),
|
|
1335
|
+
parseVsixFile: sinon.stub(),
|
|
1336
|
+
VSCODE_EXTENSION_PURL_TYPE: "vscode-extension",
|
|
1337
|
+
},
|
|
1338
|
+
});
|
|
1339
|
+
const bomData = await createVscodeExtensionBomMocked(
|
|
1340
|
+
join(tmpdir(), "generic-project"),
|
|
1341
|
+
{
|
|
1342
|
+
deep: true,
|
|
1343
|
+
multiProject: false,
|
|
1344
|
+
projectType: ["ide-extension"],
|
|
1345
|
+
},
|
|
1346
|
+
);
|
|
1347
|
+
const components = bomData?.bomJson?.components || [];
|
|
1348
|
+
assert.ok(
|
|
1349
|
+
components.some(
|
|
1350
|
+
(component) =>
|
|
1351
|
+
component.purl === "pkg:vscode-extension/sample/publisher@1.0.0",
|
|
1352
|
+
),
|
|
1353
|
+
);
|
|
1354
|
+
sinon.assert.calledOnce(discoverIdeExtensionDirs);
|
|
1355
|
+
sinon.assert.calledOnce(collectInstalledExtensions);
|
|
1356
|
+
});
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
describe("createMultiXBom()", () => {
|
|
1360
|
+
it("should scan installed chrome extensions only once across multiple non-extension paths", async () => {
|
|
1361
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "cdxgen-chrome-ext-multi-"));
|
|
1362
|
+
const pathA = join(tempRoot, "project-a");
|
|
1363
|
+
const pathB = join(tempRoot, "project-b");
|
|
521
1364
|
mkdirSync(pathA, { recursive: true });
|
|
522
1365
|
mkdirSync(pathB, { recursive: true });
|
|
523
1366
|
const collectInstalledChromeExtensions = sinon.stub().returns([
|
|
@@ -654,6 +1497,103 @@ describe("CLI tests", () => {
|
|
|
654
1497
|
rmSync(tempDir, { recursive: true, force: true });
|
|
655
1498
|
}
|
|
656
1499
|
});
|
|
1500
|
+
|
|
1501
|
+
it("treats an existing local directory as a staged rootfs for docker scans", async () => {
|
|
1502
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-rootfs-"));
|
|
1503
|
+
const exportImage = sinon.stub().resolves(undefined);
|
|
1504
|
+
const getPkgPathList = sinon.stub().returns([]);
|
|
1505
|
+
try {
|
|
1506
|
+
const { createBom: createBomMocked } = await esmock("./index.js", {
|
|
1507
|
+
"../managers/binary.js": {
|
|
1508
|
+
executeOsQuery: sinon.stub(),
|
|
1509
|
+
getBinaryBom: sinon.stub(),
|
|
1510
|
+
getDotnetSlices: sinon.stub(),
|
|
1511
|
+
getOSPackages: sinon.stub().resolves({
|
|
1512
|
+
allTypes: [],
|
|
1513
|
+
binPaths: [],
|
|
1514
|
+
bundledRuntimes: [],
|
|
1515
|
+
bundledSdks: [],
|
|
1516
|
+
dependenciesList: [],
|
|
1517
|
+
executables: [],
|
|
1518
|
+
osPackages: [],
|
|
1519
|
+
sharedLibs: [],
|
|
1520
|
+
}),
|
|
1521
|
+
},
|
|
1522
|
+
"../managers/docker.js": {
|
|
1523
|
+
addSkippedSrcFiles: sinon.stub(),
|
|
1524
|
+
exportArchive: sinon.stub(),
|
|
1525
|
+
exportImage,
|
|
1526
|
+
getPkgPathList,
|
|
1527
|
+
parseImageName: sinon.stub(),
|
|
1528
|
+
},
|
|
1529
|
+
});
|
|
1530
|
+
const bomNSData = await createBomMocked(tempDir, {
|
|
1531
|
+
failOnError: true,
|
|
1532
|
+
installDeps: false,
|
|
1533
|
+
multiProject: false,
|
|
1534
|
+
projectType: ["docker"],
|
|
1535
|
+
specVersion: 1.6,
|
|
1536
|
+
});
|
|
1537
|
+
sinon.assert.notCalled(exportImage);
|
|
1538
|
+
sinon.assert.calledOnce(getPkgPathList);
|
|
1539
|
+
assert.ok(bomNSData?.bomJson);
|
|
1540
|
+
assert.strictEqual(bomNSData?.parentComponent?.type, "container");
|
|
1541
|
+
} finally {
|
|
1542
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
it("prefers an all-layers subdirectory when scanning staged rootfs inputs", async () => {
|
|
1547
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-rootfs-"));
|
|
1548
|
+
const allLayersDir = join(tempDir, "all-layers");
|
|
1549
|
+
const exportImage = sinon.stub().resolves(undefined);
|
|
1550
|
+
const getPkgPathList = sinon.stub().returns([]);
|
|
1551
|
+
mkdirSync(allLayersDir);
|
|
1552
|
+
try {
|
|
1553
|
+
const { createBom: createBomMocked } = await esmock("./index.js", {
|
|
1554
|
+
"../managers/binary.js": {
|
|
1555
|
+
executeOsQuery: sinon.stub(),
|
|
1556
|
+
getBinaryBom: sinon.stub(),
|
|
1557
|
+
getDotnetSlices: sinon.stub(),
|
|
1558
|
+
getOSPackages: sinon.stub().resolves({
|
|
1559
|
+
allTypes: [],
|
|
1560
|
+
binPaths: [],
|
|
1561
|
+
bundledRuntimes: [],
|
|
1562
|
+
bundledSdks: [],
|
|
1563
|
+
dependenciesList: [],
|
|
1564
|
+
executables: [],
|
|
1565
|
+
osPackages: [],
|
|
1566
|
+
sharedLibs: [],
|
|
1567
|
+
}),
|
|
1568
|
+
},
|
|
1569
|
+
"../managers/docker.js": {
|
|
1570
|
+
addSkippedSrcFiles: sinon.stub(),
|
|
1571
|
+
exportArchive: sinon.stub(),
|
|
1572
|
+
exportImage,
|
|
1573
|
+
getPkgPathList,
|
|
1574
|
+
parseImageName: sinon.stub(),
|
|
1575
|
+
},
|
|
1576
|
+
});
|
|
1577
|
+
await createBomMocked(tempDir, {
|
|
1578
|
+
failOnError: true,
|
|
1579
|
+
installDeps: false,
|
|
1580
|
+
multiProject: false,
|
|
1581
|
+
projectType: ["docker"],
|
|
1582
|
+
specVersion: 1.6,
|
|
1583
|
+
});
|
|
1584
|
+
sinon.assert.calledOnce(getPkgPathList);
|
|
1585
|
+
assert.strictEqual(
|
|
1586
|
+
getPkgPathList.firstCall.args[0].allLayersDir,
|
|
1587
|
+
tempDir,
|
|
1588
|
+
);
|
|
1589
|
+
assert.strictEqual(
|
|
1590
|
+
getPkgPathList.firstCall.args[0].allLayersExplodedDir,
|
|
1591
|
+
allLayersDir,
|
|
1592
|
+
);
|
|
1593
|
+
} finally {
|
|
1594
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
657
1597
|
});
|
|
658
1598
|
|
|
659
1599
|
describe("createBom() cargo cache support", () => {
|
|
@@ -848,6 +1788,387 @@ checksum = "${"a".repeat(64)}"
|
|
|
848
1788
|
});
|
|
849
1789
|
});
|
|
850
1790
|
|
|
1791
|
+
if (process.platform !== "win32") {
|
|
1792
|
+
describe("HBOM support", () => {
|
|
1793
|
+
it("delegates hbom project types to the hbom helper", async () => {
|
|
1794
|
+
const actualHbomHelpers = await import("../helpers/hbom.js");
|
|
1795
|
+
const createHbomDocument = sinon.stub().resolves({
|
|
1796
|
+
bomFormat: "CycloneDX",
|
|
1797
|
+
components: [],
|
|
1798
|
+
metadata: {
|
|
1799
|
+
component: {
|
|
1800
|
+
name: "Demo Board",
|
|
1801
|
+
type: "device",
|
|
1802
|
+
version: "rev-a",
|
|
1803
|
+
},
|
|
1804
|
+
},
|
|
1805
|
+
specVersion: "1.7",
|
|
1806
|
+
});
|
|
1807
|
+
const { createBom: createBomMocked } = await esmock("./index.js", {
|
|
1808
|
+
"../helpers/hbom.js": {
|
|
1809
|
+
...actualHbomHelpers,
|
|
1810
|
+
createHbomDocument,
|
|
1811
|
+
},
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
const bomNSData = await createBomMocked(repoDir, {
|
|
1815
|
+
projectType: ["hbom"],
|
|
1816
|
+
specVersion: 1.7,
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
sinon.assert.calledOnce(createHbomDocument);
|
|
1820
|
+
assert.strictEqual(
|
|
1821
|
+
bomNSData?.bomJson?.metadata?.component?.name,
|
|
1822
|
+
"Demo Board",
|
|
1823
|
+
);
|
|
1824
|
+
assert.strictEqual(bomNSData?.parentComponent?.type, "device");
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
it("supports dry-run mode for hbom project types in the main CLI flow", async () => {
|
|
1828
|
+
setDryRunMode(true);
|
|
1829
|
+
resetRecordedActivities();
|
|
1830
|
+
|
|
1831
|
+
try {
|
|
1832
|
+
const bomNSData = await createBom(repoDir, {
|
|
1833
|
+
projectType: ["hbom"],
|
|
1834
|
+
specVersion: 1.7,
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
assert.strictEqual(bomNSData?.bomJson?.bomFormat, "CycloneDX");
|
|
1838
|
+
assert.strictEqual(bomNSData?.bomJson?.specVersion, "1.7");
|
|
1839
|
+
assert.ok(Array.isArray(bomNSData?.bomJson?.components));
|
|
1840
|
+
assert.ok(bomNSData?.bomJson?.components.length >= 1);
|
|
1841
|
+
assert.ok(Array.isArray(bomNSData?.dependencies));
|
|
1842
|
+
} finally {
|
|
1843
|
+
setDryRunMode(false);
|
|
1844
|
+
resetRecordedActivities();
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
it("shows dedicated hbom command help", () => {
|
|
1849
|
+
const result = spawnSync(
|
|
1850
|
+
process.execPath,
|
|
1851
|
+
[join(repoDir, "bin", "hbom.js"), "--help"],
|
|
1852
|
+
{
|
|
1853
|
+
cwd: repoDir,
|
|
1854
|
+
encoding: "utf8",
|
|
1855
|
+
env: buildMinimalCliEnv(),
|
|
1856
|
+
},
|
|
1857
|
+
);
|
|
1858
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
1859
|
+
|
|
1860
|
+
assert.strictEqual(result.status, 0);
|
|
1861
|
+
assert.match(output, /Output file\.\s+Default\s+hbom\.json/u);
|
|
1862
|
+
assert.match(output, /--include-runtime/u);
|
|
1863
|
+
assert.match(output, /--privileged/u);
|
|
1864
|
+
assert.match(output, /diagnostics/u);
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
it("uses the invoked hbom binary name in help output", () => {
|
|
1868
|
+
const tempDir = mkdtempSync(join(repoDir, ".cdxgen-hbom-help-name-"));
|
|
1869
|
+
try {
|
|
1870
|
+
const slimScript = join(tempDir, "hbom-slim");
|
|
1871
|
+
copyFileSync(join(repoDir, "bin", "hbom.js"), slimScript);
|
|
1872
|
+
const result = spawnSync(process.execPath, [slimScript, "--help"], {
|
|
1873
|
+
cwd: tempDir,
|
|
1874
|
+
encoding: "utf8",
|
|
1875
|
+
env: buildMinimalCliEnv(),
|
|
1876
|
+
});
|
|
1877
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
1878
|
+
|
|
1879
|
+
assert.strictEqual(result.status, 0);
|
|
1880
|
+
assert.match(output, /hbom-slim \[command\] \[options\]/u);
|
|
1881
|
+
} finally {
|
|
1882
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
it("fails early when hbom include-runtime lacks osquery support", () => {
|
|
1887
|
+
const emptyPluginsDir = mkdtempSync(
|
|
1888
|
+
join(tmpdir(), "cdxgen-empty-plugins-"),
|
|
1889
|
+
);
|
|
1890
|
+
try {
|
|
1891
|
+
const result = spawnSync(
|
|
1892
|
+
process.execPath,
|
|
1893
|
+
[join(repoDir, "bin", "hbom.js"), "--include-runtime"],
|
|
1894
|
+
{
|
|
1895
|
+
cwd: repoDir,
|
|
1896
|
+
encoding: "utf8",
|
|
1897
|
+
env: buildMinimalCliEnv({
|
|
1898
|
+
CDXGEN_PLUGINS_DIR: emptyPluginsDir,
|
|
1899
|
+
}),
|
|
1900
|
+
},
|
|
1901
|
+
);
|
|
1902
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
1903
|
+
|
|
1904
|
+
assert.strictEqual(result.status, 1);
|
|
1905
|
+
assert.match(output, /--include-runtime/u);
|
|
1906
|
+
assert.match(output, /cdxgen-plugins-bin/u);
|
|
1907
|
+
assert.match(
|
|
1908
|
+
output,
|
|
1909
|
+
/'hbom' is the bundled option required for '--include-runtime' support/u,
|
|
1910
|
+
);
|
|
1911
|
+
assert.doesNotMatch(output, /About to generate OBOM/u);
|
|
1912
|
+
} finally {
|
|
1913
|
+
rmSync(emptyPluginsDir, { force: true, recursive: true });
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
it("guides hbom-slim users to the standard binary for include-runtime", () => {
|
|
1918
|
+
const tempDir = mkdtempSync(
|
|
1919
|
+
join(repoDir, ".cdxgen-hbom-runtime-check-"),
|
|
1920
|
+
);
|
|
1921
|
+
const emptyPluginsDir = mkdtempSync(
|
|
1922
|
+
join(tmpdir(), "cdxgen-empty-plugins-"),
|
|
1923
|
+
);
|
|
1924
|
+
try {
|
|
1925
|
+
const slimScript = join(tempDir, "hbom-slim");
|
|
1926
|
+
copyFileSync(join(repoDir, "bin", "hbom.js"), slimScript);
|
|
1927
|
+
const result = spawnSync(
|
|
1928
|
+
process.execPath,
|
|
1929
|
+
[slimScript, "--include-runtime"],
|
|
1930
|
+
{
|
|
1931
|
+
cwd: tempDir,
|
|
1932
|
+
encoding: "utf8",
|
|
1933
|
+
env: buildMinimalCliEnv({
|
|
1934
|
+
CDXGEN_PLUGINS_DIR: emptyPluginsDir,
|
|
1935
|
+
}),
|
|
1936
|
+
},
|
|
1937
|
+
);
|
|
1938
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
1939
|
+
|
|
1940
|
+
assert.strictEqual(result.status, 1);
|
|
1941
|
+
assert.match(output, /'hbom-slim' is hardware-only by default/u);
|
|
1942
|
+
assert.match(
|
|
1943
|
+
output,
|
|
1944
|
+
/Use 'hbom' for bundled '--include-runtime' support/u,
|
|
1945
|
+
);
|
|
1946
|
+
} finally {
|
|
1947
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
1948
|
+
rmSync(emptyPluginsDir, { force: true, recursive: true });
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
it("supports the hbom diagnostics subcommand for existing BOM files", () => {
|
|
1953
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-hbom-diagnostics-"));
|
|
1954
|
+
try {
|
|
1955
|
+
const inputFile = join(tempDir, "hbom.json");
|
|
1956
|
+
writeFileSync(
|
|
1957
|
+
inputFile,
|
|
1958
|
+
JSON.stringify({
|
|
1959
|
+
bomFormat: "CycloneDX",
|
|
1960
|
+
components: [],
|
|
1961
|
+
metadata: {
|
|
1962
|
+
component: {
|
|
1963
|
+
name: "demo-host",
|
|
1964
|
+
properties: [
|
|
1965
|
+
{ name: "cdx:hbom:platform", value: "linux" },
|
|
1966
|
+
{ name: "cdx:hbom:architecture", value: "amd64" },
|
|
1967
|
+
],
|
|
1968
|
+
type: "device",
|
|
1969
|
+
},
|
|
1970
|
+
},
|
|
1971
|
+
properties: [
|
|
1972
|
+
{ name: "cdx:hbom:collectorProfile", value: "linux-amd64-v1" },
|
|
1973
|
+
{
|
|
1974
|
+
name: "cdx:hbom:evidence:commandDiagnosticCount",
|
|
1975
|
+
value: "2",
|
|
1976
|
+
},
|
|
1977
|
+
{
|
|
1978
|
+
name: "cdx:hbom:evidence:commandDiagnostic",
|
|
1979
|
+
value: JSON.stringify({
|
|
1980
|
+
command: "lsusb",
|
|
1981
|
+
installHint:
|
|
1982
|
+
"Command not found: install the Linux package providing lsusb (commonly `usbutils`).",
|
|
1983
|
+
issue: "missing-command",
|
|
1984
|
+
message: "lsusb failed with missing-command",
|
|
1985
|
+
}),
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
name: "cdx:hbom:evidence:commandDiagnostic",
|
|
1989
|
+
value: JSON.stringify({
|
|
1990
|
+
command: "drm_info",
|
|
1991
|
+
issue: "permission-denied",
|
|
1992
|
+
message: "drm_info failed with permission-denied",
|
|
1993
|
+
privilegeHint:
|
|
1994
|
+
"Retry with --privileged to allow a non-interactive sudo attempt for permission-sensitive Linux commands.",
|
|
1995
|
+
}),
|
|
1996
|
+
},
|
|
1997
|
+
],
|
|
1998
|
+
specVersion: "1.7",
|
|
1999
|
+
version: 1,
|
|
2000
|
+
}),
|
|
2001
|
+
);
|
|
2002
|
+
const result = spawnSync(
|
|
2003
|
+
process.execPath,
|
|
2004
|
+
[
|
|
2005
|
+
join(repoDir, "bin", "hbom.js"),
|
|
2006
|
+
"diagnostics",
|
|
2007
|
+
"--input",
|
|
2008
|
+
inputFile,
|
|
2009
|
+
],
|
|
2010
|
+
{
|
|
2011
|
+
cwd: tempDir,
|
|
2012
|
+
encoding: "utf8",
|
|
2013
|
+
env: buildMinimalCliEnv(),
|
|
2014
|
+
},
|
|
2015
|
+
);
|
|
2016
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
2017
|
+
|
|
2018
|
+
assert.strictEqual(result.status, 0);
|
|
2019
|
+
assert.match(output, /HBOM diagnostics summary/u);
|
|
2020
|
+
assert.match(output, /Missing commands:\n- lsusb/u);
|
|
2021
|
+
assert.match(output, /Permission-sensitive enrichments:/u);
|
|
2022
|
+
assert.match(output, /--privileged/u);
|
|
2023
|
+
} finally {
|
|
2024
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
it("supports dry-run mode in the dedicated hbom command", () => {
|
|
2029
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-hbom-dry-run-"));
|
|
2030
|
+
try {
|
|
2031
|
+
const outputFile = join(tempDir, "hbom.json");
|
|
2032
|
+
const result = spawnSync(
|
|
2033
|
+
process.execPath,
|
|
2034
|
+
[join(repoDir, "bin", "hbom.js"), "--dry-run"],
|
|
2035
|
+
{
|
|
2036
|
+
cwd: tempDir,
|
|
2037
|
+
encoding: "utf8",
|
|
2038
|
+
env: buildMinimalCliEnv(),
|
|
2039
|
+
},
|
|
2040
|
+
);
|
|
2041
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
2042
|
+
|
|
2043
|
+
assert.strictEqual(result.status, 0);
|
|
2044
|
+
assert.match(output, /cdxgen dry-run activity summary/u);
|
|
2045
|
+
assert.strictEqual(existsSync(outputFile), false);
|
|
2046
|
+
} finally {
|
|
2047
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
it("rejects mixed hbom and sbom project types in the main CLI", () => {
|
|
2052
|
+
const result = spawnSync(
|
|
2053
|
+
process.execPath,
|
|
2054
|
+
[
|
|
2055
|
+
join(repoDir, "bin", "cdxgen.js"),
|
|
2056
|
+
"-t",
|
|
2057
|
+
"hbom",
|
|
2058
|
+
"-t",
|
|
2059
|
+
"js",
|
|
2060
|
+
"--no-banner",
|
|
2061
|
+
],
|
|
2062
|
+
{
|
|
2063
|
+
cwd: repoDir,
|
|
2064
|
+
encoding: "utf8",
|
|
2065
|
+
env: buildMinimalCliEnv(),
|
|
2066
|
+
},
|
|
2067
|
+
);
|
|
2068
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
2069
|
+
|
|
2070
|
+
assert.strictEqual(result.status, 1);
|
|
2071
|
+
assert.match(output, /HBOM project types cannot be mixed/u);
|
|
2072
|
+
});
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
describe("createBom() Collider lock support", () => {
|
|
2077
|
+
it("preserves Collider integrity metadata and dependency nodes in the BOM", async () => {
|
|
2078
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-collider-"));
|
|
2079
|
+
writeFileSync(
|
|
2080
|
+
join(tmpDir, "collider.lock"),
|
|
2081
|
+
JSON.stringify(
|
|
2082
|
+
{
|
|
2083
|
+
version: 1,
|
|
2084
|
+
dependencies: {
|
|
2085
|
+
fmt: {
|
|
2086
|
+
version: "11.0.2",
|
|
2087
|
+
wrap_hash: `sha256:${"a".repeat(64)}`,
|
|
2088
|
+
origin: "https://packages.example.com/collider/v2/",
|
|
2089
|
+
},
|
|
2090
|
+
},
|
|
2091
|
+
packages: {
|
|
2092
|
+
fast_float: {
|
|
2093
|
+
version: "8.0.2",
|
|
2094
|
+
wrap_hash: `sha256:${"b".repeat(64)}`,
|
|
2095
|
+
origin: "https://wrapdb.mesonbuild.com/v2/",
|
|
2096
|
+
},
|
|
2097
|
+
},
|
|
2098
|
+
},
|
|
2099
|
+
null,
|
|
2100
|
+
2,
|
|
2101
|
+
),
|
|
2102
|
+
);
|
|
2103
|
+
try {
|
|
2104
|
+
const bomNSData = await createBom(tmpDir, {
|
|
2105
|
+
failOnError: true,
|
|
2106
|
+
installDeps: false,
|
|
2107
|
+
multiProject: false,
|
|
2108
|
+
projectType: ["collider"],
|
|
2109
|
+
specVersion: 1.7,
|
|
2110
|
+
});
|
|
2111
|
+
const bomJson = bomNSData?.bomJson || {};
|
|
2112
|
+
const fmtComponent = (bomJson.components || []).find(
|
|
2113
|
+
(component) => component.name === "fmt",
|
|
2114
|
+
);
|
|
2115
|
+
const transitiveComponent = (bomJson.components || []).find(
|
|
2116
|
+
(component) => component.name === "fast_float",
|
|
2117
|
+
);
|
|
2118
|
+
assert.ok(fmtComponent);
|
|
2119
|
+
assert.ok(transitiveComponent);
|
|
2120
|
+
assert.deepStrictEqual(
|
|
2121
|
+
getProp(fmtComponent, "cdx:collider:origin"),
|
|
2122
|
+
"https://packages.example.com/collider/v2/",
|
|
2123
|
+
);
|
|
2124
|
+
assert.deepStrictEqual(
|
|
2125
|
+
getProp(fmtComponent, "cdx:collider:hasWrapHash"),
|
|
2126
|
+
"true",
|
|
2127
|
+
);
|
|
2128
|
+
assert.deepStrictEqual(
|
|
2129
|
+
getProp(transitiveComponent, "cdx:collider:dependencyKind"),
|
|
2130
|
+
"transitive",
|
|
2131
|
+
);
|
|
2132
|
+
assert.deepStrictEqual(fmtComponent.hashes, [
|
|
2133
|
+
{
|
|
2134
|
+
alg: "SHA-256",
|
|
2135
|
+
content: "a".repeat(64),
|
|
2136
|
+
},
|
|
2137
|
+
]);
|
|
2138
|
+
assert.deepStrictEqual(fmtComponent.externalReferences, [
|
|
2139
|
+
{
|
|
2140
|
+
type: "distribution",
|
|
2141
|
+
url: "https://packages.example.com/collider/v2/",
|
|
2142
|
+
},
|
|
2143
|
+
]);
|
|
2144
|
+
const parentDependency = (bomJson.dependencies || []).find(
|
|
2145
|
+
(dependency) =>
|
|
2146
|
+
dependency.ref === bomJson.metadata.component["bom-ref"],
|
|
2147
|
+
);
|
|
2148
|
+
assert.ok(parentDependency);
|
|
2149
|
+
assert.deepStrictEqual(parentDependency.dependsOn, [
|
|
2150
|
+
"pkg:generic/fmt@11.0.2",
|
|
2151
|
+
]);
|
|
2152
|
+
assert.ok(
|
|
2153
|
+
(bomJson.dependencies || []).some(
|
|
2154
|
+
(dependency) =>
|
|
2155
|
+
dependency.ref === "pkg:generic/fmt@11.0.2" &&
|
|
2156
|
+
dependency.dependsOn.length === 0,
|
|
2157
|
+
),
|
|
2158
|
+
);
|
|
2159
|
+
assert.ok(
|
|
2160
|
+
(bomJson.dependencies || []).some(
|
|
2161
|
+
(dependency) =>
|
|
2162
|
+
dependency.ref === "pkg:generic/fast_float@8.0.2" &&
|
|
2163
|
+
dependency.dependsOn.length === 0,
|
|
2164
|
+
),
|
|
2165
|
+
);
|
|
2166
|
+
} finally {
|
|
2167
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
});
|
|
2171
|
+
|
|
851
2172
|
describe("createBom() MCP inventory support", () => {
|
|
852
2173
|
it("catalogs MCP services, primitives, and audit findings for JavaScript projects", async () => {
|
|
853
2174
|
const options = {
|
|
@@ -963,7 +2284,129 @@ checksum = "${"a".repeat(64)}"
|
|
|
963
2284
|
);
|
|
964
2285
|
});
|
|
965
2286
|
|
|
966
|
-
it("
|
|
2287
|
+
it("flags disabled setup caches for npm, Python, and Cargo fixtures", async () => {
|
|
2288
|
+
const options = {
|
|
2289
|
+
bomAudit: true,
|
|
2290
|
+
bomAuditCategories: "ci-permission",
|
|
2291
|
+
bomAuditMinSeverity: "low",
|
|
2292
|
+
failOnError: true,
|
|
2293
|
+
includeFormulation: true,
|
|
2294
|
+
installDeps: false,
|
|
2295
|
+
multiProject: true,
|
|
2296
|
+
projectType: ["js", "python", "cargo", "github"],
|
|
2297
|
+
specVersion: 1.7,
|
|
2298
|
+
};
|
|
2299
|
+
const bomNSData = await createBom(cacheDisableFixtureDir, options);
|
|
2300
|
+
const processedBomNSData = postProcess(
|
|
2301
|
+
bomNSData,
|
|
2302
|
+
options,
|
|
2303
|
+
cacheDisableFixtureDir,
|
|
2304
|
+
);
|
|
2305
|
+
const bomJson = processedBomNSData?.bomJson || {};
|
|
2306
|
+
const setupNodeComponent = (bomJson.components || []).find(
|
|
2307
|
+
(component) =>
|
|
2308
|
+
getProp(component, "cdx:github:action:uses") ===
|
|
2309
|
+
"actions/setup-node@v4",
|
|
2310
|
+
);
|
|
2311
|
+
const setupPythonComponent = (bomJson.components || []).find(
|
|
2312
|
+
(component) =>
|
|
2313
|
+
getProp(component, "cdx:github:action:uses") ===
|
|
2314
|
+
"actions/setup-python@v5",
|
|
2315
|
+
);
|
|
2316
|
+
const setupRustComponent = (bomJson.components || []).find(
|
|
2317
|
+
(component) =>
|
|
2318
|
+
getProp(component, "cdx:github:action:uses") ===
|
|
2319
|
+
"moonrepo/setup-rust@v1",
|
|
2320
|
+
);
|
|
2321
|
+
const npmComponent = (bomJson.components || []).find((component) =>
|
|
2322
|
+
component.purl?.startsWith("pkg:npm/left-pad@1.3.0"),
|
|
2323
|
+
);
|
|
2324
|
+
const pythonComponent = (bomJson.components || []).find((component) =>
|
|
2325
|
+
component.purl?.startsWith("pkg:pypi/anyio@4.6.0"),
|
|
2326
|
+
);
|
|
2327
|
+
const cargoComponent = (bomJson.components || []).find(
|
|
2328
|
+
(component) =>
|
|
2329
|
+
component.name === "git-crate" &&
|
|
2330
|
+
getProp(component, "cdx:cargo:git") ===
|
|
2331
|
+
"https://github.com/acme/git-crate.git",
|
|
2332
|
+
);
|
|
2333
|
+
const cargoRunComponent = (bomJson.components || []).find((component) =>
|
|
2334
|
+
component.properties?.some(
|
|
2335
|
+
(property) =>
|
|
2336
|
+
property.name === "cdx:github:step:cargoSubcommands" &&
|
|
2337
|
+
property.value === "build",
|
|
2338
|
+
),
|
|
2339
|
+
);
|
|
2340
|
+
assert.ok(setupNodeComponent, "expected setup-node workflow component");
|
|
2341
|
+
assert.ok(
|
|
2342
|
+
setupPythonComponent,
|
|
2343
|
+
"expected setup-python workflow component",
|
|
2344
|
+
);
|
|
2345
|
+
assert.ok(setupRustComponent, "expected setup-rust workflow component");
|
|
2346
|
+
assert.strictEqual(
|
|
2347
|
+
getProp(setupNodeComponent, "cdx:github:action:disablesBuildCache"),
|
|
2348
|
+
"true",
|
|
2349
|
+
);
|
|
2350
|
+
assert.strictEqual(
|
|
2351
|
+
getProp(setupPythonComponent, "cdx:github:action:disablesBuildCache"),
|
|
2352
|
+
"true",
|
|
2353
|
+
);
|
|
2354
|
+
assert.strictEqual(
|
|
2355
|
+
getProp(setupRustComponent, "cdx:github:action:disablesBuildCache"),
|
|
2356
|
+
"true",
|
|
2357
|
+
);
|
|
2358
|
+
assert.strictEqual(
|
|
2359
|
+
getProp(setupRustComponent, "cdx:github:action:buildCacheEcosystem"),
|
|
2360
|
+
"cargo",
|
|
2361
|
+
);
|
|
2362
|
+
assert.strictEqual(
|
|
2363
|
+
getProp(setupRustComponent, "cdx:github:action:buildCacheDisableInput"),
|
|
2364
|
+
"cache",
|
|
2365
|
+
);
|
|
2366
|
+
assert.ok(npmComponent, "expected npm dependency from package-lock");
|
|
2367
|
+
assert.ok(pythonComponent, "expected PyPI dependency from uv.lock");
|
|
2368
|
+
assert.ok(cargoComponent, "expected Cargo dependency from Cargo.toml");
|
|
2369
|
+
assert.ok(cargoRunComponent, "expected Cargo run step component");
|
|
2370
|
+
assert.strictEqual(
|
|
2371
|
+
getProp(npmComponent, "cdx:npm:manifestSourceType"),
|
|
2372
|
+
"url",
|
|
2373
|
+
);
|
|
2374
|
+
assert.strictEqual(
|
|
2375
|
+
getProp(pythonComponent, "cdx:pypi:manifestSourceType"),
|
|
2376
|
+
"url",
|
|
2377
|
+
);
|
|
2378
|
+
assert.strictEqual(
|
|
2379
|
+
getProp(cargoComponent, "cdx:cargo:git"),
|
|
2380
|
+
"https://github.com/acme/git-crate.git",
|
|
2381
|
+
);
|
|
2382
|
+
assert.strictEqual(
|
|
2383
|
+
getProp(cargoComponent, "cdx:cargo:gitBranch"),
|
|
2384
|
+
"main",
|
|
2385
|
+
);
|
|
2386
|
+
assert.strictEqual(
|
|
2387
|
+
getProp(cargoRunComponent, "cdx:github:step:usesCargo"),
|
|
2388
|
+
"true",
|
|
2389
|
+
);
|
|
2390
|
+
|
|
2391
|
+
const findings = await auditBom(bomJson, {
|
|
2392
|
+
bomAuditCategories: "ci-permission",
|
|
2393
|
+
bomAuditMinSeverity: "low",
|
|
2394
|
+
});
|
|
2395
|
+
assert.ok(
|
|
2396
|
+
findings.some((finding) => finding.ruleId === "CI-022"),
|
|
2397
|
+
"expected npm disabled cache finding",
|
|
2398
|
+
);
|
|
2399
|
+
assert.ok(
|
|
2400
|
+
findings.some((finding) => finding.ruleId === "CI-023"),
|
|
2401
|
+
"expected Python disabled cache finding",
|
|
2402
|
+
);
|
|
2403
|
+
assert.ok(
|
|
2404
|
+
findings.some((finding) => finding.ruleId === "CI-024"),
|
|
2405
|
+
"expected Cargo disabled cache finding",
|
|
2406
|
+
);
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
it("requires explicit opt-in for AI inventory in js and python scans", async () => {
|
|
967
2410
|
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-ai-inventory-"));
|
|
968
2411
|
mkdirSync(join(tmpDir, ".claude", "skills", "release"), {
|
|
969
2412
|
recursive: true,
|
|
@@ -1081,88 +2524,158 @@ checksum = "${"a".repeat(64)}"
|
|
|
1081
2524
|
tmpDir,
|
|
1082
2525
|
).bomJson;
|
|
1083
2526
|
assert.ok(
|
|
1084
|
-
(jsBomJson.components || []).some(
|
|
1085
|
-
(
|
|
1086
|
-
getProp(component, "cdx:file:kind")
|
|
1087
|
-
|
|
2527
|
+
!(jsBomJson.components || []).some((component) =>
|
|
2528
|
+
["agent-instructions", "mcp-config", "skill-file"].includes(
|
|
2529
|
+
getProp(component, "cdx:file:kind"),
|
|
2530
|
+
),
|
|
2531
|
+
),
|
|
2532
|
+
"did not expect AI inventory components in js scan without opt-in",
|
|
2533
|
+
);
|
|
2534
|
+
assert.ok(
|
|
2535
|
+
!(jsBomJson.services || []).some((service) =>
|
|
2536
|
+
service.properties?.some((property) =>
|
|
2537
|
+
property.name.startsWith("cdx:mcp:"),
|
|
2538
|
+
),
|
|
2539
|
+
),
|
|
2540
|
+
"did not expect MCP services in js scan without opt-in",
|
|
2541
|
+
);
|
|
2542
|
+
|
|
2543
|
+
const dockerOptions = {
|
|
2544
|
+
...baseOptions,
|
|
2545
|
+
projectType: ["js", "docker"],
|
|
2546
|
+
};
|
|
2547
|
+
const dockerBomJson = postProcess(
|
|
2548
|
+
await createNodejsBom(tmpDir, dockerOptions),
|
|
2549
|
+
dockerOptions,
|
|
2550
|
+
tmpDir,
|
|
2551
|
+
).bomJson;
|
|
2552
|
+
assert.ok(
|
|
2553
|
+
!(dockerBomJson.components || []).some((component) =>
|
|
2554
|
+
["agent-instructions", "mcp-config", "skill-file"].includes(
|
|
2555
|
+
getProp(component, "cdx:file:kind"),
|
|
2556
|
+
),
|
|
1088
2557
|
),
|
|
1089
|
-
"
|
|
2558
|
+
"did not expect AI inventory components in docker js scan without opt-in",
|
|
1090
2559
|
);
|
|
2560
|
+
|
|
2561
|
+
const exactAiSkillOptions = {
|
|
2562
|
+
...baseOptions,
|
|
2563
|
+
projectType: ["ai-skill"],
|
|
2564
|
+
};
|
|
2565
|
+
const aiSkillBomJson = postProcess(
|
|
2566
|
+
await createBom(tmpDir, exactAiSkillOptions),
|
|
2567
|
+
exactAiSkillOptions,
|
|
2568
|
+
tmpDir,
|
|
2569
|
+
).bomJson;
|
|
1091
2570
|
assert.ok(
|
|
1092
|
-
(
|
|
2571
|
+
(aiSkillBomJson.components || []).some(
|
|
1093
2572
|
(component) =>
|
|
1094
2573
|
component.name === "CLAUDE.md" &&
|
|
1095
2574
|
getProp(component, "cdx:file:kind") === "agent-instructions",
|
|
1096
2575
|
),
|
|
1097
|
-
"expected CLAUDE.md in
|
|
2576
|
+
"expected CLAUDE.md in exact ai-skill scan",
|
|
2577
|
+
);
|
|
2578
|
+
assert.ok(
|
|
2579
|
+
!(aiSkillBomJson.components || []).some(
|
|
2580
|
+
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
2581
|
+
),
|
|
2582
|
+
"did not expect MCP configs in exact ai-skill scan",
|
|
2583
|
+
);
|
|
2584
|
+
|
|
2585
|
+
const optedInJsOptions = {
|
|
2586
|
+
...baseOptions,
|
|
2587
|
+
projectType: ["js", "ai-skill", "mcp"],
|
|
2588
|
+
};
|
|
2589
|
+
const optedInJsBomJson = postProcess(
|
|
2590
|
+
await createBom(tmpDir, optedInJsOptions),
|
|
2591
|
+
optedInJsOptions,
|
|
2592
|
+
tmpDir,
|
|
2593
|
+
).bomJson;
|
|
2594
|
+
assert.ok(
|
|
2595
|
+
(optedInJsBomJson.components || []).some(
|
|
2596
|
+
(component) =>
|
|
2597
|
+
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
2598
|
+
getProp(component, "cdx:skill:name") === "release",
|
|
2599
|
+
),
|
|
2600
|
+
"expected skill file in opted-in js scan",
|
|
1098
2601
|
);
|
|
1099
2602
|
assert.ok(
|
|
1100
|
-
(
|
|
2603
|
+
(optedInJsBomJson.components || []).some(
|
|
1101
2604
|
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1102
2605
|
),
|
|
1103
|
-
"expected MCP config in js scan",
|
|
2606
|
+
"expected MCP config in opted-in js scan",
|
|
1104
2607
|
);
|
|
1105
2608
|
assert.ok(
|
|
1106
|
-
(
|
|
2609
|
+
(optedInJsBomJson.services || []).some(
|
|
1107
2610
|
(service) =>
|
|
1108
2611
|
service.name === "releaseDocs" &&
|
|
1109
2612
|
getProp(service, "cdx:mcp:inventorySource") === "config-file",
|
|
1110
2613
|
),
|
|
1111
|
-
"expected MCP config service in js scan",
|
|
2614
|
+
"expected MCP config service in opted-in js scan",
|
|
1112
2615
|
);
|
|
1113
2616
|
|
|
1114
|
-
const
|
|
2617
|
+
const auditAliasJsOptions = {
|
|
1115
2618
|
...baseOptions,
|
|
1116
|
-
|
|
2619
|
+
bomAuditCategories: "ai-inventory",
|
|
2620
|
+
projectType: ["js"],
|
|
1117
2621
|
};
|
|
1118
|
-
const
|
|
1119
|
-
await
|
|
1120
|
-
|
|
2622
|
+
const auditAliasJsBomJson = postProcess(
|
|
2623
|
+
await createBom(tmpDir, auditAliasJsOptions),
|
|
2624
|
+
auditAliasJsOptions,
|
|
1121
2625
|
tmpDir,
|
|
1122
2626
|
).bomJson;
|
|
1123
2627
|
assert.ok(
|
|
1124
|
-
(
|
|
2628
|
+
(auditAliasJsBomJson.components || []).some(
|
|
1125
2629
|
(component) =>
|
|
1126
2630
|
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
1127
2631
|
getProp(component, "cdx:skill:name") === "release",
|
|
1128
2632
|
),
|
|
1129
|
-
"expected skill file in
|
|
2633
|
+
"expected skill file in ai-inventory audit-category js scan",
|
|
1130
2634
|
);
|
|
1131
2635
|
assert.ok(
|
|
1132
|
-
(
|
|
2636
|
+
(auditAliasJsBomJson.components || []).some(
|
|
1133
2637
|
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1134
2638
|
),
|
|
1135
|
-
"expected MCP config in
|
|
2639
|
+
"expected MCP config in ai-inventory audit-category js scan",
|
|
2640
|
+
);
|
|
2641
|
+
assert.ok(
|
|
2642
|
+
(auditAliasJsBomJson.services || []).some(
|
|
2643
|
+
(service) =>
|
|
2644
|
+
service.name === "releaseDocs" &&
|
|
2645
|
+
getProp(service, "cdx:mcp:inventorySource") === "config-file",
|
|
2646
|
+
),
|
|
2647
|
+
"expected MCP config service in ai-inventory audit-category js scan",
|
|
1136
2648
|
);
|
|
1137
2649
|
|
|
1138
|
-
const
|
|
2650
|
+
const auditAgentJsOptions = {
|
|
1139
2651
|
...baseOptions,
|
|
1140
|
-
|
|
2652
|
+
bomAuditCategories: "ai-agent",
|
|
2653
|
+
projectType: ["js"],
|
|
1141
2654
|
};
|
|
1142
|
-
const
|
|
1143
|
-
await createBom(tmpDir,
|
|
1144
|
-
|
|
2655
|
+
const auditAgentJsBomJson = postProcess(
|
|
2656
|
+
await createBom(tmpDir, auditAgentJsOptions),
|
|
2657
|
+
auditAgentJsOptions,
|
|
1145
2658
|
tmpDir,
|
|
1146
2659
|
).bomJson;
|
|
1147
2660
|
assert.ok(
|
|
1148
|
-
(
|
|
2661
|
+
(auditAgentJsBomJson.components || []).some(
|
|
1149
2662
|
(component) =>
|
|
1150
|
-
component
|
|
1151
|
-
getProp(component, "cdx:
|
|
2663
|
+
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
2664
|
+
getProp(component, "cdx:skill:name") === "release",
|
|
1152
2665
|
),
|
|
1153
|
-
"expected
|
|
2666
|
+
"expected skill file in ai-agent audit-category js scan",
|
|
1154
2667
|
);
|
|
1155
2668
|
assert.ok(
|
|
1156
|
-
!(
|
|
2669
|
+
!(auditAgentJsBomJson.components || []).some(
|
|
1157
2670
|
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1158
2671
|
),
|
|
1159
|
-
"did not expect MCP
|
|
2672
|
+
"did not expect MCP config in ai-agent audit-category js scan",
|
|
1160
2673
|
);
|
|
1161
2674
|
|
|
1162
2675
|
const filteredOptions = {
|
|
1163
2676
|
...baseOptions,
|
|
1164
2677
|
excludeType: ["ai-skill", "mcp"],
|
|
1165
|
-
projectType: ["js"],
|
|
2678
|
+
projectType: ["js", "ai-skill", "mcp"],
|
|
1166
2679
|
};
|
|
1167
2680
|
const filteredBomJson = postProcess(
|
|
1168
2681
|
await createBom(tmpDir, filteredOptions),
|
|
@@ -1196,28 +2709,233 @@ checksum = "${"a".repeat(64)}"
|
|
|
1196
2709
|
tmpDir,
|
|
1197
2710
|
).bomJson;
|
|
1198
2711
|
assert.ok(
|
|
1199
|
-
(pyBomJson.components || []).some(
|
|
2712
|
+
!(pyBomJson.components || []).some((component) =>
|
|
2713
|
+
["agent-instructions", "mcp-config", "skill-file"].includes(
|
|
2714
|
+
getProp(component, "cdx:file:kind"),
|
|
2715
|
+
),
|
|
2716
|
+
),
|
|
2717
|
+
"did not expect AI inventory components in python scan without opt-in",
|
|
2718
|
+
);
|
|
2719
|
+
assert.ok(
|
|
2720
|
+
!(pyBomJson.services || []).some((service) =>
|
|
2721
|
+
service.properties?.some((property) =>
|
|
2722
|
+
property.name.startsWith("cdx:mcp:"),
|
|
2723
|
+
),
|
|
2724
|
+
),
|
|
2725
|
+
"did not expect MCP services in python scan without opt-in",
|
|
2726
|
+
);
|
|
2727
|
+
|
|
2728
|
+
const optedInPyOptions = {
|
|
2729
|
+
...baseOptions,
|
|
2730
|
+
projectType: ["py", "ai-skill", "mcp"],
|
|
2731
|
+
};
|
|
2732
|
+
const optedInPyBomJson = postProcess(
|
|
2733
|
+
await createPythonBom(tmpDir, optedInPyOptions),
|
|
2734
|
+
optedInPyOptions,
|
|
2735
|
+
tmpDir,
|
|
2736
|
+
).bomJson;
|
|
2737
|
+
assert.ok(
|
|
2738
|
+
(optedInPyBomJson.components || []).some(
|
|
1200
2739
|
(component) =>
|
|
1201
2740
|
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
1202
2741
|
getProp(component, "cdx:skill:name") === "release",
|
|
1203
2742
|
),
|
|
1204
|
-
"expected skill file in python scan",
|
|
2743
|
+
"expected skill file in opted-in python scan",
|
|
1205
2744
|
);
|
|
1206
2745
|
assert.ok(
|
|
1207
|
-
(
|
|
2746
|
+
(optedInPyBomJson.components || []).some(
|
|
1208
2747
|
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
1209
2748
|
),
|
|
1210
|
-
"expected MCP config in python scan",
|
|
2749
|
+
"expected MCP config in opted-in python scan",
|
|
1211
2750
|
);
|
|
1212
2751
|
assert.ok(
|
|
1213
|
-
(
|
|
2752
|
+
(optedInPyBomJson.services || []).some(
|
|
1214
2753
|
(service) =>
|
|
1215
2754
|
service.name === "python-release-docs" &&
|
|
1216
2755
|
getProp(service, "cdx:mcp:inventorySource") ===
|
|
1217
2756
|
"source-code-analysis",
|
|
1218
2757
|
),
|
|
1219
|
-
"expected Python MCP service in python scan",
|
|
2758
|
+
"expected Python MCP service in opted-in python scan",
|
|
1220
2759
|
);
|
|
2760
|
+
|
|
2761
|
+
const auditMcpPyOptions = {
|
|
2762
|
+
...baseOptions,
|
|
2763
|
+
bomAuditCategories: "mcp-server",
|
|
2764
|
+
projectType: ["py"],
|
|
2765
|
+
};
|
|
2766
|
+
const auditMcpPyBomJson = postProcess(
|
|
2767
|
+
await createPythonBom(tmpDir, auditMcpPyOptions),
|
|
2768
|
+
auditMcpPyOptions,
|
|
2769
|
+
tmpDir,
|
|
2770
|
+
).bomJson;
|
|
2771
|
+
assert.ok(
|
|
2772
|
+
(auditMcpPyBomJson.services || []).some(
|
|
2773
|
+
(service) =>
|
|
2774
|
+
service.name === "python-release-docs" &&
|
|
2775
|
+
getProp(service, "cdx:mcp:inventorySource") ===
|
|
2776
|
+
"source-code-analysis",
|
|
2777
|
+
),
|
|
2778
|
+
"expected Python MCP service in mcp-server audit-category scan",
|
|
2779
|
+
);
|
|
2780
|
+
assert.ok(
|
|
2781
|
+
!(auditMcpPyBomJson.components || []).some(
|
|
2782
|
+
(component) => getProp(component, "cdx:file:kind") === "skill-file",
|
|
2783
|
+
),
|
|
2784
|
+
"did not expect skill file in mcp-server audit-category python scan",
|
|
2785
|
+
);
|
|
2786
|
+
} finally {
|
|
2787
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
it("does not trace an npm registry config read when opening .npmrc fails", async () => {
|
|
2792
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-npmrc-read-fail-"));
|
|
2793
|
+
writeFileSync(
|
|
2794
|
+
join(tmpDir, "package.json"),
|
|
2795
|
+
JSON.stringify({
|
|
2796
|
+
name: "npmrc-read-fail",
|
|
2797
|
+
version: "1.0.0",
|
|
2798
|
+
}),
|
|
2799
|
+
);
|
|
2800
|
+
mkdirSync(join(tmpDir, ".npmrc"), { recursive: true });
|
|
2801
|
+
setDryRunMode(true);
|
|
2802
|
+
resetRecordedActivities();
|
|
2803
|
+
try {
|
|
2804
|
+
await assert.rejects(() =>
|
|
2805
|
+
createNodejsBom(tmpDir, {
|
|
2806
|
+
installDeps: true,
|
|
2807
|
+
multiProject: false,
|
|
2808
|
+
projectType: ["npm"],
|
|
2809
|
+
}),
|
|
2810
|
+
);
|
|
2811
|
+
const readActivities = getRecordedActivities().filter(
|
|
2812
|
+
(activity) =>
|
|
2813
|
+
activity.kind === "read" &&
|
|
2814
|
+
activity.target === join(tmpDir, ".npmrc"),
|
|
2815
|
+
);
|
|
2816
|
+
assert.deepStrictEqual(readActivities, []);
|
|
2817
|
+
} finally {
|
|
2818
|
+
setDryRunMode(false);
|
|
2819
|
+
resetRecordedActivities();
|
|
2820
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
});
|
|
2824
|
+
|
|
2825
|
+
describe("node_modules multi-ecosystem filtering", () => {
|
|
2826
|
+
it("ignores composer manifests in node_modules during mixed npm/php scans", () => {
|
|
2827
|
+
const tmpDir = createComposerNodeModulesFixture();
|
|
2828
|
+
try {
|
|
2829
|
+
const bomData = createPHPBom(tmpDir, {
|
|
2830
|
+
installDeps: false,
|
|
2831
|
+
multiProject: true,
|
|
2832
|
+
projectType: ["js", "php"],
|
|
2833
|
+
specVersion: 1.7,
|
|
2834
|
+
});
|
|
2835
|
+
assert.deepStrictEqual(bomData, {});
|
|
2836
|
+
} finally {
|
|
2837
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
|
|
2841
|
+
it("still allows explicit php scans to inspect composer manifests in node_modules", () => {
|
|
2842
|
+
const tmpDir = createComposerNodeModulesFixture();
|
|
2843
|
+
try {
|
|
2844
|
+
const bomData = createPHPBom(tmpDir, {
|
|
2845
|
+
installDeps: false,
|
|
2846
|
+
multiProject: true,
|
|
2847
|
+
projectType: ["php"],
|
|
2848
|
+
specVersion: 1.7,
|
|
2849
|
+
});
|
|
2850
|
+
assert.ok(bomData?.bomJson?.components?.length);
|
|
2851
|
+
} finally {
|
|
2852
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2853
|
+
}
|
|
2854
|
+
});
|
|
2855
|
+
|
|
2856
|
+
it("still allows direct php scans without projectType to inspect composer manifests in node_modules", () => {
|
|
2857
|
+
const tmpDir = createComposerNodeModulesFixture();
|
|
2858
|
+
try {
|
|
2859
|
+
const bomData = createPHPBom(tmpDir, {
|
|
2860
|
+
installDeps: false,
|
|
2861
|
+
multiProject: true,
|
|
2862
|
+
specVersion: 1.7,
|
|
2863
|
+
});
|
|
2864
|
+
assert.ok(bomData?.bomJson?.components?.length);
|
|
2865
|
+
} finally {
|
|
2866
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
2869
|
+
|
|
2870
|
+
it("still allows explicit php alias combinations to inspect composer manifests in node_modules", () => {
|
|
2871
|
+
const tmpDir = createComposerNodeModulesFixture();
|
|
2872
|
+
try {
|
|
2873
|
+
const bomData = createPHPBom(tmpDir, {
|
|
2874
|
+
installDeps: false,
|
|
2875
|
+
multiProject: true,
|
|
2876
|
+
projectType: ["php", "composer"],
|
|
2877
|
+
specVersion: 1.7,
|
|
2878
|
+
});
|
|
2879
|
+
assert.ok(bomData?.bomJson?.components?.length);
|
|
2880
|
+
} finally {
|
|
2881
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
|
|
2885
|
+
it("ignores jar artifacts in node_modules during mixed npm/jar scans", async () => {
|
|
2886
|
+
const tmpDir = createJarNodeModulesFixture();
|
|
2887
|
+
try {
|
|
2888
|
+
const createJarBom = await loadStubbedCreateJarBom();
|
|
2889
|
+
const bomData = await createJarBom(tmpDir, {
|
|
2890
|
+
multiProject: true,
|
|
2891
|
+
projectType: ["js", "jar"],
|
|
2892
|
+
specVersion: 1.7,
|
|
2893
|
+
});
|
|
2894
|
+
assert.strictEqual(bomData?.bomJson?.components?.length || 0, 0);
|
|
2895
|
+
} finally {
|
|
2896
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
|
|
2900
|
+
it("still allows explicit jar scans to inspect node_modules artifacts", async () => {
|
|
2901
|
+
const tmpDir = createJarNodeModulesFixture();
|
|
2902
|
+
try {
|
|
2903
|
+
const createJarBom = await loadStubbedCreateJarBom();
|
|
2904
|
+
const bomData = await createJarBom(tmpDir, {
|
|
2905
|
+
multiProject: true,
|
|
2906
|
+
projectType: ["jar"],
|
|
2907
|
+
specVersion: 1.7,
|
|
2908
|
+
});
|
|
2909
|
+
assert.ok(bomData?.bomJson?.components?.length);
|
|
2910
|
+
} finally {
|
|
2911
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2912
|
+
}
|
|
2913
|
+
});
|
|
2914
|
+
|
|
2915
|
+
it("still allows direct jar scans without projectType to inspect node_modules artifacts", async () => {
|
|
2916
|
+
const tmpDir = createJarNodeModulesFixture();
|
|
2917
|
+
try {
|
|
2918
|
+
const createJarBom = await loadStubbedCreateJarBom();
|
|
2919
|
+
const bomData = await createJarBom(tmpDir, {
|
|
2920
|
+
multiProject: true,
|
|
2921
|
+
specVersion: 1.7,
|
|
2922
|
+
});
|
|
2923
|
+
assert.ok(bomData?.bomJson?.components?.length);
|
|
2924
|
+
} finally {
|
|
2925
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2926
|
+
}
|
|
2927
|
+
});
|
|
2928
|
+
|
|
2929
|
+
it("still allows explicit jar alias combinations to inspect node_modules artifacts", async () => {
|
|
2930
|
+
const tmpDir = createJarNodeModulesFixture();
|
|
2931
|
+
try {
|
|
2932
|
+
const createJarBom = await loadStubbedCreateJarBom();
|
|
2933
|
+
const bomData = await createJarBom(tmpDir, {
|
|
2934
|
+
multiProject: true,
|
|
2935
|
+
projectType: ["jar", "war"],
|
|
2936
|
+
specVersion: 1.7,
|
|
2937
|
+
});
|
|
2938
|
+
assert.ok(bomData?.bomJson?.components?.length);
|
|
1221
2939
|
} finally {
|
|
1222
2940
|
rmSync(tmpDir, { force: true, recursive: true });
|
|
1223
2941
|
}
|