incognigo

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

Incognigo

Incognigo is a dependency-free Go library for detecting and transforming personally identifiable information (PII) before it reaches logs, external services, or other data destinations.

Version v0.1.0 is the first production-candidate release. Its implementation is designed to fail closed, bound input-driven work, avoid source data in library diagnostics, and support concurrent reuse. The public API remains a v0 API: compatibility-breaking improvements are still possible before v1.

Incognigo provides:

  • configurable policy and priority per PII type;
  • byte, string, and bounded-retention stream processing;
  • a protecting log/slog handler;
  • built-in ASCII email, IPv4/IPv6, payment-card, and NANP phone detectors;
  • removal, fixed replacement, byte masking, HMAC tokenization, and reversible AES-256-GCM tokenization; and
  • extension interfaces for application-owned detectors and transformers.

The built-in detectors are deliberately syntactic and limited. Incognigo does not establish that a match belongs to a person, and it does not include names, postal addresses, international phone formats outside NANP, government identifiers, or contextual free-text entity recognition.

Requirements and installation

Incognigo supports Go 1.25 and Go 1.26. Use the latest patch release available for either line.

go get github.com/Chadi00/incognigo@v0.1.0

The module has no third-party runtime or test dependencies.

Quick start

This complete example protects every built-in PII type with a fixed marker. Always treat construction or processing errors as protection failures; never fall back to the original input.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Chadi00/incognigo"
	"github.com/Chadi00/incognigo/detector"
	"github.com/Chadi00/incognigo/transform"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	engine, err := incognigo.NewEngine(incognigo.Config{
		Detectors: []incognigo.DetectorRegistration{
			detector.Email(),
			detector.IPAddress(),
			detector.PaymentCard(),
			detector.NANPPhone(),
		},
		Policies: []incognigo.Policy{
			{
				Type:        incognigo.PIITypeEmailAddress,
				Priority:    100,
				Transformer: transform.Replace([]byte("[email]")),
				Determinism: incognigo.Deterministic,
			},
			{
				Type:        incognigo.PIITypeIPAddress,
				Priority:    90,
				Transformer: transform.Replace([]byte("[ip]")),
				Determinism: incognigo.Deterministic,
			},
			{
				Type:        incognigo.PIITypePaymentCardNumber,
				Priority:    80,
				Transformer: transform.Replace([]byte("[payment-card]")),
				Determinism: incognigo.Deterministic,
			},
			{
				Type:        incognigo.PIITypePhoneNumber,
				Priority:    70,
				Transformer: transform.Replace([]byte("[nanp-phone]")),
				Determinism: incognigo.Deterministic,
			},
		},
	})
	if err != nil {
		return fmt.Errorf("configure PII protection: %w", err)
	}

	protected, err := engine.ProtectString(
		context.Background(),
		"email owner@example.test from 192.0.2.1 "+
			"with card 4111 1111 1111 1111 or call 212-555-0100",
	)
	if err != nil {
		return fmt.Errorf("protect data: %w", err)
	}
	fmt.Println(protected)
	return nil
}

Output:

email [email] from [ip] with card [payment-card] or call [nanp-phone]

The repository's Example... functions compile and execute under go test. They cover every public workflow, including custom extensions, all built-in detectors and transformations, streaming, structured logging, detokenization, and inspectable failures.

Processing APIs

Engine.ProtectBytes is the canonical batch API. It never mutates or retains its input, and every successful result is newly owned by the caller. Engine.ProtectString is a byte-preserving adapter over the same path. On failure they return no partial or unprotected output.

protectedBytes, err := engine.ProtectBytes(ctx, inputBytes)
protectedString, err := engine.ProtectString(ctx, inputString)
Streaming

Engine.ProtectStream processes an arbitrary-length io.Reader while bounding undecided source retention:

var destination bytes.Buffer
written, err := engine.ProtectStream(
	ctx,
	&destination,
	strings.NewReader("contact owner@example.test from 192.0.2.1"),
)

written counts only protected bytes accepted by the destination. A reader, detector, transformer, or writer failure after an earlier write can leave a protected prefix. Incognigo never appends undecided or unprotected source bytes after that prefix. Use a transactional destination or buffer before commit when delivery must be all-or-nothing.

Structured logging

Wrap a downstream slog.Handler to protect record messages and attribute values before formatting or transport:

downstream := slog.NewJSONHandler(os.Stdout, nil)
handler, err := sloghandler.New(engine, downstream)
if err != nil {
	return err
}

logger := slog.New(handler)
logger.Info(
	"contact owner@example.test",
	"peer", "192.0.2.1",
	"backup", []byte("backup@example.test"),
)

String and exact []byte values keep their logical kinds. slog.LogValuer values are resolved, groups are traversed without recursive stack growth, and other non-nil slog.Any values are eagerly converted with fmt.Sprint, protected, and passed downstream as strings. This conversion prevents a downstream-only serializer from bypassing protection, but intentionally loses the original dynamic type.

Attribute and group keys, nil Any values, native non-textual values, and record metadata are preserved. Downstream handler errors are returned unchanged. A canceled record context does not cancel protection, as required by the slog.Handler contract.

