@link-foundation/sensitive-data-sanitizer 0.20.0 → 0.21.1
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 +14 -1
- package/bin/sensitive-data-sanitizer.js +1 -0
- package/docs/API.md +179 -16
- package/docs/COVERAGE.md +16 -6
- package/docs/RULES.md +1 -0
- package/examples/second-pass-placeholders.mjs +28 -0
- package/package.json +12 -3
- package/src/context.js +176 -25
- package/src/credential-spans.js +180 -0
- package/src/credentials-entry.d.ts +23 -0
- package/src/credentials-entry.js +62 -0
- package/src/credentials.js +8 -1
- package/src/detection.js +68 -37
- package/src/encoded.js +137 -51
- package/src/engines.js +33 -10
- package/src/exempt.js +72 -0
- package/src/fake-memory.js +32 -0
- package/src/fake.js +4 -28
- package/src/features.js +29 -0
- package/src/html-tags.js +8 -0
- package/src/identity-types.js +63 -0
- package/src/identity.js +3 -61
- package/src/index.d.ts +41 -2
- package/src/index.js +6 -0
- package/src/known.js +33 -15
- package/src/personal-features.js +39 -0
- package/src/personal.js +29 -5
- package/src/placeholder-types.js +164 -4
- package/src/placeholders.js +144 -45
- package/src/projection.js +1 -8
- package/src/proper-names.js +48 -5
- package/src/prose-words.js +116 -0
- package/src/public-technologies.js +32 -0
- package/src/public.js +3 -0
- package/src/record-worker.js +1 -0
- package/src/rule-catalog.js +2 -0
- package/src/rules.js +8 -0
- package/src/sanitizer.js +92 -21
- package/src/stream-worker.js +1 -0
- package/src/strict-attributes.js +60 -4
- package/src/strict.js +1 -1
- package/src/structured.js +2 -0
- package/src/transforms.js +38 -8
- package/src/vocabulary.js +39 -2
- package/src/worker-options.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e0e05a4: Cover 34 missing strict personal-attribute terms, preserve their typed placeholders over generic prose names, cache caller vocabulary and cue sets, avoid unnecessary placeholder scans and JSON parsing, and preserve exact public technology names in Cyrillic prose.
|
|
8
|
+
|
|
9
|
+
## 0.21.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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`.
|
|
14
|
+
|
|
3
15
|
## 0.20.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -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),
|
|
@@ -248,7 +261,7 @@ See [contributing](docs/CONTRIBUTING.md). `add`, `multiply` and `delay` live in
|
|
|
248
261
|
|
|
249
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.
|
|
250
263
|
|
|
251
|
-
Automatic `--env-secrets` discovery skips
|
|
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.
|
|
252
265
|
|
|
253
266
|
For shared tool logs, `--credentials-only --env-secrets --gh-config` preserves
|
|
254
267
|
public account details while redacting credential patterns and exact local
|
package/docs/API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Library and detector contract
|
|
2
2
|
|
|
3
|
-
Import the
|
|
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
|
|
|
@@ -23,6 +23,31 @@ Import the ESM root entry; complete declarations are in `src/index.d.ts`. Inputs
|
|
|
23
23
|
|
|
24
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).
|
|
25
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
|
+
|
|
26
51
|
## Options
|
|
27
52
|
|
|
28
53
|
### Replace only known values
|
|
@@ -71,6 +96,31 @@ Unicode letters, marks, digits and underscores bound those tokens. Ordinary
|
|
|
71
96
|
technical names such as GPT-4, ISO-9001 and Windows-10 are not reserved. The map's format cannot change
|
|
72
97
|
on reuse. A malformed map or ambiguous exact restoration blocks output.
|
|
73
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
|
+
|
|
74
124
|
`createRestoreMap({format?})` creates an empty reusable map. Pass a map as
|
|
75
125
|
`transformation.map` to continue a corpus; the operation updates that same
|
|
76
126
|
object. A `createSanitizer()` engine also keeps its registry across calls.
|
|
@@ -151,6 +201,13 @@ JSON keys remain unchanged by automatic personal detection in all transformation
|
|
|
151
201
|
modes. Explicit known values and credentials retain priority. Parseability and
|
|
152
202
|
duplicate-key checks remain enforced. Plain redact/fake policies retain their
|
|
153
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]"`.
|
|
154
211
|
The normal encoding limits and credential redaction policy continue to apply.
|
|
155
212
|
|
|
156
213
|
CLI: `redact FILE --placeholder --map private.json` creates or continues a map;
|
|
@@ -169,7 +226,7 @@ new document recognizers; `documentEntities: false` turns them off explicitly.
|
|
|
169
226
|
| `scripts` | `['Cyrillic']`; Unicode Script aliases that activate the prose rule; `[]` disables only script activation |
|
|
170
227
|
| `profile` | Off; `'publication'` sets a 0.3 threshold and identity masking; `'documents'` enables document entities |
|
|
171
228
|
| `knownOnly` | `false`; disable all automatic detection (including credentials, Secretlint and custom detectors); use explicit findings, known values and reused map identities |
|
|
172
|
-
| `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 |
|
|
173
230
|
| `knownPersonal` | `[]`; `{type, value}` values with whole-word Unicode case-insensitive matching and bounded Russian forms; list parts to match them alone |
|
|
174
231
|
| `publicEntities` | `[]`; exact PERSON/ORGANIZATION/EDUCATION/LOCATION/EMAIL exception with HTTPS evidence and review date |
|
|
175
232
|
| `findings` | `[]`; validated detections for the original input, supplied by the caller |
|
|
@@ -196,7 +253,7 @@ new document recognizers; `documentEntities: false` turns them off explicitly.
|
|
|
196
253
|
| `strict` | `false`; aggressive stored-log personal attributes and bare values |
|
|
197
254
|
| `structuralFields` | `[]`; declared property names suppress weak name/ID/entropy heuristics in their string values |
|
|
198
255
|
|
|
199
|
-
Unknown options and invalid limits are errors. `knownSecretsFromEnv(env, names?)` discovers credential-named variables (excluding
|
|
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.
|
|
200
257
|
|
|
201
258
|
## Required detectors
|
|
202
259
|
|
|
@@ -260,7 +317,7 @@ Generated replacements carry `faked: true`. A private bounded registry recognize
|
|
|
260
317
|
|
|
261
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).
|
|
262
319
|
|
|
263
|
-
`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.
|
|
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.
|
|
264
321
|
|
|
265
322
|
## Identity policy and structured sessions
|
|
266
323
|
|
|
@@ -308,9 +365,15 @@ status identifiers. Explicit `knownPersonal` values override these heuristics.
|
|
|
308
365
|
|
|
309
366
|
Oversized encoded runs receive a conservative complete-run `ENCODED_LIMIT`
|
|
310
367
|
redaction rather than aborting the record. Decoding probes are capped at 4,096
|
|
311
|
-
characters; full decoding stays capped at 8,192 characters.
|
|
312
|
-
|
|
313
|
-
|
|
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.
|
|
314
377
|
|
|
315
378
|
## Streaming and worker publication
|
|
316
379
|
|
|
@@ -451,12 +514,28 @@ findings retain the normal engine union, confidence rules and verification.
|
|
|
451
514
|
This keeps EMAIL/account-ID values without per-value `publicEntities` entries.
|
|
452
515
|
The default policy continues to protect personal data.
|
|
453
516
|
|
|
454
|
-
`knownSecretsFromEnv(env = process.env, names?)` discovers
|
|
455
|
-
when `names` is omitted
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
`
|
|
459
|
-
|
|
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.
|
|
460
539
|
These values are loaded only when the helper or CLI `--env-secrets` is requested.
|
|
461
540
|
|
|
462
541
|
`await knownSecretsFromGitHubConfig({path?})` reads `oauth_token` and `api_token`
|
|
@@ -467,6 +546,9 @@ symlinks, aliases, files above 1 MiB and read failures produce value-free
|
|
|
467
546
|
`ERR_AUTH`. YAML quoting is parsed with the `yaml` package, with alias expansion
|
|
468
547
|
disabled. CLI `--gh-config` adds these exact values to `knownSecrets`.
|
|
469
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.
|
|
470
552
|
|
|
471
553
|
Plain log streams and CLI file/stdin/output/in-place paths preserve invalid
|
|
472
554
|
UTF-8 bytes exactly. Valid Unicode keeps its normal meaning for detection.
|
|
@@ -559,9 +641,33 @@ also applies independently within serialized JSON strings, to the existing
|
|
|
559
641
|
bounded decoding depth. Findings do not seed document-wide name propagation.
|
|
560
642
|
Repeated redaction is stable. Reusing a placeholder map for the same original
|
|
561
643
|
produces stable tokens and exact restoration; the existing placeholder-namespace
|
|
562
|
-
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:
|
|
563
652
|
|
|
564
|
-
|
|
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.
|
|
565
671
|
Lowercase organizations without employment context stay; the document rules
|
|
566
672
|
replace a lowercase employer after `работал в` / `работала в`. Complete contact
|
|
567
673
|
and identifier findings take priority over overlapping proper-name fragments.
|
|
@@ -644,7 +750,29 @@ Cues ignore case and support bounded Russian noun/adjective forms; personal fiel
|
|
|
644
750
|
use normalized field names. Capitalized proper values are required after prose
|
|
645
751
|
cues. Inputs allow at most 256 nonempty strings of 128 code units per list, with
|
|
646
752
|
no line breaks/NUL. Unknown lists and malformed replacement records fail with
|
|
647
|
-
`ERR_CONFIG`. Compilation uses a bounded 64-configuration cache
|
|
753
|
+
`ERR_CONFIG`. Compilation uses a bounded 64-configuration cache and a weak
|
|
754
|
+
object-identity cache. Treat a vocabulary object as immutable after first use;
|
|
755
|
+
pass a new object to change its lists. Repeated lookups reuse compiled patterns
|
|
756
|
+
and a cue-word Set without normalizing or serializing the lists again.
|
|
757
|
+
|
|
758
|
+
`personalAttributes: ['вепс', 'старовер']` adds special-category stems to the
|
|
759
|
+
`strict` rule (it has no effect without `strict: true`). Each word matches as a
|
|
760
|
+
stem followed by up to eight letters, so `вепска` and `староверами` match, and the
|
|
761
|
+
words of a multi-word entry may be separated by spaces or tabs. Matches are
|
|
762
|
+
reported as `PERSONAL_ATTRIBUTE` with the `strict-attribute` rule. The default
|
|
763
|
+
list is empty.
|
|
764
|
+
|
|
765
|
+
Typed special-category attributes take precedence over generic `ProperName`
|
|
766
|
+
guesses, including capitalized caller stems in Cyrillic prose. Strict coverage
|
|
767
|
+
includes the 34 additional nationality, religion, health, criminal-record,
|
|
768
|
+
political and union terms reported in issue 144, even as standalone words.
|
|
769
|
+
|
|
770
|
+
With default `publicKnowledge`, exact public technology spellings such as Python,
|
|
771
|
+
SQL, PostgreSQL, Docker, Kubernetes, React, AWS and Azure remain readable when
|
|
772
|
+
the capitalized prose detector flags them. The offline catalog includes source
|
|
773
|
+
URLs in `src/public-technologies.js`. `publicKnowledge: false` disables this
|
|
774
|
+
exception; known private values and credentials still redact, and an adjacent
|
|
775
|
+
personal name never inherits a technology's exemption.
|
|
648
776
|
|
|
649
777
|
## Callback transformations and stages
|
|
650
778
|
|
|
@@ -787,6 +915,41 @@ A mixed value such as `alice:${DB_PASSWORD}` or `<x>abc123…` is still redacted
|
|
|
787
915
|
Findings from custom detectors are never dropped this way. The value-free debug
|
|
788
916
|
event `{event: 'template-value', rule}` reports each skipped match.
|
|
789
917
|
|
|
918
|
+
### Exempting findings with a hook
|
|
919
|
+
|
|
920
|
+
`exempt(finding, text) => boolean` keeps findings that the caller's own policy
|
|
921
|
+
knows are not sensitive: a configuration flag, a token counter, a version
|
|
922
|
+
range, a value another tool already masked. It runs after detection and before
|
|
923
|
+
transformation, once per finding. The finding carries `value` and the offsets
|
|
924
|
+
into `text`. Return `true` to keep the span unchanged and `false` to transform
|
|
925
|
+
it. Any other result, including `undefined` from a forgotten `return`, is
|
|
926
|
+
`ERR_CONFIG`.
|
|
927
|
+
|
|
928
|
+
```js
|
|
929
|
+
createSyncSanitizer({
|
|
930
|
+
credentialsOnly: true,
|
|
931
|
+
// The key before the value decides; offsets point into `text`.
|
|
932
|
+
exempt: (finding, text) =>
|
|
933
|
+
/FEATURE_TOKEN=$/.test(text.slice(0, finding.start)),
|
|
934
|
+
}).sanitize(
|
|
935
|
+
'FEATURE_TOKEN=keep-this-value-123 other_token=keep-this-value-123'
|
|
936
|
+
);
|
|
937
|
+
// → 'FEATURE_TOKEN=keep-this-value-123 other_token=[REDACTED]'
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
Unlike a callback transformation that returns the original value, which fails
|
|
941
|
+
with `ERR_RESIDUAL`, an exempted span is excluded from residual verification
|
|
942
|
+
by span, not by value. Verification of the published text calls the hook again
|
|
943
|
+
for every finding in the output, so the same value elsewhere is still caught
|
|
944
|
+
unless the hook keeps that span too. The hook is explicit code, so it can keep
|
|
945
|
+
credential findings without `allowCredentialDisable`. Declared
|
|
946
|
+
`knownSecrets`/`knownPersonal` values, confirmed personal values and structured
|
|
947
|
+
`JSON_VALUE` denials are never offered to it.
|
|
948
|
+
|
|
949
|
+
`createSanitizer` awaits a hook that returns a `Promise`.
|
|
950
|
+
`createSyncSanitizer` and `sanitize()` need a synchronous hook. A `Promise`
|
|
951
|
+
there is `ERR_CONFIG`.
|
|
952
|
+
|
|
790
953
|
## Per-type thresholds
|
|
791
954
|
|
|
792
955
|
`minConfidenceByType` maps finding types to a threshold that overrides
|
package/docs/COVERAGE.md
CHANGED
|
@@ -8,7 +8,7 @@ The native provider supplements in `src/rules.js` cover GitHub, GitLab, OpenAI,
|
|
|
8
8
|
|
|
9
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
|
|
|
@@ -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 `отбывал
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/docs/RULES.md
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Sanitizes a CV note into readable placeholders, then sanitizes the result
|
|
2
|
+
// again after a model added text, keeping the tokens already in it.
|
|
3
|
+
import {
|
|
4
|
+
createSyncSanitizer,
|
|
5
|
+
isPlaceholderToken,
|
|
6
|
+
} from '@link-foundation/sensitive-data-sanitizer';
|
|
7
|
+
|
|
8
|
+
const policy = (existing) => ({
|
|
9
|
+
profile: 'documents',
|
|
10
|
+
properNames: 'capitalized',
|
|
11
|
+
strict: true,
|
|
12
|
+
transformation: { mode: 'placeholder', map: false, existing },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const first = createSyncSanitizer(policy()).sanitize(
|
|
16
|
+
'Кандидат Петров Иван Сергеевич, тел. +7 916 123-45-67. Работал в ООО «Ромашка», живу в Казани. Перенёс инсульт.'
|
|
17
|
+
).text;
|
|
18
|
+
console.log(first);
|
|
19
|
+
// Кандидат Person-1, тел. Phone-1. Работал в Organization-1, живу в Location-1. Перенёс PersonalAttribute-1.
|
|
20
|
+
|
|
21
|
+
const answer = `${first} Рекомендует Анна Смирнова.`;
|
|
22
|
+
const second = createSyncSanitizer(policy('keep')).sanitize(answer).text;
|
|
23
|
+
console.log(second);
|
|
24
|
+
// ... Перенёс PersonalAttribute-1. Рекомендует Person-2.
|
|
25
|
+
|
|
26
|
+
console.log(
|
|
27
|
+
second.split(/[\s,.]+/u).filter((word) => isPlaceholderToken(word))
|
|
28
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-foundation/sensitive-data-sanitizer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Offline sensitive-text sanitization with credential, multilingual personal-data and scanner union detection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -27,12 +27,21 @@
|
|
|
27
27
|
"examples/reversible-placeholders.mjs",
|
|
28
28
|
"examples/prose-logs.mjs",
|
|
29
29
|
"docs/RULES.md",
|
|
30
|
-
"examples/custom-log-policy.mjs"
|
|
30
|
+
"examples/custom-log-policy.mjs",
|
|
31
|
+
"examples/second-pass-placeholders.mjs"
|
|
31
32
|
],
|
|
32
33
|
"exports": {
|
|
33
34
|
".": {
|
|
34
35
|
"types": "./src/index.d.ts",
|
|
35
|
-
"import": "./src/index.js"
|
|
36
|
+
"import": "./src/index.js",
|
|
37
|
+
"require": "./src/index.js",
|
|
38
|
+
"default": "./src/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./credentials": {
|
|
41
|
+
"types": "./src/credentials-entry.d.ts",
|
|
42
|
+
"import": "./src/credentials-entry.js",
|
|
43
|
+
"require": "./src/credentials-entry.js",
|
|
44
|
+
"default": "./src/credentials-entry.js"
|
|
36
45
|
},
|
|
37
46
|
"./eslint-plugin": {
|
|
38
47
|
"types": "./src/eslint-plugin.d.ts",
|