@spool-lab/redact 0.0.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/LICENSE +32 -0
- package/dist/detectors.d.ts +8 -0
- package/dist/detectors.d.ts.map +1 -0
- package/dist/detectors.js +205 -0
- package/dist/detectors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/mask.d.ts +6 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +184 -0
- package/dist/mask.js.map +1 -0
- package/dist/providers.d.ts +22 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +74 -0
- package/dist/providers.js.map +1 -0
- package/dist/severity.d.ts +13 -0
- package/dist/severity.d.ts.map +1 -0
- package/dist/severity.js +51 -0
- package/dist/severity.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +64 -0
- package/dist/types.js.map +1 -0
- package/dist/validators.d.ts +25 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +78 -0
- package/dist/validators.js.map +1 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Spool Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
TRADEMARK NOTICE
|
|
26
|
+
|
|
27
|
+
"Spool" and the Spool logo are trademarks of TypeSafe Limited. The MIT
|
|
28
|
+
License above governs the source code only; it does not grant any right or
|
|
29
|
+
license to use the "Spool" name, the Spool logo, or any other TypeSafe
|
|
30
|
+
Limited trademark. Forks and redistributions must not use the "Spool" name
|
|
31
|
+
or logo in a way that suggests endorsement by, or affiliation with,
|
|
32
|
+
TypeSafe Limited.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SensitiveMatch } from './types.js';
|
|
2
|
+
/** Synchronous regex-driven scan. Rules earlier in the priority list
|
|
3
|
+
* claim their regions first; later, broader rules whose match
|
|
4
|
+
* overlaps a claimed region are dropped. This is what makes a JWT
|
|
5
|
+
* inside a `Bearer` header surface as a JWT, and a Stripe key inside
|
|
6
|
+
* `STRIPE_SECRET_KEY=…` surface as the whole assignment. */
|
|
7
|
+
export declare function detectWithRegex(text: string, providerName?: string): SensitiveMatch[];
|
|
8
|
+
//# sourceMappingURL=detectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectors.d.ts","sourceRoot":"","sources":["../src/detectors.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,YAAY,CAAA;AAoL/D;;;;6DAI6D;AAC7D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,SAAU,GAAG,cAAc,EAAE,CAsCtF"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Regex detection pipeline for @spool-lab/redact.
|
|
2
|
+
//
|
|
3
|
+
// Primary scenario: Spool sessions captured from local coding agents
|
|
4
|
+
// (Claude Code, codex, gemini) — terminal output, tool results, files
|
|
5
|
+
// the agent read, error logs. The patterns favour structured leaks
|
|
6
|
+
// that *appear in this kind of content*: a stray `cat ~/.aws/creds`
|
|
7
|
+
// line, `gh auth status` dumping a token, a `kubectl config view`
|
|
8
|
+
// pasted into the conversation, a `psql "postgresql://…"` invocation.
|
|
9
|
+
//
|
|
10
|
+
// Rule ordering matters. Wider patterns whose prefix LEAKS context
|
|
11
|
+
// (env-var name reveals the vendor; URL host + creds leak the host;
|
|
12
|
+
// connection-string scheme reveals the database type) run before
|
|
13
|
+
// bare vendor api-keys so the whole assignment is masked, not just
|
|
14
|
+
// the secret value. JWT runs before `Bearer` so a JWT-shaped bearer
|
|
15
|
+
// surfaces as a JWT rather than a generic bearer wrapping a JWT.
|
|
16
|
+
import { hasQuotedEntropy, luhnOk } from './validators.js';
|
|
17
|
+
// ── Credential blocks (multi-line, highest specificity) ───────────
|
|
18
|
+
// PEM-armoured key: header + base64 body + footer. Match runs to the
|
|
19
|
+
// END footer (non-greedy across newlines) so the whole block — armor
|
|
20
|
+
// headers and all — is masked as a single unit.
|
|
21
|
+
const PEM_RX = /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |PGP |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |PGP |)PRIVATE KEY-----/g;
|
|
22
|
+
// Raw OpenSSH private key body when a user pasted only the body
|
|
23
|
+
// without the BEGIN/END armour. Conservative: requires the canonical
|
|
24
|
+
// `b3BlbnNzaC1rZXk` magic prefix (base64 of "openssh-key") to keep
|
|
25
|
+
// the FP rate near zero.
|
|
26
|
+
const SSH_KEY_BODY_RX = /b3BlbnNzaC1rZXk[A-Za-z0-9+/=\n\r]{60,}/g;
|
|
27
|
+
// ~/.aws/credentials INI block: starts at a `[profile]` section
|
|
28
|
+
// header that immediately precedes one of the canonical AWS keys,
|
|
29
|
+
// runs through whatever AWS settings follow until a blank line.
|
|
30
|
+
// Also catches `aws_access_key_id = …` lines outside an INI section
|
|
31
|
+
// (one-off pasted line from `aws configure`).
|
|
32
|
+
const AWS_INI_RX = /(?:\[[^\]\n]+\]\s*\n)?(?:aws_(?:access_key_id|secret_access_key|session_token)\s*=\s*\S+\s*\n?){1,4}/g;
|
|
33
|
+
// gcloud INI line. `gcloud auth print-access-token` output is just a
|
|
34
|
+
// bare `ya29.…` token — covered by the api-key vendor list. The INI
|
|
35
|
+
// rule here catches `~/.config/gcloud/application_default_credentials.json`
|
|
36
|
+
// JSON paste where keys leak as fielded values.
|
|
37
|
+
const GCLOUD_JSON_RX = /"(?:refresh_token|client_secret|access_token)"\s*:\s*"[A-Za-z0-9_\-./]{20,}"/g;
|
|
38
|
+
// kubeconfig token / cert-data fields. Either YAML or JSON shape.
|
|
39
|
+
// Catches `token:`, `client-certificate-data:`, `client-key-data:`,
|
|
40
|
+
// `id-token:`, `refresh-token:`. The value is base64 PEM or an
|
|
41
|
+
// opaque bearer — either way it must not ship.
|
|
42
|
+
const KUBECONFIG_RX = /\b(?:token|client-certificate-data|client-key-data|certificate-authority-data|id-token|refresh-token)\s*:\s*[A-Za-z0-9+/=_\-.]{20,}\b/g;
|
|
43
|
+
// .netrc machine/login/password line — single-line shape since that's
|
|
44
|
+
// how it's usually pasted from a terminal.
|
|
45
|
+
const NETRC_RX = /\bmachine\s+\S+\s+login\s+\S+\s+password\s+\S+/g;
|
|
46
|
+
// Database connection strings — scheme-aware, capture the whole URI
|
|
47
|
+
// so the host/db/user/password are all masked.
|
|
48
|
+
const CONN_STRING_RX = /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis(?:s)?|amqps?|mssql|sqlserver|jdbc:[a-z]+|cassandra|clickhouse|kafka|nats):\/\/[^\s"'`<>]+/gi;
|
|
49
|
+
// ── Single-token credentials ─────────────────────────────────────
|
|
50
|
+
const JWT_RX = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
51
|
+
// Vendor-prefixed credentials. Order each alt so the most specific
|
|
52
|
+
// pattern is unambiguous — `sk-ant-` before `sk-` (with a lookahead
|
|
53
|
+
// on the latter to avoid double-matching Anthropic keys as OpenAI).
|
|
54
|
+
const VENDOR_API_KEY_RX = new RegExp('\\b(?:' + [
|
|
55
|
+
// Payment / SaaS
|
|
56
|
+
'sk_(?:live|test)_[A-Za-z0-9]{16,}',
|
|
57
|
+
'rk_(?:live|test)_[A-Za-z0-9]{16,}',
|
|
58
|
+
'pk_(?:live|test)_[A-Za-z0-9]{16,}',
|
|
59
|
+
// AI vendors
|
|
60
|
+
'sk-ant-[A-Za-z0-9_-]{32,}',
|
|
61
|
+
'sk-proj-[A-Za-z0-9_-]{32,}',
|
|
62
|
+
'sk-(?!ant-|proj-)[A-Za-z0-9]{32,}',
|
|
63
|
+
'hf_[A-Za-z0-9]{30,}',
|
|
64
|
+
// Source forges
|
|
65
|
+
'gh[pousr]_[A-Za-z0-9]{36}',
|
|
66
|
+
'glpat-[A-Za-z0-9_-]{20}',
|
|
67
|
+
// Cloud providers
|
|
68
|
+
'AKIA[0-9A-Z]{16}',
|
|
69
|
+
'ASIA[0-9A-Z]{16}',
|
|
70
|
+
'AIza[0-9A-Za-z_-]{35}',
|
|
71
|
+
'ya29\\.[A-Za-z0-9_-]{40,}',
|
|
72
|
+
'AccDB[A-Za-z0-9+/=]{40,}',
|
|
73
|
+
'dop_v1_[A-Fa-f0-9]{64}',
|
|
74
|
+
'vc_[A-Za-z0-9]{24,}',
|
|
75
|
+
'vercel_[A-Za-z0-9]{24,}',
|
|
76
|
+
// CDN / PaaS
|
|
77
|
+
'CFPAT-[A-Za-z0-9_-]{40,}',
|
|
78
|
+
'dckr_pat_[A-Za-z0-9_-]{27,}',
|
|
79
|
+
// Comms / mail
|
|
80
|
+
'xox[abprs]-[A-Za-z0-9-]{10,}',
|
|
81
|
+
'xapp-[A-Za-z0-9-]{10,}',
|
|
82
|
+
'SG\\.[A-Za-z0-9_-]{20,24}\\.[A-Za-z0-9_-]{39,50}',
|
|
83
|
+
'key-[a-f0-9]{32}',
|
|
84
|
+
'AC[a-f0-9]{32}',
|
|
85
|
+
'SK[a-f0-9]{32}',
|
|
86
|
+
'sq0csp-[A-Za-z0-9_-]{43}',
|
|
87
|
+
// Package managers
|
|
88
|
+
'npm_[A-Za-z0-9]{36}',
|
|
89
|
+
'pypi-AgEIc[A-Za-z0-9_-]{50,}',
|
|
90
|
+
// Data platforms
|
|
91
|
+
'dapi[a-f0-9]{32}',
|
|
92
|
+
// Telemetry / monitoring
|
|
93
|
+
'datadog_api_key_[a-f0-9]{32}',
|
|
94
|
+
].map((p) => `(?:${p})`).join('|') + ')\\b', 'g');
|
|
95
|
+
const BEARER_RX = /\b[Bb]earer\s+[A-Za-z0-9_\-.=+/]{16,}\b/g;
|
|
96
|
+
const BASIC_AUTH_RX = /\b[Bb]asic\s+[A-Za-z0-9+/=]{12,}={0,2}\b/g;
|
|
97
|
+
const URL_CREDS_RX = /\b[a-z][a-z0-9+\-.]*:\/\/[^\s/@:"'`<>]+:[^\s/@:"'`<>]+@[A-Za-z0-9.\-]+(?::\d+)?(?:\/[^\s"'`<>]*)?/g;
|
|
98
|
+
// NAME=VALUE assignment with a credential-shaped suffix. Captures
|
|
99
|
+
// the whole assignment so the name (which itself leaks vendor
|
|
100
|
+
// intent) is masked alongside the value.
|
|
101
|
+
const ENV_VAR_RX = /\b[A-Z][A-Z0-9_]{2,}(?:_KEY|_SECRET|_TOKEN|_PASSWORD|_PASSWD|_PWD|_API_?KEY|_DSN|_URL)\s*=\s*\S+/g;
|
|
102
|
+
const GENERIC_SECRET_RX = /\b(?:api[_-]?key|secret|token|password|passwd|auth|access[_-]?key|client[_-]?secret|webhook)\b\s*[:=]\s*["']([A-Za-z0-9+/=_\-]{20,})["']/gi;
|
|
103
|
+
// ── Identity ─────────────────────────────────────────────────────
|
|
104
|
+
const CC_RX = /\b(?:4\d{3}|5[1-5]\d{2}|6(?:011|5\d{2})|3[47]\d{2}|3(?:0[0-5]|[68]\d)\d|(?:2131|1800|35\d{3}))[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{3,4}\b/g;
|
|
105
|
+
const SSN_RX = /\b(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b/g;
|
|
106
|
+
const EMAIL_RX = /\b[A-Za-z0-9](?:[A-Za-z0-9._+-]*[A-Za-z0-9])?@[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}\b/g;
|
|
107
|
+
const PHONE_RX = /(?:\+\d[\d .\-()]{7,16}\d|\(\d{2,4}\)[\d .\-]{6,14}\d)/g;
|
|
108
|
+
const IPV4_RX = /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\b/g;
|
|
109
|
+
// Permissive match; `validIPv6` below rejects look-alikes (timestamps,
|
|
110
|
+
// short colon-separated number strings). Real IPv6 either uses the
|
|
111
|
+
// compressed `::` marker or contains the full 8 groups.
|
|
112
|
+
const IPV6_RX = /\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:|\b(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\b/g;
|
|
113
|
+
/** Distinguishes real IPv6 addresses from look-alikes the broad
|
|
114
|
+
* regex above also accepts. Timestamps like `12:22:57` and other
|
|
115
|
+
* short colon-separated decimal sequences must be rejected:
|
|
116
|
+
*
|
|
117
|
+
* - A canonical IPv6 has 8 groups (`2001:0db8:…:7334`).
|
|
118
|
+
* - The `::` compression marker is unambiguous when present.
|
|
119
|
+
*
|
|
120
|
+
* Anything else (e.g. 3-group decimal time-of-day) we drop. */
|
|
121
|
+
function validIPv6(value) {
|
|
122
|
+
if (value.includes('::'))
|
|
123
|
+
return true;
|
|
124
|
+
return value.split(':').length === 8;
|
|
125
|
+
}
|
|
126
|
+
// ── Location & infra ─────────────────────────────────────────────
|
|
127
|
+
const ABSOLUTE_PATH_RX = /(?:\/Users\/|\/home\/|\/var\/|\/etc\/|\/opt\/|[A-Z]:\\Users\\)[A-Za-z0-9._\-/\\À-]+/g;
|
|
128
|
+
// Internal-only hostnames that aren't routable on the public DNS
|
|
129
|
+
// but reveal an org's network shape.
|
|
130
|
+
//
|
|
131
|
+
// Note `.local` is intentionally OMITTED: while it's the mDNS TLD,
|
|
132
|
+
// it's also the dominant filename suffix (`.env.local`,
|
|
133
|
+
// `settings.local`, `next.config.local`, …) — and we saw ~50%
|
|
134
|
+
// false-positive rate from a real-world scan with `.local` enabled.
|
|
135
|
+
// The remaining TLDs (`.internal`, `.corp`, `.lan`, `.intra`,
|
|
136
|
+
// `.home`, `.prod.*`, `.stg.*`) are unambiguous internal-DNS markers.
|
|
137
|
+
const INTERNAL_HOST_RX = /\b[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.(?:internal|corp|lan|intra|home|prod\.[a-z0-9]+|stg\.[a-z0-9]+)\b/gi;
|
|
138
|
+
const RULES = [
|
|
139
|
+
{ kind: 'private-key', rx: PEM_RX, confidence: 1.0 },
|
|
140
|
+
{ kind: 'ssh-key', rx: SSH_KEY_BODY_RX, confidence: 0.95 },
|
|
141
|
+
{ kind: 'cloud-cred-ini', rx: AWS_INI_RX, confidence: 0.95 },
|
|
142
|
+
{ kind: 'cloud-cred-ini', rx: GCLOUD_JSON_RX, confidence: 0.9 },
|
|
143
|
+
{ kind: 'kubeconfig-token', rx: KUBECONFIG_RX, confidence: 0.85 },
|
|
144
|
+
{ kind: 'netrc', rx: NETRC_RX, confidence: 0.95 },
|
|
145
|
+
{ kind: 'connection-string', rx: CONN_STRING_RX, confidence: 0.9 },
|
|
146
|
+
{ kind: 'url-creds', rx: URL_CREDS_RX, confidence: 0.95 },
|
|
147
|
+
{ kind: 'basic-auth', rx: BASIC_AUTH_RX, confidence: 0.9 },
|
|
148
|
+
{ kind: 'env-var', rx: ENV_VAR_RX, confidence: 0.9 },
|
|
149
|
+
{ kind: 'generic-secret', rx: GENERIC_SECRET_RX, confidence: 0.6, validate: hasQuotedEntropy(4.0) },
|
|
150
|
+
{ kind: 'jwt', rx: JWT_RX, confidence: 0.95 },
|
|
151
|
+
{ kind: 'api-key', rx: VENDOR_API_KEY_RX, confidence: 0.98 },
|
|
152
|
+
{ kind: 'bearer', rx: BEARER_RX, confidence: 0.85 },
|
|
153
|
+
{ kind: 'credit-card', rx: CC_RX, confidence: 0.95, validate: luhnOk },
|
|
154
|
+
{ kind: 'ssn', rx: SSN_RX, confidence: 0.85 },
|
|
155
|
+
{ kind: 'email', rx: EMAIL_RX, confidence: 0.85 },
|
|
156
|
+
{ kind: 'phone', rx: PHONE_RX, confidence: 0.7 },
|
|
157
|
+
{ kind: 'ip', rx: IPV4_RX, confidence: 0.55 },
|
|
158
|
+
{ kind: 'ip', rx: IPV6_RX, confidence: 0.6, validate: validIPv6 },
|
|
159
|
+
{ kind: 'internal-host', rx: INTERNAL_HOST_RX, confidence: 0.55 },
|
|
160
|
+
{ kind: 'absolute-path', rx: ABSOLUTE_PATH_RX, confidence: 0.75 },
|
|
161
|
+
];
|
|
162
|
+
/** Synchronous regex-driven scan. Rules earlier in the priority list
|
|
163
|
+
* claim their regions first; later, broader rules whose match
|
|
164
|
+
* overlaps a claimed region are dropped. This is what makes a JWT
|
|
165
|
+
* inside a `Bearer` header surface as a JWT, and a Stripe key inside
|
|
166
|
+
* `STRIPE_SECRET_KEY=…` surface as the whole assignment. */
|
|
167
|
+
export function detectWithRegex(text, providerName = 'regex') {
|
|
168
|
+
const matches = [];
|
|
169
|
+
const claimed = [];
|
|
170
|
+
const overlaps = (s, e) => claimed.some((c) => s < c.end && e > c.start);
|
|
171
|
+
for (const rule of RULES) {
|
|
172
|
+
rule.rx.lastIndex = 0;
|
|
173
|
+
let m;
|
|
174
|
+
while ((m = rule.rx.exec(text)) !== null) {
|
|
175
|
+
const value = m[0];
|
|
176
|
+
const start = m.index;
|
|
177
|
+
const end = start + value.length;
|
|
178
|
+
const advance = () => {
|
|
179
|
+
if (m && m.index === rule.rx.lastIndex)
|
|
180
|
+
rule.rx.lastIndex++;
|
|
181
|
+
};
|
|
182
|
+
if (rule.validate && !rule.validate(value)) {
|
|
183
|
+
advance();
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (overlaps(start, end)) {
|
|
187
|
+
advance();
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
matches.push({
|
|
191
|
+
kind: rule.kind,
|
|
192
|
+
value,
|
|
193
|
+
start,
|
|
194
|
+
end,
|
|
195
|
+
confidence: rule.confidence,
|
|
196
|
+
provider: providerName,
|
|
197
|
+
});
|
|
198
|
+
claimed.push({ start, end });
|
|
199
|
+
advance();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
matches.sort((a, b) => a.start - b.start);
|
|
203
|
+
return matches;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=detectors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectors.js","sourceRoot":"","sources":["../src/detectors.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type { SensitiveKind, SensitiveMatch, SensitiveGroup, SensitiveValue, } from './types.js';
|
|
2
|
+
export { SENSITIVE_KIND_LABEL, SENSITIVE_KIND_ORDER, } from './types.js';
|
|
3
|
+
export { detectWithRegex } from './detectors.js';
|
|
4
|
+
export { regexProvider, analyzeWith, } from './providers.js';
|
|
5
|
+
export type { RedactProvider } from './providers.js';
|
|
6
|
+
export { luhnOk, shannon, hashValueForRedactExclude } from './validators.js';
|
|
7
|
+
export { maskValueByKind } from './mask.js';
|
|
8
|
+
export { HIGH_SEVERITY_KINDS, INFO_SEVERITY_KINDS, severityOf } from './severity.js';
|
|
9
|
+
export type { Severity } from './severity.js';
|
|
10
|
+
import type { SensitiveGroup, SensitiveMatch } from './types.js';
|
|
11
|
+
/** Convenience wrapper for the common "scan one string" call. Equivalent
|
|
12
|
+
* to `regexProvider.analyze(text)` minus the Promise — synchronous so
|
|
13
|
+
* it's safe to use during React render. */
|
|
14
|
+
export declare function detectSensitiveSpans(text: string): SensitiveMatch[];
|
|
15
|
+
/** Group matches by kind, deduplicating identical literals so the
|
|
16
|
+
* editor's expanded list shows one row per decision (not one row
|
|
17
|
+
* per occurrence). `group.count` keeps the total occurrence sum so
|
|
18
|
+
* the header `×N` still reflects how often a value appears; each
|
|
19
|
+
* per-value entry carries its own occurrence count for callers that
|
|
20
|
+
* want to surface duplicates explicitly. */
|
|
21
|
+
export declare function groupBySensitiveKind(matches: SensitiveMatch[]): SensitiveGroup[];
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,YAAY,EACV,aAAa,EACb,cAAc,EACd,cAAc,EACd,cAAc,GACf,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD,OAAO,EACL,aAAa,EACb,WAAW,GACZ,MAAM,gBAAgB,CAAA;AAEvB,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAA;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE3C,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAG7C,OAAO,KAAK,EAAE,cAAc,EAAiB,cAAc,EAAE,MAAM,YAAY,CAAA;AAG/E;;4CAE4C;AAC5C,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CAEnE;AAED;;;;;6CAK6C;AAC7C,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CA6BhF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @spool-lab/redact — sensitive-data detection for Spool sessions.
|
|
2
|
+
//
|
|
3
|
+
// Surfaces:
|
|
4
|
+
// • Share editor (pre-publish review of artifact content)
|
|
5
|
+
// • Security Scan (planned) — background sweep of every local
|
|
6
|
+
// `.spool` session, surfaces token/credential leaks in a report
|
|
7
|
+
// • CLI `spool doctor` (planned) — same pipeline, headless
|
|
8
|
+
//
|
|
9
|
+
// Everything here runs locally, with no network access of any kind.
|
|
10
|
+
// See `providers.ts` for the pluggable boundary.
|
|
11
|
+
export { SENSITIVE_KIND_LABEL, SENSITIVE_KIND_ORDER, } from './types.js';
|
|
12
|
+
export { detectWithRegex } from './detectors.js';
|
|
13
|
+
export { regexProvider, analyzeWith, } from './providers.js';
|
|
14
|
+
export { luhnOk, shannon, hashValueForRedactExclude } from './validators.js';
|
|
15
|
+
export { maskValueByKind } from './mask.js';
|
|
16
|
+
export { HIGH_SEVERITY_KINDS, INFO_SEVERITY_KINDS, severityOf } from './severity.js';
|
|
17
|
+
import { detectWithRegex } from './detectors.js';
|
|
18
|
+
import { SENSITIVE_KIND_ORDER } from './types.js';
|
|
19
|
+
/** Convenience wrapper for the common "scan one string" call. Equivalent
|
|
20
|
+
* to `regexProvider.analyze(text)` minus the Promise — synchronous so
|
|
21
|
+
* it's safe to use during React render. */
|
|
22
|
+
export function detectSensitiveSpans(text) {
|
|
23
|
+
return detectWithRegex(text);
|
|
24
|
+
}
|
|
25
|
+
/** Group matches by kind, deduplicating identical literals so the
|
|
26
|
+
* editor's expanded list shows one row per decision (not one row
|
|
27
|
+
* per occurrence). `group.count` keeps the total occurrence sum so
|
|
28
|
+
* the header `×N` still reflects how often a value appears; each
|
|
29
|
+
* per-value entry carries its own occurrence count for callers that
|
|
30
|
+
* want to surface duplicates explicitly. */
|
|
31
|
+
export function groupBySensitiveKind(matches) {
|
|
32
|
+
const byKind = new Map();
|
|
33
|
+
for (const m of matches) {
|
|
34
|
+
const list = byKind.get(m.kind) ?? [];
|
|
35
|
+
list.push(m);
|
|
36
|
+
byKind.set(m.kind, list);
|
|
37
|
+
}
|
|
38
|
+
return Array.from(byKind.entries())
|
|
39
|
+
.map(([kind, list]) => {
|
|
40
|
+
// Preserve first-seen order while counting duplicates.
|
|
41
|
+
const order = [];
|
|
42
|
+
const counts = new Map();
|
|
43
|
+
for (const m of list) {
|
|
44
|
+
const n = counts.get(m.value);
|
|
45
|
+
if (n === undefined) {
|
|
46
|
+
order.push(m.value);
|
|
47
|
+
counts.set(m.value, 1);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
counts.set(m.value, n + 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
kind,
|
|
55
|
+
count: list.length,
|
|
56
|
+
values: order.map((v) => ({ value: v, count: counts.get(v) })),
|
|
57
|
+
minConfidence: list.reduce((acc, m) => Math.min(acc, m.confidence), 1),
|
|
58
|
+
};
|
|
59
|
+
})
|
|
60
|
+
.sort((a, b) => SENSITIVE_KIND_ORDER.indexOf(a.kind) - SENSITIVE_KIND_ORDER.indexOf(b.kind));
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/mask.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SensitiveKind } from './types.js';
|
|
2
|
+
/** Compute the per-kind replacement string for a detected match.
|
|
3
|
+
* `kind` is loosely typed so synthetic categories (author names,
|
|
4
|
+
* manual entries) also get a sensible default. */
|
|
5
|
+
export declare function maskValueByKind(value: string, kind: SensitiveKind | string): string;
|
|
6
|
+
//# sourceMappingURL=mask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C;;mDAEmD;AACnD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,MAAM,CA0GnF"}
|
package/dist/mask.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Per-kind redaction masks.
|
|
2
|
+
//
|
|
3
|
+
// The naive substitution — every match → `[redacted]` — works but
|
|
4
|
+
// throws away too much context. A reader of the published artifact
|
|
5
|
+
// sees a wall of identical placeholders with no idea which were
|
|
6
|
+
// credentials, which were emails, which were paths. Per-kind masks
|
|
7
|
+
// keep just enough structure for the artifact to remain readable:
|
|
8
|
+
//
|
|
9
|
+
// STRIPE_SECRET_KEY=sk_live_abc… → STRIPE_SECRET_KEY=[redacted]
|
|
10
|
+
// maya@example.com → m***@example.com
|
|
11
|
+
// 4111 1111 1111 1111 → **** **** **** 1111
|
|
12
|
+
// postgres://u:p@host/db → postgres://[redacted]
|
|
13
|
+
// ghp_abcdef… → [redacted: GitHub key]
|
|
14
|
+
//
|
|
15
|
+
// Trade-off: a mask that retains structure (scheme, domain, last-4,
|
|
16
|
+
// vendor name) leaks slightly more than `[redacted]`. The judgement
|
|
17
|
+
// call is that those fragments are NOT secrets themselves — the
|
|
18
|
+
// scheme of a connection string is public, the domain of an email
|
|
19
|
+
// is usually public, vendor token prefixes are documented. What's
|
|
20
|
+
// secret is the random tail.
|
|
21
|
+
/** Compute the per-kind replacement string for a detected match.
|
|
22
|
+
* `kind` is loosely typed so synthetic categories (author names,
|
|
23
|
+
* manual entries) also get a sensible default. */
|
|
24
|
+
export function maskValueByKind(value, kind) {
|
|
25
|
+
switch (kind) {
|
|
26
|
+
case 'private-key':
|
|
27
|
+
return `[redacted private key · ${formatBytes(value.length)}]`;
|
|
28
|
+
case 'ssh-key':
|
|
29
|
+
return `[redacted SSH key · ${formatBytes(value.length)}]`;
|
|
30
|
+
case 'api-key': {
|
|
31
|
+
const vendor = detectVendor(value);
|
|
32
|
+
return vendor ? `[redacted: ${vendor} key]` : '[redacted API key]';
|
|
33
|
+
}
|
|
34
|
+
case 'jwt':
|
|
35
|
+
return '[redacted JWT]';
|
|
36
|
+
case 'cloud-cred-ini': {
|
|
37
|
+
if (/aws_(access|secret|session)_/i.test(value))
|
|
38
|
+
return '[redacted AWS credentials]';
|
|
39
|
+
if (/refresh_token|client_secret|access_token/i.test(value))
|
|
40
|
+
return '[redacted Google credentials]';
|
|
41
|
+
return '[redacted cloud credentials]';
|
|
42
|
+
}
|
|
43
|
+
case 'kubeconfig-token': {
|
|
44
|
+
const m = value.match(/^([a-z-]+)\s*:/i);
|
|
45
|
+
return m ? `${m[1]}: [redacted]` : '[redacted kubeconfig token]';
|
|
46
|
+
}
|
|
47
|
+
case 'netrc':
|
|
48
|
+
return 'machine [redacted] login [redacted] password [redacted]';
|
|
49
|
+
case 'connection-string': {
|
|
50
|
+
const m = value.match(/^([a-z][a-z0-9+\-.]*):\/\//i);
|
|
51
|
+
return m ? `${m[1]}://[redacted]` : '[redacted connection string]';
|
|
52
|
+
}
|
|
53
|
+
case 'url-creds': {
|
|
54
|
+
const m = value.match(/^([a-z][a-z0-9+\-.]*):\/\/[^@]+@([^\s/]+)(\/[^\s]*)?$/i);
|
|
55
|
+
return m ? `${m[1]}://[redacted:redacted]@${m[2]}${m[3] ?? ''}` : '[redacted URL]';
|
|
56
|
+
}
|
|
57
|
+
case 'bearer':
|
|
58
|
+
return 'Bearer [redacted]';
|
|
59
|
+
case 'basic-auth':
|
|
60
|
+
return 'Basic [redacted]';
|
|
61
|
+
case 'env-var': {
|
|
62
|
+
// NAME=VALUE — preserve NAME, mask VALUE. The user has already
|
|
63
|
+
// consented to redact by toggling, so the assignment NAME is
|
|
64
|
+
// surfaced to keep the line readable.
|
|
65
|
+
const m = value.match(/^([A-Z][A-Z0-9_]*)\s*=/);
|
|
66
|
+
return m ? `${m[1]}=[redacted]` : '[redacted env-var]';
|
|
67
|
+
}
|
|
68
|
+
case 'generic-secret': {
|
|
69
|
+
// The match capture is "keyword : value" or "keyword = value".
|
|
70
|
+
// Preserve the keyword prefix verbatim so the assignment shape
|
|
71
|
+
// survives.
|
|
72
|
+
const m = value.match(/^([^"'\s:=]+\s*[:=]\s*)["']/);
|
|
73
|
+
return m ? `${m[1]}"[redacted]"` : '[redacted secret]';
|
|
74
|
+
}
|
|
75
|
+
case 'credit-card': {
|
|
76
|
+
const digits = value.replace(/\D/g, '');
|
|
77
|
+
const last4 = digits.slice(-4) || '****';
|
|
78
|
+
return `**** **** **** ${last4}`;
|
|
79
|
+
}
|
|
80
|
+
case 'ssn': {
|
|
81
|
+
const last4 = value.slice(-4);
|
|
82
|
+
return `***-**-${last4}`;
|
|
83
|
+
}
|
|
84
|
+
case 'email': {
|
|
85
|
+
const at = value.indexOf('@');
|
|
86
|
+
if (at <= 0)
|
|
87
|
+
return '[redacted email]';
|
|
88
|
+
const first = value[0] ?? '';
|
|
89
|
+
const domain = value.slice(at + 1);
|
|
90
|
+
// Don't reveal more than the first char of the local part. For
|
|
91
|
+
// single-char local parts ("a@x.com"), use a literal asterisk.
|
|
92
|
+
return `${first}***@${domain}`;
|
|
93
|
+
}
|
|
94
|
+
case 'phone':
|
|
95
|
+
return '[redacted phone]';
|
|
96
|
+
case 'person-name':
|
|
97
|
+
return '[redacted name]';
|
|
98
|
+
case 'street-address':
|
|
99
|
+
return '[redacted address]';
|
|
100
|
+
case 'date-of-birth':
|
|
101
|
+
return '[redacted DOB]';
|
|
102
|
+
case 'ip':
|
|
103
|
+
// Preserve shape so readers know it was a network address; the
|
|
104
|
+
// four-octet pattern matters more than the digits.
|
|
105
|
+
return value.includes(':') ? '[redacted IPv6]' : '[redacted IPv4]';
|
|
106
|
+
case 'internal-host': {
|
|
107
|
+
const m = value.match(/\.([a-z0-9-]+(?:\.[a-z0-9-]+)*)$/i);
|
|
108
|
+
return m ? `[redacted].${m[1]}` : '[redacted internal host]';
|
|
109
|
+
}
|
|
110
|
+
case 'absolute-path': {
|
|
111
|
+
const m = value.match(/^(\/Users\/|\/home\/|\/var\/|\/etc\/|\/opt\/|[A-Z]:\\Users\\)/);
|
|
112
|
+
return m ? `${m[1]}[redacted]` : '[redacted path]';
|
|
113
|
+
}
|
|
114
|
+
// Synthetic kinds emitted by the Share editor's PII layer.
|
|
115
|
+
case 'synthetic:author':
|
|
116
|
+
return '[redacted name]';
|
|
117
|
+
case 'synthetic:manual':
|
|
118
|
+
return '[redacted]';
|
|
119
|
+
default:
|
|
120
|
+
return '[redacted]';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Recognise the vendor behind a token by its prefix. Returns null
|
|
124
|
+
* when the prefix isn't one we've codified — the caller falls
|
|
125
|
+
* back to a generic `[redacted API key]` mask. */
|
|
126
|
+
function detectVendor(token) {
|
|
127
|
+
if (/^sk_(live|test)_/.test(token))
|
|
128
|
+
return 'Stripe';
|
|
129
|
+
if (/^(rk|pk)_(live|test)_/.test(token))
|
|
130
|
+
return 'Stripe';
|
|
131
|
+
if (/^sk-ant-/.test(token))
|
|
132
|
+
return 'Anthropic';
|
|
133
|
+
if (/^sk-proj-/.test(token))
|
|
134
|
+
return 'OpenAI';
|
|
135
|
+
if (/^sk-/.test(token))
|
|
136
|
+
return 'OpenAI';
|
|
137
|
+
if (/^hf_/.test(token))
|
|
138
|
+
return 'Hugging Face';
|
|
139
|
+
if (/^gh[pousr]_/.test(token))
|
|
140
|
+
return 'GitHub';
|
|
141
|
+
if (/^glpat-/.test(token))
|
|
142
|
+
return 'GitLab';
|
|
143
|
+
if (/^(AKIA|ASIA)/.test(token))
|
|
144
|
+
return 'AWS';
|
|
145
|
+
if (/^AIza/.test(token))
|
|
146
|
+
return 'Google';
|
|
147
|
+
if (/^ya29\./.test(token))
|
|
148
|
+
return 'Google';
|
|
149
|
+
if (/^xox[abprs]-|^xapp-/.test(token))
|
|
150
|
+
return 'Slack';
|
|
151
|
+
if (/^SG\./.test(token))
|
|
152
|
+
return 'SendGrid';
|
|
153
|
+
if (/^key-[a-f0-9]/.test(token))
|
|
154
|
+
return 'Mailgun';
|
|
155
|
+
if (/^AC[a-f0-9]/.test(token))
|
|
156
|
+
return 'Twilio';
|
|
157
|
+
if (/^SK[a-f0-9]/.test(token))
|
|
158
|
+
return 'Twilio';
|
|
159
|
+
if (/^sq0csp-/.test(token))
|
|
160
|
+
return 'Square';
|
|
161
|
+
if (/^npm_/.test(token))
|
|
162
|
+
return 'npm';
|
|
163
|
+
if (/^pypi-/.test(token))
|
|
164
|
+
return 'PyPI';
|
|
165
|
+
if (/^dop_v1_/.test(token))
|
|
166
|
+
return 'DigitalOcean';
|
|
167
|
+
if (/^vc_|^vercel_/.test(token))
|
|
168
|
+
return 'Vercel';
|
|
169
|
+
if (/^CFPAT-/.test(token))
|
|
170
|
+
return 'Cloudflare';
|
|
171
|
+
if (/^dckr_pat_/.test(token))
|
|
172
|
+
return 'Docker Hub';
|
|
173
|
+
if (/^dapi/.test(token))
|
|
174
|
+
return 'Databricks';
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function formatBytes(n) {
|
|
178
|
+
if (n < 1024)
|
|
179
|
+
return `${n} B`;
|
|
180
|
+
if (n < 1024 * 1024)
|
|
181
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
182
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=mask.js.map
|
package/dist/mask.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SensitiveMatch } from './types.js';
|
|
2
|
+
export interface RedactProvider {
|
|
3
|
+
/** Stable identifier used in `SensitiveMatch.provider` and shown
|
|
4
|
+
* to the user when explaining where a flag came from. */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** Human-readable label for settings UI. */
|
|
7
|
+
readonly displayName: string;
|
|
8
|
+
/** True when the provider is loaded and ready to call `analyze`.
|
|
9
|
+
* Always true for regex; future ML providers return false until
|
|
10
|
+
* the model bundle is on disk. */
|
|
11
|
+
available(): boolean;
|
|
12
|
+
/** Run detection on the given text. Even synchronous providers
|
|
13
|
+
* return a Promise so callers can compose providers uniformly. */
|
|
14
|
+
analyze(text: string): Promise<SensitiveMatch[]>;
|
|
15
|
+
}
|
|
16
|
+
/** The default, always-available provider. */
|
|
17
|
+
export declare const regexProvider: RedactProvider;
|
|
18
|
+
/** Merge results from multiple providers and de-overlap by priority.
|
|
19
|
+
* When two providers flag overlapping spans, the higher-priority
|
|
20
|
+
* provider wins. Higher priority = earlier in the array. */
|
|
21
|
+
export declare function analyzeWith(providers: RedactProvider[], text: string): Promise<SensitiveMatch[]>;
|
|
22
|
+
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGhD,MAAM,WAAW,cAAc;IAC7B;8DAC0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B;;uCAEmC;IACnC,SAAS,IAAI,OAAO,CAAA;IACpB;uEACmE;IACnE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;CACjD;AAED,8CAA8C;AAC9C,eAAO,MAAM,aAAa,EAAE,cAK3B,CAAA;AAED;;6DAE6D;AAC7D,wBAAsB,WAAW,CAC/B,SAAS,EAAE,cAAc,EAAE,EAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,cAAc,EAAE,CAAC,CAgB3B"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Pluggable provider boundary for sensitive-data detection.
|
|
2
|
+
//
|
|
3
|
+
// `RedactProvider` is the single seam through which the Share editor,
|
|
4
|
+
// the future Security Scan feature, and the CLI all consume detection.
|
|
5
|
+
// Today the only implementation is `regexProvider` — deterministic,
|
|
6
|
+
// zero-dep, <1ms on a typical session body. Tomorrow another provider
|
|
7
|
+
// (e.g. OpenAI Privacy Filter served via transformers.js + WebGPU)
|
|
8
|
+
// can plug in *without touching call sites*.
|
|
9
|
+
//
|
|
10
|
+
// Local-first invariant: every provider must run entirely on-device.
|
|
11
|
+
// Network round-trips for detection are out of scope by policy —
|
|
12
|
+
// Spool data does not leave the user's machine.
|
|
13
|
+
//
|
|
14
|
+
// Why we are NOT shipping OpenAI Privacy Filter today
|
|
15
|
+
// (Apache-2.0, 1.5B/50M-active, 96% F1 on PII-Masking-300k):
|
|
16
|
+
//
|
|
17
|
+
// 1. The model bundle (Q4-quantised ONNX) is ~800 MB. Downloading
|
|
18
|
+
// that the first time a user opens the Share editor is a poor
|
|
19
|
+
// experience for an interactive surface; it makes more sense
|
|
20
|
+
// behind an explicit "enable enhanced detection" toggle, paired
|
|
21
|
+
// with the planned background Security Scan feature.
|
|
22
|
+
// 2. Privacy Filter's `secret` category is broad but does not
|
|
23
|
+
// reliably recognise the long tail of vendor token formats
|
|
24
|
+
// (`ghp_…`, `sk-ant-…`, `dop_v1_…`). Regex remains the right
|
|
25
|
+
// mechanism for structurally-distinctive credentials.
|
|
26
|
+
// 3. The marginal win is on prose-style PII — sentences like "my
|
|
27
|
+
// SSN is 123-45-6789", names embedded in free text, addresses
|
|
28
|
+
// in error messages — where regex fundamentally can't help.
|
|
29
|
+
// That win matters for the Security Scan report; for the
|
|
30
|
+
// Share editor (where the user is also reviewing the artifact
|
|
31
|
+
// visually) it is much less critical.
|
|
32
|
+
//
|
|
33
|
+
// Concretely, the integration path when we do enable it:
|
|
34
|
+
// • Add `@huggingface/transformers` as an optional dependency
|
|
35
|
+
// • Implement `privacyFilterProvider` in this file (or a sibling)
|
|
36
|
+
// that lazy-imports the package, downloads the ONNX bundle to
|
|
37
|
+
// userData/, and exposes `analyze(text): Promise<SensitiveMatch[]>`
|
|
38
|
+
// emitting matches with `provider: 'privacy-filter'`
|
|
39
|
+
// • Map its 8 entity types (`private_person`, `private_email`,
|
|
40
|
+
// `private_phone`, `private_url`, `private_address`,
|
|
41
|
+
// `private_date`, `account_number`, `secret`) to our existing
|
|
42
|
+
// SensitiveKind values, adding new kinds for what's net-new
|
|
43
|
+
// (`person-name`, `postal-address`, `date-of-birth`)
|
|
44
|
+
// • In the Settings panel, gate behind a toggle showing model
|
|
45
|
+
// size, where the file lives, and how to delete it
|
|
46
|
+
import { detectWithRegex } from './detectors.js';
|
|
47
|
+
/** The default, always-available provider. */
|
|
48
|
+
export const regexProvider = {
|
|
49
|
+
name: 'regex',
|
|
50
|
+
displayName: 'Pattern matcher (built-in)',
|
|
51
|
+
available: () => true,
|
|
52
|
+
analyze: async (text) => detectWithRegex(text, 'regex'),
|
|
53
|
+
};
|
|
54
|
+
/** Merge results from multiple providers and de-overlap by priority.
|
|
55
|
+
* When two providers flag overlapping spans, the higher-priority
|
|
56
|
+
* provider wins. Higher priority = earlier in the array. */
|
|
57
|
+
export async function analyzeWith(providers, text) {
|
|
58
|
+
if (providers.length === 0)
|
|
59
|
+
return [];
|
|
60
|
+
const lists = await Promise.all(providers.map((p) => (p.available() ? p.analyze(text) : Promise.resolve([]))));
|
|
61
|
+
const claimed = [];
|
|
62
|
+
const out = [];
|
|
63
|
+
// Priority = order in `providers`; lower index wins.
|
|
64
|
+
for (const matches of lists) {
|
|
65
|
+
for (const m of matches) {
|
|
66
|
+
if (claimed.some((c) => m.start < c.end && m.end > c.start))
|
|
67
|
+
continue;
|
|
68
|
+
out.push(m);
|
|
69
|
+
claimed.push({ start: m.start, end: m.end });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out.sort((a, b) => a.start - b.start);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=providers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,sEAAsE;AACtE,uEAAuE;AACvE,oEAAoE;AACpE,sEAAsE;AACtE,mEAAmE;AACnE,6CAA6C;AAC7C,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,gDAAgD;AAChD,EAAE;AACF,sDAAsD;AACtD,6DAA6D;AAC7D,EAAE;AACF,oEAAoE;AACpE,mEAAmE;AACnE,kEAAkE;AAClE,qEAAqE;AACrE,0DAA0D;AAC1D,gEAAgE;AAChE,gEAAgE;AAChE,kEAAkE;AAClE,2DAA2D;AAC3D,mEAAmE;AACnE,mEAAmE;AACnE,iEAAiE;AACjE,8DAA8D;AAC9D,mEAAmE;AACnE,2CAA2C;AAC3C,EAAE;AACF,yDAAyD;AACzD,gEAAgE;AAChE,oEAAoE;AACpE,kEAAkE;AAClE,wEAAwE;AACxE,yDAAyD;AACzD,iEAAiE;AACjE,yDAAyD;AACzD,kEAAkE;AAClE,gEAAgE;AAChE,yDAAyD;AACzD,gEAAgE;AAChE,uDAAuD;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAiBhD,8CAA8C;AAC9C,MAAM,CAAC,MAAM,aAAa,GAAmB;IAC3C,IAAI,EAAE,OAAO;IACb,WAAW,EAAE,4BAA4B;IACzC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI;IACrB,OAAO,EAAE,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC;CAChE,CAAA;AAED;;6DAE6D;AAC7D,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,SAA2B,EAC3B,IAAY;IAEZ,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACrC,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAsB,CAAC,CAAC,CAAC,CAClG,CAAA;IACD,MAAM,OAAO,GAAqC,EAAE,CAAA;IACpD,MAAM,GAAG,GAAqB,EAAE,CAAA;IAChC,qDAAqD;IACrD,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC;gBAAE,SAAQ;YACrE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACX,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;AAC9C,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SensitiveKind } from './types.js';
|
|
2
|
+
export declare const HIGH_SEVERITY_KINDS: ReadonlySet<SensitiveKind>;
|
|
3
|
+
/** Pattern hits that are useful as signal but are almost never an
|
|
4
|
+
* actual sensitive-data leak. Real-world audit on a 317-session
|
|
5
|
+
* archive showed ~100% noise from these kinds (file paths flagged
|
|
6
|
+
* every cwd reference, timestamps misclassified as IPv6, mDNS
|
|
7
|
+
* hostnames overlapping with `.env.local` filenames). Keeping them
|
|
8
|
+
* off the default Security view is the only way the surface stays
|
|
9
|
+
* signal-dense. */
|
|
10
|
+
export declare const INFO_SEVERITY_KINDS: ReadonlySet<SensitiveKind>;
|
|
11
|
+
export type Severity = 'high' | 'low' | 'info';
|
|
12
|
+
export declare function severityOf(kind: SensitiveKind): Severity;
|
|
13
|
+
//# sourceMappingURL=severity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"severity.d.ts","sourceRoot":"","sources":["../src/severity.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,aAAa,CAczD,CAAA;AAEF;;;;;;oBAMoB;AACpB,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,aAAa,CAIzD,CAAA;AAEF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;AAE9C,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,CAIxD"}
|
package/dist/severity.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Severity tiers for the Security Scan surfaces.
|
|
2
|
+
//
|
|
3
|
+
// high — a single leak is potentially catastrophic. Credentials,
|
|
4
|
+
// cred files, structured tokens.
|
|
5
|
+
// low — annoying but recoverable. Identity-level (email / phone /
|
|
6
|
+
// person-name / address / DOB / credit-card / SSN).
|
|
7
|
+
// info — signal-only, not a credential leak. File paths, IP
|
|
8
|
+
// addresses, internal-DNS hostnames. Audit-positive to
|
|
9
|
+
// collect, but heavy false-positive rate when surfaced as
|
|
10
|
+
// "findings" — the Security page hides this tier by default
|
|
11
|
+
// and offers an opt-in toggle.
|
|
12
|
+
//
|
|
13
|
+
// The Library badge colour, the Security page Risk-panel grouping,
|
|
14
|
+
// and the inline-confirm policy for Dismiss vs Purge all derive from
|
|
15
|
+
// this three-way split. `scan_high_count` only counts `high`; the
|
|
16
|
+
// session badge only fires when a finding is `high` or `low`.
|
|
17
|
+
export const HIGH_SEVERITY_KINDS = new Set([
|
|
18
|
+
'private-key',
|
|
19
|
+
'ssh-key',
|
|
20
|
+
'cloud-cred-ini',
|
|
21
|
+
'kubeconfig-token',
|
|
22
|
+
'netrc',
|
|
23
|
+
'connection-string',
|
|
24
|
+
'url-creds',
|
|
25
|
+
'api-key',
|
|
26
|
+
'jwt',
|
|
27
|
+
'bearer',
|
|
28
|
+
'basic-auth',
|
|
29
|
+
'env-var',
|
|
30
|
+
'generic-secret',
|
|
31
|
+
]);
|
|
32
|
+
/** Pattern hits that are useful as signal but are almost never an
|
|
33
|
+
* actual sensitive-data leak. Real-world audit on a 317-session
|
|
34
|
+
* archive showed ~100% noise from these kinds (file paths flagged
|
|
35
|
+
* every cwd reference, timestamps misclassified as IPv6, mDNS
|
|
36
|
+
* hostnames overlapping with `.env.local` filenames). Keeping them
|
|
37
|
+
* off the default Security view is the only way the surface stays
|
|
38
|
+
* signal-dense. */
|
|
39
|
+
export const INFO_SEVERITY_KINDS = new Set([
|
|
40
|
+
'absolute-path',
|
|
41
|
+
'ip',
|
|
42
|
+
'internal-host',
|
|
43
|
+
]);
|
|
44
|
+
export function severityOf(kind) {
|
|
45
|
+
if (HIGH_SEVERITY_KINDS.has(kind))
|
|
46
|
+
return 'high';
|
|
47
|
+
if (INFO_SEVERITY_KINDS.has(kind))
|
|
48
|
+
return 'info';
|
|
49
|
+
return 'low';
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=severity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"severity.js","sourceRoot":"","sources":["../src/severity.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,mEAAmE;AACnE,0CAA0C;AAC1C,qEAAqE;AACrE,6DAA6D;AAC7D,8DAA8D;AAC9D,gEAAgE;AAChE,mEAAmE;AACnE,qEAAqE;AACrE,wCAAwC;AACxC,EAAE;AACF,mEAAmE;AACnE,qEAAqE;AACrE,kEAAkE;AAClE,8DAA8D;AAI9D,MAAM,CAAC,MAAM,mBAAmB,GAA+B,IAAI,GAAG,CAAC;IACrE,aAAa;IACb,SAAS;IACT,gBAAgB;IAChB,kBAAkB;IAClB,OAAO;IACP,mBAAmB;IACnB,WAAW;IACX,SAAS;IACT,KAAK;IACL,QAAQ;IACR,YAAY;IACZ,SAAS;IACT,gBAAgB;CACjB,CAAC,CAAA;AAEF;;;;;;oBAMoB;AACpB,MAAM,CAAC,MAAM,mBAAmB,GAA+B,IAAI,GAAG,CAAC;IACrE,eAAe;IACf,IAAI;IACJ,eAAe;CAChB,CAAC,CAAA;AAIF,MAAM,UAAU,UAAU,CAAC,IAAmB;IAC5C,IAAI,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,MAAM,CAAA;IAChD,IAAI,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,MAAM,CAAA;IAChD,OAAO,KAAK,CAAA;AACd,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Every category the detector can emit. New kinds get appended; the
|
|
2
|
+
* set is exhaustive so consumers can pattern-match it safely. */
|
|
3
|
+
export type SensitiveKind = 'private-key' | 'ssh-key' | 'api-key' | 'cloud-cred-ini' | 'kubeconfig-token' | 'connection-string' | 'jwt' | 'bearer' | 'basic-auth' | 'env-var' | 'generic-secret' | 'netrc' | 'email' | 'phone' | 'person-name' | 'street-address' | 'date-of-birth' | 'credit-card' | 'ssn' | 'ip' | 'url-creds' | 'absolute-path' | 'internal-host';
|
|
4
|
+
export interface SensitiveMatch {
|
|
5
|
+
kind: SensitiveKind;
|
|
6
|
+
/** The literal substring detected, suitable to pass to a redact
|
|
7
|
+
* substituter. For multi-line kinds (private-key, cloud-cred-ini)
|
|
8
|
+
* this is the entire block so the whole thing gets masked. */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Character offset within the input string. */
|
|
11
|
+
start: number;
|
|
12
|
+
/** Exclusive end offset. */
|
|
13
|
+
end: number;
|
|
14
|
+
/** Heuristic 0–1.
|
|
15
|
+
* 1.0 Structural certainty — PEM block, Luhn-valid card.
|
|
16
|
+
* ≥0.9 Strong vendor prefix or composite signal.
|
|
17
|
+
* ≥0.7 Format match without checksum.
|
|
18
|
+
* <0.7 Context-only signal, surface for user review.
|
|
19
|
+
*/
|
|
20
|
+
confidence: number;
|
|
21
|
+
/** Which provider produced this match (`'regex'` today; future
|
|
22
|
+
* values: `'privacy-filter'`, `'gliner'`, …). Lets the editor UI
|
|
23
|
+
* show "model-detected" vs "rule-detected" badges and lets a
|
|
24
|
+
* future security-scan report attribute matches. */
|
|
25
|
+
provider: string;
|
|
26
|
+
}
|
|
27
|
+
export interface SensitiveValue {
|
|
28
|
+
value: string;
|
|
29
|
+
/** Number of times this exact literal appeared in the source.
|
|
30
|
+
* Always ≥ 1. Useful for surfacing "same token printed twice"
|
|
31
|
+
* without rendering duplicate rows. */
|
|
32
|
+
count: number;
|
|
33
|
+
}
|
|
34
|
+
export interface SensitiveGroup {
|
|
35
|
+
kind: SensitiveKind;
|
|
36
|
+
/** Total number of matches in this group (sum of every value's
|
|
37
|
+
* occurrence count). Drives the `×N` header label. */
|
|
38
|
+
count: number;
|
|
39
|
+
/** Distinct values for this kind, in first-seen detection order,
|
|
40
|
+
* each paired with its occurrence count. Editor renders one row
|
|
41
|
+
* per entry — so a value that appears 3× is one decision, not
|
|
42
|
+
* three. */
|
|
43
|
+
values: SensitiveValue[];
|
|
44
|
+
/** Worst-case (lowest) confidence in this group — drives the
|
|
45
|
+
* "review" badge in the editor. */
|
|
46
|
+
minConfidence: number;
|
|
47
|
+
}
|
|
48
|
+
/** Display order — strict risk descent. Credentials (a single leak
|
|
49
|
+
* is potentially catastrophic) come first; identity / location
|
|
50
|
+
* signals (annoying but recoverable) come last. Items that travel
|
|
51
|
+
* together stay adjacent: cred files (PEM / SSH / cloud INI /
|
|
52
|
+
* kubeconfig / netrc); URL-form creds (conn-string / url-creds);
|
|
53
|
+
* token-form creds (api-key / jwt / bearer / basic-auth /
|
|
54
|
+
* env-var / generic-secret); finally financial / identity / infra. */
|
|
55
|
+
export declare const SENSITIVE_KIND_ORDER: SensitiveKind[];
|
|
56
|
+
export declare const SENSITIVE_KIND_LABEL: Record<SensitiveKind, string>;
|
|
57
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA;kEACkE;AAClE,MAAM,MAAM,aAAa,GAErB,aAAa,GACb,SAAS,GACT,SAAS,GACT,gBAAgB,GAChB,kBAAkB,GAClB,mBAAmB,GACnB,KAAK,GACL,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,gBAAgB,GAChB,OAAO,GAEP,OAAO,GACP,OAAO,GACP,aAAa,GACb,gBAAgB,GAChB,eAAe,GACf,aAAa,GACb,KAAK,GACL,IAAI,GAEJ,WAAW,GACX,eAAe,GACf,eAAe,CAAA;AAEnB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,aAAa,CAAA;IACnB;;mEAE+D;IAC/D,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,4BAA4B;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;yDAGqD;IACrD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb;;4CAEwC;IACxC,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,aAAa,CAAA;IACnB;2DACuD;IACvD,KAAK,EAAE,MAAM,CAAA;IACb;;;iBAGa;IACb,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB;wCACoC;IACpC,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;uEAMuE;AACvE,eAAO,MAAM,oBAAoB,EAAE,aAAa,EAwB/C,CAAA;AAED,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAwB9D,CAAA"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Public types for @spool-lab/redact.
|
|
2
|
+
//
|
|
3
|
+
// Stable interface so callers (Share editor, future Security Scan,
|
|
4
|
+
// CLI doctor) don't break when we add detector kinds or swap the
|
|
5
|
+
// internal provider. New `SensitiveKind` values are additive — never
|
|
6
|
+
// rename or repurpose existing ones.
|
|
7
|
+
/** Display order — strict risk descent. Credentials (a single leak
|
|
8
|
+
* is potentially catastrophic) come first; identity / location
|
|
9
|
+
* signals (annoying but recoverable) come last. Items that travel
|
|
10
|
+
* together stay adjacent: cred files (PEM / SSH / cloud INI /
|
|
11
|
+
* kubeconfig / netrc); URL-form creds (conn-string / url-creds);
|
|
12
|
+
* token-form creds (api-key / jwt / bearer / basic-auth /
|
|
13
|
+
* env-var / generic-secret); finally financial / identity / infra. */
|
|
14
|
+
export const SENSITIVE_KIND_ORDER = [
|
|
15
|
+
'private-key',
|
|
16
|
+
'ssh-key',
|
|
17
|
+
'cloud-cred-ini',
|
|
18
|
+
'kubeconfig-token',
|
|
19
|
+
'netrc',
|
|
20
|
+
'connection-string',
|
|
21
|
+
'url-creds',
|
|
22
|
+
'api-key',
|
|
23
|
+
'jwt',
|
|
24
|
+
'bearer',
|
|
25
|
+
'basic-auth',
|
|
26
|
+
'env-var',
|
|
27
|
+
'generic-secret',
|
|
28
|
+
'credit-card',
|
|
29
|
+
'ssn',
|
|
30
|
+
'email',
|
|
31
|
+
'phone',
|
|
32
|
+
'person-name',
|
|
33
|
+
'street-address',
|
|
34
|
+
'date-of-birth',
|
|
35
|
+
'ip',
|
|
36
|
+
'absolute-path',
|
|
37
|
+
'internal-host',
|
|
38
|
+
];
|
|
39
|
+
export const SENSITIVE_KIND_LABEL = {
|
|
40
|
+
'private-key': 'Private key',
|
|
41
|
+
'ssh-key': 'SSH private key',
|
|
42
|
+
'cloud-cred-ini': 'Cloud credentials',
|
|
43
|
+
'kubeconfig-token': 'kubeconfig token',
|
|
44
|
+
'connection-string': 'Connection string',
|
|
45
|
+
'api-key': 'API key',
|
|
46
|
+
'netrc': '.netrc entry',
|
|
47
|
+
'jwt': 'JWT',
|
|
48
|
+
'bearer': 'Bearer token',
|
|
49
|
+
'basic-auth': 'Basic auth',
|
|
50
|
+
'env-var': 'Env-var secret',
|
|
51
|
+
'generic-secret': 'Generic secret',
|
|
52
|
+
'url-creds': 'URL credential',
|
|
53
|
+
'credit-card': 'Credit card',
|
|
54
|
+
'ssn': 'SSN',
|
|
55
|
+
'email': 'Email',
|
|
56
|
+
'phone': 'Phone',
|
|
57
|
+
'person-name': 'Person name',
|
|
58
|
+
'street-address': 'Street address',
|
|
59
|
+
'date-of-birth': 'Date of birth',
|
|
60
|
+
'ip': 'IP address',
|
|
61
|
+
'absolute-path': 'Absolute path',
|
|
62
|
+
'internal-host': 'Internal hostname',
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,mEAAmE;AACnE,iEAAiE;AACjE,qEAAqE;AACrE,qCAAqC;AA+ErC;;;;;;uEAMuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAoB;IACnD,aAAa;IACb,SAAS;IACT,gBAAgB;IAChB,kBAAkB;IAClB,OAAO;IACP,mBAAmB;IACnB,WAAW;IACX,SAAS;IACT,KAAK;IACL,QAAQ;IACR,YAAY;IACZ,SAAS;IACT,gBAAgB;IAChB,aAAa;IACb,KAAK;IACL,OAAO;IACP,OAAO;IACP,aAAa;IACb,gBAAgB;IAChB,eAAe;IACf,IAAI;IACJ,eAAe;IACf,eAAe;CAChB,CAAA;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAkC;IACjE,aAAa,EAAE,aAAa;IAC5B,SAAS,EAAE,iBAAiB;IAC5B,gBAAgB,EAAE,mBAAmB;IACrC,kBAAkB,EAAE,kBAAkB;IACtC,mBAAmB,EAAE,mBAAmB;IACxC,SAAS,EAAE,SAAS;IACpB,OAAO,EAAE,cAAc;IACvB,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE,cAAc;IACxB,YAAY,EAAE,YAAY;IAC1B,SAAS,EAAE,gBAAgB;IAC3B,gBAAgB,EAAE,gBAAgB;IAClC,WAAW,EAAE,gBAAgB;IAC7B,aAAa,EAAE,aAAa;IAC5B,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE,OAAO;IAChB,OAAO,EAAE,OAAO;IAChB,aAAa,EAAE,aAAa;IAC5B,gBAAgB,EAAE,gBAAgB;IAClC,eAAe,EAAE,eAAe;IAChC,IAAI,EAAE,YAAY;IAClB,eAAe,EAAE,eAAe;IAChC,eAAe,EAAE,mBAAmB;CACrC,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Luhn check for credit-card-shaped digit runs. Operates on the
|
|
2
|
+
* raw match (separators allowed); returns true if the embedded
|
|
3
|
+
* digit sequence is a valid mod-10 checksum. */
|
|
4
|
+
export declare function luhnOk(value: string): boolean;
|
|
5
|
+
/** Stable 32-bit non-cryptographic hash of `value`, hex-encoded as
|
|
6
|
+
* 8 lowercase characters. Used by the Share editor to persist per-
|
|
7
|
+
* item redact opt-outs in `RedactExclude.valueHashes` *without*
|
|
8
|
+
* storing the literal value.
|
|
9
|
+
*
|
|
10
|
+
* Threat model: an attacker who can read the persisted draft can
|
|
11
|
+
* also read the conversation body in the same file, so a stronger
|
|
12
|
+
* hash would not raise the bar. The goal here is to ensure the
|
|
13
|
+
* Share editor itself doesn't produce a NEW on-disk artifact that
|
|
14
|
+
* names a sensitive literal. FNV-1a is deterministic, sync, and
|
|
15
|
+
* fast enough to call inside a React render. */
|
|
16
|
+
export declare function hashValueForRedactExclude(value: string): string;
|
|
17
|
+
/** Shannon entropy of a string. Used to gate the generic-secret rule
|
|
18
|
+
* so that `password = "letmeinletmeinletmein"` (low-entropy) doesn't
|
|
19
|
+
* trigger but `password = "j82H1xK9pQrSt7VwYzA3"` does. */
|
|
20
|
+
export declare function shannon(s: string): number;
|
|
21
|
+
/** Curried entropy floor for use as a rule validator. The match
|
|
22
|
+
* string is expected to be a "keyword = value" capture; we pull the
|
|
23
|
+
* quoted body so we don't include the key name in the entropy. */
|
|
24
|
+
export declare function hasQuotedEntropy(min: number): (value: string) => boolean;
|
|
25
|
+
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AASA;;iDAEiD;AACjD,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAe7C;AAED;;;;;;;;;;iDAUiD;AACjD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAU/D;AAED;;4DAE4D;AAC5D,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAWzC;AAED;;mEAEmE;AACnE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAKxE"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Small validators used by the detector rules to suppress false
|
|
2
|
+
// positives that pure regex can't. Each is side-effect-free and
|
|
3
|
+
// returns boolean — discard-on-false at the detector level.
|
|
4
|
+
//
|
|
5
|
+
// Also home to `hashValueForRedactExclude`: the non-crypto hash
|
|
6
|
+
// used by the Share editor to record per-item opt-outs WITHOUT
|
|
7
|
+
// writing the literal value back to disk. See `RedactExclude` in
|
|
8
|
+
// `@spool/share-kit` for the threat model and rationale.
|
|
9
|
+
/** Luhn check for credit-card-shaped digit runs. Operates on the
|
|
10
|
+
* raw match (separators allowed); returns true if the embedded
|
|
11
|
+
* digit sequence is a valid mod-10 checksum. */
|
|
12
|
+
export function luhnOk(value) {
|
|
13
|
+
const digits = value.replace(/\D/g, '');
|
|
14
|
+
if (digits.length < 13 || digits.length > 19)
|
|
15
|
+
return false;
|
|
16
|
+
let sum = 0;
|
|
17
|
+
let alt = false;
|
|
18
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
19
|
+
let d = digits.charCodeAt(i) - 48;
|
|
20
|
+
if (alt) {
|
|
21
|
+
d *= 2;
|
|
22
|
+
if (d > 9)
|
|
23
|
+
d -= 9;
|
|
24
|
+
}
|
|
25
|
+
sum += d;
|
|
26
|
+
alt = !alt;
|
|
27
|
+
}
|
|
28
|
+
return sum % 10 === 0;
|
|
29
|
+
}
|
|
30
|
+
/** Stable 32-bit non-cryptographic hash of `value`, hex-encoded as
|
|
31
|
+
* 8 lowercase characters. Used by the Share editor to persist per-
|
|
32
|
+
* item redact opt-outs in `RedactExclude.valueHashes` *without*
|
|
33
|
+
* storing the literal value.
|
|
34
|
+
*
|
|
35
|
+
* Threat model: an attacker who can read the persisted draft can
|
|
36
|
+
* also read the conversation body in the same file, so a stronger
|
|
37
|
+
* hash would not raise the bar. The goal here is to ensure the
|
|
38
|
+
* Share editor itself doesn't produce a NEW on-disk artifact that
|
|
39
|
+
* names a sensitive literal. FNV-1a is deterministic, sync, and
|
|
40
|
+
* fast enough to call inside a React render. */
|
|
41
|
+
export function hashValueForRedactExclude(value) {
|
|
42
|
+
// FNV-1a 32-bit. Offset basis 0x811c9dc5, prime 0x01000193.
|
|
43
|
+
let h = 0x811c9dc5;
|
|
44
|
+
for (let i = 0; i < value.length; i++) {
|
|
45
|
+
h ^= value.charCodeAt(i);
|
|
46
|
+
// Force back to unsigned 32-bit after each multiply so JS's
|
|
47
|
+
// 53-bit-mantissa Number doesn't drift into floating-point.
|
|
48
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
49
|
+
}
|
|
50
|
+
return h.toString(16).padStart(8, '0');
|
|
51
|
+
}
|
|
52
|
+
/** Shannon entropy of a string. Used to gate the generic-secret rule
|
|
53
|
+
* so that `password = "letmeinletmeinletmein"` (low-entropy) doesn't
|
|
54
|
+
* trigger but `password = "j82H1xK9pQrSt7VwYzA3"` does. */
|
|
55
|
+
export function shannon(s) {
|
|
56
|
+
if (!s)
|
|
57
|
+
return 0;
|
|
58
|
+
const counts = new Map();
|
|
59
|
+
for (const c of s)
|
|
60
|
+
counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
61
|
+
const len = s.length;
|
|
62
|
+
let h = 0;
|
|
63
|
+
for (const n of counts.values()) {
|
|
64
|
+
const p = n / len;
|
|
65
|
+
h -= p * Math.log2(p);
|
|
66
|
+
}
|
|
67
|
+
return h;
|
|
68
|
+
}
|
|
69
|
+
/** Curried entropy floor for use as a rule validator. The match
|
|
70
|
+
* string is expected to be a "keyword = value" capture; we pull the
|
|
71
|
+
* quoted body so we don't include the key name in the entropy. */
|
|
72
|
+
export function hasQuotedEntropy(min) {
|
|
73
|
+
return (value) => {
|
|
74
|
+
const inner = value.match(/["']([^"']{6,})["']/)?.[1] ?? value;
|
|
75
|
+
return shannon(inner) >= min;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.js","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,gEAAgE;AAChE,4DAA4D;AAC5D,EAAE;AACF,gEAAgE;AAChE,+DAA+D;AAC/D,iEAAiE;AACjE,yDAAyD;AAEzD;;iDAEiD;AACjD,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACvC,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,KAAK,CAAA;IAC1D,IAAI,GAAG,GAAG,CAAC,CAAA;IACX,IAAI,GAAG,GAAG,KAAK,CAAA;IACf,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,IAAI,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;QACjC,IAAI,GAAG,EAAE,CAAC;YACR,CAAC,IAAI,CAAC,CAAA;YACN,IAAI,CAAC,GAAG,CAAC;gBAAE,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;QACD,GAAG,IAAI,CAAC,CAAA;QACR,GAAG,GAAG,CAAC,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,GAAG,GAAG,EAAE,KAAK,CAAC,CAAA;AACvB,CAAC;AAED;;;;;;;;;;iDAUiD;AACjD,MAAM,UAAU,yBAAyB,CAAC,KAAa;IACrD,4DAA4D;IAC5D,IAAI,CAAC,GAAG,UAAU,CAAA;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QACxB,4DAA4D;QAC5D,4DAA4D;QAC5D,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAA;IACpC,CAAC;IACD,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AACxC,CAAC;AAED;;4DAE4D;AAC5D,MAAM,UAAU,OAAO,CAAC,CAAS;IAC/B,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAA;IAChB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1D,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAA;IACpB,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAA;QACjB,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED;;mEAEmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,CAAC,KAAa,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAA;QAC9D,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,CAAA;IAC9B,CAAC,CAAA;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spool-lab/redact",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Local, pure-TS sensitive-data detection for Spool sessions. Pattern + checksum + entropy pipeline today; pluggable provider boundary for future on-device ML (e.g. OpenAI Privacy Filter via transformers.js).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.19.17",
|
|
22
|
+
"vitest": "^3.2.4"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "pnpm run clean && tsc",
|
|
26
|
+
"dev": "tsc --watch",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"clean": "rm -rf dist"
|
|
31
|
+
}
|
|
32
|
+
}
|