* fix(audit-log): neutralize CSV export injection, clamp pagination, bound anomaly history
Harden the Admiral audit log without changing its tier or hub-only gating.
- CSV export now defuses formula injection: any field that a spreadsheet
would evaluate as a formula (leading = + - @, or a trigger behind leading
whitespace, or a leading tab/CR) is prefixed with a single quote before
RFC 4180 quoting. Audit summaries embed user-controlled resource names, so
this closes a path where a crafted name could execute on export open.
- Clamp page and limit to positive bounds on the list endpoint so a negative
limit can no longer reach SQLite as "unlimited" and dump the whole table.
- Bound the anomaly and stats history reads to a capped slice of recent rows
so the analysis paths stay within fixed memory and latency on large
histories instead of scanning the full retention window per request.
- Surface failed audit list and stats fetches through the standard error
toast instead of leaving the view silently stale.
- Add a developer-mode-gated diagnostic log to the stats endpoint for parity
with the list and export handlers.
Covered by new unit and HTTP-integration tests (CSV neutralization through
the real export route, pagination clamps, bounded history, stats endpoint,
anomaly annotation) and verified end to end in the browser.
* fix(audit-log): compute signal-rail stats with exact SQL aggregates
Address review feedback on the earlier history-cap change. The cap was
correct for the anomaly baseline but made the stats tiles (events, actors,
failure rate, hourly series) silently undercount on a hub with more than the
cap's worth of rows in the window, since they were derived from the capped
row slice.
- Add DatabaseService.getAuditStatsInputs: exact counts via SQL COUNT /
COUNT(DISTINCT) / GROUP BY hour, and new-ip detection over the small
DISTINCT (user, ip) pair sets. No row cap, so the tiles stay exact at any
window size while memory stays bounded.
- Reduce computeAuditStats to a pure formatter over those aggregates.
- Keep the bounded history read only for the list endpoint's anomaly
annotation, where a recent-activity baseline is an acceptable heuristic.
- Skip the redundant load-failure toast when a fetch fails with a handled
401, so an expired session does not stack toasts on top of logout.
Adds exactness tests for the aggregate counts, distinct-actor handling, and
new-ip detection, and strengthens the pagination-clamp tests.
* fix(audit-log): exclude future-dated rows and make the new-ip sample deterministic
Two small parity fixes on the stats aggregates: upper-bound every current
window by `now` so a future-dated row (clock skew or a fixture) cannot inflate
the live counts, and order the new-ip pair scan so the sample actor shown in
the tile detail is stable. Adds a test asserting a future row is excluded.
Add a Stream view to the Audit Log that leads with a four-tile signal
rail (events, actors, failure rate with inline sparkline, peak hour)
and presents the feed grouped by day with severity dots, relative
times, and inline anomaly callouts. The existing Table view is
preserved behind a toggle for power users.
Anomaly flags are computed at read time against strictly prior history
and returned on demand via ?with_anomalies=1:
- unusual_hour: hour outside the actor's central 7-day window
- new_ip: IP unseen for this actor in the last 30 days
- first_seen_actor: no prior history in the 30-day window
New /audit-log/stats endpoint returns the signal-rail aggregates over
24h/7d/30d windows; stats are derived from a single 30-day scan.