@privacyscrubber/sdk 2.3.9 → 2.4.1
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/README.md +85 -27
- package/benchmark.js +355 -0
- package/browser.js +276 -39
- package/cli.js +1 -1
- package/examples/python-bridge/MANIFEST.in +5 -0
- package/examples/python-bridge/README.md +265 -0
- package/examples/python-bridge/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/fastapi_example.py +206 -0
- package/examples/python-bridge/langchain_adapter.py +20 -0
- package/examples/python-bridge/langchain_example.py +126 -0
- package/examples/python-bridge/llamaindex_example.py +103 -0
- package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/privacyscrubber/cli.py +138 -0
- package/examples/python-bridge/privacyscrubber/client.py +548 -0
- package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
- package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
- package/examples/python-bridge/privacyscrubber/types.py +52 -0
- package/examples/python-bridge/privacyscrubber.py +36 -0
- package/examples/python-bridge/pyproject.toml +62 -0
- package/examples/python-bridge/setup.py +66 -0
- package/examples/python-bridge/test_python_bridge.py +320 -0
- package/examples/vercel-ai-stream.js +195 -0
- package/fastmcp.cjs +289 -8
- package/fastmcp.d.ts +18 -0
- package/fastmcp.js +4 -0
- package/index.cjs +99 -42
- package/index.js +103 -46
- package/package.json +15 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +400 -0
- package/vercel-ai.d.ts +156 -0
- package/vercel-ai.js +36 -0
- package/vision.cjs +13 -5
- package/vision.js +13 -5
package/index.cjs
CHANGED
|
@@ -36,16 +36,33 @@ if (scrubberPkg && typeof scrubberPkg.init === 'function') {
|
|
|
36
36
|
* Built-in patterns for DevOps & Cloud Secrets detection.
|
|
37
37
|
*/
|
|
38
38
|
const DEVOPS_SECRETS_DETECTOR = [
|
|
39
|
-
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{
|
|
39
|
+
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g, label: 'AWS_KEY' },
|
|
40
|
+
{ name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g, label: 'API_TOKEN' },
|
|
41
|
+
{ name: 'Anthropic API Key', regex: /\bsk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}\b/g, label: 'API_TOKEN' },
|
|
42
|
+
{ name: 'HuggingFace / Perplexity API Key', regex: /\b(?:hf_[a-zA-Z0-9]{20,64}|pplx-[a-zA-Z0-9]{24,64})\b/g, label: 'API_TOKEN' },
|
|
43
|
+
{ name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g, label: 'API_TOKEN' },
|
|
44
|
+
{ name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g, label: 'API_TOKEN' },
|
|
45
|
+
{ name: 'GitHub Fine-Grained PAT', regex: /\bgithub_pat_[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{40,80}\b/g, label: 'API_TOKEN' },
|
|
46
|
+
{ name: 'Slack Enterprise / Refresh Token', regex: /\bxoxe\.xox[baprs]-\d-[a-zA-Z0-9-]{10,}\b|\bxox[eapbrs]-[a-zA-Z0-9-]{10,}\b/g, label: 'API_TOKEN' },
|
|
47
|
+
{ name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g, label: 'API_TOKEN' },
|
|
48
|
+
{ name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g, label: 'SECRET' },
|
|
49
|
+
{ name: 'Modern Cloud & AI Platform Key', regex: /\b(?:sbp_[a-fA-F0-9]{40}|dop_v1_[a-fA-F0-9]{64}|doo_v1_[a-fA-F0-9]{64}|dp\.(?:st|pt|ct|sa)\.[a-zA-Z0-9_-]{20,}|lin_api_[a-zA-Z0-9]{32,64}|ntn_[a-zA-Z0-9]{32,64}|secret_[a-zA-Z0-9]{32,64}|gsk_[a-zA-Z0-9]{32,64}|r8_[a-zA-Z0-9]{24,64}|tvly-[a-zA-Z0-9_-]{20,64}|fc-[a-fA-F0-9]{32}|dapi[a-fA-F0-9]{32}(?:-\d)?|re_[a-zA-Z0-9_]{20,64}|napi_[a-zA-Z0-9]{32,64}|[a-zA-Z0-9]{14}\.atlasv1\.[a-zA-Z0-9]{50,80}|pypi-[A-Za-z0-9_-]{16,128}|whsec_[A-Za-z0-9+/=_-]{24,128}|shpat_[a-fA-F0-9]{32}|shpss_[a-fA-F0-9]{32}|shpca_[a-fA-F0-9]{32}|shppa_[a-fA-F0-9]{32}|hvs\.[A-Za-z0-9_-]{24,128}|hvb\.[A-Za-z0-9_-]{24,128}|glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}|glc_[A-Za-z0-9+/=_-]{20,128}|pscale_tkn_[A-Za-z0-9_.-]{20,64}|pscale_pw_[A-Za-z0-9_.-]{20,64}|pat[A-Za-z0-9]{14}\.[a-fA-F0-9]{64}|pat-(?:na1|eu1)-[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|sq0atp-[A-Za-z0-9_-]{22,64}|sq0csp-[A-Za-z0-9_-]{32,64}|NRAK-[A-Z0-9]{27}|v1\.0-[a-fA-F0-9]{24}-[a-fA-F0-9]{16,160}|ATATT3xFfGF0[A-Za-z0-9+/=_-]{24,128}|tskey-(?:auth|api|client)-[A-Za-z0-9_-]{20,96}|sntry[su]_[A-Za-z0-9+/=_-]{32,160}|(?:pk|sk)\.eyJ1[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{16,}|key-[a-f0-9]{32}|[a-f0-9]{32}-us\d{1,2})\b/g, label: 'API_TOKEN' },
|
|
50
|
+
{ name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g, label: 'API_TOKEN' },
|
|
51
|
+
{ name: 'Bcrypt / Argon2 Password Hash', regex: /(?<![A-Za-z0-9])(?:\$2[aby]\$\d{2}\$[./A-Za-z0-9=]{20,60}|\$argon2(?:id|i|d)\$[^\s"']{15,120})/g, label: 'SECRET' },
|
|
40
52
|
{ name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g, label: 'JWT_TOKEN' },
|
|
41
|
-
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g, label: 'API_TOKEN' },
|
|
42
|
-
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{
|
|
53
|
+
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_-]{10,}\b/g, label: 'API_TOKEN' },
|
|
54
|
+
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g, label: 'STRIPE_KEY' },
|
|
43
55
|
{ name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi, label: 'OPENAI_KEY' },
|
|
56
|
+
{ name: 'Slack / Discord Incoming Webhook URL', regex: /\bhttps:\/\/(?:hooks\.slack\.com\/services\/T[A-Z0-9_]+\/B[A-Z0-9_]+\/[A-Za-z0-9_-]+|(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d{17,20}\/[A-Za-z0-9_-]{60,70})\b/gi, label: 'SECRET' },
|
|
57
|
+
{ name: 'Azure Connection String / SAS Token', regex: /(?:DefaultEndpointsProtocol=https?;AccountName=[^;\s"']+;AccountKey=[A-Za-z0-9+/=]{20,}(?:;EndpointSuffix=[^;\s"']+)?|Endpoint=sb:\/\/[^;\s"']+;SharedAccessKeyName=[^;\s"']+;SharedAccessKey=[A-Za-z0-9+/=]{16,}|\bSharedAccessSignature=sr=[^\s"']+&sig=[A-Za-z0-9%/+=]{16,}|\bAccountKey=[A-Za-z0-9+/=]{40,88}==)/gi, label: 'SECRET' },
|
|
44
58
|
{ name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi, label: 'DB_URI' },
|
|
45
|
-
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
|
|
59
|
+
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|hxxps?|s?ftp|fxp|wss?):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?:\[\.\][a-zA-Z0-9_.-]+|\(\.\)[a-zA-Z0-9_.-]+)*(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
|
|
46
60
|
{ name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi, label: 'SECRET' },
|
|
61
|
+
{ name: 'CLI Credential Flag / Curl User Auth', regex: /(?:\bcurl\b[^\r\n]*?\s(?:-u|--user)\s+["']?[A-Za-z0-9_.+-]+:[^\s"']+|\b(?:--password|--api-key|--apikey|--access-token|--auth-token|--client-secret|--secret-key|--private-key)(?:=|\s+)["']?[^\s"']{6,}["']?)/gi, label: 'SECRET' },
|
|
62
|
+
{ name: 'HTTP API Key Header', regex: /\b(?:X-API-Key|X-Amz-Security-Token|X-Functions-Key|X-Shopify-Access-Token|X-Auth-Token|Api-Key)\s*:\s*["']?[A-Za-z0-9_.\-~+/=]{12,}["']?/gi, label: 'SECRET' },
|
|
63
|
+
{ name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi, label: 'SECRET' },
|
|
47
64
|
{ name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi, label: 'SECRET' },
|
|
48
|
-
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}
|
|
65
|
+
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}/gi, label: 'SECRET' },
|
|
49
66
|
{ name: 'Private Cryptographic Key (PEM Block)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]{10,8192}?-----END (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' },
|
|
50
67
|
{ name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' }
|
|
51
68
|
];
|
|
@@ -141,8 +158,8 @@ function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
141
158
|
frameworksSet.add('CCPA/CPRA');
|
|
142
159
|
}
|
|
143
160
|
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
|
|
144
|
-
frameworksSet.add('SOC 2
|
|
145
|
-
frameworksSet.add('ISO 27001
|
|
161
|
+
frameworksSet.add('SOC 2 controls');
|
|
162
|
+
frameworksSet.add('ISO 27001 A.8.11 controls');
|
|
146
163
|
}
|
|
147
164
|
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
|
|
148
165
|
frameworksSet.add('PCI DSS v4.0');
|
|
@@ -162,7 +179,7 @@ function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
const ipHoldings = {
|
|
165
|
-
standard: 'IETF draft-sibiryakov-ztds-protocol-
|
|
182
|
+
standard: 'IETF draft-sibiryakov-ztds-protocol-01',
|
|
166
183
|
standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
|
|
167
184
|
patent: "U.S. & Int'l Patents Pending (Paris Convention Priority)",
|
|
168
185
|
trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
|
|
@@ -172,27 +189,27 @@ function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
172
189
|
{
|
|
173
190
|
statute: 'EU AI Act (Art. 50)',
|
|
174
191
|
scope: 'AI Transparency & Data Minimization',
|
|
175
|
-
rule: '
|
|
192
|
+
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
|
|
176
193
|
},
|
|
177
194
|
{
|
|
178
195
|
statute: 'GDPR (Art. 4, 25, 28, 32)',
|
|
179
196
|
scope: 'Pseudonymization & Processor Liability',
|
|
180
|
-
rule: '
|
|
197
|
+
rule: 'Client-side pseudonymization is a technical measure supporting Art. 25 and 32. It does not by itself remove controller or processor obligations.'
|
|
181
198
|
},
|
|
182
199
|
{
|
|
183
200
|
statute: 'HIPAA 45 CFR § 164.514(b)',
|
|
184
201
|
scope: 'Safe Harbor De-Identification',
|
|
185
|
-
rule: '
|
|
202
|
+
rule: 'Pattern-based masking of direct identifiers supports Safe Harbor de-identification workflows. Review output for residual identifiers before sharing.'
|
|
186
203
|
},
|
|
187
204
|
{
|
|
188
205
|
statute: 'IRC § 7216 & IRS Pub. 1075',
|
|
189
206
|
scope: 'Tax Return Information Confidentiality',
|
|
190
|
-
rule: '
|
|
207
|
+
rule: 'Masking identifiers before AI use can reduce exposure of tax return information. Informational mapping, not legal advice.'
|
|
191
208
|
},
|
|
192
209
|
{
|
|
193
210
|
statute: 'FRE 502',
|
|
194
211
|
scope: 'Attorney-Client Privilege Preservation',
|
|
195
|
-
rule: '
|
|
212
|
+
rule: 'In-memory redaction can reduce disclosure of privileged details in AI workflows. Privilege determinations remain a legal judgment.'
|
|
196
213
|
}
|
|
197
214
|
];
|
|
198
215
|
|
|
@@ -1466,13 +1483,13 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
1466
1483
|
|
|
1467
1484
|
for (const k of sortedKeys) {
|
|
1468
1485
|
const val = tokenMap[k];
|
|
1469
|
-
const inner = k.replace(/^\[|<|\{\{
|
|
1486
|
+
const inner = k.replace(/^\[|<|\{\{?|__|\]|>|\}\}?|__/g, '');
|
|
1470
1487
|
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
1471
1488
|
|
|
1472
1489
|
if (match) {
|
|
1473
|
-
const label = escapeRegex(match[1]);
|
|
1490
|
+
const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
|
|
1474
1491
|
const idx = match[2];
|
|
1475
|
-
const patternStr = '(?:\\[\\s*' + label + '[-_
|
|
1492
|
+
const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『)\\s*[*`~]*' + label + '[-_\\\\\\s]*0*' + idx + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』)|<\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + label + '[-_\\\\\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
1476
1493
|
patterns.push(patternStr);
|
|
1477
1494
|
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
1478
1495
|
lookup.set(normKey, val);
|
|
@@ -1501,10 +1518,14 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
1501
1518
|
} else if (lookup.has(coreMatch.toUpperCase())) {
|
|
1502
1519
|
restored = lookup.get(coreMatch.toUpperCase());
|
|
1503
1520
|
} else {
|
|
1504
|
-
const inner = coreMatch
|
|
1505
|
-
|
|
1521
|
+
const inner = coreMatch
|
|
1522
|
+
.replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
|
|
1523
|
+
.replace(/^[*`~\s]+|[.,;:!?*`~\s]+$/g, '')
|
|
1524
|
+
.trim();
|
|
1525
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
|
|
1506
1526
|
if (m) {
|
|
1507
|
-
const
|
|
1527
|
+
const cleanLabel = m[1].replace(/\\([_-])/g, '$1');
|
|
1528
|
+
const normKey = (cleanLabel + '_' + parseInt(m[2], 10)).toUpperCase();
|
|
1508
1529
|
if (lookup.has(normKey)) {
|
|
1509
1530
|
restored = lookup.get(normKey);
|
|
1510
1531
|
}
|
|
@@ -1610,18 +1631,17 @@ class StreamingDetokenizer {
|
|
|
1610
1631
|
this.pendingBuffer += text;
|
|
1611
1632
|
|
|
1612
1633
|
// Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
|
|
1613
|
-
// Matches incomplete tokens starting with [, <, {
|
|
1634
|
+
// Matches incomplete tokens starting with [, 【, [, <, {, __, %5B, \u005b, or HTML entity [ / [
|
|
1614
1635
|
const tailLen = 160;
|
|
1615
1636
|
const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
1616
1637
|
const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
1617
|
-
const m = tail.match(/(?:\[
|
|
1638
|
+
const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-А-Яа-яЁё *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)$/);
|
|
1618
1639
|
|
|
1619
1640
|
if (m) {
|
|
1620
1641
|
const candidate = m[0];
|
|
1621
|
-
//
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
|
|
1642
|
+
// Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
|
|
1643
|
+
const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
|
|
1644
|
+
if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
|
|
1625
1645
|
const toEmit = this.pendingBuffer;
|
|
1626
1646
|
this.pendingBuffer = '';
|
|
1627
1647
|
return this.replacer(toEmit);
|
|
@@ -1843,13 +1863,8 @@ function withPrivacyScrubber(handler, options = {}) {
|
|
|
1843
1863
|
tokenMap = res.tokenMap;
|
|
1844
1864
|
telemetry = res.telemetry;
|
|
1845
1865
|
} else if (rawBody && typeof rawBody === 'object') {
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
try {
|
|
1849
|
-
scrubbedBody = JSON.parse(res.scrubbedText);
|
|
1850
|
-
} catch (_) {
|
|
1851
|
-
scrubbedBody = rawBody;
|
|
1852
|
-
}
|
|
1866
|
+
const res = sanitizeObject(rawBody, options);
|
|
1867
|
+
scrubbedBody = res.sanitized;
|
|
1853
1868
|
tokenMap = res.tokenMap;
|
|
1854
1869
|
telemetry = res.telemetry;
|
|
1855
1870
|
}
|
|
@@ -1860,7 +1875,8 @@ function withPrivacyScrubber(handler, options = {}) {
|
|
|
1860
1875
|
scrubbedBody,
|
|
1861
1876
|
tokenMap,
|
|
1862
1877
|
telemetry,
|
|
1863
|
-
restore: (text) => restore(text, tokenMap),
|
|
1878
|
+
restore: (text) => (text && typeof text === 'object') ? restoreObject(text, tokenMap) : restore(text, tokenMap),
|
|
1879
|
+
restoreObject: (obj) => restoreObject(obj, tokenMap),
|
|
1864
1880
|
createStreamRestorer: () => createStreamTokenRestorer(tokenMap)
|
|
1865
1881
|
};
|
|
1866
1882
|
|
|
@@ -1878,15 +1894,13 @@ function withPrivacyScrubber(handler, options = {}) {
|
|
|
1878
1894
|
function createExpressMiddleware(options = {}) {
|
|
1879
1895
|
return function privacyScrubberExpress(req, res, next) {
|
|
1880
1896
|
if (req && req.body && typeof req.body === 'object') {
|
|
1881
|
-
const
|
|
1882
|
-
const { scrubbedText, tokenMap, telemetry } = sanitize(serialized, options);
|
|
1897
|
+
const { sanitized, tokenMap, telemetry } = sanitizeObject(req.body, options);
|
|
1883
1898
|
req.rawBody = req.body;
|
|
1884
|
-
|
|
1885
|
-
req.body = JSON.parse(scrubbedText);
|
|
1886
|
-
} catch (_) {}
|
|
1899
|
+
req.body = sanitized;
|
|
1887
1900
|
req.psTokenMap = tokenMap;
|
|
1888
1901
|
req.psTelemetry = telemetry;
|
|
1889
|
-
req.restore = (aiResponse) => restore(aiResponse, tokenMap);
|
|
1902
|
+
req.restore = (aiResponse) => (aiResponse && typeof aiResponse === 'object') ? restoreObject(aiResponse, tokenMap) : restore(aiResponse, tokenMap);
|
|
1903
|
+
req.restoreObject = (obj) => restoreObject(obj, tokenMap);
|
|
1890
1904
|
req.createStreamRestorer = () => createStreamTokenRestorer(tokenMap);
|
|
1891
1905
|
}
|
|
1892
1906
|
if (typeof next === 'function') next();
|
|
@@ -1903,7 +1917,7 @@ function formatCefEvent(eventData = {}) {
|
|
|
1903
1917
|
const {
|
|
1904
1918
|
vendor = 'PrivacyScrubber',
|
|
1905
1919
|
product = 'ZTDS',
|
|
1906
|
-
version = '2.
|
|
1920
|
+
version = '2.4.1',
|
|
1907
1921
|
eventClassId = 'PII_REDACTION',
|
|
1908
1922
|
eventName = 'Zero-Trust PII Sanitization Executed',
|
|
1909
1923
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -1972,7 +1986,7 @@ function formatSyslogEvent(eventData = {}) {
|
|
|
1972
1986
|
function formatJsonlEvent(eventData = {}) {
|
|
1973
1987
|
const {
|
|
1974
1988
|
timestamp = new Date().toISOString(),
|
|
1975
|
-
version = '2.
|
|
1989
|
+
version = '2.4.1',
|
|
1976
1990
|
protocol = 'ZTDS',
|
|
1977
1991
|
event = 'sanitization',
|
|
1978
1992
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -2508,7 +2522,46 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
|
|
|
2508
2522
|
throw new Error(`[guard_read_file] Path is a directory, not a file: ${resolved}`);
|
|
2509
2523
|
}
|
|
2510
2524
|
|
|
2511
|
-
const
|
|
2525
|
+
const rawBuf = fs.readFileSync(resolved);
|
|
2526
|
+
const checkLen = Math.min(rawBuf.length, 8000);
|
|
2527
|
+
for (let i = 0; i < checkLen; i++) {
|
|
2528
|
+
if (rawBuf[i] === 0) {
|
|
2529
|
+
throw new Error(`[guard_read_file] Binary file detected (${path.basename(resolved)}). Only text, code, and MIME files are supported.`);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
let raw = rawBuf.toString('utf8');
|
|
2533
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
2534
|
+
if (ext === '.eml') {
|
|
2535
|
+
const decodeQpUtf8 = (str) =>
|
|
2536
|
+
str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
2537
|
+
try {
|
|
2538
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
2539
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2540
|
+
bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
2541
|
+
}
|
|
2542
|
+
return bytes.toString('utf8');
|
|
2543
|
+
} catch (_) { return seq; }
|
|
2544
|
+
});
|
|
2545
|
+
raw = raw.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, _cs, enc, payload) => {
|
|
2546
|
+
try {
|
|
2547
|
+
return enc.toUpperCase() === 'B'
|
|
2548
|
+
? Buffer.from(payload.replace(/\s+/g, ''), 'base64').toString('utf8')
|
|
2549
|
+
: decodeQpUtf8(payload.replace(/_/g, ' '));
|
|
2550
|
+
} catch (_) { return full; }
|
|
2551
|
+
});
|
|
2552
|
+
raw = decodeQpUtf8(raw);
|
|
2553
|
+
} else if (ext === '.rtf') {
|
|
2554
|
+
raw = raw
|
|
2555
|
+
.replace(/\\par\b|\\line\b/gi, '\n')
|
|
2556
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
2557
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
2558
|
+
const n = parseInt(code, 10);
|
|
2559
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
2560
|
+
})
|
|
2561
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, ' ')
|
|
2562
|
+
.replace(/[{}]/g, ' ')
|
|
2563
|
+
.replace(/[ \t]+/g, ' ');
|
|
2564
|
+
}
|
|
2512
2565
|
const lines = raw.split(/\r?\n/);
|
|
2513
2566
|
const capped = lines.slice(0, maxLines).join('\n');
|
|
2514
2567
|
const wasTruncated = lines.length > maxLines;
|
|
@@ -2664,6 +2717,10 @@ const _defaultExport = {
|
|
|
2664
2717
|
buildCisoAuditTelemetry,
|
|
2665
2718
|
formatAuditReceipt,
|
|
2666
2719
|
generateAuditReport,
|
|
2720
|
+
PSR2_VERSION,
|
|
2721
|
+
PSR2_STATEMENTS,
|
|
2722
|
+
buildPsr2Receipt,
|
|
2723
|
+
psr2Lines,
|
|
2667
2724
|
formatCefEvent,
|
|
2668
2725
|
formatSyslogEvent,
|
|
2669
2726
|
formatJsonlEvent,
|
package/index.js
CHANGED
|
@@ -38,16 +38,33 @@ if (scrubberPkg && typeof scrubberPkg.init === 'function') {
|
|
|
38
38
|
* Built-in patterns for DevOps & Cloud Secrets detection.
|
|
39
39
|
*/
|
|
40
40
|
export const DEVOPS_SECRETS_DETECTOR = [
|
|
41
|
-
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{
|
|
41
|
+
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g, label: 'AWS_KEY' },
|
|
42
|
+
{ name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g, label: 'API_TOKEN' },
|
|
43
|
+
{ name: 'Anthropic API Key', regex: /\bsk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}\b/g, label: 'API_TOKEN' },
|
|
44
|
+
{ name: 'HuggingFace / Perplexity API Key', regex: /\b(?:hf_[a-zA-Z0-9]{20,64}|pplx-[a-zA-Z0-9]{24,64})\b/g, label: 'API_TOKEN' },
|
|
45
|
+
{ name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g, label: 'API_TOKEN' },
|
|
46
|
+
{ name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g, label: 'API_TOKEN' },
|
|
47
|
+
{ name: 'GitHub Fine-Grained PAT', regex: /\bgithub_pat_[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{40,80}\b/g, label: 'API_TOKEN' },
|
|
48
|
+
{ name: 'Slack Enterprise / Refresh Token', regex: /\bxoxe\.xox[baprs]-\d-[a-zA-Z0-9-]{10,}\b|\bxox[eapbrs]-[a-zA-Z0-9-]{10,}\b/g, label: 'API_TOKEN' },
|
|
49
|
+
{ name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g, label: 'API_TOKEN' },
|
|
50
|
+
{ name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g, label: 'SECRET' },
|
|
51
|
+
{ name: 'Modern Cloud & AI Platform Key', regex: /\b(?:sbp_[a-fA-F0-9]{40}|dop_v1_[a-fA-F0-9]{64}|doo_v1_[a-fA-F0-9]{64}|dp\.(?:st|pt|ct|sa)\.[a-zA-Z0-9_-]{20,}|lin_api_[a-zA-Z0-9]{32,64}|ntn_[a-zA-Z0-9]{32,64}|secret_[a-zA-Z0-9]{32,64}|gsk_[a-zA-Z0-9]{32,64}|r8_[a-zA-Z0-9]{24,64}|tvly-[a-zA-Z0-9_-]{20,64}|fc-[a-fA-F0-9]{32}|dapi[a-fA-F0-9]{32}(?:-\d)?|re_[a-zA-Z0-9_]{20,64}|napi_[a-zA-Z0-9]{32,64}|[a-zA-Z0-9]{14}\.atlasv1\.[a-zA-Z0-9]{50,80}|pypi-[A-Za-z0-9_-]{16,128}|whsec_[A-Za-z0-9+/=_-]{24,128}|shpat_[a-fA-F0-9]{32}|shpss_[a-fA-F0-9]{32}|shpca_[a-fA-F0-9]{32}|shppa_[a-fA-F0-9]{32}|hvs\.[A-Za-z0-9_-]{24,128}|hvb\.[A-Za-z0-9_-]{24,128}|glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}|glc_[A-Za-z0-9+/=_-]{20,128}|pscale_tkn_[A-Za-z0-9_.-]{20,64}|pscale_pw_[A-Za-z0-9_.-]{20,64}|pat[A-Za-z0-9]{14}\.[a-fA-F0-9]{64}|pat-(?:na1|eu1)-[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|sq0atp-[A-Za-z0-9_-]{22,64}|sq0csp-[A-Za-z0-9_-]{32,64}|NRAK-[A-Z0-9]{27}|v1\.0-[a-fA-F0-9]{24}-[a-fA-F0-9]{16,160}|ATATT3xFfGF0[A-Za-z0-9+/=_-]{24,128}|tskey-(?:auth|api|client)-[A-Za-z0-9_-]{20,96}|sntry[su]_[A-Za-z0-9+/=_-]{32,160}|(?:pk|sk)\.eyJ1[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{16,}|key-[a-f0-9]{32}|[a-f0-9]{32}-us\d{1,2})\b/g, label: 'API_TOKEN' },
|
|
52
|
+
{ name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g, label: 'API_TOKEN' },
|
|
53
|
+
{ name: 'Bcrypt / Argon2 Password Hash', regex: /(?<![A-Za-z0-9])(?:\$2[aby]\$\d{2}\$[./A-Za-z0-9=]{20,60}|\$argon2(?:id|i|d)\$[^\s"']{15,120})/g, label: 'SECRET' },
|
|
42
54
|
{ name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g, label: 'JWT_TOKEN' },
|
|
43
|
-
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g, label: 'API_TOKEN' },
|
|
44
|
-
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{
|
|
55
|
+
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_-]{10,}\b/g, label: 'API_TOKEN' },
|
|
56
|
+
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g, label: 'STRIPE_KEY' },
|
|
45
57
|
{ name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi, label: 'OPENAI_KEY' },
|
|
58
|
+
{ name: 'Slack / Discord Incoming Webhook URL', regex: /\bhttps:\/\/(?:hooks\.slack\.com\/services\/T[A-Z0-9_]+\/B[A-Z0-9_]+\/[A-Za-z0-9_-]+|(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d{17,20}\/[A-Za-z0-9_-]{60,70})\b/gi, label: 'SECRET' },
|
|
59
|
+
{ name: 'Azure Connection String / SAS Token', regex: /(?:DefaultEndpointsProtocol=https?;AccountName=[^;\s"']+;AccountKey=[A-Za-z0-9+/=]{20,}(?:;EndpointSuffix=[^;\s"']+)?|Endpoint=sb:\/\/[^;\s"']+;SharedAccessKeyName=[^;\s"']+;SharedAccessKey=[A-Za-z0-9+/=]{16,}|\bSharedAccessSignature=sr=[^\s"']+&sig=[A-Za-z0-9%/+=]{16,}|\bAccountKey=[A-Za-z0-9+/=]{40,88}==)/gi, label: 'SECRET' },
|
|
46
60
|
{ name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi, label: 'DB_URI' },
|
|
47
|
-
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
|
|
61
|
+
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|hxxps?|s?ftp|fxp|wss?):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?:\[\.\][a-zA-Z0-9_.-]+|\(\.\)[a-zA-Z0-9_.-]+)*(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
|
|
48
62
|
{ name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi, label: 'SECRET' },
|
|
63
|
+
{ name: 'CLI Credential Flag / Curl User Auth', regex: /(?:\bcurl\b[^\r\n]*?\s(?:-u|--user)\s+["']?[A-Za-z0-9_.+-]+:[^\s"']+|\b(?:--password|--api-key|--apikey|--access-token|--auth-token|--client-secret|--secret-key|--private-key)(?:=|\s+)["']?[^\s"']{6,}["']?)/gi, label: 'SECRET' },
|
|
64
|
+
{ name: 'HTTP API Key Header', regex: /\b(?:X-API-Key|X-Amz-Security-Token|X-Functions-Key|X-Shopify-Access-Token|X-Auth-Token|Api-Key)\s*:\s*["']?[A-Za-z0-9_.\-~+/=]{12,}["']?/gi, label: 'SECRET' },
|
|
65
|
+
{ name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi, label: 'SECRET' },
|
|
49
66
|
{ name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi, label: 'SECRET' },
|
|
50
|
-
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}
|
|
67
|
+
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}/gi, label: 'SECRET' },
|
|
51
68
|
{ name: 'Private Cryptographic Key (PEM Block)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]{10,8192}?-----END (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' },
|
|
52
69
|
{ name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' }
|
|
53
70
|
];
|
|
@@ -143,8 +160,8 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
143
160
|
frameworksSet.add('CCPA/CPRA');
|
|
144
161
|
}
|
|
145
162
|
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
|
|
146
|
-
frameworksSet.add('SOC 2
|
|
147
|
-
frameworksSet.add('ISO 27001
|
|
163
|
+
frameworksSet.add('SOC 2 controls');
|
|
164
|
+
frameworksSet.add('ISO 27001 A.8.11 controls');
|
|
148
165
|
}
|
|
149
166
|
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
|
|
150
167
|
frameworksSet.add('PCI DSS v4.0');
|
|
@@ -164,7 +181,7 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
const ipHoldings = {
|
|
167
|
-
standard: 'IETF draft-sibiryakov-ztds-protocol-
|
|
184
|
+
standard: 'IETF draft-sibiryakov-ztds-protocol-01',
|
|
168
185
|
standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
|
|
169
186
|
patent: "U.S. & Int'l Patents Pending (Paris Convention Priority)",
|
|
170
187
|
trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
|
|
@@ -174,27 +191,27 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
174
191
|
{
|
|
175
192
|
statute: 'EU AI Act (Art. 50)',
|
|
176
193
|
scope: 'AI Transparency & Data Minimization',
|
|
177
|
-
rule: '
|
|
194
|
+
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
|
|
178
195
|
},
|
|
179
196
|
{
|
|
180
197
|
statute: 'GDPR (Art. 4, 25, 28, 32)',
|
|
181
198
|
scope: 'Pseudonymization & Processor Liability',
|
|
182
|
-
rule: '
|
|
199
|
+
rule: 'Client-side pseudonymization is a technical measure supporting Art. 25 and 32. It does not by itself remove controller or processor obligations.'
|
|
183
200
|
},
|
|
184
201
|
{
|
|
185
202
|
statute: 'HIPAA 45 CFR § 164.514(b)',
|
|
186
203
|
scope: 'Safe Harbor De-Identification',
|
|
187
|
-
rule: '
|
|
204
|
+
rule: 'Pattern-based masking of direct identifiers supports Safe Harbor de-identification workflows. Review output for residual identifiers before sharing.'
|
|
188
205
|
},
|
|
189
206
|
{
|
|
190
207
|
statute: 'IRC § 7216 & IRS Pub. 1075',
|
|
191
208
|
scope: 'Tax Return Information Confidentiality',
|
|
192
|
-
rule: '
|
|
209
|
+
rule: 'Masking identifiers before AI use can reduce exposure of tax return information. Informational mapping, not legal advice.'
|
|
193
210
|
},
|
|
194
211
|
{
|
|
195
212
|
statute: 'FRE 502',
|
|
196
213
|
scope: 'Attorney-Client Privilege Preservation',
|
|
197
|
-
rule: '
|
|
214
|
+
rule: 'In-memory redaction can reduce disclosure of privileged details in AI workflows. Privilege determinations remain a legal judgment.'
|
|
198
215
|
}
|
|
199
216
|
];
|
|
200
217
|
|
|
@@ -1468,13 +1485,13 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
1468
1485
|
|
|
1469
1486
|
for (const k of sortedKeys) {
|
|
1470
1487
|
const val = tokenMap[k];
|
|
1471
|
-
const inner = k.replace(/^\[|<|\{\{
|
|
1488
|
+
const inner = k.replace(/^\[|<|\{\{?|__|\]|>|\}\}?|__/g, '');
|
|
1472
1489
|
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
1473
1490
|
|
|
1474
1491
|
if (match) {
|
|
1475
|
-
const label = escapeRegex(match[1]);
|
|
1492
|
+
const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
|
|
1476
1493
|
const idx = match[2];
|
|
1477
|
-
const patternStr = '(?:\\[\\s*' + label + '[-_
|
|
1494
|
+
const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『)\\s*[*`~]*' + label + '[-_\\\\\\s]*0*' + idx + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』)|<\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + label + '[-_\\\\\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
1478
1495
|
patterns.push(patternStr);
|
|
1479
1496
|
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
1480
1497
|
lookup.set(normKey, val);
|
|
@@ -1503,10 +1520,14 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
1503
1520
|
} else if (lookup.has(coreMatch.toUpperCase())) {
|
|
1504
1521
|
restored = lookup.get(coreMatch.toUpperCase());
|
|
1505
1522
|
} else {
|
|
1506
|
-
const inner = coreMatch
|
|
1507
|
-
|
|
1523
|
+
const inner = coreMatch
|
|
1524
|
+
.replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
|
|
1525
|
+
.replace(/^[*`~\s]+|[.,;:!?*`~\s]+$/g, '')
|
|
1526
|
+
.trim();
|
|
1527
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
|
|
1508
1528
|
if (m) {
|
|
1509
|
-
const
|
|
1529
|
+
const cleanLabel = m[1].replace(/\\([_-])/g, '$1');
|
|
1530
|
+
const normKey = (cleanLabel + '_' + parseInt(m[2], 10)).toUpperCase();
|
|
1510
1531
|
if (lookup.has(normKey)) {
|
|
1511
1532
|
restored = lookup.get(normKey);
|
|
1512
1533
|
}
|
|
@@ -1612,18 +1633,17 @@ export class StreamingDetokenizer {
|
|
|
1612
1633
|
this.pendingBuffer += text;
|
|
1613
1634
|
|
|
1614
1635
|
// Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
|
|
1615
|
-
// Matches incomplete tokens starting with [, <, {
|
|
1636
|
+
// Matches incomplete tokens starting with [, 【, [, <, {, __, %5B, \u005b, or HTML entity [ / [
|
|
1616
1637
|
const tailLen = 160;
|
|
1617
1638
|
const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
1618
1639
|
const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
1619
|
-
const m = tail.match(/(?:\[
|
|
1640
|
+
const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-А-Яа-яЁё *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)$/);
|
|
1620
1641
|
|
|
1621
1642
|
if (m) {
|
|
1622
1643
|
const candidate = m[0];
|
|
1623
|
-
//
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
|
|
1644
|
+
// Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
|
|
1645
|
+
const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
|
|
1646
|
+
if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
|
|
1627
1647
|
const toEmit = this.pendingBuffer;
|
|
1628
1648
|
this.pendingBuffer = '';
|
|
1629
1649
|
return this.replacer(toEmit);
|
|
@@ -1845,13 +1865,8 @@ export function withPrivacyScrubber(handler, options = {}) {
|
|
|
1845
1865
|
tokenMap = res.tokenMap;
|
|
1846
1866
|
telemetry = res.telemetry;
|
|
1847
1867
|
} else if (rawBody && typeof rawBody === 'object') {
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
try {
|
|
1851
|
-
scrubbedBody = JSON.parse(res.scrubbedText);
|
|
1852
|
-
} catch (_) {
|
|
1853
|
-
scrubbedBody = rawBody;
|
|
1854
|
-
}
|
|
1868
|
+
const res = sanitizeObject(rawBody, options);
|
|
1869
|
+
scrubbedBody = res.sanitized;
|
|
1855
1870
|
tokenMap = res.tokenMap;
|
|
1856
1871
|
telemetry = res.telemetry;
|
|
1857
1872
|
}
|
|
@@ -1862,7 +1877,8 @@ export function withPrivacyScrubber(handler, options = {}) {
|
|
|
1862
1877
|
scrubbedBody,
|
|
1863
1878
|
tokenMap,
|
|
1864
1879
|
telemetry,
|
|
1865
|
-
restore: (text) => restore(text, tokenMap),
|
|
1880
|
+
restore: (text) => (text && typeof text === 'object') ? restoreObject(text, tokenMap) : restore(text, tokenMap),
|
|
1881
|
+
restoreObject: (obj) => restoreObject(obj, tokenMap),
|
|
1866
1882
|
createStreamRestorer: () => createStreamTokenRestorer(tokenMap)
|
|
1867
1883
|
};
|
|
1868
1884
|
|
|
@@ -1880,15 +1896,13 @@ export function withPrivacyScrubber(handler, options = {}) {
|
|
|
1880
1896
|
export function createExpressMiddleware(options = {}) {
|
|
1881
1897
|
return function privacyScrubberExpress(req, res, next) {
|
|
1882
1898
|
if (req && req.body && typeof req.body === 'object') {
|
|
1883
|
-
const
|
|
1884
|
-
const { scrubbedText, tokenMap, telemetry } = sanitize(serialized, options);
|
|
1899
|
+
const { sanitized, tokenMap, telemetry } = sanitizeObject(req.body, options);
|
|
1885
1900
|
req.rawBody = req.body;
|
|
1886
|
-
|
|
1887
|
-
req.body = JSON.parse(scrubbedText);
|
|
1888
|
-
} catch (_) {}
|
|
1901
|
+
req.body = sanitized;
|
|
1889
1902
|
req.psTokenMap = tokenMap;
|
|
1890
1903
|
req.psTelemetry = telemetry;
|
|
1891
|
-
req.restore = (aiResponse) => restore(aiResponse, tokenMap);
|
|
1904
|
+
req.restore = (aiResponse) => (aiResponse && typeof aiResponse === 'object') ? restoreObject(aiResponse, tokenMap) : restore(aiResponse, tokenMap);
|
|
1905
|
+
req.restoreObject = (obj) => restoreObject(obj, tokenMap);
|
|
1892
1906
|
req.createStreamRestorer = () => createStreamTokenRestorer(tokenMap);
|
|
1893
1907
|
}
|
|
1894
1908
|
if (typeof next === 'function') next();
|
|
@@ -1905,7 +1919,7 @@ export function formatCefEvent(eventData = {}) {
|
|
|
1905
1919
|
const {
|
|
1906
1920
|
vendor = 'PrivacyScrubber',
|
|
1907
1921
|
product = 'ZTDS',
|
|
1908
|
-
version = '2.
|
|
1922
|
+
version = '2.4.1',
|
|
1909
1923
|
eventClassId = 'PII_REDACTION',
|
|
1910
1924
|
eventName = 'Zero-Trust PII Sanitization Executed',
|
|
1911
1925
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -1974,7 +1988,7 @@ export function formatSyslogEvent(eventData = {}) {
|
|
|
1974
1988
|
export function formatJsonlEvent(eventData = {}) {
|
|
1975
1989
|
const {
|
|
1976
1990
|
timestamp = new Date().toISOString(),
|
|
1977
|
-
version = '2.
|
|
1991
|
+
version = '2.4.1',
|
|
1978
1992
|
protocol = 'ZTDS',
|
|
1979
1993
|
event = 'sanitization',
|
|
1980
1994
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -2138,8 +2152,8 @@ export function createAuditLogger(options = {}) {
|
|
|
2138
2152
|
}
|
|
2139
2153
|
|
|
2140
2154
|
// ── PSR-2 Receipt Integrity (real SHA-256; parity with src/modules/receipt-integrity.js) ──
|
|
2141
|
-
const PSR2_VERSION = 'PSR-2';
|
|
2142
|
-
const PSR2_STATEMENTS = {
|
|
2155
|
+
export const PSR2_VERSION = 'PSR-2';
|
|
2156
|
+
export const PSR2_STATEMENTS = {
|
|
2143
2157
|
measured: [
|
|
2144
2158
|
'Token Manifest SHA-256: digest of the sorted token labels held in this session (original values are never hashed or exported).',
|
|
2145
2159
|
'Entity counts per type.',
|
|
@@ -2159,7 +2173,7 @@ const PSR2_STATEMENTS = {
|
|
|
2159
2173
|
function psr2Sha256(input) {
|
|
2160
2174
|
return require('crypto').createHash('sha256').update(String(input == null ? '' : input), 'utf8').digest('hex');
|
|
2161
2175
|
}
|
|
2162
|
-
function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
|
|
2176
|
+
export function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
|
|
2163
2177
|
const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
|
|
2164
2178
|
const counts = {};
|
|
2165
2179
|
for (const t of labels) {
|
|
@@ -2172,7 +2186,7 @@ function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
|
|
|
2172
2186
|
const canonical = [PSR2_VERSION, timestamp, 'none', manifestSha256, entityCounts, 'na'].join('|');
|
|
2173
2187
|
return { version: PSR2_VERSION, timestamp, outputSha256: null, manifestSha256, entityCounts, tokensTotal: labels.length, tokensPresentInOutput: null, canonical, receiptSha256: psr2Sha256(canonical) };
|
|
2174
2188
|
}
|
|
2175
|
-
function psr2Lines(r) {
|
|
2189
|
+
export function psr2Lines(r) {
|
|
2176
2190
|
return [
|
|
2177
2191
|
`Receipt Version: ${r.version}`,
|
|
2178
2192
|
`Timestamp (UTC): ${r.timestamp}`,
|
|
@@ -2510,7 +2524,46 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
|
|
|
2510
2524
|
throw new Error(`[guard_read_file] Path is a directory, not a file: ${resolved}`);
|
|
2511
2525
|
}
|
|
2512
2526
|
|
|
2513
|
-
const
|
|
2527
|
+
const rawBuf = fs.readFileSync(resolved);
|
|
2528
|
+
const checkLen = Math.min(rawBuf.length, 8000);
|
|
2529
|
+
for (let i = 0; i < checkLen; i++) {
|
|
2530
|
+
if (rawBuf[i] === 0) {
|
|
2531
|
+
throw new Error(`[guard_read_file] Binary file detected (${path.basename(resolved)}). Only text, code, and MIME files are supported.`);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
let raw = rawBuf.toString('utf8');
|
|
2535
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
2536
|
+
if (ext === '.eml') {
|
|
2537
|
+
const decodeQpUtf8 = (str) =>
|
|
2538
|
+
str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
2539
|
+
try {
|
|
2540
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
2541
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2542
|
+
bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
2543
|
+
}
|
|
2544
|
+
return bytes.toString('utf8');
|
|
2545
|
+
} catch (_) { return seq; }
|
|
2546
|
+
});
|
|
2547
|
+
raw = raw.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, _cs, enc, payload) => {
|
|
2548
|
+
try {
|
|
2549
|
+
return enc.toUpperCase() === 'B'
|
|
2550
|
+
? Buffer.from(payload.replace(/\s+/g, ''), 'base64').toString('utf8')
|
|
2551
|
+
: decodeQpUtf8(payload.replace(/_/g, ' '));
|
|
2552
|
+
} catch (_) { return full; }
|
|
2553
|
+
});
|
|
2554
|
+
raw = decodeQpUtf8(raw);
|
|
2555
|
+
} else if (ext === '.rtf') {
|
|
2556
|
+
raw = raw
|
|
2557
|
+
.replace(/\\par\b|\\line\b/gi, '\n')
|
|
2558
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
2559
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
2560
|
+
const n = parseInt(code, 10);
|
|
2561
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
2562
|
+
})
|
|
2563
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, ' ')
|
|
2564
|
+
.replace(/[{}]/g, ' ')
|
|
2565
|
+
.replace(/[ \t]+/g, ' ');
|
|
2566
|
+
}
|
|
2514
2567
|
const lines = raw.split(/\r?\n/);
|
|
2515
2568
|
const capped = lines.slice(0, maxLines).join('\n');
|
|
2516
2569
|
const wasTruncated = lines.length > maxLines;
|
|
@@ -2671,6 +2724,10 @@ export default {
|
|
|
2671
2724
|
buildCisoAuditTelemetry,
|
|
2672
2725
|
formatAuditReceipt,
|
|
2673
2726
|
generateAuditReport,
|
|
2727
|
+
PSR2_VERSION,
|
|
2728
|
+
PSR2_STATEMENTS,
|
|
2729
|
+
buildPsr2Receipt,
|
|
2730
|
+
psr2Lines,
|
|
2674
2731
|
formatCefEvent,
|
|
2675
2732
|
formatSyslogEvent,
|
|
2676
2733
|
formatJsonlEvent,
|