Chronicle. Redaction
(chronicle v0.1.0)
Copy Markdown
Central protection policy for event data and Ecto record snapshots.
Protection is applied by field name, recursively, at every nesting depth. A protected field is never written to the ledger in clear text.
Built-in protection
Chronicle protects credential-shaped field names by default. Detection is deliberately generous because an audit ledger is append-only: a value written in clear text cannot be removed later without breaking the hash chain.
A field is protected when its name
- contains
password,passwd,secret,token,credential,apikey,privkey, orpassphrase; or - has an underscore-separated segment equal to
pwd,ssn,cvv,cvc,iban,pin,salt, orjwt; or - exactly matches a known credential or financial identifier such as
api_key,card_number, orrouting_number.
Built-in protection covers credentials, not general personal data. Classify application-specific personal data explicitly.
Configuration
Configured fields extend the built-in list:
config :chronicle,
redaction: [
fields: [:internal_note],
hash_fields: [:email],
omit_fields: [:raw_response]
]Adding one field never silently disables built-in protection. To take complete ownership of the policy, opt out explicitly:
config :chronicle,
redaction: [builtin: false, fields: [:password, :token]]Effect on Ecto record versions
A protected field cannot be reconstructed, so a snapshot containing one is
reported as incomplete and Chronicle.at/2 and Chronicle.revert/2 fail
closed rather than inventing a value. mix chronicle.doctor lists the
protected fields of every audited schema so this is a visible decision.
What name matching structurally cannot see
Every rule here matches on a field name, which means a sensitive value that
never sits under a name is invisible to all of them. A secret embedded in a
free-text description, a token inside an error message being logged as
context, an element of a list whose own key did not match — none of these are
protected, and no amount of extending the name lists will reach them.
This is not a gap to be closed later; it is the boundary of what the approach
can do. Values that carry their sensitivity in content rather than in naming
have to be marked at the point they enter, with Chronicle.secret/1,
Chronicle.hash/1, or Chronicle.omit/0. Those markers are the escape hatch
precisely because the policy cannot infer what it cannot name.
Summary
Functions
Returns whether a field name is protected by the built-in rules.
Resolves the effective policy once.
Builds the field redactor used for Ecto changes and snapshots.
Applies the policy to one field, returning the value or a Sensitive marker.
Returns the protected fields of an Ecto schema, for diagnostics.
Functions
Returns whether a field name is protected by the built-in rules.
@spec compile(keyword()) :: Chronicle.Redaction.Policy.t()
Resolves the effective policy once.
Options:
:policy- an already-resolvedChronicle.Redaction.Policy:schema- an Ecto schema whoseChronicle.Schemapolicy contributes:redact- extra fields to redact for this call
Builds the field redactor used for Ecto changes and snapshots.
@spec protect_field(Chronicle.Redaction.Policy.t(), term(), term()) :: term()
Applies the policy to one field, returning the value or a Sensitive marker.
Returns the protected fields of an Ecto schema, for diagnostics.