security: redact audit logs with env policy
Jaime Fournier <jaimef@linbsd.org>
520417d9556c19e62d40ea350d3403a35ded38a1
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 2d32502..585b2bc 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -772,6 +772,12 @@ they are build-time. Close the runtime loop.
shipped artifact for `$HOME` path leakage.
- **Accept:** tests: crafted failing request yields opaque ref + logged
detail; scanner catches a synthetic leak; artifact grep clean.
+- **Status:** partially complete. `(std security audit-log)` now exposes
+ `audit-log-use-env-policy!`, workers bind their env policy to per-run audit
+ logs before recording events, and tests assert registered secret values are
+ redacted from both JSONL and human summaries. Remaining work: protocol
+ surfaces need opaque client error refs, the scanner needs a condition-message
+ HTTP-response rule, and release artifact path-leak checks still need a gate.
### K3-P1-11 — Parameterized-only SQL in the safe surface
**Serves:** G1. **Effort:** 2–3 days.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index 91a85fb..e206983 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -815,6 +815,7 @@ These modules are implemented but not covered in depth above.
| `(std security sanitize)` | Context-aware sanitization: `sanitize-html`, `sanitize-html-attribute`, `sanitize-url-attribute`, `sql-escape`, `sanitize-path`, `safe-path-join`, `sanitize-header-value`, `sanitize-url`. Raises `&path-traversal`, `&header-injection`, `&url-scheme-violation`. |
| `(std security errors)` | Error classification (internal vs client-safe). Generates opaque error references for correlation. Prevents leaking internal details in error responses. |
| `(std security audit)` | Append-only JSONL audit log with SHA-256 hash chain. `audit-log!`, `verify-audit-chain`, `check-capability!/audit`. |
+| `(std security audit-log)` | Per-run structured audit records with JSONL/summary rendering, per-log redactors, and `audit-log-use-env-policy!` to scrub `(std security env)` registered secret values before emission. |
| `(std security auth)` | API key stores, session tokens with expiry, auth middleware pattern, rate limiting for auth attempts. |
| `(std security flow)` | Information flow control. Security levels form a lattice: public < internal < secret < top-secret. Data flows up freely; downward flow requires explicit `declassify` which is logged. |
| `(std security metrics)` | Security counters, gauges, histograms with alerting thresholds. |
diff --git a/lib/std/security/audit-log.ss b/lib/std/security/audit-log.ss
index 3e790b3..08b2cfd 100644
--- a/lib/std/security/audit-log.ss
+++ b/lib/std/security/audit-log.ss
@@ -36,6 +36,7 @@
audit-log-summary
audit-log-redactor-set!
audit-log-redactor
+ audit-log-use-env-policy!
audit-log-now
audit-event
@@ -51,7 +52,8 @@
audit-encode-jsonl-line)
(import (chezscheme)
- (only (jerboa core) def defstruct))
+ (only (jerboa core) def defstruct)
+ (only (std security env) env-policy? env-policy-redact))
(defstruct audit-log
(records-tail records-rev redactor))
@@ -62,6 +64,15 @@
(def (audit-log)
(make-audit-log #f '() #f))
+ (def (audit-log-use-env-policy! log pol)
+ (unless (audit-log? log)
+ (error 'audit-log-use-env-policy! "expected audit-log" log))
+ (unless (env-policy? pol)
+ (error 'audit-log-use-env-policy! "expected env-policy" pol))
+ (audit-log-redactor-set! log
+ (lambda (s) (env-policy-redact pol s)))
+ log)
+
;; defstruct already provides `audit-log-redactor` (reader) and
;; `audit-log-redactor-set!` (writer); both are re-exported above.
@@ -273,8 +284,8 @@
[(symbol? v) (symbol->string v)]
[(boolean? v) (if v "true" "false")]
[(number? v) (number->string v)]
- [(pair? v) (format-as-string v)]
- [else (format-as-string v)]))
+ [(pair? v) (maybe-redact (format-as-string v) redactor)]
+ [else (maybe-redact (format-as-string v) redactor)]))
;; ---------- Event constructors ----------
;; Lightweight (type . fields) cons; not a defstruct so we keep audit
diff --git a/lib/std/security/worker.ss b/lib/std/security/worker.ss
index 71e5bdc..e9a019f 100644
--- a/lib/std/security/worker.ss
+++ b/lib/std/security/worker.ss
@@ -48,6 +48,7 @@
audit-log-now
audit-log-record!
audit-log-records
+ audit-log-use-env-policy!
audit-process-start
audit-process-exit)
(only (std security env)
@@ -281,6 +282,7 @@
[else
(let ([log (audit-log)]
[start-ms (audit-log-now)])
+ (audit-log-use-env-policy! log (worker-policy-env-policy pol))
(audit-log-record!
log
(audit-process-start
diff --git a/tests/test-limits-primitives.ss b/tests/test-limits-primitives.ss
index ebad6bf..e8e5e4e 100644
--- a/tests/test-limits-primitives.ss
+++ b/tests/test-limits-primitives.ss
@@ -446,6 +446,29 @@
jsonl
(lambda (s) (string-contains? s "process_start")))))
+(let ([log (audit-log)]
+ [pol (env-policy-default)])
+ (env-policy-secret! pol "DEMO_SECRET" "hunter2")
+ (audit-log-use-env-policy! log pol)
+ (audit-log-record! log
+ (audit-process-start
+ 'command: '("/bin/echo" "hunter2")
+ 'tag: "token=hunter2"))
+ (let ([jsonl (audit-log-jsonl-string log)]
+ [summary (audit-log-summary log)])
+ (test-pred "audit log env-policy redactor scrubs JSONL"
+ jsonl
+ (lambda (s)
+ (and (string? s)
+ (not (string-contains? s "hunter2"))
+ (string-contains? s "<redacted>"))))
+ (test-pred "audit log env-policy redactor scrubs summary"
+ summary
+ (lambda (s)
+ (and (string? s)
+ (not (string-contains? s "hunter2"))
+ (string-contains? s "<redacted>"))))))
+
(let ([log (audit-log)])
(audit-log-record! log
(audit-net-event 'host: "1.2.3.4" 'port: 443 'verdict: 'denied