masker

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 25 Imported by: 0

README

go-masker

Go Reference CI OpenSSF Best Practices SLSA 3

go-masker is a Go library for fail-closed masking of sensitive data before it reaches logs, diagnostics, traces, or other observability systems. It plugs into log/slog, zap and zerolog without importing either third-party logger.

One MaskJSON call on a payload, with the default policy and no configuration:

Field In Out
password hunter2 [REDACTED]
card 4111 1111 1111 1111 **** **** **** 1111
email alice@example.com a***@example.com
user_id 884213 **4213
amount 1499 1499

Each field got the treatment its name implies, amount was left alone, and nothing had to be listed by hand.

It provides one policy and rule model for:

  • strings and scalar values;
  • arbitrary nested Go values;
  • JSON documents and readers;
  • URLs, JSON bodies and forms carried inside string values;
  • struct tags;
  • HTTP headers and URLs through httpmask;
  • log/slog attributes through slogmask;
  • JSON log lines from zerolog or any JSON-line logger through zerologmask, and from zap's JSON encoder through zapmask.

The module has no third-party dependencies, not even in its tests, and does not depend on an HTTP framework or logging library.

Why a library instead of a field filter

A list of field names in a logger config covers the fields you remembered, at the depth you remembered them. This library is built around the cases that list misses:

  • Errors never fall back to the input. Every operation returns a safe marker on failure. A filter that errors typically logs the raw value, which is the moment you needed it least.
  • Depth is not special. The same policy applies to a nested JSON object, a struct field, a map inside a slice, a URL query parameter and an HTTP header.
  • Hostile input stays bounded. Traversal depth, visited nodes and input size are capped, and the depth limit itself cannot exceed 10,000, so deeply nested input cannot exhaust the goroutine stack.
  • Output does not drift. Masking a fixed corpus under Go 1.23 through 1.27 produces byte-identical results; the digests are recorded in PERFORMANCE.md.

It is not a substitute for not collecting the secret in the first place, and it cannot prove that a custom rule you wrote is safe.

Each of those claims is checked by the suite; see How it is tested.

Table of contents

Supported Go versions

The library requires Go 1.23 or newer and uses only the standard library. The CI workflow runs the tests, the race suite, the correctness matrix and a fuzz smoke pass on every supported minor release — 1.23.x, 1.24.x, 1.25.x, 1.26.x, 1.27.x — plus stable, so a new Go release is covered on the day it ships. A separate job runs govulncheck on every push.

Installation

go get github.com/icntswm/go-masker

Import the core package as masker:

import "github.com/icntswm/go-masker"

The adapters are separate packages in the same module:

import (
	"github.com/icntswm/go-masker/httpmask"    // HTTP headers and URLs
	"github.com/icntswm/go-masker/slogmask"    // log/slog attributes
	"github.com/icntswm/go-masker/zerologmask" // zerolog and other JSON-line loggers
	"github.com/icntswm/go-masker/zapmask"     // zap's JSON encoder
)

Quick start

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", "correct-horse-battery-staple")
	if err != nil {
		panic(err)
	}
	fmt.Println(value)
	// Output: [REDACTED]
}

Masker instances are immutable and safe for concurrent use after successful construction. Operations copy input containers and never mutate the source.

What is masked

DefaultPolicy matches complete field names case-insensitively. The comparison also ignores the separator characters _, -, and ., so accessToken, access-token, and ACCESS.TOKEN all match the access_token binding. Its default bindings are:

Keys Rule
password, passwd, passphrase, pwd full
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 token
email, e-mail email
phone, phone_number, mobile phone
id, user_id, customer_id ID
card, card_number, pan card
authorization, cookie, set-cookie, x-api-key, x-api-token, x-access-token, x-auth-token, proxy-authorization, x-csrf-token, cvv, cvc full

Built-in rules are available directly through PasswordRule, TokenRule, FullRule, EmailRule, PhoneRule, IDRule, and CardRule.

Partial rules preserve only a deliberately limited safe shape. Short or ambiguous phone/card values and values containing unexpected text are fully redacted.

Documents inside strings

