Documentation
¶
Overview ¶
Package cloak provides a slog.Handler wrapper that masks sensitive values.
Key-based masking is exact and cheap. Optional value detectors use a two-stage strategy: a small structural pre-filter first, followed by the exact validation required by the detector (for example, Luhn for PAN or check digits for CPF/CNPJ/SSN). This keeps the common logging path inexpensive and reduces false positives from arbitrary strings.
Naming ¶
Every option that takes a Masker takes it first:
cloak.WithKeys(cloak.KeepLast(4), "card_number") cloak.WithKeysContaining(cloak.Redact, "password") cloak.WithKeyRegex(cloak.Redact, `^x-.*-token$`) cloak.WithTag(cloak.Redact, "cloak", "secret") cloak.WithValuePredicate(cloak.Redact, pred)
The reason is Go, not taste: the subject of a key rule is a variadic list, and a variadic parameter has to come last. A Masker in the last position is therefore impossible for exactly the options people reach for most. Putting it first gives one rule with no exceptions to remember.
An option that takes several subjects says so in its name — WithKeys, WithKeysContaining. One that takes a single pattern or a single tag does not.
Index ¶
- Constants
- Variables
- func MaskCNPJ(s string) (string, bool)
- func MaskCPF(s string) (string, bool)
- func MaskEmail(s string) (string, bool)
- func MaskIBAN(s string) (string, bool)
- func MaskIPv4(s string) (string, bool)
- func MaskPAN(s string) (string, bool)
- func MaskPhone(s string) (string, bool)
- func MaskSSN(s string) (string, bool)
- func MaskUUID(s string) (string, bool)
- func New(next slog.Handler, opts ...Options) slog.Handler
- func NewDefaultPII(next slog.Handler, opts ...Options) slog.Handler
- func NewGDPR(next slog.Handler, opts ...Options) slog.Handler
- func NewLGPD(next slog.Handler, opts ...Options) slog.Handler
- func NewPCI(next slog.Handler, opts ...Options) slog.Handler
- func Redact(v slog.Value) slog.Value
- type Handler
- type Masker
- type Options
- func JoinOptions(opts ...Options) Options
- func WithCompositeScan() Options
- func WithContain(secrets ...string) Options
- func WithContextAttrs(pulls ...func(context.Context) []slog.Attr) Options
- func WithDefaultPII() Options
- func WithDefaultPIIKeys() Options
- func WithDefaultPIIValues() Options
- func WithGDPR() Options
- func WithKeyRegex(m Masker, pattern string) Options
- func WithKeyRegexp(m Masker, re *regexp.Regexp) Options
- func WithKeys(m Masker, keys ...string) Options
- func WithKeysContaining(m Masker, keys ...string) Options
- func WithLGPD() Options
- func WithMapScan() Options
- func WithMessageScan() Options
- func WithPCI() Options
- func WithRedactedValue(msg string) Options
- func WithSkipValueScan(keys ...string) Options
- func WithSliceScan() Options
- func WithStructScan() Options
- func WithTag(m Masker, key, value string) Options
- func WithType[T any](maskers ...Masker) Options
- func WithValueFunc(f ValueFunc) Options
- func WithValuePredicate(m Masker, pred func(slog.Value) bool) Options
- func WithValueRule(r ValueRule) Options
- type ValueFunc
- type ValueRule
Constants ¶
const Placeholder = "[REDACTED]"
Placeholder is the replacement a Handler uses for a fully redacted value unless WithRedactedValue says otherwise.
Variables ¶
var DefaultPIIKeys = []string{}/* 102 elements not displayed */
var GDPRKeys = []string{
"email", "emailaddress", "contactemail", "phone", "phonenumber", "telephone",
"telephonenumber", "mobile", "mobilenumber", "address", "addressline", "streetaddress",
"postaladdress", "postcode", "zip", "zipcode", "postalcode", "cep", "geolocation",
"latitude", "longitude", "gps", "ipaddress", "ip",
"ssn", "socialsecuritynumber", "cpf", "cnpj", "document", "documentnumber", "taxid",
"nationalid", "nationalidentifier", "passport", "passportnumber", "driverlicense",
"driverslicense", "licensenumber", "birthdate", "dateofbirth", "dob",
"bankaccount", "bankaccountnumber", "accountnumber", "iban", "routingnumber",
"sortcode", "swift", "creditcard", "cardnumber", "pan", "cvv", "cvc",
"health", "medical", "medicalrecord", "diagnosis", "prescription", "disability",
"genetic", "biometric", "biometricdata", "political", "politicalopinion",
"religion", "religious", "sexualorientation", "tradeunion", "unionmembership",
}
GDPRKeys are the personal-data categories of the GDPR, as attribute names.
The regulation names categories rather than fields, so this list is necessarily a guess about your schema. Treat it as a starting point and add your own: "name" is absent on purpose, since masking every name would make the logs useless.
var LGPDKeys = GDPRKeys
LGPDKeys is GDPRKeys under its Brazilian name. The two laws name the same categories, so this is the same list rather than a parallel one that could drift.
var PCIKeys = []string{
"pan", "primaryaccountnumber", "cardnumber", "creditcardnumber",
"ccnumber", "accountnumber", "cardholdername", "expmonth", "expyear", "expirationdate",
"expiry", "cvv", "cvc", "cid", "csc", "cvv2", "securitycode", "cardsecuritycode",
"pin", "pinblock", "track1", "track2", "tracks", "servicecode", "cryptogram",
"emv", "chipdata", "magstripe", "signature",
}
PCIKeys are the cardholder-data fields named by PCI DSS: the data that must never reach a log, as distinct from the many fields that merely describe a transaction.
Functions ¶
func NewDefaultPII ¶
NewDefaultPII returns a handler masking the built-in key set and value detectors.
It is New(next, WithDefaultPII()) with the compliance choice visible at the call site, which matters more here than for the other options because "what is sensitive" is a decision a reader of the code should be able to see:
slog.SetDefault(slog.New(cloak.NewDefaultPII(os.Stdout)))
func NewGDPR ¶
NewGDPR returns a handler masking the personal-data categories GDPR names.
cloak.NewGDPR(slog.NewJSONHandler(os.Stdout, nil))
func NewLGPD ¶
NewLGPD is NewGDPR under its Brazilian name, for code that already speaks LGPD.
cloak.NewLGPD(slog.NewJSONHandler(os.Stdout, nil))
func NewPCI ¶
NewPCI returns a handler masking the cardholder data PCI DSS forbids in logs.
cloak.NewPCI(slog.NewJSONHandler(os.Stdout, nil), cloak.WithStructScan())
func Redact ¶
Redact replaces a value entirely. It is the request for a full redaction rather than a message of its own: inside a handler the replacement is that handler's redacted value, which WithRedactedValue sets. Called outside one, it falls back to Placeholder.
Partial maskers like KeepLast are unaffected, since they state their own output.
Types ¶
type Masker ¶
func MaskMiddle ¶
type Options ¶
type Options interface {
// contains filtered or unexported methods
}
Options configures a Handler. One Options value carries either a single option or a set of them, following the shape encoding/json/v2 uses: opaque, composable, and impossible to construct from outside the package.
opt := cloak.WithDefaultPII() // one option policy := cloak.JoinOptions(opt, opt2, opt3) // a set, storable as a value
Individual constructors return a single option; JoinOptions coalesces any number into one value that can be named, stored and passed around, which a slice of options cannot be. Properties set later override earlier ones, as everywhere else here.
func JoinOptions ¶
JoinOptions coalesces options into a single Options value, applied in order.
func WithCompositeScan ¶
func WithCompositeScan() Options
WithCompositeScan enables WithStructScan, WithMapScan and WithSliceScan at once, and is what you want unless you have a reason to scope the walk.
Pointers are always followed, since a pointer is just an address to the value behind it.
func WithContain ¶
WithContain masks any value that contains one of the given strings, wherever it appears.
cloak.WithContain("s3cr3t-token")
It solves the problem the other rules cannot: you know the secret but not where it will be logged. A token spliced into a URL, an error message quoting a response body, an auth header assembled by a client — none of those carry a field name worth matching, and the value detectors have no idea what your token looks like.
It is a value rule, so it applies to the message, to attribute values, and everywhere the composite walk descends. Matching is case sensitive, since a secret is a specific byte sequence. The whole value is replaced rather than the substring, because the rest of the value may be sensitive too.
Empty secrets are ignored: an empty needle matches everything, which would silence every log line.
func WithContextAttrs ¶
WithContextAttrs registers functions that pull values out of the context and copy them onto every record, masked by the same rules as attributes you logged yourself.
It exists because a value carried in the context is attached once, usually far from the log call that will expose it:
ctx = context.WithValue(ctx, userKey, user) // middleware, no logging in sight slog.InfoContext(ctx, "loaded") // somewhere else, entirely
A pull function rather than a key, because the idiomatic context key is a private type: naming it from outside the package that owns it does not compile, and exporting it just to configure a logger gives up the collision safety that makes the private type worth having. The closure lives where the key is reachable, so the key never leaves its package:
// package auth
func LogAttrs(ctx context.Context) []slog.Attr {
u, ok := userFrom(ctx) // uses the unexported key
if !ok {
return nil
}
return []slog.Attr{slog.Any("user", u)}
}
// package main
cloak.WithContextAttrs(auth.LogAttrs)
Masking is not special-cased: a key rule, a detector, a composite walk and LogValuer precedence all apply exactly as they would to an attribute you logged yourself. Return nil for anything absent, so a missing value is skipped rather than logged as null.
It also suits sources that are not context values at all, such as request-scoped state kept by a web framework.
func WithDefaultPII ¶
func WithDefaultPII() Options
func WithDefaultPIIKeys ¶
func WithDefaultPIIKeys() Options
func WithDefaultPIIValues ¶
func WithDefaultPIIValues() Options
func WithGDPR ¶
func WithGDPR() Options
WithGDPR enables the personal-data categories named by the GDPR.
It is a starting point rather than a compliance claim: the regulation names categories, and only your schema says which attribute holds each one. Review it, and add whatever your domain calls something else.
func WithKeyRegex ¶
WithKeyRegex masks attributes whose key matches the pattern, compiled with regexp.MustCompile.
It panics on an invalid pattern, which is the point: a typo would otherwise become a rule that silently matches nothing, and PII would leave the program with no signal. Failing at construction time means it fails at startup, where it is loud. Use WithKeyRegexp if you would rather handle the compile error yourself.
The pattern matches the attribute name **as written**, not the normalized key, so `_key$` and `^x-.*-token$` mean what they look like. normalizeKey strips the underscores and hyphens a pattern usually wants to anchor on.
cloak.WithKeyRegex(cloak.Redact, `_key$`) cloak.WithKeyRegex(cloak.Redact, `^x-.*-token$`)
Precedence is key rule, then this, then WithKeysContaining: an exact key is the most specific statement of intent, a pattern is deliberate, and a substring is the loosest.
func WithKeyRegexp ¶
WithKeyRegexp is WithKeyRegex for a pattern you compiled yourself, so a bad pattern is your error to handle rather than a panic.
pattern, err := regexp.Compile(os.Getenv("REDACT_PATTERN"))
if err != nil {
return err
}
cloak.WithKeyRegexp(cloak.Redact, pattern)
func WithKeys ¶
WithKeys masks attributes whose normalized key exactly matches one of keys.
cloak.WithKeys(cloak.Redact, "password", "cpf") cloak.WithKeys(cloak.KeepLast(4), "card_number")
Matching is on the normalized key, so card_number, cardNumber, CARD-NUMBER and "Card Number" are all the same rule from one entry.
func WithKeysContaining ¶
WithKeysContaining masks attributes whose normalized key contains one of keys as a substring. It catches the qualified names that exact matching misses, such as "db.password" or "headers.Authorization".
cloak.WithKeysContaining(cloak.Redact, "password")
It is looser than WithKeys: "token" also matches "tokens_used". Keys holding unrelated data ("id", "name") make it a poor choice, so prefer WithKeys and reach for this only where the prefixes are known.
func WithLGPD ¶
func WithLGPD() Options
WithLGPD is WithGDPR under its Brazilian name, for code that already speaks LGPD.
func WithMapScan ¶
func WithMapScan() Options
WithMapScan walks map values logged through slog.Any, masking both keys and values. A key match wins over the value detectors, so map[string]any{"password": "x"} is caught by name while a bare address is caught as a value.
cloak.WithMapScan() // slog.Any("ctx", map[string]any{"email": ...})
func WithMessageScan ¶
func WithMessageScan() Options
WithMessageScan also runs the value detectors over the log message.
Attributes are opt-in per key, but the message is free text nobody reviews, and `slog.Info("login for "+email)` is a routine way to leak. It is off by default because rewriting the message surprises readers and costs a scan per record.
func WithPCI ¶
func WithPCI() Options
WithPCI enables the cardholder-data fields PCI DSS forbids in logs, plus the PAN detector so a card number is caught even under a field name you did not anticipate.
It does not mask every field that mentions money — amounts, currencies and merchant descriptors are not cardholder data, and masking them would cost you the transaction history that makes a log useful.
func WithRedactedValue ¶
WithRedactedValue sets what a fully redacted value is replaced with.
cloak.NewPCI(next, cloak.WithRedactedValue("***"))
It is a property of the handler rather than of any rule: every request for a full redaction answers with this string, whatever asked for it — a key rule, a tag, a type, a pattern, or one of the presets. Partial maskers keep their own output, since KeepLast and friends state exactly what they produce.
func WithSkipValueScan ¶
WithSkipValueScan keeps the value detectors off a set of keys, so a field that legitimately holds something shaped like PII keeps its payload readable.
cloak.WithSkipValueScan("raw_payload", "request_body")
The key names are matched like any other key rule, so the spelling does not have to match exactly. It applies wherever the name can appear: an attribute, a struct field, or an entry in a walked map.
An explicit rule beats the skip list, the same way it beats everything else — a key named here and also named by WithKeys or a preset is masked, since the explicit statement is the more specific one.
func WithSliceScan ¶
func WithSliceScan() Options
WithSliceScan walks slice values logged through slog.Any, masking each element and recursing into nested containers.
[]byte is excluded: it is data, not a container of things to log, and walking it would rewrite a payload into decimal digits.
cloak.WithSliceScan() // slog.Any("emails", []string{"a@example.com"})
func WithStructScan ¶
func WithStructScan() Options
WithStructScan walks struct values logged through slog.Any, masking PII in their fields by field name and by value.
The value keeps its type: only the offending fields are replaced, so the record still logs a structured object.
cloak.WithStructScan() // slog.Any("user", User{Email: ...})
func WithTag ¶
WithTag masks a struct field carrying the given struct tag.
type Account struct {
ID int
Password string `cloak:"secret"`
}
cloak.WithTag(cloak.Redact, "cloak", "secret")
A tag is the most durable way to mark a field: renaming the field does not lose the rule, and the intent sits on the field rather than in the logger's configuration.
Rules for different tag keys can coexist, since the key is part of each rule. A field matching no rule is walked normally.
func WithType ¶
WithType masks every value whose Go type is exactly T.
type EmailAddr string type Password string cloak.WithType[EmailAddr](), cloak.WithType[Password](cloak.KeepLast(0))
This is the strongest rule available and the cheapest to use correctly. The compiler refuses to pass a type that does not exist, the value cannot be logged under a name you forgot to add to a preset, and there are no false positives: a Password is masked wherever it is, and nothing else is.
It applies wherever the value appears: as an attribute, a struct field, a map value or a slice element. Values of a named type are not KindString to slog, so this also reaches types the value detectors cannot see.
With no masker, the value is fully redacted.
Precedence: an explicit key rule wins over a type rule, and a type rule wins over the value detectors.
func WithValueFunc ¶
func WithValuePredicate ¶
WithValuePredicate masks a value of any kind when pred accepts it. It is WithValueRule for the common case where deciding and replacing are separate.
cloak.WithValuePredicate(cloak.Redact, func(v slog.Value) bool {
return v.Kind() == slog.KindInt64 && v.Int64() > 1_000_000_000_000
})
func WithValueRule ¶
WithValueRule registers a rule over values of any kind.
A rule runs after key, tag and type rules and before the format detectors, because a rule is a deliberate statement while a detector infers from the shape of a string. It is not applied to a group, since replacing one would discard its structure.
WithSkipValueScan governs value rules as it does the detectors.
type ValueFunc ¶
func DefaultPIIValueFuncs ¶
func DefaultPIIValueFuncs() []ValueFunc
DefaultPIIValueFuncs returns the built-in detectors, in the order they must run.
Two rules decide the order. Cheapest structural gate first, so a string that cannot possibly match costs one comparison. Most specific before least specific, because maskString chains: each detector sees the previous one's output.
That chaining is why IPv4 must precede SSN. An IPv4 address like "192.168.1.42" is exactly nine digits and passes the SSN shape check, so SSN first turns the most common private address into "***-**-****" and leaves IPv4 nothing to match. Phone is last because it claims almost any number, documents included.
type ValueRule ¶
ValueRule inspects a value of any kind and returns its replacement, or reports that it has nothing to say. It mirrors ValueFunc exactly — same shape, same direction — so the two are one idea rather than two.
Use it where a string detector cannot reach: an int, a duration, a time, a bool, or any value at all, including a struct reached through slog.Any.
cloak.WithValueRule(func(v slog.Value) (slog.Value, bool) {
if v.Kind() != slog.KindInt64 {
return v, false
}
n := v.Int64()
if n < 1_000_000_000_000 {
return v, false
}
return slog.StringValue("[large]"), true
})
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
basic
command
Command basic is the smallest setup that does anything useful: wrap the handler you already have and turn on the preset.
|
Command basic is the smallest setup that does anything useful: wrap the handler you already have and turn on the preset. |
|
containers
command
Command containers reaches inside the data slog hands over as one opaque value.
|
Command containers reaches inside the data slog hands over as one opaque value. |
|
context
command
Command context masks a value that was attached far from the log call that exposes it.
|
Command context masks a value that was attached far from the log call that exposes it. |
|
context/auth
Package auth stands in for whatever package owns the identity.
|
Package auth stands in for whatever package owns the identity. |
|
keys
command
Command keys shows the three ways to name a field, and the fixed order they are tried in.
|
Command keys shows the three ways to name a field, and the fixed order they are tried in. |
|
pci
command
Command pci shows a compliance preset, and the one thing worth doing with it: overriding a rule when your schema disagrees with the default.
|
Command pci shows a compliance preset, and the one thing worth doing with it: overriding a rule when your schema disagrees with the default. |