Skip to content

ADR-0040: Deep audit, validation, and adversarial-testing pass (2026-06-14)

Status

Accepted

Date

2026-06-14

Authors

praxis maintainers

Context

After the feature waves through ADR-0038 the backlog reached fully resolved (103 items). This pass re-validates the security posture against the nine invariants and the STPA constraints, with adversarial runtime testing, and brings the prose documentation back in line with the code. It follows the 2026-06-12 pass (ADR-0017, audit/00-inventory.md..03-final-report.md) and ran in parallel with the same-day read-only refresh in ADR-0039 (#70, which refreshed audit/00..03 in place and filed BL-104); this pass is the deeper one that additionally remediated code findings, and is recorded separately under audit/2026-06-14/.

Method: five parallel read-only domain audits (execution core and invariants; transport/SSRF/redaction/audit-integrity; store/actuation/collectors/drift; documentation drift; STPA and governance traceability), plus hands-on adversarial probing of the security-critical functions, plus the existing gates (make ci-success green at 92% coverage; scripts/fuzz.py 20000 iterations, no violations).

Headline result: no critical or high findings. The nine invariants are each mechanically enforced with a passing test (re-confirmed); STPA traceability is complete (28/28 UCAs covered, 10/10 SEC constraints enforced in code, the compliance validator reports 0 violations across its 11 rules). The pass produced six code findings (all remediated in-pass with regression tests), three documented dispositions, and a set of documentation-drift corrections.

Decision

Record the findings and their dispositions. The detail is in audit/2026-06-14/01-findings.md; the disposition summary is in 02-report.md.

Remediated in this pass (each with a regression test):

ID Sev Area Fix
F-001 Medium execution/audit.py _canonical used default=str, uncontained on a hostile __str__ and on a circular reference, breaking the invariant-3 "logger never raises" claim. Added _safe_str plus an acyclic fallback so record never raises on any input.
F-006 Medium execution/redaction.py Anthropic (sk-ant-), HuggingFace (hf_), and DigitalOcean (do[opr]_v1_) tokens leaked (the generic sk-/alnum patterns miss them). Added explicit value patterns.
F-007 Medium store/sqlite.py, store/postgres.py supersede did not check the UPDATE rowcount, so the loser of a concurrent supersede received a false success carrying the winner's actor/reason. Both backends now report a lost race as None (provenance fidelity, SEC-10).
F-004 Low execution/patterns.py The rm -rf / deny missed //, /*, and /. (caught at T3 but not by the global deny wall). Extended the pattern; bumped PATTERNS_VERSION 3 to 4.
F-008 Low collectors/talos.py The non-JSON status fallback stored unbounded attacker text. Capped at 4096 chars with a truncation marker (invariant 8).
F-003 Low actuation/opentofu.py OpenTofu passed an unconfined chdir as -chdir (dead code: chdir is not a RunActionArgs field). Removed the unconfined passthrough; safe re-add tracked as BL-105.

Dispositioned as documented (no code behaviour change):

  • F-002 (redaction is pattern-based, so an unkeyed high-entropy secret in no recognised format is not detected): documented in SECURITY.md. The load-bearing controls remain that output bodies are never logged and secret-named keys are always redacted.
  • F-005 (SyslogAuditSink does not SSRF-filter PRAXIS_AUDIT_SYSLOG_ADDRESS): intended. The syslog address is operator-trusted deploy configuration, not a model-influenced destination, and a local SIEM on a private address is the normal case. Documented in the SyslogAuditSink docstring, operate.md, and the ADR-0037 audit note.
  • F-009 (ADR-0015 lacked a ratification note): added, matching ADR-0024/0029.

Deferred hardening filed as backlog items: BL-105 (OpenTofu workspace selection via a PRAXIS_TOFU_ROOT-confined chdir), BL-106 (timing-safe approval-token comparison before any network-accessible submission path), BL-107 (a total-message-byte cap for a multi-client transport), BL-108 (per-pair/per-value caps in CommandProbeCollector), BL-109 (make compliance-controls.json proving-test lists exhaustive rather than representative). BL-106 and BL-107 are prerequisites of the HTTP transport (BL-012).

Consequences

Positive: an invariant-3 robustness gap, a redaction-coverage gap (including Anthropic API keys, relevant for an MCP server), a supersede provenance race, a deny-wall miss, an untrusted-data storage gap, and a latent unconfined path are closed, each with a test. The documentation is current. The validated-solid posture is recorded for the next pass.

Negative: F-002 and F-005 are accepted, documented limitations rather than code changes. The deferred BL items remain open until the HTTP transport work schedules them.

Neutral: PATTERNS_VERSION is now 4, so audit records and approval bindings minted after this change cite ruleset 4; the bump is the documented signal that the deny set changed.

Alternatives considered and rejected

  • Apply the SSRF egress filter to the syslog sink (F-005). Rejected: it would block the normal LAN-SIEM case, and the destination is operator-trusted, not model-influenced.
  • Entropy-based redaction (F-002). Rejected: high false-positive rate on legitimate high-entropy values; the no-bodies rule plus secret-key plus curated-shape redaction is the chosen balance.
  • Narrow the invariant-3 docstring to "never raises on JSON-native args" instead of fixing _canonical (F-001). Rejected: invariant 3 is load-bearing; make it true by construction for any input.
  • Defer F-006 to a backlog item rather than fixing in-pass. Rejected: a leaking secret pattern (Anthropic keys) is cheap to close now.

Revisit triggers

  • The HTTP transport (BL-012) is scheduled: BL-106 and BL-107 become prerequisites, and the approval-token comparison and message-byte cap must land first.
  • A new provider token format appears in the field: extend the redaction value patterns (the BL-097 / F-006 set).
  • The next periodic deep-audit pass.