Transformations

Detection and transformation are independent. A policy can choose any transformer for its PII type.

Transformer Stable Reversible Principal disclosure
transform.Remove() Yes No A removed value may affect surrounding structure
transform.Replace(marker) Yes No Marker can reveal that a value or type was present
transform.NewMasker(options) Yes No Exact length plus configured prefix and suffix
transform.NewHMACTokenizer(options) Yes No Equality and frequency in one key/type domain
transform.NewReversibleTokenizer(options) No Yes Exact input length through token length

Masking counts bytes, not Unicode code points. Callers choose the retained prefix and suffix, one replacement byte, and whether unexpectedly short values are fully masked or rejected without output.

masker, err := transform.NewMasker(transform.MaskOptions{
	PrefixBytes: 2,
	SuffixBytes: 4,
	MaskByte:    '*',
	ShortInput:  transform.ShortInputMaskAll,
})

All built-in transformations are immutable and concurrency-safe after construction. Redaction and masking are directly idempotent; end-to-end engine idempotence additionally depends on whether detectors recognize transformed output. Tokenization is not idempotent.

Custom extensions

Applications can implement the single-method incognigo.Detector and incognigo.Transformer interfaces. Registrations declare stable detector IDs, authorized PII types, and finite match/lookaround bounds. Custom extensions are trusted code: they must treat borrowed values as read-only, must not retain or disclose them, must honor cancellation, and must be safe for concurrent calls.

NewEngine copies configuration slices and metadata, but retains detector and transformer implementations as executable interfaces.

Built-in detector formats

Built-ins are deterministic, byte-oriented syntactic detectors. Non-ASCII and malformed UTF-8 bytes are barriers, original byte offsets are preserved, and input is never normalized. Detection resumes after a barrier, so a valid ASCII candidate that follows a non-ASCII byte can still match independently.

Detector Supported Deliberately unsupported Streaming bounds
detector.Email() ASCII dot-atom local part; DNS-style domain with at least two labels; alphabetic final label of at least two bytes; uppercase and lowercase Quoted local parts, comments, domain literals, Unicode local/domain names, single-label domains, numeric final labels 254-byte match, 1-byte lookbehind, 1-byte lookahead
detector.IPAddress() Strict dotted-decimal IPv4, compressed/full IPv6, IPv4-mapped IPv6, bracketed IPv6 without brackets in the match, IPv4 before a colon-delimited port or field IPv4 leading zeroes, scoped zones, CIDR prefixes, hostnames, integer/hex IPv4 notation 45-byte match, 1-byte lookbehind, 1-byte lookahead
detector.PaymentCard() 13–19 ASCII digits, contiguous or separated by consistent single spaces/hyphens, valid Luhn checksum Mixed/repeated separators, Unicode digits, invalid Luhn values, repeated one-digit placeholders; card-brand validity is not inferred 37-byte match, 1-byte lookbehind, 1-byte lookahead
detector.NANPPhone() Ten-digit NANP forms, consistent space/dot/hyphen separators, parentheses, optional 1/+1; area and exchange first digits 2–9 Non-NANP international formats, mixed separators, N11 area/exchange service codes; extensions are not part of a match 17-byte match, 2-byte lookbehind, 2-byte lookahead

Additional format details:

  • Email local parts contain 1–64 bytes made from ASCII letters, digits, dots, and !#$%&'*+-/=?^_`{|}~. Dots separate non-empty atoms. Domain labels contain 1–63 letters, digits, or interior hyphens; the domain is at most 253 bytes and the complete address at most 254 bytes. A terminal period is surrounding punctuation.
  • IPv4 spans the full 0.0.0.0 through 255.255.255.255 range. IP syntax is structurally validated with net/netip.
  • Payment-card rendered length is at most 37 bytes. Passing Luhn does not prove that an account was issued or is active.
  • NANP examples include 2125550100, 212-555-0100, (212) 555-0100, 1-212-555-0100, +12125550100, and +1 (212) 555-0100.
Accuracy evidence

The committed JSON corpora label exact expected byte spans using entirely synthetic or officially reserved values. TestAccuracyCorpora rejects any span drift, false positive, or false negative.

Detector Corpus cases Expected spans False positives False negatives Precision Recall
Email 28 15 0 0 100% 100%
IP address 24 15 0 0 100% 100%
Payment card 22 14 0 0 100% 100%
NANP phone 27 18 0 0 100% 100%

These figures are regression evidence only. They are not an estimate of recall or precision on arbitrary production traffic. Applications must validate detector scope and false-negative risk against representative, privacy-safe data before relying on Incognigo at a production trust boundary.

Behavioral contracts

Configuration and overlap resolution

Engines must be created with incognigo.NewEngine. Detector IDs and policies are unique. Every type declared by a detector has exactly one policy, and every policy corresponds to a declared type. Invalid or typed-nil implementations are rejected. A nil or zero-valued engine fails closed.

Overlapping candidates are preferred deterministically by:

  1. policy priority, descending;
  2. span length, descending;
  3. start offset, ascending;
  4. end offset, ascending;
  5. PII type, lexicographically ascending; and
  6. detector ID, lexicographically ascending.

Candidates are greedily accepted only when they do not overlap an accepted span. Adjacent spans do not overlap. Identical accepted matches collapse.

Byte and ownership semantics

All offsets and limits count bytes. Incognigo performs no Unicode normalization. Malformed UTF-8 outside a match is preserved exactly.

Detector input and transformer values are borrowed, read-only views valid only for the call. Extensions transfer ownership of returned slices. Batch processing never mutates or retains caller input and always returns newly owned output, including empty and no-match results.

One engine may be reused concurrently when its custom extensions are also concurrency-safe. Built-in detectors and transformations are immutable and safe for concurrent use.

Limits

Zero-valued fields select these defaults:

Limit Default Meaning
MaxBatchInputBytes 8 MiB Input accepted by one batch operation
MaxBatchOutputBytes 32 MiB Completed output from one batch operation
MaxBatchMatches 10,000 Candidates retained per batch or streaming decision
MaxStreamBufferBytes 256 KiB Undecided source retained across stream reads

Maximum values are inclusive. Negative limits are invalid. Total stream input and output are not bounded by the batch byte limits.

Failures and diagnostics

Incognigo-generated runtime failures are *incognigo.Error values. Use errors.Is with the fixed sentinels and errors.As for allowlisted metadata:

var protectionErr *incognigo.Error
if errors.As(err, &protectionErr) {
	actual, maximum, hasCounts := protectionErr.Counts()
	_ = actual
	_ = maximum
	_ = hasCounts
}

Library errors may contain only fixed operation/field/limit names, validated detector IDs and PII types, and numeric counts. Arbitrary detector, transformer, reader, and writer errors are discarded rather than wrapped because their messages may contain source data. Batch failures return no partial output. Transformer output returned with an error is discarded.

Tokenization design

Both tokenizers require a caller-supplied, unpredictable 32-byte key and a public key ID. They accept arbitrary bytes, bind the PII type, use unpadded base64url output, default to an 8 MiB direct-call limit, and perform no network, file, secret-manager, database, or token-vault access.

Deterministic irreversible tokens use:

icg.v1.hmac-sha256.<key-id>.<32-byte-hmac>

The HMAC input is:

"incognigo\0token\0v1\0hmac-sha256\0"
|| complete-token-header
|| uint64be(len(pii-type)) || pii-type
|| uint64be(len(value))    || value

Randomized reversible tokens use:

icg.v1.aes-256-gcm.<key-id>.<12-byte-nonce || ciphertext || 16-byte-tag>

AES-GCM authenticates:

"incognigo\0token\0v1\0aes-256-gcm\0"
|| complete-token-header
|| uint64be(len(pii-type)) || pii-type

The complete header ends after the key-ID separator. Parsers reject unknown versions, algorithms, malformed encodings, wrong PII types, tampering, and non-canonical payloads.

Threat model and key rotation

HMAC tokens expose equality and frequency. Possession of their key permits offline guessing, especially for low-entropy values. Reversible tokens expose encoded plaintext length and become readable and forgeable if their key is compromised. Neither mode prevents disclosure from other plaintext copies, process memory, crash dumps, malicious extensions, or application code authorized to detokenize.

Go does not guarantee zeroization of compiler-managed key or plaintext memory. Callers own secret storage, process controls, aggregate usage accounting, and rotation. One reversible key must not encrypt more than 2^32 values across all processes and tokenizer instances.

To rotate a reversible key:

  1. create a tokenizer with the new key as ActiveKey;
  2. retain required prior keys in DecryptionKeys;
  3. route new transformations through the new instance;
  4. optionally detokenize and re-tokenize persisted old values; and
  5. remove an old key only when recovery and retention policies permit.

Missing keys are never fetched automatically. A recognized reversible token with an unavailable key returns ErrTokenKeyUnavailable; malformed or unauthenticated tokens return ErrInvalidToken; oversized values return ErrTokenValueTooLarge. Every failure returns nil plaintext.

Test vectors

The following vectors use the synthetic value person@example.test, PII type email_address, key ID 2026-01, and a test-only key containing bytes 00 01 ... 1f.

icg.v1.hmac-sha256.2026-01.3rDSpGYbv2Mt9LkfD0SiBefyvnDQN47gLdInd8STLyk

The reversible decryption vector uses the fixed nonce 00 01 02 03 04 05 06 07 08 09 0a 0b only for verification:

icg.v1.aes-256-gcm.2026-01.AAECAwQFBgcICQoLN2ekaKqLgn71IPr73YxWGeal84tlruM4lmxUjjKxV2SbAHM

Production tokenization always obtains fresh randomness from Go's cryptographic implementation.

Security and limitations

Incognigo processes sensitive data at application trust boundaries. Report a suspected privacy bypass, source-data disclosure, unbounded resource path, concurrency leak, or cryptographic defect through GitHub's private security advisory flow:

https://github.com/Chadi00/incognigo/security/advisories/new

Do not include customer data, production logs, credentials, real PII, or public breach data. Use a minimal synthetic reproduction. If private advisories are unavailable, contact the repository owner privately without sending exploit details in the initial message.

