add false-positive filter and bulk-clear for findings
Active Feedback Rules gets a status filter, and Field Baseline Deviations gets multi-select with a bulk apply bar. Bulk scope is restricted to exact-value/field only, so a batch action can never create a broad rule that would suppress a genuinely new deviation.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -105,6 +105,28 @@ class FeedbackStore:
|
|||||||
self.path.write_text(json.dumps(entries, indent=2), encoding="utf-8")
|
self.path.write_text(json.dumps(entries, indent=2), encoding="utf-8")
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
def add_many(self, items: list[dict[str, object]]) -> dict[str, object]:
|
||||||
|
"""Normalize and save a batch of entries with a single read-modify-write.
|
||||||
|
|
||||||
|
One bad item never blocks the rest — each is validated independently and
|
||||||
|
failures are reported by index instead of aborting the whole batch.
|
||||||
|
"""
|
||||||
|
added_by_id: dict[str, dict[str, object]] = {}
|
||||||
|
errors: list[dict[str, object]] = []
|
||||||
|
for index, item in enumerate(items):
|
||||||
|
try:
|
||||||
|
entry = _normalize(item)
|
||||||
|
added_by_id[entry["id"]] = entry # later duplicates in the same batch win, like sequential add() calls would
|
||||||
|
except ValueError as exc:
|
||||||
|
errors.append({"index": index, "error": str(exc)})
|
||||||
|
added = list(added_by_id.values())
|
||||||
|
entries = self.entries()
|
||||||
|
entries = [value for value in entries if value.get("id") not in added_by_id]
|
||||||
|
entries.extend(added)
|
||||||
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.path.write_text(json.dumps(entries, indent=2), encoding="utf-8")
|
||||||
|
return {"added": added, "errors": errors, "remaining": len(entries)}
|
||||||
|
|
||||||
def revoke(self, entry_id: str) -> dict[str, object]:
|
def revoke(self, entry_id: str) -> dict[str, object]:
|
||||||
entries = self.entries()
|
entries = self.entries()
|
||||||
before = len(entries)
|
before = len(entries)
|
||||||
|
|||||||
@@ -81,11 +81,35 @@ class DashboardTests(unittest.TestCase):
|
|||||||
self.assertIn("/api/feedback/revoke", source)
|
self.assertIn("/api/feedback/revoke", source)
|
||||||
self.assertIn(".revoke(", source)
|
self.assertIn(".revoke(", source)
|
||||||
|
|
||||||
|
def test_dashboard_exposes_feedback_bulk_endpoint(self):
|
||||||
|
source = inspect.getsource(dashboard)
|
||||||
|
self.assertIn("/api/feedback/bulk", source)
|
||||||
|
self.assertIn(".add_many(", source)
|
||||||
|
|
||||||
|
def test_dashboard_has_bulk_review_bar(self):
|
||||||
|
self.assertIn('id="bulkReviewBar"', HTML)
|
||||||
|
self.assertIn('id="selectAllDeviations"', HTML)
|
||||||
|
self.assertIn("deviation-select", HTML)
|
||||||
|
self.assertIn('id="bulkReviewSubmit"', HTML)
|
||||||
|
|
||||||
|
def test_bulk_review_scope_never_offers_broad_scopes(self):
|
||||||
|
start = HTML.index('id="bulkReviewScope"')
|
||||||
|
end = HTML.index('</select>', start)
|
||||||
|
bulk_scope_html = HTML[start:end]
|
||||||
|
self.assertIn('value="value"', bulk_scope_html)
|
||||||
|
self.assertIn('value="field"', bulk_scope_html)
|
||||||
|
self.assertNotIn('value="entity"', bulk_scope_html)
|
||||||
|
self.assertNotIn('value="stream"', bulk_scope_html)
|
||||||
|
|
||||||
def test_dashboard_has_active_feedback_rules_panel(self):
|
def test_dashboard_has_active_feedback_rules_panel(self):
|
||||||
self.assertIn('id="feedbackRules"', HTML)
|
self.assertIn('id="feedbackRules"', HTML)
|
||||||
self.assertIn("revoke-feedback-rule", HTML)
|
self.assertIn("revoke-feedback-rule", HTML)
|
||||||
self.assertIn('id="feedbackRuleSummary"', HTML)
|
self.assertIn('id="feedbackRuleSummary"', HTML)
|
||||||
|
|
||||||
|
def test_dashboard_has_feedback_rule_status_filter(self):
|
||||||
|
self.assertIn('id="feedbackRuleStatusFilter"', HTML)
|
||||||
|
self.assertIn('value="false_positive">False positive', HTML)
|
||||||
|
|
||||||
def test_dashboard_has_scoped_review_form(self):
|
def test_dashboard_has_scoped_review_form(self):
|
||||||
self.assertIn('id="reviewScope"', HTML)
|
self.assertIn('id="reviewScope"', HTML)
|
||||||
self.assertIn('value="entity"', HTML)
|
self.assertIn('value="entity"', HTML)
|
||||||
|
|||||||
@@ -72,6 +72,37 @@ class FeedbackTests(unittest.TestCase):
|
|||||||
missing = store.revoke("does-not-exist")
|
missing = store.revoke("does-not-exist")
|
||||||
self.assertEqual(missing, {"removed": 0, "remaining": 1, "id": "does-not-exist"})
|
self.assertEqual(missing, {"removed": 0, "remaining": 1, "id": "does-not-exist"})
|
||||||
|
|
||||||
|
def test_add_many_writes_once_and_dedupes_by_id(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
store = FeedbackStore(str(Path(directory) / "feedback.json"))
|
||||||
|
result = store.add_many([
|
||||||
|
{"status": "false_positive", "entity": "alice", "stream_id": "windows", "field": "username", "value": "bob"},
|
||||||
|
{"status": "confirmed", "entity": "carol", "stream_id": "vpn", "field": "username", "value": "dave"},
|
||||||
|
# Same (stream_id, entity, field, value) as the first item -> same id -> collapses to one.
|
||||||
|
{"status": "expected", "entity": "alice", "stream_id": "windows", "field": "username", "value": "bob"},
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertEqual(len(result["added"]), 2)
|
||||||
|
self.assertEqual(result["errors"], [])
|
||||||
|
self.assertEqual(result["remaining"], 2)
|
||||||
|
entries = store.entries()
|
||||||
|
self.assertEqual(len(entries), 2)
|
||||||
|
collapsed = next(item for item in entries if item["entity"] == "alice")
|
||||||
|
self.assertEqual(collapsed["status"], "expected")
|
||||||
|
|
||||||
|
def test_add_many_reports_errors_without_aborting_the_batch(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
store = FeedbackStore(str(Path(directory) / "feedback.json"))
|
||||||
|
result = store.add_many([
|
||||||
|
{"status": "false_positive", "entity": "alice", "stream_id": "windows", "field": "username", "value": "bob"},
|
||||||
|
{"status": "expected", "scope": "entity", "entity": "carol"}, # broad scope, no expiry -> invalid
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertEqual(len(result["added"]), 1)
|
||||||
|
self.assertEqual(len(result["errors"]), 1)
|
||||||
|
self.assertEqual(result["errors"][0]["index"], 1)
|
||||||
|
self.assertEqual(len(store.entries()), 1)
|
||||||
|
|
||||||
def test_feedback_matches_respects_scope_and_precedence(self):
|
def test_feedback_matches_respects_scope_and_precedence(self):
|
||||||
value_rule = {"scope": "value", "entity": "alice", "stream_id": "windows", "field": "username", "value": "10.0.0.1", "status": "confirmed"}
|
value_rule = {"scope": "value", "entity": "alice", "stream_id": "windows", "field": "username", "value": "10.0.0.1", "status": "confirmed"}
|
||||||
field_rule = {"scope": "field", "entity": "alice", "stream_id": "windows", "field": "username", "status": "expected"}
|
field_rule = {"scope": "field", "entity": "alice", "stream_id": "windows", "field": "username", "status": "expected"}
|
||||||
|
|||||||
Reference in New Issue
Block a user