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, or passphrase; or
  • has an underscore-separated segment equal to pwd, ssn, cvv, cvc, iban, pin, salt, or jwt; or
  • exactly matches a known credential or financial identifier such as api_key, card_number, or routing_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

builtin?(name)

@spec builtin?(String.t()) :: boolean()

Returns whether a field name is protected by the built-in rules.

compile(opts \\ [])

@spec compile(keyword()) :: Chronicle.Redaction.Policy.t()

Resolves the effective policy once.

Options:

ecto_redactor(schema, opts)

@spec ecto_redactor(
  module() | nil,
  keyword()
) :: (atom(), term() -> term())

Builds the field redactor used for Ecto changes and snapshots.

protect_field(policy, field, value)

@spec protect_field(Chronicle.Redaction.Policy.t(), term(), term()) :: term()

Applies the policy to one field, returning the value or a Sensitive marker.

protected_fields(schema, opts \\ [])

@spec protected_fields(
  module(),
  keyword()
) :: [atom()]

Returns the protected fields of an Ecto schema, for diagnostics.