Operational limitations:

  • syntactic detection cannot guarantee that every production PII value is found;
  • unsupported formats pass through unchanged unless a custom detector handles them;
  • attribute keys and native non-textual slog.Value kinds are not transformed;
  • custom extensions are trusted code;
  • streaming can leave a protected prefix after failure;
  • deterministic tokens disclose equality and reversible tokens depend on caller-owned key protection; and
  • during v0, exported APIs may change to address material safety, correctness, or usability issues.

Never treat protection failure as permission to send the original value.

Development and release checks

Test fixtures, fuzz seeds, examples, benchmarks, issues, and pull requests must use synthetic data. Never commit customer samples, real-person data, public breach data, raw vulnerability-report values, or secrets. The canonical detector corpora live under detector/testdata.

Common checks:

go vet ./...
go test ./...
go test -race ./...
go test -run '^$' -bench . -benchmem -benchtime=1x ./...
go test -v ./detector -run '^TestAccuracyCorpora$'
./scripts/fuzz.sh 30s

The complete release gate checks formatting, module tidiness, the zero-dependency policy, vetting, ordinary and race tests, benchmark compilation, accuracy corpora, every fuzz target, and pinned vulnerability analysis:

./scripts/release-check.sh 30s

Pull requests affecting detection, transformation, streaming, logging, public or persisted behavior must explain failure behavior, resource bounds, test and fuzz coverage, and compatibility impact.

License

Incognigo is licensed under the Apache License 2.0.

Documentation

Overview

Package incognigo provides bounded PII detection and transformation.

Detectors report only PII types and half-open byte spans. Policies select a transformer and an explicit overlap priority for each type. NewEngine validates and copies configuration before exposing an immutable engine that is safe for concurrent use. Engine.ProtectBytes is the canonical batch processing path; Engine.ProtectString is its byte-preserving string adapter. Engine.ProtectStream applies the same semantics to an arbitrary-length io.Reader while bounding undecided source retention. Package sloghandler protects structured log records with the same configured engine before a downstream slog handler can format or transport them.

The module README documents the complete behavioral contracts, including ownership, malformed UTF-8, overlap resolution, and streaming failure semantics.

Example
package main

import (
	"context"
	"fmt"

	"github.com/Chadi00/incognigo"
	"github.com/Chadi00/incognigo/detector"
	"github.com/Chadi00/incognigo/transform"
)

func main() {
	engine, err := incognigo.NewEngine(incognigo.Config{
		Detectors: []incognigo.DetectorRegistration{
			detector.Email(),
			detector.IPAddress(),
			detector.PaymentCard(),
			detector.NANPPhone(),
		},
		Policies: []incognigo.Policy{
			{
				Type:        incognigo.PIITypeEmailAddress,
				Priority:    100,
				Transformer: transform.Replace([]byte("[email]")),
				Determinism: incognigo.Deterministic,
			},
			{
				Type:        incognigo.PIITypeIPAddress,
				Priority:    90,
				Transformer: transform.Replace([]byte("[ip]")),
				Determinism: incognigo.Deterministic,
			},
			{
				Type:        incognigo.PIITypePaymentCardNumber,
				Priority:    80,
				Transformer: transform.Replace([]byte("[payment-card]")),
				Determinism: incognigo.Deterministic,
			},
			{
				Type:        incognigo.PIITypePhoneNumber,
				Priority:    70,
				Transformer: transform.Replace([]byte("[nanp-phone]")),
				Determinism: incognigo.Deterministic,
			},
		},
	})
	if err != nil {
		panic(err)
	}

	protected, err := engine.ProtectString(
		context.Background(),
		"email owner@example.test from 192.0.2.1 "+
			"with card 4111 1111 1111 1111 or call 212-555-0100",
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(protected)
}
Output:
email [email] from [ip] with card [payment-card] or call [nanp-phone]

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidConfiguration identifies rejected configuration, use of an
	// unconfigured engine, or a required operation argument that is nil.
	ErrInvalidConfiguration = errors.New("incognigo: invalid configuration")

	// ErrLimitExceeded identifies an operation that crossed a configured
	// processing limit.
	ErrLimitExceeded = errors.New("incognigo: processing limit exceeded")

	// ErrInvalidMatch identifies invalid metadata returned by a detector.
	ErrInvalidMatch = errors.New("incognigo: invalid detector match")

	// ErrDetection identifies a detector that could not finish safely.
	ErrDetection = errors.New("incognigo: detection failed")

	// ErrTransformation identifies a transformer that could not finish safely.
	ErrTransformation = errors.New("incognigo: transformation failed")

	// ErrStreamRead identifies a streaming source that could not be read
	// safely.
	ErrStreamRead = errors.New("incognigo: stream read failed")

	// ErrStreamWrite identifies a streaming destination that could not accept
	// protected output safely.
	ErrStreamWrite = errors.New("incognigo: stream write failed")
)

Functions

This section is empty.

Types

type Config

type Config struct {
	// Detectors lists the detection extensions and their bounded metadata.
	Detectors []DetectorRegistration

	// Policies contains exactly one transformation policy per declared type.
	Policies []Policy

	// Limits bounds processing work; zero fields use documented defaults.
	Limits ProcessingLimits
}

