feat(go): port FIM/pkgdb/rootcheck integrity engines and add go-sidecar dashboard consumer

Adds sidecar-only, --state-dir-gated FIM, package-DB-anchor, and rootcheck
engines to the Go migration sidecar, wired into agent.Sweep/Initialize
behind the existing baseline lifecycle. Adds a read-only, schema-checked
Python dashboard consumer (go_sidecar_state_dir, /api/go-sidecar/*, Sidecar
tab) that never starts, stops, or mutates the Go sidecar's state.

Also fixes drift found while reconciling this work: enodia_sentinel/web.py
had reinvented local schema constants instead of using the canonical
enodia_sentinel/schemas.py catalog (now registers enodia.go.sidecar.v1
there and reuses ALERT_SNAPSHOT_V1/INCIDENT_V1); docs/RULES.md had drifted
from what `rules docs` actually generates for SIDs 100069-100078 (ruleops.py
was missing metadata for four SIDs and had stale drill text for four more),
now back in sync with a regression test pinning them together.

Updates CLAUDE.md, README.md, go-agent/README.md, and docs/{ROADMAP,
GO_PORT_HANDOFF,SURICATA_ASSIMILATION,THREAT_MODEL,OPERATIONS,SCHEMAS,
COMMAND_REFERENCE}.md to reflect the landed work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQivSKBqQJsayz1xcYqWzZ
This commit is contained in:
Luna 2026-07-24 09:59:09 -07:00
parent 1d1dee7f6e
commit d835386381
No known key found for this signature in database
43 changed files with 3419 additions and 72 deletions

View file

@ -37,6 +37,19 @@ def _write_alert(tmp: Path, name: str, severity: str, sigs):
(tmp / f"{name}.log").write_text(f"=== ENODIA SENTINEL ALERT ===\n{severity}\n")
def _write_go_alert(tmp: Path, name: str = "alert-20260722-101112"):
(tmp / f"{name}.json").write_text(json.dumps({
"schema": "enodia.alert.snapshot.v1",
"time": "2026-07-22T10:11:12Z", "host": "go-host", "severity": "HIGH",
"incident_id": "inc-go-1",
"alerts": [{"sid": 100017, "severity": "HIGH", "signature": "fim_modified",
"classtype": "integrity-violation", "key": "fim:mod:/etc/passwd",
"detail": "changed", "pids": []}],
"processes": [{"pid": 42}], "enrichment": {"source": "enodia-sentinel-go"},
}))
(tmp / f"{name}.log").write_text("go sidecar alert\n")
def _incident_alert(sig: str, pids=()):
return Alert(Severity.CRITICAL, sig, f"k:{sig}", f"{sig} detail",
tuple(pids), sid=100010, classtype="test")
@ -133,6 +146,39 @@ class TestDataLayer(unittest.TestCase):
self.assertEqual(st["ebpf_exec"], "enabled")
self.assertEqual(st["ebpf_syscall"], "disabled (not running as root)")
def test_go_sidecar_data_is_separate_read_only_evidence(self):
sidecar = self.tmp / "go-state"
sidecar.mkdir()
self.cfg.go_sidecar_state_dir = str(sidecar)
(sidecar / "heartbeat").write_text("1000\n")
_write_go_alert(sidecar)
(sidecar / "incidents.json").write_text(json.dumps({
"inc-go-1": {"schema": "enodia.incident.v1", "id": "inc-go-1",
"last_ts": 1000.0, "snapshots": ["alert-20260722-101112.log"]},
}))
(sidecar / "events.jsonl.1").write_text('{"sequence": 1}\n')
(sidecar / "events.jsonl").write_text('{"sequence": 2}\n')
status = web.go_sidecar_status(self.cfg, now=1010)
self.assertTrue(status["configured"])
self.assertEqual(status["heartbeat"]["status"], "fresh")
alerts = web.list_go_sidecar_alerts(self.cfg)
self.assertEqual(alerts[0]["source"], "go-sidecar")
self.assertEqual(web.get_go_sidecar_alert(self.cfg, "../../etc/passwd"), None)
self.assertEqual(web.go_sidecar_events(self.cfg)["events"][-1]["sequence"], 2)
incident_view = web.get_go_sidecar_incident(self.cfg, "inc-go-1")
self.assertEqual(incident_view["source"], "go-sidecar")
self.assertEqual(incident_view["timeline"][0]["pids"], [42])
def test_go_sidecar_events_fail_closed_on_corruption(self):
sidecar = self.tmp / "go-state"
sidecar.mkdir()
self.cfg.go_sidecar_state_dir = str(sidecar)
(sidecar / "events.jsonl").write_text("not json\n")
report = web.go_sidecar_events(self.cfg)
self.assertEqual(report["events"], [])
self.assertIn("corrupt", report["error"])
def test_incident_and_response_plan_data(self):
_write_alert(self.tmp, "alert-20260531-000001", "CRITICAL", ["reverse_shell"])
iid = incident.record(self.cfg, "alert-20260531-000001.log",
@ -267,6 +313,14 @@ class TestDataLayer(unittest.TestCase):
self.assertIn('function renderIntegrity()', html)
self.assertIn('Integrity state', html)
def test_dashboard_has_sidecar_evidence_console(self):
html = (web._STATIC / "dashboard.html").read_text()
self.assertIn('data-tab="sidecar"', html)
self.assertIn('id="sidecarTab"', html)
self.assertIn('function renderSidecar()', html)
self.assertIn('/api/go-sidecar/status', html)
self.assertIn('Migration evidence is shown separately', html)
class TestNetworkHelpers(unittest.TestCase):
def test_is_loopback(self):
@ -371,6 +425,17 @@ class TestAuth(unittest.TestCase):
self.assertIn("anchors", data)
self.assertTrue(data["read_only"])
def test_go_sidecar_endpoints(self):
sidecar = self.tmp / "go-state"
sidecar.mkdir()
self.cfg.go_sidecar_state_dir = str(sidecar)
(sidecar / "heartbeat").write_text("1000\n")
_write_go_alert(sidecar)
status = json.loads(self._get("/api/go-sidecar/status", token="secret-token").read())
self.assertTrue(status["configured"])
alerts = json.loads(self._get("/api/go-sidecar/alerts", token="secret-token").read())
self.assertEqual(alerts[0]["source"], "go-sidecar")
if __name__ == "__main__":
unittest.main()