@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/README.md CHANGED
@@ -1,3 +1,185 @@
1
- # Temporary Holding Version
1
+ # Sensitive Data Sanitizer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An offline JavaScript library, detector framework, and CLI for redacting credentials and personal data from logs, AI sessions, and text files, including logs with invalid UTF-8 bytes. It combines required Secretlint and 221 bundled Gitleaks text rules with contextual credentials, default multilingual name/PII recognition, known values and bounded encoding detection. Optional local Gitleaks, TruffleHog, and Presidio detectors extend the union.
4
+
5
+ Library calls without a profile replace every actionable finding completely with `[REDACTED]`. Personal findings below the default confidence threshold of 0.5 remain inspectable without replacing ordinary log numbers; credentials always qualify. Publication methods verify full redaction and block output if a required detector fails or finds a residual. Optional typed transformations, compatible partial masks and encoding preservation are described in the API. Findings contain offsets and rule metadata; they omit matched values and source excerpts.
6
+
7
+ For publishing sessions, use `createSanitizer({profile: 'publication'})`:
8
+ the personal threshold is 0.3, identity values use the fixed first/last-two
9
+ `***` mask, and fake-identity exemptions remain disabled. CLI `redact` defaults
10
+ to this profile. Explicit configuration such as `identityMask: false` selects
11
+ complete redaction. Credentials always receive complete redaction unless an
12
+ explicit credential transformation is selected.
13
+
14
+ Detection has limits: the default 17-locale name gazetteer has finite vocabulary; unfamiliar names need an additional NER model or known-personal dictionary. New token formats, unsupported encodings, and unconfigured languages can escape detection. Successful verification means the enabled detectors found no residual, not that the input is universally anonymous. See [coverage and limits](docs/COVERAGE.md) and the [issue #1 case study](docs/case-studies/issue-1/README.md).
15
+
16
+ For real session files whose personal values are confirmed later, use atomic
17
+ two-pass publication:
18
+
19
+ ```sh
20
+ sensitive-data-sanitizer redact session.jsonl --stream --jsonl --two-pass --output safe.jsonl
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ Sanitizer APIs require Node.js 22.13 or later. Template arithmetic utilities are available under `@link-foundation/sensitive-data-sanitizer/legacy`; the existing universal example uses that browser-compatible entry. From this checkout:
26
+
27
+ ```sh
28
+ npm ci
29
+ node bin/sensitive-data-sanitizer.js redact session.jsonl --output session.safe.jsonl
30
+ node bin/sensitive-data-sanitizer.js scan ./logs
31
+ node bin/sensitive-data-sanitizer.js history ./repository
32
+ ```
33
+
34
+ The npm package is `@link-foundation/sensitive-data-sanitizer`; the installed executable is `sensitive-data-sanitizer`. This PR prepares its release through Changesets.
35
+
36
+ First npm publication currently requires owner setup; see the
37
+ [verified release blockers and exact setup steps](docs/case-studies/issue-32/REQUIREMENTS.md#exact-npm-owner-steps).
38
+
39
+ ```js
40
+ import {
41
+ createSanitizer,
42
+ knownSecretsFromEnv,
43
+ } from '@link-foundation/sensitive-data-sanitizer';
44
+
45
+ const sanitizer = createSanitizer({
46
+ profile: 'publication',
47
+ knownSecrets: knownSecretsFromEnv(), // Explicitly include local credentials.
48
+ });
49
+ const result = await sanitizer.sanitize('password=short person@example.org');
50
+ console.log(result.text); // password=[REDACTED] [REDACTED]
51
+ ```
52
+
53
+ `createSanitizer()` requires the pinned Secretlint recommended rules to succeed. The synchronous `sanitize()` and `inspect()` functions use the native rules only. `redact()` applies supplied findings without a residual scan. Prefer the asynchronous sanitizer for publication.
54
+
55
+ ## CLI
56
+
57
+ ```sh
58
+ printf 'password=short\n' | sensitive-data-sanitizer redact -
59
+ sensitive-data-sanitizer redact input.txt --in-place
60
+ sensitive-data-sanitizer scan logs --config /private/policy.json
61
+ sensitive-data-sanitizer redact input.txt --gitleaks --trufflehog --output safe.txt
62
+ sensitive-data-sanitizer redact input.txt --presidio /path/presidio-bridge.py --model en_core_web_sm --language en
63
+ sensitive-data-sanitizer redact large-session.jsonl --jsonl --stream --output safe-session.jsonl
64
+ sensitive-data-sanitizer history rewrite ./repository --output ./private-preview
65
+ sensitive-data-sanitizer --help
66
+ ```
67
+
68
+ `--output` creates a new file without clobbering an existing one. `--in-place` replaces the input atomically after verification. Intermediate and output files use mode `0600` on POSIX. Symlinks and NUL-containing binary input are refused. Plain logs preserve invalid UTF-8 bytes exactly; JSON/JSONL and configuration files require valid UTF-8. Directory scans omit `.git` and `node_modules`; skipped binary/malformed-structured/oversized files make the report incomplete. Stdout is written after the complete operation succeeds.
69
+
70
+ | Exit | Meaning |
71
+ | ---- | ------------------------------------------------------------------------ |
72
+ | 0 | Redaction succeeded, or a complete scan found nothing |
73
+ | 1 | A complete scan/history audit found sensitive data |
74
+ | 2 | Error, detector failure, or incomplete scan; redaction output is blocked |
75
+
76
+ Defaults: 32 MiB whole-text input bytes, 100,000 findings and 10,000 files. `--stream` permits 1 GiB total with plain-text records up to the total input limit or 8 MiB JSONL records, bounded workers and atomic publication. `--max-bytes` sets the byte limit; JSON config sets the other sanitizer limits. `--paranoid` adds entropy heuristics and increases false positives. `--native-only` explicitly disables Secretlint.
77
+
78
+ ## Configuration
79
+
80
+ For AI sessions, `--jsonl` preserves JSON records and declares `id`, `type`,
81
+ `name`, and `description` as metadata. `--json` handles a single JSON document;
82
+ use JSONL for streaming. Known private values and credential formats still
83
+ override declarations. Set `structuralFields` in private config to choose your
84
+ own property names, or use an empty array to scan every field normally.
85
+ The library provides `structured: 'json' | 'jsonl'` and
86
+ `engine.sanitizeJsonl(text)` with an empty structural policy by default.
87
+
88
+ For input above the whole-text limit, pass an iterable to
89
+ `sanitizeStream(source, {structured: 'jsonl', profile: 'publication'})` or use `sanitizeStreamToFile` for
90
+ atomic output. JSONL defaults to two ordered record workers. See [the streaming
91
+ example](examples/stream-jsonl.mjs).
92
+
93
+ Native coverage includes tolerant passport MRZ fragments, country passport
94
+ formats, PNR/ticket/visa identifiers, contextual birth dates, national IDs and
95
+ messenger phone numbers. `identityMask: true` (the publication/CLI redact default) uses
96
+ 2+2 characters with a fixed `***` marker. Opt-in fake identity policies retain
97
+ audited specimen/synthetic values while credentials remain protected.
98
+ See [the API](docs/API.md) and [all issue 13 requirements and research](docs/case-studies/repository-issue-13/REQUIREMENTS.md).
99
+
100
+ `transformation: {mode: 'fake', key}` generates deterministic names, valid
101
+ dates/MRZs/checksummed IDs and reserved email/domain destinations. Credentials
102
+ remain fully redacted by default. Keep the key private: this preserves relations
103
+ and structure and is pseudonymisation. See [realistic fakes](examples/realistic-fakes.mjs)
104
+ and [the complete issue 24 requirements, research and solution plans](docs/case-studies/issue-24/REQUIREMENTS.md).
105
+
106
+ Keep policies containing real private values outside Git. Built-in exact public knowledge and narrowly scoped role/domain heuristics are enabled offline. Explicit private literals override public exemptions. Keep custom public policies reviewed against evidence; `--verify-public` separately opts into Wikidata name lookup:
107
+
108
+ ```json
109
+ {
110
+ "knownSecrets": [],
111
+ "knownPersonal": [{ "type": "PERSON", "value": "Private Person" }],
112
+ "publicEntities": [
113
+ {
114
+ "type": "EMAIL",
115
+ "value": "contact@example.org",
116
+ "source": "https://example.org/contact",
117
+ "reviewedAt": "2026-10-07"
118
+ }
119
+ ]
120
+ }
121
+ ```
122
+
123
+ Custom policies preserve exact reviewed `PERSON`, `ORGANIZATION` and `EMAIL` detections. Built-in public resolvers/documentation IPs are also exempt. Credential detections always win, including a public email used as a password or authorization value. Evidence is an explicit caller decision and does not make every occurrence harmless.
124
+
125
+ ## Extend detection
126
+
127
+ ```js
128
+ import {
129
+ createSanitizer,
130
+ createGitleaksDetector,
131
+ createTrufflehogDetector,
132
+ createPresidioDetector,
133
+ } from '@link-foundation/sensitive-data-sanitizer';
134
+
135
+ const sanitizer = createSanitizer({
136
+ detectors: [
137
+ createGitleaksDetector(),
138
+ createTrufflehogDetector(),
139
+ createPresidioDetector({ args: ['/absolute/path/presidio-bridge.py'] }),
140
+ ],
141
+ });
142
+ ```
143
+
144
+ Install external binaries and language models separately. TruffleHog runs with verification and updates disabled. The Presidio example checks for an installed model and never downloads it at analysis time. Each selected detector is required, including during residual verification. See [the API and adapter contract](docs/API.md), [combined scanners](examples/combined-scanners.mjs), and [the local NER bridge](examples/presidio-bridge.py).
145
+
146
+ The framework also accepts Gitleaks, TruffleHog, detect-secrets, Presidio, or explicitly typed offset reports. Cloud reports can be converted locally; the package itself makes no cloud requests.
147
+
148
+ For CLI name recognition, `--presidio PATH` selects a local Python JSON bridge. `--python COMMAND` selects its interpreter, and `--model` / `--language` select an already installed model. These options require the bridge and participate in final-output verification.
149
+
150
+ ## Git history
151
+
152
+ `history` audits reachable blobs, commit messages/authors, annotated tags, and reflog-reachable objects without changing refs. It can find deleted files. It reports object hashes and safe findings, and refuses limits or reports skipped binary objects as incomplete. The `history rewrite SOURCE --output NEW_DIRECTORY` command previews private replacement/mailmap files in a fresh mirror clone; add `--apply` to rewrite only that clone and re-audit blobs, metadata, paths and refs. It never pushes. Remote caches, forks, artifacts and unreachable objects require separate remediation; see the [history guide](docs/HISTORY-REMEDIATION.md).
153
+
154
+ ## Contributing
155
+
156
+ ```sh
157
+ npm test
158
+ npm run check
159
+ bun test --timeout 30000
160
+ deno test --allow-read --allow-env
161
+ ```
162
+
163
+ See [contributing](docs/CONTRIBUTING.md). `add`, `multiply` and `delay` live in the `legacy` subpath, with the `sensitive-data-sanitizer-legacy` CLI and retained universal app; inherited pipeline instructions are in [the template guide](docs/TEMPLATE-PIPELINE.md).
164
+
165
+ ### Auto-regenerated preview screenshots
166
+
167
+ The universal app's preview images regenerate on pushes to the default branch. Run `npm run example:web:preview-images` to regenerate them locally; see [the template guide](docs/TEMPLATE-PIPELINE.md) for setup.
168
+
169
+ For shared tool logs, `--credentials-only --env-secrets --gh-config` preserves
170
+ public account details while redacting credential patterns and exact local
171
+ secret values. Environment discovery matches credential-related variable names,
172
+ including AI-provider keys; `--gh-config` reads every host/account token from the
173
+ local GitHub CLI `hosts.yml`. Both local sources are opt-in. Literal references
174
+ such as `${{ secrets.NPM_TOKEN }}` and `$(gh auth token)` remain readable; actual
175
+ credential values are still redacted.
176
+
177
+ Long plain-text records are held until a safe boundary, including credentials
178
+ longer than a 64 KiB input chunk. Memory grows with the largest held record;
179
+ set `--max-record-bytes` for a smaller deployment budget. Crossing an explicit
180
+ record or total limit blocks atomic publication. See [the tool-log integration
181
+ example](examples/shared-tool-logs.mjs) and [API byte handling](docs/API.md).
182
+
183
+ ## License
184
+
185
+ Released under the [Unlicense](LICENSE). External components keep their own licenses; see [component provenance](docs/case-studies/issue-1/COMPETITORS.md).
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, realpathSync } from 'node:fs';
4
+ import { fileURLToPath, URL } from 'node:url';
5
+
6
+ import { add, multiply } from '../src/legacy.js';
7
+
8
+ const COMMANDS = {
9
+ add,
10
+ multiply,
11
+ };
12
+
13
+ function usage() {
14
+ return [
15
+ 'Usage: sensitive-data-sanitizer-legacy <command> <left> <right>',
16
+ '',
17
+ 'Commands:',
18
+ ' add Add two numbers',
19
+ ' multiply Multiply two numbers',
20
+ '',
21
+ 'Options:',
22
+ ' --help Show this help',
23
+ ' --version Show package version',
24
+ ].join('\n');
25
+ }
26
+
27
+ function readVersion() {
28
+ const packageUrl = new URL('../package.json', import.meta.url);
29
+ const packageJson = JSON.parse(readFileSync(packageUrl, 'utf8'));
30
+ return packageJson.version;
31
+ }
32
+
33
+ function parseNumber(value, label) {
34
+ const parsed = Number(value);
35
+
36
+ if (!Number.isFinite(parsed)) {
37
+ throw new Error(`${label} must be a finite number`);
38
+ }
39
+
40
+ return parsed;
41
+ }
42
+
43
+ export function runCli(
44
+ argv,
45
+ { stderr = console.error, stdout = console.log } = {}
46
+ ) {
47
+ const [command, left, right] = argv;
48
+
49
+ if (!command || command === '--help' || command === '-h') {
50
+ stdout(usage());
51
+ return 0;
52
+ }
53
+
54
+ if (command === '--version' || command === '-v') {
55
+ stdout(readVersion());
56
+ return 0;
57
+ }
58
+
59
+ const operation = COMMANDS[command];
60
+
61
+ if (!operation) {
62
+ stderr(`Unknown command: ${command}`);
63
+ stderr(usage());
64
+ return 1;
65
+ }
66
+
67
+ try {
68
+ const leftNumber = parseNumber(left, 'left');
69
+ const rightNumber = parseNumber(right, 'right');
70
+ stdout(String(operation(leftNumber, rightNumber)));
71
+ return 0;
72
+ } catch (error) {
73
+ stderr(error.message);
74
+ return 1;
75
+ }
76
+ }
77
+
78
+ function isCliEntryPoint() {
79
+ if (!process.argv[1]) {
80
+ return false;
81
+ }
82
+
83
+ try {
84
+ return (
85
+ realpathSync(process.argv[1]) ===
86
+ realpathSync(fileURLToPath(import.meta.url))
87
+ );
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ if (isCliEntryPoint()) {
94
+ process.exitCode = runCli(process.argv.slice(2));
95
+ }