@link-foundation/sensitive-data-sanitizer 0.21.2 → 0.21.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 +6 -0
- package/docs/API.md +6 -1
- package/package.json +1 -1
- package/src/encoded.js +57 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ee1ec36: Preserve harmless wrapped base64 binary within a bounded complete inspection, while retaining embedded credential detection and fail-closed handling of uninspected oversized payloads. Add regression coverage for complete JSON re-encoding after surrounding prose through sync, async and streaming sanitization.
|
|
8
|
+
|
|
3
9
|
## 0.21.2
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/docs/API.md
CHANGED
|
@@ -365,7 +365,12 @@ status identifiers. Explicit `knownPersonal` values override these heuristics.
|
|
|
365
365
|
|
|
366
366
|
Oversized encoded runs receive a conservative complete-run `ENCODED_LIMIT`
|
|
367
367
|
redaction rather than aborting the record. Decoding probes are capped at 4,096
|
|
368
|
-
characters;
|
|
368
|
+
characters; ordinary text decoding stays capped at 8,192 characters. Uniform
|
|
369
|
+
wrapped binary base64 up to 256 KiB of encoded input is decoded completely
|
|
370
|
+
and its embedded text is inspected. Harmless binary bytes are preserved; a
|
|
371
|
+
sensitive binary payload is masked as a whole because a text re-encoding
|
|
372
|
+
cannot preserve its bytes. Larger uninspected wrapped binary runs still
|
|
373
|
+
receive `ENCODED_LIMIT`. A multi-line run is
|
|
369
374
|
treated as one wrapped payload only when its probe decodes as text or its lines
|
|
370
375
|
share one base64 width (a multiple of four, standard alphabet, not plain hex);
|
|
371
376
|
lists of identifiers or commit hashes are scanned line by line. Up to 4,096
|
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.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/encoded.js
CHANGED
|
@@ -7,6 +7,9 @@ import { jsonStringRuns } from './json-string-runs.js';
|
|
|
7
7
|
// remaining candidates as `encoded-limit` runs, without blocking the text.
|
|
8
8
|
const WINDOW = 65536;
|
|
9
9
|
const WINDOW_CANDIDATES = 4096;
|
|
10
|
+
// Inspect a complete bounded binary payload, including any embedded text.
|
|
11
|
+
// Ordinary text detection retains its 8 KiB cap.
|
|
12
|
+
const WRAPPED_BINARY_LIMIT = 256 * 1024;
|
|
10
13
|
|
|
11
14
|
const WRAPPED =
|
|
12
15
|
/(?<![A-Za-z0-9_+/-])[A-Za-z0-9_+/-]{16,128}(?:\r?\n[A-Za-z0-9_+/-]{1,128})+={0,2}(?![A-Za-z0-9_+/-])/g;
|
|
@@ -56,7 +59,8 @@ function* matchRuns(match, wrapped, scan) {
|
|
|
56
59
|
const start = match.index;
|
|
57
60
|
const end = start + raw.length;
|
|
58
61
|
const limited = { start, end, limited: true, depth: scan.depth };
|
|
59
|
-
|
|
62
|
+
const candidates = runCandidates(raw, wrapped);
|
|
63
|
+
if (!candidates) {
|
|
60
64
|
if (isLimitedTextRun(raw, wrapped)) {
|
|
61
65
|
scan.covered.push(limited);
|
|
62
66
|
yield limited;
|
|
@@ -64,7 +68,7 @@ function* matchRuns(match, wrapped, scan) {
|
|
|
64
68
|
return;
|
|
65
69
|
}
|
|
66
70
|
const window = Math.floor(start / WINDOW);
|
|
67
|
-
for (const candidate of
|
|
71
|
+
for (const candidate of candidates) {
|
|
68
72
|
const { text: decoded, encoding } = candidate;
|
|
69
73
|
const key = `${start}:${raw.length}:${decoded}`;
|
|
70
74
|
if (scan.seen.has(key) || !decodedText(raw, decoded)) {
|
|
@@ -91,11 +95,48 @@ function* matchRuns(match, wrapped, scan) {
|
|
|
91
95
|
};
|
|
92
96
|
yield run;
|
|
93
97
|
for (const inner of decodedRuns(decoded, scan.depth + 1)) {
|
|
94
|
-
yield
|
|
98
|
+
yield inner.limited
|
|
99
|
+
? { ...limited, depth: inner.depth }
|
|
100
|
+
: { ...run, text: inner.text, depth: inner.depth };
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
function runCandidates(raw, wrapped) {
|
|
106
|
+
if (raw.length <= 8192) {
|
|
107
|
+
return decodeCandidates(raw);
|
|
108
|
+
}
|
|
109
|
+
const binary = wrapped && boundedWrappedBinary(raw);
|
|
110
|
+
return binary ? [binary] : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function boundedWrappedBinary(raw) {
|
|
114
|
+
if (raw.length > WRAPPED_BINARY_LIMIT || !isWrappedBinary(raw)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const bytes = base64Bytes(raw.replace(/\s/g, ''));
|
|
118
|
+
if (!bytes) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
123
|
+
if (decodedText(raw, decoded)) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
/* Binary content still needs an embedded-text detection view. */
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
encoding: 'binary',
|
|
131
|
+
// Preserve text offsets within the view while separating binary controls
|
|
132
|
+
// and invalid UTF-8. This view is never used to reconstruct binary bytes.
|
|
133
|
+
text: bytes
|
|
134
|
+
.toString('utf8')
|
|
135
|
+
// eslint-disable-next-line no-control-regex -- Replace binary controls in the text detection view.
|
|
136
|
+
.replace(/[\x00-\x08\x0e-\x1f\x7f\ufffd]/g, ' '),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
99
140
|
function candidateMatches(text, pattern) {
|
|
100
141
|
return pattern ? text.matchAll(pattern) : jsonStringRuns(text);
|
|
101
142
|
}
|
|
@@ -211,11 +252,8 @@ function decodeCandidates(raw, partial) {
|
|
|
211
252
|
} else {
|
|
212
253
|
const compact = raw.replace(/\s/g, '');
|
|
213
254
|
// Decode canonical base64 text only.
|
|
214
|
-
const bytes =
|
|
215
|
-
if (
|
|
216
|
-
bytes.toString('base64url') ===
|
|
217
|
-
compact.replace(/=+$/, '').replaceAll('+', '-').replaceAll('/', '_')
|
|
218
|
-
) {
|
|
255
|
+
const bytes = base64Bytes(compact);
|
|
256
|
+
if (bytes) {
|
|
219
257
|
try {
|
|
220
258
|
candidates.push({
|
|
221
259
|
encoding:
|
|
@@ -248,7 +286,18 @@ function decodeCandidates(raw, partial) {
|
|
|
248
286
|
return candidates;
|
|
249
287
|
}
|
|
250
288
|
|
|
289
|
+
function base64Bytes(compact) {
|
|
290
|
+
const bytes = Buffer.from(compact, 'base64url');
|
|
291
|
+
return bytes.toString('base64url') ===
|
|
292
|
+
compact.replace(/=+$/, '').replaceAll('+', '-').replaceAll('/', '_')
|
|
293
|
+
? bytes
|
|
294
|
+
: undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
251
297
|
export function encodeRun(run, text) {
|
|
298
|
+
if (run.encoding === 'binary') {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
252
301
|
const bytes = Buffer.from(text, 'utf8');
|
|
253
302
|
let encoded;
|
|
254
303
|
if (run.encoding === 'json-content') {
|