@link-foundation/sensitive-data-sanitizer 0.0.0-stage → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +578 -0
  2. package/LICENSE +24 -0
  3. package/README.md +184 -2
  4. package/bin/legacy-example-package-name.js +95 -0
  5. package/bin/sensitive-data-sanitizer.js +430 -0
  6. package/bin/stream.js +56 -0
  7. package/docs/API.md +336 -0
  8. package/docs/COVERAGE.md +136 -0
  9. package/docs/HISTORY-REMEDIATION.md +41 -0
  10. package/examples/combined-scanners.mjs +23 -0
  11. package/examples/presidio-bridge.py +32 -0
  12. package/examples/realistic-fakes.mjs +18 -0
  13. package/examples/sanitize-session.mjs +27 -0
  14. package/examples/shared-tool-logs.mjs +21 -0
  15. package/examples/stream-jsonl.mjs +17 -0
  16. package/package.json +124 -4
  17. package/src/adapters.js +204 -0
  18. package/src/arithmetic.js +11 -0
  19. package/src/confidence.js +65 -0
  20. package/src/context.js +424 -0
  21. package/src/credentials.js +54 -0
  22. package/src/detection.js +293 -0
  23. package/src/encoded.js +233 -0
  24. package/src/engines.js +348 -0
  25. package/src/entities.js +166 -0
  26. package/src/escape-spans.js +20 -0
  27. package/src/eslint-plugin.d.ts +10 -0
  28. package/src/eslint-plugin.js +193 -0
  29. package/src/external.js +169 -0
  30. package/src/fake-names.js +222 -0
  31. package/src/fake.js +524 -0
  32. package/src/files.js +131 -0
  33. package/src/gitleaks.js +194 -0
  34. package/src/history-rewrite.js +339 -0
  35. package/src/history.js +103 -0
  36. package/src/identity.js +274 -0
  37. package/src/index.d.ts +321 -0
  38. package/src/index.js +40 -0
  39. package/src/jsonl-stream.js +198 -0
  40. package/src/keywords.js +45 -0
  41. package/src/known.js +164 -0
  42. package/src/legacy.d.ts +3 -0
  43. package/src/legacy.js +2 -0
  44. package/src/locales.js +217 -0
  45. package/src/log-bytes.js +85 -0
  46. package/src/names.js +65 -0
  47. package/src/national-checks.js +129 -0
  48. package/src/national.js +128 -0
  49. package/src/outbound.js +28 -0
  50. package/src/personal-context.js +85 -0
  51. package/src/personal.js +248 -0
  52. package/src/projection.js +126 -0
  53. package/src/propagation.js +163 -0
  54. package/src/public-verifier.js +77 -0
  55. package/src/public.js +153 -0
  56. package/src/record-worker.js +20 -0
  57. package/src/rules.js +210 -0
  58. package/src/sanitizer.js +794 -0
  59. package/src/specimens.js +34 -0
  60. package/src/stream-identity.js +129 -0
  61. package/src/stream-worker.js +18 -0
  62. package/src/stream.js +449 -0
  63. package/src/structured.js +278 -0
  64. package/src/transforms.js +192 -0
  65. package/src/transliteration.js +75 -0
  66. package/src/utf8.js +60 -0
  67. package/src/vendor/entities/aws.json +42 -0
  68. package/src/vendor/entities/azure.json +182 -0
  69. package/src/vendor/entities/google.json +219 -0
  70. package/src/vendor/gitleaks/LICENSE +21 -0
  71. package/src/vendor/gitleaks/provenance.json +8 -0
  72. package/src/vendor/gitleaks/rules-0.json +268 -0
  73. package/src/vendor/gitleaks/rules-1.json +1794 -0
  74. package/src/vendor/gitleaks/rules-2.json +292 -0
  75. package/src/vendor/gitleaks/rules-3.json +263 -0
  76. package/src/vendor/gitleaks/rules-4.json +277 -0
  77. package/src/vendor/gitleaks/rules-5.json +154 -0
  78. package/src/worker-options.js +27 -0
package/docs/API.md ADDED
@@ -0,0 +1,336 @@
1
+ # Library and detector contract
2
+
3
+ Import the ESM root entry; complete declarations are in `src/index.d.ts`. Inputs are strings, offsets are UTF-16 code units, starts are zero-based, and ends are exclusive. Returned line and column numbers are one-based; columns also count UTF-16 units.
4
+
5
+ ## Methods
6
+
7
+ | Method | Behavior |
8
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
9
+ | `createSanitizer(options)` | Asynchronous native + required Secretlint + selected detector union |
10
+ | `engine.inspect(text)` | Detect, validate, deduplicate, apply public policy, and return metadata |
11
+ | `engine.sanitize(text)` | Detect, union overlapping spans, mask completely, verify every engine, return `{text, findings, redactions}` |
12
+ | `engine.sanitizeJsonl(text)` | Batched structured JSONL sanitization using the same options and required engines |
13
+ | `inspect(text, options)` | Synchronous native detection and caller findings |
14
+ | `sanitize(text, options)` | Synchronous native redaction and native residual verification |
15
+ | `redact(text, findings, options)` | Apply validated ranges without automatic detection/verification |
16
+ | `auditGitHistory(repository, options)` | Read-only audit of local reachable text objects |
17
+
18
+ `redactions` counts merged replacement spans. `findings` can contain several detector hits for one replacement. Findings have `{start, end, type, category, rule, line, column}`; no text, quote, match, entropy score, provider details, or raw diagnostic is copied. Debugging is off by default; `debug(event)` receives only `{event: 'detected', count}`.
19
+
20
+ ## Options
21
+
22
+ | Option | Default / meaning |
23
+ | ------------------ | --------------------------------------------------------------------------------------------- |
24
+ | `profile` | Off; `'publication'` sets a 0.3 personal threshold, identity masking and strict fake policy |
25
+ | `knownSecrets` | `[]`; exact literal credential values, regardless of length or format |
26
+ | `knownPersonal` | `[]`; `{type, value}` literals with Unicode case-insensitive matching |
27
+ | `publicEntities` | `[]`; exact PERSON/ORGANIZATION/EMAIL exception with HTTPS source and review date |
28
+ | `findings` | `[]`; validated detections for the original input, supplied by the caller |
29
+ | `detectors` | `[]`; additional required callbacks in the asynchronous engine |
30
+ | `secretlint` | `true` in the asynchronous engine |
31
+ | `decode` | `true`; bounded base64/base64url, hex and percent decoding |
32
+ | `paranoid` | `false`; entropy-based supplemental credential heuristic |
33
+ | `maxInputLength` | 33,554,432 UTF-16 units |
34
+ | `maxFindings` | 100,000 detector hits before deduplication |
35
+ | `minConfidence` | 0.5; personal findings below this score remain visible to inspect but are not replaced |
36
+ | `threshold` | Alias for minConfidence; both options must agree when supplied |
37
+ | `debug` | Optional trusted callback for counts only |
38
+ | `identityMask` | `false`; identity types keep 2+2 Unicode characters with fixed `***`, or redact below five |
39
+ | `fakeIdentity` | Off; `'specimen-and-synthetic'` retains explicitly fake identity values |
40
+ | `fakeValues` | `[]`; exact caller-reviewed identity values to retain, regardless of fakeIdentity |
41
+ | `structured` | Off; `'json'` or `'jsonl'` validates and sanitizes string keys/values separately |
42
+ | `structuralFields` | `[]`; declared property names suppress weak name/ID/entropy heuristics in their string values |
43
+
44
+ Unknown options and invalid limits are errors. `knownSecretsFromEnv(env, names?)` discovers credential-named variables when names are omitted, or uses an explicit selection, deduplicating nonempty values. It never invokes shell commands or reads auth files. `personalVariants(entries)` generates numeric and ISO/day-first date spellings; name case variants are already handled by detection. Neither helper invents transliterations.
45
+
46
+ ## Required detectors
47
+
48
+ A detector supplies `detect(text)`, returning an array of `{start, end, type, category, rule}` or a Promise of that array. Every range must refer to the exact input passed to that invocation. `category` is `credential` or `personal`; types are uppercase identifiers, rules are bounded lowercase identifiers. Throw if analysis is incomplete. The framework invokes engines on original input, normalized input when changed, bounded decoded views, and finally the sanitized output. Decoded matches redact the entire original encoded run. Malformed findings and engine errors block publication with generic error codes.
49
+
50
+ Callbacks, policies, binaries, model packages, and their rule identifiers are trusted code/configuration. A malicious adapter can omit findings, leak input through its own logging/network calls, or never resolve. Local subprocess helpers use 30-second termination and 10 MiB output caps; arbitrary callback time budgets belong to the embedding application. Pin binary/model versions in deployments. Do not convert a failed scan into an empty result.
51
+
52
+ ## Report adapters
53
+
54
+ - `fromGitleaks(text, report)` matches every literal `Secret` occurrence and supported decoded representations.
55
+ - `fromTrufflehog(text, report)` maps `Raw`/`RawV2` to every literal or bounded decoded occurrence. Unmapped raw values are errors.
56
+ - `fromDetectSecrets(text, baseline, filename)` redacts each flagged complete line because hashed baselines contain no recoverable secret span.
57
+ - `fromPresidio(text, results)` converts Python Unicode codepoint ranges, mapping standard PII types and credential passwords.
58
+ - `fromOffsetReport(text, entries, {unit, rule})` accepts explicitly declared `utf16`, `codepoint`, or UTF-8 `byte` ranges. Entries supply category/type. Unknown units and partial UTF-8 character boundaries fail.
59
+
60
+ `codePointRange()` and `byteRange()` expose the same conversions. Extra provider fields are discarded. Reject incomplete/paginated/truncated reports in the caller before adaptation.
61
+
62
+ For [Google findings](https://docs.cloud.google.com/java/docs/reference/google-cloud-dlp/latest/com.google.privacy.dlp.v2.Location), convert `location.byteRange` numeric strings to numbers and use `unit: 'byte'`; map infoTypes to appropriate category/type. Use only a report for the exact single text item, not offsets relative to a table cell. For [Azure](https://learn.microsoft.com/en-us/rest/api/language/analyze-text/analyze-text/analyze-text?view=rest-language-analyze-text-2026-05-01), request `stringIndexType: 'Utf16CodeUnit'` and use `offset` through `offset + length` with `unit: 'utf16'`. AWS's [PII entity documentation](https://docs.aws.amazon.com/comprehend/latest/APIReference/API_PiiEntity.html) describes character offsets; establish non-ASCII/end-boundary behavior for the SDK/report version before choosing an explicit unit. There is no guessed cloud offset default.
63
+
64
+ The native package does not send text to these services. An application that chooses remote detection controls its own consent, regions, credentials, and retention.
65
+
66
+ ## Transformations and confidence
67
+
68
+ Findings also carry `confidence` (0–1) and `likelihood` (`VERY_UNLIKELY` through `VERY_LIKELY`). Native scores describe rule strength; they are not probabilities calibrated across engines. External detectors can supply their own validated scores.
69
+
70
+ Bare checksum coincidences generally score 0.35. Identity labels raise confidence; specific national structures can supply stronger evidence independently. Timestamp/elapsed metadata and plausible epoch numbers are negative evidence. `minConfidence` defaults to 0.5, applies to personal findings in every detection/rendering/verification path, and never weakens credential protection. Set it to 0.3 to replace bare checksum matches. Generic calendar dates remain unchanged unless personal/birth context identifies them.
71
+
72
+ `profile: 'publication'` sets `minConfidence: 0.3`, `identityMask: true` and
73
+ `fakeIdentity: false` unless explicitly overridden. It retains required engines,
74
+ decoding, input limits and full-redaction residual verification. Both synchronous
75
+ and async APIs accept it; CLI `redact` uses it by default. Set `identityMask: false`
76
+ for complete identity replacement. A supplied `threshold` is honored, and
77
+ conflicting threshold aliases still fail configuration validation. Formatted
78
+ checksum-valid SNILS and 12-digit personal INN score 0.99 even without this
79
+ profile; bare 10-digit organizational INN remains 0.35. See the effective
80
+ [national-ID defaults](COVERAGE.md#national-id-defaults).
81
+
82
+ Without a profile, full redaction is the library default. `transformation` selects a global mode; `transformations` maps finding types to overrides. Credentials receive full redaction except for explicit `hive-mask` or `fake` with `credentials: true`:
83
+
84
+ | Mode | Configuration and behavior |
85
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------- |
86
+ | `redact` | Complete `[REDACTED]` replacement |
87
+ | `hive-mask` | First/last three Unicode characters with `…` for values longer than 12; shorter values remain opaque |
88
+ | `mask` | Personal data only; `keepStart`/`keepEnd` reveal a bounded prefix/suffix and mask the rest |
89
+ | `pseudonym` | Personal data only; keyed HMAC-SHA256, type-separated deterministic opaque identifier |
90
+ | `format-preserving` | Personal data only; keyed deterministic character-class pseudonym, preserving punctuation and ASCII letter/digit shape |
91
+ | `fake` | Keyed realistic names, valid dates/MRZ/checksummed numbers and reserved contact destinations |
92
+ | `date-shift` | Personal ISO dates; finite integer `days`, UTC calendar arithmetic; invalid dates become opaque |
93
+ | `bucket` | Personal safe integers; positive integer `size`, inclusive numeric interval |
94
+
95
+ All keyed modes require a key of at least 16 UTF-8 bytes. `format-preserving` is a non-reversible shape pseudonym, not NIST FF1 encryption; it does not preserve checksum validity, country-specific alphabets or guarantee uniqueness. Partial masks, date shifts and buckets intentionally retain information. Keep pseudonym keys outside logs. Residual verification checks the corresponding fully redacted representation; opted-in disclosures can remain recognizable in the transformed output. Use default redaction for publication that requires opaque output.
96
+
97
+ `{mode: 'fake', key}` infers name locale, script and gender from the supported gazetteer/datasets and recognizable surname/patronymic endings. Independent HMAC-seeded Faker instances make results independent of processing order. Russian Cyrillic/transliterated spelling, name order and filename separators share component identities. Ambiguous names can use a fallback locale; this is not universal name/gender inference. Dates use one keyed backward offset of 1–365 days and preserve ISO, slash and dot formats. Complete TD1/TD2/TD3 MRZs regenerate individual/composite checks and match separately faked name/date fields. Issuer/nationality remain original unless `mrzCountry: 'UTO'` is supplied. Unsupported/damaged formats fall back to full redaction.
98
+
99
+ Implemented national checksums, IBANs and payment-card Luhn checks remain valid. Phones retain calling code, length and separators; NANP uses 555-0100–0199 and UK mobile uses 07700 900000–900999. Email/domain outputs use example.com/.test. Other national identities and phone numbers have no universal reserved range: generated arithmetic does not prove that a value is unassigned.
100
+
101
+ Generated replacements carry `faked: true`. A private bounded registry recognizes values produced by the same transformation context as `kept: 'fake'` during later inspection/verification. Caller-supplied flags and arbitrary FAKE-looking strings grant no exemption. A fresh sanitizer does not inherit that registry; reviewed persisted fake identity values can be configured through `fakeValues`. Credentials remain fully redacted unless `credentials: true` requests provider-shaped values containing FAKE and punctuation that violates token grammar. Generated personal values cannot exempt credential spans.
102
+
103
+ Faking is pseudonymisation: a private key and retained ages, relations and structure can permit linking. Keep the key out of published data. See [the realistic fake example](../examples/realistic-fakes.mjs) and [the complete requirement/research record](case-studies/issue-24/REQUIREMENTS.md).
104
+
105
+ `preserveEncoding: true` sanitizes decoded payloads with the same required engine union, re-encodes them and verifies decoding reproduces the sanitized content. Supported formats include base64/base64url, hex, percent, numeric HTML and UTF-8 byte escapes. Escaped JSON string content is sanitized and re-encoded even under default redaction so nested session JSON stays parseable. Malformed or unsupported encodings block opted-in preservation. Known encoding candidates and nesting are bounded.
106
+
107
+ ## Identity policy and structured sessions
108
+
109
+ `identityMask: true` applies `{mode: 'mask', keepStart: 2, keepEnd: 2,
110
+ marker: '***', minLength: 5}` to document/travel/national-ID types, including
111
+ catalog passport types. Explicit per-type/global transformations take precedence.
112
+ `mask.marker` is an optional fixed string; omitting it preserves length-based
113
+ asterisks. `mask.minLength` defaults to five for identity types and zero for other
114
+ types. Length and retained boundaries count Unicode characters.
115
+
116
+ `fakeIdentity: 'specimen-and-synthetic'` recognizes ICAO Utopia/Eriksson specimens,
117
+ national specimen markers, repeated document digits and at least six consecutive
118
+ ascending/descending document digits. `fakeValues` supplies exact reviewed
119
+ identity values independently of that option. Kept findings carry `kept: 'fake'`
120
+ and do not count as redactions. The same value check applies during verification.
121
+ Words such as test/example/fake do not grant exemptions. Credentials from any
122
+ engine and overlapping known secrets always win. Both policies are opt-in.
123
+
124
+ ```js
125
+ const sessionSanitizer = createSanitizer({
126
+ structured: 'jsonl',
127
+ structuralFields: ['id', 'type', 'name', 'description'],
128
+ });
129
+ const result = await sessionSanitizer.sanitizeJsonl(sessionText);
130
+ ```
131
+
132
+ Structured mode scans string keys and values in bounded batches, supplies field
133
+ context, retains original source offsets/formatting, and verifies parseable
134
+ output. Numbers, booleans and null remain their original types. Declarations
135
+ apply to property names at every nesting level, including string array elements
136
+ inside that property. Declare only fields your application treats as metadata:
137
+ `name` may also contain a person's name. Known private values, explicit provider
138
+ credentials, password/auth context, document formats and other strong findings
139
+ remain protected in declared fields. Custom required detectors still run.
140
+ Escaped nested JSON retains encoding. Invalid JSON and key collisions block
141
+ publication with `ERR_JSON` / `ERR_JSON_COLLISION`.
142
+
143
+ Inferred personal values propagate only inside JSON string values. They never
144
+ rewrite keys, numbers, booleans, null or structural punctuation. Direct findings
145
+ on sensitive non-identifier keys and explicitly known private keys remain
146
+ protected. Personal name labels require plausible name words; lowercase code
147
+ identifiers, product/tool words and punctuation do not establish a person.
148
+ Place labels require natural place text rather than code expressions or lowercase
149
+ status identifiers. Explicit `knownPersonal` values override these heuristics.
150
+
151
+ Oversized encoded runs receive a conservative complete-run `ENCODED_LIMIT`
152
+ redaction rather than aborting the record. Decoding probes are capped at 4,096
153
+ characters; full decoding stays capped at 8,192 characters. Whole-input,
154
+ candidate-count, record and worker limits still apply. This fallback can remove
155
+ large non-sensitive encoded outputs; its finding makes that decision auditable.
156
+
157
+ ## Streaming and worker publication
158
+
159
+ `sanitizeStream(source, options)` accepts iterable bytes/strings and yields sanitized records. It holds incomplete lines, quoted multiline values, PEM/PuTTY blocks, netrc triplets and wrapped base64 groups across chunks. Defaults: 256 KiB batch and 1 GiB total input. Plain-text records inherit the total input limit unless `maxRecordBytes` is set explicitly; JSONL retains its 8 MiB record default. A record exceeding an explicit `maxRecordBytes` fails. Incomplete records are held whole, so memory grows with the largest held record. Boundary scans occur at geometrically increasing sizes to avoid repeated scans of long lines. The generator may have yielded earlier verified records when a later record fails. Use `sanitizeStreamToFile` for atomic publication of the whole stream.
160
+
161
+ For sessions above the whole-input limit, use `sanitizeStream(source, {structured: 'jsonl'})` alongside `engine.sanitizeJsonl(text)`. JSONL defaults to 8 MiB per record, supports arbitrary UTF-8 chunk boundaries and CRLF, and scans bounded record batches with two persistent workers in input order. `workers` selects 1–16; each worker has a 60-second job deadline. Node requests a 256 MiB old-generation heap and a 4 MiB stack; parent V8 sizing flags can override heap limits. Bun ignores worker resourceLimits, so its byte/queue limits and deadlines still apply but its worker heap is not capped. Worker failures and consumer cancellation terminate all workers. `workers: 1` runs in process; callbacks/custom sanitizers default to one and cannot be combined with parallel workers. Deno uses its in-process fallback because its Node compatibility lacks resource-limited worker threads.
162
+
163
+ Each stream carries confirmed names, MRZ components and strong personal findings
164
+ into later batches, including bounded Cyrillic/passport/GOST spelling aliases.
165
+ Workers receive ordered private registry snapshots. `maxRegistryValues` defaults
166
+ to 100,000 combined confirmed aliases and prior-token digests; exhaustion fails
167
+ with `ERR_LIMIT`. Prior unredacted word tokens and weak inspected identity spans
168
+ are retained as per-stream keyed HMAC digests, not plaintext history. Repeated
169
+ tokens are deduplicated before hashing. A late confirmation matching this history
170
+ fails with `ERR_LATE_PERSONAL`; this conservative component check can block
171
+ ordinary words reused in a later personal name, username or address. Use `sanitizeStreamToFile` or CLI streaming for
172
+ atomic publication: an unfinished generator can already have yielded earlier
173
+ records and must not be published directly. Registries are never shared between
174
+ streams. `knownPersonal` remains available for caller-confirmed private values;
175
+ `confirmedPersonal` supplies boundary-matched initial values when needed.
176
+
177
+ ```js
178
+ import { createReadStream } from 'node:fs';
179
+ import { sanitizeStreamToFile } from '@link-foundation/sensitive-data-sanitizer';
180
+ await sanitizeStreamToFile(
181
+ createReadStream('large-session.jsonl'),
182
+ 'safe.jsonl',
183
+ {
184
+ profile: 'publication',
185
+ structured: 'jsonl',
186
+ workers: 2,
187
+ }
188
+ );
189
+ ```
190
+
191
+ `sanitizeFileToFile` rejects source symlinks and writes private temporary output, publishing only after complete verification. `sanitizeFileBounded` additionally uses a worker with a 60-second deadline and requests a 256 MiB old-generation heap and 4 MiB stack on Node (Bun ignores those resourceLimits); `workerHeapMb` and `workerTimeoutMs` are configurable. Worker failure removes its private staging directory before returning. Limits bound package work, not arbitrary external detectors. Callbacks supplied through `sanitizer` run in process; set `worker: false` explicitly to disable isolation. Pass serializable options through `sanitizerOptions` for workers. Memory limits do not include all native/WASM allocation.
192
+
193
+ ```js
194
+ import { sanitizeFileBounded } from '@link-foundation/sensitive-data-sanitizer';
195
+ await sanitizeFileBounded('session.jsonl', 'session.safe.jsonl', {
196
+ profile: 'publication',
197
+ maxTotalBytes: 1024 * 1024 * 1024,
198
+ maxRecordBytes: 1024 * 1024,
199
+ });
200
+ ```
201
+
202
+ The CLI supports `redact FILE --stream --output NEWFILE` and stdin streaming. Stdout uses a private spool and receives nothing until the entire scan succeeds. `--max-record-bytes` controls held records; `--max-bytes` defaults to 1 GiB with `--stream`. `--json` / `--jsonl`, `--hive-mask`, `--preserve-encoding` and `--gh-auth` are explicit opt-ins. CLI redact uses the publication profile and identity masking by default; configure `identityMask: false` for full redaction.
203
+
204
+ ## Public verification and publication helpers
205
+
206
+ Built-in Wikidata names/organizations, resolver/documentation IPs and role emails on known organization domains are enabled by default; `publicKnowledge: false` disables those heuristics. `knownPersonal` forces private treatment. `publicEntities` remains an audited exact override; credentials always outrank exemptions.
207
+
208
+ `createWikidataVerifier(options)` provides optional asynchronous `verifyPublic`; the CLI enables it only with `--verify-public`. It sends eligible PERSON/ORGANIZATION names to Wikidata, requires an exact label and a public-role description, caches bounded results, and blocks output on network failure. Name collisions remain possible: this is a public-name heuristic, not proof of a person's identity. Explicit private literals and sensitive relationship contexts prevent automatic exemptions. Offline mode makes no network requests.
209
+
210
+ `knownSecretsFromGitHubAuth({command, hostname, timeoutMs})` explicitly invokes local `gh auth token` and returns its nonempty exact value without diagnostics. It is never called automatically; failed authentication blocks opted-in scans. `knownSecretsFromEnv` remains separate and reads credential-named environment values or an explicit name selection.
211
+
212
+ `sanitizePayload(payload, {sanitizer})` sanitizes JSON payload values with the required union while retaining JSON structure. `createSentryBeforeSend(options)` returns a Sentry hook that drops the event (`null`) if sanitization fails. `createOutboundSanitizer(send, options)` calls the supplied transport only after successful sanitization. These helpers do not send data themselves.
213
+
214
+ The optional ESLint plugin exports `require-sanitized-output` from the package's `eslint-plugin` subpath. It checks configured outbound sinks and common GitHub CLI body/title arguments, including `--body-file`, templates and argv arrays. Configure `sinks` and `sanitizers` for your application. It is a conservative syntactic aid, not a complete data-flow proof; runtime verification remains required.
215
+
216
+ ## Native documented entity catalogs
217
+
218
+ `entityCatalogs` exposes source URLs, source-page SHA256 and documented names for Google (213), Azure (176) and AWS (36). Their canonical uppercase underscore labels are recognized natively, with complete value spans, independent of cloud services. The package also implements selected automatic formats and checksum algorithms. Catalog vocabulary support does not reproduce proprietary NER, image/document inspection, regional identity validators, cloud risk analysis or their accuracy. See the coverage table for that distinction.
219
+
220
+ ## Two-pass session publication
221
+
222
+ Pass `twoPass: true` to `sanitizeStreamToFile`, `sanitizeFileToFile` or
223
+ `sanitizeFileBounded` when personal values may be confirmed after their first
224
+ appearance. Pass 1 reads the source once into a private temporary input file and
225
+ collects confirmed values with every configured detector. Pass 2 reads that
226
+ snapshot and redacts with the complete registry, including in parallel JSONL
227
+ workers. Publication is atomic; success and failure remove the input spool and
228
+ unfinished output. Existing targets remain intact on failure. Raw input is
229
+ stored with mode `0600`; budget disk space for both input and sanitized output.
230
+
231
+ ```js
232
+ await sanitizeStreamToFile(createReadStream('session.jsonl'), 'safe.jsonl', {
233
+ structured: 'jsonl',
234
+ profile: 'publication',
235
+ twoPass: true,
236
+ workers: 4,
237
+ });
238
+ ```
239
+
240
+ The default remains one pass. `twoPass` is supported only by atomic file APIs;
241
+ `sanitizeStream` rejects it with `ERR_CONFIG`. The CLI equivalent is
242
+ `redact session.jsonl --stream --jsonl --two-pass --output safe.jsonl`, including
243
+ stdin and `--in-place`. Record, input-byte and registry bounds still apply;
244
+ discovery retains confirmed values only and does not retain prior-token history.
245
+ Required detectors run during collection and final publication verification.
246
+
247
+ Confirmation values are trimmed before normalization in both passes. Registry
248
+ matches use the `confirmed-personal` finding rule and do not seed new values
249
+ from their matched spans. Pass 2 redacts any additional confirmation without
250
+ raising `ERR_LATE_PERSONAL`; it still enforces registry limits and residual
251
+ verification. Optional `debug` reports a `two-pass-confirmation` event containing
252
+ only the entity type when that happens. One-pass streaming retains its late
253
+ confirmation guard.
254
+
255
+ Personal label values stop at escaped line breaks and quotes and are capped at
256
+ 512 UTF-16 units. Phone and email labels require matching value shapes. Inferred
257
+ propagation seeds are capped at 256 units; longer detected spans are still
258
+ redacted but do not enter the propagation registry. `debug` reports a
259
+ `propagation-skipped` event containing only the type and reason when this happens.
260
+
261
+ Credential label values stay inside their logical line and enclosing JSON
262
+ string: they stop at escaped line breaks, escape sequences and the closing quote,
263
+ and never continue into the next JSON key. A qualifier such as `token for
264
+ staging:` needs a separator on the same line, verbs such as `is` must be whole
265
+ words, and generic labels such as `token`, `sig` or `secret` followed by a verb
266
+ need a generated-looking value, so prose such as `the token is valid` remains
267
+ text. A credential covering a whole escaped JSON run is redacted as one value.
268
+
269
+ Residual verification redacts the fully masked output again when any engine
270
+ still finds an actionable value, for example when raw and decoded layers
271
+ disagree about a span. Up to three additional rounds run; each reports a
272
+ `residual-redaction` debug event containing only the round number. Output that
273
+ does not converge, or whose structure becomes invalid, still fails closed with
274
+ `ERR_RESIDUAL` or the matching structured error.
275
+ Ordinary identifier keys retain their spelling unless explicitly configured as
276
+ private. Document names require a filename extension or path plus name-shaped
277
+ words or a known given name. Known CI service accounts in home paths and the
278
+ GitHub runner workspace are preserved; caller-confirmed private values retain
279
+ precedence.
280
+
281
+ Valid JSON lone surrogate escapes (such as `\ud83d`) remain escaped and round-trip
282
+ through redaction alongside emoji. Malformed UTF-8 bytes in structured JSONL still block publication
283
+ with `ERR_ENCODING`. For JSONL streams, the error includes a one-based `record`,
284
+ a zero-based absolute byte `offset`, and a zero-based `recordOffset`, identifying
285
+ the start of the malformed sequence without exposing its contents. These
286
+ positions survive bounded file workers and appear in CLI diagnostics.
287
+
288
+ ## Shared tool logs and byte preservation
289
+
290
+ `credentialsOnly: true` filters all personal findings, including supplied,
291
+ known, inferred, decoded, external-detector and structured findings. Credential
292
+ findings retain the normal engine union, confidence rules and verification.
293
+ This keeps EMAIL/account-ID values without per-value `publicEntities` entries.
294
+ The default policy continues to protect personal data.
295
+
296
+ `knownSecretsFromEnv(env = process.env, names?)` discovers nonempty exact values
297
+ when `names` is omitted, using
298
+ `/(?:^|_)(?:TOKEN|(?:API|ACCESS|PRIVATE)?KEY|SECRET|PASSWORD|PASSWD|CREDENTIALS?|AUTH|PAT)(?:_|$)/i`.
299
+ This covers `MY_SERVICE_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `CODEX_API_KEY`,
300
+ `OPENCODE_API_KEY`, `DASHSCOPE_API_KEY`, `HF_TOKEN` and `TELEGRAM_BOT_TOKEN`.
301
+ An explicit array keeps the previous selection behavior; `[]` selects nothing.
302
+ These values are loaded only when the helper or CLI `--env-secrets` is requested.
303
+
304
+ `await knownSecretsFromGitHubConfig({path?})` reads `oauth_token` and `api_token`
305
+ strings from every host/account in a local YAML file. The default follows gh's
306
+ `GH_CONFIG_DIR`, `XDG_CONFIG_HOME`, Windows `AppData`, then `~/.config/gh`
307
+ precedence and appends `hosts.yml`. A missing file returns `[]`; malformed YAML,
308
+ symlinks, aliases, files above 1 MiB and read failures produce value-free
309
+ `ERR_AUTH`. YAML quoting is parsed with the `yaml` package, with alias expansion
310
+ disabled. CLI `--gh-config` adds these exact values to `knownSecrets`.
311
+ `knownSecretsFromGitHubAuth` / `--gh-auth` remain available independently.
312
+
313
+ Plain log streams and CLI file/stdin/output/in-place paths preserve invalid
314
+ UTF-8 bytes exactly. Valid Unicode keeps its normal meaning for detection.
315
+ String APIs can use a reversible view:
316
+
317
+ ```js
318
+ const view = decodeLogBytes(inputBytes);
319
+ const result = await createSanitizer({ credentialsOnly: true }).sanitize(view);
320
+ const outputBytes = encodeLogText(result.text);
321
+ ```
322
+
323
+ `decodeLogBytes(Uint8Array)` represents each invalid byte with an unpaired
324
+ U+DC80–U+DCFF surrogate; `encodeLogText(string)` restores those bytes. Use the
325
+ encoder when collecting `sanitizeStream` string output containing such bytes.
326
+ Ordinary UTF-8 string encoding would replace them and lose information.
327
+ `sanitizeStreamToFile` and `sanitizeFileBounded` perform the encoding themselves,
328
+ including workers and both passes. NUL bytes still identify binary input and
329
+ block publication. JSON/JSONL, scanner protocol files and configuration retain
330
+ strict UTF-8 validation; malformed bytes cannot represent valid JSON text.
331
+
332
+ Default detection keeps RFC 1918, loopback, link-local and IPv6 local addresses,
333
+ recognized auth-mode metadata, npm dependency version declarations, Actions
334
+ permission values and literal credential references. `publicKnowledge: false`
335
+ restores IP detection; explicit `knownSecrets` and `knownPersonal` retain
336
+ priority. No shell command or workflow expression is executed.
@@ -0,0 +1,136 @@
1
+ # Detection coverage and limits
2
+
3
+ Coverage is a union of maintained detectors, explicit known values, context and conservative native rules. Detection is heuristic; there is no claim of perfect recall, universal anonymity, or superiority over every competitor.
4
+
5
+ ## Credential coverage
6
+
7
+ The native provider supplements in `src/rules.js` cover GitHub, GitLab, OpenAI, Anthropic, AWS access IDs, Google API/OAuth, Slack, Stripe, SendGrid, Twilio, npm, PyPI, Telegram, Discord, Hugging Face, Shopify, Databricks, Square, Mailchimp, DigitalOcean, Vault, Grafana, Linear, Notion, Vercel, and JWT-shaped values. Prefix matches also protect selected truncated tokens; they do not validate a token with a provider.
8
+
9
+ The default native engine bundles 221 text rules from Gitleaks 8.30.1 (222 upstream rules; the path-only PKCS12 rule is outside text detection), compiled unchanged with RE2/WASM or RE2JS for inputs above 4 MiB. The MIT license, commit and sync script are retained in `src/vendor/gitleaks`. Keyword prefilters and configured entropy thresholds run natively; upstream comment/path allowlists cannot suppress a sanitizer finding. Quoted hex/base64 entropy detection supplements those rules. The default asynchronous engine additionally uses the pinned Secretlint recommended scanners, excluding its comment-filter rule. Embedded disable directives in untrusted text cannot suppress findings. Optional Gitleaks and TruffleHog invoke their installed detector sets, and custom detectors can add formats. Service counts are not a measure of comparative recall.
10
+
11
+ Context rules recognize credential labels with bounded key affixes, JSON and nested escaped JSON, YAML/env/INI assignments, shell flags, XML values, URL userinfo, authorization headers, cookies, webhook paths, and complete or truncated private-key PEM blocks. Low-entropy contextual values such as `password=1234` are protected. Generic token counters, booleans/null/language literals and ordinary Git hashes are excluded to retain useful logs. Supply literal known values for ambiguous credentials or unusual key names.
12
+
13
+ Broader unquoted entropy scanning is opt-in (`paranoid: true`), uses Shannon entropy ≥4.2 on bounded candidates, and exempts ordinary hex hashes and common tool identifiers. Quoted hex/base64 and entropy checks belonging to bundled provider rules run by default. Entropy has both false positives and false negatives; contextual detection and known-secret dictionaries remain necessary.
14
+
15
+ ## Personal data and languages
16
+
17
+ Native rules cover international/Unicode emails, international and selected national-format phones, Luhn-valid payment cards, US SSNs, IPv4/IPv6, checksum-validated IBAN/CPF/Spanish DNI/US NPI, Indian PAN, UK NI, wallet shapes, MACs, messenger handles, home usernames, labelled PII and selected postal address grammars. A bundled 17-locale name gazetteer supplies default free-text detection, including selected inflections/transliterations. Each locale has credential prose and eight labelled PII fixtures. This finite dictionary/grammar is not unrestricted statistical NER; unfamiliar names and unsupported national formats require known values or additional recognizers.
18
+
19
+ Credential labels have fixtures for 17 language variants:
20
+
21
+ | Language | Example credential label |
22
+ | ------------------- | ------------------------ |
23
+ | English | password |
24
+ | Portuguese | senha |
25
+ | Spanish | contraseña |
26
+ | French | mot_de_passe |
27
+ | German | passwort |
28
+ | Russian | пароль |
29
+ | Simplified Chinese | 密码 |
30
+ | Traditional Chinese | 密碼 |
31
+ | Japanese | パスワード |
32
+ | Korean | 비밀번호 |
33
+ | Arabic | كلمة المرور |
34
+ | Hebrew | סיסמה |
35
+ | Hindi | पासवर्ड |
36
+ | Turkish | şifre |
37
+ | Indonesian | kata_sandi |
38
+ | Vietnamese | mật_khẩu |
39
+ | Thai | รหัสผ่าน |
40
+
41
+ These are tested label patterns, not 17 complete NER language models. `src/locales.js` and `src/rules.js` define the exact vocabulary. Unicode NFKC, combining marks, common invisible/bidi controls, ANSI escapes and JSON Unicode escapes are projected to a detection view with original offset mapping. Original delimiters/content outside redactions remain intact. NFKC does not equate every homoglyph or transliteration.
42
+
43
+ The optional [Presidio bridge](../examples/presidio-bridge.py) accepts `--language` and `--model` for separately installed spaCy models. [Presidio's multilingual guide](https://presidio.dataprivacystack.org/tutorial/05_languages/) explains that models and recognizers must be configured per language. Installing one English model does not provide every language. There is no runtime model download. Offline public classification uses bundled sourced entries and documented role/domain heuristics. Callers can add reviewed exact policies or explicitly enable Wikidata verification; known-private values and credential overlaps always retain private treatment.
44
+
45
+ ## Identity and additional credential formats
46
+
47
+ Native MRZ extraction covers TD1/TD2/TD3 and MRV-A/MRV-B, including standalone
48
+ name/data fragments and bounded whitespace reflow. Check digits raise confidence;
49
+ invalid checks do not allow MRZ through. Country passport shapes include Russian
50
+ domestic/international, US/UK, German, Indian, Korean and Italian documents.
51
+ Ambiguous bare nine-digit values receive lower confidence and redact under the publication profile.
52
+ Travel labels protect PNR/booking references, 13-digit tickets and visa numbers.
53
+ Birth-date vocabulary supports numeric and English/Russian month-name dates.
54
+
55
+ Native country types: UK_NHS, KR_RRN, IT_FISCAL_CODE, SG_NRIC_FIN, PL_PESEL,
56
+ SE_PERSONNUMMER, ZA_ID_NUMBER, AU_TFN, CA_SIN, RU_SNILS and RU_INN. Checks validate
57
+ format arithmetic, not issuance; modern Korean RRNs may use randomized suffixes,
58
+ so damaged/non-legacy checks still receive contextual or shape coverage. Bare
59
+ checksum-only numeric formats receive low confidence (usually 0.35) and are
60
+ inspectable but unchanged under the default 0.5 personal threshold. Specific
61
+ structures and identity/health context supply stronger evidence. Card detection
62
+ requires a supported network prefix and length as well as Luhn; timestamp
63
+ contexts and epoch-shaped numbers are negatives.
64
+ Messenger/call context adds RU/VN/TH/ID national trunk-prefix phone shapes.
65
+
66
+ Confirmed names and strong personal values propagate within a document and
67
+ across later streaming batches, including filename/path occurrences and bounded
68
+ Cyrillic/ICAO/GOST/common spelling aliases. Russian patronymics, capitalized or
69
+ all-caps Slavic surname pairs in either order, and two/three-part names adjacent
70
+ to document filename words supplement the gazetteer. Late confirmations and
71
+ registry exhaustion block completed stream publication; see [API.md](API.md).
72
+
73
+ ### National-ID defaults
74
+
75
+ Scores below apply to valid reported shapes outside timestamp/order metadata.
76
+ Default API personal threshold is 0.5; publication profile/CLI redact use 0.3.
77
+ Identity/health labels generally raise checksum-valid scores to 0.99 and damaged
78
+ checks to 0.9. These scores indicate rule strength, not issuance or probability.
79
+
80
+ | Type/shape | Bare score | API default | Publication profile |
81
+ | --------------------------------------------- | ---------- | ------------ | ------------------- |
82
+ | UK_NHS, spaced or compact | 0.35 | Inspect only | Replace/mask |
83
+ | KR_RRN, plausible date and checksum | 0.99 | Replace | Replace/mask |
84
+ | IT_FISCAL_CODE, alphanumeric structure | 0.99 | Replace | Replace/mask |
85
+ | SG_NRIC_FIN, prefix and check letter | 0.99 | Replace | Replace/mask |
86
+ | PL_PESEL, valid checksum | 0.35 | Inspect only | Replace/mask |
87
+ | SE_PERSONNUMMER, valid formatted checksum | 0.35 | Inspect only | Replace/mask |
88
+ | ZA_ID_NUMBER, valid checksum | 0.35 | Inspect only | Replace/mask |
89
+ | AU_TFN, valid spaced/compact checksum | 0.35 | Inspect only | Replace/mask |
90
+ | CA_SIN, valid spaced/compact checksum | 0.35 | Inspect only | Replace/mask |
91
+ | RU_SNILS, `ddd-ddd-ddd dd` / `ddd ddd ddd dd` | 0.99 | Replace | Replace/mask |
92
+ | RU_SNILS, compact checksum only | 0.35 | Inspect only | Replace/mask |
93
+ | RU_INN, 12 digits/two checks | 0.99 | Replace | Replace/mask |
94
+ | RU_INN, 10 digits/one check | 0.35 | Inspect only | Replace/mask |
95
+
96
+ Russian SNILS contexts include СНИЛС, пенсионное, страховое свидетельство and
97
+ страховой номер. English passport contexts include series and number, passport
98
+ series and document number. Unlabelled Russian passport shapes retain their
99
+ 0.4 score and are protected by the publication profile. Numeric-noise exclusions
100
+ continue to preserve timestamps, order IDs and routine tool identifiers.
101
+
102
+ Credential rules add complete netrc triplets, NuGet oy2 keys, all eight WordPress
103
+ key/salt constants and complete/truncated PuTTY v2/v3 private blocks. Streaming
104
+ holds netrc/PuTTY blocks across chunks. Placeholder exclusions do not override
105
+ known secrets. [The requirement matrix](case-studies/repository-issue-13/REQUIREMENTS.md)
106
+ records standards, alternatives and regression coverage for each feature.
107
+
108
+ ## Bounds and unsupported inputs
109
+
110
+ | Boundary | Behavior |
111
+ | --------------------- | ----------------------------------------------------------------------------------------------------- |
112
+ | Input | 32 MiB CLI bytes / 32 Mi UTF-16 library units by default; configurable |
113
+ | Normalized projection | At most 18 times input length for Unicode normalization expansion; input limits apply |
114
+ | Encoded candidates | 8,192 raw characters per decoded run; audited redaction for longer runs; 4,096 candidates, two layers |
115
+ | Directory | 10,000 files; `.git` and `node_modules` omitted |
116
+ | Findings | 100,000 hits; exceeding the limit blocks analysis |
117
+ | Local subprocess | Default 30 seconds and 10 MiB report/stdout cap; failures block output |
118
+ | Git objects | 10,000 objects, 10 MiB per object and 100 MiB total by default |
119
+
120
+ Supported decoding includes canonical base64/base64url, line-wrapped base64, hex, percent, HTML entities, UTF-8 byte escapes and JSON string escapes, with fatal UTF-8 checks for decoded payloads. Plain log files and streams preserve invalid UTF-8 bytes reversibly while scanning surrounding valid text; structured JSON/JSONL and configuration remain strict. Opt-in encoding preservation round-trips sanitized payloads; nested escaped JSON retains its structure by default. Arbitrarily split fragments, unlimited nesting, compressed/encrypted payloads, malformed encodings, archives, PDFs and OCR/images are outside this text profile. Whole-input/candidate limits fail; oversized textual percent/base64 runs receive audited complete-run redaction. Unsupported representations may remain undetected. `decode: false` intentionally reduces coverage. A caller needing another format should decode it through a bounded preprocessing adapter and retain correct original spans.
121
+
122
+ Default text mode can replace JSON numeric values with string markers and change syntax/types. Opt-in structured JSON/JSONL mode scans string keys/values separately, preserves primitive types, and rejects malformed output or key collisions. Numeric personal values need a separate caller schema policy. Existing quoted JSON strings and escaped-string fixtures retain their delimiters. Review sanitized artifacts for their intended consumer.
123
+
124
+ An unchanged result is not proof that text contains no sensitive information. The original sensitive input, private config, scanner reports, temporary files during execution, backups, screenshots, and remote caches remain separate handling concerns. POSIX permissions do not create equivalent Windows ACL guarantees or secure memory/disk erasure. In-place replacement creates a new file; it does not preserve ownership metadata, ACLs, or guarantee crash durability of the directory entry.
125
+
126
+ ## Entity catalogs and optional modes
127
+
128
+ Documented Google/Azure/AWS catalog names (213/176/36) are recognized as explicit native labels, including canonical underscore and original camel-case forms. Generic DATE is excluded so ordinary headers/calendar dates remain intact; personal birth-date context still qualifies. This vocabulary is distinct from automatic vendor NLP/image/cloud-service features. Typed transformations and confidence are described in [API.md](API.md); full redaction remains the default. Public Wikidata names, known role/domain emails and public resolver/documentation IPs use explicit offline knowledge and heuristics. Opt-in network lookup does not establish identity; private overrides and credential priority remain authoritative.
129
+
130
+ Large text logs use bounded record streaming and worker file publication. Plain-text records inherit the configurable total limit (1 GiB by default), so memory grows with the largest held record. The whole-string default is 32 Mi UTF-16 units. `credentialsOnly: true` preserves personal/account details across all finding sources while retaining credential detection and verification. The review corpus benchmark is a finite, synthetic regression comparison; [method, settings and results](../experiments/benchmark/README.md) explain its scope. It does not demonstrate general market-wide superiority.
131
+
132
+ Structured JSONL streaming supports 8 MiB records and ordered record workers;
133
+ [issue 24 evidence](case-studies/issue-24/README.md) records the 7 MiB regression
134
+ and reproducible throughput measurement. Realistic fake mode preserves supported
135
+ scripts/formats/checksums and uses reserved contact ranges where available.
136
+ Generated identities are pseudonyms; valid arithmetic does not prove non-issuance.
@@ -0,0 +1,41 @@
1
+ # Auditing and remediating Git history
2
+
3
+ Deleting a file in the current checkout does not remove its older blobs. The sanitizer's `history` command reads all local refs and reflog-reachable objects, scanning blobs, commit authors/messages and annotated tag metadata. It emits object IDs and safe findings only, and never changes refs or pushes.
4
+
5
+ ```sh
6
+ sensitive-data-sanitizer history /path/to/audit-clone --config /private/policy.json
7
+ ```
8
+
9
+ Exit 1 means findings; exit 2 means incomplete analysis or failure. Trees are traversed by Git but their filename bytes are not sanitized. Binary/non-UTF-8 objects are skipped with `complete: false`. Input/object/count limits prevent treating a partial audit as success. Unreachable objects without refs/reflogs, un-fetched branches, Git LFS storage, PR refs not fetched locally, submodules, release artifacts and remote mirrors are outside that local scope. A local report cannot certify erasure on GitHub.
10
+
11
+ ## Reviewed rewrite procedure
12
+
13
+ 1. Revoke/rotate exposed credentials immediately. Removing their text does not undo exposure.
14
+ 2. Inventory affected branches, tags, PR refs, authors/messages, filenames and external artifacts. Keep a separate offline restricted backup of the original mirror and the private value list. Do not refresh the only backup after a rewrite.
15
+ 3. Create a fresh disposable clone for rewriting with the maintained [git-filter-repo](https://github.com/newren/git-filter-repo) tool. Record its version and follow its fresh-clone safety requirements. Install it separately; this package does not install or invoke a rewrite automatically.
16
+ 4. Create mode-0600 replacement and optional mailmap files outside the repository, using the tool's documented literal syntax. Include known date/phone/name spellings and case/transliteration variants that require removal. Values containing delimiters or newlines require appropriately reviewed callback logic; do not interpolate them into shell commands.
17
+ 5. Run the following only in the disposable clone, with reviewed private inputs. Text replacements handle blobs; message replacements and a mailmap address different kinds of metadata. Path filtering requires separate decisions.
18
+
19
+ ```sh
20
+ git filter-repo --sensitive-data-removal --replace-text /private/replacements.txt \
21
+ --replace-message /private/replacements.txt --mailmap /private/mailmap
22
+ ```
23
+
24
+ 6. Audit complete rewritten commit snapshots, messages and tags with the sanitizer and independent scanners. Inspect selected revisions, run the repository's tests, and check that intended public values and structure survive. A scan of only added diff lines cannot prove historical removal.
25
+ 7. Compare refs and commit mappings against the immutable backup. Remove retained original refs from the _disposable cleaned clone_ according to git-filter-repo documentation; keep the backup private. Re-audit the cleaned clone, including reflogs. Partial rewrites that retain old refs can still expose old objects.
26
+ 8. Have the repository owner review the actual rewritten refs, then coordinate publication and collaborator recloning. GitHub branch protections, PR references, caches, forks, Actions artifacts and other clones may require separate cleanup. Follow [GitHub's sensitive-data removal guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository), including Support assistance where applicable. Do not issue an unreviewed blanket force push.
27
+
28
+ The automated regression creates a repository with a credential-bearing file, commits its deletion, and confirms the audit still finds the old blob and author email while leaving refs unchanged. It tests local discovery, not destructive rewriting or remote erasure.
29
+
30
+ ## Automated clone-only preview and rewrite
31
+
32
+ `history rewrite SOURCE --output NEW_DIRECTORY` creates a new private mirror clone with `--no-local`, audits reachable objects and reflogs, and writes private `replace-text.txt`, `mailmap`, `replacements.json` and callback files for review. The original repository is unchanged. These preview files contain original private values: keep the directory private and remove it after review. The clone's origin remote is removed; no operation pushes.
33
+
34
+ Add `--apply` to rewrite only the fresh clone using locally installed `git-filter-repo` (tested with 2.47.0). Use `--filter-repo PATH` to select its executable. Exact original-object callbacks sanitize complete blob contents, commit/tag messages, author/committer/tagger identities, filenames and refnames. They avoid replacement-file newline/regex ambiguity. Path/ref collisions fail rather than dropping objects. After filtering, the command audits again, checks names and refs, runs `git fsck --full`, removes private plans and writes a metadata-only manifest. An incomplete audit, binary object, remaining finding or missing tool fails and removes the new directory. A pre-existing destination is never reused or deleted.
35
+
36
+ ```sh
37
+ sensitive-data-sanitizer history rewrite ./repository --output ./private-preview
38
+ sensitive-data-sanitizer history rewrite ./repository --output ./private-rewritten --apply
39
+ ```
40
+
41
+ The library exports `rewriteGitHistory(source, destination, options)` with `apply: false` by default and the same audit limits. A rewritten history changes object IDs and invalidates signatures. Review the clone before any separate repository replacement or remote push. Rotate affected credentials; deleting history cannot revoke a leaked secret or clean other clones. The disposable integration experiment in `experiments/history-rewrite-integration.mjs` verifies deleted blobs, messages, identities, paths and tags with the actual rewrite tool.
@@ -0,0 +1,23 @@
1
+ import {
2
+ createSanitizer,
3
+ createGitleaksDetector,
4
+ createTrufflehogDetector,
5
+ createPresidioDetector,
6
+ } from '../src/index.js';
7
+ import { fileURLToPath, URL } from 'node:url';
8
+
9
+ // Install these optional local scanners before running this example.
10
+ // All selected detectors are required: an unavailable scanner blocks output.
11
+ const sanitizer = createSanitizer({
12
+ detectors: [
13
+ createGitleaksDetector(),
14
+ createTrufflehogDetector(),
15
+ createPresidioDetector({
16
+ args: [fileURLToPath(new URL('./presidio-bridge.py', import.meta.url))],
17
+ }),
18
+ ],
19
+ });
20
+ const result = await sanitizer.sanitize(
21
+ 'name: Jane Doe; password=short; jane@example.org'
22
+ );
23
+ console.log(result.text);
@@ -0,0 +1,32 @@
1
+ """Local stdin/stdout adapter. Install Presidio and models before use.
2
+
3
+ Example: python -m pip install presidio-analyzer
4
+ python -m spacy download en_core_web_lg
5
+ Configure NlpEngineProvider here for other installed language models.
6
+ This script does not download models or contact a remote analysis service.
7
+ """
8
+ import argparse
9
+ import json
10
+ import sys
11
+
12
+ from presidio_analyzer import AnalyzerEngine
13
+ from presidio_analyzer.nlp_engine import NlpEngineProvider
14
+ import spacy
15
+
16
+ parser = argparse.ArgumentParser()
17
+ parser.add_argument('--language', default='en')
18
+ parser.add_argument('--model', default='en_core_web_lg')
19
+ options = parser.parse_args()
20
+ if not spacy.util.is_package(options.model):
21
+ raise SystemExit('Install the selected model before running this offline bridge.')
22
+
23
+ text = sys.stdin.read(10 * 1024 * 1024 + 1)
24
+ if len(text) > 10 * 1024 * 1024:
25
+ raise SystemExit(2)
26
+ nlp = NlpEngineProvider(nlp_configuration={
27
+ 'nlp_engine_name': 'spacy',
28
+ 'models': [{'lang_code': options.language, 'model_name': options.model}],
29
+ }).create_engine()
30
+ analyzer = AnalyzerEngine(nlp_engine=nlp, supported_languages=[options.language])
31
+ results = analyzer.analyze(text=text, language=options.language)
32
+ json.dump([{"start":r.start,"end":r.end,"entity_type":r.entity_type} for r in results], sys.stdout)
@@ -0,0 +1,18 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createSanitizer } from '../src/index.js';
3
+
4
+ // Persist a private key outside published logs when stable identities are needed.
5
+ const key = process.env.SANITIZER_FAKE_KEY ?? randomBytes(32).toString('hex');
6
+ const sanitizer = createSanitizer({ transformation: { mode: 'fake', key } });
7
+ const input = [
8
+ 'Applicant: Marina Kovaleva',
9
+ 'born on 07/12/1985',
10
+ 'PNR K7QWZP; phone +1 (212) 234-5678',
11
+ 'email marina@private.org; password=short-private',
12
+ ].join('\n');
13
+ const result = await sanitizer.sanitize(input);
14
+ console.log(result.text);
15
+ console.log(
16
+ 'Generated fake findings:',
17
+ result.findings.filter((f) => f.faked).length
18
+ );