Config describes an engine before validation and compilation.

NewEngine copies the slices and metadata reachable through Config. Detector and transformer implementations are retained as interfaces and must satisfy their documented immutability and concurrency contracts.

type Detector

type Detector interface {
	Detect(ctx context.Context, input []byte) ([]Match, error)
}

Detector finds candidate PII matches in input.

Detect may be called concurrently. It must treat input as read-only, must not retain input after returning, and must honor cancellation through ctx. Apart from cancellation or failure, its results must depend only on input; streaming may call Detect repeatedly with overlapping views. DetectorBounds must be sufficient for a view containing a possible match and its declared context to produce the same decision as the complete input. Returned matches must use byte offsets, must be within input, and must use a type declared by the detector's registration. The returned slice and its backing array are transferred to the caller and must not be reused.

Detector errors must not contain input, matched values, or data derived from them. Incognigo still converts detector failures into its own safe Error rather than returning an extension error directly.

type DetectorBounds

type DetectorBounds struct {
	// MaxMatchBytes is the maximum length of any span returned by the
	// detector. It must be greater than zero.
	MaxMatchBytes int

	// LookbehindBytes is the maximum number of bytes before a possible match
	// that can affect whether it is a match.
	LookbehindBytes int

	// LookaheadBytes is the maximum number of bytes after a possible match
	// that can affect whether it is a match.
	LookaheadBytes int
}

DetectorBounds declares the finite context a detector needs. Incognigo uses these values to retain enough bytes across streaming read boundaries.

type DetectorID

type DetectorID string

DetectorID is stable, non-sensitive metadata used to identify a configured detector and to break otherwise identical match-ordering ties.

func (DetectorID) Valid

func (id DetectorID) Valid() bool

Valid reports whether id is a valid detector identifier. Detector IDs use the same grammar as PIIType and contain at most 128 bytes.

type DetectorRegistration

type DetectorRegistration struct {
	// ID is unique, stable, and safe to include in diagnostics.
	ID DetectorID

	// Detector is the executable extension.
	Detector Detector

	// Types is the complete set of PII types Detector may return.
	Types []PIIType

	// Bounds declares Detector's finite byte-context requirements.
	Bounds DetectorBounds
}

DetectorRegistration binds a detector implementation to stable metadata.

ID must be unique within a Config. Types must be non-empty and contain no duplicates. Incognigo copies Types during construction.

type Determinism

type Determinism uint8

Determinism states whether a transformer promises stable output.

const (
	// Deterministic means identical PII type and value bytes produce identical
	// output bytes for the lifetime of the effective configuration.
	Deterministic Determinism = iota + 1

	// NonDeterministic means repeated transformations may produce different
	// output bytes.
	NonDeterministic
)

func (Determinism) Valid

func (d Determinism) Valid() bool

Valid reports whether d is a recognized determinism declaration.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is an immutable, concurrency-safe compiled configuration.

The zero value is not a configured engine. Engines must be constructed with NewEngine.

func NewEngine

func NewEngine(config Config) (*Engine, error)

NewEngine validates and copies config into an immutable engine.

At least one detector and one policy are required. Detector IDs and policy types must be unique. Every type declared by a detector must have one policy, and every policy must correspond to a declared detector type.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/Chadi00/incognigo"
)

type exampleDetector struct{}

func (exampleDetector) Detect(
	_ context.Context,
	input []byte,
) ([]incognigo.Match, error) {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil, nil
	}
	return []incognigo.Match{
		{
			Type: "company.employee_id",
			Span: incognigo.Span{
				Start: start,
				End:   start + len(value),
			},
		},
	}, nil
}

type exampleTransformer struct{}

func (exampleTransformer) Transform(
	context.Context,
	incognigo.PIIType,
	[]byte,
) ([]byte, error) {
	return []byte("[protected]"), nil
}

func main() {
	engine, err := newExampleEngine()
	if err != nil {
		panic(err)
	}

	fmt.Println(engine.Limits().MaxBatchInputBytes > 0)
}

func newExampleEngine() (*incognigo.Engine, error) {
	return incognigo.NewEngine(incognigo.Config{
		Detectors: []incognigo.DetectorRegistration{
			{
				ID:       "company.employee_id",
				Detector: exampleDetector{},
				Types:    []incognigo.PIIType{"company.employee_id"},
				Bounds: incognigo.DetectorBounds{
					MaxMatchBytes: 8,
				},
			},
		},
		Policies: []incognigo.Policy{
			{
				Type:        "company.employee_id",
				Priority:    100,
				Transformer: exampleTransformer{},
				Determinism: incognigo.Deterministic,
			},
		},
	})
}
Output:
true

func (*Engine) Limits

func (e *Engine) Limits() ProcessingLimits

Limits returns the effective, fully populated processing limits.

func (*Engine) ProtectBytes

func (e *Engine) ProtectBytes(ctx context.Context, input []byte) ([]byte, error)

ProtectBytes detects and transforms configured PII matches in input.

ProtectBytes never mutates or retains input. On success, the returned slice is owned by the caller and does not alias input, including when input is empty or no matches are found. Bytes outside resolved matches are preserved exactly. On failure, ProtectBytes returns a nil result rather than partial or unprotected output.

