@link-foundation/sensitive-data-sanitizer 0.19.3 → 0.21.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 (71) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +22 -2
  3. package/bin/sensitive-data-sanitizer.js +50 -28
  4. package/docs/API.md +355 -16
  5. package/docs/COVERAGE.md +19 -9
  6. package/docs/RULES.md +218 -0
  7. package/examples/second-pass-placeholders.mjs +28 -0
  8. package/package.json +12 -3
  9. package/src/allow-list.js +93 -0
  10. package/src/confidence.js +24 -9
  11. package/src/context.js +287 -40
  12. package/src/credential-spans.js +180 -0
  13. package/src/credentials-entry.d.ts +23 -0
  14. package/src/credentials-entry.js +62 -0
  15. package/src/credentials.js +51 -0
  16. package/src/crypto-addresses.js +74 -0
  17. package/src/detection.js +87 -41
  18. package/src/document-person-cues.js +1 -1
  19. package/src/encoded.js +137 -51
  20. package/src/encrypt.js +95 -0
  21. package/src/engines.js +33 -10
  22. package/src/entities.js +4 -3
  23. package/src/exempt.js +72 -0
  24. package/src/fake-memory.js +32 -0
  25. package/src/fake.js +33 -29
  26. package/src/features.js +29 -0
  27. package/src/files.js +57 -12
  28. package/src/gitleaks.js +82 -27
  29. package/src/html-tags.js +8 -0
  30. package/src/identifier-detector.js +36 -0
  31. package/src/identity-types.js +63 -0
  32. package/src/identity.js +3 -47
  33. package/src/index.d.ts +157 -3
  34. package/src/index.js +13 -0
  35. package/src/known.js +33 -15
  36. package/src/national-checks.js +209 -0
  37. package/src/national.js +310 -92
  38. package/src/pattern-detector.js +219 -0
  39. package/src/personal-context.js +3 -2
  40. package/src/personal-evidence.js +233 -0
  41. package/src/personal-features.js +39 -0
  42. package/src/personal.js +66 -10
  43. package/src/placeholder-types.js +154 -4
  44. package/src/placeholders.js +127 -45
  45. package/src/projection.js +78 -26
  46. package/src/proper-names.js +48 -5
  47. package/src/prose-words.js +120 -0
  48. package/src/record-worker.js +1 -0
  49. package/src/rule-catalog.js +426 -0
  50. package/src/rules.js +8 -0
  51. package/src/sanitizer.js +133 -23
  52. package/src/sarif.js +127 -0
  53. package/src/stream-worker.js +1 -0
  54. package/src/strict-attributes.js +48 -4
  55. package/src/strict.js +1 -1
  56. package/src/structured.js +2 -0
  57. package/src/token-checksum.js +49 -0
  58. package/src/transforms.js +146 -33
  59. package/src/vendor/betterleaks/LICENSE +21 -0
  60. package/src/vendor/betterleaks/provenance.json +11 -0
  61. package/src/vendor/betterleaks/rules-0.json +463 -0
  62. package/src/vendor/betterleaks/rules-1.json +454 -0
  63. package/src/vendor/betterleaks/rules-2.json +461 -0
  64. package/src/vendor/betterleaks/rules-3.json +455 -0
  65. package/src/vendor/betterleaks/rules-4.json +483 -0
  66. package/src/vendor/betterleaks/rules-5.json +35 -0
  67. package/src/vendor/entities/aws.json +1 -1
  68. package/src/vendor/entities/azure.json +6 -2
  69. package/src/vendor/entities/google.json +2 -1
  70. package/src/vocabulary.js +27 -1
  71. package/src/worker-options.js +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d1d7a22: Resolve the sanitizer requests of issues 130–141. Placeholder mode accepts `transformation.existing: 'keep'` for a second pass over sanitized text, types whole JSON values (`Sensitive-1`, `Age-1`), and keeps pronouns, cue words and sentence-initial greetings and verbs readable beside `Person-n`; compact organization and location spans become one typed token. `reservedPlaceholderTypes()` and `isPlaceholderToken()` expose the namespace. `strict` covers more nationality, religion, health, sexuality, conviction and trade-union wording, and `vocabulary.personalAttributes` adds caller stems. A credentials-only entry (`@link-foundation/sensitive-data-sanitizer/credentials`) starts about three times faster, and both entries support `require`. Credential context stays readable and masks stay stable; configuration settings are kept and an `exempt(finding, text)` hook can keep findings by policy. New detections cover CI credential markers, XML attributes, hash rockets, markup values and plural credential flags. Wrapped base64 and hex keep their layout; candidates are bounded per 64 KiB window; `knownSecrets` also match their hex and base64 spellings. Environment secret discovery selects `PGPASSWORD`-style names and cookies, skips more configuration names, and accepts `minLength`.
8
+
9
+ ## 0.20.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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.
14
+
3
15
  ## 0.19.3
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 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.
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
 
@@ -20,6 +20,12 @@ two-pass publication:
20
20
  sensitive-data-sanitizer redact session.jsonl --stream --jsonl --two-pass --output safe.jsonl
21
21
  ```
22
22
 
23
+ For loggers that only mask credentials, import the
24
+ [credentials-only entry](docs/API.md#credentials-only-entry)
25
+ `@link-foundation/sensitive-data-sanitizer/credentials`: it skips the
26
+ personal-data modules and starts about three times faster. Both entries also
27
+ support `require`.
28
+
23
29
  For synchronous log hooks, `createSyncSanitizer` supports custom detectors and
24
30
  verified callback transformations. Rule/type controls, caller vocabulary,
25
31
  structured field selectors and `compileKnown` configure reusable policies; see
@@ -89,6 +95,13 @@ Known Russian names match in all six cases and in any word order, with `ё`/`е`
89
95
  spelling variants and initials (`с Петровым Данилом Сергеевичем`, `Петров Д. С.`).
90
96
  A part of a longer name is matched alone only when it is listed itself.
91
97
 
98
+ With `properNames: 'capitalized'`, placeholder output keeps the sentence around a
99
+ name (`Кандидат Person-1`, `Меня зовут Person-1`, `Благодарим Вас, Person-1`), and
100
+ `transformation.existing: 'keep'` lets a second pass over sanitized text keep its
101
+ `Type-n` tokens and number new values after them. `isPlaceholderToken` and
102
+ `reservedPlaceholderTypes` expose the namespace; see the
103
+ [second-pass example](examples/second-pass-placeholders.mjs).
104
+
92
105
  For irreversible output, choose `transformation: { mode: 'placeholder', map:
93
106
  false }`: no restore map is returned or written. Detection limits still apply
94
107
  in either mode. See the [placeholder API](docs/API.md#reversible-placeholders),
@@ -131,6 +144,7 @@ console.log(result.text); // password=[REDACTED] [REDACTED]
131
144
  printf 'password=short\n' | sensitive-data-sanitizer redact -
132
145
  sensitive-data-sanitizer redact input.txt --in-place
133
146
  sensitive-data-sanitizer scan logs --config /private/policy.json
147
+ sensitive-data-sanitizer scan src/a.js src/b.js --format sarif > report.sarif
134
148
  sensitive-data-sanitizer redact input.txt --gitleaks --trufflehog --output safe.txt
135
149
  sensitive-data-sanitizer redact input.txt --presidio /path/presidio-bridge.py --model en_core_web_sm --language en
136
150
  sensitive-data-sanitizer redact large-session.jsonl --jsonl --stream --output safe-session.jsonl
@@ -148,6 +162,8 @@ sensitive-data-sanitizer --help
148
162
 
149
163
  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
164
 
165
+ `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.
166
+
151
167
  ## Configuration
152
168
 
153
169
  For AI sessions, `--jsonl` preserves JSON records and declares `id`, `type`,
@@ -195,6 +211,8 @@ Keep policies containing real private values outside Git. Built-in exact public
195
211
  }
196
212
  ```
197
213
 
214
+ 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).
215
+
198
216
  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
217
 
200
218
  ## Extend detection
@@ -218,6 +236,8 @@ const sanitizer = createSanitizer({
218
236
 
219
237
  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
238
 
239
+ `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).
240
+
221
241
  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
242
 
223
243
  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.
@@ -241,7 +261,7 @@ See [contributing](docs/CONTRIBUTING.md). `add`, `multiply` and `delay` live in
241
261
 
242
262
  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.
243
263
 
244
- Automatic `--env-secrets` discovery skips authentication configuration names such as `GIT_AUTH_TRANSPORT`, `AUTH_MODE` and `AUTH_METHOD`. Explicit `knownSecretsFromEnv(env, names)` selections still include their exact values.
264
+ Automatic `--env-secrets` discovery also selects `PGPASSWORD`-style suffixes and cookies, and skips configuration names such as `GIT_AUTH_TRANSPORT`, `AUTH_MODE`, `*_AUTH_GUARD`, `MAX_TOKEN_COUNT`, `SSH_KEY_PATH`, `SECRET_NAME`, `AWS_ACCESS_KEY_ID` and `GIT_CONFIG_KEY_<n>`; `knownSecretsFromEnv(env, undefined, { minLength: 8 })` also skips short values. Explicit `knownSecretsFromEnv(env, names)` selections still include their exact values.
245
265
 
246
266
  For shared tool logs, `--credentials-only --env-secrets --gh-config` preserves
247
267
  public account details while redacting credential patterns and exact local
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import '../src/personal-features.js';
2
3
  import { readFileSync, realpathSync } from 'node:fs';
3
4
  import { fileURLToPath, URL } from 'node:url';
4
5
  import { resolve } from 'node:path';
@@ -6,9 +7,11 @@ import { createSanitizer } from '../src/engines.js';
6
7
  import { failure } from '../src/detection.js';
7
8
  import {
8
9
  readTextFile,
10
+ readTextInput,
9
11
  atomicWrite,
10
12
  listTextCandidates,
11
- decodeText,
13
+ decodeTextInput,
14
+ encodeTextOutput,
12
15
  } from '../src/files.js';
13
16
  import { auditGitHistory } from '../src/history.js';
14
17
  import { rewriteGitHistory } from '../src/history-rewrite.js';
@@ -17,7 +20,7 @@ import {
17
20
  knownSecretsFromEnv,
18
21
  knownSecretsFromGitHubConfig,
19
22
  } from '../src/known.js';
20
- import { encodeLogText } from '../src/log-bytes.js';
23
+ import { toSarif } from '../src/sarif.js';
21
24
  import { createWikidataVerifier } from '../src/public-verifier.js';
22
25
  import { streamCommand } from './stream.js';
23
26
  import { sessionStructuralFields } from '../src/structured.js';
@@ -31,7 +34,8 @@ import {
31
34
  const help = `Usage: sensitive-data-sanitizer <redact|scan|restore|history> [path|-] [options]
32
35
 
33
36
  redact Sanitize one text log or stdin; publish only after verification
34
- scan Report metadata for a file, directory, or stdin (never source text)
37
+ scan Report metadata for files, directories, or stdin (never source text);
38
+ several paths may follow scan (for example staged files)
35
39
  restore Restore placeholders in a file using --map PATH (private output)
36
40
  history Audit local reachable Git blobs, commit/tag metadata, and reflogs
37
41
  history rewrite SOURCE --output DIRECTORY [--apply]
@@ -67,6 +71,7 @@ history rewrite SOURCE --output DIRECTORY [--apply]
67
71
  --no-map Discard placeholder mapping after full anonymization
68
72
  --placeholder-format TEMPLATE Default {Type}-{n}; also [{TYPE}_{n}]
69
73
  --map-format FORMAT json or lino (otherwise inferred from .lino extension)
74
+ --format FORMAT scan report: json (default) or sarif (SARIF 2.1.0)
70
75
  --map-credentials Explicitly keep credentials in the private restore map
71
76
  --max-record-bytes N Maximum record bytes (text: total limit; JSONL: 8388608)
72
77
  --hive-mask Opt-in first/last 3 mask for values longer than 12 characters
@@ -129,8 +134,9 @@ function parseArgs(argv) {
129
134
  ['--map', 'map'],
130
135
  ['--map-format', 'mapFormat'],
131
136
  ['--placeholder-format', 'placeholderFormat'],
137
+ ['--format', 'format'],
132
138
  ]);
133
- let hasPath = false;
139
+ const paths = [];
134
140
  for (let i = rewrite ? 2 : 1; i < argv.length; i++) {
135
141
  const arg = argv[i];
136
142
  if (boolean.has(arg)) {
@@ -140,13 +146,18 @@ function parseArgs(argv) {
140
146
  throw failure('ERR_ARGUMENT');
141
147
  }
142
148
  config[valued.get(arg)] = argv[++i];
143
- } else if (!hasPath && !arg.startsWith('--')) {
144
- config.path = arg;
145
- hasPath = true;
149
+ } else if (!arg.startsWith('--')) {
150
+ paths.push(arg);
146
151
  } else {
147
152
  throw failure('ERR_ARGUMENT');
148
153
  }
149
154
  }
155
+ // Only scan accepts several paths (for example staged files from a hook).
156
+ if (paths.length > 1 && (command !== 'scan' || paths.includes('-'))) {
157
+ throw failure('ERR_ARGUMENT');
158
+ }
159
+ config.path = paths[0] ?? '-';
160
+ config.paths = paths.length ? paths : ['-'];
150
161
  validateArgs(config);
151
162
  return config;
152
163
  }
@@ -194,7 +205,11 @@ function validateFeatureArgs(config) {
194
205
 
195
206
  function validateStructuredArgs(config) {
196
207
  validateStreamArgs(config);
197
- if (config.twoPass && !config.stream) {
208
+ if (
209
+ (config.twoPass && !config.stream) ||
210
+ (config.format !== undefined &&
211
+ (config.command !== 'scan' || !['json', 'sarif'].includes(config.format)))
212
+ ) {
198
213
  throw failure('ERR_ARGUMENT');
199
214
  }
200
215
  if ((config.json && config.jsonl) || (config.stream && config.json)) {
@@ -249,7 +264,7 @@ async function stdinText(stream, maxBytes, options) {
249
264
  }
250
265
  chunks.push(buffer);
251
266
  }
252
- return decodeText(Buffer.concat(chunks), options);
267
+ return decodeTextInput(Buffer.concat(chunks), options);
253
268
  }
254
269
 
255
270
  export async function runCli(
@@ -266,9 +281,7 @@ export async function runCli(
266
281
  return 0;
267
282
  }
268
283
  if (['--version', '-v'].includes(argv[0])) {
269
- stdout.write(
270
- `${JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version}\n`
271
- );
284
+ stdout.write(`${packageVersion()}\n`);
272
285
  return 0;
273
286
  }
274
287
  const config = parseArgs(argv);
@@ -422,24 +435,20 @@ function inputText(path, config, stdin) {
422
435
  };
423
436
  return path === '-'
424
437
  ? stdinText(stdin, config.maxBytes, options)
425
- : readTextFile(path, config.maxBytes, options);
438
+ : readTextInput(path, config.maxBytes, options);
426
439
  }
427
440
 
428
441
  async function redactCommand(config, engine, { stdin, stdout }) {
429
- const result = await engine.sanitize(
430
- await inputText(config.path, config, stdin)
431
- );
442
+ const input = await inputText(config.path, config, stdin);
443
+ const result = await engine.sanitize(input.text);
432
444
  await saveMap(config);
445
+ const output = encodeTextOutput(result.text, input.encoding);
433
446
  if (config.output || config.inPlace) {
434
- await atomicWrite(
435
- config.output ?? config.path,
436
- encodeLogText(result.text),
437
- {
438
- replace: Boolean(config.inPlace),
439
- }
440
- );
447
+ await atomicWrite(config.output ?? config.path, output, {
448
+ replace: Boolean(config.inPlace),
449
+ });
441
450
  } else {
442
- stdout.write(encodeLogText(result.text));
451
+ stdout.write(output);
443
452
  }
444
453
  return 0;
445
454
  }
@@ -464,8 +473,10 @@ async function historyCommand(config, engine, { stdout }) {
464
473
  }
465
474
 
466
475
  async function scanCommand(config, engine, { stdin, stdout }) {
467
- const files =
468
- config.path === '-' ? ['-'] : await listTextCandidates(config.path);
476
+ const files = [];
477
+ for (const path of config.paths) {
478
+ files.push(...(path === '-' ? ['-'] : await listTextCandidates(path)));
479
+ }
469
480
  const results = [],
470
481
  skipped = [];
471
482
  for (const file of files) {
@@ -473,7 +484,9 @@ async function scanCommand(config, engine, { stdin, stdout }) {
473
484
  try {
474
485
  results.push({
475
486
  path,
476
- findings: await engine.inspect(await inputText(file, config, stdin)),
487
+ findings: await engine.inspect(
488
+ (await inputText(file, config, stdin)).text
489
+ ),
477
490
  });
478
491
  } catch (error) {
479
492
  if (['ERR_BINARY', 'ERR_ENCODING', 'ERR_LIMIT'].includes(error.code)) {
@@ -484,7 +497,10 @@ async function scanCommand(config, engine, { stdin, stdout }) {
484
497
  }
485
498
  }
486
499
  const complete = skipped.length === 0;
487
- stdout.write(`${JSON.stringify({ complete, results, skipped }, null, 2)}\n`);
500
+ const report = { complete, results, skipped };
501
+ stdout.write(
502
+ `${JSON.stringify(config.format === 'sarif' ? toSarif(report, { version: packageVersion() }) : report, null, 2)}\n`
503
+ );
488
504
  return !complete ? 2 : results.some((r) => r.findings.length) ? 1 : 0;
489
505
  }
490
506
 
@@ -495,6 +511,12 @@ if (
495
511
  process.exitCode = await runCli(process.argv.slice(2));
496
512
  }
497
513
 
514
+ function packageVersion() {
515
+ return JSON.parse(
516
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8')
517
+ ).version;
518
+ }
519
+
498
520
  function defaultByteLimit(config) {
499
521
  return config.stream ? 1073741824 : 33554432;
500
522
  }