@link-foundation/sensitive-data-sanitizer 0.19.2 → 0.19.3

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 6bdaf11: Recognize additional inflected special-category attributes and unfamiliar values after explicit category cues in strict mode. Require personal context for ambiguous English attributes so operational logs and JSON statuses stay intact.
8
+
3
9
  ## 0.19.2
4
10
 
5
11
  ### Patch Changes
package/docs/API.md CHANGED
@@ -17,7 +17,7 @@ Import the ESM root entry; complete declarations are in `src/index.d.ts`. Inputs
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
19
 
20
- `redactions` counts merged replacement spans. `findings` can contain several detector hits for one replacement. Findings have `{start, end, type, category, rule, line, column}`; no text, quote, match, entropy score, provider details, or raw diagnostic is copied. Debugging is off by default; `debug(event)` receives value-free counts, rule timings, residual rounds and registry event metadata (see `DebugEvent` in the TypeScript declarations).
20
+ `redactions` counts merged replacement spans. `findings` can contain several detector hits for one replacement. Findings have `{start, end, type, category, rule, line, column}`; no text, quote, match, entropy score, provider details, or raw diagnostic is copied. 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
21
 
22
22
  ## Options
23
23
 
package/docs/COVERAGE.md CHANGED
@@ -476,7 +476,25 @@ proper-name mode. Romashka Inc keeps its existing legal-form treatment.
476
476
  `strict: true` independently opts into personal attributes (gender, family,
477
477
  ethnicity, religion, health, criminal/political status) and bare dates, ages,
478
478
  passport/mobile/postal shapes. It is intended for stored personal prose and can
479
- remove ordinary uses of these words. It preserves ISO timestamps, work durations
479
+ remove ordinary uses of unambiguous category words. Bounded Russian case/stem
480
+ rules cover disability, ethnicity, religion, diagnoses, intimate life and criminal
481
+ status, including `инвалидом`, `армянин`, `еврейка`, `казах`, `христианин`, `иудей`,
482
+ `эпилепсией`, `астма`, `онкологию`, `туберкулез`, `психиатра`, `депрессия`,
483
+ `гепатит`, `гей` and `отбывал наказание`. English coverage includes `epilepsy`,
484
+ `gay` and `Armenian`. Explicit category cues such as `по национальности X`,
485
+ `исповедует X`, `болеет X`, `на учете у X`, `diagnosed with X` and `ethnicity: X`
486
+ protect one following word (including hyphenated words) up to 64 characters,
487
+ including unfamiliar values outside the lexicon. These cues stay on one line.
488
+ Ambiguous English `disabled`, `single`, `man`, `men`, `russian`
489
+ and `asian` require an adjacent personal cue (pronoun/person role, personal
490
+ label, or following person role). Thus `he is disabled`, `candidate is single`
491
+ and `gender: male` redact, while `the module is disabled`, `single run finished`,
492
+ `single-flight lock acquired`, `man page missing`, `locale russian selected`
493
+ and `asian region endpoint` remain intact. Bare operational JSON statuses
494
+ `disabled`, `single` and `Russian` also remain intact; explicit personal labels
495
+ and caller-declared known personal values still redact. A cue in another clause
496
+ or string value does not make an ambiguous word personal.
497
+ It preserves ISO timestamps, work durations
480
498
  (including `Стаж: 5 лет`, `работал там 5 лет`, and duration cues from issue 87),
481
499
  and parenthesized durations after known technologies such as `Kubernetes (5 лет)`
482
500
  and `Power BI (5 лет)`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-foundation/sensitive-data-sanitizer",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
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",
package/src/index.d.ts CHANGED
@@ -118,6 +118,7 @@ export interface SanitizerOptions {
118
118
  }
119
119
  /** Value-free diagnostics; enabled only when a debug callback is supplied. */
120
120
  export type DebugEvent =
121
+ | { event: 'strict-attributes'; detected: number; suppressed: number }
121
122
  | { event: 'detected'; count: number }
