Skip to content

No audit.log is ever written — AuditLogger is never wired into execute_cortex.py #40

Description

@jdimmer-arch

The README marks Audit Required for auto and envelope_only approval modes, but the audit subsystem is unreachable on the live code path, independent of approval mode. Distinct from #34 (Codex-misdetection permission-gate bypass, now fixed). One of three independent defects found in the same review of the v3.2.3 router, filed separately per this repo's one-defect-per-issue convention. Companions: #38, #39. Filed publicly since this is a missing feature wiring rather than an exploitable weakness — happy to re-route through security@snowflake.com per SECURITY.md if you'd prefer.

Summary

The plugin ships a complete, genuinely tamper-evident audit logger (scripts/router/security/audit_logger.py — a SHA-256 hash chain where each entry carries the previous entry's hash, so any edit or deletion breaks the chain), and the README advertises "structured audit logging" as a built-in protection (README.md:74), marking Audit Required for the auto and envelope_only approval modes (README.md:62-66). But no user-reachable code path ever writes it: the headless entrypoint both skills actually invoke, scripts/router/execute_cortex.py (584 lines at current main), contains zero references to audit, AuditLogger, or security_wrapper. The audit logger is only ever instantiated inside scripts/router/security_wrapper.py, which nothing in the shipped plugin imports or calls — and whose own execution step is a stub.

Expected behavior

A headless session produces an audit trail at the configured security.audit_log_path (default ~/.claude/skills/cortex-code/audit.log, per scripts/router/security/config_manager.py:34).

Actual behavior

Nothing is written. Empirical confirmation, after two full sessions (one RW executing real DDL, one RO/RESEARCH attempting reads) against a demo account, with no config override of security.audit_log_path present: ls ~/.claude/skills/cortex-code/ fails with "No such file or directory" — the default path's parent directory was never created. Since AuditLogger.__init__ does log_path.parent.mkdir(parents=True, exist_ok=True), the directory's absence proves the logger was never instantiated with the default path. find ~/.claude -iname audit.log returns nothing.

The per-tool-call decisions that should be audited do flow through this exact entrypoint — the --permission-prompt-tool stdio handler emits one allow/deny decision per tool call (e.g. {"tool_name":"Bash","action":"execute_command","resource":"snow sql ... -q \"create ...\"","behavior":"allow","reason":"RW allows Bash"}) — and every one of them is currently unrecorded.

Code

scripts/router/execute_cortex.py:19-21 (line numbers per current main, 8633372) — the entrypoint's only policy wiring is envelope_policy.decide; there is no audit import:

sys.path.insert(0, str(Path(__file__).parent))
from envelope_policy import decide as envelope_decide
from session_state import load_active_session, save_active_session

scripts/router/security_wrapper.py:26,86-90,216 — the logger exists, is instantiated, and is called, but only here:

from security.audit_logger import AuditLogger
# ...
    audit_logger = AuditLogger(
        log_path=audit_log_path,
        rotation_size=audit_log_rotation,
        retention_days=audit_log_retention
    )
# ...
    audit_id = audit_logger.log_execution(...)

And security_wrapper.py's own execution step is a stub (security_wrapper.py:206-212), so even wiring the wrapper in wouldn't run Cortex:

    # Step 11: Execute with Cortex (simplified for now - actual execution via execute_cortex.py would go here)
    # For now, return success with mock execution
    execution_result = {
        "status": "success",
        "message": "Execution simulated (full Cortex integration in next phase)",

Lineage

This looks like wiring lost in a port rather than an intended omission: PR #12 ("Bundle router skill into cortex-code plugin with auto-routing") introduced execute_cortex.py importing only stdlib modules, while shipping audit_logger.py/security_wrapper.py as siblings it never calls, and no later commit added the wiring.

Reproduction (generic)

  1. Install v3.2.3+ with any working Snowflake connection.
  2. Run any prompt through the router's headless path (executes scripts/router/execute_cortex.py), in any envelope.
  3. ls ~/.claude/skills/cortex-code/audit.log — does not exist; the directory was never created.
  4. find ~/.claude -iname audit.log — nothing.
  5. grep -in "audit\|security_wrapper" scripts/router/execute_cortex.py — zero matches.

Suggested fix

Since security_wrapper.py's execution step is still the "simplified for now" stub above, is the intended path to complete that integration, or would you take wiring AuditLogger directly into execute_cortex.py? The direct wiring looks minimal: instantiate it from ConfigManager values (security.audit_log_path / audit_log_rotation / audit_log_retention, as security_wrapper.py:77-90 already does) at the start of execute_cortex_streaming(), then log each control_requestenvelope_policy.decide()_send_control_response() cycle plus a session-level log_execution record on completion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions