Documentation
¶
Overview ¶
Package redaction is the data-governance engine for support bundles (REDACTION-MODEL.md, ADR-0009). It classifies collected data by DataClass and redacts STRUCTURALLY at the source: SECRET/NEVER_EXPORT fields are dropped, SENSITIVE fields are masked, and any unclassified field fails closed to SENSITIVE (masked, never passed through). Regexes are a later backstop; the primary control is field-class-driven and deterministic.
Index ¶
Constants ¶
const DefaultClass = ClassSensitive
DefaultClass is the fail-closed class for any collected field with no explicit classification: masked, never leaked (REDACTION-MODEL §1/§9, P3).
ShareableCeiling is the highest class permitted, post-redaction, in a shareable bundle. SENSITIVE data is only allowed in masked form (which the redactor treats as effectively INTERNAL), and SECRET/NEVER_EXPORT are dropped, so a redacted section's class_max never exceeds this by construction.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type DataClass ¶
type DataClass int
DataClass is the ordered sensitivity taxonomy. Higher = more restricted.
const ( // ClassPublic is non-sensitive; safe for anyone (version, build, counts). ClassPublic DataClass = iota // ClassInternal is operationally revealing but not secret (rule names, // hostnames, health verdicts). Default ceiling for a shareable bundle. ClassInternal // ClassSensitive is identifying/confidential; masked before export // (usernames, client IPs, full URLs, DNs). ClassSensitive // ClassSecret is credential/authenticator material; dropped, never masked // and kept (passwords, HMACs, tokens, client secrets). ClassSecret // ClassNeverExport is key material that must never cross the process // boundary (KEK, CA/TLS private keys). The redactor only asserts, in tests, // that no collector can name such a field — the bytes are unreachable by // construction (internal/secret, ADR-0007). ClassNeverExport )
func ParseClass ¶
ParseClass maps a struct-tag/wire spelling to a DataClass (case-insensitive). The second return is false for an unknown token — callers fail closed.
type Redactor ¶
type Redactor interface {
// Struct returns the redacted, JSON-serializable form of v. Collectors call
// this on every value before writing it to a section.
Struct(v any) any
// Classify is Struct plus the post-redaction class_max and mask/drop counts,
// used by the runner for the manifest section entry and redaction report.
Classify(v any) Result
}
Redactor redacts collected values structurally by DataClass. Field classes are declared with a `redact:"public|internal|sensitive|secret|never_export"` struct tag; an untagged (or unknown-tagged) exported field fails closed to SENSITIVE.
func New ¶
func New() Redactor
New builds a Redactor with a fresh random per-bundle salt. The salt is NEVER_EXPORT — never written to a bundle — so masked tokens are irreversible and do not correlate across bundles (REDACTION-MODEL §4).
func NewWithSalt ¶
NewWithSalt is the deterministic constructor for tests (fixed salt → fixed masked tokens), mirroring the injected-clock discipline of the engine tests.
type Result ¶
type Result struct {
Value any // JSON-serializable redacted form
ClassMax DataClass // highest class present AFTER redaction (<= ShareableCeiling)
Masked int // count of SENSITIVE fields masked
Dropped int // count of SECRET/NEVER_EXPORT fields dropped
Scrubbed int // count of free-form secret-shapes redacted in KEPT strings
// RetainedFreeForm is a BOUNDED, post-scrub sample of INTERNAL free-form
// string values KEPT in the shareable output. The scrubber is precision-first
// (no entropy rule, by design), so a bare shapeless secret typed into an
// operator-controlled free-form field (a policy rule name, an upstream
// endpoint, a diagnostic message) survives it — and the bundled
// redaction-report is counts-only (P4/P6). These samples feed the
// pre-export consent surface ONLY (server-side; never written to the tar) so
// the approving admin can SEE what automated redaction structurally cannot
// catch. Deterministic; capped by the maxRetained* constants below.
RetainedFreeForm []string
}
Result is the outcome of redacting one value.
type Scrubber ¶
type Scrubber struct {
// contains filtered or unexported fields
}
Scrubber is the free-form secret backstop (REDACTION-MODEL: "Regexes are a later backstop"). Structural DataClass redaction masks/drops whole classified FIELDS; the scrubber is the second layer that catches a secret EMBEDDED inside a field that is legitimately KEPT — e.g. an INTERNAL diagnostic message "dial tcp: bad auth Bearer ghp_…", an audit Detail, a config value, a log URI.
Design (adversarially reviewed twice, M2 PR1): precision over recall. Every pattern is a fixed, self-identifying credential SHAPE; there is deliberately NO bare length/entropy rule, so git SHAs, SHA-256 hashes, base64 cert fingerprints, ULIDs, UUIDs, and X-Request-IDs pass through verbatim and bundles stay useful. Bare context-free secrets are left to the structural SECRET class (dropped).
The engine is bounded and deterministic (bundle hashing depends on it): Go regexp is RE2 (linear, no catastrophic backtracking); the clean path is at most two scans (format-rune check + one detector match) with no output allocation; a leaf over maxScanBytes or with more than maxReplacements matches is replaced WHOLE (fail-closed). The replacement token contains no trigger substring, so Scrub is idempotent: Scrub(Scrub(s))==Scrub(s).