@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 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. Known credential references remain exempt. The debug callback receives value-free `gitleaks-rule` events with rule ID, input length and elapsed milliseconds; tracing is off by default.
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.1",
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
- if (text.startsWith(REDACTED, start)) {
167
- return undefined;
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 match = /^(?:[a-z][a-z0-9_-]*[ \t]+)?[^\s"'\\,<>}\]]+/i.exec(
170
- text.slice(start)
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 largePatterns = new Map();
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 + (offsets ? offsets[match.index] : match.index) + offset,
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
- let linear = largePatterns.get(rule.id);
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 match;
178
- while ((match = pattern.exec(text)) !== null) {
179
- pattern.lastIndex =
180
- match.index + Array.from(match[0]).length || pattern.lastIndex + 1;
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) {