Documentation
¶
Overview ¶
Package masker provides configurable, fail-closed masking of sensitive data.
A Masker is immutable: it does not mutate input values, returns normalized copies of supported containers, and is safe for concurrent use after construction. The package requires Go 1.23 and uses only the standard library.
Applications should use DefaultPolicy for the conservative built-in key bindings or provide a Policy that matches their own data model. Built-in rules include full, password, token, email, phone, ID, and card masking; struct tags can select them explicitly or omit a field.
MaskJSON preserves json.Number precision and MaskJSONReader reads the whole input subject to WithMaxInputBytes. Reflection operations return normalized copies, never mutate inputs, and fail closed on invalid UTF-8, unsupported values, callback errors, cycles, or resource limits. WithPreserveSafeTypes keeps safe primitive types in reflection results.
Error categories are available through errors.Is and the exported Err* values; detailed safe context is available through errors.As.
Subpackages apply the same policy at the edges: httpmask to headers and URLs, always fully redacting cookies and URL userinfo; slogmask to log/slog attributes; zerologmask to the JSON lines written by zerolog and any other logger that writes one JSON object per line; and zapmask to the JSON lines of zap's JSON encoder.
Example (CycleDetection) ¶
package main
import (
"errors"
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
node := map[string]any{"name": "root"}
node["self"] = node
masked, err := m.MaskAny(node)
fmt.Println(masked, errors.Is(err, masker.ErrCycle))
}
Output: [REDACTED] true
Example (ErrorCategories) ¶
package main
import (
"errors"
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
_, err = m.MaskJSON([]byte(`{"broken":`))
// errors.Is matches the category, errors.As reaches the safe details.
// Errors never carry the original value.
var detail *masker.MaskError
fmt.Println(errors.Is(err, masker.ErrInvalidJSON), errors.As(err, &detail), detail.Code, detail.Operation)
}
Output: true true invalid_json mask_json
Example (OmitField) ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
// A policy can drop a field entirely instead of replacing its value.
policy := masker.PolicyFunc(func(field masker.Field) (masker.Decision, error) {
if field.Key == "internal_note" {
return masker.Decision{Omit: true}, nil
}
return masker.Decision{}, nil
})
m, err := masker.New(policy)
if err != nil {
panic(err)
}
masked, err := m.MaskJSON([]byte(`{"internal_note":"debug only","user":"alice"}`))
if err != nil {
panic(err)
}
fmt.Println(string(masked))
}
Output: {"user":"alice"}
Example (StructTags) ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
type payload struct {
Password string `mask:"password"`
Name string `json:"name"`
}
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
masked, err := m.MaskAny(payload{Password: "synthetic-secret", Name: "alice"})
if err != nil {
panic(err)
}
values := masked.(map[string]any)
fmt.Println(values["Password"], values["name"])
}
Output: [REDACTED] alice
Index ¶
- Constants
- Variables
- type Binding
- type Decision
- type ErrorCode
- type Field
- type KeyPolicy
- type MaskError
- type MaskErrors
- type Masker
- func (m *Masker) MaskAny(value any) (result any, err error)
- func (m *Masker) MaskField(field Field, value any) (result any, err error)
- func (m *Masker) MaskJSON(src []byte) (result []byte, err error)
- func (m *Masker) MaskJSONReader(src io.Reader) (result []byte, err error)
- func (m *Masker) MaskJSONValue(value any) (result []byte, err error)
- func (m *Masker) MaskString(value string, rule Rule) (result string, err error)
- func (m *Masker) MaskValue(key string, value any) (any, error)
- type Option
- func WithAWSKeyIDDetection() Option
- func WithCardNumberDetection() Option
- func WithMaxDepth(depth int) Option
- func WithMaxInputBytes(bytes int64) Option
- func WithMaxNodes(nodes int) Option
- func WithPreserveSafeTypes() Option
- func WithRedaction(marker string) Option
- func WithStructTag(name string) Option
- func WithTagRule(name string, rule Rule) Option
- func WithoutEmbeddedDocuments() Option
- func WithoutTextDetectors() Option
- func WithoutValueInspection() Option
- type Policy
- type PolicyFunc
- type Rule
- type RuleFunc
- type RuleInput
- type Source
- type ValueKind
Examples ¶
- Package (CycleDetection)
- Package (ErrorCategories)
- Package (OmitField)
- Package (StructTags)
- Chain
- DefaultBindings
- Masker.MaskAny (Errors)
- Masker.MaskField
- Masker.MaskJSON
- Masker.MaskJSONReader
- Masker.MaskJSONValue
- Masker.MaskString
- Masker.MaskValue (EmbeddedDocuments)
- Masker.MaskValue (SecretsInText)
- New
- NewKeyPolicy
- NewRule
- PolicyFunc
- WithAWSKeyIDDetection
- WithCardNumberDetection
- WithMaxDepth
- WithMaxInputBytes
- WithMaxNodes
- WithPreserveSafeTypes
- WithRedaction
- WithStructTag
- WithTagRule
- WithoutEmbeddedDocuments
- WithoutTextDetectors
- WithoutValueInspection
Constants ¶
const ( // DefaultRedactionMarker is used for sensitive and fail-closed values. DefaultRedactionMarker = "[REDACTED]" // DefaultStructTag is the default struct tag name. DefaultStructTag = "mask" )
Variables ¶
var ( // ErrInvalidConfig reports invalid masker configuration. ErrInvalidConfig = errors.New("masker: invalid configuration") // ErrInvalidJSON reports invalid JSON syntax. ErrInvalidJSON = errors.New("masker: invalid JSON") // ErrInvalidUTF8 reports invalid UTF-8 input. ErrInvalidUTF8 = errors.New("masker: invalid UTF-8") // ErrInputLimit reports input exceeding the configured byte limit. ErrInputLimit = errors.New("masker: input limit exceeded") // ErrDepthLimit reports traversal exceeding the configured depth limit. ErrDepthLimit = errors.New("masker: depth limit exceeded") // ErrNodeLimit reports traversal exceeding the configured node limit. ErrNodeLimit = errors.New("masker: node limit exceeded") // ErrCycle reports an active traversal cycle. ErrCycle = errors.New("masker: cycle detected") // ErrUnsupportedType reports an unsupported input value type. ErrUnsupportedType = errors.New("masker: unsupported type") // ErrUnsupportedKey reports an unsupported map key type. ErrUnsupportedKey = errors.New("masker: unsupported map key") // ErrFieldConflict reports conflicting visible fields. ErrFieldConflict = errors.New("masker: field conflict") // ErrPolicyFailure reports a policy failure. ErrPolicyFailure = errors.New("masker: policy failure") // ErrRuleFailure reports a rule failure. ErrRuleFailure = errors.New("masker: rule failure") // ErrPanic reports a panic from a policy or rule callback. ErrPanic = errors.New("masker: callback panic") )
Functions ¶
This section is empty.
Types ¶
type Binding ¶
Binding associates field keys with a rule. Keys compare equal when they differ only by Unicode case or by the separator characters "_", "-", and ".", so access_token, access-token, and accessToken are equivalent.
func DefaultBindings ¶
func DefaultBindings() []Binding
DefaultBindings returns a defensive copy of the built-in key policy.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
// The built-in key bindings are public and inspectable; DefaultBindings
// returns a copy that can be extended without touching the defaults.
for _, binding := range masker.DefaultBindings() {
fmt.Printf("%-8s %v\n", binding.Rule.Name(), binding.Keys)
}
}
Output: password [password passwd passphrase pwd] token [token access_token refresh_token api_key apikey api_token secret client_secret secret_key secret_access_key aws_secret_access_key id_token private_key private_token session_id credentials auth_token otp] email [email e-mail] phone [phone phone_number mobile] id [id user_id customer_id] card [card card_number pan] full [authorization cookie set-cookie x-api-key x-api-token x-access-token x-auth-token proxy-authorization x-csrf-token cvv cvc]
type ErrorCode ¶
type ErrorCode string
ErrorCode identifies a safe, non-sensitive masking failure category.
const ( CodeInvalidConfig ErrorCode = "invalid_config" // Invalid configuration. CodeInvalidJSON ErrorCode = "invalid_json" // Invalid JSON syntax. CodeInvalidUTF8 ErrorCode = "invalid_utf8" // Invalid UTF-8 input. CodeInputLimit ErrorCode = "input_limit" // Input exceeded its byte limit. CodeDepthLimit ErrorCode = "depth_limit" // Traversal exceeded its depth limit. CodeNodeLimit ErrorCode = "node_limit" // Traversal exceeded its node limit. CodeCycle ErrorCode = "cycle" // An active traversal cycle was found. CodeUnsupportedType ErrorCode = "unsupported_type" // A value type is unsupported. CodeUnsupportedKey ErrorCode = "unsupported_map_key" // A map key type is unsupported. CodeFieldConflict ErrorCode = "field_conflict" // Visible fields conflict. CodePolicyFailure ErrorCode = "policy_failure" // A policy failed. CodeRuleFailure ErrorCode = "rule_failure" // A rule failed. CodePanic ErrorCode = "panic" // A callback panicked. )
type KeyPolicy ¶
type KeyPolicy struct {
// contains filtered or unexported fields
}
KeyPolicy matches complete keys across all naming conventions: keys compare equal when they differ only by Unicode case or by the separator characters "_", "-", and ".".
func NewKeyPolicy ¶
NewKeyPolicy validates and compiles key bindings. Keys compare equal when they differ only by Unicode case or by the separator characters "_", "-", and "."; a key that becomes empty once those separators are removed is rejected as an empty key. Duplicate detection is a one-time cost paid here so conflicting fold-equivalent keys never reach the per-decision hot path. Such keys are accepted only when they refer to the same comparable Rule instance; RuleFunc values are not comparable, so repeated fold-equivalent keys using a custom callback are rejected even when the callback function is the same.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
policy, err := masker.NewKeyPolicy(
masker.Binding{Keys: []string{"ssn", "tax_id"}, Rule: masker.FullRule()},
masker.Binding{Keys: []string{"contact"}, Rule: masker.EmailRule()},
)
if err != nil {
panic(err)
}
m, err := masker.New(policy)
if err != nil {
panic(err)
}
// Key matching is case-insensitive.
masked, err := m.MaskJSON([]byte(`{"Tax_ID":"123-45-6789","contact":"alice@example.com","team":"core"}`))
if err != nil {
panic(err)
}
fmt.Println(string(masked))
}
Output: {"Tax_ID":"[REDACTED]","contact":"a***@example.com","team":"core"}
type MaskError ¶
type MaskError struct {
Code ErrorCode
Operation string
Path string
Field string
ConflictingField string
Depth int
Rule string
}
MaskError describes one masking failure without exposing source values.
type MaskErrors ¶
type MaskErrors struct {
Items []*MaskError
}
MaskErrors aggregates local failures without retaining unsafe callback errors.
func (*MaskErrors) Error ¶
func (e *MaskErrors) Error() string
Error returns a safe summary of the aggregate.
func (*MaskErrors) Unwrap ¶
func (e *MaskErrors) Unwrap() []error
Unwrap exposes aggregate members to errors.Is and errors.As.
type Masker ¶
type Masker struct {
// contains filtered or unexported fields
}
Masker masks sensitive values according to an immutable policy.
func New ¶
New validates configuration and returns a concurrency-safe Masker.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
value, err := m.MaskValue("password", "synthetic-secret")
if err != nil {
panic(err)
}
fmt.Println(value)
}
Output: [REDACTED]
func (*Masker) MaskAny ¶
MaskAny masks an arbitrary supported value.
Example (Errors) ¶
package main
import (
"errors"
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
masked, err := m.MaskAny(func() {})
var maskErr *masker.MaskError
fmt.Println(masked, errors.As(err, &maskErr), maskErr.Code)
}
Output: [REDACTED] true unsupported_type
func (*Masker) MaskField ¶
MaskField masks a value using an explicit field context.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
// The same key and value under two sources. Transport metadata is treated
// paranoidly: a header is fully redacted even when the policy rule is
// partial.
body, err := m.MaskField(masker.Field{Key: "email", Source: masker.SourceJSON}, "alice@example.com")
if err != nil {
panic(err)
}
header, err := m.MaskField(masker.Field{Key: "email", Source: masker.SourceHeader}, "alice@example.com")
if err != nil {
panic(err)
}
fmt.Println(body)
fmt.Println(header)
}
Output: a***@example.com [REDACTED]
func (*Masker) MaskJSON ¶
MaskJSON masks exactly one JSON document and returns valid JSON on success.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
masked, err := m.MaskJSON([]byte(`{"email":"alice@example.com","token":"synthetic-secret","count":42}`))
if err != nil {
panic(err)
}
fmt.Println(string(masked))
}
Output: {"count":42,"email":"a***@example.com","token":"[REDACTED]"}
func (*Masker) MaskJSONReader ¶
MaskJSONReader reads, validates, masks, and encodes one JSON document. The reader is not closed and the complete input is held in memory.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
// The reader is read to completion and is not closed by the library.
body := strings.NewReader(`{"api_key":"synthetic-key","page":2}`)
masked, err := m.MaskJSONReader(body)
if err != nil {
panic(err)
}
fmt.Println(string(masked))
}
Output: {"api_key":"[REDACTED]","page":2}
func (*Masker) MaskJSONValue ¶ added in v0.6.0
MaskJSONValue masks value as MaskAny does and returns the result encoded as JSON, without building the intermediate tree. Object keys are sorted, as encoding/json sorts map keys. With WithPreserveSafeTypes a safe number or boolean is written as a JSON number or boolean, a value of a named scalar type as its underlying value (its MarshalJSON method is never called), and a NaN or infinite float fails closed. On any error the result is the redaction marker encoded as JSON.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
out, err := m.MaskJSONValue(map[string]any{"user": "alice", "password": "s3cret", "attempts": 3})
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
Output: {"attempts":"3","password":"[REDACTED]","user":"alice"}
func (*Masker) MaskString ¶
MaskString applies a rule to one string and fails closed on rule errors.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
for _, c := range []struct {
rule masker.Rule
value string
}{
{masker.EmailRule(), "alice@example.com"},
{masker.PhoneRule(), "+1 (555) 123-4567"},
{masker.CardRule(), "4111 1111 1111 1111"},
{masker.IDRule(), "user-8891"},
{masker.TokenRule(), "synthetic-token"},
} {
masked, err := m.MaskString(c.value, c.rule)
if err != nil {
panic(err)
}
fmt.Printf("%-8s %s\n", c.rule.Name(), masked)
}
}
Output: email a***@example.com phone +* (***) ***-4567 card **** **** **** 1111 id *****8891 token [REDACTED]
func (*Masker) MaskValue ¶
MaskValue masks a value under a map-like key.
Example (EmbeddedDocuments) ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
// A callback URL and a request body carried as plain strings are masked
// as the documents they hold: the field names never appear in a
// sensitive-key list, but the values speak for themselves.
callback, err := m.MaskValue("note", "https://u:dummy@h/cb?token=dummy-token")
if err != nil {
panic(err)
}
body, err := m.MaskValue("note", `{"password":"dummy-secret"}`)
if err != nil {
panic(err)
}
fmt.Println(callback)
fmt.Println(body)
}
Output: https://%5BREDACTED%5D@h/cb?token=%5BREDACTED%5D {"password":"[REDACTED]"}
Example (SecretsInText) ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
for _, text := range []string{
"login failed: password=dummy-pass",
"upstream said: Bearer dummy-token-placeholder rejected",
"dial postgres://app:dummy@db:5432/app failed",
} {
masked, err := m.MaskValue("message", text)
if err != nil {
panic(err)
}
fmt.Println(masked)
}
}
Output: login failed: password=[REDACTED] upstream said: Bearer [REDACTED] rejected dial postgres://[REDACTED]@db:5432/app failed
type Option ¶
type Option func(*config) error
Option configures a Masker during construction. The option type is intentionally closed; callers use the exported With* constructors.
func WithAWSKeyIDDetection ¶ added in v0.5.0
func WithAWSKeyIDDetection() Option
WithAWSKeyIDDetection also finds AWS access key ids, such as "AKIA..." with sixteen more uppercase letters or digits, in free text. A key id alone is not a credential, so it is off by default.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy(), masker.WithAWSKeyIDDetection())
if err != nil {
panic(err)
}
masked, err := m.MaskValue("message", "rotated AKIADUMMYEXAMPLE0000")
if err != nil {
panic(err)
}
fmt.Println(masked)
}
Output: rotated [REDACTED]
func WithCardNumberDetection ¶ added in v0.5.0
func WithCardNumberDetection() Option
WithCardNumberDetection also finds payment card numbers in free text: 13 to 19 digits, optionally grouped by spaces or dashes, that pass the Luhn check. They are masked by CardRule, keeping the last four digits. It is off by default because long numeric identifiers pass the Luhn check by chance one time in ten.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy(), masker.WithCardNumberDetection())
if err != nil {
panic(err)
}
// 4111 1111 1111 1111 is the well-known example Visa number.
masked, err := m.MaskValue("message", "paid with 4111 1111 1111 1111")
if err != nil {
panic(err)
}
fmt.Println(masked)
}
Output: paid with **** **** **** 1111
func WithMaxDepth ¶
WithMaxDepth limits recursive traversal depth. Zero permits root values only; values above 10000 are rejected.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
// Limits are fail-closed: exceeding one discards the whole result rather
// than returning a partially masked document.
m, err := masker.New(masker.DefaultPolicy(), masker.WithMaxDepth(1))
if err != nil {
panic(err)
}
masked, err := m.MaskJSON([]byte(`{"outer":{"inner":"value"}}`))
fmt.Println(string(masked), errors.Is(err, masker.ErrDepthLimit))
}
Output: "[REDACTED]" true
func WithMaxInputBytes ¶
WithMaxInputBytes limits input accepted by MaskJSON and MaskJSONReader, and the length of every string value that is inspected for embedded documents or secrets in text: a longer string becomes the marker and the operation reports ErrInputLimit. It also bounds the total content of the byte slices and json.RawMessage values one reflection operation encodes or decodes; a slice past that budget becomes the marker the same way.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy(), masker.WithMaxInputBytes(16))
if err != nil {
panic(err)
}
masked, err := m.MaskJSON([]byte(`{"password":"synthetic-secret"}`))
fmt.Println(string(masked), errors.Is(err, masker.ErrInputLimit))
}
Output: "[REDACTED]" true
func WithMaxNodes ¶
WithMaxNodes limits the number of visited values in one operation.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy(), masker.WithMaxNodes(3))
if err != nil {
panic(err)
}
masked, err := m.MaskJSON([]byte(`{"a":1,"b":2,"c":3,"d":4}`))
fmt.Println(string(masked), errors.Is(err, masker.ErrNodeLimit))
}
Output: "[REDACTED]" true
func WithPreserveSafeTypes ¶
func WithPreserveSafeTypes() Option
WithPreserveSafeTypes keeps safe primitive values in their concrete types.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy(), masker.WithPreserveSafeTypes())
if err != nil {
panic(err)
}
masked, err := m.MaskAny(map[string]any{"enabled": true, "count": 3})
if err != nil {
panic(err)
}
value := masked.(map[string]any)
fmt.Printf("%T %T %v %v\n", value["enabled"], value["count"], value["enabled"], value["count"])
}
Output: bool int true 3
func WithRedaction ¶
WithRedaction sets the marker used for sensitive and fail-closed values.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy(), masker.WithRedaction("***"))
if err != nil {
panic(err)
}
masked, err := m.MaskJSON([]byte(`{"password":"synthetic-secret"}`))
if err != nil {
panic(err)
}
fmt.Println(string(masked))
}
Output: {"password":"***"}
func WithStructTag ¶
WithStructTag changes the masking tag name. Empty selects the default name.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
type payload struct {
Token string `secret:"token"`
Team string
}
m, err := masker.New(masker.DefaultPolicy(), masker.WithStructTag("secret"))
if err != nil {
panic(err)
}
masked, err := m.MaskAny(payload{Token: "synthetic-token", Team: "core"})
if err != nil {
panic(err)
}
values := masked.(map[string]any)
fmt.Println(values["Token"], values["Team"])
}
Output: [REDACTED] core
func WithTagRule ¶ added in v0.2.0
WithTagRule registers rule under name for the struct tag grammar, so a field tagged `mask:"name"` is masked by it. Built-in names and "omit" cannot be redefined: a tag that already means "hide this" must not quietly weaken.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
type payload struct {
ID string `mask:"last2"`
}
last2, err := masker.NewRule("last2", func(input masker.RuleInput) (string, error) {
runes := []rune(input.Value)
if len(runes) < 2 {
return "**", nil
}
return string(runes[len(runes)-2:]), nil
})
if err != nil {
panic(err)
}
m, err := masker.New(masker.DefaultPolicy(), masker.WithTagRule("last2", last2))
if err != nil {
panic(err)
}
masked, err := m.MaskAny(payload{ID: "customer-8891"})
if err != nil {
panic(err)
}
values := masked.(map[string]any)
fmt.Println(values["ID"])
}
Output: 91
func WithoutEmbeddedDocuments ¶ added in v0.5.0
func WithoutEmbeddedDocuments() Option
WithoutEmbeddedDocuments stops masking URLs, JSON documents and forms carried inside string values. By default a string whose field the policy leaves alone is masked as the document it holds, so a body or a callback URL logged as a string does not leak the secrets inside it.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
value := "https://u:dummy@h/cb?token=dummy-token"
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
masked, err := m.MaskValue("note", value)
if err != nil {
panic(err)
}
// Without embedded documents the URL is no longer parsed, but the text
// detectors still find its userinfo and the token=... pair.
text, err := masker.New(masker.DefaultPolicy(), masker.WithoutEmbeddedDocuments())
if err != nil {
panic(err)
}
asText, err := text.MaskValue("note", value)
if err != nil {
panic(err)
}
fmt.Println(masked)
fmt.Println(asText)
}
Output: https://%5BREDACTED%5D@h/cb?token=%5BREDACTED%5D https://[REDACTED]@h/cb?token=[REDACTED]
func WithoutTextDetectors ¶ added in v0.5.0
func WithoutTextDetectors() Option
WithoutTextDetectors stops looking for secrets inside free text. By default a string whose field the policy leaves alone, and which is not a whole URL, JSON document or form, is searched for credentials recognizable by shape — a token after "Bearer" or "Basic", a PEM private key, a JWT, provider tokens such as "ghp_..." and the userinfo of a URL — and for key=value and key: value pairs, whose key the policy judges as a field of SourceText. Only the secret is replaced; the rest of the text is kept.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy(), masker.WithoutTextDetectors())
if err != nil {
panic(err)
}
masked, err := m.MaskValue("message", "login failed: password=dummy-pass")
if err != nil {
panic(err)
}
fmt.Println(masked)
}
Output: login failed: password=dummy-pass
func WithoutValueInspection ¶ added in v0.5.0
func WithoutValueInspection() Option
WithoutValueInspection leaves every string the policy does not decide exactly as it is: it combines WithoutEmbeddedDocuments and WithoutTextDetectors.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
m, err := masker.New(masker.DefaultPolicy(), masker.WithoutValueInspection())
if err != nil {
panic(err)
}
leaked, err := m.MaskValue("note", "retry with password=dummy-pass")
if err != nil {
panic(err)
}
fmt.Println(leaked)
}
Output: retry with password=dummy-pass
type Policy ¶
Policy decides how a field should be handled. Decide must be safe for concurrent use and deterministic: the same Field must always get the same Decision. The masker caches decisions for struct fields, and adapters decide the same field more than once, such as a slog group for each of its members.
func Chain ¶
Chain evaluates policies in order until one gives an opinion.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
// The first policy with an opinion wins; the defaults act as a fallback.
tenant := masker.PolicyFunc(func(field masker.Field) (masker.Decision, error) {
if field.Key == "tenant" {
return masker.Decision{Rule: masker.FullRule()}, nil
}
return masker.Decision{}, nil
})
m, err := masker.New(masker.Chain(tenant, masker.DefaultPolicy()))
if err != nil {
panic(err)
}
masked, err := m.MaskJSON([]byte(`{"tenant":"acme","password":"synthetic-secret","region":"eu"}`))
if err != nil {
panic(err)
}
fmt.Println(string(masked))
}
Output: {"password":"[REDACTED]","region":"eu","tenant":"[REDACTED]"}
func DefaultPolicy ¶
func DefaultPolicy() Policy
DefaultPolicy returns the standard sensitive-key policy.
type PolicyFunc ¶
PolicyFunc adapts a function to Policy.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
policy := masker.PolicyFunc(func(field masker.Field) (masker.Decision, error) {
if field.Key == "session_id" {
return masker.Decision{Rule: masker.FullRule()}, nil
}
return masker.Decision{}, nil
})
m, err := masker.New(policy)
if err != nil {
panic(err)
}
masked, err := m.MaskValue("session_id", "synthetic-session")
if err != nil {
panic(err)
}
fmt.Println(masked)
}
Output: [REDACTED]
type Rule ¶
Rule transforms one sensitive scalar into a safe string.
func NewRule ¶
NewRule validates and creates a named custom rule.
Example ¶
package main
import (
"fmt"
"github.com/icntswm/go-masker"
)
func main() {
rule, err := masker.NewRule("prefix", func(input masker.RuleInput) (string, error) {
return "masked-" + input.Redaction, nil
})
if err != nil {
panic(err)
}
m, err := masker.New(masker.DefaultPolicy())
if err != nil {
panic(err)
}
masked, err := m.MaskString("synthetic-secret", rule)
if err != nil {
panic(err)
}
fmt.Println(masked)
}
Output: masked-[REDACTED]
type RuleFunc ¶
RuleFunc adapts a function to Rule. Its name is "custom".
type Source ¶
type Source uint8
Source identifies the input representation in a policy decision.
const ( // SourceUnknown identifies an unspecified input source. SourceUnknown Source = iota // SourceAny identifies reflection-based arbitrary input. SourceAny // SourceMap identifies map-like input. SourceMap // SourceStruct identifies struct input. SourceStruct // SourceJSON identifies JSON input. SourceJSON // SourceHeader identifies an HTTP header. SourceHeader // SourceURLQuery identifies a URL query parameter. SourceURLQuery // SourceURLUserInfo identifies URL userinfo. SourceURLUserInfo // SourceURLFragment identifies a URL fragment. SourceURLFragment // SourceText identifies a key=value pair found inside free text, such as // "password=..." in a log message. SourceText )
type ValueKind ¶
type ValueKind uint8
ValueKind is the normalized kind visible to policies and rules.
const ( // KindInvalid identifies an unknown value kind. KindInvalid ValueKind = iota // KindNil identifies a nil value. KindNil // KindString identifies a string value. KindString // KindBool identifies a boolean value. KindBool // KindNumber identifies a numeric value. KindNumber // KindObject identifies an object or map value. KindObject // KindArray identifies an array or slice value. KindArray )
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package httpmask adapts masker to HTTP headers and URLs.
|
Package httpmask adapts masker to HTTP headers and URLs. |
|
internal
|
|
|
adapter
Package adapter connects logger adapters to unexported masking code.
|
Package adapter connects logger adapters to unexported masking code. |
|
detect
Package detect finds secrets inside free text: tokens recognized by their shape, and key=value pairs whose key a policy may judge.
|
Package detect finds secrets inside free text: tokens recognized by their shape, and key=value pairs whose key a policy may judge. |
|
jsonline
Package jsonline holds the JSON-line masking writer shared by zerologmask and zapmask.
|
Package jsonline holds the JSON-line masking writer shared by zerologmask and zapmask. |
|
outputdigest
command
Command outputdigest hashes the output of a fixed corpus so that masking output can be compared across Go toolchains.
|
Command outputdigest hashes the output of a fixed corpus so that masking output can be compared across Go toolchains. |
|
urlquery
Package urlquery rewrites raw URL query and form text.
|
Package urlquery rewrites raw URL query and form text. |
|
Package slogmask adapts masker to log/slog.
|
Package slogmask adapts masker to log/slog. |
|
Package zapmask masks the JSON lines written by zap's JSON encoder.
|
Package zapmask masks the JSON lines written by zap's JSON encoder. |
|
Package zerologmask masks the JSON lines written by zerolog, or by any other logger that writes one JSON object per line.
|
Package zerologmask masks the JSON lines written by zerolog, or by any other logger that writes one JSON object per line. |