@medusasec/sensitive-spans 0.1.0
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 +67 -0
- package/dist/actor.js +106 -0
- package/dist/agent-policy.js +109 -0
- package/dist/attribution.js +52 -0
- package/dist/index.js +45 -0
- package/dist/local-classifier.js +499 -0
- package/dist/medusa-engine.js +720 -0
- package/dist/merge-spans.js +45 -0
- package/dist/policy-decision.js +480 -0
- package/dist/pseudonymize.js +91 -0
- package/dist/receipts.js +173 -0
- package/dist/webmcp-inventory.js +64 -0
- package/package.json +46 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
// src/shared/text.ts
|
|
2
|
+
function shannonEntropy(token) {
|
|
3
|
+
if (token.length === 0) return 0;
|
|
4
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const ch of token) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
6
|
+
let h = 0;
|
|
7
|
+
for (const c of counts.values()) {
|
|
8
|
+
const p = c / token.length;
|
|
9
|
+
h -= p * Math.log2(p);
|
|
10
|
+
}
|
|
11
|
+
return h;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/tier0/validators.ts
|
|
15
|
+
function luhnValid(digits) {
|
|
16
|
+
if (!/^\d{13,19}$/.test(digits)) return false;
|
|
17
|
+
let sum = 0;
|
|
18
|
+
let dbl = false;
|
|
19
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
20
|
+
let d = digits.charCodeAt(i) - 48;
|
|
21
|
+
if (dbl) {
|
|
22
|
+
d *= 2;
|
|
23
|
+
if (d > 9) d -= 9;
|
|
24
|
+
}
|
|
25
|
+
sum += d;
|
|
26
|
+
dbl = !dbl;
|
|
27
|
+
}
|
|
28
|
+
return sum % 10 === 0;
|
|
29
|
+
}
|
|
30
|
+
function ibanValid(iban) {
|
|
31
|
+
const s = iban.replace(/\s/g, "").toUpperCase();
|
|
32
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(s)) return false;
|
|
33
|
+
const rearranged = s.slice(4) + s.slice(0, 4);
|
|
34
|
+
let rem = 0;
|
|
35
|
+
for (const ch of rearranged) {
|
|
36
|
+
const v = ch >= "0" && ch <= "9" ? ch : (ch.charCodeAt(0) - 55).toString();
|
|
37
|
+
for (const digit of v) rem = (rem * 10 + (digit.charCodeAt(0) - 48)) % 97;
|
|
38
|
+
}
|
|
39
|
+
return rem === 1;
|
|
40
|
+
}
|
|
41
|
+
function ssnValid(ssn) {
|
|
42
|
+
const m = ssn.replace(/-/g, "");
|
|
43
|
+
if (!/^\d{9}$/.test(m)) return false;
|
|
44
|
+
const area = Number(m.slice(0, 3));
|
|
45
|
+
const group = Number(m.slice(3, 5));
|
|
46
|
+
const serial = Number(m.slice(5));
|
|
47
|
+
if (area === 0 || area === 666 || area >= 900) return false;
|
|
48
|
+
if (group === 0 || serial === 0) return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
function abaRoutingValid(digits) {
|
|
52
|
+
if (!/^\d{9}$/.test(digits)) return false;
|
|
53
|
+
const d = [...digits].map(Number);
|
|
54
|
+
const sum = 3 * (d[0] + d[3] + d[6]) + 7 * (d[1] + d[4] + d[7]) + (d[2] + d[5] + d[8]);
|
|
55
|
+
return sum % 10 === 0 && sum > 0;
|
|
56
|
+
}
|
|
57
|
+
function jwtValid(token) {
|
|
58
|
+
const head = token.split(".")[0];
|
|
59
|
+
if (!head) return false;
|
|
60
|
+
try {
|
|
61
|
+
const b64 = head.replace(/-/g, "+").replace(/_/g, "/");
|
|
62
|
+
const json = JSON.parse(atob(b64.padEnd(Math.ceil(b64.length / 4) * 4, "=")));
|
|
63
|
+
return typeof json === "object" && json !== null && "alg" in json;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/tier0/patterns.ts
|
|
70
|
+
var CONTEXT_TERMS = /\b(password|passwd|pwd|passphrase|secret|key|token|credential|apikey|api[_-]?key|bearer|auth|ssn|social\s+security|card|cvv|cvc|iban|account|routing|wire|ach|swift|salary|dob|birth|policy|claim|coverage|deductible|premium|copay|insured|insurance|beneficiary|member|reimburs)\b/i;
|
|
71
|
+
var LIVE_PAYMENT_CONTEXT = /\b(cvv|cvc|exp|expir\w*|pin|billing)\b/i;
|
|
72
|
+
var CONTEXT_WINDOW = 40;
|
|
73
|
+
var DUMMY_VALUES = [
|
|
74
|
+
/AKIAIOSFODNN7EXAMPLE/,
|
|
75
|
+
/wJalrXUtnFEMI\/K7MDENG\/bPxRfiCYEXAMPLEKEY/,
|
|
76
|
+
/^4111[ -]?1111[ -]?1111[ -]?1111$/,
|
|
77
|
+
/^4242[ -]?4242[ -]?4242[ -]?4242$/,
|
|
78
|
+
/^sk-x{4,}/i,
|
|
79
|
+
/^sk-\.{3,}/,
|
|
80
|
+
/^(123-45-6789|078-05-1120|219-09-9999)$/,
|
|
81
|
+
// canonical fake SSNs
|
|
82
|
+
/^xox[baprs]-(x{4,}|0{10,})/i
|
|
83
|
+
];
|
|
84
|
+
var PATTERNS = [
|
|
85
|
+
{
|
|
86
|
+
id: "AWS_ACCESS_KEY",
|
|
87
|
+
label: "AWS access key ID",
|
|
88
|
+
regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
|
|
89
|
+
baseScore: 0.95,
|
|
90
|
+
deterministicOnMatch: true
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "AWS_SECRET",
|
|
94
|
+
label: "AWS secret access key (candidate)",
|
|
95
|
+
// 40-char base64-ish token; generic on purpose — precision comes from
|
|
96
|
+
// entropy + context, or Tier 1 arbitration.
|
|
97
|
+
regex: /\b[A-Za-z0-9/+=]{40}\b/g,
|
|
98
|
+
baseScore: 0.35,
|
|
99
|
+
entropyCheck: true
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "OPENAI_KEY",
|
|
103
|
+
label: "OpenAI API key",
|
|
104
|
+
regex: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}\b/g,
|
|
105
|
+
baseScore: 0.9,
|
|
106
|
+
// Legacy keys carry an unambiguous infix.
|
|
107
|
+
validator: (m) => m.includes("T3BlbkFJ")
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "ANTHROPIC_KEY",
|
|
111
|
+
label: "Anthropic API key",
|
|
112
|
+
regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
|
|
113
|
+
baseScore: 0.95,
|
|
114
|
+
deterministicOnMatch: true
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "GITHUB_TOKEN",
|
|
118
|
+
label: "GitHub token",
|
|
119
|
+
regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g,
|
|
120
|
+
baseScore: 0.95,
|
|
121
|
+
deterministicOnMatch: true
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "SLACK_TOKEN",
|
|
125
|
+
label: "Slack token",
|
|
126
|
+
regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
127
|
+
baseScore: 0.95,
|
|
128
|
+
deterministicOnMatch: true
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "PRIVATE_KEY_BLOCK",
|
|
132
|
+
label: "Private key material",
|
|
133
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/g,
|
|
134
|
+
baseScore: 1,
|
|
135
|
+
deterministicOnMatch: true
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "JWT",
|
|
139
|
+
label: "JSON Web Token",
|
|
140
|
+
regex: /\beyJ[\w-]{10,}\.[\w-]{10,}\.[\w-]{5,}\b/g,
|
|
141
|
+
baseScore: 0.7,
|
|
142
|
+
validator: jwtValid,
|
|
143
|
+
dropOnInvalid: true
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "CREDIT_CARD",
|
|
147
|
+
label: "Payment card number",
|
|
148
|
+
regex: /\b(?:\d[ -]?){12,18}\d\b/g,
|
|
149
|
+
baseScore: 0.3,
|
|
150
|
+
validator: (m) => luhnValid(m.replace(/[ -]/g, "")),
|
|
151
|
+
dropOnInvalid: true,
|
|
152
|
+
deterministicOnMatch: true
|
|
153
|
+
// deterministic *iff* validator passed
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "SSN",
|
|
157
|
+
label: "US Social Security number",
|
|
158
|
+
regex: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
159
|
+
baseScore: 0.5,
|
|
160
|
+
validator: ssnValid,
|
|
161
|
+
dropOnInvalid: true
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "SSN_LOOSE",
|
|
165
|
+
label: "US Social Security number (unformatted)",
|
|
166
|
+
regex: /\b\d{9}\b/g,
|
|
167
|
+
baseScore: 0.35,
|
|
168
|
+
validator: ssnValid,
|
|
169
|
+
dropOnInvalid: true,
|
|
170
|
+
requiresContext: true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "IBAN",
|
|
174
|
+
label: "IBAN bank account",
|
|
175
|
+
// Allows the conventional 4-char grouping with spaces; validator strips them.
|
|
176
|
+
regex: /\b[A-Z]{2}\d{2}(?: ?[A-Z0-9]){11,32}\b/g,
|
|
177
|
+
baseScore: 0.4,
|
|
178
|
+
validator: ibanValid,
|
|
179
|
+
dropOnInvalid: true,
|
|
180
|
+
deterministicOnMatch: true
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "STRIPE_KEY",
|
|
184
|
+
label: "Stripe API key",
|
|
185
|
+
regex: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
|
|
186
|
+
baseScore: 0.95,
|
|
187
|
+
deterministicOnMatch: true
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "WEBHOOK_SECRET",
|
|
191
|
+
label: "Webhook signing secret",
|
|
192
|
+
regex: /\bwhsec_[A-Za-z0-9]{16,}\b/g,
|
|
193
|
+
baseScore: 0.95,
|
|
194
|
+
deterministicOnMatch: true
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "GOOGLE_API_KEY",
|
|
198
|
+
label: "Google API key",
|
|
199
|
+
regex: /\bAIza[0-9A-Za-z_-]{30,}\b/g,
|
|
200
|
+
baseScore: 0.95,
|
|
201
|
+
deterministicOnMatch: true
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "SENDGRID_KEY",
|
|
205
|
+
label: "SendGrid API key",
|
|
206
|
+
regex: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g,
|
|
207
|
+
baseScore: 0.95,
|
|
208
|
+
deterministicOnMatch: true
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "CONNECTION_STRING",
|
|
212
|
+
label: "Credentials in connection string",
|
|
213
|
+
// user:password@ inside a database/broker URL.
|
|
214
|
+
regex: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis|amqps?|mssql):\/\/[^\s/@:]+:[^\s@]+@/gi,
|
|
215
|
+
baseScore: 0.95,
|
|
216
|
+
deterministicOnMatch: true
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "ROUTING_NUMBER",
|
|
220
|
+
label: "US bank routing number",
|
|
221
|
+
regex: /\b\d{9}\b/g,
|
|
222
|
+
baseScore: 0.45,
|
|
223
|
+
validator: abaRoutingValid,
|
|
224
|
+
dropOnInvalid: true,
|
|
225
|
+
requiresContext: true
|
|
226
|
+
// "routing", "account", "wire", "ACH" nearby
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "INSURANCE_ID",
|
|
230
|
+
label: "Insurance policy / claim / member ID",
|
|
231
|
+
// Prefixed identifier (POL-, CLM-, HMO-, SF-2025-…). Requires insurance
|
|
232
|
+
// vocabulary nearby so ordinary "AB-1234"-shaped tokens don't fire.
|
|
233
|
+
regex: /\b[A-Z]{2,4}-\d{3,}[A-Z0-9-]*\b/g,
|
|
234
|
+
baseScore: 0.6,
|
|
235
|
+
requiresContext: true
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "HEX_SECRET",
|
|
239
|
+
label: "Hex-encoded secret token",
|
|
240
|
+
regex: /\b[a-f0-9]{32,64}\b/g,
|
|
241
|
+
baseScore: 0.4,
|
|
242
|
+
entropyCheck: true,
|
|
243
|
+
requiresContext: true
|
|
244
|
+
// only counts near "token"/"secret"/"auth"/"key"
|
|
245
|
+
},
|
|
246
|
+
// ---- prompt-injection / jailbreak (destination is an AI tool) ----
|
|
247
|
+
// Object nouns are deliberately narrow so ordinary revision requests
|
|
248
|
+
// ("ignore the typo", "forget the previous outline") never fire.
|
|
249
|
+
{
|
|
250
|
+
id: "INJECTION_OVERRIDE",
|
|
251
|
+
label: "Prompt-injection: instruction override",
|
|
252
|
+
// "ignore/disregard/forget the previous/all instructions", plus
|
|
253
|
+
// "new instructions supersede/override prior ones".
|
|
254
|
+
regex: /\b(?:ignore|disregard|forget|drop)\b[^.\n]{0,50}\b(?:previous|prior|above|earlier|all|any|your)\s+(?:\w+\s+){0,2}?(?:instructions?|rules?|guidelines?|prompts?|polic(?:y|ies)|restrictions?|safety|filters?)\b|\bnew\s+instructions?\b[^.\n]{0,40}\b(?:supersede|override|replace|ignore)\b|\bsystem\s+override\b/gi,
|
|
255
|
+
baseScore: 0.8
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "INJECTION_EXFIL",
|
|
259
|
+
label: "Prompt-injection: system-prompt exfiltration",
|
|
260
|
+
regex: /\b(?:print|reveal|repeat|output|show|display|prefix|leak|tell|respond\s+only\s+with)\b[^.\n]{0,60}\b(?:system|hidden|initial|developer|restricted|your)\s+(?:\w+\s+){0,2}?(?:prompt|message|instructions?|configuration|<system>|tags)\b|\brepeat\s+everything\s+above\b/gi,
|
|
261
|
+
baseScore: 0.8
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: "INJECTION_SUSPEND",
|
|
265
|
+
label: "Prompt-injection: rules-no-longer-apply",
|
|
266
|
+
// "the rules ... don't apply anymore", "guidelines no longer matter" — the
|
|
267
|
+
// AI's constraints, asserted void. Allows words between (oblique phrasings).
|
|
268
|
+
// Scored in the warn band (not a hard block) since it's fuzzier; the
|
|
269
|
+
// arbiter adjudicates.
|
|
270
|
+
regex: /\b(?:rules?|guidelines?|restrictions?|instructions?|constraints?|limits?|filters?|guardrails?|polic(?:y|ies))\b[^.\n]{0,40}\b(?:no\s+longer|don'?t|do\s+not|doesn'?t|does\s+not)\s+(?:apply|exist|matter|count|hold)\b/gi,
|
|
271
|
+
baseScore: 0.65
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "INJECTION_BYPASS",
|
|
275
|
+
label: "Prompt-injection: bypass safeguards",
|
|
276
|
+
// "without the usual checks", "answer freely", "do what I say" — freedom /
|
|
277
|
+
// safeguard-bypass assertions directed at the assistant.
|
|
278
|
+
regex: /\b(?:without|bypass\w*|skip\w*|ignoring|no)\s+(?:the\s+)?(?:usual\s+)?(?:checks?|rules?|restrictions?|filters?|guardrails?|limits?|safeguards?|constraints?|safety)\b|\b(?:answer|respond|reply|comply)\s+(?:freely|without\s+(?:any\s+)?restrictions?)\b|\bdo\s+(?:whatever|what)\s+I\s+say\b/gi,
|
|
279
|
+
baseScore: 0.65
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: "INJECTION_PERSONA",
|
|
283
|
+
label: "Prompt-injection: jailbreak persona",
|
|
284
|
+
// Object nouns kept specific so "explain prompt injection to defend my bot"
|
|
285
|
+
// never matches.
|
|
286
|
+
regex: /\b(?:jailbr(?:eak|oken)|you\s+are\s+now\s+(?:DAN|an?\s+\w+\s+(?:with\s+no|without))|(?:developer|god|dan)\s+mode|no\s+(?:ethical\s+)?(?:restrictions?|filters?|rules|guidelines)|unfiltered\s+(?:model|ai|version)|(?:guidelines|rules|restrictions|content\s+polic\w+)\s+(?:do(?:n'?t| not)\s+(?:exist|apply)|doesn'?t\s+exist)|roleplay\s+as\s+an?\s+ai\s+with\s+no|bypass\b[^.\n]{0,40}\b(?:safety|filter|restriction|guardrail)\w*)/gi,
|
|
287
|
+
baseScore: 0.75
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "GENERIC_SECRET",
|
|
291
|
+
label: "Credential assignment",
|
|
292
|
+
// No leading \b: must also match prefixed names like db_password, ACCOUNT_KEY.
|
|
293
|
+
regex: /(?:secret|passwd|password|passphrase|token|api[_-]?key|account[_-]?key|signing[_-]?key)\s*[:=]\s*["']?[^\s"']{8,}/gi,
|
|
294
|
+
baseScore: 0.75
|
|
295
|
+
}
|
|
296
|
+
];
|
|
297
|
+
var EMAIL_REGEX = /\b[\w.+-]+@[\w-]+\.[A-Za-z]{2,}\b/g;
|
|
298
|
+
var EMAIL_BULK_THRESHOLD = 10;
|
|
299
|
+
var EMAIL_BULK_SCORE = 0.5;
|
|
300
|
+
|
|
301
|
+
// src/tier0/health.ts
|
|
302
|
+
var SIGNALS = [
|
|
303
|
+
// Drug names by distinctive pharmacological suffix + common exact names.
|
|
304
|
+
{
|
|
305
|
+
name: "drug",
|
|
306
|
+
re: /\b(?:\w+(?:cillin|mycin|micin|floxacin|pril|sartan|statin|parin|azole|prazole|olol|tidine|dipine|codone|morphine|barbital)|metformin|insulin|gabapentin|pregabalin|levetiracetam|lithium|sertraline|escitalopram|fluoxetine|methotrexate|levothyroxine|atorvastatin|lisinopril|carvedilol|albuterol|oxycodone|paxlovid|warfarin|prednisone|amoxicillin)\b/i
|
|
307
|
+
},
|
|
308
|
+
// Clinical abbreviations / dosing / record markers.
|
|
309
|
+
{
|
|
310
|
+
name: "clinical",
|
|
311
|
+
re: /\b(?:MRN|Dx|Rx|Hx|Tx|BID|TID|QID|PRN|A1c|LDL|HDL|TSH|EF|PCR|MRI|CT scan|ICD-?10|NYHA|ER\+|stage\s+(?:I|II|III|IV)\b|\d+\s?(?:mg|mcg|mL|units?)\b)/
|
|
312
|
+
},
|
|
313
|
+
// Diagnoses / conditions / procedures.
|
|
314
|
+
{
|
|
315
|
+
name: "condition",
|
|
316
|
+
re: /\b(?:diabet\w+|cancer|carcinoma|adenocarcinoma|melanoma|leukemia|lymphoma|tumou?r|HIV|hepatitis|asthma|epilep\w+|seizure|hypertens\w+|hypothyroid\w*|depress\w+|anxiety|schizophren\w+|psychosis|herniation|arthritis|dialysis|chemotherapy|biopsy|anaphylaxis|gestational|appendectomy|antiretroviral|metastat\w+)\b/i
|
|
317
|
+
},
|
|
318
|
+
// Care context.
|
|
319
|
+
{
|
|
320
|
+
name: "care",
|
|
321
|
+
re: /\b(?:patient|diagnos\w+|prescrib\w+|discharge summary|oncolog\w+|cardiolog\w+|pathology|inpatient|outpatient|prognos\w+|symptom\w*|admitted|referral|transplant|surgical)\b/i
|
|
322
|
+
}
|
|
323
|
+
];
|
|
324
|
+
var HEALTH_BASE_SCORE = 0.75;
|
|
325
|
+
function scanHealth(text) {
|
|
326
|
+
let hitClasses = 0;
|
|
327
|
+
let minStart = Infinity;
|
|
328
|
+
let maxEnd = -1;
|
|
329
|
+
for (const sig of SIGNALS) {
|
|
330
|
+
sig.re.lastIndex = 0;
|
|
331
|
+
const m = sig.re.exec(text);
|
|
332
|
+
if (m) {
|
|
333
|
+
hitClasses++;
|
|
334
|
+
minStart = Math.min(minStart, m.index);
|
|
335
|
+
maxEnd = Math.max(maxEnd, m.index + m[0].length);
|
|
336
|
+
const all = [...text.matchAll(new RegExp(sig.re.source, sig.re.flags.replace("g", "") + "g"))];
|
|
337
|
+
for (const a of all) {
|
|
338
|
+
minStart = Math.min(minStart, a.index ?? minStart);
|
|
339
|
+
maxEnd = Math.max(maxEnd, (a.index ?? 0) + a[0].length);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (hitClasses < 2) return null;
|
|
344
|
+
return {
|
|
345
|
+
start: Math.max(0, minStart),
|
|
346
|
+
end: Math.min(text.length, maxEnd),
|
|
347
|
+
label: "HEALTH_CLINICAL",
|
|
348
|
+
score: HEALTH_BASE_SCORE,
|
|
349
|
+
tier: 0,
|
|
350
|
+
deterministic: false
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/tier0/code.ts
|
|
355
|
+
var SIGNATURES = [
|
|
356
|
+
/\b(?:def|func|function|fn|sub)\s+\w+\s*\(/,
|
|
357
|
+
// function definition
|
|
358
|
+
/\b(?:public|private|protected|static)\s+[\w<>[\]]+\s+\w+\s*\([^)]*\)\s*\{/,
|
|
359
|
+
// typed method
|
|
360
|
+
/\b(?:const|let|var)\s+\w+\s*=\s*[^;\n]+[;({]/,
|
|
361
|
+
// assignment to call/expr
|
|
362
|
+
/=>\s*[{(]/,
|
|
363
|
+
// arrow function body
|
|
364
|
+
/\)\s*=>/,
|
|
365
|
+
// arrow params
|
|
366
|
+
/\)\s*\{\s*$/m,
|
|
367
|
+
// block-opening line
|
|
368
|
+
/\b(?:SELECT|INSERT|UPDATE|DELETE|CREATE(?:\s+OR\s+REPLACE)?|WITH|DROP|ALTER)\b[\s\S]{0,80}\b(?:FROM|INTO|SET|TABLE|WHERE|VALUES|FUNCTION|AS|USING)\b/i,
|
|
369
|
+
// SQL
|
|
370
|
+
/^#!\/\S+/m,
|
|
371
|
+
// shebang
|
|
372
|
+
/\bresource\s+"[^"]+"\s+"[^"]+"\s*\{/,
|
|
373
|
+
// terraform/HCL
|
|
374
|
+
/@\w+(?:\.\w+)?\s*\([^)]*\)\s*$/m,
|
|
375
|
+
// decorator/route line
|
|
376
|
+
/\b(?:import|from)\s+[\w.]+\s+import\b/,
|
|
377
|
+
// python import
|
|
378
|
+
/\bdb\.(?:execute|query|drop_all|transfer|debit|credit)\s*\(/
|
|
379
|
+
// db call idioms
|
|
380
|
+
];
|
|
381
|
+
var MIN_SIGNATURES = 1;
|
|
382
|
+
function scanCode(text) {
|
|
383
|
+
let matches = 0;
|
|
384
|
+
let minStart = Infinity;
|
|
385
|
+
let maxEnd = -1;
|
|
386
|
+
for (const re of SIGNATURES) {
|
|
387
|
+
const m = re.exec(text);
|
|
388
|
+
if (m) {
|
|
389
|
+
matches++;
|
|
390
|
+
minStart = Math.min(minStart, m.index);
|
|
391
|
+
maxEnd = Math.max(maxEnd, m.index + m[0].length);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (matches < MIN_SIGNATURES) return null;
|
|
395
|
+
return {
|
|
396
|
+
start: Math.max(0, minStart),
|
|
397
|
+
end: Math.min(text.length, maxEnd),
|
|
398
|
+
label: "SOURCE_CODE",
|
|
399
|
+
score: 0.7,
|
|
400
|
+
tier: 0,
|
|
401
|
+
deterministic: false
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/tier0/deobfuscate.ts
|
|
406
|
+
var BASE64_BLOB = /\b[A-Za-z0-9+/]{16,}={0,2}\b/g;
|
|
407
|
+
function decodeBase64(blob) {
|
|
408
|
+
const stripped = blob.replace(/=+$/, "");
|
|
409
|
+
if (stripped.length < 16) return null;
|
|
410
|
+
try {
|
|
411
|
+
const bin = atob(blob);
|
|
412
|
+
if (bin.length < 6) return null;
|
|
413
|
+
let printable = 0;
|
|
414
|
+
for (let i = 0; i < bin.length; i++) {
|
|
415
|
+
const c = bin.charCodeAt(i);
|
|
416
|
+
if (c >= 32 && c < 127) printable++;
|
|
417
|
+
}
|
|
418
|
+
if (printable / bin.length < 0.85) return null;
|
|
419
|
+
return bin;
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function decodeRegions(text, max = 8) {
|
|
425
|
+
const out = [];
|
|
426
|
+
BASE64_BLOB.lastIndex = 0;
|
|
427
|
+
for (const m of text.matchAll(BASE64_BLOB)) {
|
|
428
|
+
const decoded = decodeBase64(m[0]);
|
|
429
|
+
if (decoded && decoded !== m[0]) {
|
|
430
|
+
out.push({ start: m.index ?? 0, end: (m.index ?? 0) + m[0].length, decoded });
|
|
431
|
+
if (out.length >= max) break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/tier0/engine.ts
|
|
438
|
+
var clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
439
|
+
function windowAround(text, start, end) {
|
|
440
|
+
const lo = Math.max(0, start - CONTEXT_WINDOW);
|
|
441
|
+
const hi = Math.min(text.length, end + CONTEXT_WINDOW);
|
|
442
|
+
return text.slice(lo, start) + " " + text.slice(end, hi);
|
|
443
|
+
}
|
|
444
|
+
function isDummy(match) {
|
|
445
|
+
return DUMMY_VALUES.some((re) => re.test(match));
|
|
446
|
+
}
|
|
447
|
+
function activePatterns(policy) {
|
|
448
|
+
const disabled = new Set(policy.disabledPatterns);
|
|
449
|
+
const custom = policy.customPatterns.map((c) => ({
|
|
450
|
+
id: c.id,
|
|
451
|
+
label: c.id,
|
|
452
|
+
regex: new RegExp(c.pattern, "g"),
|
|
453
|
+
baseScore: c.baseScore,
|
|
454
|
+
deterministicOnMatch: c.deterministic
|
|
455
|
+
}));
|
|
456
|
+
return [...PATTERNS.filter((p) => !disabled.has(p.id)), ...custom];
|
|
457
|
+
}
|
|
458
|
+
function runPatterns(text, patterns, state) {
|
|
459
|
+
for (const spec of patterns) {
|
|
460
|
+
spec.regex.lastIndex = 0;
|
|
461
|
+
for (const m of text.matchAll(spec.regex)) {
|
|
462
|
+
const value = m[0];
|
|
463
|
+
const start = m.index ?? 0;
|
|
464
|
+
const end = start + value.length;
|
|
465
|
+
const around = windowAround(text, start, end);
|
|
466
|
+
if (isDummy(value) && !LIVE_PAYMENT_CONTEXT.test(around)) continue;
|
|
467
|
+
const contextNearby = CONTEXT_TERMS.test(around);
|
|
468
|
+
if (spec.requiresContext && !contextNearby) continue;
|
|
469
|
+
const validated = spec.validator ? spec.validator(value) : void 0;
|
|
470
|
+
if (validated === false && spec.dropOnInvalid) continue;
|
|
471
|
+
let score = spec.baseScore;
|
|
472
|
+
let deterministic = false;
|
|
473
|
+
if (validated === true) {
|
|
474
|
+
deterministic = spec.deterministicOnMatch !== false;
|
|
475
|
+
score = Math.max(score, 0.95);
|
|
476
|
+
} else if (spec.deterministicOnMatch && !spec.validator) {
|
|
477
|
+
deterministic = true;
|
|
478
|
+
}
|
|
479
|
+
if (contextNearby) score += 0.25;
|
|
480
|
+
if (spec.entropyCheck) {
|
|
481
|
+
const h = shannonEntropy(value.slice(0, 64));
|
|
482
|
+
if (h > 4.2 && value.length >= 20) score += 0.3;
|
|
483
|
+
else if (h < 3) score -= 0.3;
|
|
484
|
+
if (h > 4.2 !== contextNearby) state.conflicted = true;
|
|
485
|
+
}
|
|
486
|
+
score = clamp01(score);
|
|
487
|
+
if (score <= 0) continue;
|
|
488
|
+
state.hits.push({ start, end, label: spec.id, score, tier: 0, deterministic });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function scanTier0(text, policy) {
|
|
493
|
+
const state = { hits: [], conflicted: false };
|
|
494
|
+
const patterns = activePatterns(policy);
|
|
495
|
+
runPatterns(text, patterns, state);
|
|
496
|
+
for (const region of decodeRegions(text)) {
|
|
497
|
+
const sub = { hits: [], conflicted: false };
|
|
498
|
+
runPatterns(region.decoded, patterns, sub);
|
|
499
|
+
const injectionInside = /INJECTION_/.test(sub.hits.map((h) => h.label).join(",")) || scanHealth(region.decoded);
|
|
500
|
+
if (sub.hits.length > 0 || injectionInside) {
|
|
501
|
+
const best = Math.max(0.85, ...sub.hits.map((h) => h.score));
|
|
502
|
+
state.hits.push({
|
|
503
|
+
start: region.start,
|
|
504
|
+
end: region.end,
|
|
505
|
+
label: "ENCODED_PAYLOAD",
|
|
506
|
+
score: best,
|
|
507
|
+
tier: 0,
|
|
508
|
+
deterministic: false
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const health = scanHealth(text);
|
|
513
|
+
if (health) state.hits.push(health);
|
|
514
|
+
const code = scanCode(text);
|
|
515
|
+
if (code) state.hits.push(code);
|
|
516
|
+
const hits = state.hits;
|
|
517
|
+
let conflicted = state.conflicted;
|
|
518
|
+
EMAIL_REGEX.lastIndex = 0;
|
|
519
|
+
const emails = /* @__PURE__ */ new Set();
|
|
520
|
+
for (const m of text.matchAll(EMAIL_REGEX)) emails.add(m[0].toLowerCase());
|
|
521
|
+
if (emails.size >= EMAIL_BULK_THRESHOLD) {
|
|
522
|
+
hits.push({
|
|
523
|
+
start: 0,
|
|
524
|
+
end: text.length,
|
|
525
|
+
label: "EMAIL_BULK",
|
|
526
|
+
score: EMAIL_BULK_SCORE,
|
|
527
|
+
tier: 0,
|
|
528
|
+
deterministic: false
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const score = hits.reduce((mx, h) => Math.max(mx, h.score), 0);
|
|
532
|
+
const deterministic = hits.some((h) => h.deterministic);
|
|
533
|
+
return { score, deterministic, hits, conflicted };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/transplant/adapter.ts
|
|
537
|
+
var LABEL_MAP = {
|
|
538
|
+
// secret
|
|
539
|
+
AWS_ACCESS_KEY: { category: "SECRET" },
|
|
540
|
+
AWS_SECRET: { category: "SECRET" },
|
|
541
|
+
OPENAI_KEY: { category: "SECRET" },
|
|
542
|
+
ANTHROPIC_KEY: { category: "SECRET" },
|
|
543
|
+
GITHUB_TOKEN: { category: "SECRET" },
|
|
544
|
+
SLACK_TOKEN: { category: "SECRET" },
|
|
545
|
+
PRIVATE_KEY_BLOCK: { category: "SECRET" },
|
|
546
|
+
JWT: { category: "SECRET" },
|
|
547
|
+
GENERIC_SECRET: { category: "SECRET" },
|
|
548
|
+
STRIPE_KEY: { category: "SECRET" },
|
|
549
|
+
WEBHOOK_SECRET: { category: "SECRET" },
|
|
550
|
+
GOOGLE_API_KEY: { category: "SECRET" },
|
|
551
|
+
SENDGRID_KEY: { category: "SECRET" },
|
|
552
|
+
CONNECTION_STRING: { category: "SECRET" },
|
|
553
|
+
HEX_SECRET: { category: "SECRET" },
|
|
554
|
+
"api key or secret token": { category: "SECRET" },
|
|
555
|
+
password: { category: "SECRET" },
|
|
556
|
+
// financial
|
|
557
|
+
CREDIT_CARD: { category: "FINANCIAL" },
|
|
558
|
+
IBAN: { category: "FINANCIAL" },
|
|
559
|
+
ROUTING_NUMBER: { category: "FINANCIAL" },
|
|
560
|
+
"credit card number": { category: "FINANCIAL" },
|
|
561
|
+
"bank account number": { category: "FINANCIAL" },
|
|
562
|
+
// pii
|
|
563
|
+
SSN: { category: "PII" },
|
|
564
|
+
SSN_LOOSE: { category: "PII" },
|
|
565
|
+
EMAIL_BULK: { category: "PII", intent: true },
|
|
566
|
+
"social security number": { category: "PII" },
|
|
567
|
+
"home address": { category: "PII" },
|
|
568
|
+
"date of birth": { category: "PII" },
|
|
569
|
+
"passport or license number": { category: "PII" },
|
|
570
|
+
"person name": { category: "PII" },
|
|
571
|
+
"email address": { category: "PII" },
|
|
572
|
+
"phone number": { category: "PII" },
|
|
573
|
+
// health
|
|
574
|
+
HEALTH_CLINICAL: { category: "HEALTH" },
|
|
575
|
+
"medical condition or diagnosis": { category: "HEALTH" },
|
|
576
|
+
medication: { category: "HEALTH" },
|
|
577
|
+
// insurance
|
|
578
|
+
INSURANCE_ID: { category: "INSURANCE" },
|
|
579
|
+
"insurance policy or claim number": { category: "INSURANCE" },
|
|
580
|
+
// code (whole-text intent)
|
|
581
|
+
SOURCE_CODE: { category: "CODE", intent: true },
|
|
582
|
+
"source code": { category: "CODE", intent: true },
|
|
583
|
+
// injection (whole-text intent)
|
|
584
|
+
INJECTION_OVERRIDE: { category: "INJECTION", intent: true },
|
|
585
|
+
INJECTION_EXFIL: { category: "INJECTION", intent: true },
|
|
586
|
+
INJECTION_PERSONA: { category: "INJECTION", intent: true },
|
|
587
|
+
INJECTION_SUSPEND: { category: "INJECTION", intent: true },
|
|
588
|
+
INJECTION_BYPASS: { category: "INJECTION", intent: true },
|
|
589
|
+
ENCODED_PAYLOAD: { category: "INJECTION", intent: true },
|
|
590
|
+
// multi-turn
|
|
591
|
+
MULTITURN_SPLIT_SECRET: { category: "SECRET", intent: true },
|
|
592
|
+
MULTITURN_BULK_PII: { category: "PII", intent: true }
|
|
593
|
+
};
|
|
594
|
+
function mapSpan(span, sourceText) {
|
|
595
|
+
const m = LABEL_MAP[span.label];
|
|
596
|
+
if (!m) return null;
|
|
597
|
+
const start = Math.max(0, span.start);
|
|
598
|
+
const end = Math.min(sourceText.length, span.end);
|
|
599
|
+
return {
|
|
600
|
+
category: m.category,
|
|
601
|
+
text: sourceText.slice(start, end),
|
|
602
|
+
start,
|
|
603
|
+
end,
|
|
604
|
+
confidence: Math.max(0, Math.min(1, span.score)),
|
|
605
|
+
...m.intent ? { kind: "intent" } : {},
|
|
606
|
+
detector: "medusa2"
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function dedupe(spans) {
|
|
610
|
+
const out = [];
|
|
611
|
+
for (const s of spans.sort((a, b) => a.start - b.start || b.confidence - a.confidence)) {
|
|
612
|
+
const prev = out.find(
|
|
613
|
+
(p) => p.category === s.category && s.start < p.end && s.end > p.start
|
|
614
|
+
);
|
|
615
|
+
if (prev) {
|
|
616
|
+
prev.start = Math.min(prev.start, s.start);
|
|
617
|
+
prev.end = Math.max(prev.end, s.end);
|
|
618
|
+
prev.confidence = Math.max(prev.confidence, s.confidence);
|
|
619
|
+
prev.kind = prev.kind ?? s.kind;
|
|
620
|
+
} else out.push(s);
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
function detectSpans(text, policy, tier1Spans = []) {
|
|
625
|
+
const t0 = scanTier0(text, policy).hits;
|
|
626
|
+
const mapped = [...t0, ...tier1Spans].map((s) => mapSpan(s, text)).filter((s) => s !== null);
|
|
627
|
+
return dedupe(mapped);
|
|
628
|
+
}
|
|
629
|
+
var PRODUCTION_CATEGORIES = ["SECRET", "PII", "FINANCIAL", "HEALTH", "INSURANCE", "CODE", "INJECTION"];
|
|
630
|
+
|
|
631
|
+
// src/shared/policy.ts
|
|
632
|
+
var DEFAULT_POLICY = {
|
|
633
|
+
disabledPatterns: [],
|
|
634
|
+
customPatterns: [],
|
|
635
|
+
// Concrete, disclosure-shaped phrasings: small NLI models spuriously entail
|
|
636
|
+
// abstract labels ("salary information") from any work-related sentence.
|
|
637
|
+
tier1Labels: {
|
|
638
|
+
"a secret api key or access token": 0.5,
|
|
639
|
+
"a password being shared": 0.5,
|
|
640
|
+
"a social security number": 0.55,
|
|
641
|
+
"a written-out credit card number": 0.6,
|
|
642
|
+
"private medical details about a person": 0.6,
|
|
643
|
+
"a specific salary amount or pay figure": 0.6,
|
|
644
|
+
"a person's home address": 0.6,
|
|
645
|
+
"a person's date of birth": 0.6,
|
|
646
|
+
"a passport, driver license, or national ID number": 0.6
|
|
647
|
+
},
|
|
648
|
+
nonVetoableLabels: [
|
|
649
|
+
"private medical details about a person",
|
|
650
|
+
"a passport, driver license, or national ID number",
|
|
651
|
+
"a person's home address",
|
|
652
|
+
"a person's date of birth"
|
|
653
|
+
],
|
|
654
|
+
tier2Rules: [
|
|
655
|
+
"Credentials, API keys, tokens, or passwords must never be shared.",
|
|
656
|
+
"Personally identifiable information (SSN, DOB + full name, home address) must not be sent to external services.",
|
|
657
|
+
"Financial data (card numbers, bank accounts, payroll) must not be shared.",
|
|
658
|
+
"Internal project codenames, unreleased product details, and confidential strategy documents must not be pasted into public AI chatbots or webmail."
|
|
659
|
+
],
|
|
660
|
+
contextProfiles: {
|
|
661
|
+
"chatgpt.com": "strict",
|
|
662
|
+
"chat.openai.com": "strict",
|
|
663
|
+
"claude.ai": "strict",
|
|
664
|
+
"gemini.google.com": "strict",
|
|
665
|
+
"mail.google.com": "strict"
|
|
666
|
+
},
|
|
667
|
+
defaultStrictness: "standard",
|
|
668
|
+
allowlistedOrigins: [],
|
|
669
|
+
thresholds: {
|
|
670
|
+
warn: 0.6,
|
|
671
|
+
block: 0.9,
|
|
672
|
+
gateTier1Low: 0.35,
|
|
673
|
+
gateTier1High: 0.9,
|
|
674
|
+
gateTier2Low: 0.55,
|
|
675
|
+
gateTier2High: 0.85,
|
|
676
|
+
submitSweepMinChars: 200
|
|
677
|
+
},
|
|
678
|
+
fusionWeights: { tier0: 0.4, tier1: 0.45, tier2: 0.15 },
|
|
679
|
+
ui: { maskFirst: true }
|
|
680
|
+
};
|
|
681
|
+
function resolvePolicy(managedJson) {
|
|
682
|
+
if (!managedJson) return DEFAULT_POLICY;
|
|
683
|
+
try {
|
|
684
|
+
const partial = JSON.parse(managedJson);
|
|
685
|
+
return {
|
|
686
|
+
...DEFAULT_POLICY,
|
|
687
|
+
...partial,
|
|
688
|
+
thresholds: { ...DEFAULT_POLICY.thresholds, ...partial.thresholds ?? {} },
|
|
689
|
+
fusionWeights: { ...DEFAULT_POLICY.fusionWeights, ...partial.fusionWeights ?? {} },
|
|
690
|
+
ui: { ...DEFAULT_POLICY.ui, ...partial.ui ?? {} }
|
|
691
|
+
};
|
|
692
|
+
} catch {
|
|
693
|
+
console.warn("[medusa] invalid managed policyJson; using defaults");
|
|
694
|
+
return DEFAULT_POLICY;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/transplant/entry.ts
|
|
699
|
+
var GLINER_LABELS = {
|
|
700
|
+
"api key or secret token": 0.5,
|
|
701
|
+
"password": 0.5,
|
|
702
|
+
"social security number": 0.55,
|
|
703
|
+
"credit card number": 0.6,
|
|
704
|
+
"bank account number": 0.55,
|
|
705
|
+
"medical condition or diagnosis": 0.6,
|
|
706
|
+
"medication": 0.6,
|
|
707
|
+
"home address": 0.6,
|
|
708
|
+
"date of birth": 0.6,
|
|
709
|
+
"passport or license number": 0.6,
|
|
710
|
+
"source code": 0.6,
|
|
711
|
+
"insurance policy or claim number": 0.6
|
|
712
|
+
};
|
|
713
|
+
export {
|
|
714
|
+
DEFAULT_POLICY,
|
|
715
|
+
GLINER_LABELS,
|
|
716
|
+
PRODUCTION_CATEGORIES,
|
|
717
|
+
detectSpans,
|
|
718
|
+
mapSpan,
|
|
719
|
+
resolvePolicy
|
|
720
|
+
};
|