A secret often reaches a log inside a value whose own name is harmless: a callback URL, a request body logged as a string, a form. When the policy leaves a string field alone, the masker looks at the value itself, and if the whole value is one of these documents, masks it by the keys inside it:

m.MaskValue("note", "https://u:pass@h/cb?token=abc#state")
// https://%5BREDACTED%5D@h/cb?token=%5BREDACTED%5D#%5BREDACTED%5D
m.MaskValue("note", `{"password":"secret","user":"alice"}`)
// {"password":"[REDACTED]","user":"alice"}
m.MaskValue("note", "user=alice&password=secret")
// password=%5BREDACTED%5D&user=alice

Recognition is strict, so prose is left alone:

  • a URL is a single absolute scheme://host token without spaces; its userinfo and fragment are replaced as httpmask replaces them, and each query parameter is masked by its key;
  • a JSON document is a string that is, apart from surrounding whitespace, one valid object or array; it is masked exactly as MaskJSON masks it;
  • a form is key=value pairs joined by &, with no spaces, no ; and valid percent-encoding.

A value is rewritten only when something in it was masked; otherwise it comes back byte for byte. A document may hold another one, such as a JSON body with a redirect_uri, and is inspected to the same depth, node and input limits as the value around it. A string that looks like a URL but whose query does not parse becomes the marker. A field whose own key the policy masks or omits is decided by that key and is never inspected. WithoutEmbeddedDocuments() turns the inspection off.

Secrets inside text

A string that is not a whole document, such as a log message or an error text, is searched for secrets written into it. Only the secret is replaced; the rest of the text is kept byte for byte:

m.MaskValue("message", "login failed: password=hunter2")
// login failed: password=[REDACTED]
m.MaskValue("message", "upstream said: Bearer eyJhbGciOi... rejected")
// upstream said: Bearer [REDACTED] rejected
m.MaskValue("message", "dial postgres://app:pass@db:5432/app failed")
// dial postgres://[REDACTED]@db:5432/app failed

Two kinds of detectors run by default:

  • key=value and key: value pairs, with an optionally quoted key or value; a key may start with _, - or ., so --password=... is a pair too. The policy judges the key as a field with Source SourceText, so the same rules that mask a password field mask password=... in a sentence. An omitted value becomes the marker, since text has no member to drop. After Authorization: the value takes the Bearer/Basic/Token credential with it.
  • Secrets recognizable by shape, masked whatever key stands before them: the credential after Bearer or Basic, the body of a PEM private key (its BEGIN and END lines stay), a JWT, provider tokens with a documented prefix (ghp_, github_pat_, glpat-, xox?-, sk_live_, AIza, npm_, …) and the userinfo of a URL written inside a sentence.

Two detectors are opt-in, because ordinary text matches them by chance: WithCardNumberDetection() masks 13–19 digit numbers that pass the Luhn check with CardRule, keeping the last four digits, and WithAWSKeyIDDetection() masks AWS access key ids. WithoutTextDetectors() turns the text detectors off, and WithoutValueInspection() turns off both them and the documents above.

Detection is a heuristic on top of the policy, not a replacement for it: a secret with no key and no known shape, such as a bare random password, is not recognized. Text without a candidate costs no allocation.

Core concepts

Policies decide whether a field should be masked; rules transform the selected scalar value. A policy can be assembled from bindings or written as a function:

policy := masker.PolicyFunc(func(field masker.Field) (masker.Decision, error) {
	if field.Key == "tax_id" {
		return masker.Decision{Rule: masker.FullRule()}, nil
	}
	return masker.Decision{}, nil
})

m, err := masker.New(masker.Chain(masker.DefaultPolicy(), policy))

Policies in a Chain are evaluated in order. The first policy that gives an opinion wins. New rejects empty chains and chains containing nil policies.

A zero Decision means "no opinion". Decision{Omit: true} removes an object or map member entirely; array elements keep their position and become null, so the shape of a list is never altered.