One Engine may be used concurrently. The configured detectors and transformers must satisfy their own concurrency contracts.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/Chadi00/incognigo"
)

type exampleDetector struct{}

func (exampleDetector) Detect(
	_ context.Context,
	input []byte,
) ([]incognigo.Match, error) {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil, nil
	}
	return []incognigo.Match{
		{
			Type: "company.employee_id",
			Span: incognigo.Span{
				Start: start,
				End:   start + len(value),
			},
		},
	}, nil
}

type exampleTransformer struct{}

func (exampleTransformer) Transform(
	context.Context,
	incognigo.PIIType,
	[]byte,
) ([]byte, error) {
	return []byte("[protected]"), nil
}

func main() {
	engine, err := newExampleEngine()
	if err != nil {
		panic(err)
	}

	protected, err := engine.ProtectBytes(
		context.Background(),
		[]byte("employee EMP-0042 signed in"),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(string(protected))
}

func newExampleEngine() (*incognigo.Engine, error) {
	return incognigo.NewEngine(incognigo.Config{
		Detectors: []incognigo.DetectorRegistration{
			{
				ID:       "company.employee_id",
				Detector: exampleDetector{},
				Types:    []incognigo.PIIType{"company.employee_id"},
				Bounds: incognigo.DetectorBounds{
					MaxMatchBytes: 8,
				},
			},
		},
		Policies: []incognigo.Policy{
			{
				Type:        "company.employee_id",
				Priority:    100,
				Transformer: exampleTransformer{},
				Determinism: incognigo.Deterministic,
			},
		},
	})
}
Output:
employee [protected] signed in

func (*Engine) ProtectStream

func (e *Engine) ProtectStream(
	ctx context.Context,
	destination io.Writer,
	source io.Reader,
) (int64, error)

ProtectStream reads source, protects configured PII, and writes the protected bytes to destination.

ProtectStream applies the same detector ordering, match validation, overlap resolution, and transformations as ProtectBytes. Total stream length and total output are not bounded by the batch input and output limits. Undecided source bytes are bounded by MaxStreamBufferBytes, and MaxBatchMatches bounds the candidates retained during each streaming decision.

The returned count includes only bytes successfully written to destination. A failure after an earlier write can therefore leave a protected prefix in destination. ProtectStream never follows that prefix with undecided or unprotected source bytes. Callers requiring all-or-nothing delivery must use a transactional destination or buffer the result before committing it.

Reader and writer failures are converted to source-data-safe Error values; errors returned by source or destination are not retained. One Engine may be used concurrently. The configured extensions and the supplied Reader and Writer must satisfy their own concurrency contracts.

Example
package main

import (
	"bytes"
	"context"
	"fmt"
	"strings"

	"github.com/Chadi00/incognigo"
)

type exampleDetector struct{}

func (exampleDetector) Detect(
	_ context.Context,
	input []byte,
) ([]incognigo.Match, error) {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil, nil
	}
	return []incognigo.Match{
		{
			Type: "company.employee_id",
			Span: incognigo.Span{
				Start: start,
				End:   start + len(value),
			},
		},
	}, nil
}

type exampleTransformer struct{}

func (exampleTransformer) Transform(
	context.Context,
	incognigo.PIIType,
	[]byte,
) ([]byte, error) {
	return []byte("[protected]"), nil
}

func main() {
	engine, err := newExampleEngine()
	if err != nil {
		panic(err)
	}

	var destination bytes.Buffer
	written, err := engine.ProtectStream(
		context.Background(),
		&destination,
		strings.NewReader("employee EMP-0042 signed in"),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(destination.String())
	fmt.Println(written)
}

func newExampleEngine() (*incognigo.Engine, error) {
	return incognigo.NewEngine(incognigo.Config{
		Detectors: []incognigo.DetectorRegistration{
			{
				ID:       "company.employee_id",
				Detector: exampleDetector{},
				Types:    []incognigo.PIIType{"company.employee_id"},
				Bounds: incognigo.DetectorBounds{
					MaxMatchBytes: 8,
				},
			},
		},
		Policies: []incognigo.Policy{
			{
				Type:        "company.employee_id",
				Priority:    100,
				Transformer: exampleTransformer{},
				Determinism: incognigo.Deterministic,
			},
		},
	})
}
Output:
employee [protected] signed in
30

func (*Engine) ProtectString

func (e *Engine) ProtectString(ctx context.Context, input string) (string, error)

ProtectString detects and transforms configured PII matches in input.

String processing preserves the exact bytes in input and delegates to the byte processing path. On failure, ProtectString returns an empty string and an error, never the original input as a fallback.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/Chadi00/incognigo"
)

type exampleDetector struct{}

func (exampleDetector) Detect(
	_ context.Context,
	input []byte,
) ([]incognigo.Match, error) {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil, nil
	}
	return []incognigo.Match{
		{
			Type: "company.employee_id",
			Span: incognigo.Span{
				Start: start,
				End:   start + len(value),
			},
		},
	}, nil
}

type exampleTransformer struct{}

