Problem
scrub_details (app/services/scrubber.py) redacts any string of 32+ characters composed of word-ish characters:
LONG_KEY_RE = re.compile(r"^[A-Za-z0-9+/=_\-]{32,}$")
...
elif len(value) >= 32 and LONG_KEY_RE.match(value):
safe[key] = "[REDACTED_KEY_MATERIAL]"
The pattern matches a 32-character hex string, a 36-character UUID, a long single-word identifier, a base64 blob, or a hyphenated memo — none of which are necessarily secrets.
Consequences:
- Legitimate diagnostic data is destroyed: a
details value like {"token_family": "fam_3f2a...64-chars"} or {"job_id": "<36-char-uuid>"} is replaced with [REDACTED_KEY_MATERIAL], so auditors lose exactly the identifiers they need to correlate events.
- The redaction is permanent in the chain: once scrubbed, the original value cannot be recovered from the audit log, and downstream consumers (GDPR export, dispute review) see the placeholder.
- The heuristic is both too aggressive and too weak: it mangles UUIDs/long ids (false positives) while still missing real secrets in nested structures (tracked separately).
Root cause
A length+character-class heuristic was chosen as a catch-all for "key-like" strings without considering the codebase's own long identifiers (UUIDs, token family ids, job ids).
Why this is architecturally hard
- Distinguishing secrets from long identifiers requires more than length: the checksummed formats (Stellar secret
S... 56 chars, ed25519 88 chars) are precise, but the 32+ catch-all exists to cover unknown formats — narrowing it risks leaking genuinely new secret formats.
- The redaction policy is a security-vs-observability tradeoff that should be configurable per event type; today it is global, so a fix that removes the catch-all affects every audit consumer.
- Tests must assert both directions: known secret formats are redacted and legitimate long identifiers (UUIDs, fam_/job_/user_ ids) survive.
Proposed design
Tighten LONG_KEY_RE to the formats the codebase actually treats as secret material (or gate it behind an AUDIT_SCRUB_LONG_STRINGS flag defaulting to the precise regexes only), and add tests covering the codebase's own id formats (UUID, fam_*, job_*, atk_*/rtk_* token prefixes are not stored raw).
Acceptance criteria
Service
Tests
Out of scope
Nested-secret recursion (tracked separately) and retention.
Getting started
pytest tests/test_gdpr.py -q
make typecheck
Good first files to read: app/services/scrubber.py, app/models/orm/audit_log.py.
Problem
scrub_details(app/services/scrubber.py) redacts any string of 32+ characters composed of word-ish characters:The pattern matches a 32-character hex string, a 36-character UUID, a long single-word identifier, a base64 blob, or a hyphenated memo — none of which are necessarily secrets.
Consequences:
detailsvalue like{"token_family": "fam_3f2a...64-chars"}or{"job_id": "<36-char-uuid>"}is replaced with[REDACTED_KEY_MATERIAL], so auditors lose exactly the identifiers they need to correlate events.Root cause
A length+character-class heuristic was chosen as a catch-all for "key-like" strings without considering the codebase's own long identifiers (UUIDs, token family ids, job ids).
Why this is architecturally hard
S...56 chars, ed25519 88 chars) are precise, but the 32+ catch-all exists to cover unknown formats — narrowing it risks leaking genuinely new secret formats.Proposed design
Tighten
LONG_KEY_REto the formats the codebase actually treats as secret material (or gate it behind anAUDIT_SCRUB_LONG_STRINGSflag defaulting to the precise regexes only), and add tests covering the codebase's own id formats (UUID,fam_*,job_*,atk_*/rtk_*token prefixes are not stored raw).Acceptance criteria
Service
Tests
Out of scope
Nested-secret recursion (tracked separately) and retention.
Getting started
Good first files to read:
app/services/scrubber.py,app/models/orm/audit_log.py.