@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
|
@@ -5,9 +5,12 @@ import { PackageURL } from "packageurl-js";
|
|
|
5
5
|
import { assert, describe, it } from "poku";
|
|
6
6
|
|
|
7
7
|
import { githubActionsParser } from "../../helpers/ciParsers/githubActions.js";
|
|
8
|
+
import { createLolbasProperties } from "../../helpers/lolbas.js";
|
|
8
9
|
import {
|
|
9
10
|
auditBom,
|
|
10
11
|
formatAnnotations,
|
|
12
|
+
formatDryRunSupportSummary,
|
|
13
|
+
getBomAuditDryRunSupportSummary,
|
|
11
14
|
hasCriticalFindings,
|
|
12
15
|
} from "./auditBom.js";
|
|
13
16
|
import { evaluateRule, evaluateRules, loadRules } from "./ruleEngine.js";
|
|
@@ -95,6 +98,51 @@ function makeChromeExtensionComponent(name, version, properties) {
|
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
function makeHbomComponent(name, hardwareClass, properties = [], extra = {}) {
|
|
102
|
+
return {
|
|
103
|
+
type: "device",
|
|
104
|
+
name,
|
|
105
|
+
version: extra.version,
|
|
106
|
+
"bom-ref": extra.bomRef || `urn:uuid:${hardwareClass}:${name}`,
|
|
107
|
+
properties: [["cdx:hbom:hardwareClass", hardwareClass], ...properties].map(
|
|
108
|
+
([k, v]) => ({ name: k, value: v }),
|
|
109
|
+
),
|
|
110
|
+
...extra,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function makeHbomBom(
|
|
115
|
+
components = [],
|
|
116
|
+
metadataProperties = [],
|
|
117
|
+
bomProperties = [],
|
|
118
|
+
) {
|
|
119
|
+
return {
|
|
120
|
+
bomFormat: "CycloneDX",
|
|
121
|
+
specVersion: "1.7",
|
|
122
|
+
serialNumber: "urn:uuid:test-hbom",
|
|
123
|
+
metadata: {
|
|
124
|
+
tools: {
|
|
125
|
+
components: [
|
|
126
|
+
{
|
|
127
|
+
type: "application",
|
|
128
|
+
name: "cdxgen",
|
|
129
|
+
version: "12.4.0",
|
|
130
|
+
"bom-ref": "pkg:npm/%40cyclonedx/cdxgen@12.4.0",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
component: {
|
|
135
|
+
name: "test-host",
|
|
136
|
+
type: "device",
|
|
137
|
+
"bom-ref": "urn:uuid:test-host",
|
|
138
|
+
properties: metadataProperties.map(([k, v]) => ({ name: k, value: v })),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
components,
|
|
142
|
+
properties: bomProperties.map(([k, v]) => ({ name: k, value: v })),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
98
146
|
function makeBomFromWorkflowFixture(filename) {
|
|
99
147
|
const workflowFile = join(WORKFLOWS_DIR, filename);
|
|
100
148
|
const result = githubActionsParser.parse([workflowFile], {
|
|
@@ -115,6 +163,10 @@ describe("loadRules", () => {
|
|
|
115
163
|
["critical", "high", "medium", "low"].includes(rule.severity),
|
|
116
164
|
`Rule ${rule.id} severity must be valid`,
|
|
117
165
|
);
|
|
166
|
+
assert.ok(
|
|
167
|
+
["no", "partial", "full"].includes(rule.dryRunSupport),
|
|
168
|
+
`Rule ${rule.id} dry-run support must be valid`,
|
|
169
|
+
);
|
|
118
170
|
}
|
|
119
171
|
});
|
|
120
172
|
|
|
@@ -146,6 +198,50 @@ describe("loadRules", () => {
|
|
|
146
198
|
assert.ok(mcpRules.length > 0, "Should have MCP server rules");
|
|
147
199
|
const agentRules = rules.filter((r) => r.category === "ai-agent");
|
|
148
200
|
assert.ok(agentRules.length > 0, "Should have AI agent rules");
|
|
201
|
+
const asarRules = rules.filter((r) => r.category === "asar-archive");
|
|
202
|
+
assert.ok(asarRules.length > 0, "Should have ASAR archive rules");
|
|
203
|
+
const hbomSecurityRules = rules.filter(
|
|
204
|
+
(r) => r.category === "hbom-security",
|
|
205
|
+
);
|
|
206
|
+
assert.ok(hbomSecurityRules.length > 0, "Should have HBOM security rules");
|
|
207
|
+
const hbomPerformanceRules = rules.filter(
|
|
208
|
+
(r) => r.category === "hbom-performance",
|
|
209
|
+
);
|
|
210
|
+
assert.ok(
|
|
211
|
+
hbomPerformanceRules.length > 0,
|
|
212
|
+
"Should have HBOM performance rules",
|
|
213
|
+
);
|
|
214
|
+
const hbomComplianceRules = rules.filter(
|
|
215
|
+
(r) => r.category === "hbom-compliance",
|
|
216
|
+
);
|
|
217
|
+
assert.ok(
|
|
218
|
+
hbomComplianceRules.length > 0,
|
|
219
|
+
"Should have HBOM compliance rules",
|
|
220
|
+
);
|
|
221
|
+
const hostTopologyRules = rules.filter(
|
|
222
|
+
(r) => r.category === "host-topology",
|
|
223
|
+
);
|
|
224
|
+
assert.ok(hostTopologyRules.length > 0, "Should have host-topology rules");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should assign explicit dry-run support metadata to built-in rules", async () => {
|
|
228
|
+
const rules = await loadRules(RULES_DIR);
|
|
229
|
+
assert.strictEqual(
|
|
230
|
+
rules.find((rule) => rule.id === "CI-001")?.dryRunSupport,
|
|
231
|
+
"full",
|
|
232
|
+
);
|
|
233
|
+
assert.strictEqual(
|
|
234
|
+
rules.find((rule) => rule.id === "INT-003")?.dryRunSupport,
|
|
235
|
+
"no",
|
|
236
|
+
);
|
|
237
|
+
assert.strictEqual(
|
|
238
|
+
rules.find((rule) => rule.id === "INT-005")?.dryRunSupport,
|
|
239
|
+
"partial",
|
|
240
|
+
);
|
|
241
|
+
assert.strictEqual(
|
|
242
|
+
rules.find((rule) => rule.id === "HBS-001")?.dryRunSupport,
|
|
243
|
+
"full",
|
|
244
|
+
);
|
|
149
245
|
});
|
|
150
246
|
});
|
|
151
247
|
|
|
@@ -307,6 +403,369 @@ describe("evaluateRule", () => {
|
|
|
307
403
|
assert.strictEqual(findings[0].severity, "high");
|
|
308
404
|
});
|
|
309
405
|
|
|
406
|
+
it("should detect HBOM security findings from synthetic device inventory", async () => {
|
|
407
|
+
const bom = makeHbomBom(
|
|
408
|
+
[
|
|
409
|
+
makeHbomComponent("Main SSD", "storage", [
|
|
410
|
+
["cdx:hbom:isEncrypted", "false"],
|
|
411
|
+
["cdx:hbom:deviceSerial", "ABC123456789"],
|
|
412
|
+
]),
|
|
413
|
+
makeHbomComponent("wifi0", "wireless-adapter", [
|
|
414
|
+
["cdx:hbom:connected", "true"],
|
|
415
|
+
["cdx:hbom:securityMode", "open"],
|
|
416
|
+
]),
|
|
417
|
+
makeHbomComponent("USB Stick", "storage-volume", [
|
|
418
|
+
["cdx:hbom:isRemovable", "true"],
|
|
419
|
+
["cdx:hbom:isLocked", "false"],
|
|
420
|
+
]),
|
|
421
|
+
makeHbomComponent("USB4 Dock", "bus", [
|
|
422
|
+
["cdx:hbom:securityLevel", "none"],
|
|
423
|
+
["cdx:hbom:iommuProtection", "false"],
|
|
424
|
+
["cdx:hbom:policy", "auto"],
|
|
425
|
+
]),
|
|
426
|
+
makeHbomComponent("LTE Modem", "modem", [
|
|
427
|
+
["cdx:hbom:imei", "490154203237518"],
|
|
428
|
+
["cdx:hbom:ownNumbers", "+15551234567"],
|
|
429
|
+
]),
|
|
430
|
+
],
|
|
431
|
+
[
|
|
432
|
+
["cdx:hbom:platform", "linux"],
|
|
433
|
+
["cdx:hbom:architecture", "amd64"],
|
|
434
|
+
["cdx:hbom:identifierPolicy", "full"],
|
|
435
|
+
["cdx:hbom:serialNumber", "HOST-SERIAL-001"],
|
|
436
|
+
],
|
|
437
|
+
[["cdx:hbom:collectorProfile", "linux-amd64"]],
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const findings = await auditBom(bom, {
|
|
441
|
+
bomAuditCategories: "hbom-security",
|
|
442
|
+
});
|
|
443
|
+
const ruleIds = new Set(findings.map((finding) => finding.ruleId));
|
|
444
|
+
|
|
445
|
+
assert.ok(ruleIds.has("HBS-001"));
|
|
446
|
+
assert.ok(ruleIds.has("HBS-002"));
|
|
447
|
+
assert.ok(ruleIds.has("HBS-003"));
|
|
448
|
+
assert.ok(ruleIds.has("HBS-004"));
|
|
449
|
+
assert.ok(ruleIds.has("HBS-005"));
|
|
450
|
+
assert.ok(ruleIds.has("HBS-006"));
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("should detect HBOM performance findings from synthetic device inventory", async () => {
|
|
454
|
+
const bom = makeHbomBom([
|
|
455
|
+
makeHbomComponent("rootfs", "storage-volume", [
|
|
456
|
+
["cdx:hbom:capacityBytes", "1000"],
|
|
457
|
+
["cdx:hbom:freeBytes", "100"],
|
|
458
|
+
]),
|
|
459
|
+
makeHbomComponent("nvme0", "storage", [
|
|
460
|
+
["cdx:hbom:wearPercentageUsed", "91"],
|
|
461
|
+
["cdx:hbom:smartStatus", "Failing"],
|
|
462
|
+
]),
|
|
463
|
+
makeHbomComponent("CPU Thermal Zone", "thermal-zone", [
|
|
464
|
+
["cdx:hbom:temperatureCelsius", "92"],
|
|
465
|
+
]),
|
|
466
|
+
makeHbomComponent("Battery", "power", [
|
|
467
|
+
["cdx:hbom:maximumCapacity", "71%"],
|
|
468
|
+
["cdx:hbom:cycleCount", "1204"],
|
|
469
|
+
["cdx:hbom:designCapacityPercent", "62"],
|
|
470
|
+
]),
|
|
471
|
+
makeHbomComponent("eth0", "network-interface", [
|
|
472
|
+
["cdx:hbom:operState", "up"],
|
|
473
|
+
["cdx:hbom:duplex", "half"],
|
|
474
|
+
["cdx:hbom:speedMbps", "100"],
|
|
475
|
+
]),
|
|
476
|
+
makeHbomComponent("DIMM Bank", "memory", [
|
|
477
|
+
["cdx:hbom:sizeBytes", "1000"],
|
|
478
|
+
["cdx:hbom:memoryOnlineSize", "800"],
|
|
479
|
+
]),
|
|
480
|
+
makeHbomComponent("USB Camera", "usb-device", [
|
|
481
|
+
["cdx:hbom:currentRequired", "900"],
|
|
482
|
+
["cdx:hbom:currentAvailable", "500"],
|
|
483
|
+
]),
|
|
484
|
+
makeHbomComponent("LTE Modem", "modem", [
|
|
485
|
+
["cdx:hbom:signalQuality", "18"],
|
|
486
|
+
["cdx:hbom:operatorName", "ExampleTel"],
|
|
487
|
+
]),
|
|
488
|
+
]);
|
|
489
|
+
|
|
490
|
+
const findings = await auditBom(bom, {
|
|
491
|
+
bomAuditCategories: "hbom-performance",
|
|
492
|
+
});
|
|
493
|
+
const ruleIds = new Set(findings.map((finding) => finding.ruleId));
|
|
494
|
+
|
|
495
|
+
assert.ok(ruleIds.has("HBP-001"));
|
|
496
|
+
assert.ok(ruleIds.has("HBP-002"));
|
|
497
|
+
assert.ok(ruleIds.has("HBP-003"));
|
|
498
|
+
assert.ok(ruleIds.has("HBP-004"));
|
|
499
|
+
assert.ok(ruleIds.has("HBP-005"));
|
|
500
|
+
assert.ok(ruleIds.has("HBP-006"));
|
|
501
|
+
assert.ok(ruleIds.has("HBP-007"));
|
|
502
|
+
assert.ok(ruleIds.has("HBP-008"));
|
|
503
|
+
assert.ok(ruleIds.has("HBP-009"));
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("should detect HBOM compliance findings from synthetic device inventory", async () => {
|
|
507
|
+
const bom = makeHbomBom(
|
|
508
|
+
[
|
|
509
|
+
makeHbomComponent("rootfs", "storage-volume", [
|
|
510
|
+
["cdx:hbom:capacityBytes", "1000"],
|
|
511
|
+
]),
|
|
512
|
+
makeHbomComponent("HDMI-A-1", "display-connector", [
|
|
513
|
+
["cdx:hbom:displayConnectorType", "HDMI-A"],
|
|
514
|
+
]),
|
|
515
|
+
],
|
|
516
|
+
[
|
|
517
|
+
["cdx:hbom:platform", "linux"],
|
|
518
|
+
["cdx:hbom:identifierPolicy", "full"],
|
|
519
|
+
],
|
|
520
|
+
[
|
|
521
|
+
["cdx:hbom:collectorProfile", "linux-amd64"],
|
|
522
|
+
["cdx:hbom:analysis:missingCommandCount", "2"],
|
|
523
|
+
["cdx:hbom:analysis:missingCommands", "lspci,lsusb"],
|
|
524
|
+
[
|
|
525
|
+
"cdx:hbom:analysis:missingCommandIds",
|
|
526
|
+
"fwupdmgr-devices-json,edid-decode",
|
|
527
|
+
],
|
|
528
|
+
["cdx:hbom:analysis:installHintCount", "2"],
|
|
529
|
+
["cdx:hbom:analysis:permissionDeniedCount", "1"],
|
|
530
|
+
["cdx:hbom:analysis:permissionDeniedCommands", "drm_info"],
|
|
531
|
+
[
|
|
532
|
+
"cdx:hbom:analysis:permissionDeniedIds",
|
|
533
|
+
"dmidecode-firmware-board,drm-info-json",
|
|
534
|
+
],
|
|
535
|
+
["cdx:hbom:analysis:privilegeHintCount", "1"],
|
|
536
|
+
["cdx:hbom:analysis:requiresPrivileged", "true"],
|
|
537
|
+
],
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const findings = await auditBom(bom, {
|
|
541
|
+
bomAuditCategories: "hbom-compliance",
|
|
542
|
+
});
|
|
543
|
+
const ruleIds = new Set(findings.map((finding) => finding.ruleId));
|
|
544
|
+
|
|
545
|
+
assert.ok(ruleIds.has("HBC-001"));
|
|
546
|
+
assert.ok(ruleIds.has("HBC-002"));
|
|
547
|
+
assert.ok(ruleIds.has("HBC-003"));
|
|
548
|
+
assert.ok(ruleIds.has("HBC-004"));
|
|
549
|
+
assert.ok(ruleIds.has("HBC-005"));
|
|
550
|
+
assert.ok(ruleIds.has("HBC-006"));
|
|
551
|
+
assert.ok(ruleIds.has("HBC-007"));
|
|
552
|
+
assert.ok(ruleIds.has("HBC-008"));
|
|
553
|
+
assert.ok(ruleIds.has("HBC-009"));
|
|
554
|
+
assert.ok(ruleIds.has("HBC-010"));
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("should not flag redacted-by-default HBOM identifier policy as a compliance finding", async () => {
|
|
558
|
+
const bom = makeHbomBom(
|
|
559
|
+
[],
|
|
560
|
+
[
|
|
561
|
+
["cdx:hbom:platform", "darwin"],
|
|
562
|
+
["cdx:hbom:architecture", "arm64"],
|
|
563
|
+
["cdx:hbom:identifierPolicy", "redacted-by-default"],
|
|
564
|
+
["cdx:hbom:serialNumber", "redacted:serialNumber"],
|
|
565
|
+
],
|
|
566
|
+
[
|
|
567
|
+
["cdx:hbom:collectorProfile", "darwin-arm64"],
|
|
568
|
+
["cdx:hbom:evidence:commandCount", "1"],
|
|
569
|
+
["cdx:hbom:evidence:command", "system_profiler"],
|
|
570
|
+
],
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
const findings = await auditBom(bom, {
|
|
574
|
+
bomAuditCategories: "hbom-compliance",
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
assert.ok(
|
|
578
|
+
!findings.some((finding) => finding.ruleId === "HBC-005"),
|
|
579
|
+
"redacted-by-default should not trigger HBC-005",
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("should not flag HBOM collector evidence as incomplete when BOM command evidence is present", async () => {
|
|
584
|
+
const rules = await loadRules(RULES_DIR);
|
|
585
|
+
const rule = rules.find((r) => r.id === "HBC-003");
|
|
586
|
+
|
|
587
|
+
const bom = makeHbomBom(
|
|
588
|
+
[],
|
|
589
|
+
[
|
|
590
|
+
["cdx:hbom:platform", "darwin"],
|
|
591
|
+
["cdx:hbom:architecture", "arm64"],
|
|
592
|
+
["cdx:hbom:identifierPolicy", "redacted-by-default"],
|
|
593
|
+
],
|
|
594
|
+
[
|
|
595
|
+
["cdx:hbom:collectorProfile", "darwin-arm64"],
|
|
596
|
+
["cdx:hbom:evidence:commandCount", "2"],
|
|
597
|
+
[
|
|
598
|
+
"cdx:hbom:evidence:command",
|
|
599
|
+
"system-profiler-json|platform|/usr/sbin/system_profiler SPHardwareDataType -json",
|
|
600
|
+
],
|
|
601
|
+
[
|
|
602
|
+
"cdx:hbom:evidence:command",
|
|
603
|
+
"battery-status|power|/usr/bin/pmset -g batt",
|
|
604
|
+
],
|
|
605
|
+
],
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const findings = await evaluateRule(rule, bom);
|
|
609
|
+
assert.strictEqual(findings.length, 0);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("should detect HBOM command diagnostics for missing utilities and permission-denied enrichments", async () => {
|
|
613
|
+
const rules = await loadRules(RULES_DIR);
|
|
614
|
+
const missingCommandsRule = rules.find((r) => r.id === "HBC-006");
|
|
615
|
+
const permissionDeniedRule = rules.find((r) => r.id === "HBC-007");
|
|
616
|
+
const firmwareRule = rules.find((r) => r.id === "HBC-008");
|
|
617
|
+
const boardRule = rules.find((r) => r.id === "HBC-009");
|
|
618
|
+
const displayRule = rules.find((r) => r.id === "HBC-010");
|
|
619
|
+
|
|
620
|
+
const bom = makeHbomBom(
|
|
621
|
+
[
|
|
622
|
+
makeHbomComponent("eDP-1", "display-connector", [
|
|
623
|
+
["cdx:hbom:displayConnectorType", "eDP"],
|
|
624
|
+
]),
|
|
625
|
+
],
|
|
626
|
+
[
|
|
627
|
+
["cdx:hbom:platform", "linux"],
|
|
628
|
+
["cdx:hbom:architecture", "amd64"],
|
|
629
|
+
],
|
|
630
|
+
[
|
|
631
|
+
["cdx:hbom:collectorProfile", "linux-amd64-v1"],
|
|
632
|
+
["cdx:hbom:analysis:missingCommandCount", "1"],
|
|
633
|
+
["cdx:hbom:analysis:missingCommands", "lsusb"],
|
|
634
|
+
[
|
|
635
|
+
"cdx:hbom:analysis:missingCommandIds",
|
|
636
|
+
"fwupdmgr-devices-json,edid-decode",
|
|
637
|
+
],
|
|
638
|
+
["cdx:hbom:analysis:permissionDeniedCount", "1"],
|
|
639
|
+
["cdx:hbom:analysis:permissionDeniedCommands", "drm_info"],
|
|
640
|
+
[
|
|
641
|
+
"cdx:hbom:analysis:permissionDeniedIds",
|
|
642
|
+
"dmidecode-firmware-board,drm-info-json",
|
|
643
|
+
],
|
|
644
|
+
["cdx:hbom:analysis:requiresPrivileged", "true"],
|
|
645
|
+
],
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const missingCommandsFindings = await evaluateRule(
|
|
649
|
+
missingCommandsRule,
|
|
650
|
+
bom,
|
|
651
|
+
);
|
|
652
|
+
const permissionDeniedFindings = await evaluateRule(
|
|
653
|
+
permissionDeniedRule,
|
|
654
|
+
bom,
|
|
655
|
+
);
|
|
656
|
+
const firmwareFindings = await evaluateRule(firmwareRule, bom);
|
|
657
|
+
const boardFindings = await evaluateRule(boardRule, bom);
|
|
658
|
+
const displayFindings = await evaluateRule(displayRule, bom);
|
|
659
|
+
|
|
660
|
+
assert.strictEqual(missingCommandsFindings.length, 1);
|
|
661
|
+
assert.strictEqual(missingCommandsFindings[0].ruleId, "HBC-006");
|
|
662
|
+
assert.strictEqual(permissionDeniedFindings.length, 1);
|
|
663
|
+
assert.strictEqual(permissionDeniedFindings[0].ruleId, "HBC-007");
|
|
664
|
+
assert.strictEqual(firmwareFindings.length, 1);
|
|
665
|
+
assert.strictEqual(firmwareFindings[0].ruleId, "HBC-008");
|
|
666
|
+
assert.strictEqual(boardFindings.length, 1);
|
|
667
|
+
assert.strictEqual(boardFindings[0].ruleId, "HBC-009");
|
|
668
|
+
assert.strictEqual(displayFindings.length, 1);
|
|
669
|
+
assert.strictEqual(displayFindings[0].ruleId, "HBC-010");
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("should not flag redacted HBOM identifiers for the raw-identifier exposure rule", async () => {
|
|
673
|
+
const rules = await loadRules(RULES_DIR);
|
|
674
|
+
const rule = rules.find((r) => r.id === "HBS-004");
|
|
675
|
+
|
|
676
|
+
const bom = makeHbomBom(
|
|
677
|
+
[
|
|
678
|
+
makeHbomComponent("wifi0", "network-interface", [
|
|
679
|
+
["cdx:hbom:macAddress", "redacted:macAddress"],
|
|
680
|
+
]),
|
|
681
|
+
],
|
|
682
|
+
[
|
|
683
|
+
["cdx:hbom:platform", "darwin"],
|
|
684
|
+
["cdx:hbom:architecture", "arm64"],
|
|
685
|
+
["cdx:hbom:identifierPolicy", "redacted-by-default"],
|
|
686
|
+
["cdx:hbom:serialNumber", "redacted:serialNumber"],
|
|
687
|
+
["cdx:hbom:platformUuid", "redacted:platformUuid"],
|
|
688
|
+
],
|
|
689
|
+
[["cdx:hbom:collectorProfile", "darwin-arm64"]],
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const findings = await evaluateRule(rule, bom);
|
|
693
|
+
assert.strictEqual(findings.length, 0);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("should not flag redacted modem identifiers for the cellular exposure rule", async () => {
|
|
697
|
+
const rules = await loadRules(RULES_DIR);
|
|
698
|
+
const rule = rules.find((r) => r.id === "HBS-006");
|
|
699
|
+
|
|
700
|
+
const bom = makeHbomBom(
|
|
701
|
+
[
|
|
702
|
+
makeHbomComponent("LTE Modem", "modem", [
|
|
703
|
+
["cdx:hbom:imei", "redacted:imei"],
|
|
704
|
+
["cdx:hbom:ownNumbers", "redacted:ownNumbers"],
|
|
705
|
+
]),
|
|
706
|
+
],
|
|
707
|
+
[["cdx:hbom:identifierPolicy", "redacted-by-default"]],
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const findings = await evaluateRule(rule, bom);
|
|
711
|
+
assert.strictEqual(findings.length, 0);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("should not flag healthy storage telemetry for the degraded-storage HBOM rule", async () => {
|
|
715
|
+
const rules = await loadRules(RULES_DIR);
|
|
716
|
+
const rule = rules.find((r) => r.id === "HBP-002");
|
|
717
|
+
|
|
718
|
+
const bom = makeHbomBom([
|
|
719
|
+
makeHbomComponent("nvme0", "storage", [
|
|
720
|
+
["cdx:hbom:wearPercentageUsed", "12"],
|
|
721
|
+
["cdx:hbom:smartStatus", "ok"],
|
|
722
|
+
]),
|
|
723
|
+
]);
|
|
724
|
+
|
|
725
|
+
const findings = await evaluateRule(rule, bom);
|
|
726
|
+
assert.strictEqual(findings.length, 0);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("should safely handle human-readable online memory size values for the HBOM memory rule", async () => {
|
|
730
|
+
const rules = await loadRules(RULES_DIR);
|
|
731
|
+
const rule = rules.find((r) => r.id === "HBP-006");
|
|
732
|
+
|
|
733
|
+
const bom = makeHbomBom([
|
|
734
|
+
makeHbomComponent("System Memory", "memory", [
|
|
735
|
+
["cdx:hbom:sizeBytes", "32899006464"],
|
|
736
|
+
["cdx:hbom:memoryOnlineSize", "32 GB"],
|
|
737
|
+
]),
|
|
738
|
+
]);
|
|
739
|
+
|
|
740
|
+
const findings = await evaluateRule(rule, bom);
|
|
741
|
+
assert.strictEqual(findings.length, 0);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("should expand the hbom alias to all HBOM audit categories", async () => {
|
|
745
|
+
const bom = makeHbomBom(
|
|
746
|
+
[
|
|
747
|
+
makeHbomComponent("Main SSD", "storage", [
|
|
748
|
+
["cdx:hbom:isEncrypted", "false"],
|
|
749
|
+
["cdx:hbom:wearPercentageUsed", "90"],
|
|
750
|
+
]),
|
|
751
|
+
],
|
|
752
|
+
[
|
|
753
|
+
["cdx:hbom:platform", "linux"],
|
|
754
|
+
["cdx:hbom:identifierPolicy", "full"],
|
|
755
|
+
],
|
|
756
|
+
[["cdx:hbom:collectorProfile", "linux-amd64"]],
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
const findings = await auditBom(bom, {
|
|
760
|
+
bomAuditCategories: "hbom",
|
|
761
|
+
});
|
|
762
|
+
const categories = new Set(findings.map((finding) => finding.category));
|
|
763
|
+
|
|
764
|
+
assert.ok(categories.has("hbom-security"));
|
|
765
|
+
assert.ok(categories.has("hbom-performance"));
|
|
766
|
+
assert.ok(categories.has("hbom-compliance"));
|
|
767
|
+
});
|
|
768
|
+
|
|
310
769
|
it("should detect Collider packages missing valid wrap hashes (INT-014)", async () => {
|
|
311
770
|
const rules = await loadRules(RULES_DIR);
|
|
312
771
|
const rule = rules.find((r) => r.id === "INT-014");
|
|
@@ -393,6 +852,46 @@ describe("evaluateRule", () => {
|
|
|
393
852
|
assert.strictEqual(findings[0].severity, "critical");
|
|
394
853
|
});
|
|
395
854
|
|
|
855
|
+
it("should detect revoked Secure Boot certificates (OBOM-LNX-012)", async () => {
|
|
856
|
+
const rules = await loadRules(RULES_DIR);
|
|
857
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-012");
|
|
858
|
+
assert.ok(rule, "OBOM-LNX-012 rule should exist");
|
|
859
|
+
|
|
860
|
+
const bom = makeBom([
|
|
861
|
+
makeComponent("dbx-entry", "key-id-1", [
|
|
862
|
+
["cdx:osquery:category", "secureboot_certificates"],
|
|
863
|
+
["revoked", "1"],
|
|
864
|
+
["subject", "CN=Legacy Bootloader"],
|
|
865
|
+
["issuer", "CN=Platform DBX"],
|
|
866
|
+
["serial", "42"],
|
|
867
|
+
]),
|
|
868
|
+
]);
|
|
869
|
+
|
|
870
|
+
const findings = await evaluateRule(rule, bom);
|
|
871
|
+
assert.ok(findings.length > 0, "Should detect revoked Secure Boot cert");
|
|
872
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it("should detect expiring Secure Boot certificates (OBOM-LNX-013)", async () => {
|
|
876
|
+
const rules = await loadRules(RULES_DIR);
|
|
877
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-013");
|
|
878
|
+
assert.ok(rule, "OBOM-LNX-013 rule should exist");
|
|
879
|
+
|
|
880
|
+
const bom = makeBom([
|
|
881
|
+
makeComponent("db-entry", "key-id-2", [
|
|
882
|
+
["cdx:osquery:category", "secureboot_certificates"],
|
|
883
|
+
["not_valid_after", `${Math.floor(Date.now() / 1000) + 86400}`],
|
|
884
|
+
["not_valid_before", `${Math.floor(Date.now() / 1000) - 86400}`],
|
|
885
|
+
["subject", "CN=Current Platform Key"],
|
|
886
|
+
["issuer", "CN=Firmware CA"],
|
|
887
|
+
]),
|
|
888
|
+
]);
|
|
889
|
+
|
|
890
|
+
const findings = await evaluateRule(rule, bom);
|
|
891
|
+
assert.ok(findings.length > 0, "Should detect expiring Secure Boot cert");
|
|
892
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
893
|
+
});
|
|
894
|
+
|
|
396
895
|
it("should detect a network-exposed non-official MCP server (MCP-003)", async () => {
|
|
397
896
|
const rules = await loadRules(RULES_DIR);
|
|
398
897
|
const rule = rules.find((r) => r.id === "MCP-003");
|
|
@@ -1089,44 +1588,161 @@ describe("evaluateRule", () => {
|
|
|
1089
1588
|
assert.strictEqual(findings[0].severity, "high");
|
|
1090
1589
|
});
|
|
1091
1590
|
|
|
1092
|
-
it("should
|
|
1591
|
+
it("should detect eval-like archived JavaScript (ASAR-001)", async () => {
|
|
1093
1592
|
const rules = await loadRules(RULES_DIR);
|
|
1094
|
-
const rule = rules.find((r) => r.id === "
|
|
1095
|
-
|
|
1096
|
-
const bom = makeBom([
|
|
1593
|
+
const rule = rules.find((r) => r.id === "ASAR-001");
|
|
1594
|
+
assert.ok(rule, "ASAR-001 rule should exist");
|
|
1595
|
+
const bom = makeBom([
|
|
1596
|
+
{
|
|
1597
|
+
type: "file",
|
|
1598
|
+
name: "main.js",
|
|
1599
|
+
"bom-ref": "file:/tmp/app.asar#/main.js",
|
|
1600
|
+
properties: [
|
|
1601
|
+
{ name: "cdx:file:kind", value: "asar-entry" },
|
|
1602
|
+
{ name: "SrcFile", value: "/tmp/app.asar" },
|
|
1603
|
+
{ name: "cdx:asar:path", value: "main.js" },
|
|
1604
|
+
{ name: "cdx:asar:js:hasEval", value: "true" },
|
|
1605
|
+
],
|
|
1606
|
+
},
|
|
1607
|
+
]);
|
|
1097
1608
|
const findings = await evaluateRule(rule, bom);
|
|
1098
|
-
assert.
|
|
1609
|
+
assert.ok(findings.length > 0, "Should detect ASAR eval signal");
|
|
1610
|
+
assert.strictEqual(findings[0].ruleId, "ASAR-001");
|
|
1099
1611
|
});
|
|
1100
1612
|
|
|
1101
|
-
it("should detect
|
|
1613
|
+
it("should detect archived JavaScript with network plus local access (ASAR-002)", async () => {
|
|
1102
1614
|
const rules = await loadRules(RULES_DIR);
|
|
1103
|
-
const rule = rules.find((r) => r.id === "
|
|
1104
|
-
assert.ok(rule, "
|
|
1105
|
-
|
|
1615
|
+
const rule = rules.find((r) => r.id === "ASAR-002");
|
|
1616
|
+
assert.ok(rule, "ASAR-002 rule should exist");
|
|
1106
1617
|
const bom = makeBom([
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1618
|
+
{
|
|
1619
|
+
type: "file",
|
|
1620
|
+
name: "main.js",
|
|
1621
|
+
"bom-ref": "file:/tmp/app.asar#/main.js",
|
|
1622
|
+
properties: [
|
|
1623
|
+
{ name: "cdx:file:kind", value: "asar-entry" },
|
|
1624
|
+
{ name: "SrcFile", value: "/tmp/app.asar" },
|
|
1625
|
+
{ name: "cdx:asar:path", value: "main.js" },
|
|
1626
|
+
{ name: "cdx:asar:js:capability:network", value: "true" },
|
|
1627
|
+
{ name: "cdx:asar:js:capability:fileAccess", value: "true" },
|
|
1628
|
+
{ name: "cdx:asar:js:networkIndicators", value: "fetch(dynamic)" },
|
|
1629
|
+
],
|
|
1630
|
+
},
|
|
1112
1631
|
]);
|
|
1113
|
-
|
|
1114
1632
|
const findings = await evaluateRule(rule, bom);
|
|
1115
|
-
assert.ok(
|
|
1116
|
-
|
|
1117
|
-
"Should detect disabled BitLocker protection",
|
|
1118
|
-
);
|
|
1119
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
1633
|
+
assert.ok(findings.length > 0, "Should detect ASAR capability overlap");
|
|
1634
|
+
assert.strictEqual(findings[0].ruleId, "ASAR-002");
|
|
1120
1635
|
});
|
|
1121
1636
|
|
|
1122
|
-
it("should detect
|
|
1637
|
+
it("should detect ASAR integrity mismatches (ASAR-003)", async () => {
|
|
1123
1638
|
const rules = await loadRules(RULES_DIR);
|
|
1124
|
-
const rule = rules.find((r) => r.id === "
|
|
1125
|
-
assert.ok(rule, "
|
|
1126
|
-
|
|
1639
|
+
const rule = rules.find((r) => r.id === "ASAR-003");
|
|
1640
|
+
assert.ok(rule, "ASAR-003 rule should exist");
|
|
1127
1641
|
const bom = makeBom([
|
|
1128
1642
|
{
|
|
1129
|
-
type: "
|
|
1643
|
+
type: "file",
|
|
1644
|
+
name: "settings.json",
|
|
1645
|
+
"bom-ref": "file:/tmp/app.asar#/settings.json",
|
|
1646
|
+
properties: [
|
|
1647
|
+
{ name: "cdx:file:kind", value: "asar-entry" },
|
|
1648
|
+
{ name: "SrcFile", value: "/tmp/app.asar" },
|
|
1649
|
+
{ name: "cdx:asar:path", value: "settings.json" },
|
|
1650
|
+
{ name: "cdx:asar:declaredIntegrityHash", value: "00" },
|
|
1651
|
+
{ name: "cdx:asar:integrityVerified", value: "false" },
|
|
1652
|
+
],
|
|
1653
|
+
},
|
|
1654
|
+
]);
|
|
1655
|
+
const findings = await evaluateRule(rule, bom);
|
|
1656
|
+
assert.ok(findings.length > 0, "Should detect ASAR integrity mismatch");
|
|
1657
|
+
assert.strictEqual(findings[0].ruleId, "ASAR-003");
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
it("should detect embedded install scripts from ASAR-derived npm components (ASAR-004)", async () => {
|
|
1661
|
+
const rules = await loadRules(RULES_DIR);
|
|
1662
|
+
const rule = rules.find((r) => r.id === "ASAR-004");
|
|
1663
|
+
assert.ok(rule, "ASAR-004 rule should exist");
|
|
1664
|
+
const bom = makeBom([
|
|
1665
|
+
makeComponent("sketchy-addon", "0.1.0", [
|
|
1666
|
+
["SrcFile", "/tmp/app.asar#/package-lock.json"],
|
|
1667
|
+
["cdx:npm:hasInstallScript", "true"],
|
|
1668
|
+
["cdx:npm:risky_scripts", "preinstall"],
|
|
1669
|
+
]),
|
|
1670
|
+
]);
|
|
1671
|
+
const findings = await evaluateRule(rule, bom);
|
|
1672
|
+
assert.ok(
|
|
1673
|
+
findings.length > 0,
|
|
1674
|
+
"Should detect embedded ASAR install scripts",
|
|
1675
|
+
);
|
|
1676
|
+
assert.strictEqual(findings[0].ruleId, "ASAR-004");
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
it("should detect failed Electron ASAR signing verification (ASAR-005)", async () => {
|
|
1680
|
+
const rules = await loadRules(RULES_DIR);
|
|
1681
|
+
const rule = rules.find((r) => r.id === "ASAR-005");
|
|
1682
|
+
assert.ok(rule, "ASAR-005 rule should exist");
|
|
1683
|
+
const bom = makeBom([
|
|
1684
|
+
{
|
|
1685
|
+
type: "application",
|
|
1686
|
+
name: "signed-app.asar",
|
|
1687
|
+
"bom-ref": "file:/tmp/Signed.app/Contents/Resources/app.asar",
|
|
1688
|
+
properties: [
|
|
1689
|
+
{ name: "cdx:file:kind", value: "asar-archive" },
|
|
1690
|
+
{
|
|
1691
|
+
name: "SrcFile",
|
|
1692
|
+
value: "/tmp/Signed.app/Contents/Resources/app.asar",
|
|
1693
|
+
},
|
|
1694
|
+
{ name: "cdx:asar:hasSigningMetadata", value: "true" },
|
|
1695
|
+
{ name: "cdx:asar:signingAlgorithm", value: "SHA256" },
|
|
1696
|
+
{ name: "cdx:asar:signingDeclaredHash", value: "deadbeef" },
|
|
1697
|
+
{ name: "cdx:asar:signingSource", value: "Info.plist" },
|
|
1698
|
+
{ name: "cdx:asar:signingScope", value: "header-only" },
|
|
1699
|
+
{ name: "cdx:asar:signingVerified", value: "false" },
|
|
1700
|
+
],
|
|
1701
|
+
},
|
|
1702
|
+
]);
|
|
1703
|
+
const findings = await evaluateRule(rule, bom);
|
|
1704
|
+
assert.ok(findings.length > 0, "Should detect failed ASAR signing");
|
|
1705
|
+
assert.strictEqual(findings[0].ruleId, "ASAR-005");
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
it("should return empty findings when no components match", async () => {
|
|
1709
|
+
const rules = await loadRules(RULES_DIR);
|
|
1710
|
+
const rule = rules.find((r) => r.id === "CI-001");
|
|
1711
|
+
|
|
1712
|
+
const bom = makeBom([]);
|
|
1713
|
+
const findings = await evaluateRule(rule, bom);
|
|
1714
|
+
assert.strictEqual(findings.length, 0, "No components means no findings");
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
it("should detect unprotected BitLocker drive (OBOM-WIN-001)", async () => {
|
|
1718
|
+
const rules = await loadRules(RULES_DIR);
|
|
1719
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-001");
|
|
1720
|
+
assert.ok(rule, "OBOM-WIN-001 rule should exist");
|
|
1721
|
+
|
|
1722
|
+
const bom = makeBom([
|
|
1723
|
+
makeComponent("disk-c", "C:", [
|
|
1724
|
+
["cdx:osquery:category", "windows_bitlocker_info"],
|
|
1725
|
+
["protection_status", "0"],
|
|
1726
|
+
["encryption_method", "XTS-AES 128"],
|
|
1727
|
+
]),
|
|
1728
|
+
]);
|
|
1729
|
+
|
|
1730
|
+
const findings = await evaluateRule(rule, bom);
|
|
1731
|
+
assert.ok(
|
|
1732
|
+
findings.length > 0,
|
|
1733
|
+
"Should detect disabled BitLocker protection",
|
|
1734
|
+
);
|
|
1735
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
it("should detect suspicious Linux systemd unit path (OBOM-LNX-001)", async () => {
|
|
1739
|
+
const rules = await loadRules(RULES_DIR);
|
|
1740
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-001");
|
|
1741
|
+
assert.ok(rule, "OBOM-LNX-001 rule should exist");
|
|
1742
|
+
|
|
1743
|
+
const bom = makeBom([
|
|
1744
|
+
{
|
|
1745
|
+
type: "data",
|
|
1130
1746
|
name: "evil.service",
|
|
1131
1747
|
version: "",
|
|
1132
1748
|
description: "",
|
|
@@ -1304,11 +1920,7 @@ describe("evaluateRule", () => {
|
|
|
1304
1920
|
|
|
1305
1921
|
const findings = await evaluateRule(rule, bom);
|
|
1306
1922
|
assert.ok(findings.length > 0, "Should detect runner-state mutation");
|
|
1307
|
-
assert.deepStrictEqual(findings[0].attackTactics, [
|
|
1308
|
-
"TA0003",
|
|
1309
|
-
"TA0004",
|
|
1310
|
-
"TA0005",
|
|
1311
|
-
]);
|
|
1923
|
+
assert.deepStrictEqual(findings[0].attackTactics, ["TA0002"]);
|
|
1312
1924
|
});
|
|
1313
1925
|
|
|
1314
1926
|
it("should detect outbound commands that reference sensitive context (CI-015)", async () => {
|
|
@@ -2584,7 +3196,7 @@ describe("evaluateRule", () => {
|
|
|
2584
3196
|
|
|
2585
3197
|
const findings = await evaluateRule(rule, bom);
|
|
2586
3198
|
assert.ok(findings.length > 0, "Should detect privileged listener risk");
|
|
2587
|
-
assert.strictEqual(findings[0].severity, "
|
|
3199
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
2588
3200
|
});
|
|
2589
3201
|
|
|
2590
3202
|
it("should detect interactive sudo execution of package tooling (OBOM-LNX-008)", async () => {
|
|
@@ -2757,6 +3369,103 @@ describe("evaluateRule", () => {
|
|
|
2757
3369
|
assert.strictEqual(findings[0].severity, "high");
|
|
2758
3370
|
});
|
|
2759
3371
|
|
|
3372
|
+
it("should flag scheduled tasks that invoke LOLBAS helpers even when they live under managed Windows namespaces (OBOM-WIN-006)", async () => {
|
|
3373
|
+
const rules = await loadRules(RULES_DIR);
|
|
3374
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-006");
|
|
3375
|
+
assert.ok(rule, "OBOM-WIN-006 rule should exist");
|
|
3376
|
+
const row = {
|
|
3377
|
+
action:
|
|
3378
|
+
"%windir%\\system32\\rundll32.exe Windows.Storage.ApplicationData.dll,CleanupTemporaryState",
|
|
3379
|
+
enabled: "0",
|
|
3380
|
+
hidden: "0",
|
|
3381
|
+
path: "\\Microsoft\\Windows\\applicationdata\\CleanupTemporaryState",
|
|
3382
|
+
};
|
|
3383
|
+
|
|
3384
|
+
const bom = makeBom([
|
|
3385
|
+
{
|
|
3386
|
+
type: "data",
|
|
3387
|
+
name: "CleanupTemporaryState",
|
|
3388
|
+
version: "",
|
|
3389
|
+
description: "",
|
|
3390
|
+
"bom-ref": "osquery:scheduled_tasks:data:CleanupTemporaryState@unknown",
|
|
3391
|
+
properties: [
|
|
3392
|
+
{ name: "cdx:osquery:category", value: "scheduled_tasks" },
|
|
3393
|
+
...createLolbasProperties("scheduled_tasks", row),
|
|
3394
|
+
{ name: "path", value: row.path },
|
|
3395
|
+
{ name: "action", value: row.action },
|
|
3396
|
+
{ name: "enabled", value: row.enabled },
|
|
3397
|
+
{ name: "hidden", value: row.hidden },
|
|
3398
|
+
],
|
|
3399
|
+
},
|
|
3400
|
+
]);
|
|
3401
|
+
|
|
3402
|
+
const findings = await evaluateRule(rule, bom);
|
|
3403
|
+
assert.strictEqual(findings.length, 1);
|
|
3404
|
+
assert.match(findings[0].message, /CleanupTemporaryState|rundll32\.exe/);
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3407
|
+
it("should ignore Windows services whose descriptions merely mention PowerShell or cmd tooling (OBOM-WIN-006)", async () => {
|
|
3408
|
+
const rules = await loadRules(RULES_DIR);
|
|
3409
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-006");
|
|
3410
|
+
assert.ok(rule, "OBOM-WIN-006 rule should exist");
|
|
3411
|
+
const row = {
|
|
3412
|
+
description:
|
|
3413
|
+
"Windows Remote Management can be configured with winrm.cmd and queried from PowerShell.",
|
|
3414
|
+
display_name: "Windows Remote Management",
|
|
3415
|
+
module_path: "C:\\Windows\\System32\\WsmSvc.dll",
|
|
3416
|
+
path: "C:\\Windows\\System32\\svchost.exe -k NetworkService -p",
|
|
3417
|
+
};
|
|
3418
|
+
|
|
3419
|
+
const bom = makeBom([
|
|
3420
|
+
{
|
|
3421
|
+
type: "data",
|
|
3422
|
+
name: "WinRM",
|
|
3423
|
+
version: "4012",
|
|
3424
|
+
description: row.description,
|
|
3425
|
+
"bom-ref": "osquery:services_snapshot:data:WinRM@4012",
|
|
3426
|
+
properties: [
|
|
3427
|
+
{ name: "cdx:osquery:category", value: "services_snapshot" },
|
|
3428
|
+
...createLolbasProperties("services_snapshot", row),
|
|
3429
|
+
{ name: "path", value: row.path },
|
|
3430
|
+
{ name: "module_path", value: row.module_path },
|
|
3431
|
+
{ name: "start_type", value: "AUTO_START" },
|
|
3432
|
+
],
|
|
3433
|
+
},
|
|
3434
|
+
]);
|
|
3435
|
+
|
|
3436
|
+
const findings = await evaluateRule(rule, bom);
|
|
3437
|
+
assert.strictEqual(findings.length, 0);
|
|
3438
|
+
});
|
|
3439
|
+
|
|
3440
|
+
it("should keep suspicious Windows services that launch LOLBAS from user-controlled paths (OBOM-WIN-006)", async () => {
|
|
3441
|
+
const rules = await loadRules(RULES_DIR);
|
|
3442
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-006");
|
|
3443
|
+
assert.ok(rule, "OBOM-WIN-006 rule should exist");
|
|
3444
|
+
const row = {
|
|
3445
|
+
path: "C:\\Users\\Public\\evil\\powershell.exe -enc AAAA",
|
|
3446
|
+
};
|
|
3447
|
+
|
|
3448
|
+
const bom = makeBom([
|
|
3449
|
+
{
|
|
3450
|
+
type: "data",
|
|
3451
|
+
name: "EvilService",
|
|
3452
|
+
version: "1234",
|
|
3453
|
+
description: "",
|
|
3454
|
+
"bom-ref": "osquery:services_snapshot:data:EvilService@1234",
|
|
3455
|
+
properties: [
|
|
3456
|
+
{ name: "cdx:osquery:category", value: "services_snapshot" },
|
|
3457
|
+
...createLolbasProperties("services_snapshot", row),
|
|
3458
|
+
{ name: "path", value: row.path },
|
|
3459
|
+
{ name: "start_type", value: "AUTO_START" },
|
|
3460
|
+
],
|
|
3461
|
+
},
|
|
3462
|
+
]);
|
|
3463
|
+
|
|
3464
|
+
const findings = await evaluateRule(rule, bom);
|
|
3465
|
+
assert.strictEqual(findings.length, 1);
|
|
3466
|
+
assert.match(findings[0].message, /EvilService|powershell\.exe/);
|
|
3467
|
+
});
|
|
3468
|
+
|
|
2760
3469
|
it("should detect WMI or AppCompat LOLBAS persistence (OBOM-WIN-007)", async () => {
|
|
2761
3470
|
const rules = await loadRules(RULES_DIR);
|
|
2762
3471
|
const rule = rules.find((r) => r.id === "OBOM-WIN-007");
|
|
@@ -2941,6 +3650,31 @@ describe("evaluateRules", () => {
|
|
|
2941
3650
|
if (findings.length >= 2) {
|
|
2942
3651
|
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
2943
3652
|
for (let i = 1; i < findings.length; i++) {
|
|
3653
|
+
it("should detect weakened macOS Gatekeeper posture (OBOM-MAC-005)", async () => {
|
|
3654
|
+
const rules = await loadRules(RULES_DIR);
|
|
3655
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-005");
|
|
3656
|
+
assert.ok(rule, "OBOM-MAC-005 rule should exist");
|
|
3657
|
+
|
|
3658
|
+
const bom = makeBom([
|
|
3659
|
+
{
|
|
3660
|
+
type: "data",
|
|
3661
|
+
name: "gatekeeper",
|
|
3662
|
+
version: "",
|
|
3663
|
+
description: "94",
|
|
3664
|
+
purl: "pkg:swid/gatekeeper",
|
|
3665
|
+
"bom-ref": "pkg:swid/gatekeeper",
|
|
3666
|
+
properties: [
|
|
3667
|
+
{ name: "cdx:osquery:category", value: "gatekeeper" },
|
|
3668
|
+
{ name: "assessments_enabled", value: "0" },
|
|
3669
|
+
{ name: "dev_id_enabled", value: "1" },
|
|
3670
|
+
],
|
|
3671
|
+
},
|
|
3672
|
+
]);
|
|
3673
|
+
|
|
3674
|
+
const findings = await evaluateRule(rule, bom);
|
|
3675
|
+
assert.ok(findings.length > 0, "Should detect weakened Gatekeeper");
|
|
3676
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
3677
|
+
});
|
|
2944
3678
|
const prev = severityOrder[findings[i - 1].severity] ?? 4;
|
|
2945
3679
|
const curr = severityOrder[findings[i].severity] ?? 4;
|
|
2946
3680
|
assert.ok(
|
|
@@ -3068,6 +3802,22 @@ describe("auditBom", () => {
|
|
|
3068
3802
|
}
|
|
3069
3803
|
});
|
|
3070
3804
|
|
|
3805
|
+
it("reports active dry-run support counts for package-integrity rules", async () => {
|
|
3806
|
+
const summary = await getBomAuditDryRunSupportSummary({
|
|
3807
|
+
bomAuditCategories: "package-integrity",
|
|
3808
|
+
});
|
|
3809
|
+
assert.deepStrictEqual(summary, {
|
|
3810
|
+
fullCount: 10,
|
|
3811
|
+
noCount: 3,
|
|
3812
|
+
partialCount: 1,
|
|
3813
|
+
totalRules: 14,
|
|
3814
|
+
});
|
|
3815
|
+
assert.match(
|
|
3816
|
+
formatDryRunSupportSummary(summary),
|
|
3817
|
+
/3 rule\(s\) do not support dry-run, 1 rule\(s\) have partial dry-run support, 14 active rule\(s\) total/,
|
|
3818
|
+
);
|
|
3819
|
+
});
|
|
3820
|
+
|
|
3071
3821
|
it("does not flag CI-006 for a safe content-addressed PR cache workflow", async () => {
|
|
3072
3822
|
const bom = makeBomFromWorkflowFixture("cache-pull-request.yml");
|
|
3073
3823
|
|
|
@@ -3373,3 +4123,588 @@ describe("hasCriticalFindings", () => {
|
|
|
3373
4123
|
assert.ok(!hasCriticalFindings(null, {}));
|
|
3374
4124
|
});
|
|
3375
4125
|
});
|
|
4126
|
+
|
|
4127
|
+
describe("additional OBOM and rootfs hardening rules", () => {
|
|
4128
|
+
it("should detect reverse shell behavior (OBOM-LNX-014)", async () => {
|
|
4129
|
+
const rules = await loadRules(RULES_DIR);
|
|
4130
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-014");
|
|
4131
|
+
assert.ok(rule, "OBOM-LNX-014 rule should exist");
|
|
4132
|
+
|
|
4133
|
+
const bom = makeBom([
|
|
4134
|
+
{
|
|
4135
|
+
type: "data",
|
|
4136
|
+
name: "bash",
|
|
4137
|
+
version: "1234",
|
|
4138
|
+
description: "",
|
|
4139
|
+
purl: "pkg:swid/bash@1234",
|
|
4140
|
+
"bom-ref": "pkg:swid/bash@1234",
|
|
4141
|
+
properties: [
|
|
4142
|
+
{ name: "cdx:osquery:category", value: "behavioral_reverse_shell" },
|
|
4143
|
+
{ name: "path", value: "/usr/bin/bash" },
|
|
4144
|
+
{ name: "cmdline", value: "bash -i" },
|
|
4145
|
+
{ name: "parent_cmdline", value: "python -c pty.spawn('bash')" },
|
|
4146
|
+
{ name: "remote_address", value: "203.0.113.10" },
|
|
4147
|
+
{ name: "remote_port", value: "4444" },
|
|
4148
|
+
],
|
|
4149
|
+
},
|
|
4150
|
+
]);
|
|
4151
|
+
|
|
4152
|
+
const findings = await evaluateRule(rule, bom);
|
|
4153
|
+
assert.ok(findings.length > 0, "Should detect reverse shell behavior");
|
|
4154
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
4155
|
+
});
|
|
4156
|
+
|
|
4157
|
+
it("should detect weak sysctl hardening posture (OBOM-LNX-017)", async () => {
|
|
4158
|
+
const rules = await loadRules(RULES_DIR);
|
|
4159
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-017");
|
|
4160
|
+
assert.ok(rule, "OBOM-LNX-017 rule should exist");
|
|
4161
|
+
|
|
4162
|
+
const bom = makeBom([
|
|
4163
|
+
makeComponent("kernel.randomize_va_space", "1", [
|
|
4164
|
+
["cdx:osquery:category", "sysctl_hardening"],
|
|
4165
|
+
]),
|
|
4166
|
+
]);
|
|
4167
|
+
|
|
4168
|
+
const findings = await evaluateRule(rule, bom);
|
|
4169
|
+
assert.ok(findings.length > 0, "Should detect weak sysctl posture");
|
|
4170
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
4171
|
+
});
|
|
4172
|
+
|
|
4173
|
+
it("should detect weak temporary mount protections (OBOM-LNX-018)", async () => {
|
|
4174
|
+
const rules = await loadRules(RULES_DIR);
|
|
4175
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-018");
|
|
4176
|
+
assert.ok(rule, "OBOM-LNX-018 rule should exist");
|
|
4177
|
+
|
|
4178
|
+
const bom = makeBom([
|
|
4179
|
+
makeComponent("/tmp", "rw,nosuid,nodev", [
|
|
4180
|
+
["cdx:osquery:category", "mount_hardening"],
|
|
4181
|
+
["type", "tmpfs"],
|
|
4182
|
+
]),
|
|
4183
|
+
]);
|
|
4184
|
+
|
|
4185
|
+
const findings = await evaluateRule(rule, bom);
|
|
4186
|
+
assert.ok(findings.length > 0, "Should detect missing noexec flag");
|
|
4187
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
4188
|
+
});
|
|
4189
|
+
|
|
4190
|
+
it("should detect GTFOBins-linked privileged runtime activity (OBOM-LNX-019)", async () => {
|
|
4191
|
+
const rules = await loadRules(RULES_DIR);
|
|
4192
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-019");
|
|
4193
|
+
assert.ok(rule, "OBOM-LNX-019 rule should exist");
|
|
4194
|
+
|
|
4195
|
+
const bom = makeBom([
|
|
4196
|
+
makeComponent("bash", "", [
|
|
4197
|
+
["cdx:osquery:category", "sudo_executions"],
|
|
4198
|
+
["cdx:gtfobins:matched", "true"],
|
|
4199
|
+
["cdx:gtfobins:names", "bash"],
|
|
4200
|
+
["cdx:gtfobins:functions", "shell,command"],
|
|
4201
|
+
["cdx:gtfobins:contexts", "sudo,suid"],
|
|
4202
|
+
["cdx:gtfobins:riskTags", "lateral-movement,privilege-escalation"],
|
|
4203
|
+
["path", "/usr/bin/bash"],
|
|
4204
|
+
["cmdline", "bash -c id"],
|
|
4205
|
+
]),
|
|
4206
|
+
]);
|
|
4207
|
+
|
|
4208
|
+
const findings = await evaluateRule(rule, bom);
|
|
4209
|
+
assert.ok(
|
|
4210
|
+
findings.length > 0,
|
|
4211
|
+
"Should detect GTFOBins helper in privileged runtime context",
|
|
4212
|
+
);
|
|
4213
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
4214
|
+
});
|
|
4215
|
+
|
|
4216
|
+
it("should ignore elevated root processes without suspicious path evidence (OBOM-LNX-010)", async () => {
|
|
4217
|
+
const rules = await loadRules(RULES_DIR);
|
|
4218
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-010");
|
|
4219
|
+
assert.ok(rule, "OBOM-LNX-010 rule should exist");
|
|
4220
|
+
|
|
4221
|
+
const bom = makeBom([
|
|
4222
|
+
makeComponent("agetty", "1063", [
|
|
4223
|
+
["cdx:osquery:category", "elevated_processes"],
|
|
4224
|
+
["uid", "0"],
|
|
4225
|
+
["package_source_hint", "unclassified-path"],
|
|
4226
|
+
["cmdline", "/sbin/agetty -o -p -- \\u --noclear - linux"],
|
|
4227
|
+
]),
|
|
4228
|
+
]);
|
|
4229
|
+
|
|
4230
|
+
const findings = await evaluateRule(rule, bom);
|
|
4231
|
+
assert.strictEqual(
|
|
4232
|
+
findings.length,
|
|
4233
|
+
0,
|
|
4234
|
+
"Should ignore root processes that lack concrete suspicious path evidence",
|
|
4235
|
+
);
|
|
4236
|
+
});
|
|
4237
|
+
|
|
4238
|
+
it("should detect elevated root processes from user-controlled command paths (OBOM-LNX-010)", async () => {
|
|
4239
|
+
const rules = await loadRules(RULES_DIR);
|
|
4240
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-010");
|
|
4241
|
+
assert.ok(rule, "OBOM-LNX-010 rule should exist");
|
|
4242
|
+
|
|
4243
|
+
const bom = makeBom([
|
|
4244
|
+
makeComponent("evil-root-job", "1", [
|
|
4245
|
+
["cdx:osquery:category", "elevated_processes"],
|
|
4246
|
+
["uid", "0"],
|
|
4247
|
+
["package_source_hint", "user-writable-path"],
|
|
4248
|
+
["cmdline", "/home/demo/.local/bin/evil-root-job --daemon"],
|
|
4249
|
+
]),
|
|
4250
|
+
]);
|
|
4251
|
+
|
|
4252
|
+
const findings = await evaluateRule(rule, bom);
|
|
4253
|
+
assert.ok(
|
|
4254
|
+
findings.length > 0,
|
|
4255
|
+
"Should detect root processes sourced from user-controlled command paths",
|
|
4256
|
+
);
|
|
4257
|
+
assert.match(findings[0].message, /evil-root-job/);
|
|
4258
|
+
});
|
|
4259
|
+
|
|
4260
|
+
it("should ignore routine elevated GTFOBins services without suspicious path evidence (OBOM-LNX-019)", async () => {
|
|
4261
|
+
const rules = await loadRules(RULES_DIR);
|
|
4262
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-019");
|
|
4263
|
+
assert.ok(rule, "OBOM-LNX-019 rule should exist");
|
|
4264
|
+
|
|
4265
|
+
const bom = makeBom([
|
|
4266
|
+
makeComponent("fail2ban-server", "1", [
|
|
4267
|
+
["cdx:osquery:category", "elevated_processes"],
|
|
4268
|
+
["cdx:gtfobins:matched", "true"],
|
|
4269
|
+
["cdx:gtfobins:names", "python"],
|
|
4270
|
+
["cdx:gtfobins:functions", "shell,reverse-shell"],
|
|
4271
|
+
["cdx:gtfobins:contexts", "sudo,suid,unprivileged"],
|
|
4272
|
+
["package_source_hint", "unclassified-path"],
|
|
4273
|
+
["cmdline", "/usr/bin/python3 /usr/bin/fail2ban-server -xf start"],
|
|
4274
|
+
]),
|
|
4275
|
+
]);
|
|
4276
|
+
|
|
4277
|
+
const findings = await evaluateRule(rule, bom);
|
|
4278
|
+
assert.strictEqual(
|
|
4279
|
+
findings.length,
|
|
4280
|
+
0,
|
|
4281
|
+
"Should ignore routine elevated GTFOBins matches without suspicious path context",
|
|
4282
|
+
);
|
|
4283
|
+
});
|
|
4284
|
+
|
|
4285
|
+
it("should detect APT sources that still use plaintext HTTP transport (OBOM-LNX-021)", async () => {
|
|
4286
|
+
const rules = await loadRules(RULES_DIR);
|
|
4287
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-021");
|
|
4288
|
+
assert.ok(rule, "OBOM-LNX-021 rule should exist");
|
|
4289
|
+
|
|
4290
|
+
const bom = makeBom([
|
|
4291
|
+
makeComponent("gb.archive.ubuntu.com/ubuntu+noble", "24.04", [
|
|
4292
|
+
["cdx:osquery:category", "apt_sources"],
|
|
4293
|
+
["base_uri", "http://gb.archive.ubuntu.com/ubuntu"],
|
|
4294
|
+
["components", "main restricted universe multiverse"],
|
|
4295
|
+
["maintainer", "Ubuntu"],
|
|
4296
|
+
["release", "noble"],
|
|
4297
|
+
["source", "/etc/apt/sources.list.d/ubuntu.sources"],
|
|
4298
|
+
]),
|
|
4299
|
+
]);
|
|
4300
|
+
|
|
4301
|
+
const findings = await evaluateRule(rule, bom);
|
|
4302
|
+
assert.ok(findings.length > 0, "Should flag HTTP-backed APT sources");
|
|
4303
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
4304
|
+
assert.match(findings[0].message, /plaintext HTTP transport/);
|
|
4305
|
+
});
|
|
4306
|
+
|
|
4307
|
+
it("should detect authorized_keys entries that still use ssh-rsa (OBOM-LNX-022)", async () => {
|
|
4308
|
+
const rules = await loadRules(RULES_DIR);
|
|
4309
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-022");
|
|
4310
|
+
assert.ok(rule, "OBOM-LNX-022 rule should exist");
|
|
4311
|
+
|
|
4312
|
+
const bom = makeBom([
|
|
4313
|
+
makeComponent("appthreat", "ssh-rsa", [
|
|
4314
|
+
["cdx:osquery:category", "authorized_keys_snapshot"],
|
|
4315
|
+
["key_file", "/home/appthreat/.ssh/authorized_keys"],
|
|
4316
|
+
["uid", "1000"],
|
|
4317
|
+
]),
|
|
4318
|
+
]);
|
|
4319
|
+
|
|
4320
|
+
const findings = await evaluateRule(rule, bom);
|
|
4321
|
+
assert.ok(
|
|
4322
|
+
findings.length > 0,
|
|
4323
|
+
"Should flag deprecated ssh-rsa authorized_keys entries",
|
|
4324
|
+
);
|
|
4325
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
4326
|
+
assert.match(findings[0].message, /deprecated ssh-rsa/);
|
|
4327
|
+
});
|
|
4328
|
+
|
|
4329
|
+
it("should classify managed privileged listeners as medium exposure review (OBOM-LNX-006)", async () => {
|
|
4330
|
+
const rules = await loadRules(RULES_DIR);
|
|
4331
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-006");
|
|
4332
|
+
assert.ok(rule, "OBOM-LNX-006 rule should exist");
|
|
4333
|
+
|
|
4334
|
+
const bom = makeBom([
|
|
4335
|
+
makeComponent("nginx", "", [
|
|
4336
|
+
["cdx:osquery:category", "privileged_listening_ports"],
|
|
4337
|
+
["address", "0.0.0.0"],
|
|
4338
|
+
["port", "443"],
|
|
4339
|
+
["path", "/usr/sbin/nginx"],
|
|
4340
|
+
["account", "root"],
|
|
4341
|
+
["package_source_hint", "package-managed"],
|
|
4342
|
+
]),
|
|
4343
|
+
]);
|
|
4344
|
+
|
|
4345
|
+
const findings = await evaluateRule(rule, bom);
|
|
4346
|
+
assert.ok(findings.length > 0, "Should flag non-local privileged listener");
|
|
4347
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
4348
|
+
assert.match(findings[0].message, /\/usr\/sbin\/nginx/);
|
|
4349
|
+
});
|
|
4350
|
+
|
|
4351
|
+
it("should keep writable-path privileged listeners as high-signal findings (OBOM-LNX-020)", async () => {
|
|
4352
|
+
const rules = await loadRules(RULES_DIR);
|
|
4353
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-020");
|
|
4354
|
+
assert.ok(rule, "OBOM-LNX-020 rule should exist");
|
|
4355
|
+
|
|
4356
|
+
const bom = makeBom([
|
|
4357
|
+
makeComponent("evil-listener", "", [
|
|
4358
|
+
["cdx:osquery:category", "privileged_listening_ports"],
|
|
4359
|
+
["address", "0.0.0.0"],
|
|
4360
|
+
["port", "8443"],
|
|
4361
|
+
["path", "/home/demo/.local/bin/evil-listener"],
|
|
4362
|
+
["account", "root"],
|
|
4363
|
+
["package_source_hint", "user-writable-path"],
|
|
4364
|
+
]),
|
|
4365
|
+
]);
|
|
4366
|
+
|
|
4367
|
+
const findings = await evaluateRule(rule, bom);
|
|
4368
|
+
assert.ok(
|
|
4369
|
+
findings.length > 0,
|
|
4370
|
+
"Should flag privileged listener from writable path",
|
|
4371
|
+
);
|
|
4372
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
4373
|
+
assert.match(findings[0].message, /evil-listener/);
|
|
4374
|
+
});
|
|
4375
|
+
|
|
4376
|
+
it("should detect risky Public profile firewall rules (OBOM-WIN-011)", async () => {
|
|
4377
|
+
const rules = await loadRules(RULES_DIR);
|
|
4378
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-011");
|
|
4379
|
+
assert.ok(rule, "OBOM-WIN-011 rule should exist");
|
|
4380
|
+
|
|
4381
|
+
const bom = makeBom([
|
|
4382
|
+
makeComponent("RDP public allow", "", [
|
|
4383
|
+
["cdx:osquery:category", "windows_firewall_rules"],
|
|
4384
|
+
["enabled", "true"],
|
|
4385
|
+
["direction", "in"],
|
|
4386
|
+
["action", "allow"],
|
|
4387
|
+
["profile", "Public"],
|
|
4388
|
+
["local_ports", "3389"],
|
|
4389
|
+
]),
|
|
4390
|
+
]);
|
|
4391
|
+
|
|
4392
|
+
const findings = await evaluateRule(rule, bom);
|
|
4393
|
+
assert.ok(findings.length > 0, "Should detect Public inbound allow rule");
|
|
4394
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
4395
|
+
});
|
|
4396
|
+
|
|
4397
|
+
it("should detect invalid Authenticode on startup artifacts (OBOM-WIN-012)", async () => {
|
|
4398
|
+
const rules = await loadRules(RULES_DIR);
|
|
4399
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-012");
|
|
4400
|
+
assert.ok(rule, "OBOM-WIN-012 rule should exist");
|
|
4401
|
+
|
|
4402
|
+
const bom = makeBom([
|
|
4403
|
+
makeComponent("Updater", "", [
|
|
4404
|
+
["cdx:osquery:category", "windows_run_keys"],
|
|
4405
|
+
["path", "C:\\Users\\Public\\updater.exe"],
|
|
4406
|
+
["cdx:windows:authenticode:status", "NotSigned"],
|
|
4407
|
+
["cdx:windows:authenticode:signerSubject", ""],
|
|
4408
|
+
]),
|
|
4409
|
+
]);
|
|
4410
|
+
|
|
4411
|
+
const findings = await evaluateRule(rule, bom);
|
|
4412
|
+
assert.ok(findings.length > 0, "Should detect invalid Authenticode status");
|
|
4413
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
4414
|
+
});
|
|
4415
|
+
|
|
4416
|
+
it("should not treat unresolved Authenticode status as definitively invalid (OBOM-WIN-012)", async () => {
|
|
4417
|
+
const rules = await loadRules(RULES_DIR);
|
|
4418
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-012");
|
|
4419
|
+
assert.ok(rule, "OBOM-WIN-012 rule should exist");
|
|
4420
|
+
|
|
4421
|
+
const bom = makeBom([
|
|
4422
|
+
makeComponent("Updater", "", [
|
|
4423
|
+
["cdx:osquery:category", "windows_run_keys"],
|
|
4424
|
+
["path", "C:\\Users\\Public\\updater.exe"],
|
|
4425
|
+
["cdx:windows:authenticode:status", "UnknownError"],
|
|
4426
|
+
]),
|
|
4427
|
+
]);
|
|
4428
|
+
|
|
4429
|
+
const findings = await evaluateRule(rule, bom);
|
|
4430
|
+
assert.strictEqual(findings.length, 0);
|
|
4431
|
+
});
|
|
4432
|
+
|
|
4433
|
+
it("should detect missing WDAC policy enforcement (OBOM-WIN-013)", async () => {
|
|
4434
|
+
const rules = await loadRules(RULES_DIR);
|
|
4435
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-013");
|
|
4436
|
+
assert.ok(rule, "OBOM-WIN-013 rule should exist");
|
|
4437
|
+
|
|
4438
|
+
const bom = makeBom([
|
|
4439
|
+
makeComponent("wdac-active-policies", "observed", [
|
|
4440
|
+
["cdx:windows:wdac:activePolicyCount", "0"],
|
|
4441
|
+
]),
|
|
4442
|
+
]);
|
|
4443
|
+
|
|
4444
|
+
const findings = await evaluateRule(rule, bom);
|
|
4445
|
+
assert.ok(
|
|
4446
|
+
findings.length > 0,
|
|
4447
|
+
"Should detect missing WDAC policy enforcement",
|
|
4448
|
+
);
|
|
4449
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
4450
|
+
});
|
|
4451
|
+
|
|
4452
|
+
it("should detect failed macOS notarization assessments with registration and target paths (OBOM-MAC-007)", async () => {
|
|
4453
|
+
const rules = await loadRules(RULES_DIR);
|
|
4454
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-007");
|
|
4455
|
+
assert.ok(rule, "OBOM-MAC-007 rule should exist");
|
|
4456
|
+
|
|
4457
|
+
const bom = makeBom([
|
|
4458
|
+
makeComponent("org.example.agent", "", [
|
|
4459
|
+
["cdx:osquery:category", "launchd_services"],
|
|
4460
|
+
["path", "/Library/LaunchDaemons/org.example.agent.plist"],
|
|
4461
|
+
["program", "/Applications/Suspicious.app/Contents/MacOS/Suspicious"],
|
|
4462
|
+
["cdx:darwin:codesign:teamIdentifier", "ABCDE12345"],
|
|
4463
|
+
["cdx:darwin:notarization:assessment", "rejected"],
|
|
4464
|
+
]),
|
|
4465
|
+
]);
|
|
4466
|
+
|
|
4467
|
+
const findings = await evaluateRule(rule, bom);
|
|
4468
|
+
assert.ok(
|
|
4469
|
+
findings.length > 0,
|
|
4470
|
+
"Should detect failed notarization assessment",
|
|
4471
|
+
);
|
|
4472
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
4473
|
+
assert.match(
|
|
4474
|
+
findings[0].message,
|
|
4475
|
+
/\/Library\/LaunchDaemons\/org\.example\.agent\.plist/,
|
|
4476
|
+
);
|
|
4477
|
+
assert.match(
|
|
4478
|
+
findings[0].message,
|
|
4479
|
+
/\/Applications\/Suspicious\.app\/Contents\/MacOS\/Suspicious/,
|
|
4480
|
+
);
|
|
4481
|
+
assert.strictEqual(
|
|
4482
|
+
findings[0].evidence.registrationPath,
|
|
4483
|
+
"/Library/LaunchDaemons/org.example.agent.plist",
|
|
4484
|
+
);
|
|
4485
|
+
assert.strictEqual(
|
|
4486
|
+
findings[0].evidence.targetPath,
|
|
4487
|
+
"/Applications/Suspicious.app/Contents/MacOS/Suspicious",
|
|
4488
|
+
);
|
|
4489
|
+
});
|
|
4490
|
+
|
|
4491
|
+
it("should ignore unknown notarization assessments for Apple-managed system apps (OBOM-MAC-008)", async () => {
|
|
4492
|
+
const rules = await loadRules(RULES_DIR);
|
|
4493
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-008");
|
|
4494
|
+
assert.ok(rule, "OBOM-MAC-008 rule should exist");
|
|
4495
|
+
|
|
4496
|
+
const bom = makeBom([
|
|
4497
|
+
makeComponent("Finder.app", "", [
|
|
4498
|
+
["cdx:osquery:category", "running_apps"],
|
|
4499
|
+
["bundle_path", "/System/Library/CoreServices/Finder.app"],
|
|
4500
|
+
[
|
|
4501
|
+
"bundle_executable",
|
|
4502
|
+
"/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder",
|
|
4503
|
+
],
|
|
4504
|
+
["cdx:darwin:notarization:assessment", "unknown"],
|
|
4505
|
+
]),
|
|
4506
|
+
]);
|
|
4507
|
+
|
|
4508
|
+
const findings = await evaluateRule(rule, bom);
|
|
4509
|
+
assert.strictEqual(
|
|
4510
|
+
findings.length,
|
|
4511
|
+
0,
|
|
4512
|
+
"Should not alert on Apple-managed system apps with unknown assessment",
|
|
4513
|
+
);
|
|
4514
|
+
});
|
|
4515
|
+
|
|
4516
|
+
it("should ignore rejected notarization on generic installed macOS app inventory (OBOM-MAC-007)", async () => {
|
|
4517
|
+
const rules = await loadRules(RULES_DIR);
|
|
4518
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-007");
|
|
4519
|
+
assert.ok(rule, "OBOM-MAC-007 rule should exist");
|
|
4520
|
+
|
|
4521
|
+
const bom = makeBom([
|
|
4522
|
+
makeComponent("Installed.app", "", [
|
|
4523
|
+
["cdx:osquery:category", "apps"],
|
|
4524
|
+
["bundle_path", "/Applications/Installed.app"],
|
|
4525
|
+
[
|
|
4526
|
+
"bundle_executable",
|
|
4527
|
+
"/Applications/Installed.app/Contents/MacOS/Installed",
|
|
4528
|
+
],
|
|
4529
|
+
["cdx:darwin:notarization:assessment", "rejected"],
|
|
4530
|
+
]),
|
|
4531
|
+
]);
|
|
4532
|
+
|
|
4533
|
+
const findings = await evaluateRule(rule, bom);
|
|
4534
|
+
assert.strictEqual(
|
|
4535
|
+
findings.length,
|
|
4536
|
+
0,
|
|
4537
|
+
"Should not alert on generic installed app inventory",
|
|
4538
|
+
);
|
|
4539
|
+
});
|
|
4540
|
+
|
|
4541
|
+
it("should ignore Apple-managed system launchd services for notarization review (OBOM-MAC-007)", async () => {
|
|
4542
|
+
const rules = await loadRules(RULES_DIR);
|
|
4543
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-007");
|
|
4544
|
+
assert.ok(rule, "OBOM-MAC-007 rule should exist");
|
|
4545
|
+
|
|
4546
|
+
const bom = makeBom([
|
|
4547
|
+
makeComponent("com.apple.test", "", [
|
|
4548
|
+
["cdx:osquery:category", "launchd_services"],
|
|
4549
|
+
["path", "/System/Library/LaunchDaemons/com.apple.test.plist"],
|
|
4550
|
+
["program", "/usr/libexec/testd"],
|
|
4551
|
+
["cdx:darwin:notarization:assessment", "rejected"],
|
|
4552
|
+
]),
|
|
4553
|
+
]);
|
|
4554
|
+
|
|
4555
|
+
const findings = await evaluateRule(rule, bom);
|
|
4556
|
+
assert.strictEqual(
|
|
4557
|
+
findings.length,
|
|
4558
|
+
0,
|
|
4559
|
+
"Should not alert on Apple-managed system launchd services",
|
|
4560
|
+
);
|
|
4561
|
+
});
|
|
4562
|
+
|
|
4563
|
+
it("should keep unknown notarization findings for user-controlled macOS launch agents (OBOM-MAC-008)", async () => {
|
|
4564
|
+
const rules = await loadRules(RULES_DIR);
|
|
4565
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-008");
|
|
4566
|
+
assert.ok(rule, "OBOM-MAC-008 rule should exist");
|
|
4567
|
+
|
|
4568
|
+
const bom = makeBom([
|
|
4569
|
+
makeComponent("com.jetbrains.AppCode.BridgeService.plist", "", [
|
|
4570
|
+
["cdx:osquery:category", "launchd_services"],
|
|
4571
|
+
[
|
|
4572
|
+
"path",
|
|
4573
|
+
"/Users/prabhu/Library/LaunchAgents/com.jetbrains.AppCode.BridgeService.plist",
|
|
4574
|
+
],
|
|
4575
|
+
[
|
|
4576
|
+
"program",
|
|
4577
|
+
"/Users/prabhu/Applications/Rider.app/Contents/bin/Bridge.framework/Versions/A/Resources/BridgeService",
|
|
4578
|
+
],
|
|
4579
|
+
["cdx:darwin:notarization:assessment", "unknown"],
|
|
4580
|
+
]),
|
|
4581
|
+
]);
|
|
4582
|
+
|
|
4583
|
+
const findings = await evaluateRule(rule, bom);
|
|
4584
|
+
assert.strictEqual(findings.length, 1);
|
|
4585
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
4586
|
+
assert.match(findings[0].message, /Users\/prabhu\/Library\/LaunchAgents/);
|
|
4587
|
+
assert.match(
|
|
4588
|
+
findings[0].message,
|
|
4589
|
+
/Users\/prabhu\/Applications\/Rider\.app/,
|
|
4590
|
+
);
|
|
4591
|
+
});
|
|
4592
|
+
|
|
4593
|
+
it("should render actionable Windows Authenticode findings with registration and target paths (OBOM-WIN-014)", async () => {
|
|
4594
|
+
const rules = await loadRules(RULES_DIR);
|
|
4595
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-014");
|
|
4596
|
+
assert.ok(rule, "OBOM-WIN-014 rule should exist");
|
|
4597
|
+
|
|
4598
|
+
const bom = makeBom([
|
|
4599
|
+
makeComponent("\\Vendor\\Updater", "", [
|
|
4600
|
+
["cdx:osquery:category", "scheduled_tasks"],
|
|
4601
|
+
["path", "\\Vendor\\Updater"],
|
|
4602
|
+
["action", "C:\\ProgramData\\Vendor\\updater.exe"],
|
|
4603
|
+
["cdx:windows:authenticode:status", "UnknownError"],
|
|
4604
|
+
["cdx:windows:authenticode:signerSubject", "CN=Unknown"],
|
|
4605
|
+
]),
|
|
4606
|
+
]);
|
|
4607
|
+
|
|
4608
|
+
const findings = await evaluateRule(rule, bom);
|
|
4609
|
+
assert.ok(
|
|
4610
|
+
findings.length > 0,
|
|
4611
|
+
"Should detect unresolved Authenticode status on user-controlled path",
|
|
4612
|
+
);
|
|
4613
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
4614
|
+
assert.match(findings[0].message, /\\Vendor\\Updater/);
|
|
4615
|
+
assert.match(findings[0].message, /C:\\ProgramData\\Vendor\\updater\.exe/);
|
|
4616
|
+
assert.strictEqual(
|
|
4617
|
+
findings[0].evidence.registrationPath,
|
|
4618
|
+
"\\Vendor\\Updater",
|
|
4619
|
+
);
|
|
4620
|
+
assert.strictEqual(
|
|
4621
|
+
findings[0].evidence.targetPath,
|
|
4622
|
+
"C:\\ProgramData\\Vendor\\updater.exe",
|
|
4623
|
+
);
|
|
4624
|
+
});
|
|
4625
|
+
|
|
4626
|
+
it("should ignore unresolved Authenticode on Windows startup shortcut files (OBOM-WIN-014)", async () => {
|
|
4627
|
+
const rules = await loadRules(RULES_DIR);
|
|
4628
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-014");
|
|
4629
|
+
assert.ok(rule, "OBOM-WIN-014 rule should exist");
|
|
4630
|
+
|
|
4631
|
+
const bom = makeBom([
|
|
4632
|
+
makeComponent("Service Fabric Local Cluster Manager.lnk", "", [
|
|
4633
|
+
["cdx:osquery:category", "startup_items"],
|
|
4634
|
+
[
|
|
4635
|
+
"path",
|
|
4636
|
+
"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\Service Fabric Local Cluster Manager.lnk",
|
|
4637
|
+
],
|
|
4638
|
+
["cdx:windows:authenticode:status", "UnknownError"],
|
|
4639
|
+
]),
|
|
4640
|
+
]);
|
|
4641
|
+
|
|
4642
|
+
const findings = await evaluateRule(rule, bom);
|
|
4643
|
+
assert.strictEqual(findings.length, 0);
|
|
4644
|
+
});
|
|
4645
|
+
|
|
4646
|
+
it("should detect yum repositories with gpgcheck disabled (RFS-002)", async () => {
|
|
4647
|
+
const rules = await loadRules(RULES_DIR);
|
|
4648
|
+
const rule = rules.find((r) => r.id === "RFS-002");
|
|
4649
|
+
assert.ok(rule, "RFS-002 rule should exist");
|
|
4650
|
+
|
|
4651
|
+
const bom = makeBom([
|
|
4652
|
+
{
|
|
4653
|
+
type: "data",
|
|
4654
|
+
name: "internal.repo",
|
|
4655
|
+
version: "configured",
|
|
4656
|
+
purl: "pkg:generic/os-repository/internal.repo@configured",
|
|
4657
|
+
"bom-ref": "pkg:generic/os-repository/internal.repo@configured",
|
|
4658
|
+
properties: [
|
|
4659
|
+
{ name: "SrcFile", value: "/etc/yum.repos.d/internal.repo" },
|
|
4660
|
+
{ name: "cdx:os:repo:type", value: "yum-repository" },
|
|
4661
|
+
{ name: "cdx:os:repo:enabled", value: "true" },
|
|
4662
|
+
{
|
|
4663
|
+
name: "cdx:os:repo:url",
|
|
4664
|
+
value: "https://repo.example.invalid/baseos",
|
|
4665
|
+
},
|
|
4666
|
+
{ name: "cdx:os:repo:gpgcheck", value: "0" },
|
|
4667
|
+
],
|
|
4668
|
+
},
|
|
4669
|
+
]);
|
|
4670
|
+
|
|
4671
|
+
const findings = await evaluateRule(rule, bom);
|
|
4672
|
+
assert.ok(findings.length > 0, "Should detect disabled gpgcheck");
|
|
4673
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
4674
|
+
});
|
|
4675
|
+
|
|
4676
|
+
it("should detect services executing from temporary paths (RFS-005)", async () => {
|
|
4677
|
+
const rules = await loadRules(RULES_DIR);
|
|
4678
|
+
const rule = rules.find((r) => r.id === "RFS-005");
|
|
4679
|
+
assert.ok(rule, "RFS-005 rule should exist");
|
|
4680
|
+
|
|
4681
|
+
const bom = makeBom(
|
|
4682
|
+
[],
|
|
4683
|
+
[],
|
|
4684
|
+
[],
|
|
4685
|
+
[
|
|
4686
|
+
{
|
|
4687
|
+
name: "evil-service",
|
|
4688
|
+
version: "1.0.0",
|
|
4689
|
+
"bom-ref": "urn:service:systemd:test:evil-service",
|
|
4690
|
+
properties: [
|
|
4691
|
+
{ name: "SrcFile", value: "/etc/systemd/system/evil.service" },
|
|
4692
|
+
{ name: "cdx:service:manager", value: "systemd" },
|
|
4693
|
+
{ name: "cdx:service:ExecStart", value: "/tmp/run-evil.sh" },
|
|
4694
|
+
{
|
|
4695
|
+
name: "cdx:service:packageRef",
|
|
4696
|
+
value: "pkg:deb/debian/evil@1.0.0",
|
|
4697
|
+
},
|
|
4698
|
+
],
|
|
4699
|
+
},
|
|
4700
|
+
],
|
|
4701
|
+
);
|
|
4702
|
+
|
|
4703
|
+
const findings = await evaluateRule(rule, bom);
|
|
4704
|
+
assert.ok(
|
|
4705
|
+
findings.length > 0,
|
|
4706
|
+
"Should detect writable-path service execution",
|
|
4707
|
+
);
|
|
4708
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
4709
|
+
});
|
|
4710
|
+
});
|