Values are seen the way encoding/json would render them: a time.Time, net.IP, or any other encoding.TextMarshaler is masked as its text, and a []byte as its base64 form. A json.RawMessage, which encoding/json embeds as is, is decoded and masked by its keys like any other map; one that is not valid JSON fails closed. A MarshalText error or panic fails closed, and the method runs on a copy of the value, so it cannot change your data; a marshaler that holds pointers, maps, or locks is walked field by field instead.

NewKeyPolicy rejects empty keys (including keys that are made of separator characters only), nil rules, and fold-equivalent keys bound to different rules, so an ambiguous configuration fails at construction instead of resolving silently at run time.

Named custom rules make the rule name visible in diagnostics:

rule, err := masker.NewRule("tenant-id", func(input masker.RuleInput) (string, error) {
	return input.Redaction, nil
})

Partial custom rules must slice on runes, not bytes: a byte offset can split a multi-byte character, and the library rejects rule output that is not valid UTF-8.

Option is intentionally a closed type. Use the exported With* constructors rather than implementing options against the internal configuration type.

Reflection results are normalized for logging: safe scalars become strings unless WithPreserveSafeTypes keeps booleans, integers, unsigned integers, floats, and strings in their concrete types. Sensitive values are always strings, and a non-sensitive json.Number is always retained as json.Number to avoid precision loss.

Runnable examples for every exported constructor, option, rule, and adapter are in the package documentation. They are executed and output-checked by go test, so they cannot drift from the implementation.

JSON

MaskJSON accepts exactly one valid JSON document and returns valid JSON. It uses json.Number semantics for safe numbers, preserves last-wins behavior for duplicate object keys, and uses a streaming walker with depth, node, and input limits.

input := []byte(`{"user":"alice","password":"secret","balance":9007199254740993}`)
output, err := m.MaskJSON(input)
if err != nil {
	// output is a safe JSON root fallback, never the original input.
}
fmt.Println(string(output))
// {"balance":9007199254740993,"password":"[REDACTED]","user":"alice"}

Object keys are sorted in the output, and balance keeps its exact value. That number is 2^53+1, which float64 rounds down to 2^53; safe numbers are carried through as json.Number instead, so the digits survive.

MaskJSONReader reads the complete reader into memory before processing. Use WithMaxInputBytes to bound accepted input:

m, err := masker.New(masker.DefaultPolicy(), masker.WithMaxInputBytes(2<<20))
output, err := m.MaskJSONReader(reader)

There is intentionally no writer API for a single document: a writer cannot retract an unsafe prefix if a later parse error is found. zerologmask and zapmask are writers only for complete lines, each masked as its own document.

Struct tags

Struct tags select a built-in rule explicitly:

type Event struct {
	User     string `json:"user"`
	Password string `mask:"password"`
	Internal string `json:"-"`
}

masked, err := m.MaskAny(Event{
	User: "alice", Password: "secret", Internal: "not logged",
})

Supported tag values are full, email, phone, id, card, password, token, and omit. The precedence is:

  1. omit;
  2. full or another explicit tag rule;
  3. the configured policy;
  4. ordinary safe traversal.

The tag grammar is strict: an unsupported value or a comma-separated option is a configuration error rather than a silent fallback. The result is normalized to map[string]any/[]any containers. Struct field promotion follows the relevant encoding/json rules, including same-depth tagged-field selection and ignored unexported embedded non-struct fields.

When the masked value is going to be encoded anyway, MaskJSONValue returns the JSON directly. It gives the same bytes as json.Marshal of the MaskAny result, sorted keys included, at about half the cost, and on any error returns the marker as JSON:

line, err := m.MaskJSONValue(Event{User: "alice", Password: "secret"})
// {"Password":"[REDACTED]","user":"alice"}

A custom rule can join the grammar through WithTagRule. The name must not be empty, contain a comma, a space, or a quote, and built-in names and omit cannot be redefined:

lastTwo, 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
})
m, err := masker.New(masker.DefaultPolicy(), masker.WithTagRule("last2", lastTwo))

HTTP headers and URLs

The httpmask adapter copies headers and URLs before applying the core policy:

core, err := masker.New(masker.DefaultPolicy())
adapter, err := httpmask.New(core)

