@link-foundation/sensitive-data-sanitizer 0.21.5 → 0.21.6
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 +6 -0
- package/package.json +1 -1
- package/src/context.js +96 -7
- package/src/credentials.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cbd6b3b: Preserve complete credential placeholders and public configuration values before Markdown backticks, including the exact `HIVE_MIND_CLAUDE_AUTH_AUTHORITY=conventional` opt-out. Keep masking actual HTML/script and keyword-prefixed credentials, with exact known secrets taking priority.
|
|
8
|
+
|
|
3
9
|
## 0.21.5
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-foundation/sensitive-data-sanitizer",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.6",
|
|
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/context.js
CHANGED
|
@@ -143,6 +143,10 @@ export function configurationSetting(keyName, value) {
|
|
|
143
143
|
function credentialMetadata(keyName, value) {
|
|
144
144
|
return (
|
|
145
145
|
configurationSetting(keyName, value) ||
|
|
146
|
+
// This documented opt-out enum is public; other authority values and
|
|
147
|
+
// names still carry credential evidence.
|
|
148
|
+
(keyName === 'HIVE_MIND_CLAUDE_AUTH_AUTHORITY' &&
|
|
149
|
+
value === 'conventional') ||
|
|
146
150
|
(/(?:^|[_.-])auth[_.-](?:mode|method|type)$/i.test(keyName) &&
|
|
147
151
|
/^(?:chatgpt|oauth2?|apikey|api_key|bearer|basic|none)$/i.test(value)) ||
|
|
148
152
|
(keyName === 'id-token' && /^(?:write|read|none)$/.test(value)) ||
|
|
@@ -160,11 +164,13 @@ function credentialMetadata(keyName, value) {
|
|
|
160
164
|
// Only a quote at the same escape depth closes it; embedded quotes belong to
|
|
161
165
|
// the value. Offsets cover escape bytes as well as the decoded characters.
|
|
162
166
|
export function valueRange(text, start, mode = 'token', maxLength = Infinity) {
|
|
163
|
-
const reference = referencePrefix(
|
|
167
|
+
const reference = referencePrefix(
|
|
168
|
+
text.slice(start, markdownEnd(text, start, 512))
|
|
169
|
+
);
|
|
164
170
|
if (reference) {
|
|
165
171
|
return { start, end: start + reference[0].length };
|
|
166
172
|
}
|
|
167
|
-
const opening = text.slice(start).match(/^(\\*)(["'])/);
|
|
173
|
+
const opening = text.slice(start).match(/^(\\*)(["']|`+)/);
|
|
168
174
|
if (opening) {
|
|
169
175
|
const range = quotedRange(text, start, opening, maxLength);
|
|
170
176
|
const outer = outerQuoteEnd(text, range.start, range.end);
|
|
@@ -173,7 +179,8 @@ export function valueRange(text, start, mode = 'token', maxLength = Infinity) {
|
|
|
173
179
|
if (/^[{[]/.test(text.slice(start, start + 1))) {
|
|
174
180
|
return undefined;
|
|
175
181
|
}
|
|
176
|
-
const tail = text.slice(start, start
|
|
182
|
+
const tail = text.slice(start, markdownEnd(text, start, maxLength));
|
|
183
|
+
// Only a matching surrounding Markdown delimiter ends an unquoted value.
|
|
177
184
|
const terminator =
|
|
178
185
|
mode === 'query'
|
|
179
186
|
? /[\s&#"'<>]/
|
|
@@ -321,6 +328,9 @@ function quotedRange(text, start, opening, maxLength) {
|
|
|
321
328
|
const depth = opening[1].length,
|
|
322
329
|
quote = opening[2];
|
|
323
330
|
const valueStart = start + opening[0].length;
|
|
331
|
+
if (quote[0] === '`') {
|
|
332
|
+
return markdownRange(text, valueStart, quote.length, maxLength);
|
|
333
|
+
}
|
|
324
334
|
for (
|
|
325
335
|
let i = valueStart;
|
|
326
336
|
i < Math.min(text.length, valueStart + maxLength);
|
|
@@ -353,6 +363,64 @@ function quotedRange(text, start, opening, maxLength) {
|
|
|
353
363
|
};
|
|
354
364
|
}
|
|
355
365
|
|
|
366
|
+
// Markdown closes only at a run of the same length. Scan each run once so
|
|
367
|
+
// long unmatched backtick runs cannot cause repeated prefix comparisons.
|
|
368
|
+
function markdownRange(text, start, length, maxLength) {
|
|
369
|
+
const limit = Math.min(text.length, start + maxLength);
|
|
370
|
+
for (let i = start; i < limit; i++) {
|
|
371
|
+
if (text[i] !== '`') {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
let end = i + 1;
|
|
375
|
+
while (end < limit && text[end] === '`') {
|
|
376
|
+
end++;
|
|
377
|
+
}
|
|
378
|
+
if (end - i === length && text[end] !== '`') {
|
|
379
|
+
return { start, end: i };
|
|
380
|
+
}
|
|
381
|
+
i = end - 1;
|
|
382
|
+
}
|
|
383
|
+
return { start, end: limit };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Recognize surrounding inline Markdown within a bounded logical line. When
|
|
387
|
+
// its opening or closing delimiter cannot be established, keep scanning the
|
|
388
|
+
// full credential, including any literal material following a placeholder.
|
|
389
|
+
function markdownEnd(text, start, maxLength) {
|
|
390
|
+
const end = Math.min(text.length, start + maxLength);
|
|
391
|
+
const from = Math.max(0, start - 4096);
|
|
392
|
+
const prefix = text.slice(from, start);
|
|
393
|
+
const lineStart =
|
|
394
|
+
Math.max(prefix.lastIndexOf('\n'), prefix.lastIndexOf('\r')) + 1;
|
|
395
|
+
if (from && !lineStart) {
|
|
396
|
+
return end;
|
|
397
|
+
}
|
|
398
|
+
let length = 0;
|
|
399
|
+
for (const run of prefix.slice(lineStart).matchAll(/`+/g)) {
|
|
400
|
+
if (!length) {
|
|
401
|
+
let slashes = 0;
|
|
402
|
+
for (
|
|
403
|
+
let i = lineStart + run.index - 1;
|
|
404
|
+
i >= 0 && prefix[i] === '\\';
|
|
405
|
+
i--
|
|
406
|
+
) {
|
|
407
|
+
slashes++;
|
|
408
|
+
}
|
|
409
|
+
if (slashes % 2 === 0) {
|
|
410
|
+
length = run[0].length;
|
|
411
|
+
}
|
|
412
|
+
} else if (length === run[0].length) {
|
|
413
|
+
length = 0;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!length) {
|
|
417
|
+
return end;
|
|
418
|
+
}
|
|
419
|
+
const limit = Math.min(end, start + 4096);
|
|
420
|
+
const close = markdownRange(text, start, length, limit - start).end;
|
|
421
|
+
return close < limit ? close : end;
|
|
422
|
+
}
|
|
423
|
+
|
|
356
424
|
function plausibleLabel(value, type) {
|
|
357
425
|
if (operationalValue(value, type)) {
|
|
358
426
|
return false;
|
|
@@ -534,8 +602,11 @@ const angleEnd = {
|
|
|
534
602
|
// detection view instead.
|
|
535
603
|
const formattingTag = new RegExp(`^${htmlTag}`, 'i');
|
|
536
604
|
function angleValue(text, start, mode) {
|
|
537
|
-
|
|
538
|
-
|
|
605
|
+
if (text[start] !== '<' || !angleEnd[mode]) {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
const tail = text.slice(start, markdownEnd(text, start, 4096));
|
|
609
|
+
if (formattingTag.test(tail)) {
|
|
539
610
|
return undefined;
|
|
540
611
|
}
|
|
541
612
|
const placeholder = /^<[^<>\r\n]{1,64}>/.exec(tail)?.[0];
|
|
@@ -550,6 +621,23 @@ function angleValue(text, start, mode) {
|
|
|
550
621
|
return { start, end: start + (stop < 0 ? tail.length : stop) };
|
|
551
622
|
}
|
|
552
623
|
|
|
624
|
+
// Scheme-prefixed values carry the credential after the keyword, just as
|
|
625
|
+
// Authorization headers do. Never mask only `token` and leave its value.
|
|
626
|
+
function credentialValueRange(text, start, mode) {
|
|
627
|
+
const valueStart = start;
|
|
628
|
+
if (mode === 'token') {
|
|
629
|
+
start +=
|
|
630
|
+
/^(?:token|Bearer|Basic)[ \t]+(?=\S)/i.exec(text.slice(start))?.[0]
|
|
631
|
+
.length ?? 0;
|
|
632
|
+
}
|
|
633
|
+
const range = angleValue(text, start, mode) ?? valueRange(text, start, mode);
|
|
634
|
+
// Keep the prefix in the value used for metadata checks: `token 0` is not
|
|
635
|
+
// the exact public setting `0`, even under a configuration name.
|
|
636
|
+
return range && start !== valueStart
|
|
637
|
+
? { ...range, start: valueStart }
|
|
638
|
+
: range;
|
|
639
|
+
}
|
|
640
|
+
|
|
553
641
|
function credentialRange(text, match, camel) {
|
|
554
642
|
const start = match.index + match[0].length;
|
|
555
643
|
if (
|
|
@@ -562,7 +650,7 @@ function credentialRange(text, match, camel) {
|
|
|
562
650
|
return authorizationRanges(text, start);
|
|
563
651
|
}
|
|
564
652
|
const mode = credentialMode(text, match);
|
|
565
|
-
const range =
|
|
653
|
+
const range = credentialValueRange(text, start, mode);
|
|
566
654
|
const value = range && mode === 'prose' ? generatedWord(text, range) : range;
|
|
567
655
|
if (value && /^(?:set-)?cookie$/i.test(match[1])) {
|
|
568
656
|
return cookieValues(text, value.start, value.end);
|
|
@@ -601,7 +689,8 @@ function detectCredentials(text, emit) {
|
|
|
601
689
|
}
|
|
602
690
|
const flags = new RegExp(`--(${key})(?:[ \\t]+|=)`, 'giu');
|
|
603
691
|
for (const match of text.matchAll(flags)) {
|
|
604
|
-
const
|
|
692
|
+
const start = match.index + match[0].length;
|
|
693
|
+
const range = credentialValueRange(text, start, 'token');
|
|
605
694
|
if (range) {
|
|
606
695
|
emitValue(text, range.start, range.end, emit, details, match[1]);
|
|
607
696
|
}
|
package/src/credentials.js
CHANGED
|
@@ -89,7 +89,7 @@ export function templatedCredential(text, finding) {
|
|
|
89
89
|
// Only variable references and the documented gh lookup are exempt. Arbitrary
|
|
90
90
|
// shell commands and Actions expressions can contain literal credentials.
|
|
91
91
|
export function referencePrefix(value) {
|
|
92
|
-
return /^(?:(?:token|Bearer|Basic)\s+)?(?:\$\{\{\s*(?:secrets|env|vars|github|inputs)\.[A-Za-z_][A-Za-z0-9_.]*\s*\}\}|\$\(gh\s+auth\s+token(?:\s+--hostname\s+[a-z0-9.-]+)?\))(?=$|[\s"',;\\])/i.exec(
|
|
92
|
+
return /^(?:(?:token|Bearer|Basic)\s+)?(?:\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*|\$\{\{\s*(?:secrets|env|vars|github|inputs)\.[A-Za-z_][A-Za-z0-9_.]*\s*\}\}|\$\(gh\s+auth\s+token(?:\s+--hostname\s+[a-z0-9.-]+)?\))(?=$|[\s"',;\\])/i.exec(
|
|
93
93
|
value
|
|
94
94
|
);
|
|
95
95
|
}
|