Add dashboard integrity/watchdog console
Surface integrity and watchdog state in the read-only dashboard via a new /api/integrity endpoint and integrity_report(): watchdog/heartbeat verdict, FIM baseline and package-DB anchor freshness, pacman keyring and SigLevel posture, and Sentinel's own self-integrity footprint. The endpoint summarizes existing anchors and heartbeats only — it runs no live FIM or package verification from the request path, keeping the dashboard read-only. Add the enodia.integrity.v1 schema (schemas.py + docs/SCHEMAS.md) with a contract test, a new "Integrity" dashboard tab and summary metric, and web tests for the report shape, schema contract, endpoint, and console wiring. Docs updated; completes the v1.0 "dashboard to local console" roadmap item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e0e5cd62d9
commit
11d91a40b4
12 changed files with 337 additions and 29 deletions
|
|
@ -178,6 +178,24 @@ class TestSchemaContracts(unittest.TestCase):
|
|||
"heartbeat_stale": bool,
|
||||
})
|
||||
|
||||
def test_integrity_v1_contract(self):
|
||||
report = web.integrity_report(self.cfg, status={
|
||||
"running": True,
|
||||
"heartbeat_age": 1.0,
|
||||
"heartbeat_stale": False,
|
||||
})
|
||||
self.assertEqual(report["schema"], schemas.INTEGRITY_V1)
|
||||
_assert_types(self, report, {
|
||||
"schema": str,
|
||||
"generated_at": float,
|
||||
"status": str,
|
||||
"checks": dict,
|
||||
"watchdog": dict,
|
||||
"anchors": dict,
|
||||
"sentinel_footprint": dict,
|
||||
"read_only": bool,
|
||||
})
|
||||
|
||||
def test_response_plan_and_audit_contracts(self):
|
||||
iid = self._record_incident_with_snapshot()
|
||||
bundle = respond.load_bundle(self.cfg, iid)
|
||||
|
|
|
|||
|
|
@ -172,6 +172,28 @@ class TestDataLayer(unittest.TestCase):
|
|||
self.assertEqual(rule["event"], "exec")
|
||||
self.assertIn("conditions", rule)
|
||||
|
||||
def test_integrity_report_shape(self):
|
||||
(self.tmp / "fim-baseline.json").write_text(json.dumps({
|
||||
"/etc/passwd": {"sha256": "a"},
|
||||
"/etc/sudoers": {"sha256": "b"},
|
||||
}))
|
||||
(self.tmp / "pkgdb-anchor.json").write_text(json.dumps({
|
||||
"fingerprint": "0123456789abcdef0123456789abcdef",
|
||||
"time": 999.0,
|
||||
}))
|
||||
status = {
|
||||
"running": True,
|
||||
"heartbeat_age": 10.0,
|
||||
"heartbeat_stale": False,
|
||||
}
|
||||
report = web.integrity_report(self.cfg, status=status, now=1000.0)
|
||||
self.assertEqual(report["schema"], "enodia.integrity.v1")
|
||||
self.assertEqual(report["checks"]["watchdog"], "ok")
|
||||
self.assertEqual(report["anchors"]["fim_baseline"]["count"], 2)
|
||||
self.assertEqual(report["anchors"]["pkgdb"]["fingerprint_prefix"], "0123456789abcdef")
|
||||
self.assertTrue(report["read_only"])
|
||||
self.assertIn("sentinel_footprint", report)
|
||||
|
||||
def test_dashboard_has_persistent_theme_controls(self):
|
||||
html = (web._STATIC / "dashboard.html").read_text()
|
||||
self.assertIn('id="settingsMenu"', html)
|
||||
|
|
@ -234,6 +256,14 @@ class TestDataLayer(unittest.TestCase):
|
|||
f"{theme} {foreground} on {background} contrast is {ratio:.2f}",
|
||||
)
|
||||
|
||||
def test_dashboard_has_integrity_console(self):
|
||||
html = (web._STATIC / "dashboard.html").read_text()
|
||||
self.assertIn('data-tab="integrity"', html)
|
||||
self.assertIn('id="integrityTab"', html)
|
||||
self.assertIn('/api/integrity', html)
|
||||
self.assertIn('function renderIntegrity()', html)
|
||||
self.assertIn('Integrity state', html)
|
||||
|
||||
|
||||
class TestNetworkHelpers(unittest.TestCase):
|
||||
def test_is_loopback(self):
|
||||
|
|
@ -330,6 +360,14 @@ class TestAuth(unittest.TestCase):
|
|||
self.assertIn("rules", data)
|
||||
self.assertGreater(data["count"], 0)
|
||||
|
||||
def test_integrity_endpoint(self):
|
||||
resp = self._get("/api/integrity", token="secret-token")
|
||||
data = json.loads(resp.read())
|
||||
self.assertEqual(data["schema"], "enodia.integrity.v1")
|
||||
self.assertIn("watchdog", data)
|
||||
self.assertIn("anchors", data)
|
||||
self.assertTrue(data["read_only"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue