incognigo

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 29, 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.

The current development line 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 and are recorded in CHANGELOG.md.

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, NANP phone, narrow global E.164/telephone-URI, IBAN, and MAC detectors, plus opt-in US and GB identifier packs;
  • source-content-free inspection results with ordinal confidence and fixed evidence metadata;
  • 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, national phone formats outside NANP, phone-number assignment metadata, government identifiers outside the explicit locale packs, or contextual free-text entity recognition. It does not detect all PII and cannot guarantee anonymization.

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@latest

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

Quick start

This complete example protects all six core built-in PII types with seven detectors (NANP and E.164 share the phone policy). Locale-pack detectors remain explicitly opt-in. 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(),
			detector.E164Phone(),
			detector.IBAN(),
			detector.MACAddress(),
		},
		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:    110,
				Transformer: transform.Replace([]byte("[phone]")),
				Determinism: incognigo.Deterministic,
			},
			{
				Type:        incognigo.PIITypeIBAN,
				Priority:    60,
				Transformer: transform.Replace([]byte("[iban]")),
				Determinism: incognigo.Deterministic,
			},
			{
				Type:        incognigo.PIITypeMACAddress,
				Priority:    50,
				Transformer: transform.Replace([]byte("[mac]")),
				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, call 212-555-0100 or "+
			"tel:+442079460958, "+
			"IBAN GB29NWBK60161331926819, MAC 00:1A:2B:3C:4D:5E",
	)
	if err != nil {
		return fmt.Errorf("protect data: %w", err)
	}
	fmt.Println(protected)
	return nil
}

Output:

email [email] from [ip] with card [payment-card], call [phone] or tel:[phone], IBAN [iban], MAC [mac]

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)

Engine.Inspect runs the same validation, candidate budget, confidence thresholds, and overlap resolution without transforming source bytes:

findings, err := engine.Inspect(ctx, inputBytes)

Each finding contains only type, exact original byte span, detector/version, locale, ordinal confidence, fixed evidence flags, configured threshold, and outcome. It contains no source excerpt, copied match, source hash, or provider error. The metadata is content-derived and is not automatically safe to log.

Engine.ContainsPII is the metadata-free, early-exit trust-boundary check. It stops on the first valid detector candidate, including candidates below policy confidence thresholds, and returns only a boolean:

containsPII, err := engine.ContainsPII(ctx, inputBytes)

An error means the result is indeterminate and must fail closed. The sloghandler strict-key policy uses this path.

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.

For file output, write to a restricted temporary file on the destination filesystem, sync and close it after success, then atomically rename it. For databases or object stores, write inside a transaction or to an uncommitted staging object and publish only after ProtectStream succeeds. Remove or abort the staging destination on every error.

Structured logging

For production logging, start with an opt-in profile. The telephony profile is strict: it scans free text, directly redacts known PSTN/SIP fields case-insensitively, rejects PII-looking attribute keys and group names, protects numeric values as strings, and rejects remaining unsupported native representations:

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

logger := slog.New(handler)
logger.Info(
	"routing tel:+1-416-555-0100",
	"ANI", "local extension 42",
	"To", "sip:+12125550100@voice.example.test",
)

TelephonyProfile directly replaces phone_number, phone, ani, dnis, from, to, caller_id, contact, and common asserted-identity fields even when their values are local, malformed, or outside detector syntax. It also inherits common credential-field redactions. ProductionProfile keeps native bool, duration, and time values for generic services; StrictProductionProfile rejects them. All profiles are starting points returned by value, so applications can review and amend their FieldPolicies before construction.

Custom exact field policies use the same complete-value behavior:

options := sloghandler.ProductionProfile()
options.FieldPolicies = options.FieldPolicies.With(sloghandler.FieldPolicy{
	Key:         "customer_reference",
	Replacement: "[REDACTED]",
})
handler, err := sloghandler.NewWithOptions(engine, downstream, options)

Matching is exact rather than substring-based. Enable CaseInsensitiveFieldMatching for unstable header/key casing. A matched field is replaced before resolving slog.LogValuer, formatting opaque values, or traversing a group, so known-sensitive values do not depend on free-text detection.

With RejectPIILookingKeys, every non-empty attribute key and group name is inspected by the configured engine. Any detector candidate—including one below its policy confidence threshold—returns sloghandler.ErrUnsafeKey and prevents downstream handling. The error never includes the key. Keys are not transformed because renaming can create collisions and alter group semantics.

String and exact []byte values keep their logical kinds when they are not field-redacted. 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/group names that pass strict inspection, nil Any values, native values allowed by the selected options, 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.

Native integers, unsigned integers, and floats can be converted to protected strings explicitly. Strict mode rejects unprotected native representations:

handler, err := sloghandler.NewWithOptions(
	engine,
	downstream,
	sloghandler.Options{
		ProtectNumericValues:    true,
		RejectUnsupportedValues: true,
	},
)

Numeric conversion and field replacement change downstream schemas. The zero-value Options remains compatibility-oriented and opt-in profiles do not change sloghandler.New.

See examples/http for a small net/http service using the strict telephony setup without a framework.

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.

Safe production configuration

For logs and telemetry, fixed replacement or removal is the recommended baseline: it does not preserve value fragments, equality, or reversibility. Combine it with a production sloghandler profile, register every detector needed by the service domain, keep MinimumConfidence at ConfidenceWeak/zero when recall is the security priority, and bound limits for the largest intended record.

Masking is appropriate only when the explicitly retained prefix/suffix is permitted to leave the trust boundary. HMAC tokenization is appropriate only when cross-event correlation is required and equality/frequency disclosure is accepted. Reversible tokenization is appropriate only when an authorized recovery path is a requirement; load its random 32-byte key from a secret boundary, rotate it, and never log it.

For PSTN/SIP services, a safe starting point is:

detectors := []incognigo.DetectorRegistration{
	detector.Email(),
	detector.IPAddress(),
	detector.NANPPhone(),
	detector.E164Phone(),
}
// Configure replacement policies for email, IP, and the shared phone type,
// then wrap the downstream handler with sloghandler.TelephonyProfile().

Free-text detection and exact field policy are complementary: scanning catches PII embedded in messages and generic values; direct field redaction covers known-sensitive ani, dnis, from, to, and similar values even when their syntax is not recognized. Strict key inspection closes the third path where caller-controlled PII becomes a key or group name.

Give the phone policy a higher overlap priority than email when SIP subscriber forms should be classified as phone numbers. A value such as +12125550100@voice.example.test is also a syntactically valid email dot-atom/domain; deterministic policy priority decides which transformation wins.

Custom extensions

Applications can implement incognigo.Detector and incognigo.Transformer. A detector emits through the borrowed incognigo.MatchSink; Add validates authorization, spans, match length, and the configured budget before engine-owned storage grows. Detectors must stop when Add returns false and must never retain or call the sink concurrently. Registrations declare stable detector IDs, algorithm versions, confidence, evidence, authorized PII types, and finite match/lookaround bounds.

Transformers can additionally implement incognigo.SizedTransformer. When every selected transformer does so, batch output size is checked before any replacement is allocated and the output buffer is allocated exactly once. All built-in transformers implement this contract.

Custom extensions remain trusted code: they must treat borrowed values as read-only, must not retain or disclose them, must honor cancellation, must not panic, and must be safe for concurrent calls. Incognigo cannot enforce that an extension does not retain data, start hidden work, race, or allocate internally.

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
detector.E164Phone() Standalone + and 2–15 ASCII digits; common balanced RFC 3966 visual separators when immediately prefixed by tel:, sip:, or sips:; the match is only the subscriber number Calling-code/assignment validation, national/local numbers, standalone visual separators, spaces, extensions in the match, percent encoding, SIP parameters before @, arbitrary dial strings 30-byte match, 5-byte lookbehind, 1-byte lookahead
detector.IBAN() SWIFT registry release 102 country lengths, uppercase electronic and canonical print forms, ISO 7064 mod-97 Unknown country codes, lowercase, mixed spacing, invalid checksum 42-byte match, 1-byte lookbehind, 1-byte lookahead
detector.MACAddress() 48-bit colon/hyphen octets and dotted 16-bit groups EUI-64 and mixed separators 17-byte match, 1-byte lookbehind, 1-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 and uses a strict allocation-free parser. IPv6 is validated with net/netip. Colon-label recovery is bounded to eight prefix components and 64 bytes per component; longer delimiter chains are intentionally outside the detector's contract.
  • 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.
  • E.164/global examples include +12125550100, tel:+358-555-1234567, sip:+12125550100@voice.example.test, and sips:+44.20.7946.0958@voice.example.test. The detector enforces the international 15-digit maximum but deliberately does not embed country calling-code or national-plan metadata. Its scope follows the maximum in ITU-T E.164, the telephone-subscriber syntax in RFC 3966, and the SIP user placement in RFC 3261.

detector.StructuredGlobal() returns independent registrations for email, IP, payment card, IBAN, and MAC. Phone numbers remain excluded to preserve profile compatibility and because national forms need explicit region metadata; add E164Phone and/or NANPPhone explicitly. detector/us.Identifiers() and detector/gb.Identifiers() are opt-in locale packs for checksum- or range-validated identifiers; their package documentation states the exact scope. Adding a detector to any profile is a behavioral compatibility change.

Locale detector Rule source reviewed for algorithm v1 Accepted rendering
US SSN SSA post-randomization rules Nine compact digits or AAA-GG-SSSS
US ITIN IRS IRM 3.21.263 Nine compact digits or 9AA-GG-SSSS
US ABA routing ABA Routing Number Policy Nine compact digits, allocated prefix class, checksum
US NPI CMS NPI check-digit specification Ten compact digits, 1/2 prefix, checksum
GB NINO HMRC prefix rules Compact uppercase or canonical spaces
GB NHS number NHS number specification Ten compact digits or 3-3-4 spaces, modulus 11

The locale packs do not query assignment registries. Checksum/range validity does not prove issuance or identity, and compact numeric values can collide with unrelated identifiers.

External detection evaluation

The checked-in detector cases are conformance fixtures, not statistical accuracy evidence. They prove intended formats and exact byte spans remain stable; no precision or recall claim is derived from them. The generated built-in detector coverage report inventories matched formats, exercised non-matches/limitations, detector versions, and fixture counts for every built-in package. The built-ins are deterministic rules rather than trained models, but fixtures authored with those rules still cannot measure generalization.

Incognigo v0.2.0 was instead evaluated on a frozen revision of the independent, Apache-2.0 Gravitee PII Detection Dataset: 175,881 English documents with 781,052 labeled spans. Eight directly mapped labels supplied 153,912 unique scored spans. Exact type-and-span results were:

Dataset label Expected Precision Recall F1
Credit card 16,899 54.22% 51.22% 52.68%
Email address 32,122 96.86% 73.43% 83.53%
IBAN 8,456 99.86% 83.09% 90.70%
IP address 15,348 96.12% 95.65% 95.89%
MAC address 6,333 99.72% 67.68% 80.63%
Phone number 31,663 73.28% 27.96% 40.48%
US ITIN 9,218 71.45% 64.95% 68.05%
US SSN 33,873 45.16% 49.95% 47.43%

The dataset is mostly synthetic, English-only, and its global phone label was broader than the NANP-only phone configuration evaluated in v0.2.0. The new opt-in E.164 detector has not been scored into that frozen result. The results are useful independent coverage evidence, not an estimate for arbitrary production traffic. See EVALUATION.md for the pinned revision and checksum, full TP/FP/FN counts, methodology, limitations, and one-command reproduction.

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;
  6. detector ID, lexicographically ascending; and
  7. detector emission sequence, 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, match sinks, and transformer values are borrowed, call-scoped views. 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 validated 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.

The match sink stops built-in work at limit+1; low-confidence candidates are budgeted before filtering. Built-in and conforming-transformer output is sized before replacement allocation. Arbitrary custom transformers remain trusted and can allocate internally.

Streaming validation requires one full window to commit at least max(1, MaxStreamBufferBytes/8) new source bytes. The exact progress formula is buffer - maxLookbehind - max(MaxMatchBytes+LookaheadBytes) + 1. Configurations below this threshold are rejected to prevent near-full-window rescans for every source byte.

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.

Canceled operations still match their Incognigo category and the fixed context.Canceled or context.DeadlineExceeded sentinel. An extension cannot forge cancellation by merely returning one of those errors while the supplied context remains active.

Never return, write, enqueue, or log the original input after protection fails. This fallback is unsafe:

protected, err := engine.ProtectBytes(ctx, input)
if err != nil {
	protected = input // UNSAFE: turns a protection failure into disclosure.
}
_, _ = destination.Write(protected)

Fail closed or send an input-independent replacement instead:

protected, err := engine.ProtectBytes(ctx, input)
if err != nil {
	return fmt.Errorf("protect outbound data: %w", err)
}
if _, err := destination.Write(protected); err != nil {
	return fmt.Errorf("write protected data: %w", err)
}

The same rule applies to slog: do not retry a failed protecting handler with the unwrapped downstream handler. Standard slog.Logger calls do not return handler errors; a protecting handler still drops the record before downstream delivery. Applications that must observe logging failures should invoke or wrap the handler at an application-owned boundary and emit only input-independent health signals.

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 or an exact field policy replaces the complete value;
  • attribute/group keys are not transformed; opt-in strict inspection rejects PII-looking names, field policies and numeric protection can change downstream schemas, and strict profiles can reject other native values;
  • 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 '^TestConformanceCorpora$'
./scripts/check-detector-fuzz-coverage.sh
./scripts/generate-detector-coverage.sh --check
./scripts/evaluate-gravitee.sh evaluation.md
./scripts/fuzz.sh 30s
./scripts/api-diff.sh v0.1.0

The complete release gate checks formatting, module tidiness, the zero-dependency policy, vetting, ordinary and race tests, benchmark compilation, hot-path allocation and 25% same-runner median slowdown guards, conformance fixtures, every fuzz target, and pinned vulnerability analysis:

./scripts/release-check.sh 30s

compatibility-manifest.json records persisted token formats, PII and detector IDs, profile composition, algorithm/metadata versions, confidence order, overlap rules, and streaming progress. MIGRATING.md records adoption and rollout impact for detector behavior changes. Release-candidate CI retains a public API diff and raw benchmark output. Every release has a committed snapshot under benchmarks, and tag builds add a fresh checksummed platform snapshot beside the SHA-bound source archive and signed GitHub/Sigstore provenance evidence.

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.Inspect returns source-content-free classification and resolution metadata without transforming source bytes. 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. ContainsPII provides an early-exit, metadata-free check for validation boundaries such as strict log-key inspection.

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 ConfidenceBand added in v0.2.0

type ConfidenceBand uint8

ConfidenceBand is an ordinal evidence classification, not a calibrated probability and not an overlap priority.

const (
	// ConfidenceUnspecified uses the detector registration's default.
	ConfidenceUnspecified ConfidenceBand = iota
	ConfidenceWeak
	ConfidencePossible
	ConfidenceLikely
	ConfidenceStrong
)

func (ConfidenceBand) Valid added in v0.2.0

func (confidence ConfidenceBand) Valid() bool

Valid reports whether confidence is an explicit recognized band.

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, sink MatchSink) error
}

Detector finds candidate PII matches in input and emits them to sink.

Detect may be called concurrently with independent inputs and sinks. It must treat input as read-only, must not retain input or sink after returning, and must honor cancellation through ctx. Apart from cancellation or failure, emitted matches 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.