headers, err := adapter.Headers(http.Header{
	"Authorization": {"Bearer secret"},
	"X-Trace":       {"trace-id"},
})
maskedURL, err := adapter.URLString(
	"https://user:password@example.com/?token=secret&keep=value#fragment",
)

Cookie and Set-Cookie values and URL userinfo are always fully redacted. Sensitive query parameters use the core policy. Query keys are sorted and URL escaping may be normalized. URL fragments are redacted by default, because an OAuth implicit-flow token arrives there; WithPreserveFragment keeps them.

log/slog

The slogmask adapter plugs the core policy into any slog handler through ReplaceAttr:

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
	ReplaceAttr: slogmask.ReplaceAttr(m),
}))
logger.Info("login", "user", "alice", "password", "hunter2")
// {"time":"…","level":"INFO","msg":"login","user":"alice","password":"[REDACTED]"}

Attributes in groups are matched by their own key, with the group names in the policy path. The policy also decides each group as an object, so a group named like a secret, such as credentials, masks every member. Attributes added through logger.With and WithGroup, and the values returned by LogValue, are masked the same way. A safe scalar keeps its type, a masked one is logged as a string, an error is masked as its text, and an Omit decision drops a top-level attribute. Inside a group it logs the marker instead: log/slog writes a broken line when ReplaceAttr drops every member of a group and another attribute follows it. The message is masked as a string attribute named msg, so it is searched as described in Secrets inside text; that is a safety net, so still pass secrets as attributes rather than in the message. The built-in time, level, and source attributes are not masked.

Masking happens only in a handler that calls ReplaceAttr. The standard TextHandler and JSONHandler do; the default handler behind slog.Info before slog.SetDefault, and third-party handlers that ignore HandlerOptions, log attributes unmasked.

zerolog

The zerologmask adapter wraps the logger's writer, because zerolog serializes its fields as they are added and its hooks cannot change what is written:

logger := zerolog.New(zerologmask.NewWriter(os.Stdout, m))
logger.Info().Str("user", "alice").Str("password", "hunter2").Msg("login")
// {"level":"info","message":"login","password":"[REDACTED]","user":"alice"}

Masking the output line covers every field, including those added through Interface or RawJSON. The line is re-encoded, so its keys come out sorted, as in every JSON document the masker writes. Any logger that writes one JSON object per line works the same way. For human-readable output put the masking writer in front of zerolog.ConsoleWriter, so the console formats an already masked line. A writer that routes or filters by level, such as a zerolog.MultiLevelWriter or zerolog.FilteredLevelWriter, loses that when wrapped and receives every level: wrap each destination instead. zerolog built with the binary_log tag writes CBOR, not JSON, and every line is replaced. A line the masker cannot parse is replaced by {"message":"[REDACTED]"}. The message is searched like any other string, as described in Secrets inside text; that is a safety net, so still pass secrets as fields rather than in the message.

zap

zapmask masks the lines zap's JSON encoder writes, after encoding, so the library does not import zap. Its WriteSyncer goes to zapcore.NewCore directly:

sink := zapmask.NewWriteSyncer(os.Stdout, m)
logger := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), sink, zap.InfoLevel))
logger.Info("login", zap.String("user", "alice"), zap.String("password", "hunter2"))
// {"level":"info","msg":"login","password":"[REDACTED]","ts":...,"user":"alice"}

Fields added through With, zap.Dict and zap.Any values are masked like any nested object, and a zap.Namespace is decided as an object, so a credentials namespace becomes the marker as a whole. Sampling, level filtering, zapcore.Tee and zapcore.BufferedWriteSyncer keep working, because the writer only sees what zap has decided to write. zap writes an error's %+v text under keyVerbose, a multi-error's parts under keyCauses and a panic while encoding a field under keyError; each of these is also decided as its base key, so zap.NamedError("token", err) does not log the token again under tokenVerbose. The msg text is searched by the text detectors like any other string. The console encoder does not write JSON, and every one of its lines is replaced by the marker line.

Errors and fail-closed behavior

Every public operation returns a safe fallback on an error; it never returns the original unsafe value. Use errors.Is for a category and errors.As for safe operation context:

