@link-foundation/sensitive-data-sanitizer 0.19.2 → 0.20.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.
- package/CHANGELOG.md +12 -0
- package/README.md +8 -1
- package/bin/sensitive-data-sanitizer.js +49 -28
- package/docs/API.md +193 -2
- package/docs/COVERAGE.md +22 -4
- package/docs/RULES.md +217 -0
- package/package.json +1 -1
- package/src/allow-list.js +93 -0
- package/src/confidence.js +24 -9
- package/src/context.js +121 -25
- package/src/credentials.js +44 -0
- package/src/crypto-addresses.js +74 -0
- package/src/detection.js +21 -6
- package/src/document-person-cues.js +1 -1
- package/src/encrypt.js +95 -0
- package/src/entities.js +4 -3
- package/src/fake.js +29 -1
- package/src/files.js +57 -12
- package/src/gitleaks.js +82 -27
- package/src/identifier-detector.js +36 -0
- package/src/identity.js +14 -0
- package/src/index.d.ts +117 -1
- package/src/index.js +7 -0
- package/src/national-checks.js +209 -0
- package/src/national.js +310 -92
- package/src/pattern-detector.js +219 -0
- package/src/personal-context.js +3 -2
- package/src/personal-evidence.js +233 -0
- package/src/personal.js +42 -10
- package/src/projection.js +85 -26
- package/src/rule-catalog.js +424 -0
- package/src/sanitizer.js +48 -3
- package/src/sarif.js +127 -0
- package/src/strict-attributes.js +71 -0
- package/src/strict.js +2 -12
- package/src/token-checksum.js +49 -0
- package/src/transforms.js +108 -25
- package/src/vendor/betterleaks/LICENSE +21 -0
- package/src/vendor/betterleaks/provenance.json +11 -0
- package/src/vendor/betterleaks/rules-0.json +463 -0
- package/src/vendor/betterleaks/rules-1.json +454 -0
- package/src/vendor/betterleaks/rules-2.json +461 -0
- package/src/vendor/betterleaks/rules-3.json +455 -0
- package/src/vendor/betterleaks/rules-4.json +483 -0
- package/src/vendor/betterleaks/rules-5.json +35 -0
- package/src/vendor/entities/aws.json +1 -1
- package/src/vendor/entities/azure.json +6 -2
- package/src/vendor/entities/google.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- befe673: Match the strongest features of competing secret and PII tools. Add national, tax, bank and device identifiers from Presidio, OpenRedaction, DataFog and Google; 203 vendored Betterleaks rules; offline CRC32 checks for GitHub and npm tokens (`tokenChecksum`, `tokenWithChecksum`); escape-run, HTML-tag and UTF-16 decoding; `allowList`, `inlineAllow` and `minConfidenceByType`; mask tuning and deterministic `encrypt` with `decrypt`; `createPatternDetector`, `createIdentifierDetector`, `identifierChecks` and the read-only `nationalRules` table; SARIF output (`toSarif`, `scan --format sarif`), multi-path `scan` and pre-commit hooks. Detect an IPv4 address followed by a full stop. Add a competitor comparison with measured results on 853 competitor test cases.
|
|
8
|
+
|
|
9
|
+
## 0.19.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 6bdaf11: Recognize additional inflected special-category attributes and unfamiliar values after explicit category cues in strict mode. Require personal context for ambiguous English attributes so operational logs and JSON statuses stay intact.
|
|
14
|
+
|
|
3
15
|
## 0.19.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sensitive Data Sanitizer
|
|
2
2
|
|
|
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
|
|
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, 221 bundled Gitleaks text rules and 203 further Betterleaks 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
4
|
|
|
5
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
6
|
|
|
@@ -131,6 +131,7 @@ console.log(result.text); // password=[REDACTED] [REDACTED]
|
|
|
131
131
|
printf 'password=short\n' | sensitive-data-sanitizer redact -
|
|
132
132
|
sensitive-data-sanitizer redact input.txt --in-place
|
|
133
133
|
sensitive-data-sanitizer scan logs --config /private/policy.json
|
|
134
|
+
sensitive-data-sanitizer scan src/a.js src/b.js --format sarif > report.sarif
|
|
134
135
|
sensitive-data-sanitizer redact input.txt --gitleaks --trufflehog --output safe.txt
|
|
135
136
|
sensitive-data-sanitizer redact input.txt --presidio /path/presidio-bridge.py --model en_core_web_sm --language en
|
|
136
137
|
sensitive-data-sanitizer redact large-session.jsonl --jsonl --stream --output safe-session.jsonl
|
|
@@ -148,6 +149,8 @@ sensitive-data-sanitizer --help
|
|
|
148
149
|
|
|
149
150
|
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. `--worker-timeout-ms N` sets the worker inactivity/per-batch deadline (default 60000 ms); verified file progress resets the deadline, so large files can run longer overall. `--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.
|
|
150
151
|
|
|
152
|
+
`scan` accepts several paths and `--format sarif` emits SARIF 2.1.0 for code-scanning viewers. The repository's `.pre-commit-hooks.yaml` provides `sensitive-data-sanitizer` (credentials) and `sensitive-data-sanitizer-pii` hooks for [pre-commit](https://pre-commit.com). UTF-16 files with a byte order mark are read and written back in their own encoding.
|
|
153
|
+
|
|
151
154
|
## Configuration
|
|
152
155
|
|
|
153
156
|
For AI sessions, `--jsonl` preserves JSON records and declares `id`, `type`,
|
|
@@ -195,6 +198,8 @@ Keep policies containing real private values outside Git. Built-in exact public
|
|
|
195
198
|
}
|
|
196
199
|
```
|
|
197
200
|
|
|
201
|
+
Reviewed false positives can be kept with `allowList` (whole-value strings or anchored regexes) or, opt-in, with `inlineAllow: true`, which honours `gitleaks:allow`, `pragma: allowlist secret` and similar comments; credentials stay redacted unless `allowCredentialDisable: true` is also set. `minConfidenceByType` sets per-type thresholds, the `mask` transformation accepts `char`, `ignore`, `ignoreClasses`, `count` and `fromEnd`, and `{mode: 'encrypt', key}` produces deterministic AES-256-GCM tokens that `decrypt(text, key)` restores. See [the API](docs/API.md#allow-lists-suppression-comments-and-template-values) and [the competitor comparison](docs/COMPARISON.md).
|
|
202
|
+
|
|
198
203
|
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.
|
|
199
204
|
|
|
200
205
|
## Extend detection
|
|
@@ -218,6 +223,8 @@ const sanitizer = createSanitizer({
|
|
|
218
223
|
|
|
219
224
|
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).
|
|
220
225
|
|
|
226
|
+
`createPatternDetector([{type, patterns, denyList, context, validate}])` adds Presidio-style pattern recognizers or Google-style custom infoTypes with hotword boosts, and `createIdentifierDetector` adds national identifiers with checksum validators; see [declarative pattern detectors](docs/API.md#declarative-pattern-detectors).
|
|
227
|
+
|
|
221
228
|
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.
|
|
222
229
|
|
|
223
230
|
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.
|
|
@@ -6,9 +6,11 @@ import { createSanitizer } from '../src/engines.js';
|
|
|
6
6
|
import { failure } from '../src/detection.js';
|
|
7
7
|
import {
|
|
8
8
|
readTextFile,
|
|
9
|
+
readTextInput,
|
|
9
10
|
atomicWrite,
|
|
10
11
|
listTextCandidates,
|
|
11
|
-
|
|
12
|
+
decodeTextInput,
|
|
13
|
+
encodeTextOutput,
|
|
12
14
|
} from '../src/files.js';
|
|
13
15
|
import { auditGitHistory } from '../src/history.js';
|
|
14
16
|
import { rewriteGitHistory } from '../src/history-rewrite.js';
|
|
@@ -17,7 +19,7 @@ import {
|
|
|
17
19
|
knownSecretsFromEnv,
|
|
18
20
|
knownSecretsFromGitHubConfig,
|
|
19
21
|
} from '../src/known.js';
|
|
20
|
-
import {
|
|
22
|
+
import { toSarif } from '../src/sarif.js';
|
|
21
23
|
import { createWikidataVerifier } from '../src/public-verifier.js';
|
|
22
24
|
import { streamCommand } from './stream.js';
|
|
23
25
|
import { sessionStructuralFields } from '../src/structured.js';
|
|
@@ -31,7 +33,8 @@ import {
|
|
|
31
33
|
const help = `Usage: sensitive-data-sanitizer <redact|scan|restore|history> [path|-] [options]
|
|
32
34
|
|
|
33
35
|
redact Sanitize one text log or stdin; publish only after verification
|
|
34
|
-
scan Report metadata for
|
|
36
|
+
scan Report metadata for files, directories, or stdin (never source text);
|
|
37
|
+
several paths may follow scan (for example staged files)
|
|
35
38
|
restore Restore placeholders in a file using --map PATH (private output)
|
|
36
39
|
history Audit local reachable Git blobs, commit/tag metadata, and reflogs
|
|
37
40
|
history rewrite SOURCE --output DIRECTORY [--apply]
|
|
@@ -67,6 +70,7 @@ history rewrite SOURCE --output DIRECTORY [--apply]
|
|
|
67
70
|
--no-map Discard placeholder mapping after full anonymization
|
|
68
71
|
--placeholder-format TEMPLATE Default {Type}-{n}; also [{TYPE}_{n}]
|
|
69
72
|
--map-format FORMAT json or lino (otherwise inferred from .lino extension)
|
|
73
|
+
--format FORMAT scan report: json (default) or sarif (SARIF 2.1.0)
|
|
70
74
|
--map-credentials Explicitly keep credentials in the private restore map
|
|
71
75
|
--max-record-bytes N Maximum record bytes (text: total limit; JSONL: 8388608)
|
|
72
76
|
--hive-mask Opt-in first/last 3 mask for values longer than 12 characters
|
|
@@ -129,8 +133,9 @@ function parseArgs(argv) {
|
|
|
129
133
|
['--map', 'map'],
|
|
130
134
|
['--map-format', 'mapFormat'],
|
|
131
135
|
['--placeholder-format', 'placeholderFormat'],
|
|
136
|
+
['--format', 'format'],
|
|
132
137
|
]);
|
|
133
|
-
|
|
138
|
+
const paths = [];
|
|
134
139
|
for (let i = rewrite ? 2 : 1; i < argv.length; i++) {
|
|
135
140
|
const arg = argv[i];
|
|
136
141
|
if (boolean.has(arg)) {
|
|
@@ -140,13 +145,18 @@ function parseArgs(argv) {
|
|
|
140
145
|
throw failure('ERR_ARGUMENT');
|
|
141
146
|
}
|
|
142
147
|
config[valued.get(arg)] = argv[++i];
|
|
143
|
-
} else if (!
|
|
144
|
-
|
|
145
|
-
hasPath = true;
|
|
148
|
+
} else if (!arg.startsWith('--')) {
|
|
149
|
+
paths.push(arg);
|
|
146
150
|
} else {
|
|
147
151
|
throw failure('ERR_ARGUMENT');
|
|
148
152
|
}
|
|
149
153
|
}
|
|
154
|
+
// Only scan accepts several paths (for example staged files from a hook).
|
|
155
|
+
if (paths.length > 1 && (command !== 'scan' || paths.includes('-'))) {
|
|
156
|
+
throw failure('ERR_ARGUMENT');
|
|
157
|
+
}
|
|
158
|
+
config.path = paths[0] ?? '-';
|
|
159
|
+
config.paths = paths.length ? paths : ['-'];
|
|
150
160
|
validateArgs(config);
|
|
151
161
|
return config;
|
|
152
162
|
}
|
|
@@ -194,7 +204,11 @@ function validateFeatureArgs(config) {
|
|
|
194
204
|
|
|
195
205
|
function validateStructuredArgs(config) {
|
|
196
206
|
validateStreamArgs(config);
|
|
197
|
-
if (
|
|
207
|
+
if (
|
|
208
|
+
(config.twoPass && !config.stream) ||
|
|
209
|
+
(config.format !== undefined &&
|
|
210
|
+
(config.command !== 'scan' || !['json', 'sarif'].includes(config.format)))
|
|
211
|
+
) {
|
|
198
212
|
throw failure('ERR_ARGUMENT');
|
|
199
213
|
}
|
|
200
214
|
if ((config.json && config.jsonl) || (config.stream && config.json)) {
|
|
@@ -249,7 +263,7 @@ async function stdinText(stream, maxBytes, options) {
|
|
|
249
263
|
}
|
|
250
264
|
chunks.push(buffer);
|
|
251
265
|
}
|
|
252
|
-
return
|
|
266
|
+
return decodeTextInput(Buffer.concat(chunks), options);
|
|
253
267
|
}
|
|
254
268
|
|
|
255
269
|
export async function runCli(
|
|
@@ -266,9 +280,7 @@ export async function runCli(
|
|
|
266
280
|
return 0;
|
|
267
281
|
}
|
|
268
282
|
if (['--version', '-v'].includes(argv[0])) {
|
|
269
|
-
stdout.write(
|
|
270
|
-
`${JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version}\n`
|
|
271
|
-
);
|
|
283
|
+
stdout.write(`${packageVersion()}\n`);
|
|
272
284
|
return 0;
|
|
273
285
|
}
|
|
274
286
|
const config = parseArgs(argv);
|
|
@@ -422,24 +434,20 @@ function inputText(path, config, stdin) {
|
|
|
422
434
|
};
|
|
423
435
|
return path === '-'
|
|
424
436
|
? stdinText(stdin, config.maxBytes, options)
|
|
425
|
-
:
|
|
437
|
+
: readTextInput(path, config.maxBytes, options);
|
|
426
438
|
}
|
|
427
439
|
|
|
428
440
|
async function redactCommand(config, engine, { stdin, stdout }) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
);
|
|
441
|
+
const input = await inputText(config.path, config, stdin);
|
|
442
|
+
const result = await engine.sanitize(input.text);
|
|
432
443
|
await saveMap(config);
|
|
444
|
+
const output = encodeTextOutput(result.text, input.encoding);
|
|
433
445
|
if (config.output || config.inPlace) {
|
|
434
|
-
await atomicWrite(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
{
|
|
438
|
-
replace: Boolean(config.inPlace),
|
|
439
|
-
}
|
|
440
|
-
);
|
|
446
|
+
await atomicWrite(config.output ?? config.path, output, {
|
|
447
|
+
replace: Boolean(config.inPlace),
|
|
448
|
+
});
|
|
441
449
|
} else {
|
|
442
|
-
stdout.write(
|
|
450
|
+
stdout.write(output);
|
|
443
451
|
}
|
|
444
452
|
return 0;
|
|
445
453
|
}
|
|
@@ -464,8 +472,10 @@ async function historyCommand(config, engine, { stdout }) {
|
|
|
464
472
|
}
|
|
465
473
|
|
|
466
474
|
async function scanCommand(config, engine, { stdin, stdout }) {
|
|
467
|
-
const files =
|
|
468
|
-
|
|
475
|
+
const files = [];
|
|
476
|
+
for (const path of config.paths) {
|
|
477
|
+
files.push(...(path === '-' ? ['-'] : await listTextCandidates(path)));
|
|
478
|
+
}
|
|
469
479
|
const results = [],
|
|
470
480
|
skipped = [];
|
|
471
481
|
for (const file of files) {
|
|
@@ -473,7 +483,9 @@ async function scanCommand(config, engine, { stdin, stdout }) {
|
|
|
473
483
|
try {
|
|
474
484
|
results.push({
|
|
475
485
|
path,
|
|
476
|
-
findings: await engine.inspect(
|
|
486
|
+
findings: await engine.inspect(
|
|
487
|
+
(await inputText(file, config, stdin)).text
|
|
488
|
+
),
|
|
477
489
|
});
|
|
478
490
|
} catch (error) {
|
|
479
491
|
if (['ERR_BINARY', 'ERR_ENCODING', 'ERR_LIMIT'].includes(error.code)) {
|
|
@@ -484,7 +496,10 @@ async function scanCommand(config, engine, { stdin, stdout }) {
|
|
|
484
496
|
}
|
|
485
497
|
}
|
|
486
498
|
const complete = skipped.length === 0;
|
|
487
|
-
|
|
499
|
+
const report = { complete, results, skipped };
|
|
500
|
+
stdout.write(
|
|
501
|
+
`${JSON.stringify(config.format === 'sarif' ? toSarif(report, { version: packageVersion() }) : report, null, 2)}\n`
|
|
502
|
+
);
|
|
488
503
|
return !complete ? 2 : results.some((r) => r.findings.length) ? 1 : 0;
|
|
489
504
|
}
|
|
490
505
|
|
|
@@ -495,6 +510,12 @@ if (
|
|
|
495
510
|
process.exitCode = await runCli(process.argv.slice(2));
|
|
496
511
|
}
|
|
497
512
|
|
|
513
|
+
function packageVersion() {
|
|
514
|
+
return JSON.parse(
|
|
515
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
516
|
+
).version;
|
|
517
|
+
}
|
|
518
|
+
|
|
498
519
|
function defaultByteLimit(config) {
|
|
499
520
|
return config.stream ? 1073741824 : 33554432;
|
|
500
521
|
}
|
package/docs/API.md
CHANGED
|
@@ -16,8 +16,12 @@ Import the ESM root entry; complete declarations are in `src/index.d.ts`. Inputs
|
|
|
16
16
|
| `createSyncSanitizer(options)` | Reusable synchronous `inspect` / `sanitize` with the same policies |
|
|
17
17
|
| `compileKnown(...lists)` | Copy, freeze, merge and precompile caller-known personal values |
|
|
18
18
|
| `auditGitHistory(repository, options)` | Read-only audit of local reachable text objects |
|
|
19
|
+
| `decrypt(text, key, options)` | Restore values from `[ENCRYPTED:TYPE:…]` tokens written by the `encrypt` transformation |
|
|
20
|
+
| `createPatternDetector(specs, opts)` | Declarative regex, deny-list, hotword and validator detector |
|
|
21
|
+
| `createIdentifierDetector(rules)` | Detector for caller-supplied national identifier rules (`nationalRules` shape) |
|
|
22
|
+
| `toSarif(report, options)` | Convert a scan report or findings to SARIF 2.1.0 without values |
|
|
19
23
|
|
|
20
|
-
`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 value-free counts, rule timings, residual rounds and registry event metadata (see `DebugEvent` in the TypeScript declarations).
|
|
24
|
+
`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. The optional `strict-attributes` debug event reports only detected and suppressed match counts. Debugging is off by default; `debug(event)` receives value-free counts, rule timings, residual rounds and registry event metadata (see `DebugEvent` in the TypeScript declarations).
|
|
21
25
|
|
|
22
26
|
## Options
|
|
23
27
|
|
|
@@ -242,6 +246,7 @@ Without a profile, full redaction is the library default. `transformation` selec
|
|
|
242
246
|
| `fake` | Keyed realistic names, valid dates/MRZ/checksummed numbers and reserved contact destinations |
|
|
243
247
|
| `date-shift` | Personal ISO dates; finite integer `days`, UTC calendar arithmetic; invalid dates become opaque |
|
|
244
248
|
| `bucket` | Personal safe integers; positive integer `size`, inclusive numeric interval |
|
|
249
|
+
| `encrypt` | Personal data only; deterministic AES-256-GCM token reversible with `decrypt` and the key (see below) |
|
|
245
250
|
|
|
246
251
|
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.
|
|
247
252
|
|
|
@@ -368,7 +373,7 @@ The optional ESLint plugin exports `require-sanitized-output` from the package's
|
|
|
368
373
|
|
|
369
374
|
## Native documented entity catalogs
|
|
370
375
|
|
|
371
|
-
`entityCatalogs` exposes source URLs, source-page SHA256 and documented names for Google (
|
|
376
|
+
`entityCatalogs` exposes source URLs, source-page SHA256 and documented names for Google (214), Azure (180) 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.
|
|
372
377
|
|
|
373
378
|
## Two-pass session publication
|
|
374
379
|
|
|
@@ -742,3 +747,189 @@ one or both languages. Each invocation is limited to 1 Mi UTF-16 units; larger
|
|
|
742
747
|
inputs throw `ERR_LIMIT`. `debug` is off by default and emits counts only. Core
|
|
743
748
|
imports never load the optional entry or NLP.js; leaving it off adds no optional
|
|
744
749
|
runtime/module cost.
|
|
750
|
+
|
|
751
|
+
## Allow lists, suppression comments and template values
|
|
752
|
+
|
|
753
|
+
`allowList` keeps detected values that the caller has reviewed as safe, as in
|
|
754
|
+
Presidio's `allow_list`, Google's exclusion rules with
|
|
755
|
+
`MATCHING_TYPE_FULL_MATCH` and DataFog's `allowlist_patterns`. A string must
|
|
756
|
+
equal the whole detected value; a `RegExp` must match the whole value (it is
|
|
757
|
+
anchored internally, `g`/`y` flags are ignored). A partial match never shortens
|
|
758
|
+
a finding. At most 10 000 entries; anything else is `ERR_CONFIG`.
|
|
759
|
+
|
|
760
|
+
```js
|
|
761
|
+
sanitize('Contact support@example.org or alice@private.test', {
|
|
762
|
+
allowList: ['support@example.org', /.+@example\.org/],
|
|
763
|
+
});
|
|
764
|
+
// → 'Contact support@example.org or [REDACTED]'
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
`inlineAllow: true` honours the suppression comments of other scanners on the
|
|
768
|
+
same line: `gitleaks:allow`, `betterleaks:allow`, `kingfisher:allow`/`ignore`,
|
|
769
|
+
`trufflehog:ignore`, `sanitizer:allow`, detect-secrets/ripsecrets
|
|
770
|
+
`pragma: allowlist secret` and SonarQube `NOSONAR`. A line containing
|
|
771
|
+
`pragma: allowlist nextline secret` suppresses findings on the next line. It is
|
|
772
|
+
off by default because anyone who can write to a log could otherwise disable
|
|
773
|
+
redaction.
|
|
774
|
+
|
|
775
|
+
Both controls follow the same safety rules:
|
|
776
|
+
|
|
777
|
+
- declared `knownPersonal`/`knownSecrets` values, confirmed personal values and
|
|
778
|
+
structured `JSON_VALUE` denials are never exempted;
|
|
779
|
+
- credentials are exempted only together with `allowCredentialDisable: true`;
|
|
780
|
+
- they apply to text, structured and asynchronous paths alike.
|
|
781
|
+
|
|
782
|
+
Built-in credential rules also skip values that consist only of a template or
|
|
783
|
+
mask: `${PASSWORD}`, `$${PYPI_PASSWORD}`, `$DB_PASS`, `$1`, `<password>`,
|
|
784
|
+
`<Openverse API token>`, `[PASSWORD]`, `****`, `XXXXXXXX`, `...`, optionally
|
|
785
|
+
after `Bearer`/`Basic`/`token` and on either side of a `user:password` colon.
|
|
786
|
+
A mixed value such as `alice:${DB_PASSWORD}` or `<x>abc123…` is still redacted.
|
|
787
|
+
Findings from custom detectors are never dropped this way. The value-free debug
|
|
788
|
+
event `{event: 'template-value', rule}` reports each skipped match.
|
|
789
|
+
|
|
790
|
+
## Per-type thresholds
|
|
791
|
+
|
|
792
|
+
`minConfidenceByType` maps finding types to a threshold that overrides
|
|
793
|
+
`minConfidence` for those types only, as in Presidio score thresholds or
|
|
794
|
+
Google's per-infoType likelihood:
|
|
795
|
+
|
|
796
|
+
```js
|
|
797
|
+
sanitize(text, { minConfidence: 0.5, minConfidenceByType: { PERSON: 0.8 } });
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Values must be numbers from 0 to 1. Credentials are never filtered by a
|
|
801
|
+
threshold.
|
|
802
|
+
|
|
803
|
+
## Character masks
|
|
804
|
+
|
|
805
|
+
The `mask` transformation accepts Presidio/Google-style tuning in addition to
|
|
806
|
+
`keepStart`, `keepEnd`, `minLength` and `marker`:
|
|
807
|
+
|
|
808
|
+
| Option | Meaning |
|
|
809
|
+
| --------------- | -------------------------------------------------------------- |
|
|
810
|
+
| `char` | One masking character (default `*`) |
|
|
811
|
+
| `ignore` | Characters that are never masked, e.g. `'-@.'` |
|
|
812
|
+
| `ignoreClasses` | Any of `digits`, `upper`, `lower`, `punctuation`, `whitespace` |
|
|
813
|
+
| `count` | Mask at most this many characters |
|
|
814
|
+
| `fromEnd` | Mask the last `count` characters instead of the first |
|
|
815
|
+
|
|
816
|
+
```js
|
|
817
|
+
sanitize('Card 4111-1111-1111-1111', {
|
|
818
|
+
transformation: { mode: 'mask', char: '#', ignore: '-', count: 12 },
|
|
819
|
+
});
|
|
820
|
+
// → 'Card ####-####-####-1111'
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
`marker` cannot be combined with the tuning options. A value with nothing left
|
|
824
|
+
to mask is fully redacted. Masks apply to personal data only.
|
|
825
|
+
|
|
826
|
+
## Deterministic encryption
|
|
827
|
+
|
|
828
|
+
`{mode: 'encrypt', key}` replaces each personal value with
|
|
829
|
+
`[ENCRYPTED:TYPE:…]`, comparable to Google's `CryptoDeterministicConfig` and
|
|
830
|
+
Presidio's `encrypt` operator. The token is AES-256-GCM with a synthetic IV
|
|
831
|
+
(HMAC of type and value), so equal values of one type produce equal tokens and
|
|
832
|
+
joins or counts still work. Keys are at least 16 UTF-8 bytes and are expanded
|
|
833
|
+
with HKDF-SHA256.
|
|
834
|
+
|
|
835
|
+
```js
|
|
836
|
+
import { sanitize, decrypt } from '@link-foundation/sensitive-data-sanitizer';
|
|
837
|
+
const key = process.env.SANITIZER_KEY;
|
|
838
|
+
const { text } = sanitize('Mail alice@private.test', {
|
|
839
|
+
transformation: { mode: 'encrypt', key },
|
|
840
|
+
});
|
|
841
|
+
decrypt(text, key); // → 'Mail alice@private.test'
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
`decrypt(text, key, {json: true})` escapes restored values inside JSON string
|
|
845
|
+
literals. A token that fails authentication throws `ERR_MAP`; a non-string text
|
|
846
|
+
throws `ERR_INPUT`. Credentials are still fully redacted and never encrypted.
|
|
847
|
+
Anyone holding the key can recover the values, so treat encrypted output as
|
|
848
|
+
pseudonymised, not anonymous.
|
|
849
|
+
|
|
850
|
+
## Declarative pattern detectors
|
|
851
|
+
|
|
852
|
+
`createPatternDetector(specs, {id})` builds a synchronous Detector from
|
|
853
|
+
Presidio-style pattern recognizers or Google custom infoTypes without writing
|
|
854
|
+
code:
|
|
855
|
+
|
|
856
|
+
```js
|
|
857
|
+
import {
|
|
858
|
+
createPatternDetector,
|
|
859
|
+
sanitize,
|
|
860
|
+
} from '@link-foundation/sensitive-data-sanitizer';
|
|
861
|
+
const detector = createPatternDetector([
|
|
862
|
+
{
|
|
863
|
+
type: 'MEDICAL_RECORD',
|
|
864
|
+
category: 'personal',
|
|
865
|
+
patterns: [{ regex: /\bMRN-\d{6}\b/g, confidence: 0.4 }],
|
|
866
|
+
denyList: ['Mr. Bean'],
|
|
867
|
+
context: ['mrn', 'medical record'],
|
|
868
|
+
contextWindow: { before: 64, after: 16 },
|
|
869
|
+
contextBoost: 0.35,
|
|
870
|
+
validate: (value) => value !== 'MRN-000000',
|
|
871
|
+
},
|
|
872
|
+
]);
|
|
873
|
+
sanitize('medical record MRN-123456', { detectors: [detector] });
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
| Field | Meaning |
|
|
877
|
+
| ------------------ | ---------------------------------------------------------------------------------------------- |
|
|
878
|
+
| `type`, `category` | Uppercase type; `personal` or `credential` |
|
|
879
|
+
| `rule` | Lowercase rule id (default `custom-pattern`) |
|
|
880
|
+
| `confidence` | Base confidence (default 0.6) |
|
|
881
|
+
| `patterns` | Up to 64 `RegExp`, strings or `{regex, confidence, group}` (a capture group narrows the span) |
|
|
882
|
+
| `denyList` | Whole-word values, case-insensitive unless `caseSensitive: true`; `denyListConfidence` (1) |
|
|
883
|
+
| `context` | Hotwords (strings or RegExp) searched in `contextWindow` (number or `{before, after}`, ≤ 4096) |
|
|
884
|
+
| `contextBoost` | Added when a hotword is present (default 0.35, floor 0.4); a negative value lowers confidence |
|
|
885
|
+
| `validate` | `false` drops a match; a number sets its confidence |
|
|
886
|
+
|
|
887
|
+
Malformed specifications throw `ERR_CONFIG`.
|
|
888
|
+
|
|
889
|
+
`createIdentifierDetector(rules, {id})` adapts rules with the shape of the
|
|
890
|
+
exported `nationalRules` table (`{type, pattern, context, checksum, plausible?,
|
|
891
|
+
valid?}`) so callers can add or replace national identifiers. `identifierChecks`
|
|
892
|
+
exposes the checksum and format validators (Luhn, mod-11, mod-97, Verhoeff and
|
|
893
|
+
the national algorithms) for reuse; a valid checksum never proves issuance.
|
|
894
|
+
|
|
895
|
+
## Offline token checksums
|
|
896
|
+
|
|
897
|
+
GitHub (`ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`) and npm (`npm_`) tokens end in
|
|
898
|
+
six base62 characters holding the CRC32 of the 30 characters after the first
|
|
899
|
+
`_`. As in Kingfisher and osv-scalibr, the native `github`/`npm` rules and the
|
|
900
|
+
vendored Gitleaks/Betterleaks rules give a checksum-valid token confidence
|
|
901
|
+
0.99. A token whose checksum does not match keeps its ordinary confidence and
|
|
902
|
+
is still redacted, because truncated or retyped tokens remain sensitive. No
|
|
903
|
+
network request is made. The exported `tokenChecksum(value)` returns `true`,
|
|
904
|
+
`false` or `undefined` (not a checksummed format), and
|
|
905
|
+
`tokenWithChecksum(prefix, body)` builds checksum-valid test fixtures at
|
|
906
|
+
runtime so repositories need not store token literals.
|
|
907
|
+
|
|
908
|
+
## SARIF reports
|
|
909
|
+
|
|
910
|
+
`toSarif(report, {version, path})` converts a CLI `scan` report, an array of
|
|
911
|
+
`{path, findings}` or a findings array into SARIF 2.1.0 for GitHub code
|
|
912
|
+
scanning and other CI viewers. Rules are keyed by `finding.rule`; credential
|
|
913
|
+
findings are `error` and personal findings `warning`. Regions carry
|
|
914
|
+
`charOffset`, `charLength`, `startLine` and `startColumn` in UTF-16 code units.
|
|
915
|
+
Skipped files become tool execution notifications. Values are never included.
|
|
916
|
+
|
|
917
|
+
```sh
|
|
918
|
+
sensitive-data-sanitizer scan --format sarif src > results.sarif
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
`--format` accepts `json` (default) or `sarif` and is only valid for `scan`.
|
|
922
|
+
|
|
923
|
+
CLI `scan` accepts several paths, so a CI step or a
|
|
924
|
+
[pre-commit](https://pre-commit.com) hook can pass the staged files directly.
|
|
925
|
+
The repository ships `.pre-commit-hooks.yaml` with two hooks:
|
|
926
|
+
`sensitive-data-sanitizer` (credentials only) and
|
|
927
|
+
`sensitive-data-sanitizer-pii` (credentials and personal data).
|
|
928
|
+
|
|
929
|
+
```yaml
|
|
930
|
+
repos:
|
|
931
|
+
- repo: https://github.com/link-foundation/sensitive-data-sanitizer
|
|
932
|
+
rev: v0.20.0
|
|
933
|
+
hooks:
|
|
934
|
+
- id: sensitive-data-sanitizer
|
|
935
|
+
```
|
package/docs/COVERAGE.md
CHANGED
|
@@ -6,7 +6,7 @@ Coverage is a union of maintained detectors, explicit known values, context and
|
|
|
6
6
|
|
|
7
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
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
|
|
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 1 MiB (re2-wasm has a fixed 16 MiB heap shared by every compiled rule and copied subject). Dense rule matches switch to RE2JS after at most eight WASM matches per view, bounding repeated whole-input copies without truncating secrets or changing anchors. The MIT license, commit and sync script are retained in `src/vendor/gitleaks`. Because Gitleaks is no longer updated, 203 further rules from its maintained successor Betterleaks 1.10.0 (ids absent from the Gitleaks set, such as Groq, Cerebras, xAI, Vercel, Supabase and Cloudflare v2 tokens) run the same way as `betterleaks/<id>` findings, with their MIT license, provenance and `scripts/sync-betterleaks.py` in `src/vendor/betterleaks`. Their entropy floors and secret-only `matchesAny`/`containsAny` filters are honoured; BPE token-ratio and nearby-line filters, live validation, validation-only components and the context-dependent `generic-*` rules are not, so these rules can only report more than Betterleaks, not less. 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
10
|
|
|
11
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
12
|
|
|
@@ -185,7 +185,7 @@ records standards, alternatives and regression coverage for each feature.
|
|
|
185
185
|
| Local subprocess | Default 30 seconds and 10 MiB report/stdout cap; failures block output |
|
|
186
186
|
| Git objects | 10,000 objects, 10 MiB per object and 100 MiB total by default |
|
|
187
187
|
|
|
188
|
-
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.
|
|
188
|
+
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. The detection view additionally decodes `\uXXXX`, `\u{X}`, `\x{X}`, `\UXXXXXXXX` and `%uXXXX` escapes, runs of four or more `U+XXXX` code points, CSS escape runs (`\74 \6f \6b`), drops NUL bytes of UTF-16 text read as single bytes, zero-width and bidirectional format characters and ANSI colour sequences, removes inline HTML formatting tags (`<em>`, `<span class=…>`) and turns block tags into line breaks, while findings keep their original offsets. A finding inside a `U+` or CSS escape run redacts the whole run, so no undecodable fragment of it remains. The CLI `redact` and `scan` commands also read UTF-16LE/BE files that start with a byte order mark and write redacted output back in the same encoding; BOM-less UTF-16 and stream input must be UTF-8. 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.
|
|
189
189
|
|
|
190
190
|
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.
|
|
191
191
|
|
|
@@ -193,7 +193,7 @@ An unchanged result is not proof that text contains no sensitive information. Th
|
|
|
193
193
|
|
|
194
194
|
## Entity catalogs and optional modes
|
|
195
195
|
|
|
196
|
-
Documented Google/Azure/AWS catalog names (
|
|
196
|
+
Documented Google/Azure/AWS catalog names (214/180/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.
|
|
197
197
|
|
|
198
198
|
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.
|
|
199
199
|
|
|
@@ -476,7 +476,25 @@ proper-name mode. Romashka Inc keeps its existing legal-form treatment.
|
|
|
476
476
|
`strict: true` independently opts into personal attributes (gender, family,
|
|
477
477
|
ethnicity, religion, health, criminal/political status) and bare dates, ages,
|
|
478
478
|
passport/mobile/postal shapes. It is intended for stored personal prose and can
|
|
479
|
-
remove ordinary uses of
|
|
479
|
+
remove ordinary uses of unambiguous category words. Bounded Russian case/stem
|
|
480
|
+
rules cover disability, ethnicity, religion, diagnoses, intimate life and criminal
|
|
481
|
+
status, including `инвалидом`, `армянин`, `еврейка`, `казах`, `христианин`, `иудей`,
|
|
482
|
+
`эпилепсией`, `астма`, `онкологию`, `туберкулез`, `психиатра`, `депрессия`,
|
|
483
|
+
`гепатит`, `гей` and `отбывал наказание`. English coverage includes `epilepsy`,
|
|
484
|
+
`gay` and `Armenian`. Explicit category cues such as `по национальности X`,
|
|
485
|
+
`исповедует X`, `болеет X`, `на учете у X`, `diagnosed with X` and `ethnicity: X`
|
|
486
|
+
protect one following word (including hyphenated words) up to 64 characters,
|
|
487
|
+
including unfamiliar values outside the lexicon. These cues stay on one line.
|
|
488
|
+
Ambiguous English `disabled`, `single`, `man`, `men`, `russian`
|
|
489
|
+
and `asian` require an adjacent personal cue (pronoun/person role, personal
|
|
490
|
+
label, or following person role). Thus `he is disabled`, `candidate is single`
|
|
491
|
+
and `gender: male` redact, while `the module is disabled`, `single run finished`,
|
|
492
|
+
`single-flight lock acquired`, `man page missing`, `locale russian selected`
|
|
493
|
+
and `asian region endpoint` remain intact. Bare operational JSON statuses
|
|
494
|
+
`disabled`, `single` and `Russian` also remain intact; explicit personal labels
|
|
495
|
+
and caller-declared known personal values still redact. A cue in another clause
|
|
496
|
+
or string value does not make an ambiguous word personal.
|
|
497
|
+
It preserves ISO timestamps, work durations
|
|
480
498
|
(including `Стаж: 5 лет`, `работал там 5 лет`, and duration cues from issue 87),
|
|
481
499
|
and parenthesized durations after known technologies such as `Kubernetes (5 лет)`
|
|
482
500
|
and `Power BI (5 лет)`.
|