func (exampleTransformer) Transform(
	context.Context,
	incognigo.PIIType,
	[]byte,
) ([]byte, error) {
	return []byte("[protected]"), nil
}

func main() {
	engine, err := newExampleEngine()
	if err != nil {
		panic(err)
	}

	protected, err := engine.ProtectString(
		context.Background(),
		"employee EMP-0042 signed in",
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(protected)
}

func newExampleEngine() (*incognigo.Engine, error) {
	return incognigo.NewEngine(incognigo.Config{
		Detectors: []incognigo.DetectorRegistration{
			{
				ID:       "company.employee_id",
				Detector: exampleDetector{},
				Types:    []incognigo.PIIType{"company.employee_id"},
				Bounds: incognigo.DetectorBounds{
					MaxMatchBytes: 8,
				},
			},
		},
		Policies: []incognigo.Policy{
			{
				Type:        "company.employee_id",
				Priority:    100,
				Transformer: exampleTransformer{},
				Determinism: incognigo.Deterministic,
			},
		},
	})
}
Output:
employee [protected] signed in

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error contains only non-sensitive failure metadata.

Use errors.As to inspect an Error and errors.Is with one of the sentinel errors above to classify it. Error never retains an extension error because an arbitrary extension error may contain source data.

Example
package main

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/Chadi00/incognigo"
)

type exampleDetector struct{}

func (exampleDetector) Detect(
	_ context.Context,
	input []byte,
) ([]incognigo.Match, error) {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil, nil
	}
	return []incognigo.Match{
		{
			Type: "company.employee_id",
			Span: incognigo.Span{
				Start: start,
				End:   start + len(value),
			},
		},
	}, nil
}

type exampleTransformer struct{}

func (exampleTransformer) Transform(
	context.Context,
	incognigo.PIIType,
	[]byte,
) ([]byte, error) {
	return []byte("[protected]"), nil
}

func main() {
	engine, err := newExampleEngine()
	if err != nil {
		panic(err)
	}

	_, err = engine.ProtectString(
		context.Background(),
		strings.Repeat("x", engine.Limits().MaxBatchInputBytes+1),
	)
	if err == nil {
		panic("expected the configured input limit to be enforced")
	}

	var protectionErr *incognigo.Error
	if errors.As(err, &protectionErr) {
		actual, maximum, hasCounts := protectionErr.Counts()
		fmt.Println(
			protectionErr.Kind() == incognigo.ErrorKindLimitExceeded,
		)
		fmt.Println(protectionErr.Operation())
		fmt.Println(protectionErr.Limit())
		fmt.Println(actual > maximum, hasCounts)
	}
}

func newExampleEngine() (*incognigo.Engine, error) {
	return incognigo.NewEngine(incognigo.Config{
		Detectors: []incognigo.DetectorRegistration{
			{
				ID:       "company.employee_id",
				Detector: exampleDetector{},
				Types:    []incognigo.PIIType{"company.employee_id"},
				Bounds: incognigo.DetectorBounds{
					MaxMatchBytes: 8,
				},
			},
		},
		Policies: []incognigo.Policy{
			{
				Type:        "company.employee_id",
				Priority:    100,
				Transformer: exampleTransformer{},
				Determinism: incognigo.Deterministic,
			},
		},
	})
}
Output:
true
protect_string
max_batch_input_bytes
true true

func (*Error) Counts

func (e *Error) Counts() (actual int, maximum int, ok bool)

Counts returns the observed and configured limit values. ok is false when the failure is not a limit failure with meaningful counts.

func (*Error) DetectorID

func (e *Error) DetectorID() DetectorID

DetectorID returns the non-sensitive detector identifier associated with a runtime failure, if any.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Field

func (e *Error) Field() string

Field returns the configuration or operation field associated with a failure. It never includes the field's value.

func (*Error) Kind

func (e *Error) Kind() ErrorKind

Kind returns the broad failure category.

func (*Error) Limit

func (e *Error) Limit() string

Limit returns the stable name of an exceeded processing limit, if any.

func (*Error) Operation

func (e *Error) Operation() string

Operation returns the library operation that failed. Callers should branch on Kind rather than parsing this diagnostic label.

func (*Error) PIIType

func (e *Error) PIIType() PIIType

PIIType returns the non-sensitive PII identifier associated with a runtime failure, if any.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes only a fixed Incognigo sentinel, never an arbitrary extension error.

type ErrorKind

type ErrorKind uint8

ErrorKind classifies an Incognigo failure without exposing source data.

const (
	// ErrorKindUnknown is the zero value and does not classify a failure.
	ErrorKindUnknown ErrorKind = iota

	// ErrorKindInvalidConfiguration classifies configuration and invalid
	// engine-use failures.
	ErrorKindInvalidConfiguration

	// ErrorKindLimitExceeded classifies processing-limit failures.
	ErrorKindLimitExceeded

	// ErrorKindInvalidMatch classifies invalid detector metadata.
	ErrorKindInvalidMatch

	// ErrorKindDetection classifies detector execution failures.
	ErrorKindDetection

	// ErrorKindTransformation classifies transformer execution failures.
	ErrorKindTransformation

	// ErrorKindStreamRead classifies streaming source failures.
	ErrorKindStreamRead

	// ErrorKindStreamWrite classifies streaming destination failures.
	ErrorKindStreamWrite
)

type Match

type Match struct {
	// Type selects the configured policy.
	Type PIIType

	// Span locates the value without retaining it.
	Span Span
}

Match is the non-sensitive metadata returned by a Detector.

A Match deliberately contains no copy, excerpt, hash, or token derived from the matched value.

func (Match) Valid

func (m Match) Valid(inputBytes int) bool

Valid reports whether m has a valid type and span for an input containing inputBytes bytes.

type PIIType

type PIIType string

PIIType is a stable, non-sensitive identifier for a class of personally identifiable information. Custom identifiers must satisfy Valid.

A PIIType identifies policy; it does not imply that Incognigo has a built-in detector for that type.

const (
	PIITypeEmailAddress      PIIType = "email_address"
	PIITypeIPAddress         PIIType = "ip_address"
	PIITypePaymentCardNumber PIIType = "payment_card_number"
	PIITypePhoneNumber       PIIType = "phone_number"
)

Initial PII type identifiers. Detector packages may add support for these identifiers independently.

func (PIIType) Valid

func (t PIIType) Valid() bool

Valid reports whether t is a valid public PII identifier.

Identifiers contain at most 64 lowercase ASCII bytes. They start and end with an ASCII letter or digit and may contain single '.', '-', or '_' separators between alphanumeric runs.

type Policy

type Policy struct {
	// Type is the unique PII type governed by this policy.
	Type PIIType

	// Priority resolves overlaps; a larger value wins.
	Priority int

	// Transformer replaces values selected by this policy.
	Transformer Transformer

	// Determinism declares Transformer's output stability.
	Determinism Determinism
}

Policy assigns a transformation and overlap priority to one PII type.

Higher Priority values win overlap conflicts. Equal priorities are resolved by the stable rules documented in the module README. Transformer must remain safe for concurrent use and must not be mutated while an Engine uses it.

type ProcessingLimits

type ProcessingLimits struct {
	// MaxBatchInputBytes is the largest input accepted by one batch
	// operation.
	MaxBatchInputBytes int

	// MaxBatchOutputBytes is the largest completed output from one batch
	// operation.
	MaxBatchOutputBytes int

	// MaxBatchMatches is the largest number of detector candidates retained
	// during one batch operation or one streaming decision.
	MaxBatchMatches int

	// MaxStreamBufferBytes is the largest number of undecided source bytes
	// retained across streaming reads. It does not limit total stream length.
	MaxStreamBufferBytes int
}

ProcessingLimits bounds memory and input-driven work.

A zero field selects that field's value from DefaultProcessingLimits. Negative fields are invalid. Batch byte limits apply to one batch call; the match limit also applies to one streaming decision. The stream buffer limit bounds undecided source bytes retained while processing a stream; it does not limit the total length of a stream or its output.

func DefaultProcessingLimits

func DefaultProcessingLimits() ProcessingLimits

DefaultProcessingLimits returns the limits used for zero-valued fields: 8 MiB of batch input, 32 MiB of batch output, 10,000 candidates per batch or streaming decision, and 256 KiB of retained streaming source data.

type Span

type Span struct {
	// Start is the inclusive byte offset.
	Start int

	// End is the exclusive byte offset.
	End int
}

Span identifies a non-empty half-open byte range [Start, End).

Offsets always refer to the exact input byte sequence supplied to a detector. They are not rune indexes.

func (Span) Valid

func (s Span) Valid(inputBytes int) bool

Valid reports whether s is a valid, non-empty span for an input containing inputBytes bytes.

type Transformer

type Transformer interface {
	Transform(ctx context.Context, piiType PIIType, value []byte) ([]byte, error)
}

Transformer replaces one matched value.

Transform may be called concurrently. It must treat value as read-only and must not retain value after returning. Ownership of a successful result is transferred to Incognigo; the transformer must not later mutate or reuse its backing array. A nil result with a nil error is a valid empty replacement.

Transformer errors must not contain value or data derived from it. Incognigo converts transformer failures into its own safe Error and discards any result returned alongside an error.

Directories

Path Synopsis
Package detector provides Incognigo's curated, standard-library-only PII detectors.
Package detector provides Incognigo's curated, standard-library-only PII detectors.
internal
match
Package match contains deterministic validation, ordering, and overlap resolution for detector candidates.
Package match contains deterministic validation, ordering, and overlap resolution for detector candidates.
stream
Package stream contains bounded source retention and closed-prefix mechanics used by the root package's streaming surface.
Package stream contains bounded source retention and closed-prefix mechanics used by the root package's streaming surface.
Package sloghandler protects log/slog records with a configured Incognigo engine before passing them to another slog.Handler.
Package sloghandler protects log/slog records with a configured Incognigo engine before passing them to another slog.Handler.
Package transform provides redaction, masking, and tokenization transformations that implement the contracts defined by the root incognigo package.
Package transform provides redaction, masking, and tokenization transformations that implement the contracts defined by the root incognigo package.

Jump to

Keyboard shortcuts

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