test(mesh/audit): cover read_audit_log symlink refusal + rotation-suffix filter#596
Conversation
…fix filter read_audit_log is both the operator forensic reader and the seed source for _load_seq_counters when the seq sidecar is corrupt, so its file handling is security-relevant. Three documented walker defenses had no direct regression coverage: - a rotated log file swapped for a symlink must be refused (O_NOFOLLOW discipline), never followed to attacker-controlled bytes; - only siblings whose suffix after the active-log name is purely numeric count as rotated copies (a mesh_audit.jsonl.bak decoy is ignored); - since= drops records older than the cutoff and records with a missing or non-numeric ts. Pins all three. No source change.
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
Test-only PR (+130/-0, one new file tests/mesh/test_audit_read_walker_defenses.py) that pins three previously-uncovered forensic-walker defenses in strands_robots.mesh.audit: (1) read_audit_log refuses to follow a rotated log file that has been swapped for a symlink, asserting the active record is still read and the symlinked target's bytes never enter the stream while a refusing to read ... SYMLINK warning is emitted; (2) _audit_log_files_in_order only treats <active>.<digits> siblings as rotated copies, excluding an unrelated .bak decoy; (3) the since= cutoff keeps records at/after the timestamp and drops older ones plus records with a missing or non-numeric ts. The assertions match the source behavior: the symlink-refusal branch (audit.py:1253-1259), the numeric-suffix filter (audit.py:1208, suffix.isdigit()), and the since filter (audit.py:1291). No source change.
What's good
- Pure test addition with zero production-code change — no public API, wire format, persisted schema, CLI, or env-var surface touched, so no one-way-door risk.
- Fixture isolates module state correctly and uses
monkeypatch.setenv/delenvper AGENTS.md > Review Learnings (#86) > Testing Patterns, with full setup/teardown of_SEQ_COUNTERSand_AUDIT_STATE. - The symlink test plants attacker bytes outside the audit dir and asserts the active record is still returned — a true round-trip pin, not a schema-only check.
os.symlinktest is guarded bypytest.mark.skipif(not hasattr(os, "symlink")); no host paths, no emojis in any string.
Verification suggestions
MUJOCO_GL=egl python -m pytest tests/mesh/test_audit_read_walker_defenses.py -v
Problem
read_audit_loginstrands_robots.mesh.auditis both the operator-facing forensic reader and the seed source for_load_seq_counterswhen the seq sidecar is corrupt. That dual role makes its file-handling discipline security-relevant, but three documented walker defenses had no direct regression coverage:O_NOFOLLOWdiscipline applied to every other open in the module). Following it would let an attacker redirect the forensic read to/dev/null(fail-open seq reset) or to forged content._audit_log_files_in_ordermust only treat siblings whose suffix after the active-log name is purely numeric as rotated copies; an unrelatedmesh_audit.jsonl.bakdecoy must be excluded so its bytes never enter the forensic stream.since=cutoff must keep records at/after the timestamp and drop older ones, plus records with a missing or non-numericts.Change
Adds
tests/mesh/test_audit_read_walker_defenses.pywith three focused tests pinning each behavior. The symlink test plants attacker bytes outside the audit dir, points a rotated-log path at them via symlink, and asserts the active record is still read while the symlinked target is refused with a warning. No source change.Coverage
strands_robots/mesh/audit.py: the symlink-refusal branch (therefusing to read ... SYMLINKpath) and the non-numeric rotation-suffix skip were previously uncovered. Mesh-suite coverage ofaudit.pyrises from 84% to 87% (60 -> 57 missing lines).Tests
MUJOCO_GL=egl python -m pytest tests/mesh/-> 1690 passed, 2 skipped. ruff check + ruff format + mypy all clean.