Repository navigation
[FIX-0] Verdict cache and suppression suggestions keyed by behaviour; a threat revokes them - #344
Conversation
…time replay Scorecard v2 closed 0 of 312 benign corpus_v1 alerts. RUN.md proposed five changes; this tests each offline (no model call of any kind) and ships the combination that raises benign auto-close and closes no malicious alert on any gated set or family. Shipped: - P1, eval harness: lg-sotf eval correlates on each alert's own time (agents.correlation.clock: event, set by event_time_settings), shifted by one constant so the replay starts at the wall clock. Production keeps the wall clock. Reports record run.correlation_clock and, per item, decision_inputs (what decide read). - P2, corpus: corpus_v1 declares its authorised scanner 10.20.5.10 in _meta.tenant_settings (alerts unchanged; v2's sha is the file without it). - P3, routing.benign_verdict_precedence (on): a benign deep verdict below close_max_threat skips the grey-zone band; every other close test stays. - P4, routing.context_corroboration (on): such a verdict on a low/medium alert may be corroborated by the tenant's known_scanner signal instead of rules' threat < 30; never when triage flagged steering text. - P5, routing.cap_correlated_confidence: measured, left off. lg_sotf.eval.calibration re-decides recorded items with the real decide. On v2's recorded DeepSeek outputs (decision_inputs.json, recovered from the node.completed digests) it reproduces all 520 decisions; no policy closes anything there (no tenant context, rules' score >= 42). Fake model, shipped vs 1.2: corpus_v1 benign auto-close 0/312 -> 8/312 (2.6%, 1.3-5.0), adversarial_v2 2/24 -> 3/24, synthetic and phishing unchanged; malicious auto-close 0 everywhere. CALIBRATION.md has every table. Gates: corpus_v1 gains a benign auto-close floor (0.02) and per-family malicious_auto_close_rate bounds; its wall-clock bounds move to the event-time numbers (response 0.03, recall 0.12, escalation 0.97); adversarial_v2 gains floors of 0.1 overall and for benign-lookalike. corpus_v1 cassettes recorded again (fake model). INV-03: new proofs (tenant context never closes high/critical; nothing but the declared scanner corroborates), routing_policy check probes both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
Correlation keeps both: event-time sightings and window (AI-E1.9) and the blind-audit filter of related alerts (PRD-10.2). corpus_v1 cassettes recorded again on the merged code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
CALIBRATION.md tables come from a fresh fake-model run with AI-E6.4 merged: synthetic_v1 benign auto-close is 5 of 21 under every policy (AI-E6.4), adversarial_v2 has 71 malicious items (activity-camouflage, 0 closed under every policy); corpus_v1 is unchanged (8 of 312 shipped, 0 malicious). The synthetic and phishing cassettes are recorded again (fake model): four phishing replays missed after the merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
Fatigue step 1 builds on AI-E1.9's event-time correlation replay and calibration simulator, which are not on main yet. Conflicts: the four phishing cassettes take AI-E1.9's re-recording (it already includes the #308 + #314 inputs plus its own triage change); the system card keeps main's wording plus AI-E1.9's Art. 15 scanner note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
…headline) Step 1 of the alert-fatigue redesign: make the evaluation measure what matters before the policy changes. - Analyst oracle (`lg-sotf eval --oracle propose|accept`): every escalation is decided at once from the ground truth through ReviewService.decide and WorkflowEngine.resume (the API's path, with the true_positive cache invalidation), approvals are rejected, items run in event-time order. Item status still records what automation did; `oracle_decisions` and `final_status` record the rest. - Closure attribution: each automatic close gets a mechanism (rules, model, history, cache, suppression, response). Every report and scorecard leads with the benign closure rate (n, Wilson CI) and malicious auto-close (n, exact one-sided 95% upper bound), then the per-mechanism table and every malicious item closed. The committed v1 and v2 scorecards are regenerated in the new format. - `_meta.tenant_context`: declared admin, helpdesk and automation accounts, automation and jump hosts, patch windows and scanners, validated (a configured scanner must be declared). corpus_v1 declares its estate; reports show coverage of benign and malicious items. Nothing consumes it except the scanner. - AIT-ADS loader (tests/fixtures/aitads): fetch with md5 check, seeded stratified sample (aitads_v1), labels from attack-phase times (benign / attack-only detection / inconclusive), a 19-alert CC BY fixture. First fake-model measurement on corpus_v1: plain 8/312 benign closed; oracle 86/312 (cache 78) but 4/208 attacks closed by the cache, whose fingerprint ignores what ran (service installs, ps). run_evaluation is split into helpers (budget entry removed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
The verdict cache keyed analyst-benign verdicts by alert fingerprint (rule plus hosts, users, IPs, hashes, domains). An analyst-oracle replay of corpus_v1 showed three malicious alerts closed at the gate: a ransomware PSEXESVC service and a malicious winsvchost service after an Edge update service on the same host, and a web shell's `ps` as uid 33 after monitoring's `ps` as root. - domain/verdict_key.py: verdict key = fingerprint + process image, normalised command line (GUIDs, timestamps, long hex/digit runs and generated temp names masked), service name and image path, user/uid and parent process, read by field name from unmapped and raw. None (never cached) when the event was truncated or exceeds the caps. - core/verdict_cache.py: key version 2 (v1 fingerprint entries are never read). A close needs >= 2 distinct analyst-benign decisions (one per escalation) on the key within 7 days at the alert's severity or above, never critical, and no threat on the key or its fingerprint in the window: invalidate drops decisions and writes a 7-day tombstone on both. - Benign group decisions (own_review_reason) compare verdict keys too, so an attached alert with the same fingerprint but other behaviour detaches. Grouping itself still uses the fingerprint (it only shows alerts to an analyst). - Tests: the three collisions as regressions, a corpus_v1 analyst-oracle replay through the real cache (0 malicious closes at k=2; also 0 at k=1), verdict key and cache unit tests, engine/grouping/e2e updates. - Docs: INV-03 proof, TM-WRK-10, security/architecture/user-guide/reporting, changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
…dict-cache-fingerprint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
…ingerprint Conflicting hunks take main (FIX-1): routing, benign evidence, triage and the correlation agent are main's files; the cassettes are main's re-recordings. AI-E1.9 code superseded by FIX-1 is adjusted: the offline simulator drops the removed correlation cap, its knob tests and the correlation clock test are removed, and the docs say the event-time clock is not active. INV-03 keeps FIX-1's text plus the verdict-key sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
…ttes - tests/integration/eval/test_oracle_corpus_v1.py: the analyst oracle (run_evaluation with oracle, as lg-sotf eval --oracle) in propose and accept modes closes none of the 208 attacks, including the four former collisions cv1-0113, cv1-0309, cv1-0440 and cv1-0494. - docs/evaluation.md: before/after table with Wilson intervals (fake model, policy v2 at tier off). propose: 82 -> 29 of 312 benign, 4 -> 0 of 208 malicious; accept: 85 -> 80 of 312 benign, 4 -> 0 of 208 malicious. - corpus_v1 cassettes recorded again with the fake model: main's re-recordings (FIX-1) did not match the fatigue-1 corpus and prompts. Refs #343 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
Review of #344 (verdict cache and suppressions keyed by behaviour)This is a real fix for a real class of bug, and the design is sound. The verdict key binds behaviour, a close needs k=2 decisions, and a threat leaves tombstones on both the key and the fingerprint. Failure handling is mostly fail-closed (truncated events, unreadable tombstone times, critical alerts). The oracle numbers (4 malicious closes → 0) and the replay regression tests are the right evidence for a T3 change. I read Security and correctness
Scope and process
TestsCoverage is strong: the corpus replay, the fingerprint-collision guard and the oracle acceptance tests. I would add boundary tests for the 512-char cap and each mask (items 1 and 2), a test that an alert with no behaviour fields gets no cache close (item 3), and a concurrency test for StyleThe code is typed, short and well documented. The |
…revocation tested
H1 domain/verdict_key.py: no key (no cache close, no suggestion, no benign
group inheritance) when the event names no process, command, service or
attacker-named object, when closure.never_close_class() puts the alert in
a never-close class (unmapped excepted: analysts decided this exact
behaviour), or when it was normalised by a model. Fields keep their own
names (subject vs target user) and nested roles (ECS process.parent.*,
user.name, process.args; GuardDuty ExecutablePath; Defender
InitiatingProcess*); task name/content, registry targetObject/details,
WMI name/query/destination/consumer/filter are part of the key.
H2 values are compared in full; over 16 KiB the alert gets no key.
M2 no masking: only whitespace, doubled backslashes and Windows-path case.
H3 migration 0016 disables enabled fingerprint-only suggestions, each
audited suppression.revoked in its tenant's chain; the gate ignores them.
M1 the cache counts distinct analysts (decided_by): one analyst's decisions,
a bulk close included, count once. The oracle is a two-analyst team.
M3 revocation tested through the API true positive, the blind-audit label,
plan_response and the malicious close; invalidate_verdicts logs at error
and counts lgsotf_revocation_failures_total instead of swallowing.
M4 AI-E1.9 leftovers removed from its changelog fragment and the system card.
L1 model-normalised alerts skip suppression rules at the gate too.
L3 the inert policy simulator and the event-time clock setting are removed.
L4 ai_closed:audit_required closes are attributed to the model.
L5 test docstrings name the four collisions.
The reviewer's two failing tests are adopted in
tests/unit/domain/test_verdict_key_review.py and pass; bulk proposals for
alerts without a verdict key answer 422 no_verdict_key.
Refs #343
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
Review of #344: verdict key and verdict cache v3Scope. The diff is too large for the GitHub API (353 files, about 5.6k lines added). I read The design is sound: it fails closed in the right places, and the description and threat note are clear. Comments below, in rough order of importance. Issues
Positives
Test coverageCoverage described in the PR is good. Gaps I would add:
ProcessThe PR mixes a safety fix with merged AI-E1.9 and fatigue-1 work and removals (policy simulator, correlation clock setting). That is hard to review and revert. Consider splitting it, or at least listing the removed surfaces in the changelog. |
…chors it
N1 domain/verdict_key.py (recipe v4): anchors are only process image,
command line, service, scheduled task name/content, registry key, target
file and, in Sysmon WMI events (19-21) only, the WMI name, filter and
consumer. User agents, URLs and request paths/queries, O365 operations
and their Parameters, DNS/WMI queries, destinations, registry value
details and file paths stay in the key as context but never make one:
a SQL injection behind a proxy no longer shares a benign search's key,
nor an inbox forward rule a sorting rule's. Sysmon 10 GrantedAccess and
CallTrace are in the key (0x1000 vs 0x1FFFFF on lsass differ).
N2 tests/unit/core/test_gate.py: fingerprint_suggestion() and the gate's
skip, including a fingerprint-only rule an admin enabled again.
N4 migration 0016 restores app.bypass_rls as 0004 does.
N5 the bulk proposal dialog explains 422 no_verdict_key and lists the
escalations no rule can match (vitest, tsc).
M4 ai-system-card's routing row follows FIX-1 (a declared scanner counts
only as the source of the activity; a model-led close needs the tier);
CALIBRATION.md is marked superseded by FIX-1.
The verdict cache docstring no longer says MSSP decisions lack an
analyst: decided_by is always the deciding user.
INV-03, security, threat model and the changelog fragment match.
Refs #343
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
|
Review of PR 344: verdict key and verdict cache v3 The diff is over 300 files, so GitHub wouldn't serve it whole. I read the core of the change in the merge checkout: Overall: the design is sound. Every failure mode I looked at fails closed: unreadable threat time, truncated document, oversized value, LLM-normalised alert. The docstrings are clear, and the before/after numbers in the description are helpful. Things worth a look
Tests
Security I found no injection or privilege problems in what I read. Redis keys are built from a hex digest and tenant id, and Housekeeping The PR bundles the AITADS fixtures, scorecards, eval harness and closure measurement with the safety fix. Splitting them would make the fix easier to review and revert. |
…d the script-content residual A Parameters list longer than the cap was read only up to it, so repeated benign parameters could hide one appended after them. It now gives no key, like any other value past a cap. TM-WRK-10 records that the key describes what ran, not the content of the files it ran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
|
Review (design-level) Scope note: GitHub can't return this PR's diff (over 300 files) and the checkout is shallow. I read What looks good
Concerns and suggestions
Security Overall this is a well-reasoned fix. My main requests are to split the PR if possible and to close or fail-safe the L2 revocation gap. |
INV-03 keeps both changes. The verdict-key replay's benign floor moves to 14: Wazuh authentication-failure rules are now a never-close class, so they get no verdict key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc
Closes #343
Closure cost, plainly
With analysts deciding every escalation correctly (oracle, corpus_v1):
proposeacceptproposemode, verdict-cache closes fall from 80 to 7.The safety costs most of what the cache and suggestions closed.
Summary
Safety fix, tier T3. The analyst verdict cache and the suppression rules suggested from false positives matched alerts by fingerprint only: the rule plus hosts, users, IPs, hashes and domains. With analysts deciding every escalation correctly, corpus_v1 still closed 4 attacks next to benign activity on the same host:
cv1-0113: a ransomwarePSEXESVCservice, after an Edge update service.cv1-0309andcv1-0440: dropped services.cv1-0494: a web shell'sps auxrunning as uid 33, after monitoring'spsrunning as root.Verdict key (
domain/verdict_key.py, recipe v4). The key is the fingerprint plus what ran:process.parent.*and DefenderInitiatingProcess*;Request context is in the key but never anchors one. These fields alone never produce a key, because whoever sends the request sets them:
GrantedAccessandCallTrace.Each field keeps its own name, so a subject and a target user cannot swap unnoticed.
Verdict cache v3 (
core/verdict_cache.py)decided_by) within 7 days, at the alert's severity or higher. Critical alerts never close from the cache.Suppressions
verdict_key, for both single false positives and bulk proposals. A bulk proposal for an alert without a key returns 422no_verdict_key.suppression.revoked. A confirmed threat is any of:true_positive;true_positive.lgsotf_revocation_failures_total.migration-0016with reasonfingerprint_suggestion. The gate also ignores such rules.Grouping: an attached alert inherits a benign group decision only when it has the same verdict key. Ingestion dedup never used the fingerprint.
Oracle results (corpus_v1, fake model, decision policy v2 at tier
off, two oracle analysts)"Before" is today's code with the cache and suggestions keyed by fingerprint, and one decision enough.
What it costs. Most of the lost closes come from alerts that no longer get a key:
Requiring two analysts and comparing command lines unmasked cost the rest. With one analyst enough, the unit replay on the new key closes about 20 benign alerts and still 0 attacks.
Tests
tests/integration/eval/test_oracle_corpus_v1.py::test_the_oracle_closes_no_attack_on_corpus_v1[propose|accept]passes. It callsrun_evaluationwith the oracle on, aslg-sotf eval --oracledoes.test_verdict_key_review.py,test_verdict_key.py::test_context_alone_never_makes_a_key).tests/unit/core/test_gate.pycoversfingerprint_suggestion()and the gate skip, including a fingerprint-only rule an admin enabled again.no_verdict_key(vitest andtsc -bpass).tests/unit/domain/test_verdict_key_review.pyand passing.tests/unit/domain/test_verdict_key.py::test_attacker_writable_differences_never_share_a_key.tests/integration/api/test_revocation.pycovers the API true positive, the blind-audit label, and the failure counter.tests/integration/engine/test_engine.py::test_plan_response_and_the_malicious_close_revoke_the_benign_learningcovers plan_response and the malicious close.tests/integration/db/test_migrations.py::test_0016_...checks the disabled rules and that the audit chain still verifies.ruff, global strictmypyandlint-imports(14 kept) are green; unit, deploy and invariants suites pass.Threat note (T3)
Merge notes
origin/mainwith FIX-1 (FIX-1: decision policy v2 (evidence gate replaces the rules-score gate) + per-tenant autonomy tier #341) is merged. Conflicting hunks keep FIX-1's evidence gate, tier, never-close classes and correlation-as-evidence. The corpus_v1 cassettes were re-recorded with the fake model.decision_inputsstays);correlation_clock;🤖 Generated with Claude Code
https://claude.ai/code/session_01JmdYVXkgpNHoygVJCrUeZc