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 ¶
func ScrubDetailDefault ¶ added in v1.0.179
ScrubDetailDefault runs ScrubDetail with the process-wide default scrubber. It is the package-level entry point the MCP DLP classifier consumes so it never needs a Scrubber handle or a second scrubber instance.
func SecretClass ¶ added in v1.0.179
SecretClass reports the coarse DLP classification for a scrubber detector name. It is a pure, fixed mapping (no allocation, no I/O) the MCP inspection layer uses to turn a scrubber hit into a stable data classification without re-deriving secret shapes. Unknown names fail closed to a generic credential secret classification (a hit is a hit).
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).
func (*Scrubber) Scrub ¶
Scrub returns s with every recognized secret shape replaced by an "[redacted:<kind>]" token, plus the number of redactions. The clean path (no format runes, no match) returns s unchanged with no output allocation.
func (*Scrubber) ScrubDetail ¶ added in v1.0.179
ScrubDetail is Scrub plus the ordered detector names that fired. It returns the redacted string, the stable detector names for every redaction (duplicates preserved, one per redaction), and the redaction count (len(names) == n except on the whole-leaf fail-closed sentinels, where n==1 and names carry the single sentinel). The raw matched secret is NEVER returned. Bounded by the same maxScanBytes fail-closed cap as Scrub.