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

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.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 4afc88d: Read `Mr`, `Ms` and `Dr` as person cues only when capitalized, so lowercase `ms` (milliseconds) and `dr` before a capitalized log word stay. Treat compound JSON keys ending in a personal field, such as `recruiterDisplayName`, `authorFullName` and `resumePdfOriginalFileName`, as person fields.
8
+
3
9
  ## 0.19.1
4
10
 
5
11
  ### Patch Changes
package/docs/COVERAGE.md CHANGED
@@ -370,6 +370,11 @@ this explicit vocabulary: `firstName`, `lastName`, `middleName`, `patronymic`,
370
370
  sensitive in full, including ISO birth timestamps. Thus `first_name` and
371
371
  `firstName`, or `birth_date` and `birthDate`, follow the same policy. These are
372
372
  schema assumptions: `contact` or `company` can hold operational data too.
373
+ Person fields also match as a word-boundary suffix of a compound key, so
374
+ `recruiterDisplayName`, `author_full_name` and `resumePdfOriginalFileName` are
375
+ person fields. The same suffix rule applies to `vocabulary.personalFields`.
376
+ Other types match only the whole key (`maxAge` and `fileName` stay), and a
377
+ bare `name` is not in the list.
373
378
  Existing native labels still apply. Unlisted URL-named fields have ordinary
374
379
  content detection; arbitrary names do not imply sensitivity. Declare them with
375
380
  [`sensitiveFields`](API.md#caller-declared-sensitive-fields) to replace complete
@@ -542,6 +547,9 @@ corpus, unbounded input, parallel builds or external analysis service.
542
547
 
543
548
  Document rules recognize a capitalized Latin name after `candidate`, `recruiter`,
544
549
  `applicant`, `Dear`, `Hi`, `Mr`/`Mr.`, `Ms`/`Ms.`, `Dr`/`Dr.`, or `message from`.
550
+ Honorifics count only when capitalized: lowercase `ms` (milliseconds), `mr` and
551
+ `dr` are not cues, and `Ms` right after a number is read as a unit, so
552
+ `took 30 ms Retry scheduled` and `see dr Drive mount` stay unchanged.
545
553
  Only the name is replaced; punctuation and the cue remain. These rules work
546
554
  without enabling Latin script matching. Finite generic heading nouns such as
547
555
  `Stage` and `Status` are preserved: `Candidate Stage changed` and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-foundation/sensitive-data-sanitizer",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
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",
@@ -25,7 +25,24 @@ const types = new Map(
25
25
  )
26
26
  );
27
27
  export function documentFieldType(field) {
28
- return types.get(normalizeFieldName(field));
28
+ const type = types.get(normalizeFieldName(field));
29
+ if (type) {
30
+ return type;
31
+ }
32
+ // Compound person keys (`recruiterDisplayName`, `author_full_name`) end in
33
+ // a listed person field. Only PERSON entries extend this way, so generic
34
+ // suffixes such as `maxAge` or `homeSchool` keep their exact-match meaning,
35
+ // and a bare `name` is never listed.
36
+ return fieldSuffixes(field).some((suffix) => types.get(suffix) === 'PERSON')
37
+ ? 'PERSON'
38
+ : undefined;
39
+ }
40
+ // Word-boundary suffixes of a key, normalized; the whole key comes first.
41
+ export function fieldSuffixes(field) {
42
+ const words = field
43
+ .split(/[\s_.-]+|(?<=[\p{Ll}\p{N}])(?=\p{Lu})|(?<=\p{Lu})(?=\p{Lu}\p{Ll})/u)
44
+ .filter(Boolean);
45
+ return words.map((_, i) => words.slice(i).join('').toLowerCase());
29
46
  }
30
47
  export const normalizeFieldName = (field) =>
31
48
  field.replace(/[_-]/g, '').toLowerCase();
@@ -4,8 +4,11 @@ import { collectMatches } from './detection.js';
4
4
  // capitalized-word matching for the rest of an English log or document.
5
5
  const boundary = '[\\p{L}\\p{M}\\p{N}_]';
6
6
  const name = "[A-Z][a-z]{1,47}(?:['’-][A-Z]?[a-z]{1,47})?";
7
+ // Honorifics count only when capitalized: lowercase `ms` is the millisecond
8
+ // unit in logs and lowercase `dr` is usually a word or path fragment. A
9
+ // capitalized `Ms` right after a number is still a unit (`took 30 Ms Retry`).
7
10
  const cue =
8
- '(?:[Cc]andidate|[Rr]ecruiter|[Aa]pplicant|[Dd]ear|[Hh]i|[Mm]r\\.?|[Mm]s\\.?|[Dd]r\\.?|[Mm]essage[ \\t]+from)';
11
+ '(?:[Cc]andidate|[Rr]ecruiter|[Aa]pplicant|[Dd]ear|[Hh]i|Mr\\.?|(?<!\\d[ \\t]*)Ms\\.?|Dr\\.?|[Mm]essage[ \\t]+from)';
9
12
  const pattern = new RegExp(
10
13
  `(?<!${boundary})${cue}[ \\t]+(${name})(?!${boundary})`,
11
14
  'gu'
package/src/vocabulary.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { failure, collectMatches } from './detection.js';
2
2
  import { wordPattern } from './personal-variants.js';
3
- import { normalizeFieldName } from './document-fields.js';
3
+ import { fieldSuffixes, normalizeFieldName } from './document-fields.js';
4
4
 
5
5
  export const defaultVocabulary = Object.freeze(
6
6
  Object.fromEntries(
@@ -170,7 +170,8 @@ export function vocabularyField(field, options) {
170
170
  if (!vocabulary) {
171
171
  return undefined;
172
172
  }
173
- return vocabulary.fields.has(normalizeFieldName(field))
173
+ return vocabulary.fields.has(normalizeFieldName(field)) ||
174
+ fieldSuffixes(field).some((suffix) => vocabulary.fields.has(suffix))
174
175
  ? true
175
176
  : options.vocabulary.personalFields?.replace
176
177
  ? false