122
123
  | { event: 'document-experience' | 'proper-name-prose'; count: number }
123
124
  | { event: 'residual-redaction'; round: number }
@@ -0,0 +1,71 @@
1
+ import { collectMatches } from './detection.js';
2
+
3
+ const attributes =
4
+ /(?<![\p{L}\p{N}_])(?:женщин[аыуеой]{1,2}|мужчин[аыуеой]{1,2}|замужем|женат(?:а|ы)?|(?:один|одна|двое|трое|четверо|пятеро|\d{1,2})[ \t]+(?:детей|реб[её]нок|реб[её]нка)|русск(?:ая|ий|ого|ой|ие)|татар(?:ин|ка|ы)|православн(?:ый|ая|ого|ой)|мусульман(?:ин|ка)|инвалидност[ьи](?:[ \t]+\d[ \t]+группы)?|беременн(?:а|ая|ость)|судимость(?:[ \t]+погашена)?|член[ \t]+партии|woman|women|male|female|married|divorced|pregnant|pregnancy|disability|christian|muslim|jewish|orthodox|hispanic|criminal[ \t]+record|party[ \t]+member|(?:one|two|three|\d{1,2})[ \t]+children)(?![\p{L}\p{N}_])/giu;
5
+ const additionalAttributes =
6
+ /(?<![\p{L}\p{N}_])(?:диабет(?:ом|а|у|е)?|ВИЧ(?:-положительн(?:ый|ая|ого|ой))?|болен|больна|больны|католик(?:ом|а|и)?|католичк(?:а|ой|и)|буддист(?:ка|кой|ом|а|ы)?|атеист(?:ка|кой|ом|а|ы)?|украин(?:ец|ка|цы|цем|кой)|белорус(?:ка|кой|ом|а|ы)?|несудим(?:а|ы)?|судим(?:а|ы)?|член[ \t]+политической[ \t]+партии|холост(?:а|ы)?|развед[её]н(?:а|ы)?|в[ \t]+разводе|diabetes|diabetic|HIV(?:[ \t-]+positive)?|cancer|buddhist|catholic|atheist|ukrainian|belarusian|previous[ \t]+conviction|member[ \t]+of[ \t]+(?:a[ \t]+)?(?:political[ \t]+)?party|unmarried|widowed)(?![\p{L}\p{N}_])/giu;
7
+
8
+ // Bounded suffixes cover Russian cases, plurals and adjectival forms without
9
+ // turning a stem into an unbounded substring search. Category cues below cover
10
+ // unfamiliar diagnoses, nationalities and religions outside this lexicon.
11
+ const categoryForms =
12
+ /(?<![\p{L}\p{N}_])(?:(?:диабет|католик|католич|буддист|атеист|белорус|православн|беременн|судим)[\p{L}]{0,8}|татар(?:ин(?:а|у|ом|е)?|к[аиуе]|кой|ы|ам|ами|ах)?|мусульман(?:ин(?:а|у|ом|е)?|к[аиуе]|кой|е|ам|ами|ах)?|украин(?:ец|ца|цу|цем|це|цы|цев|цам|цами|цах|к[аиуе]|кой)|инвалид(?:а|у|ом|е|ы|ов|ам|ами|ах|к[аиу]|кой)?|армян(?:ин|ина|ину|ином|ине|е|ам|ами|ах|ка|ки|ке|ку|кой)|евре(?:й|я|ю|ем|и|ев|ям|ями|ях|йк[аиуе]|йкой)|казах(?:а|у|ом|е|и|ов|ам|ами|ах|к[аиуе]|кой)?|христиан(?:ин|ина|ину|ином|ине|е|ам|ами|ах|к[аиуе]|кой)|иуде(?:й|я|ю|ем|и|ев|ям|ями|ях|йк[аиуе]|йкой)|ислам[\p{L}]{0,5}|эпилепси[\p{L}]{1,5}|астм[\p{L}]{1,5}|онкологи[\p{L}]{1,5}|туберкул[её]з[\p{L}]{0,5}|психиатр[\p{L}]{0,6}|депресси[\p{L}]{1,5}|гепатит[\p{L}]{0,5}|гомосексуал[\p{L}]{0,7}|лесбиян[\p{L}]{1,5}|ге(?:й|я|ю|ем|и|ев|ям|ями|ях)|нетрадиционн[\p{L}]{2,3}[ \t]+ориентаци[\p{L}]{1,3}|отбыва(?:л[аи]?|ет|ют|вш[\p{L}]{2,3})[ \t]+наказани[\p{L}]{1,3}|ВИЧ-инфицирован[\p{L}]{0,4}|epilepsy|asthma|tuberculosis|hepatitis|depression|gay|lesbian|homosexual|bisexual|armenian)(?![\p{L}\p{N}_])/giu;
13
+ const categoryCue =
14
+ /(?<![\p{L}\p{N}_])(?:по[ \t]+национальности|национальность|этничность|вероисповедание|религия|диагноз|исповеду(?:ет|ют|ю|ешь)|боле(?:ет|ют|ю|ешь)|страда(?:ет|ют|ю|ешь)[ \t]+от|на[ \t]+уч[её]те[ \t]+у|diagnosed[ \t]+with|suffers[ \t]+from|nationality|ethnicity|religion|diagnosis)(?:["']?[ \t]*[:=][ \t]*|[ \t]+)["']?([\p{L}][\p{L}\p{M}-]{0,63})(?![\p{L}\p{M}\p{N}_])/giu;
15
+ const ambiguous =
16
+ /(?<![\p{L}\p{N}_])(?:disabled|single|man|men|russian|asian)(?![\p{L}\p{N}_])/giu;
17
+
18
+ function personalContext(text, match) {
19
+ const prefix = text.slice(Math.max(0, match.index - 128), match.index);
20
+ const suffix = text.slice(
21
+ match.index + match[0].length,
22
+ match.index + match[0].length + 64
23
+ );
24
+ // These continuations remain technical even after a pronoun ("it is a
25
+ // single-flight lock"). No document-wide cue can authorize another clause.
26
+ if (
27
+ /^(?:-flight\b|[ \t]+(?:page|pages|run|worker|thread|lock|region|endpoint|locale|module|feature|flag)\b)/iu.test(
28
+ suffix
29
+ )
30
+ ) {
31
+ return false;
32
+ }
33
+ return (
34
+ /(?<![\p{L}\p{N}_])(?:i|he|she|we|they|candidate|applicant|patient|person|employee)(?:[ \t]+(?:am|is|are|was|were|identifies[ \t]+as))?(?:[ \t]+(?:a|an))?[ \t]+$/iu.test(
35
+ prefix
36
+ ) ||
37
+ /(?<![\p{L}\p{N}_])(?:gender|sex|nationality|ethnicity|marital[ \t_-]*status|disability)(?:["'][ \t]*)?[ \t]*[:=][ \t]*["']?$/iu.test(
38
+ prefix
39
+ ) ||
40
+ /^[ \t]+(?:candidate|applicant|patient|person|people|employee)(?![\p{L}\p{N}_])/iu.test(
41
+ suffix
42
+ )
43
+ );
44
+ }
45
+
46
+ export function detectStrictAttributes(text, emit, options) {
47
+ let detected = 0,
48
+ suppressed = 0;
49
+ const details = {
50
+ type: 'PERSONAL_ATTRIBUTE',
51
+ rule: 'strict-attribute',
52
+ category: 'personal',
53
+ confidence: 0.9,
54
+ };
55
+ const report = (finding) => {
56
+ detected++;
57
+ emit(finding);
58
+ };
59
+ for (const pattern of [attributes, additionalAttributes, categoryForms]) {
60
+ collectMatches(text, pattern, report, details);
61
+ }
62
+ collectMatches(text, categoryCue, report, details, 1);
63
+ collectMatches(text, ambiguous, report, details, 0, (_value, match) => {
64
+ const accepted = personalContext(text, match);
65
+ if (!accepted) {
66
+ suppressed++;
67
+ }
68
+ return accepted;
69
+ });
70
+ options.debug?.({ event: 'strict-attributes', detected, suppressed });
71
+ }
package/src/strict.js CHANGED
@@ -3,12 +3,9 @@ import { months } from './personal-context.js';
3
3
  import { isDocumentTechnology } from './document-experience.js';
4
4
  import { placeholderValues } from './placeholders.js';
5
5
  import { generatedValues } from './fake.js';
6
+ import { detectStrictAttributes } from './strict-attributes.js';
6
7
 
7
8
  const boundary = '[\\p{L}\\p{N}_]';
8
- const attributes =
9
- /(?<![\p{L}\p{N}_])(?:женщин[аыуеой]{1,2}|мужчин[аыуеой]{1,2}|замужем|женат(?:а|ы)?|(?:один|одна|двое|трое|четверо|пятеро|\d{1,2})[ \t]+(?:детей|реб[её]нок|реб[её]нка)|русск(?:ая|ий|ого|ой|ие)|татар(?:ин|ка|ы)|православн(?:ый|ая|ого|ой)|мусульман(?:ин|ка)|инвалидност[ьи](?:[ \t]+\d[ \t]+группы)?|беременн(?:а|ая|ость)|судимость(?:[ \t]+погашена)?|член[ \t]+партии|woman|women|man|men|male|female|married|divorced|pregnant|pregnancy|disabled|disability|christian|muslim|jewish|orthodox|russian|hispanic|asian|criminal[ \t]+record|party[ \t]+member|(?:one|two|three|\d{1,2})[ \t]+children)(?![\p{L}\p{N}_])/giu;
10
- const additionalAttributes =
11
- /(?<![\p{L}\p{N}_])(?:диабет(?:ом|а|у|е)?|ВИЧ(?:-положительн(?:ый|ая|ого|ой))?|болен|больна|больны|католик(?:ом|а|и)?|католичк(?:а|ой|и)|буддист(?:ка|кой|ом|а|ы)?|атеист(?:ка|кой|ом|а|ы)?|украин(?:ец|ка|цы|цем|кой)|белорус(?:ка|кой|ом|а|ы)?|несудим(?:а|ы)?|судим(?:а|ы)?|член[ \t]+политической[ \t]+партии|холост(?:а|ы)?|развед[её]н(?:а|ы)?|в[ \t]+разводе|diabetes|diabetic|HIV(?:[ \t-]+positive)?|cancer|buddhist|catholic|atheist|ukrainian|belarusian|previous[ \t]+conviction|member[ \t]+of[ \t]+(?:a[ \t]+)?(?:political[ \t]+)?party|single|unmarried|widowed)(?![\p{L}\p{N}_])/giu;
12
9
 
13
10
  function isDuration(text, match, replacements) {
14
11
  // Keep cues on the same clause; an age on the next line is independent.
@@ -57,14 +54,7 @@ export function detectStrict(text, emit, options) {
57
54
  if (!options.strict) {
58
55
  return;
59
56
  }
60
- for (const pattern of [attributes, additionalAttributes]) {
61
- collectMatches(text, pattern, emit, {
62
- type: 'PERSONAL_ATTRIBUTE',
63
- rule: 'strict-attribute',
64
- category: 'personal',
65
- confidence: 0.9,
66
- });
67
- }
57
+ detectStrictAttributes(text, emit, options);
68
58
  const date = new RegExp(
69
59
  `(?<!${boundary})(?:\\d{4}-\\d{2}-\\d{2}|\\d{1,2}[./]\\d{1,2}[./]\\d{4}|\\d{1,2}[ \\t]+(?:${months})[ \\t]+\\d{4})(?!${boundary}|[Tt]\\d)`,
70
60
  'giu'