Emitted matches must use byte offsets, be within input, fit MaxMatchBytes, and use a type declared by the detector's registration. Detect must return after Add returns false; Incognigo still inspects the latched sink failure if a detector ignores the return value.

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

	// AlgorithmVersion is stable audit metadata. Empty selects "v1".
	AlgorithmVersion string

	// Locale is optional stable region or locale metadata, such as "global" or
	// "nanp". It uses the public identifier grammar.
	Locale string

	// DefaultConfidence applies when a Match leaves Confidence unspecified.
	// Zero selects ConfidenceStrong for compatibility with deterministic
	// pre-confidence detectors.
	DefaultConfidence ConfidenceBand

	// Evidence is fixed registration-level evidence metadata.
	Evidence EvidenceFlags
}

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,
	sink incognigo.MatchSink,
) error {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil
	}
	sink.Add(incognigo.Match{
		Type: "company.employee_id",
		Span: incognigo.Span{
			Start: start,
			End:   start + len(value),
		},
	})
	return 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) ContainsPII added in v0.3.0

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

ContainsPII reports whether any configured detector emits a valid candidate for input.

Unlike Inspect, ContainsPII stops at the first candidate and does not allocate result metadata or apply confidence thresholds and overlap resolution. It is intended for trust-boundary validation such as rejecting PII-looking log keys, where even a below-threshold candidate is unsafe.

ContainsPII never transforms, mutates, retains, or returns input. Detector and Incognigo failures return false with a source-safe error; callers must treat an error as an indeterminate protection failure rather than as "no PII".

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/Chadi00/incognigo"
)

type exampleDetector struct{}

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

	found, err := engine.ContainsPII(
		context.Background(),
		[]byte("dynamic-key-EMP-0042"),
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(found)
}

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) Inspect added in v0.2.0

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

Inspect detects configured candidates without transforming input.

Candidate budgeting happens before confidence filtering. Eligible candidates then undergo the same deterministic overlap resolution as ProtectBytes. The returned slice contains engine-owned metadata copies and never aliases input. On failure Inspect returns nil.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/Chadi00/incognigo"
)

type exampleDetector struct{}

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

	findings, err := engine.Inspect(
		context.Background(),
		[]byte("employee EMP-0042 signed in"),
	)
	if err != nil {
		panic(err)
	}
	for _, finding := range findings {
		fmt.Println(
			finding.Type,
			finding.Span.Start,
			finding.Span.End,
			finding.Outcome == incognigo.FindingSelected,
		)
	}
}

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:
company.employee_id 9 17 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,
	sink incognigo.MatchSink,
) error {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil
	}
	sink.Add(incognigo.Match{
		Type: "company.employee_id",
		Span: incognigo.Span{
			Start: start,
			End:   start + len(value),
		},
	})
	return 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,
	sink incognigo.MatchSink,
) error {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil
	}
	sink.Add(incognigo.Match{
		Type: "company.employee_id",
		Span: incognigo.Span{
			Start: start,
			End:   start + len(value),
		},
	})
	return 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,
	sink incognigo.MatchSink,
) error {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil
	}
	sink.Add(incognigo.Match{
		Type: "company.employee_id",
		Span: incognigo.Span{
			Start: start,
			End:   start + len(value),
		},
	})
	return 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,
	sink incognigo.MatchSink,
) error {
	const value = "EMP-0042"
	start := bytes.Index(input, []byte(value))
	if start < 0 {
		return nil
	}
	sink.Add(incognigo.Match{
		Type: "company.employee_id",
		Span: incognigo.Span{
			Start: start,
			End:   start + len(value),
		},
	})
	return 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) Is added in v0.2.0

func (e *Error) Is(target error) bool

Is matches the fixed Incognigo category sentinel and, for canceled operations, context.Canceled or context.DeadlineExceeded. Arbitrary extension errors are never retained or exposed.

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 EvidenceFlags added in v0.2.0

type EvidenceFlags uint16

EvidenceFlags describes fixed, non-sensitive reasons for a classification. Flags never contain source-derived text, hashes, or excerpts.

const (
	EvidenceShape EvidenceFlags = 1 << iota
	EvidenceBoundary
	EvidenceChecksum
	EvidenceScheme
	EvidenceContext
	EvidenceDictionary
	EvidenceModel
)

func (EvidenceFlags) Valid added in v0.2.0

func (evidence EvidenceFlags) Valid() bool

Valid reports whether evidence contains only recognized flags.

type Finding added in v0.2.0

type Finding struct {
	Type              PIIType
	Span              Span
	DetectorID        DetectorID
	AlgorithmVersion  string
	Locale            string
	Confidence        ConfidenceBand
	Evidence          EvidenceFlags
	MinimumConfidence ConfidenceBand
	Outcome           FindingOutcome
}

Finding is source-content-free inspection metadata tied to the exact input bytes supplied to Inspect.

It contains no copied match, context excerpt, source-derived hash, token, or arbitrary provider error. Finding metadata is still content-derived and must not automatically be treated as safe to log.

type FindingOutcome added in v0.2.0

type FindingOutcome uint8

FindingOutcome records the deterministic result of confidence filtering and overlap resolution.

const (
	FindingOutcomeUnknown FindingOutcome = iota
	FindingSelected
	FindingBelowThreshold
	FindingOverlapRejected
)

type Match

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

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

	// Confidence optionally overrides the detector registration's default
	// confidence for this match. Zero uses the registration default.
	Confidence ConfidenceBand

	// Evidence optionally augments the detector registration's fixed evidence
	// flags. It must contain only recognized flags.
	Evidence EvidenceFlags
}

Match is the non-sensitive metadata emitted 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 MatchSink added in v0.2.0

type MatchSink interface {
	Add(Match) bool
}

MatchSink accepts candidate metadata from a Detector.

Add returns false once the operation's match budget is exhausted, a match is invalid, or Detect has returned. A detector must stop emitting promptly when Add returns false. Incognigo validates and copies the metadata before Add returns; a sink is borrowed for one Detect call and must not be retained, called concurrently, or used by another goroutine.

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"
	PIITypeIBAN              PIIType = "iban"
	PIITypeMACAddress        PIIType = "mac_address"
	PIITypeUSSSN             PIIType = "us_ssn"
	PIITypeUSITIN            PIIType = "us_itin"
	PIITypeUSABARouting      PIIType = "us_aba_routing_number"
	PIITypeUSNPI             PIIType = "us_npi"
	PIITypeGBNINO            PIIType = "gb_nino"
	PIITypeGBNHSNumber       PIIType = "gb_nhs_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

	// MinimumConfidence filters candidates before overlap resolution. Zero
	// selects ConfidenceWeak, preserving eligibility for existing detectors.
	MinimumConfidence ConfidenceBand
}

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 SizedTransformer added in v0.2.0

type SizedTransformer interface {
	TransformedSize(piiType PIIType, valueBytes int) (int, error)
}

SizedTransformer is an optional conformance contract that lets the batch engine reject oversized output before Transform allocates a replacement.

TransformedSize must return the exact successful Transform output length for the supplied type and input byte length. It must not allocate in proportion to valueBytes. A negative size or an error fails closed. Implementations are immutable, concurrency-safe extensions subject to the same trust boundary as Transformer.

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
cmd
incognigo-benchguard command
Command incognigo-benchguard fails when established hot benchmarks exceed reviewed time or allocation regression limits.
Command incognigo-benchguard fails when established hot benchmarks exceed reviewed time or allocation regression limits.
incognigo-detector-coverage command
Command incognigo-detector-coverage generates the auditable built-in detector conformance-fixture inventory.
Command incognigo-detector-coverage generates the auditable built-in detector conformance-fixture inventory.
incognigo-evaluate command
Command incognigo-evaluate scores built-in detectors against an external JSONL corpus without retaining or printing source text.
Command incognigo-evaluate scores built-in detectors against an external JSONL corpus without retaining or printing source text.
Package detector provides Incognigo's curated, standard-library-only PII detectors.
Package detector provides Incognigo's curated, standard-library-only PII detectors.
gb
Package gb provides opt-in Great Britain structured-identifier detectors.
Package gb provides opt-in Great Britain structured-identifier detectors.
us
Package us provides opt-in United States structured-identifier detectors.
Package us provides opt-in United States structured-identifier detectors.
examples
http command
Command http demonstrates a small net/http service with fail-closed, trust-boundary-aware structured logging.
Command http demonstrates a small net/http service with fail-closed, trust-boundary-aware structured logging.
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