Add real detector health tooling: learning state and historical precision
Replaces the old Prometheus-only "detector precision" metric, which recomputed from whatever field_deviations happened to be active in the current poll cycle cross-referenced against live FeedbackStore rules — a revoked or expired suppression rule made the signal vanish, and nothing was tracked between polls. Adds DetectorPrecisionStore, a permanent append-only log of review outcomes (kept deliberately separate from FeedbackStore's mutable suppression rules) recorded the moment a human reviews a finding, plus BaselineStore.detector_readiness() mirroring the existing per-field profile_readiness() for per-detector learning state, and a plain-language detector reference. Surfaced in a new Detector Health dashboard panel. Observability only — nothing here changes scoring or suppression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from fgai.feedback import FeedbackStore, feedback_matches, select_match, suggest_generalizations
|
||||
from fgai.feedback import DetectorPrecisionStore, FeedbackStore, feedback_matches, select_match, suggest_generalizations
|
||||
|
||||
|
||||
class FeedbackTests(unittest.TestCase):
|
||||
@@ -157,6 +157,55 @@ class FeedbackTests(unittest.TestCase):
|
||||
{"stream_id": "s", "entity": "alice", "field": "f", "status": "false_positive", "value_count": 2, "sample_values": ["1", "2"], "entry_ids": ["1", "2"]}
|
||||
])
|
||||
|
||||
def test_detector_precision_store_computes_ratio_from_recorded_outcomes(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
store = DetectorPrecisionStore(str(Path(directory) / "precision.sqlite3"))
|
||||
store.record("auth_failure_burst", "confirmed", stream_id="windows", field="auth_failure")
|
||||
store.record("auth_failure_burst", "confirmed", stream_id="windows", field="auth_failure")
|
||||
store.record("auth_failure_burst", "false_positive", stream_id="windows", field="auth_failure")
|
||||
store.record("new_relationship", "expected", stream_id="firewall", field="relationship:srcip->dstip")
|
||||
|
||||
precision = store.precision(days=30)
|
||||
|
||||
auth = precision["auth_failure_burst"]
|
||||
self.assertEqual(auth["confirmed"], 2)
|
||||
self.assertEqual(auth["false_positive"], 1)
|
||||
self.assertAlmostEqual(auth["precision"], 2 / 3, places=3)
|
||||
self.assertEqual(auth["reviewed_total"], 3)
|
||||
self.assertIsNone(precision["new_relationship"]["precision"])
|
||||
self.assertEqual(precision["new_relationship"]["reviewed_total"], 1)
|
||||
|
||||
def test_detector_precision_store_excludes_outcomes_outside_the_window(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
store = DetectorPrecisionStore(str(Path(directory) / "precision.sqlite3"))
|
||||
old_timestamp = int(time.time()) - 40 * 86400
|
||||
store.record("auth_failure_burst", "confirmed", recorded_at=old_timestamp)
|
||||
store.record("auth_failure_burst", "false_positive")
|
||||
|
||||
precision = store.precision(days=30)
|
||||
|
||||
self.assertEqual(precision["auth_failure_burst"]["confirmed"], 0)
|
||||
self.assertEqual(precision["auth_failure_burst"]["false_positive"], 1)
|
||||
|
||||
def test_detector_precision_store_ignores_invalid_or_empty_input(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
store = DetectorPrecisionStore(str(Path(directory) / "precision.sqlite3"))
|
||||
store.record("", "confirmed")
|
||||
store.record("auth_failure_burst", "not_a_real_status")
|
||||
|
||||
self.assertEqual(store.precision(days=30), {})
|
||||
|
||||
def test_detector_precision_store_maintenance_prunes_old_rows(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
store = DetectorPrecisionStore(str(Path(directory) / "precision.sqlite3"))
|
||||
store.record("auth_failure_burst", "confirmed", recorded_at=int(time.time()) - 200 * 86400)
|
||||
store.record("auth_failure_burst", "confirmed")
|
||||
|
||||
removed = store.maintenance(retention_days=180)
|
||||
|
||||
self.assertEqual(removed, 1)
|
||||
self.assertEqual(store.precision(days=3650)["auth_failure_burst"]["confirmed"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user