@link-foundation/sensitive-data-sanitizer 0.12.1 → 0.12.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 +6 -0
- package/docs/API.md +1 -1
- package/docs/COVERAGE.md +1 -1
- package/package.json +1 -1
- package/src/context.js +22 -6
- package/src/gitleaks.js +44 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 704c4b7: Preserve Authorization placeholders, matching brackets and control-byte boundaries. Bound repeated WASM input copies when scanning dense credentials by continuing with the existing RE2JS engine.
|
|
8
|
+
|
|
3
9
|
## 0.12.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/docs/API.md
CHANGED
|
@@ -334,4 +334,4 @@ permission values and literal credential references. `publicKnowledge: false`
|
|
|
334
334
|
restores IP detection; explicit `knownSecrets` and `knownPersonal` retain
|
|
335
335
|
priority. No shell command or workflow expression is executed.
|
|
336
336
|
|
|
337
|
-
Authorization headers (including Proxy-Authorization) redact the scheme and one credential token. Whitespace, quotes, backslashes and commas end the value, preserving subsequent log text in shell and escaped JSON prompts.
|
|
337
|
+
Authorization headers (including Proxy-Authorization) redact the scheme and one credential token. Whitespace, C0 controls (including NUL), DEL, quotes, backslashes and commas end the value, preserving subsequent log text in shell and escaped JSON prompts. Matching brackets opened within a token are included in its redaction; unmatched closing brackets remain part of the surrounding log. Already-redacted tokens and known credential references remain exempt after an optional scheme, including `<token>` placeholders. The debug callback receives value-free `gitleaks-rule` events with rule ID, input length and elapsed milliseconds; tracing is off by default.
|
package/docs/COVERAGE.md
CHANGED
|
@@ -6,7 +6,7 @@ Coverage is a union of maintained detectors, explicit known values, context and
|
|
|
6
6
|
|
|
7
7
|
The native provider supplements in `src/rules.js` cover GitHub, GitLab, OpenAI, Anthropic, AWS access IDs, Google API/OAuth, Slack, Stripe, SendGrid, Twilio, npm, PyPI, Telegram, Discord, Hugging Face, Shopify, Databricks, Square, Mailchimp, DigitalOcean, Vault, Grafana, Linear, Notion, Vercel, and JWT-shaped values. Prefix matches also protect selected truncated tokens; they do not validate a token with a provider.
|
|
8
8
|
|
|
9
|
-
The default native engine bundles 221 text rules from Gitleaks 8.30.1 (222 upstream rules; the path-only PKCS12 rule is outside text detection), compiled unchanged with RE2/WASM or RE2JS for inputs above 4 MiB. The MIT license, commit and sync script are retained in `src/vendor/gitleaks`. Keyword prefilters and configured entropy thresholds run natively; upstream comment/path allowlists cannot suppress a sanitizer finding. Quoted hex/base64 entropy detection supplements those rules. The default asynchronous engine additionally uses the pinned Secretlint recommended scanners, excluding its comment-filter rule. Embedded disable directives in untrusted text cannot suppress findings. Optional Gitleaks and TruffleHog invoke their installed detector sets, and custom detectors can add formats. Service counts are not a measure of comparative recall.
|
|
9
|
+
The default native engine bundles 221 text rules from Gitleaks 8.30.1 (222 upstream rules; the path-only PKCS12 rule is outside text detection), compiled unchanged with RE2/WASM or RE2JS for inputs above 4 MiB. Dense rule matches switch to RE2JS after at most eight WASM matches per view, bounding repeated whole-input copies without truncating secrets or changing anchors. The MIT license, commit and sync script are retained in `src/vendor/gitleaks`. Keyword prefilters and configured entropy thresholds run natively; upstream comment/path allowlists cannot suppress a sanitizer finding. Quoted hex/base64 entropy detection supplements those rules. The default asynchronous engine additionally uses the pinned Secretlint recommended scanners, excluding its comment-filter rule. Embedded disable directives in untrusted text cannot suppress findings. Optional Gitleaks and TruffleHog invoke their installed detector sets, and custom detectors can add formats. Service counts are not a measure of comparative recall.
|
|
10
10
|
|
|
11
11
|
Context rules recognize credential labels with bounded key affixes, JSON and nested escaped JSON, YAML/env/INI assignments, shell flags, XML values, URL userinfo, authorization headers, cookies, webhook paths, and complete or truncated private-key PEM blocks. Low-entropy contextual values such as `password=1234` are protected. Generic token counters, booleans/null/language literals and ordinary Git hashes are excluded to retain useful logs. Supply literal known values for ambiguous credentials or unusual key names.
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-foundation/sensitive-data-sanitizer",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.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",
|
package/src/context.js
CHANGED
|
@@ -163,13 +163,29 @@ export function authorizationRange(text, start) {
|
|
|
163
163
|
if (reference) {
|
|
164
164
|
return { start, end: start + reference[0].length };
|
|
165
165
|
}
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
const scheme = /^[a-z][a-z0-9_-]*[ \t]+/i.exec(text.slice(start));
|
|
167
|
+
const tokenStart = start + (scheme?.[0].length ?? 0);
|
|
168
|
+
const brackets = [];
|
|
169
|
+
let end = tokenStart;
|
|
170
|
+
for (; end < text.length; end++) {
|
|
171
|
+
const char = text[end];
|
|
172
|
+
const code = text.charCodeAt(end);
|
|
173
|
+
if (code < 32 || code === 127 || /[\s"'\\,]/.test(char)) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
if (char === '<' && end !== tokenStart && !brackets.length) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
const closing = { '{': '}', '[': ']', '<': '>' }[char];
|
|
180
|
+
if (closing) {
|
|
181
|
+
brackets.push(closing);
|
|
182
|
+
} else if (/[}\]>]/.test(char) && char !== brackets.pop()) {
|
|
183
|
+
// An unmatched closer belongs to the enclosing log structure.
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
168
186
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
);
|
|
172
|
-
return match ? { start, end: start + match[0].length } : undefined;
|
|
187
|
+
const token = text.slice(tokenStart, end);
|
|
188
|
+
return !token || credentialPlaceholder(token) ? undefined : { start, end };
|
|
173
189
|
}
|
|
174
190
|
function quotedRange(text, start, opening, maxLength) {
|
|
175
191
|
const depth = opening[1].length,
|
package/src/gitleaks.js
CHANGED
|
@@ -12,7 +12,7 @@ import { keywordMatcher } from './keywords.js';
|
|
|
12
12
|
|
|
13
13
|
export const gitleaksRules = [...r0, ...r1, ...r2, ...r3, ...r4, ...r5];
|
|
14
14
|
const compiled = new Map();
|
|
15
|
-
const
|
|
15
|
+
const linearPatterns = new Map();
|
|
16
16
|
const keywordIndex = keywordMatcher(
|
|
17
17
|
gitleaksRules.flatMap((r) =>
|
|
18
18
|
(r.keywords ?? []).map((word) => word.toLowerCase())
|
|
@@ -119,17 +119,6 @@ function* boundedViews(text, prefix, id) {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function scanRule(text, base, rule, emit, large) {
|
|
122
|
-
// re2-wasm 1.0.2 returns codepoint indices despite its RegExp-like API.
|
|
123
|
-
// Convert once per view, and keep its search cursor in codepoints too.
|
|
124
|
-
let offsets;
|
|
125
|
-
if (!large && /[\uD800-\uDBFF]/.test(text)) {
|
|
126
|
-
offsets = [0];
|
|
127
|
-
let offset = 0;
|
|
128
|
-
for (const char of text) {
|
|
129
|
-
offset += char.length;
|
|
130
|
-
offsets.push(offset);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
122
|
for (const match of ruleMatches(text, rule, large)) {
|
|
134
123
|
const secret = capturedSecret(rule, match);
|
|
135
124
|
if (!secret) {
|
|
@@ -140,12 +129,8 @@ function scanRule(text, base, rule, emit, large) {
|
|
|
140
129
|
const offset = match[0].indexOf(secret),
|
|
141
130
|
last = match[0].lastIndexOf(secret);
|
|
142
131
|
emit({
|
|
143
|
-
start: base +
|
|
144
|
-
end:
|
|
145
|
-
base +
|
|
146
|
-
(offsets ? offsets[match.index] : match.index) +
|
|
147
|
-
last +
|
|
148
|
-
secret.length,
|
|
132
|
+
start: base + match.index + offset,
|
|
133
|
+
end: base + match.index + last + secret.length,
|
|
149
134
|
type: 'SECRET',
|
|
150
135
|
category: 'credential',
|
|
151
136
|
rule: `gitleaks/${rule.id}`,
|
|
@@ -158,28 +143,57 @@ function* ruleMatches(text, rule, large) {
|
|
|
158
143
|
if (large) {
|
|
159
144
|
// re2-wasm has a fixed 16 MiB heap. RE2JS implements the same linear-time
|
|
160
145
|
// syntax without copying the input into WASM. Its indices are UTF-16.
|
|
161
|
-
|
|
162
|
-
if (!linear) {
|
|
163
|
-
linear = RE2JS.compile(rule.regex);
|
|
164
|
-
largePatterns.set(rule.id, linear);
|
|
165
|
-
}
|
|
166
|
-
if (linear.test(text)) {
|
|
167
|
-
yield* linear.matchAll(text);
|
|
168
|
-
}
|
|
146
|
+
yield* linearMatches(text, rule);
|
|
169
147
|
return;
|
|
170
148
|
}
|
|
149
|
+
// re2-wasm 1.0.2 returns codepoint indices despite its RegExp-like API.
|
|
150
|
+
// Normalize matches here so switching engines never changes public offsets.
|
|
151
|
+
let offsets;
|
|
152
|
+
if (/[\uD800-\uDBFF]/.test(text)) {
|
|
153
|
+
offsets = [0];
|
|
154
|
+
let offset = 0;
|
|
155
|
+
for (const char of text) {
|
|
156
|
+
offset += char.length;
|
|
157
|
+
offsets.push(offset);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
171
160
|
let pattern = compiled.get(rule.id);
|
|
172
161
|
if (!pattern) {
|
|
173
162
|
pattern = new RE2(rule.regex, 'gu');
|
|
174
163
|
compiled.set(rule.id, pattern);
|
|
175
164
|
}
|
|
176
165
|
pattern.lastIndex = 0;
|
|
177
|
-
let
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
166
|
+
for (let attempts = 0; attempts < 8; attempts++) {
|
|
167
|
+
const match = pattern.exec(text);
|
|
168
|
+
if (!match) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
pattern.lastIndex = match.index + Math.max(1, Array.from(match[0]).length);
|
|
172
|
+
match.index = offsets ? offsets[match.index] : match.index;
|
|
181
173
|
yield match;
|
|
182
174
|
}
|
|
175
|
+
// Every exec recopies the whole view into WASM. Bound those copies even
|
|
176
|
+
// for dense inputs, then replay at most eight matches in the JS engine.
|
|
177
|
+
// Keep the original subject: slicing would change anchors and boundaries.
|
|
178
|
+
const resume = offsets ? offsets[pattern.lastIndex] : pattern.lastIndex;
|
|
179
|
+
if (resume !== undefined && resume <= text.length) {
|
|
180
|
+
yield* linearMatches(text, rule, resume);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function* linearMatches(text, rule, resume = 0) {
|
|
185
|
+
let linear = linearPatterns.get(rule.id);
|
|
186
|
+
if (!linear) {
|
|
187
|
+
linear = RE2JS.compile(rule.regex);
|
|
188
|
+
linearPatterns.set(rule.id, linear);
|
|
189
|
+
}
|
|
190
|
+
if (linear.test(text)) {
|
|
191
|
+
for (const match of linear.matchAll(text)) {
|
|
192
|
+
if (match.index >= resume) {
|
|
193
|
+
yield match;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
function capturedSecret(rule, match) {
|