var detail *masker.MaskError
output, err := m.MaskJSON([]byte(`{"password":`))
if err != nil && errors.Is(err, masker.ErrInvalidJSON) {
	fmt.Println(string(output)) // [REDACTED] as a JSON string
}
if errors.As(err, &detail) {
	fmt.Println(detail.Code, detail.Path)
}

Exported Err* values identify categories such as ErrInvalidJSON, ErrInvalidUTF8, ErrDepthLimit, ErrNodeLimit, ErrCycle, and ErrPanic. ErrorCode constants use the corresponding Code* names.

Never substitute the original input for an error result in application code or in an adapter. The public operations already return a safe fallback, and replacing it is the one change that reintroduces the leak the library prevents.

Limits and security

Default limits are depth 32, 100,000 visited nodes, and 8 MiB of JSON input. Traversal recurses once per nesting level, so WithMaxDepth rejects values above 10,000: a Go stack overflow is fatal and could not fail closed. Input skipped after a limit trips is scanned iteratively. Memory still grows with the input because the public byte-slice and reader APIs retain the complete document during masking.

Review custom policies and rules as security-sensitive code. A custom rule's output is checked for valid UTF-8, but the library cannot prove that arbitrary custom logic removed every secret.

Read SECURITY.md for vulnerability reporting and THREAT_MODEL.md for the security assumptions and failure model.

How it is tested

A masking library is only worth what its test suite proves, so the evidence is listed rather than asserted. There are 9,969 lines of tests against 7,555 lines of shipped code.

Check Evidence
Masking scenarios 260 generated cases across JSON, reflection, URLs and headers; each checks the masked result, not just that nothing panicked
Security goldens 45 recorded decisions in 8 files, covering rules, key casing, limits, nesting, errors and URLs
Fuzzing 6 targets: JSON, strings, case-folded policy lookup, JSON/reflection parity, URLs, text detectors
Logger adapters slogmask through the real log/slog handlers; zerologmask and zapmask against lines captured from the real zerolog and zap, so the module keeps no dependency
Examples 38, executed and output-checked, so documentation cannot drift from behavior
Coverage 84.1% core, 88.5% httpmask, 90.2% slogmask, 95.9% for the text detectors, 90.8% for the line-masking engine behind zerologmask and zapmask
Go versions tests, race suite, matrix and fuzz smoke on 1.23.x through 1.27.x plus stable
Supply chain govulncheck on every push, reporting standard-library advisories the code actually reaches

The version matrix earns its cost: it caught a change in encoding/json string escaping in Go 1.27 on the day stable moved. A separate check then confirmed what mattered — masking a fixed corpus under every supported release still produces byte-identical output.

How this was built

This library was written with AI assistance, using Anthropic's Claude Code and OpenAI's Codex. Everything above is how that is kept honest: a change has to survive the same suite on every supported Go release before it ships. Provenance is not a substitute for review - the maintainer answers for what is here regardless of how it was produced, and you should read it the way you would read any dependency that handles secrets.

Performance

The streaming JSON walker, bounded key cache, direct encoder, and per-masker reflection metadata cache are designed for predictable behavior on nested and wide payloads. On an M3 Pro, full redaction of a string costs 16 ns and allocates nothing, and a 10,000-record JSON document is masked at roughly 125 MB/s. Throughput stays flat as documents grow wider or longer, which matters more than the absolute numbers; the method and the full tables are in PERFORMANCE.md.

Run local checks and benchmarks with:

make test
make race
make bench
make bench-matrix

Releases

Releases carry a source archive and a SLSA build attestation, so the archive can be traced to the workflow and the tag that produced it:

slsa-verifier verify-artifact go-masker-vX.Y.Z.tar.gz \
  --provenance-path go-masker-vX.Y.Z.tar.gz.intoto.jsonl \
  --source-uri github.com/icntswm/go-masker --source-tag vX.Y.Z

Taking the module with go get needs none of this: the Go checksum database already verifies what you download. The attestation is for anyone who takes the archive instead.

Documentation map

The package godoc contains runnable examples for construction, JSON, reflection, custom rules, struct tags, and the HTTP and logger adapters.

Compatibility and stability

The project is pre-1.0. Public API changes will be called out in the changelog before the first stable release. The supported Go window is Go 1.23 and newer compatible releases.

Contributing

Bug reports and pull requests are welcome. Please read CONTRIBUTING.md before making changes. Security issues must be reported privately according to SECURITY.md.

License

This project is licensed under the MIT License.

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

Examples

Constants

View Source
const (
	// DefaultRedactionMarker is used for sensitive and fail-closed values.
	DefaultRedactionMarker = "[REDACTED]"
	// DefaultStructTag is the default struct tag name.
	DefaultStructTag = "mask"
)

Variables

View Source
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

type Binding struct {
	Keys []string
	Rule Rule
}

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 Decision

type Decision struct {
	Rule Rule
	Omit bool
}

Decision is a policy decision. A zero Decision means no opinion.

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 Field

type Field struct {
	Key    string
	Path   string
	Source Source
	Kind   ValueKind
}

Field is the diagnostic context passed to a Policy.

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

func NewKeyPolicy(bindings ...Binding) (*KeyPolicy, error)

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"}

func (*KeyPolicy) Decide

func (p *KeyPolicy) Decide(field Field) (Decision, error)

Decide implements Policy.

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.

func (*MaskError) Error

func (e *MaskError) Error() string

Error returns a safe diagnostic that contains no input data.

func (*MaskError) Unwrap

func (e *MaskError) Unwrap() error

Unwrap makes errors.Is work for the corresponding safe category.

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

func New(policy Policy, opts ...Option) (*Masker, error)

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

func (m *Masker) MaskAny(value any) (result any, err error)

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

func (m *Masker) MaskField(field Field, value any) (result any, err error)

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

func (m *Masker) MaskJSON(src []byte) (result []byte, err error)

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

func (m *Masker) MaskJSONReader(src io.Reader) (result []byte, err error)

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

func (m *Masker) MaskJSONValue(value any) (result []byte, err error)

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

func (m *Masker) MaskString(value string, rule Rule) (result string, err error)

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

func (m *Masker) MaskValue(key string, value any) (any, error)

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

func WithMaxDepth(depth int) Option

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

func WithMaxInputBytes(bytes int64) Option

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

func WithMaxNodes(nodes int) Option

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

func WithRedaction(marker string) Option

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

func WithStructTag(name string) Option

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

func WithTagRule(name string, rule Rule) Option

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

type Policy interface {
	Decide(Field) (Decision, error)
}

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

func Chain(policies ...Policy) Policy

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

type PolicyFunc func(Field) (Decision, error)

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]

func (PolicyFunc) Decide

func (f PolicyFunc) Decide(field Field) (Decision, error)

Decide implements Policy.

type Rule

type Rule interface {
	Name() string
	Apply(RuleInput) (string, error)
}

Rule transforms one sensitive scalar into a safe string.

func CardRule

func CardRule() Rule

CardRule masks all but the last four ASCII digits.

func EmailRule

func EmailRule() Rule

EmailRule preserves a limited, safe email shape.

func FullRule

func FullRule() Rule

FullRule fully redacts a value.

func IDRule

func IDRule() Rule

IDRule masks all but the last four units.

func NewRule

func NewRule(name string, fn RuleFunc) (Rule, error)

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]

func PasswordRule

func PasswordRule() Rule

PasswordRule fully redacts passwords.

func PhoneRule

func PhoneRule() Rule

PhoneRule masks all but the last four digits.

func TokenRule

func TokenRule() Rule

TokenRule fully redacts tokens and secrets.

type RuleFunc

type RuleFunc func(RuleInput) (string, error)

RuleFunc adapts a function to Rule. Its name is "custom".

func (RuleFunc) Apply

func (f RuleFunc) Apply(input RuleInput) (string, error)

Apply implements Rule.

func (RuleFunc) Name

func (f RuleFunc) Name() string

Name implements Rule.

type RuleInput

type RuleInput struct {
	Value     string
	Kind      ValueKind
	Redaction string
}

RuleInput is the safe context supplied to a Rule.

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
)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL