@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/docs/API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Library and detector contract
2
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.
3
+ Import the root entry with `import` or `require`; 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
4
 
5
5
  ## Methods
6
6
 
@@ -16,9 +16,38 @@ 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
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
 
26
+ ### Credentials-only entry
27
+
28
+ `@link-foundation/sensitive-data-sanitizer/credentials` exports `inspect`,
29
+ `sanitize`, `redact`, `createSanitizer`, `createSyncSanitizer`, `REDACTED`,
30
+ `entropy`, `builtInRules`, `tokenChecksum`, `tokenWithChecksum` and the
31
+ `knownSecretsFrom*` helpers. It always runs with `credentialsOnly: true` and
32
+ does not load the personal-data detectors, faker or libphonenumber, so it starts
33
+ about three times faster than the root entry (about 0.25 s against 0.75 s on
34
+ Node.js 26). Use it for loggers that must mask every line from the first write:
35
+
36
+ ```js
37
+ const {
38
+ createSyncSanitizer,
39
+ knownSecretsFromEnv,
40
+ } = require('@link-foundation/sensitive-data-sanitizer/credentials');
41
+ const mask = createSyncSanitizer({ knownSecrets: knownSecretsFromEnv() });
42
+ console.log(mask.sanitize('Authorization: Bearer abc123def456ghi789').text);
43
+ // Authorization: Bearer [REDACTED]
44
+ ```
45
+
46
+ Options that need the personal-data modules, `credentialsOnly: false`,
47
+ `mode: 'fake'` and `properNames`, fail with `ERR_CONFIG`. Known personal values
48
+ (`knownPersonal`) still work. Both entries offer `import`, `require` (Node.js
49
+ 22.12 or later) and `default` export conditions.
50
+
22
51
  ## Options
23
52
 
24
53
  ### Replace only known values
@@ -67,6 +96,31 @@ Unicode letters, marks, digits and underscores bound those tokens. Ordinary
67
96
  technical names such as GPT-4, ISO-9001 and Windows-10 are not reserved. The map's format cannot change
68
97
  on reuse. A malformed map or ambiguous exact restoration blocks output.
69
98
 
99
+ `existing: 'keep'` on the placeholder transformation accepts input that already
100
+ contains tokens of the chosen namespace, for example the output of an earlier
101
+ pass or a model answer that quotes tokens. Those tokens stay as they are and are
102
+ never replaced, faked or treated as names; new values of a type are numbered
103
+ after the highest kept number of that type:
104
+
105
+ ```js
106
+ const policy = { transformation: { mode: 'placeholder', existing: 'keep' } };
107
+ sanitize('Person-1 встретил Анну Смирнову', policy).text;
108
+ // Person-1 встретил Person-2
109
+ ```
110
+
111
+ A second pass over sanitized text is then a fixed point. The default
112
+ `existing: 'error'` keeps the `ERR_PLACEHOLDER_COLLISION` check; other values and
113
+ a mix with per-type `transformations` are `ERR_CONFIG`. A kept token restores to
114
+ itself, so the map still reconstructs every newly replaced value exactly.
115
+
116
+ `reservedPlaceholderTypes()` returns the sorted, frozen list of every type a
117
+ built-in rule can turn into a token. `isPlaceholderToken(text, {format?, types?})`
118
+ checks whether `text` is one whole token of the namespace (`Person-1`,
119
+ `UsBankNumber-12`, `<PERSON_3>` with `format: '<{TYPE}_{n}>'`); `types` adds
120
+ custom types such as those of `createPatternDetector`. Both are also exported by
121
+ the credentials-only entry. The namespace pattern is compiled once per format and
122
+ cached.
123
+
70
124
  `createRestoreMap({format?})` creates an empty reusable map. Pass a map as
71
125
  `transformation.map` to continue a corpus; the operation updates that same
72
126
  object. A `createSanitizer()` engine also keeps its registry across calls.
@@ -147,6 +201,13 @@ JSON keys remain unchanged by automatic personal detection in all transformation
147
201
  modes. Explicit known values and credentials retain priority. Parseability and
148
202
  duplicate-key checks remain enforced. Plain redact/fake policies retain their
149
203
  existing metadata/key behavior. Use `structured` for valid structured input.
204
+ A whole JSON value replaced by a structured rule (`sensitiveFields`, or a
205
+ personal field such as `age` replaced with its JSON type) becomes one JSON
206
+ string token typed by its field: `{"notes":"любой текст"}` reads
207
+ `{"notes":"Sensitive-1"}` and `{"age":34}` reads `{"age":"Age-1"}`. Such findings
208
+ carry `valueType` when the field has a personal type. `restore()` brings back
209
+ the original JSON value, including numbers and objects. Other modes keep
210
+ `"[REDACTED]"`.
150
211
  The normal encoding limits and credential redaction policy continue to apply.
151
212
 
152
213
  CLI: `redact FILE --placeholder --map private.json` creates or continues a map;
@@ -165,7 +226,7 @@ new document recognizers; `documentEntities: false` turns them off explicitly.
165
226
  | `scripts` | `['Cyrillic']`; Unicode Script aliases that activate the prose rule; `[]` disables only script activation |
166
227
  | `profile` | Off; `'publication'` sets a 0.3 threshold and identity masking; `'documents'` enables document entities |
167
228
  | `knownOnly` | `false`; disable all automatic detection (including credentials, Secretlint and custom detectors); use explicit findings, known values and reused map identities |
168
- | `knownSecrets` | `[]`; exact literal credential values, regardless of length or format |
229
+ | `knownSecrets` | `[]`; exact literal credential values, regardless of length or format, plus their standalone hex (either case) and base64/base64url spellings |
169
230
  | `knownPersonal` | `[]`; `{type, value}` values with whole-word Unicode case-insensitive matching and bounded Russian forms; list parts to match them alone |
170
231
  | `publicEntities` | `[]`; exact PERSON/ORGANIZATION/EDUCATION/LOCATION/EMAIL exception with HTTPS evidence and review date |
171
232
  | `findings` | `[]`; validated detections for the original input, supplied by the caller |
@@ -192,7 +253,7 @@ new document recognizers; `documentEntities: false` turns them off explicitly.
192
253
  | `strict` | `false`; aggressive stored-log personal attributes and bare values |
193
254
  | `structuralFields` | `[]`; declared property names suppress weak name/ID/entropy heuristics in their string values |
194
255
 
195
- Unknown options and invalid limits are errors. `knownSecretsFromEnv(env, names?)` discovers credential-named variables (excluding auth configuration names) 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.
256
+ Unknown options and invalid limits are errors. `knownSecretsFromEnv(env, names?, {minLength?})` discovers credential-named variables (excluding configuration names) when names are omitted, or uses an explicit selection, deduplicating nonempty values (or values of at least `minLength` characters). 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.
196
257
 
197
258
  ## Required detectors
198
259
 
@@ -242,6 +303,7 @@ Without a profile, full redaction is the library default. `transformation` selec
242
303
  | `fake` | Keyed realistic names, valid dates/MRZ/checksummed numbers and reserved contact destinations |
243
304
  | `date-shift` | Personal ISO dates; finite integer `days`, UTC calendar arithmetic; invalid dates become opaque |
244
305
  | `bucket` | Personal safe integers; positive integer `size`, inclusive numeric interval |
306
+ | `encrypt` | Personal data only; deterministic AES-256-GCM token reversible with `decrypt` and the key (see below) |
245
307
 
246
308
  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
309
 
@@ -255,7 +317,7 @@ Generated replacements carry `faked: true`. A private bounded registry recognize
255
317
 
256
318
  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).
257
319
 
258
- `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.
320
+ `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. Wrapped base64 and hex keep their line width and line separator (`\n` or `\r\n`), and a wrapped run is reported as one finding. When the sanitized payload cannot be re-encoded in a form the decoder recognizes (for example hex shorter than 32 digits), the encoded text is masked as it stands rather than blocking the input. Known encoding candidates and nesting are bounded.
259
321
 
260
322
  ## Identity policy and structured sessions
261
323
 
@@ -303,9 +365,15 @@ status identifiers. Explicit `knownPersonal` values override these heuristics.
303
365
 
304
366
  Oversized encoded runs receive a conservative complete-run `ENCODED_LIMIT`
305
367
  redaction rather than aborting the record. Decoding probes are capped at 4,096
306
- characters; full decoding stays capped at 8,192 characters. Whole-input,
307
- candidate-count, record and worker limits still apply. This fallback can remove
308
- large non-sensitive encoded outputs; its finding makes that decision auditable.
368
+ characters; full decoding stays capped at 8,192 characters. A multi-line run is
369
+ treated as one wrapped payload only when its probe decodes as text or its lines
370
+ share one base64 width (a multiple of four, standard alphabet, not plain hex);
371
+ lists of identifiers or commit hashes are scanned line by line. Up to 4,096
372
+ textually decodable candidates are scanned per 64 KiB window of the input;
373
+ further candidates in a denser window receive `ENCODED_LIMIT` redactions rather
374
+ than an `ERR_LIMIT` for the whole text. Whole-input, record and worker limits
375
+ still apply. This fallback can remove large non-sensitive encoded outputs; its
376
+ finding makes that decision auditable.
309
377
 
310
378
  ## Streaming and worker publication
311
379
 
@@ -368,7 +436,7 @@ The optional ESLint plugin exports `require-sanitized-output` from the package's
368
436
 
369
437
  ## Native documented entity catalogs
370
438
 
371
- `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.
439
+ `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
440
 
373
441
  ## Two-pass session publication
374
442
 
@@ -446,12 +514,28 @@ findings retain the normal engine union, confidence rules and verification.
446
514
  This keeps EMAIL/account-ID values without per-value `publicEntities` entries.
447
515
  The default policy continues to protect personal data.
448
516
 
449
- `knownSecretsFromEnv(env = process.env, names?)` discovers nonempty exact values
450
- when `names` is omitted, using
451
- `/(?:^|_)(?:TOKEN|(?:API|ACCESS|PRIVATE)?KEY|SECRET|PASSWORD|PASSWD|CREDENTIALS?|AUTH|PAT)(?:_|$)/i`.
452
- This covers `MY_SERVICE_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `CODEX_API_KEY`,
453
- `OPENCODE_API_KEY`, `DASHSCOPE_API_KEY`, `HF_TOKEN` and `TELEGRAM_BOT_TOKEN`.
454
- Automatic discovery excludes names ending in `AUTH_TRANSPORT`, `AUTH_MODE`, `AUTH_METHOD`, `AUTH_TYPE`, `AUTH_URL`, `AUTH_URI`, `AUTH_HOST`, `AUTH_HOSTNAME`, `AUTH_PATH`, `AUTH_ENABLED`, `AUTH_DISABLED`, `AUTH_PORT`, `AUTH_PROTOCOL` or `AUTH_SCHEME` (including prefixed forms such as `HIVE_MIND_GIT_AUTH_TRANSPORT`). Credential names such as `AUTH_TOKEN` still contribute their exact values, including short/low-entropy values; no value-shape filter can reliably distinguish passwords from configuration. An explicit array bypasses the name exclusions; `[]` selects nothing.
517
+ `knownSecretsFromEnv(env = process.env, names?, { minLength = 1 }?)` discovers
518
+ nonempty exact values when `names` is omitted. A name is selected when it has a
519
+ credential keyword between `_` boundaries
520
+ (`/(?:^|_)(?:TOKEN|(?:API|ACCESS|PRIVATE)?KEY|SECRET|PASSWORD|PASSWD|CREDENTIALS?|O?AUTH|PAT|COOKIE)(?:_|$)/i`)
521
+ or ends in `PASSWORD`, `PASSWD`, `TOKEN` or `COOKIE` without one. This covers
522
+ `MY_SERVICE_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `CODEX_API_KEY`,
523
+ `OPENCODE_API_KEY`, `DASHSCOPE_API_KEY`, `HF_TOKEN`, `TELEGRAM_BOT_TOKEN`,
524
+ `PGPASSWORD`, `GITHUBTOKEN`, `OAUTH` and `SESSION_COOKIE`.
525
+ Automatic discovery excludes names whose last segment describes configuration
526
+ rather than a credential: `_TRANSPORT`, `_MODE`, `_METHOD`, `_TYPE`, `_URL`,
527
+ `_URI`, `_HOST`, `_HOSTNAME`, `_PATH`, `_ENABLED`, `_DISABLED`, `_PORT`,
528
+ `_PROTOCOL`, `_SCHEME`, `_GUARD`, `_COUNT`, `_LIMIT`, `_FILE`, `_FILENAME`,
529
+ `_DIR`, `_DIRECTORY`, `_NAME` and `_ID` (for example `HIVE_MIND_GIT_AUTH_TRANSPORT`,
530
+ `HIVE_MIND_ROUTER_AUTH_GUARD`, `MAX_TOKEN_COUNT`, `SSH_KEY_PATH`, `SECRET_NAME`
531
+ and `AWS_ACCESS_KEY_ID`), plus `GOOGLE_APPLICATION_CREDENTIALS` (a file path) and
532
+ `GIT_CONFIG_KEY_<n>` (the name of a git setting). `GIT_CONFIG_VALUE_<n>` is
533
+ selected when its `GIT_CONFIG_KEY_<n>` names a header, password, token, secret,
534
+ cookie file or auth setting, such as `http.extraheader`. Credential names such as
535
+ `AUTH_TOKEN` and `AUTH_MODE_TOKEN` still contribute their exact values, including
536
+ short/low-entropy values; no value-shape filter can reliably distinguish passwords
537
+ from configuration. Pass `minLength` to skip shorter values. An explicit array
538
+ bypasses the name exclusions; `[]` selects nothing.
455
539
  These values are loaded only when the helper or CLI `--env-secrets` is requested.
456
540
 
457
541
  `await knownSecretsFromGitHubConfig({path?})` reads `oauth_token` and `api_token`
@@ -462,6 +546,9 @@ symlinks, aliases, files above 1 MiB and read failures produce value-free
462
546
  `ERR_AUTH`. YAML quoting is parsed with the `yaml` package, with alias expansion
463
547
  disabled. CLI `--gh-config` adds these exact values to `knownSecrets`.
464
548
  `knownSecretsFromGitHubAuth` / `--gh-auth` remain available independently.
549
+ A `hosts.yml` that uses YAML aliases or is a symlink stays a hard `ERR_AUTH`
550
+ failure, so a caller relying on `--gh-config` never runs with a silently
551
+ incomplete list.
465
552
 
466
553
  Plain log streams and CLI file/stdin/output/in-place paths preserve invalid
467
554
  UTF-8 bytes exactly. Valid Unicode keeps its normal meaning for detection.
@@ -554,9 +641,33 @@ also applies independently within serialized JSON strings, to the existing
554
641
  bounded decoding depth. Findings do not seed document-wide name propagation.
555
642
  Repeated redaction is stable. Reusing a placeholder map for the same original
556
643
  produces stable tokens and exact restoration; the existing placeholder-namespace
557
- collision checks still reject input that already contains typed placeholders.
644
+ collision checks still reject input that already contains typed placeholders
645
+ unless the transformation sets `existing: 'keep'`.
646
+
647
+ Capitalized words that identify nobody stay readable, so placeholder output keeps
648
+ its sentence: pronouns (`Вас`, `Я`), role and label cues (`Кандидат`, `Сотрудник`,
649
+ the default and configured `roles` and `personalLabels`) anywhere, and greetings,
650
+ function words and verbs (past tense `Звонила`, `Перенёс`, `Работал` before a
651
+ lowercase word; present `Работаю`, `Рекомендует`) at the start of a sentence:
558
652
 
559
- This is a structural heuristic, so capitalized ordinary words also disappear.
653
+ ```js
654
+ sanitize('Кандидат Петров Иван Сергеевич. Звонила Анна Смирнова', {
655
+ properNames: 'capitalized',
656
+ transformation: { mode: 'placeholder', map: false },
657
+ }).text;
658
+ // Кандидат Person-1. Звонила Person-2
659
+ ```
660
+
661
+ A word that is also a given-name form (`Вера`, `Любовь`) never counts as such a
662
+ word, and a bare masculine `-л` form before a capitalized word may be a surname
663
+ (`Шагал Марк`), so both are still replaced. A compact organization, location,
664
+ school or person span found by the document rules (`ООО «Ромашка»`, `в Казани`)
665
+ replaces the proper-name fragments inside it as one typed value
666
+ (`Organization-1`, `Location-1`); spans with tabs or line breaks, or longer than
667
+ 160 characters, keep the separate fragments. A bare oblique surname (`Иванову`)
668
+ links to the one person whose name contains it.
669
+
670
+ Other capitalized ordinary words still disappear; this is a structural heuristic.
560
671
  Lowercase organizations without employment context stay; the document rules
561
672
  replace a lowercase employer after `работал в` / `работала в`. Complete contact
562
673
  and identifier findings take priority over overlapping proper-name fragments.
@@ -641,6 +752,13 @@ cues. Inputs allow at most 256 nonempty strings of 128 code units per list, with
641
752
  no line breaks/NUL. Unknown lists and malformed replacement records fail with
642
753
  `ERR_CONFIG`. Compilation uses a bounded 64-configuration cache.
643
754
 
755
+ `personalAttributes: ['вепс', 'старовер']` adds special-category stems to the
756
+ `strict` rule (it has no effect without `strict: true`). Each word matches as a
757
+ stem followed by up to eight letters, so `вепска` and `староверами` match, and the
758
+ words of a multi-word entry may be separated by spaces or tabs. Matches are
759
+ reported as `PERSONAL_ATTRIBUTE` with the `strict-attribute` rule. The default
760
+ list is empty.
761
+
644
762
  ## Callback transformations and stages
645
763
 
646
764
  ```js
@@ -742,3 +860,224 @@ one or both languages. Each invocation is limited to 1 Mi UTF-16 units; larger
742
860
  inputs throw `ERR_LIMIT`. `debug` is off by default and emits counts only. Core
743
861
  imports never load the optional entry or NLP.js; leaving it off adds no optional
744
862
  runtime/module cost.
863
+
864
+ ## Allow lists, suppression comments and template values
865
+
866
+ `allowList` keeps detected values that the caller has reviewed as safe, as in
867
+ Presidio's `allow_list`, Google's exclusion rules with
868
+ `MATCHING_TYPE_FULL_MATCH` and DataFog's `allowlist_patterns`. A string must
869
+ equal the whole detected value; a `RegExp` must match the whole value (it is
870
+ anchored internally, `g`/`y` flags are ignored). A partial match never shortens
871
+ a finding. At most 10 000 entries; anything else is `ERR_CONFIG`.
872
+
873
+ ```js
874
+ sanitize('Contact support@example.org or alice@private.test', {
875
+ allowList: ['support@example.org', /.+@example\.org/],
876
+ });
877
+ // → 'Contact support@example.org or [REDACTED]'
878
+ ```
879
+
880
+ `inlineAllow: true` honours the suppression comments of other scanners on the
881
+ same line: `gitleaks:allow`, `betterleaks:allow`, `kingfisher:allow`/`ignore`,
882
+ `trufflehog:ignore`, `sanitizer:allow`, detect-secrets/ripsecrets
883
+ `pragma: allowlist secret` and SonarQube `NOSONAR`. A line containing
884
+ `pragma: allowlist nextline secret` suppresses findings on the next line. It is
885
+ off by default because anyone who can write to a log could otherwise disable
886
+ redaction.
887
+
888
+ Both controls follow the same safety rules:
889
+
890
+ - declared `knownPersonal`/`knownSecrets` values, confirmed personal values and
891
+ structured `JSON_VALUE` denials are never exempted;
892
+ - credentials are exempted only together with `allowCredentialDisable: true`;
893
+ - they apply to text, structured and asynchronous paths alike.
894
+
895
+ Built-in credential rules also skip values that consist only of a template or
896
+ mask: `${PASSWORD}`, `$${PYPI_PASSWORD}`, `$DB_PASS`, `$1`, `<password>`,
897
+ `<Openverse API token>`, `[PASSWORD]`, `****`, `XXXXXXXX`, `...`, optionally
898
+ after `Bearer`/`Basic`/`token` and on either side of a `user:password` colon.
899
+ A mixed value such as `alice:${DB_PASSWORD}` or `<x>abc123…` is still redacted.
900
+ Findings from custom detectors are never dropped this way. The value-free debug
901
+ event `{event: 'template-value', rule}` reports each skipped match.
902
+
903
+ ### Exempting findings with a hook
904
+
905
+ `exempt(finding, text) => boolean` keeps findings that the caller's own policy
906
+ knows are not sensitive: a configuration flag, a token counter, a version
907
+ range, a value another tool already masked. It runs after detection and before
908
+ transformation, once per finding. The finding carries `value` and the offsets
909
+ into `text`. Return `true` to keep the span unchanged and `false` to transform
910
+ it. Any other result, including `undefined` from a forgotten `return`, is
911
+ `ERR_CONFIG`.
912
+
913
+ ```js
914
+ createSyncSanitizer({
915
+ credentialsOnly: true,
916
+ // The key before the value decides; offsets point into `text`.
917
+ exempt: (finding, text) =>
918
+ /FEATURE_TOKEN=$/.test(text.slice(0, finding.start)),
919
+ }).sanitize(
920
+ 'FEATURE_TOKEN=keep-this-value-123 other_token=keep-this-value-123'
921
+ );
922
+ // → 'FEATURE_TOKEN=keep-this-value-123 other_token=[REDACTED]'
923
+ ```
924
+
925
+ Unlike a callback transformation that returns the original value, which fails
926
+ with `ERR_RESIDUAL`, an exempted span is excluded from residual verification
927
+ by span, not by value. Verification of the published text calls the hook again
928
+ for every finding in the output, so the same value elsewhere is still caught
929
+ unless the hook keeps that span too. The hook is explicit code, so it can keep
930
+ credential findings without `allowCredentialDisable`. Declared
931
+ `knownSecrets`/`knownPersonal` values, confirmed personal values and structured
932
+ `JSON_VALUE` denials are never offered to it.
933
+
934
+ `createSanitizer` awaits a hook that returns a `Promise`.
935
+ `createSyncSanitizer` and `sanitize()` need a synchronous hook. A `Promise`
936
+ there is `ERR_CONFIG`.
937
+
938
+ ## Per-type thresholds
939
+
940
+ `minConfidenceByType` maps finding types to a threshold that overrides
941
+ `minConfidence` for those types only, as in Presidio score thresholds or
942
+ Google's per-infoType likelihood:
943
+
944
+ ```js
945
+ sanitize(text, { minConfidence: 0.5, minConfidenceByType: { PERSON: 0.8 } });
946
+ ```
947
+
948
+ Values must be numbers from 0 to 1. Credentials are never filtered by a
949
+ threshold.
950
+
951
+ ## Character masks
952
+
953
+ The `mask` transformation accepts Presidio/Google-style tuning in addition to
954
+ `keepStart`, `keepEnd`, `minLength` and `marker`:
955
+
956
+ | Option | Meaning |
957
+ | --------------- | -------------------------------------------------------------- |
958
+ | `char` | One masking character (default `*`) |
959
+ | `ignore` | Characters that are never masked, e.g. `'-@.'` |
960
+ | `ignoreClasses` | Any of `digits`, `upper`, `lower`, `punctuation`, `whitespace` |
961
+ | `count` | Mask at most this many characters |
962
+ | `fromEnd` | Mask the last `count` characters instead of the first |
963
+
964
+ ```js
965
+ sanitize('Card 4111-1111-1111-1111', {
966
+ transformation: { mode: 'mask', char: '#', ignore: '-', count: 12 },
967
+ });
968
+ // → 'Card ####-####-####-1111'
969
+ ```
970
+
971
+ `marker` cannot be combined with the tuning options. A value with nothing left
972
+ to mask is fully redacted. Masks apply to personal data only.
973
+
974
+ ## Deterministic encryption
975
+
976
+ `{mode: 'encrypt', key}` replaces each personal value with
977
+ `[ENCRYPTED:TYPE:…]`, comparable to Google's `CryptoDeterministicConfig` and
978
+ Presidio's `encrypt` operator. The token is AES-256-GCM with a synthetic IV
979
+ (HMAC of type and value), so equal values of one type produce equal tokens and
980
+ joins or counts still work. Keys are at least 16 UTF-8 bytes and are expanded
981
+ with HKDF-SHA256.
982
+
983
+ ```js
984
+ import { sanitize, decrypt } from '@link-foundation/sensitive-data-sanitizer';
985
+ const key = process.env.SANITIZER_KEY;
986
+ const { text } = sanitize('Mail alice@private.test', {
987
+ transformation: { mode: 'encrypt', key },
988
+ });
989
+ decrypt(text, key); // → 'Mail alice@private.test'
990
+ ```
991
+
992
+ `decrypt(text, key, {json: true})` escapes restored values inside JSON string
993
+ literals. A token that fails authentication throws `ERR_MAP`; a non-string text
994
+ throws `ERR_INPUT`. Credentials are still fully redacted and never encrypted.
995
+ Anyone holding the key can recover the values, so treat encrypted output as
996
+ pseudonymised, not anonymous.
997
+
998
+ ## Declarative pattern detectors
999
+
1000
+ `createPatternDetector(specs, {id})` builds a synchronous Detector from
1001
+ Presidio-style pattern recognizers or Google custom infoTypes without writing
1002
+ code:
1003
+
1004
+ ```js
1005
+ import {
1006
+ createPatternDetector,
1007
+ sanitize,
1008
+ } from '@link-foundation/sensitive-data-sanitizer';
1009
+ const detector = createPatternDetector([
1010
+ {
1011
+ type: 'MEDICAL_RECORD',
1012
+ category: 'personal',
1013
+ patterns: [{ regex: /\bMRN-\d{6}\b/g, confidence: 0.4 }],
1014
+ denyList: ['Mr. Bean'],
1015
+ context: ['mrn', 'medical record'],
1016
+ contextWindow: { before: 64, after: 16 },
1017
+ contextBoost: 0.35,
1018
+ validate: (value) => value !== 'MRN-000000',
1019
+ },
1020
+ ]);
1021
+ sanitize('medical record MRN-123456', { detectors: [detector] });
1022
+ ```
1023
+
1024
+ | Field | Meaning |
1025
+ | ------------------ | ---------------------------------------------------------------------------------------------- |
1026
+ | `type`, `category` | Uppercase type; `personal` or `credential` |
1027
+ | `rule` | Lowercase rule id (default `custom-pattern`) |
1028
+ | `confidence` | Base confidence (default 0.6) |
1029
+ | `patterns` | Up to 64 `RegExp`, strings or `{regex, confidence, group}` (a capture group narrows the span) |
1030
+ | `denyList` | Whole-word values, case-insensitive unless `caseSensitive: true`; `denyListConfidence` (1) |
1031
+ | `context` | Hotwords (strings or RegExp) searched in `contextWindow` (number or `{before, after}`, ≤ 4096) |
1032
+ | `contextBoost` | Added when a hotword is present (default 0.35, floor 0.4); a negative value lowers confidence |
1033
+ | `validate` | `false` drops a match; a number sets its confidence |
1034
+
1035
+ Malformed specifications throw `ERR_CONFIG`.
1036
+
1037
+ `createIdentifierDetector(rules, {id})` adapts rules with the shape of the
1038
+ exported `nationalRules` table (`{type, pattern, context, checksum, plausible?,
1039
+ valid?}`) so callers can add or replace national identifiers. `identifierChecks`
1040
+ exposes the checksum and format validators (Luhn, mod-11, mod-97, Verhoeff and
1041
+ the national algorithms) for reuse; a valid checksum never proves issuance.
1042
+
1043
+ ## Offline token checksums
1044
+
1045
+ GitHub (`ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`) and npm (`npm_`) tokens end in
1046
+ six base62 characters holding the CRC32 of the 30 characters after the first
1047
+ `_`. As in Kingfisher and osv-scalibr, the native `github`/`npm` rules and the
1048
+ vendored Gitleaks/Betterleaks rules give a checksum-valid token confidence
1049
+ 0.99. A token whose checksum does not match keeps its ordinary confidence and
1050
+ is still redacted, because truncated or retyped tokens remain sensitive. No
1051
+ network request is made. The exported `tokenChecksum(value)` returns `true`,
1052
+ `false` or `undefined` (not a checksummed format), and
1053
+ `tokenWithChecksum(prefix, body)` builds checksum-valid test fixtures at
1054
+ runtime so repositories need not store token literals.
1055
+
1056
+ ## SARIF reports
1057
+
1058
+ `toSarif(report, {version, path})` converts a CLI `scan` report, an array of
1059
+ `{path, findings}` or a findings array into SARIF 2.1.0 for GitHub code
1060
+ scanning and other CI viewers. Rules are keyed by `finding.rule`; credential
1061
+ findings are `error` and personal findings `warning`. Regions carry
1062
+ `charOffset`, `charLength`, `startLine` and `startColumn` in UTF-16 code units.
1063
+ Skipped files become tool execution notifications. Values are never included.
1064
+
1065
+ ```sh
1066
+ sensitive-data-sanitizer scan --format sarif src > results.sarif
1067
+ ```
1068
+
1069
+ `--format` accepts `json` (default) or `sarif` and is only valid for `scan`.
1070
+
1071
+ CLI `scan` accepts several paths, so a CI step or a
1072
+ [pre-commit](https://pre-commit.com) hook can pass the staged files directly.
1073
+ The repository ships `.pre-commit-hooks.yaml` with two hooks:
1074
+ `sensitive-data-sanitizer` (credentials only) and
1075
+ `sensitive-data-sanitizer-pii` (credentials and personal data).
1076
+
1077
+ ```yaml
1078
+ repos:
1079
+ - repo: https://github.com/link-foundation/sensitive-data-sanitizer
1080
+ rev: v0.20.0
1081
+ hooks:
1082
+ - id: sensitive-data-sanitizer
1083
+ ```
package/docs/COVERAGE.md CHANGED
@@ -6,9 +6,9 @@ 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 4 MiB. 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`. 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.
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
- 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.
11
+ Context rules recognize credential labels with bounded key affixes, JSON and nested escaped JSON, YAML/env/INI assignments, Ruby/Perl/PHP hash rockets before a quoted literal (`'password' => '…'`), shell flags including plural credential names (`--access-tokens`, but not `--max-tokens`), XML values (also with attributes and camelCase element names), values that start with markup (`token: <script>…`; a bare `<placeholder>` stays), all-caps CI/test credential markers that contain a digit (`SYNTHETIC_TOKEN_ALPHA_123456`, rule `marker-token`), 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; a number or boolean under a configuration name (`AUTH_MODE=2`, `ROUTER_AUTH_GUARD=0`) is a setting. 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
 
13
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
14
 
@@ -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 (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.
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
 
@@ -480,17 +480,27 @@ remove ordinary uses of unambiguous category words. Bounded Russian case/stem
480
480
  rules cover disability, ethnicity, religion, diagnoses, intimate life and criminal
481
481
  status, including `инвалидом`, `армянин`, `еврейка`, `казах`, `христианин`, `иудей`,
482
482
  `эпилепсией`, `астма`, `онкологию`, `туберкулез`, `психиатра`, `депрессия`,
483
- `гепатит`, `гей` and `отбывал наказание`. English coverage includes `epilepsy`,
484
- `gay` and `Armenian`. Explicit category cues such as `по национальности X`,
483
+ `гепатит`, `гей` and `отбывал наказание`, plus nationalities (`узбечка`,
484
+ `таджик`, `чеченец`, `грузинка`, `азербайджанец`, `цыганка`, `кореянка`),
485
+ religions (`протестант`), health (`инсульт`, `шизофрения`, `слабослышащий`,
486
+ `колясочник`, `передвигается на коляске`, `аутизм`), sexuality (`бисексуал`,
487
+ `трансгендер`), convictions (`сидел за кражу`) and trade-union membership
488
+ (`член профсоюза`). English coverage includes `epilepsy`, `gay`, `Armenian`,
489
+ `autism`, `transgender`, `wheelchair user`, `trade union member` and
490
+ `served time for`. Country, region and language words (`Узбекистан`, `Чувашия`,
491
+ `грузинский язык`) are not attributes. `vocabulary.personalAttributes` adds caller
492
+ stems. Explicit category cues such as `по национальности X`,
485
493
  `исповедует X`, `болеет X`, `на учете у X`, `diagnosed with X` and `ethnicity: X`
486
494
  protect one following word (including hyphenated words) up to 64 characters,
487
495
  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
496
+ Ambiguous English `disabled`, `single`, `man`, `men`, `russian`,
497
+ `asian`, `uzbek`, `korean`, `chinese`, `georgian`, `kazakh`, `roma` and
498
+ `union member` require an adjacent personal cue (pronoun/person role, personal
490
499
  label, or following person role). Thus `he is disabled`, `candidate is single`
491
500
  and `gender: male` redact, while `the module is disabled`, `single run finished`,
492
501
  `single-flight lock acquired`, `man page missing`, `locale russian selected`
493
- and `asian region endpoint` remain intact. Bare operational JSON statuses
502
+ `asian region endpoint`, `Korean locale enabled` and `union member list`
503
+ remain intact. Bare operational JSON statuses
494
504
  `disabled`, `single` and `Russian` also remain intact; explicit personal labels
495
505
  and caller-declared known personal values still redact. A cue in another clause
496
506
  or string value does not make an ambiguous word personal.