@link-foundation/sensitive-data-sanitizer 0.21.2 → 0.21.4
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/docs/API.md +14 -2
- package/package.json +1 -1
- package/src/credentials.js +26 -1
- package/src/encoded.js +57 -8
- package/src/engines.js +55 -16
- package/src/exempt.js +24 -0
- package/src/index.d.ts +3 -0
- package/src/sanitizer.js +32 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7454024: Preserve canonical short base64/base64url Hive masks and escaped JSON log structure through verified sanitization. Reinspect bounded escaped JSON views with the configured exemption policy and detectors, while retaining exact known-secret detection and fail-closed handling at inspection limits.
|
|
8
|
+
|
|
9
|
+
## 0.21.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
3
15
|
## 0.21.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/docs/API.md
CHANGED
|
@@ -317,7 +317,7 @@ Generated replacements carry `faked: true`. A private bounded registry recognize
|
|
|
317
317
|
|
|
318
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).
|
|
319
319
|
|
|
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.
|
|
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. Canonical base64/base64url encodings of complete Hive masks (including short forms such as `Z2hv4oCmOTk5`, encoding `gho…999`) remain intact on repeated passes. Arbitrary decoded text, malformed encodings and literal text appended to a mask receive no mask exemption; exact known secrets still take priority.
|
|
321
321
|
|
|
322
322
|
## Identity policy and structured sessions
|
|
323
323
|
|
|
@@ -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
|
|
@@ -946,6 +951,13 @@ credential findings without `allowCredentialDisable`. Declared
|
|
|
946
951
|
`knownSecrets`/`knownPersonal` values, confirmed personal values and structured
|
|
947
952
|
`JSON_VALUE` denials are never offered to it.
|
|
948
953
|
|
|
954
|
+
For escaped JSON findings up to 8192 characters, the engine independently
|
|
955
|
+
inspects the decoded view with the same configured policy and detectors before
|
|
956
|
+
keeping a clean view. Hooks in that inspection receive decoded text and its
|
|
957
|
+
offsets. Additional inspection stops after three levels; malformed, oversized,
|
|
958
|
+
and deeper views retain normal fail-closed handling. The optional value-free
|
|
959
|
+
`exempt-decoded-view` debug event reports a clean view.
|
|
960
|
+
|
|
949
961
|
`createSanitizer` awaits a hook that returns a `Promise`.
|
|
950
962
|
`createSyncSanitizer` and `sanitize()` need a synchronous hook. A `Promise`
|
|
951
963
|
there is `ERR_CONFIG`.
|
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.4",
|
|
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/credentials.js
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { collectMatches } from './detection.js';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
|
|
4
|
+
// Re-encoded Hive output can be shorter than the general decoder threshold.
|
|
5
|
+
// Recognize only a whole canonical base64/base64url spelling of a complete
|
|
6
|
+
// mask; arbitrary decodable text and masks glued to literal text stay sensitive.
|
|
7
|
+
function encodedMask(value) {
|
|
8
|
+
if (
|
|
9
|
+
value.length > 64 ||
|
|
10
|
+
!/^(?:[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}$/.test(value)
|
|
11
|
+
) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const bytes = Buffer.from(value, 'base64url');
|
|
15
|
+
const canonical = bytes.toString('base64');
|
|
16
|
+
const standard = value.replaceAll('-', '+').replaceAll('_', '/');
|
|
17
|
+
const decoded = bytes.toString('utf8');
|
|
18
|
+
return (
|
|
19
|
+
(standard === canonical || standard === canonical.replace(/=+$/, '')) &&
|
|
20
|
+
Buffer.from(decoded).equals(bytes) &&
|
|
21
|
+
/^(?:\[REDACTED\]|[^\s…]{0,3}…[^\s…]{0,3})$/u.test(decoded)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
2
24
|
|
|
3
25
|
export function credentialPlaceholder(value) {
|
|
4
26
|
return (
|
|
@@ -59,7 +81,10 @@ export function templatedCredential(text, finding) {
|
|
|
59
81
|
const sentenceEnd = /^[.,;:!?]+(?:\s|$)/.test(
|
|
60
82
|
text.slice(finding.start + value.length)
|
|
61
83
|
);
|
|
62
|
-
return (
|
|
84
|
+
return (
|
|
85
|
+
(!/[\w$%+/=.-]/.test(after) || sentenceEnd) &&
|
|
86
|
+
(templatedValue(value) || encodedMask(value))
|
|
87
|
+
);
|
|
63
88
|
}
|
|
64
89
|
// Only variable references and the documented gh lookup are exempt. Arbitrary
|
|
65
90
|
// shell commands and Actions expressions can contain literal credentials.
|
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') {
|
package/src/engines.js
CHANGED
|
@@ -18,7 +18,7 @@ import { projectText } from './projection.js';
|
|
|
18
18
|
import { decodedRuns } from './encoded.js';
|
|
19
19
|
import { hasCustomTransforms } from './transforms.js';
|
|
20
20
|
import { builtInEnabled } from './policy.js';
|
|
21
|
-
import { exemptFindingsAsync } from './exempt.js';
|
|
21
|
+
import { exemptFindingsAsync, escapedExemptionView } from './exempt.js';
|
|
22
22
|
import {
|
|
23
23
|
initializePlaceholders,
|
|
24
24
|
placeholderKnown,
|
|
@@ -235,6 +235,59 @@ function configuredEngines(options) {
|
|
|
235
235
|
return engines;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
async function screenDecodedFindings(text, findings, options, depth, screened) {
|
|
239
|
+
const retained = [];
|
|
240
|
+
for (const finding of findings) {
|
|
241
|
+
const view = escapedExemptionView(text, finding, depth);
|
|
242
|
+
const clean =
|
|
243
|
+
view !== undefined &&
|
|
244
|
+
!(
|
|
245
|
+
await screened(
|
|
246
|
+
view,
|
|
247
|
+
[],
|
|
248
|
+
true,
|
|
249
|
+
{ ...options, findings: [], structured: undefined },
|
|
250
|
+
undefined,
|
|
251
|
+
depth + 1
|
|
252
|
+
)
|
|
253
|
+
).length;
|
|
254
|
+
if (clean) {
|
|
255
|
+
options.debug?.({ event: 'exempt-decoded-view', rule: finding.rule });
|
|
256
|
+
} else {
|
|
257
|
+
retained.push(finding);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return retained;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function createScreened(analyze, options) {
|
|
264
|
+
// The caller's `exempt` hook screens the findings of the text it sees,
|
|
265
|
+
// including the verification passes over the published output.
|
|
266
|
+
return async function screened(
|
|
267
|
+
text,
|
|
268
|
+
initial,
|
|
269
|
+
plain,
|
|
270
|
+
scopedOptions = options,
|
|
271
|
+
nativeFindings,
|
|
272
|
+
exemptionDepth = 0
|
|
273
|
+
) {
|
|
274
|
+
const findings = await exemptFindingsAsync(
|
|
275
|
+
text,
|
|
276
|
+
await analyze(text, initial, plain, scopedOptions, nativeFindings),
|
|
277
|
+
scopedOptions.exempt
|
|
278
|
+
);
|
|
279
|
+
return scopedOptions.exempt
|
|
280
|
+
? screenDecodedFindings(
|
|
281
|
+
text,
|
|
282
|
+
findings,
|
|
283
|
+
scopedOptions,
|
|
284
|
+
exemptionDepth,
|
|
285
|
+
screened
|
|
286
|
+
)
|
|
287
|
+
: findings;
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
238
291
|
export function createSanitizer(options = {}) {
|
|
239
292
|
options = applyProfile(options);
|
|
240
293
|
validateOptions(options);
|
|
@@ -281,21 +334,7 @@ export function createSanitizer(options = {}) {
|
|
|
281
334
|
: detected;
|
|
282
335
|
}
|
|
283
336
|
|
|
284
|
-
|
|
285
|
-
// including the verification passes over the published output.
|
|
286
|
-
async function screened(
|
|
287
|
-
text,
|
|
288
|
-
initial,
|
|
289
|
-
plain,
|
|
290
|
-
scopedOptions = options,
|
|
291
|
-
nativeFindings
|
|
292
|
-
) {
|
|
293
|
-
return exemptFindingsAsync(
|
|
294
|
-
text,
|
|
295
|
-
await analyze(text, initial, plain, scopedOptions, nativeFindings),
|
|
296
|
-
scopedOptions.exempt
|
|
297
|
-
);
|
|
298
|
-
}
|
|
337
|
+
const screened = createScreened(analyze, options);
|
|
299
338
|
|
|
300
339
|
const sanitizer = {
|
|
301
340
|
inspect: guarded(screened),
|
package/src/exempt.js
CHANGED
|
@@ -16,6 +16,30 @@ const declared = new Set([
|
|
|
16
16
|
const offered = (finding) =>
|
|
17
17
|
!declared.has(finding.rule) && finding.type !== 'JSON_VALUE';
|
|
18
18
|
|
|
19
|
+
// Only bounded escaped JSON views qualify for an independent policy check.
|
|
20
|
+
// Keep the decoder's 8192-character bound and cap recursive checks at three;
|
|
21
|
+
// malformed, oversized and other encoded findings retain normal handling.
|
|
22
|
+
export function escapedExemptionView(text, finding, depth) {
|
|
23
|
+
if (
|
|
24
|
+
depth >= 3 ||
|
|
25
|
+
finding.type !== 'ENCODED_SENSITIVE' ||
|
|
26
|
+
!['encoded', 'encoded-engine'].includes(finding.rule) ||
|
|
27
|
+
finding.end - finding.start > 8192
|
|
28
|
+
) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const raw = text.slice(finding.start, finding.end);
|
|
32
|
+
if (!raw.includes('\\')) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const decoded = JSON.parse(`"${raw}"`);
|
|
37
|
+
return decoded !== raw ? decoded : undefined;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
function view(text, finding) {
|
|
20
44
|
return Object.freeze({
|
|
21
45
|
...finding,
|
package/src/index.d.ts
CHANGED
|
@@ -125,6 +125,8 @@ export interface SanitizerOptions {
|
|
|
125
125
|
* Verification of the output consults it again per span, so the same value
|
|
126
126
|
* elsewhere is still caught. Declared knownSecrets/knownPersonal/
|
|
127
127
|
* confirmedPersonal values and JSON_VALUE denials are never offered.
|
|
128
|
+
* Bounded escaped JSON findings are independently inspected with the same
|
|
129
|
+
* policy (up to three levels); hooks there receive decoded text/offsets.
|
|
128
130
|
* createSyncSanitizer and sanitize() require a synchronous hook.
|
|
129
131
|
*/
|
|
130
132
|
exempt?: (
|
|
@@ -154,6 +156,7 @@ export type DebugEvent =
|
|
|
154
156
|
| { event: 'document-experience' | 'proper-name-prose'; count: number }
|
|
155
157
|
| { event: 'residual-redaction'; round: number }
|
|
156
158
|
| { event: 'template-value'; rule: string }
|
|
159
|
+
| { event: 'exempt-decoded-view'; rule: string }
|
|
157
160
|
| { event: 'two-pass-confirmation'; type: string }
|
|
158
161
|
| { event: 'propagation-skipped'; reason: 'length'; type: string }
|
|
159
162
|
| { event: 'gitleaks-rule'; rule: string; length: number; ms: number };
|
package/src/sanitizer.js
CHANGED
|
@@ -28,7 +28,11 @@ import { isIdentityType } from './identity-types.js';
|
|
|
28
28
|
import { rejectRestoreMap } from './restore-map.js';
|
|
29
29
|
import { allowListed, validateAllowList } from './allow-list.js';
|
|
30
30
|
import { templatedCredential } from './credentials.js';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
exemptFindings,
|
|
33
|
+
escapedExemptionView,
|
|
34
|
+
validateExempt,
|
|
35
|
+
} from './exempt.js';
|
|
32
36
|
import { builtInRules } from './rule-catalog.js';
|
|
33
37
|
import {
|
|
34
38
|
initializePlaceholders,
|
|
@@ -455,16 +459,31 @@ function locations(text, findings) {
|
|
|
455
459
|
});
|
|
456
460
|
}
|
|
457
461
|
|
|
458
|
-
export function inspect(text, options = {}) {
|
|
462
|
+
export function inspect(text, options = {}, exemptionDepth = 0) {
|
|
459
463
|
try {
|
|
460
464
|
const exempt = options?.exempt;
|
|
461
465
|
if (!exempt) {
|
|
462
466
|
return inspectText(text, options);
|
|
463
467
|
}
|
|
464
|
-
//
|
|
465
|
-
// the
|
|
468
|
+
// Screen source offsets first, then independently inspect escaped JSON
|
|
469
|
+
// with the same policy. A clean decoded view must not become a stale
|
|
470
|
+
// whole-view finding that rendering would replace with [REDACTED].
|
|
466
471
|
const findings = inspectText(text, { ...options, exempt: undefined });
|
|
467
|
-
return exemptFindings(text, findings, exempt)
|
|
472
|
+
return exemptFindings(text, findings, exempt).filter((finding) => {
|
|
473
|
+
const view = escapedExemptionView(text, finding, exemptionDepth);
|
|
474
|
+
if (view === undefined) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
const clean = !inspect(
|
|
478
|
+
view,
|
|
479
|
+
{ ...options, findings: [], structured: undefined },
|
|
480
|
+
exemptionDepth + 1
|
|
481
|
+
).length;
|
|
482
|
+
if (clean) {
|
|
483
|
+
options.debug?.({ event: 'exempt-decoded-view', rule: finding.rule });
|
|
484
|
+
}
|
|
485
|
+
return !clean;
|
|
486
|
+
});
|
|
468
487
|
} catch (error) {
|
|
469
488
|
throw internalFailure(error);
|
|
470
489
|
}
|
|
@@ -542,6 +561,14 @@ function inspectText(text, options = {}) {
|
|
|
542
561
|
if (projection.changed) {
|
|
543
562
|
nativeDetect(text, options, emit);
|
|
544
563
|
detectSync(text, options, emit);
|
|
564
|
+
} else if (
|
|
565
|
+
(options.knownSecrets ?? []).some(
|
|
566
|
+
(value, index) => value !== nativeOptions.knownSecrets[index]
|
|
567
|
+
)
|
|
568
|
+
) {
|
|
569
|
+
// Encoded exact bytes must remain detectable even when the declared
|
|
570
|
+
// secret normalizes (for example the ellipsis in a known Hive mask).
|
|
571
|
+
nativeDetect(text, { ...options, knownOnly: true }, emit);
|
|
545
572
|
}
|
|
546
573
|
nativeDetect(projection.text, nativeOptions, mapped);
|
|
547
574
|
detectSync(projection.text, nativeOptions, mapped);
|