Full verify pass over all 54 built-in rules (validate_rule sweep, 30d/7d
get_rule_stats, test_analyze_rule spot checks). Fixes:
- ANALYZE-APP-007 v1.1.0: declare enrollment_failed as a non-required
condition — the exists-factor referenced an undeclared signal and could
never apply.
- ANALYZE-APP-014 v1.1.0: add non-required event_data conditions for
espCategory/espErrorCode/topCandidatePackage/candidateCount/confidence
so all explanation {{tokens}} resolve instead of rendering literally.
- ANALYZE-ENRL-002 v1.2.0: capture session_timeout.timeoutHours so
{{timeoutHours}} interpolates (verified via dry-run: renders "5").
- RuleEngine TryFlatLookup/ResolveDotPath: flatten IEnumerable data
values to a comma-joined string — List fields (e.g. app_tracking_summary
.pendingNames) stringified to "System.Collections.Generic.List`1[…]",
breaking contains-matching and {{pendingNames}} interpolation.
- GetRuleStatsAsync: raise the 500-row cap to 10000 and log when hit.
The scan walks dates ascending, so the old cap silently dropped the
NEWEST dates of a 30d window — newly added rules (e.g. ANALYZE-SEC-008)
were entirely missing from global stats.
- MCP validate_rule lint: accept event_data_array itemField as a {{token}}
resolver — {{identity}} in ANALYZE-SEC-005/006 was a false positive.
Backend 4157 tests green, MCP 548 green + tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two related gaps that blocked ANALYZE-ID-003:
(1) RuleEngine.GetDataFieldValue did flat dictionary lookup only — rules
targeting nested payloads (scan_summary.critical_cves) had no way to
descend. Extended with a dot-path resolver that walks both
Dictionary<string,object> (in-process typed sidecar) and JsonElement
(rehydrated-from-storage) trees, case-insensitive at each level, and
bails cleanly when a path lands in a scalar. Flat lookup runs first
so legacy events that wrote literal-dot keys still match.
(2) Vulnerability findings only lived in the VulnerabilityReports table
— never as an event — so analyze rules had no surface to fire on.
EventIngestProcessor now synthesises a vulnerability_report event
after a successful StoreVulnerabilityReportAsync, carrying the
scan_summary slice + reportAvailable + phase. Severity is mapped
from overall_risk (critical/high/medium/-> Critical/Error/Warning).
The full per-CVE list stays in the table.
Rerun-after-emit: the end-of-session rule analysis at line 205 is
triggered by classification.CompletionEvent and may finish before the
async vulnerability task does. ReanalyzeAfterVulnerabilityEmitAsync
runs AnalyzeSessionAsync(reanalyze=false) right after the synth-emit;
existing results are skipped (no duplicates), un-evaluated rules pick up
the new event, and SignalR fires NotifyRuleResultsAvailable so the UI
updates without manual refresh.
Tests: RuleEngineDataPathResolverTests (7 cases — flat/Dict-descent/
JsonElement-descent/case-insensitive/missing-path/scalar-descent) +
existing 752 backend tests still green.
Legacy IngestEventsFunction path intentionally not mirrored — the V1
agent ingest is on its way out, no need to backport the synth-emit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>