@cyclonedx/cdxgen 12.3.3 → 12.4.0

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