feat(audit): defensive redaction pass on log + manifest writes#148
Conversation
Closes hardening item #5 from "Secret Handling & Credential Surface Hardening" in docs/developer/SDK-ROADMAP.md. Adds a recursive walker that masks every string in the audit trail at serialization time, plus fixes a pre-existing bug where masking record.msg corrupted %s format strings whose placeholders matched the token: pattern. argus/audit/secrets.py: - New mask_secrets_in_obj(obj) walker. Recurses through dicts, lists, tuples; applies mask_secrets to every string value; leaves keys and non-string scalars untouched; returns a new structure (does NOT mutate the input). argus/audit/logger.py: - JsonLogFormatter.format: stop mutating record.msg. Mask the rendered record.getMessage() instead — catches secrets passed as printf-style args (logger.info("token: %s", real_token)) which the prior approach missed because record.msg held the format string, not the rendered output. Then walk the assembled JSON entry through mask_secrets_in_obj so extra fields a contributor might add to the formatter also get masked. - ColoredConsoleFormatter.format: same fix — mask the rendered message, not record.msg. Without this fix, "token: %s" matched the token=/token: pattern and was rewritten to "token: <REDACTED>", then record.getMessage() raised TypeError trying to substitute args into a format string with no %s placeholder. Bug had been silently masked because no test exercised the printf path. argus/audit/manifest.py: - AuditManifest.save: walk asdict(self) through mask_secrets_in_obj before json.dumps. Defense-in-depth: today's manifest schema doesn't include credential fields, but if a future field captures a docker_cmd, env dict, or credential-shaped argv it gets masked before hitting argus-audit.json. Design note (vs. roadmap text): The roadmap entry suggested reusing core/redact.redact_high_risk_patterns (the vendor-prefix-only set used by Finding.__post_init__). The existing audit/secrets.mask_secrets already covers that surface plus broader patterns appropriate for log lines (token=, password=, Bearer, URL creds, sk-keys). Extending audit/secrets keeps the redactor co-located with its callers — easier to reason about and no cross-module hop at hot-path serialization time. Test coverage (19 new): - argus/tests/audit/test_secrets.py::TestMaskSecretsInObj — 10 tests: root scalar, dict value, nested dict, list, tuple, scalar passthrough, no-mutation guard, deeply nested mix, dict-key preservation, unknown type passthrough. - argus/tests/audit/test_logger.py::TestJsonLogSecretLeakProtection — 4 tests: format-string secret, record.args secret (the regression fix), extra-field secret, non-secret strings preserved unchanged. - argus/tests/audit/test_manifest.py::TestManifestSecretLeakProtection — 5 tests: phase error, artifact path, nested dict at depth 4, input not mutated after save, clean-manifest false-positive guard. .ai/architecture.yaml: new audit/ entry in both SDK structure blocks documenting the redaction posture, the walker, and the rendered-message masking rationale. Full suite: 3126 passed (+19 new), 2 skipped.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
🔒 Argus Container Security ScanBranch: 📊 Combined Findings Summary
Scanned: 4 containers | Build Failures: 0 📦 Container Breakdown
🔍 Detailed Findings by Container🚨 cli - 73 vulnerabilities (33 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (73 findings, 33 unique)
...and 23 more ⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype 🟡 scanner-bandit - 2 vulnerabilities (2 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (2 findings, 2 unique)
⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype
|
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 7 | 44 | 63 | 114 | 50 |
🔷 Trivy Scanner (114 findings, 49 unique)
| CVE | Severity | Package | Version | Fixed |
|---|---|---|---|---|
| CVE-2026-4878 | libcap2 | 1:2.75-10+b8 | N/A | |
| CVE-2025-69720 | libncursesw6 | 6.5+20250216-2 | N/A | |
| CVE-2026-29111 | libsystemd0 | 257.9-1~deb13u1 | N/A | |
| CVE-2025-69720 | libtinfo6 | 6.5+20250216-2 | N/A | |
| CVE-2026-29111 | libudev1 | 257.9-1~deb13u1 | N/A | |
| CVE-2025-69720 | ncurses-base | 6.5+20250216-2 | N/A | |
| CVE-2025-69720 | ncurses-bin | 6.5+20250216-2 | N/A | |
| CVE-2026-27456 | 🟡 MEDIUM | bsdutils | 1:2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | bsdutils | 1:2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libblkid1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libblkid1 | 2.41-5 | N/A |
| CVE-2026-4046 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4437 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4438 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5435 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5450 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5928 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-6238 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4046 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-4437 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-4438 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5435 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5450 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5928 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-6238 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | liblastlog2-2 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | liblastlog2-2 | 2.41-5 | N/A |
| CVE-2026-34743 | 🟡 MEDIUM | liblzma5 | 5.8.1-1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libmount1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libmount1 | 2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libsmartcols1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libsmartcols1 | 2.41-5 | N/A |
| CVE-2026-40225 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40226 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-4105 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40225 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40226 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-4105 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libuuid1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libuuid1 | 2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | login | 1:4.16.0-2+really2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | login | 1:4.16.0-2+really2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | mount | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | mount | 2.41-5 | N/A |
| CVE-2026-5958 | 🟡 MEDIUM | sed | 4.9-2 | N/A |
| CVE-2026-5704 | 🟡 MEDIUM | tar | 1.35+dfsg-3.1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | util-linux | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | util-linux | 2.41-5 | N/A |
| CVE-2026-27171 | 🟡 MEDIUM | zlib1g | 1:1.3.dfsg+really1.3.1-1+b1 | N/A |
| CVE-2026-3219 | 🟡 MEDIUM | pip | 26.0.1 | N/A |
...and 64 more
⚓ Grype Scanner (0 findings, 0 unique)
✅ No vulnerabilities detected by Grype
⚠️ scanner-supply-chain - 17 vulnerabilities (17 unique)
Image: ghcr.io/huntridge-labs/argus/scanner-supply-chain:14519bbf7043689205e53c22faf0896099f1f6f0
Combined (Deduplicated)
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 9 | 8 | 0 | 17 | 17 |
🔷 Trivy Scanner (17 findings, 17 unique)
| CVE | Severity | Package | Version | Fixed |
|---|---|---|---|---|
| CVE-2026-32280 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-32281 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-32283 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-33810 | stdlib | v1.26.1 | 1.26.2 | |
| CVE-2026-33811 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-33814 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-39820 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-39836 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-42499 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-3219 | 🟡 MEDIUM | pip | 26.0.1 | N/A |
| CVE-2026-6357 | 🟡 MEDIUM | pip | 26.0.1 | 26.1 |
| CVE-2026-32282 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-32288 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-32289 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-39823 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.10, 1.26.3 |
| CVE-2026-39825 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.10, 1.26.3 |
| CVE-2026-39826 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.10, 1.26.3 |
⚓ Grype Scanner (0 findings, 0 unique)
✅ No vulnerabilities detected by Grype
Generated by Argus
Description
Closes hardening item #5 from
docs/developer/SDK-ROADMAP.md→ "Secret Handling & Credential Surface Hardening" — the final item in that section. Adds a recursive walker that masks every string in the audit trail at serialization time, plus fixes a pre-existing bug where maskingrecord.msgcorrupted%sformat strings.Changes Made
argus/audit/secrets.pywired into both audit write pathsDetails
New walker —
mask_secrets_in_obj(obj)inargus/audit/secrets.py. Recurses through dicts, lists, tuples; appliesmask_secretsto every string value; leaves keys and non-string scalars untouched; returns a new structure (does NOT mutate the input).Wired into both audit write paths:
argus/audit/logger.py::JsonLogFormatterrecord.msg(format string), thenrecord.getMessage()re-interpolatedrecord.args— secrets passed as%sargs bypassed the mask entirelyrecord.getMessage()output. Then walk the assembled JSON entry throughmask_secrets_in_objso any extra fields a contributor adds also get masked.argus/audit/logger.py::ColoredConsoleFormatterargus/audit/manifest.py::AuditManifest.savejson.dumps(asdict(self), ...)rawasdict(self)throughmask_secrets_in_objbeforejson.dumpsPre-existing bug also fixed:
The two formatters did
record.msg = mask_secrets(str(record.msg))before callinggetMessage(). When a format string contained"token: %s", thetoken:regex matched and rewrote it to"token: <REDACTED>". Thenrecord.getMessage()tried to substituterecord.argsinto a format string with no%splaceholder and raisedTypeError: not all arguments converted during string formatting. The bug had been silently latent because no existing test exercised the printf path with a token-shaped arg. The new testtest_secret_in_record_args_maskedexposed it; the fix is to mask the rendered output instead of the raw format string.Design note (vs. roadmap text):
The roadmap entry pointed at
argus.core.redact.redact_high_risk_patterns(the vendor-prefix-only set used byFinding.__post_init__). The existingargus.audit.secrets.mask_secretsalready covers that surface plus broader patterns appropriate for log lines (token=,password=,Bearer, URL creds,sk-keys). Extendingaudit/secretskeeps the redactor co-located with its callers — easier to reason about and no cross-module hop at hot-path serialization time.Testing
Test Results
19 new tests:
argus/tests/audit/test_secrets.py::TestMaskSecretsInObjargus/tests/audit/test_logger.py::TestJsonLogSecretLeakProtectionargus/tests/audit/test_manifest.py::TestManifestSecretLeakProtectionFull suite: 3126 passed (+19 new), 2 skipped, 7 deselected.
Security Considerations
Security Details
Direct implementation of hardening item (5) from the post-PR-142 audit. Two improvements:
Defense-in-depth at the audit write boundary. Today neither manifest nor logger schema includes credential fields. This PR doesn't change that — but ensures that if a future regression captures a
docker_cmd, env dict, or credential-shaped argv into a manifest field or log entry, the redaction pass catches it before the file lands on disk.Fixes a real (unrelated) bug where
logger.info("token: %s", real_token)crashed the formatter. The crash was the safety mechanism — but the right behavior is to mask, not crash. Pre-fix, callers had to avoid the printf path entirely for any string that might match the token: pattern. Now they can use the natural API and trust the formatter.AI Context Updates (.ai/)
.ai/architecture.yamlupdated — newaudit/entry in both SDK structure blocks documenting the redaction posture, the walker, and the rendered-message masking rationale..ai/workflows.yamlupdated.ai/decisions.yamlupdated — implementation of an already-decided roadmap item; no new ADR..ai/errors.yamlupdatedChecklist
Related Issues
Closes the final hardening item (#5) in
docs/developer/SDK-ROADMAP.md→ "Secret Handling & Credential Surface Hardening". All 5 items in that section are now shipped.Remaining adjacent roadmap items: Container EXPOSE ports (newly actionable from PR #147), OS image research (newly queued from PR #147), and the follow-up "migrate third-party images to
@sha256:digest pins" from PR #146.Screenshots/Logs (if applicable)
Diff: 8 files, +331 / -9.