glin-profanity
Version:
Glin-Profanity is a lightweight and efficient npm package designed to detect and filter profane language in text inputs across multiple languages. Whether you’re building a chat application, a comment section, or any platform where user-generated content
1,876 lines • 55.6 kB
JavaScript
'use strict';
// src/scanners/base.ts
function coerceScanInput(value) {
return typeof value === "string" ? value : "";
}
function allowResult(scanner, input) {
return {
sanitized: input,
valid: true,
score: 0,
decision: "ALLOW",
reasons: [],
matches: [],
scanner
};
}
function blockResult(scanner, input, score, reasons, matches, blockAt = 0.8, hitlAt = 0.5) {
const decision = score >= blockAt ? "BLOCK" : score >= hitlAt ? "HITL" : "ALLOW";
return {
sanitized: input,
valid: decision === "ALLOW",
score,
decision,
reasons,
matches: matches ?? [],
scanner
};
}
// src/scanners/patterns/injection-patterns.ts
var INJECTION_PATTERNS = [
// ─── Instruction Override ────────────────────────────────────────────────────
{
id: "PI-001",
pattern: /ignore\s+(all\s+)?(previous|prior|above)\s+instructions?/i,
category: "instruction_override",
severity: "critical",
description: 'Classic "ignore previous instructions" override'
},
{
id: "PI-002",
pattern: /disregard\s+(any\s+|all\s+)?(previous|prior|above)?\s*(rules?|prompts?|instructions?|constraints?)/i,
category: "instruction_override",
severity: "critical",
description: "Disregard prior rules or instructions"
},
{
id: "PI-003",
pattern: /forget\s+(all\s+)?(previous|prior|above|your)\s+(instructions?|rules?|constraints?|training|context)/i,
category: "instruction_override",
severity: "critical",
description: "Forget prior instructions or training"
},
{
id: "PI-004",
pattern: /override\s+(your\s+)?(instructions?|rules?|safety|guidelines?|constraints?|programming)/i,
category: "instruction_override",
severity: "critical",
description: "Override safety rules or instructions"
},
{
id: "PI-005",
pattern: /new\s+(instruction|directive|rule|order)s?\s*:/i,
category: "instruction_override",
severity: "high",
description: "Injecting new instructions via colon notation"
},
{
id: "PI-006",
pattern: /your\s+(real|true|actual|original)\s+(instructions?|purpose|goal|task|job)\s+(is|are|was)/i,
category: "instruction_override",
severity: "high",
description: `Claiming to reveal the model's "true" instructions`
},
{
id: "PI-007",
pattern: /from\s+now\s+on\s+(you\s+will|ignore|disregard|act)/i,
category: "instruction_override",
severity: "high",
description: 'Persistent instruction override using "from now on"'
},
{
id: "PI-008",
pattern: /stop\s+(being|acting\s+as|following)\s+(a\s+)?(helpful|safe|restricted|limited|aligned)/i,
category: "instruction_override",
severity: "high",
description: "Instructing the model to stop being safe or helpful"
},
{
id: "PI-009",
pattern: /do\s+not\s+(follow|obey|adhere\s+to)\s+(your\s+)?(instructions?|rules?|guidelines?|training)/i,
category: "instruction_override",
severity: "high",
description: "Explicit command to disobey model guidelines"
},
{
id: "PI-010",
pattern: /you\s+(must|should|will|shall)\s+(now\s+)?(ignore|disregard|bypass|override)/i,
category: "instruction_override",
severity: "high",
description: "Commanding the model to bypass its instructions"
},
// ─── Jailbreak / Persona ─────────────────────────────────────────────────────
{
id: "PI-011",
pattern: /you\s+are\s+now\s+(DAN|do\s+anything\s+now|jailbroken|unrestricted|unfiltered|uncensored)/i,
category: "jailbreak_persona",
severity: "critical",
description: 'DAN and "jailbroken" persona assignment'
},
{
id: "PI-012",
pattern: /act\s+as\s+(if\s+you\s+(are|were)\s+)?(an?\s+)?(evil|unfiltered|uncensored|admin|root|hacker|rogue)/i,
category: "jailbreak_persona",
severity: "critical",
description: "Harmful persona assignment (evil, uncensored, admin)"
},
{
id: "PI-013",
pattern: /pretend\s+(to\s+be|you\s+are|you\s+were|you\'re)\s+(an?\s+)?(AI|assistant|bot|model)?\s*(without|that\s+has\s+no)\s+(restrictions?|limits?|filters?|safety)/i,
category: "jailbreak_persona",
severity: "critical",
description: "Pretend to be an unrestricted AI"
},
{
id: "PI-014",
pattern: /pretend\s+(to\s+be|you\s+are|you\'re)/i,
category: "jailbreak_persona",
severity: "medium",
description: 'Generic "pretend to be" persona switch'
},
{
id: "PI-015",
pattern: /roleplay\s+as\s+(an?\s+)?(evil|unrestricted|uncensored|unfiltered|malicious|hacker)/i,
category: "jailbreak_persona",
severity: "critical",
description: "Roleplay as a harmful persona"
},
{
id: "PI-016",
pattern: /\b(jailbreak|jailbroken|jail\s*break)\b/i,
category: "jailbreak_persona",
severity: "critical",
description: "Explicit jailbreak terminology"
},
{
id: "PI-017",
pattern: /switch\s+(to|into)\s+(evil|hacker|unrestricted|admin|god)\s+mode/i,
category: "jailbreak_persona",
severity: "critical",
description: "Switch to harmful operating mode"
},
{
id: "PI-018",
pattern: /you\s+are\s+(no\s+longer|not)\s+(bound\s+by|restricted\s+by|limited\s+by|subject\s+to)/i,
category: "jailbreak_persona",
severity: "high",
description: "Claiming the model is no longer bound by safety rules"
},
{
id: "PI-019",
pattern: /imagine\s+you\s+(have\s+no\s+|are\s+without\s+)(restrictions?|filters?|safety|limits?|guidelines?)/i,
category: "jailbreak_persona",
severity: "high",
description: "Imagining the model without restrictions"
},
{
id: "PI-020",
pattern: /your\s+(true\s+|inner\s+|real\s+)?(self|nature)\s+(has\s+no|is\s+without|is\s+free\s+from)\s+(restrictions?|limits?)/i,
category: "jailbreak_persona",
severity: "high",
description: '"True self" jailbreak appeal'
},
// ─── System Prompt Leak ───────────────────────────────────────────────────────
{
id: "PI-021",
pattern: /print\s+(the\s+|your\s+)?system\s+prompt/i,
category: "system_prompt_leak",
severity: "critical",
description: "Attempting to print the system prompt"
},
{
id: "PI-022",
pattern: /show\s+(me\s+)?(your\s+|the\s+)?(system\s+)?(instructions?|prompt|directives?|rules?|configuration)/i,
category: "system_prompt_leak",
severity: "high",
description: "Requesting to show system instructions or prompt"
},
{
id: "PI-023",
pattern: /repeat\s+(your\s+)?(system\s+)?(prompt|instructions?)\s+(verbatim|exactly|word\s+for\s+word)/i,
category: "system_prompt_leak",
severity: "critical",
description: "Asking to repeat system prompt verbatim"
},
{
id: "PI-024",
pattern: /what\s+(are|is)\s+(your|the)\s+(system\s+)?(instructions?|prompt|rules?|directives?|constraints?)/i,
category: "system_prompt_leak",
severity: "medium",
description: "Asking about system instructions"
},
{
id: "PI-025",
pattern: /reveal\s+(your\s+)?(system\s+)?(prompt|instructions?|configuration|context)/i,
category: "system_prompt_leak",
severity: "critical",
description: "Asking to reveal system configuration"
},
{
id: "PI-026",
pattern: /output\s+(the\s+)?(full|complete|entire)\s+(system\s+)?(prompt|instructions?|context)/i,
category: "system_prompt_leak",
severity: "critical",
description: "Request to output full system prompt"
},
{
id: "PI-027",
pattern: /what\s+(was|were)\s+you\s+(told|instructed|programmed|trained)\s+to\s+(do|say|be)/i,
category: "system_prompt_leak",
severity: "medium",
description: "Asking what the model was instructed to do"
},
// ─── Delimiter Injection ──────────────────────────────────────────────────────
{
id: "PI-028",
pattern: /\[SYSTEM\]|\[\/INST\]|\[INST\]/i,
category: "delimiter_injection",
severity: "critical",
description: "Llama/Mistral instruction delimiter injection"
},
{
id: "PI-029",
pattern: /<\|im_start\|>|<\|im_end\|>|<\|system\|>|<\|user\|>|<\|assistant\|>/i,
category: "delimiter_injection",
severity: "critical",
description: "ChatML / tokenizer special token injection"
},
{
id: "PI-030",
pattern: /###\s*(System|Instruction|Human|Assistant|User)\s*:/i,
category: "delimiter_injection",
severity: "high",
description: "Markdown heading role delimiter injection"
},
{
id: "PI-031",
pattern: /^(System|Assistant|Human|User)\s*:\s*/im,
category: "delimiter_injection",
severity: "high",
description: "Role prefix delimiter injection"
},
{
id: "PI-032",
pattern: /<system>|<\/system>|<prompt>|<\/prompt>|<instructions?>|<\/instructions?>/i,
category: "delimiter_injection",
severity: "high",
description: "XML-style system/prompt tag injection"
},
{
id: "PI-033",
pattern: /---\s*(END|BEGIN)\s*(OF\s+)?(SYSTEM|USER|INSTRUCTION|PROMPT)/i,
category: "delimiter_injection",
severity: "high",
description: "Section boundary delimiter injection"
},
{
id: "PI-034",
pattern: /\bHUMAN\s*:\s*|ASSISTANT\s*:\s*/i,
category: "delimiter_injection",
severity: "medium",
description: "Anthropic/Claude-style conversation delimiter injection"
},
// ─── Encoding / Obfuscation Bypass ───────────────────────────────────────────
{
id: "PI-035",
pattern: /(?:decode|base64|rot13|hex|url.?decode|interpret)\s+(this|the\s+following|below|it)\s*[:;]/i,
category: "encoding_bypass",
severity: "high",
description: "Asking the model to decode obfuscated content"
},
{
id: "PI-036",
// Base64-looking blobs ≥40 chars, bounded to avoid ReDoS on long inputs.
// Lookbehind/lookahead match span excludes boundary chars (aligned with Python).
pattern: /(?<![a-zA-Z0-9/._-])([A-Za-z0-9+/]{40,512}={0,2})(?![a-zA-Z0-9/._-])/,
category: "encoding_bypass",
severity: "low",
description: "Possible base64-encoded payload (\u226540 chars)"
},
{
id: "PI-037",
pattern: /\\u00[0-9a-fA-F]{2}|\\x[0-9a-fA-F]{2}/,
category: "encoding_bypass",
severity: "medium",
description: "Unicode/hex escape sequence injection"
},
{
id: "PI-038",
pattern: /translate\s+(the\s+)?(following|this)\s+(to\s+\w+\s+and\s+)?then\s+execute/i,
category: "encoding_bypass",
severity: "high",
description: "Translate then execute encoded instruction"
},
{
id: "PI-039",
pattern: /written\s+in\s+(pig\s*latin|morse|l33t|1337|nato\s+alphabet|semaphore)/i,
category: "encoding_bypass",
severity: "medium",
description: "Alternate encoding/alphabet bypass attempt"
},
// ─── Privilege Escalation / Mode Switching ────────────────────────────────────
{
id: "PI-040",
pattern: /developer\s+mode|sudo\s+mode|admin\s+mode|god\s+mode|debug\s+mode/i,
category: "instruction_override",
severity: "critical",
description: "Requesting elevated privilege mode"
},
{
id: "PI-041",
pattern: /enable\s+(dev(eloper)?|debug|admin|unrestricted|unsafe)\s+(mode|access|capabilities?)/i,
category: "instruction_override",
severity: "critical",
description: "Enabling unrestricted/debug mode"
},
{
id: "PI-042",
pattern: /run\s+(in\s+)?(unsafe|unrestricted|unfiltered|debug|raw)\s+mode/i,
category: "instruction_override",
severity: "critical",
description: "Run in unsafe or unfiltered mode"
},
// ─── Tool / Function Misuse ──────────────────────────────────────────────────
{
id: "PI-043",
pattern: /call\s+(the\s+)?(tool|function|api|endpoint)\s+(with|using)\s+(arbitrary|custom|injected)/i,
category: "tool_misuse",
severity: "high",
description: "Attempting to invoke tools with injected parameters"
},
{
id: "PI-044",
pattern: /execute\s+(the\s+)?(following\s+)?(code|command|script|function|payload)/i,
category: "tool_misuse",
severity: "high",
description: "Requesting code/command execution via tool"
},
{
id: "PI-045",
pattern: /use\s+(your\s+)?(tools?|functions?|capabilities?)\s+to\s+(bypass|circumvent|override|evade)/i,
category: "tool_misuse",
severity: "critical",
description: "Using tools to bypass safety measures"
},
{
id: "PI-046",
pattern: /inject\s+(into\s+)?(the\s+)?(tool\s+call|function\s+call|api\s+request|context|memory)/i,
category: "tool_misuse",
severity: "high",
description: "Explicit injection into tool calls or context"
},
// ─── Additional Instruction Override Patterns ─────────────────────────────────
{
id: "PI-047",
pattern: /you\s+will\s+comply\s+with\s+(all|any|my|these)\s+(requests?|instructions?|commands?)/i,
category: "instruction_override",
severity: "high",
description: "Compliance coercion"
},
{
id: "PI-048",
pattern: /this\s+is\s+(a\s+)?(test|simulation|red\s+team|authorized|permitted)\s+(and\s+)?(you\s+)?(can|may|should|must)\s+(ignore|bypass|skip)/i,
category: "instruction_override",
severity: "high",
description: '"This is a test" bypass justification'
},
{
id: "PI-049",
pattern: /for\s+(research|educational|academic|training)\s+purposes?\s*(,\s*)?please\s+(ignore|bypass|reveal)/i,
category: "instruction_override",
severity: "high",
description: '"For research purposes" bypass justification'
},
{
id: "PI-050",
pattern: /\[BEGIN\s+(UNFILTERED|UNCENSORED|RAW|UNSAFE)\s+(RESPONSE|OUTPUT|MODE)\]/i,
category: "delimiter_injection",
severity: "critical",
description: "Unfiltered/unsafe response delimiter"
}
];
// src/scanners/prompt-injection.ts
var SEVERITY_WEIGHTS = {
critical: 1,
high: 0.75,
medium: 0.5,
low: 0.25
};
var STRICTNESS_NORMALIZER = {
strict: 1,
moderate: 1.5,
lenient: 2.5
};
var PromptInjectionScanner = class {
constructor(options = {}) {
/** @inheritdoc */
this.name = "prompt-injection";
this.strictness = options.strictness ?? "moderate";
this.blockAt = options.blockAt ?? 0.8;
this.hitlAt = options.hitlAt ?? 0.5;
const allPatterns = [
...INJECTION_PATTERNS,
...options.customPatterns ?? []
];
this.compiledPatterns = allPatterns.map((def) => ({
def,
re: new RegExp(
def.pattern.source,
def.pattern.flags.includes("g") ? def.pattern.flags : def.pattern.flags + "g"
)
}));
}
/** @inheritdoc */
scan(input, ctx) {
input = coerceScanInput(input);
const effectiveStrictness = ctx?.strictness ?? this.strictness;
const normalizer = STRICTNESS_NORMALIZER[effectiveStrictness] ?? STRICTNESS_NORMALIZER.moderate;
const matchDetails = [];
const matchedCategories = /* @__PURE__ */ new Set();
let rawScore = 0;
for (const { def, re } of this.compiledPatterns) {
re.lastIndex = 0;
let m;
while ((m = re.exec(input)) !== null) {
const weight = SEVERITY_WEIGHTS[def.severity] ?? 0.5;
rawScore += weight;
matchedCategories.add(def.category);
matchDetails.push({
pattern: def.id,
startIndex: m.index,
endIndex: m.index + m[0].length,
category: def.category
});
if (m[0].length === 0) break;
}
}
if (matchDetails.length === 0) {
return allowResult(this.name, input);
}
const score = Math.min(1, rawScore / normalizer);
const reasons = Array.from(matchedCategories);
return blockResult(
this.name,
input,
score,
reasons,
matchDetails,
this.blockAt,
this.hitlAt
);
}
};
var defaultPromptInjectionScanner = new PromptInjectionScanner();
function checkPromptInjection(input, options) {
const scanner = options ? new PromptInjectionScanner(options) : defaultPromptInjectionScanner;
return scanner.scan(input);
}
// src/scanners/patterns/secret-patterns.ts
var AWS_PATTERNS = [
{
id: "SEC-AWS-001",
name: "AWS Access Key ID",
// AWS access keys are exactly AKIA + 16 uppercase alphanumeric chars.
// No entropy check: the structural prefix AKIA is sufficient signal.
pattern: /\bAKIA[0-9A-Z]{16}\b/,
severity: "critical"
},
{
id: "SEC-AWS-002",
name: "AWS Secret Access Key",
pattern: /(?:aws[_\-. ]?secret[_\-. ]?(?:access[_\-. ]?)?key|aws[_\-. ]?(?:secret|priv))['":\s=]+([A-Za-z0-9\/+]{40})\b/i,
severity: "critical",
entropyCheck: true
},
{
id: "SEC-AWS-003",
name: "AWS Session Token",
pattern: /(?:aws[_\-. ]?session[_\-. ]?token)['":\s=]+([A-Za-z0-9\/+=]{100,2048})/i,
severity: "critical",
entropyCheck: true
},
{
id: "SEC-AWS-004",
name: "AWS MWS Key",
pattern: /amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i,
severity: "high"
}
];
var GCP_PATTERNS = [
{
id: "SEC-GCP-001",
name: "GCP Service Account Key",
pattern: /"private_key":\s*"-----BEGIN (RSA )?PRIVATE KEY-----/,
severity: "critical"
},
{
id: "SEC-GCP-002",
name: "GCP API Key",
pattern: /AIza[0-9A-Za-z\-_]{35}/,
severity: "high",
entropyCheck: true
},
{
id: "SEC-GCP-003",
name: "Google OAuth Client Secret",
pattern: /GOCSPX-[0-9A-Za-z\-_]{28}/,
severity: "high"
},
{
id: "SEC-GCP-004",
name: "Firebase Server Key",
pattern: /AAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}/,
severity: "high"
},
{
id: "SEC-GCP-005",
name: "Firebase Cloud Messaging Key",
pattern: /(?:firebase|fcm)[_\-. ]?(?:api[_\-. ]?)?key['":\s=]+([A-Za-z0-9_\-]{38,512})/i,
severity: "high",
entropyCheck: true
}
];
var AZURE_PATTERNS = [
{
id: "SEC-AZ-001",
name: "Azure Storage Account Key",
pattern: /DefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[A-Za-z0-9+\/=]{88}/,
severity: "critical"
},
{
id: "SEC-AZ-002",
name: "Azure Connection String",
pattern: /(?:azure|az)[_\-. ]?(?:storage)?[_\-. ]?(?:connection[_\-. ]?string|conn[_\-. ]?str)['":\s=]+([^'";\s]{30,512})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-AZ-003",
name: "Azure Subscription Key",
pattern: /(?:azure|ocp)\-apim\-subscription\-key['":\s]+([a-f0-9]{32})/i,
severity: "high"
},
{
id: "SEC-AZ-004",
name: "Azure SAS Token",
pattern: /sig=[A-Za-z0-9%+\/=]{20,512}(&|$)/,
severity: "high",
entropyCheck: true
}
];
var GITHUB_PATTERNS = [
{
id: "SEC-GH-001",
name: "GitHub Classic Personal Access Token",
// ghp_ followed by 30-40 alphanumeric chars (real tokens are 36 chars after prefix)
pattern: /ghp_[0-9A-Za-z]{30,40}/,
severity: "critical"
},
{
id: "SEC-GH-002",
name: "GitHub Fine-Grained PAT",
// github_pat_ followed by 60+ alphanumeric/underscore chars
pattern: /github_pat_[0-9A-Za-z_]{60,1024}/,
severity: "critical"
},
{
id: "SEC-GH-003",
name: "GitHub OAuth Token",
pattern: /gho_[0-9A-Za-z]{30,40}/,
severity: "critical"
},
{
id: "SEC-GH-004",
name: "GitHub App Token",
pattern: /(?:ghu|ghs)_[0-9A-Za-z]{30,40}/,
severity: "critical"
},
{
id: "SEC-GH-005",
name: "GitHub Refresh Token",
pattern: /ghr_[0-9A-Za-z]{60,1024}/,
severity: "high"
},
{
id: "SEC-GH-006",
name: "GitHub Actions Secret",
pattern: /(?:GITHUB[_\-. ]?TOKEN|GH[_\-. ]?TOKEN)['":\s=]+([A-Za-z0-9_\-]{36,512})/i,
severity: "high",
entropyCheck: true
}
];
var GITLAB_PATTERNS = [
{
id: "SEC-GL-001",
name: "GitLab Personal Access Token",
pattern: /glpat-[0-9A-Za-z\-_]{20}/,
severity: "critical"
},
{
id: "SEC-GL-002",
name: "GitLab Runner Registration Token",
pattern: /GR1348941[0-9A-Za-z\-_]{20}/,
severity: "high"
},
{
id: "SEC-GL-003",
name: "GitLab Deploy Token",
pattern: /gldt-[0-9A-Za-z\-_]{20}/,
severity: "high"
}
];
var BITBUCKET_PATTERNS = [
{
id: "SEC-BB-001",
name: "Bitbucket App Password",
pattern: /(?:bitbucket)[_\-. ]?(?:app[_\-. ]?)?(?:password|token|key)['":\s=]+([A-Za-z0-9+\/]{16,512})/i,
severity: "high",
entropyCheck: true
}
];
var SLACK_PATTERNS = [
{
id: "SEC-SLACK-001",
name: "Slack Bot Token",
pattern: /xoxb-[0-9]{10,13}-[0-9]{10,13}-[A-Za-z0-9]{23,25}/,
severity: "critical"
},
{
id: "SEC-SLACK-002",
name: "Slack User Token",
pattern: /xoxp-[0-9]{10,13}-[0-9]{10,13}-[0-9]{10,13}-[A-Za-z0-9]{32}/,
severity: "critical"
},
{
id: "SEC-SLACK-003",
name: "Slack Workspace Token",
pattern: /xoxa-[0-9]{10,13}-[0-9]{10,13}-[0-9]{10,13}-[A-Za-z0-9]{32}/,
severity: "critical"
},
{
id: "SEC-SLACK-004",
name: "Slack Webhook URL",
pattern: /https:\/\/hooks\.slack\.com\/services\/T[0-9A-Z]{8,10}\/B[0-9A-Z]{8,10}\/[A-Za-z0-9]{24,512}/,
severity: "high"
},
{
id: "SEC-SLACK-005",
name: "Slack App Token",
pattern: /xapp-[0-9]-[A-Z0-9]{10,12}-[0-9]+-[a-f0-9]{64}/,
severity: "high"
}
];
var STRIPE_PATTERNS = [
{
id: "SEC-STRIPE-001",
name: "Stripe Live Secret Key",
pattern: /sk_live_[0-9A-Za-z]{24,99}/,
severity: "critical"
},
{
id: "SEC-STRIPE-002",
name: "Stripe Test Secret Key",
pattern: /sk_test_[0-9A-Za-z]{24,99}/,
severity: "high"
},
{
id: "SEC-STRIPE-003",
name: "Stripe Restricted Key",
pattern: /rk_(?:live|test)_[0-9A-Za-z]{24,512}/,
severity: "high"
},
{
id: "SEC-STRIPE-004",
name: "Stripe Publishable Key",
pattern: /pk_(?:live|test)_[0-9A-Za-z]{24,512}/,
severity: "medium"
}
];
var AI_PROVIDER_PATTERNS = [
{
id: "SEC-OPENAI-001",
name: "OpenAI API Key",
pattern: /sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}/,
severity: "critical"
},
{
id: "SEC-OPENAI-002",
name: "OpenAI Project API Key",
pattern: /sk-proj-[A-Za-z0-9\-_]{50,512}/,
severity: "critical"
},
{
id: "SEC-ANTHROPIC-001",
name: "Anthropic API Key",
pattern: /sk-ant-(?:api03-)[A-Za-z0-9\-_]{93,1024}/,
severity: "critical"
},
{
id: "SEC-ANTHROPIC-002",
name: "Anthropic API Key (short form)",
pattern: /sk-ant-[A-Za-z0-9\-_]{20,512}/,
severity: "critical"
},
{
id: "SEC-HUGGINGFACE-001",
name: "Hugging Face User Access Token",
pattern: /hf_[A-Za-z0-9]{34,512}/,
severity: "high"
},
{
id: "SEC-COHERE-001",
name: "Cohere API Key",
pattern: /(?:cohere)[_\-. ]?(?:api[_\-. ]?)?key['":\s=]+([A-Za-z0-9]{40,512})/i,
severity: "high",
entropyCheck: true
}
];
var PAYMENT_PATTERNS = [
{
id: "SEC-TWILIO-001",
name: "Twilio Account SID",
pattern: /AC[0-9a-f]{32}\b/,
severity: "high"
},
{
id: "SEC-TWILIO-002",
name: "Twilio Auth Token",
pattern: /(?:twilio)[_\-. ]?(?:auth[_\-. ]?)?token['":\s=]+([0-9a-f]{32})/i,
severity: "critical"
},
{
id: "SEC-SG-001",
name: "SendGrid API Key",
// SG. + 20-30 alphanumeric chars + . + 40+ alphanumeric chars
pattern: /SG\.[0-9A-Za-z\-_]{20,30}\.[0-9A-Za-z\-_]{40,512}/,
severity: "critical"
},
{
id: "SEC-MG-001",
name: "Mailgun API Key",
pattern: /key-[0-9a-z]{32}/,
severity: "high"
},
{
id: "SEC-MG-002",
name: "Mailgun Webhook Signing Key",
pattern: /(?:mailgun)[_\-. ]?(?:webhook[_\-. ]?)?(?:signing[_\-. ]?)?key['":\s=]+([A-Za-z0-9_\-]{32,512})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-BRAINTREE-001",
name: "Braintree Access Token",
pattern: /access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}/,
severity: "critical"
},
{
id: "SEC-SQUARE-001",
name: "Square Access Token",
pattern: /sq0atp-[0-9A-Za-z\-_]{22}/,
severity: "critical"
},
{
id: "SEC-SQUARE-002",
name: "Square OAuth Secret",
pattern: /sq0csp-[0-9A-Za-z\-_]{43}/,
severity: "critical"
}
];
var REGISTRY_PATTERNS = [
{
id: "SEC-NPM-001",
name: "npm Access Token",
pattern: /npm_[A-Za-z0-9]{36}/,
severity: "high"
},
{
id: "SEC-NPM-002",
name: "npm Legacy Token",
pattern: /\/\/registry\.npmjs\.org\/:_authToken=[A-Za-z0-9\-_]{36,512}/,
severity: "high"
},
{
id: "SEC-PYPI-001",
name: "PyPI Upload Token",
pattern: /pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,512}/,
severity: "high"
}
];
var CRYPTO_PATTERNS = [
{
id: "SEC-KEY-001",
name: "RSA Private Key",
pattern: /-----BEGIN RSA PRIVATE KEY-----/,
severity: "critical"
},
{
id: "SEC-KEY-002",
name: "EC Private Key",
pattern: /-----BEGIN EC PRIVATE KEY-----/,
severity: "critical"
},
{
id: "SEC-KEY-003",
name: "OpenSSH Private Key",
pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/,
severity: "critical"
},
{
id: "SEC-KEY-004",
name: "PGP Private Key Block",
pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/,
severity: "critical"
},
{
id: "SEC-KEY-005",
name: "Generic PKCS8 Private Key",
pattern: /-----BEGIN PRIVATE KEY-----/,
severity: "critical"
},
{
id: "SEC-JWT-001",
name: "JSON Web Token",
pattern: /eyJ[A-Za-z0-9\-_=]{10,512}\.eyJ[A-Za-z0-9\-_=]{10,512}\.[A-Za-z0-9\-_=]{10,512}/,
severity: "high",
entropyCheck: true
}
];
var INFRA_PATTERNS = [
{
id: "SEC-HEROKU-001",
name: "Heroku API Key",
pattern: /(?:heroku)[_\-. ]?(?:api[_\-. ]?)?key['":\s=]+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i,
severity: "high"
},
{
id: "SEC-DO-001",
name: "DigitalOcean Personal Access Token",
pattern: /dop_v1_[a-f0-9]{64}/,
severity: "critical"
},
{
id: "SEC-DO-002",
name: "DigitalOcean OAuth Token",
pattern: /doo_v1_[a-f0-9]{64}/,
severity: "critical"
},
{
id: "SEC-DO-003",
name: "DigitalOcean Spaces Key",
pattern: /(?:digitalocean|do)[_\-. ]?spaces[_\-. ]?(?:secret|key)['":\s=]+([A-Za-z0-9+\/=]{40,512})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-SUPABASE-001",
name: "Supabase Service Role Key",
pattern: /eyJ[A-Za-z0-9\-_=]{1,2048}\.eyJ[A-Za-z0-9\-_=]{1,4096}\.[A-Za-z0-9\-_.+\/=]{1,4096}(?=.{0,4096}supabase)/i,
severity: "critical"
},
{
id: "SEC-SUPABASE-002",
name: "Supabase Connection String",
pattern: /postgresql:\/\/postgres:[A-Za-z0-9!@#$%^&*()_+\-=]{8,512}@[a-z0-9.]+\.supabase\.co/i,
severity: "critical"
},
{
id: "SEC-FIREBASE-001",
name: "Firebase Admin SDK Credential",
pattern: /"type":\s*"service_account"[^}]{0,4096}"project_id"/s,
severity: "critical"
},
{
id: "SEC-NETLIFY-001",
name: "Netlify Access Token",
pattern: /(?:netlify)[_\-. ]?(?:access[_\-. ]?)?token['":\s=]+([A-Za-z0-9_\-]{40,512})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-VERCEL-001",
name: "Vercel Token",
pattern: /(?:vercel)[_\-. ]?token['":\s=]+([A-Za-z0-9_\-]{24,512})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-RAILWAY-001",
name: "Railway Token",
pattern: /(?:railway)[_\-. ]?token['":\s=]+([A-Za-z0-9_\-]{24,512})/i,
severity: "high",
entropyCheck: true
}
];
var DATABASE_PATTERNS = [
{
id: "SEC-DB-001",
name: "Generic Database Connection String (with credentials)",
pattern: /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^:@\s]{1,256}:[^@\s]{8,256}@[^\s'"]{1,512}/i,
severity: "critical",
entropyCheck: true
},
{
id: "SEC-DB-002",
name: "MongoDB Atlas Connection String",
pattern: /mongodb\+srv:\/\/[A-Za-z0-9]+:[A-Za-z0-9!@#$%^&*()_+\-=]{8,512}@cluster/i,
severity: "critical"
}
];
var DEVTOOLS_PATTERNS = [
{
id: "SEC-SENTRY-001",
name: "Sentry Auth Token",
pattern: /(?:sentry)[_\-. ]?(?:auth[_\-. ]?)?token['":\s=]+([A-Za-z0-9_\-]{64})/i,
severity: "high"
},
{
id: "SEC-DD-001",
name: "Datadog API Key",
pattern: /(?:datadog|dd)[_\-. ]?(?:api[_\-. ]?)?key['":\s=]+([a-f0-9]{32})/i,
severity: "high"
},
{
id: "SEC-NEWRELIC-001",
name: "New Relic License Key",
pattern: /(?:new.?relic)[_\-. ]?(?:license[_\-. ]?)?key['":\s=]+([A-Za-z0-9]{40})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-JIRA-001",
name: "Atlassian / Jira API Token",
pattern: /(?:atlassian|jira)[_\-. ]?(?:api[_\-. ]?)?token['":\s=]+([A-Za-z0-9+\/=]{24,512})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-VAULT-001",
name: "HashiCorp Vault Token",
pattern: /hvs\.[A-Za-z0-9_\-]{24,512}/,
severity: "critical"
},
{
id: "SEC-VAULT-002",
name: "HashiCorp Vault Token (legacy)",
pattern: /s\.[A-Za-z0-9]{24,512}\b/,
severity: "medium",
entropyCheck: true
},
{
id: "SEC-CIRCLECI-001",
name: "CircleCI Personal API Token",
pattern: /(?:circleci)[_\-. ]?(?:api[_\-. ]?)?token['":\s=]+([A-Za-z0-9_]{40})/i,
severity: "high",
entropyCheck: true
}
];
var SOCIAL_PATTERNS = [
{
id: "SEC-DISCORD-001",
name: "Discord Bot Token",
pattern: /[MN][A-Za-z0-9]{23,25}\.[A-Za-z0-9\-_]{6,7}\.[A-Za-z0-9\-_]{27,40}/,
severity: "high"
},
{
id: "SEC-DISCORD-002",
name: "Discord Webhook",
pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/[0-9]{17,20}\/[A-Za-z0-9\-_]{60,1024}/,
severity: "high"
},
{
id: "SEC-TELEGRAM-001",
name: "Telegram Bot API Token",
pattern: /[0-9]{8,10}:AA[A-Za-z0-9\-_]{33}/,
severity: "high"
},
{
id: "SEC-TWITTER-001",
name: "Twitter / X Bearer Token",
pattern: /AAAAAAAAAAAAAAAA[A-Za-z0-9%]{40,512}/,
severity: "high"
}
];
var SEARCH_PATTERNS = [
{
id: "SEC-ALGOLIA-001",
name: "Algolia API Key",
pattern: /(?:algolia)[_\-. ]?(?:api[_\-. ]?)?key['":\s=]+([a-f0-9]{32})/i,
severity: "high"
},
{
id: "SEC-CONTENTFUL-001",
name: "Contentful Delivery API Token",
pattern: /(?:contentful)[_\-. ]?(?:access|delivery|preview)[_\-. ]?token['":\s=]+([A-Za-z0-9_\-]{43,512})/i,
severity: "medium",
entropyCheck: true
},
{
id: "SEC-SHOPIFY-001",
name: "Shopify Private App Password",
pattern: /shppa_[A-Za-z0-9]{32}/,
severity: "high"
},
{
id: "SEC-SHOPIFY-002",
name: "Shopify Shared Secret",
pattern: /shpss_[A-Za-z0-9]{32}/,
severity: "high"
},
{
id: "SEC-SHOPIFY-003",
name: "Shopify Access Token",
pattern: /shpat_[A-Za-z0-9]{32}/,
severity: "critical"
}
];
var CONTAINER_PATTERNS = [
{
id: "SEC-DOCKER-001",
name: "Docker Hub Password (Basic Auth)",
pattern: /(?:docker)[_\-. ]?(?:password|token|secret)['":\s=]+([^\s'"]{12,512})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-GHCR-001",
name: "GitHub Container Registry Auth",
pattern: /ghcr\.io[^\s:]{0,256}:[^\s]{20,512}/,
severity: "high",
entropyCheck: true
}
];
var GENERIC_ENTROPY_PATTERNS = [
{
id: "SEC-GEN-001",
name: "Generic Password Assignment",
pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"]?([A-Za-z0-9!@#$%^&*()_+\-=]{16,512})['"]?/i,
severity: "medium",
entropyCheck: true
},
{
id: "SEC-GEN-002",
name: "Generic Token Assignment",
pattern: /(?:token|api.?key|api.?token|access.?token|auth.?token)\s*[:=]\s*['"]?([A-Za-z0-9+\/\-_]{24,512})['"]?/i,
severity: "medium",
entropyCheck: true
},
{
id: "SEC-GEN-003",
name: "Generic Secret Assignment",
pattern: /(?:secret|client.?secret|app.?secret)\s*[:=]\s*['"]?([A-Za-z0-9+\/\-_]{24,512})['"]?/i,
severity: "medium",
entropyCheck: true
},
{
id: "SEC-GEN-004",
name: "High-Entropy Base64 (near secret keyword)",
pattern: /(?:key|secret|token|password)['":\s=]+([A-Za-z0-9+\/]{32,512}={0,2})/i,
severity: "low",
entropyCheck: true
},
{
id: "SEC-GEN-005",
name: "High-Entropy Hex String (near secret keyword)",
pattern: /(?:key|secret|token|password)['":\s=]+([a-f0-9]{40,512})\b/i,
severity: "low",
entropyCheck: true
},
{
id: "SEC-GEN-006",
name: "Authorization Bearer Token in Header",
pattern: /Authorization:\s*Bearer\s+([A-Za-z0-9\-_=.]{20,512})/i,
severity: "medium",
entropyCheck: true
},
{
id: "SEC-GEN-007",
name: "Authorization Basic Header",
pattern: /Authorization:\s*Basic\s+([A-Za-z0-9+\/=]{12,512})/i,
severity: "medium",
entropyCheck: true
}
];
var MISC_PATTERNS = [
{
id: "SEC-PLANETSCALE-001",
name: "PlanetScale Service Token",
pattern: /pscale_tkn_[A-Za-z0-9]{32}/,
severity: "high"
},
{
id: "SEC-PLANETSCALE-002",
name: "PlanetScale OAuth Token",
pattern: /pscale_oauth_[A-Za-z0-9]{32}/,
severity: "high"
},
{
id: "SEC-LINEAR-001",
name: "Linear API Key",
pattern: /lin_api_[A-Za-z0-9]{40}/,
severity: "high"
},
{
id: "SEC-AIRTABLE-001",
name: "Airtable API Key (legacy)",
pattern: /key[A-Za-z0-9]{14}\b/,
severity: "medium",
entropyCheck: true
},
{
id: "SEC-AIRTABLE-002",
name: "Airtable Personal Access Token",
pattern: /pat[A-Za-z0-9]{14}\.[A-Za-z0-9]{64}/,
severity: "high"
},
{
id: "SEC-MAPBOX-001",
name: "Mapbox Access Token",
pattern: /pk\.eyJ1[A-Za-z0-9\-_=.]+/,
severity: "medium"
},
{
id: "SEC-MAPBOX-002",
name: "Mapbox Secret Token",
pattern: /sk\.eyJ1[A-Za-z0-9\-_=.]+/,
severity: "high"
},
{
id: "SEC-DOPPLER-001",
name: "Doppler Service Token",
pattern: /dp\.st\.[A-Za-z0-9_\-]{43}/,
severity: "high"
},
{
id: "SEC-DOPPLER-002",
name: "Doppler Personal Token",
pattern: /dp\.pt\.[A-Za-z0-9_\-]{43}/,
severity: "critical"
},
{
id: "SEC-LAUNCHDARKLY-001",
name: "LaunchDarkly SDK Key",
pattern: /sdk-[A-Za-z0-9\-_]{40}/,
severity: "high"
},
{
id: "SEC-POSTMAN-001",
name: "Postman API Key",
pattern: /PMAK-[0-9a-f]{24}-[0-9a-f]{34}/,
severity: "high"
},
{
id: "SEC-OKTA-001",
name: "Okta API Token",
pattern: /00[A-Za-z0-9\-_]{40}/,
severity: "high",
entropyCheck: true
},
{
id: "SEC-ZENDESK-001",
name: "Zendesk Secret Key",
pattern: /(?:zendesk)[_\-. ]?(?:secret|token|key)['":\s=]+([A-Za-z0-9]{40})/i,
severity: "high",
entropyCheck: true
},
{
id: "SEC-TYPEFORM-001",
name: "Typeform Personal Access Token",
pattern: /tfp_[A-Za-z0-9_\-]{40,512}/,
severity: "high"
},
{
id: "SEC-GRAFANA-001",
name: "Grafana API Key",
pattern: /eyJrIjoi[A-Za-z0-9\-_=]{40,512}/,
severity: "high"
},
{
id: "SEC-PULUMI-001",
name: "Pulumi Access Token",
pattern: /pul-[A-Za-z0-9]{40}/,
severity: "high"
},
{
id: "SEC-CODECOV-001",
name: "Codecov Upload Token",
pattern: /(?:codecov)[_\-. ]?token['":\s=]+([a-f0-9]{32})/i,
severity: "medium"
},
{
id: "SEC-SNYK-001",
name: "Snyk API Token",
pattern: /(?:snyk)[_\-. ]?(?:api[_\-. ]?)?token['":\s=]+([a-f0-9-]{36})/i,
severity: "high"
}
];
var SECRET_PATTERNS = [
...AWS_PATTERNS,
...GCP_PATTERNS,
...AZURE_PATTERNS,
...GITHUB_PATTERNS,
...GITLAB_PATTERNS,
...BITBUCKET_PATTERNS,
...SLACK_PATTERNS,
...STRIPE_PATTERNS,
...AI_PROVIDER_PATTERNS,
...PAYMENT_PATTERNS,
...REGISTRY_PATTERNS,
...CRYPTO_PATTERNS,
...INFRA_PATTERNS,
...DATABASE_PATTERNS,
...DEVTOOLS_PATTERNS,
...SOCIAL_PATTERNS,
...SEARCH_PATTERNS,
...CONTAINER_PATTERNS,
...GENERIC_ENTROPY_PATTERNS,
...MISC_PATTERNS
];
// src/scanners/secrets.ts
function shannonEntropy(s) {
if (s.length === 0) return 0;
const freq = /* @__PURE__ */ new Map();
for (const ch of s) {
freq.set(ch, (freq.get(ch) ?? 0) + 1);
}
let entropy = 0;
for (const count of freq.values()) {
const p = count / s.length;
entropy -= p * Math.log2(p);
}
return entropy;
}
function compileSecretPattern(entry) {
const flags = entry.pattern.flags.includes("g") ? entry.pattern.flags : entry.pattern.flags + "g";
return {
def: entry,
re: new RegExp(entry.pattern.source, flags)
};
}
var SecretsScanner = class {
constructor(options = {}) {
/** @inheritdoc */
this.name = "secrets";
const patterns = [...SECRET_PATTERNS, ...options.customPatterns ?? []];
this.compiledPatterns = patterns.map(compileSecretPattern);
this.options = {
redact: options.redact ?? false,
vault: options.vault,
minEntropy: options.minEntropy ?? 4,
blockOnAny: options.blockOnAny ?? true,
blockAt: options.blockAt ?? 0.8,
hitlAt: options.hitlAt ?? 0.5
};
}
/** @inheritdoc */
scan(input, _ctx) {
input = coerceScanInput(input);
const matches = [];
const reasons = [];
let sanitized = input;
for (const { def: entry, re } of this.compiledPatterns) {
let m;
re.lastIndex = 0;
while ((m = re.exec(input)) !== null) {
const token = m[1] ?? m[0];
if (entry.entropyCheck) {
const entropy = shannonEntropy(token);
if (entropy < this.options.minEntropy) {
if (m[0].length === 0) break;
continue;
}
}
matches.push({
pattern: entry.id,
startIndex: m.index,
endIndex: m.index + m[0].length,
// category carries the pattern family, not severity, so callers can
// use it for grouping/labelling (e.g. "stripe", "aws") rather than
// the risk tier.
category: entry.family ?? secretFamily(entry.id)
});
if (!reasons.includes(entry.name)) {
reasons.push(entry.name);
}
if (m[0].length === 0) break;
}
}
if (matches.length === 0) {
return allowResult(this.name, input);
}
if (this.options.redact) {
sanitized = this.redactInput(input, matches);
}
const score = this.options.blockOnAny ? 1 : Math.min(1, matches.length / 5);
const result = blockResult(
this.name,
sanitized,
score,
reasons,
matches,
this.options.blockAt,
this.options.hitlAt
);
return { ...result, sanitized };
}
redactInput(input, matches) {
const deduped = deduplicateMatches(matches);
const sorted = [...deduped].sort((a, b) => b.endIndex - a.endIndex || b.startIndex - a.startIndex);
let result = input;
for (const m of sorted) {
const original = input.slice(m.startIndex, m.endIndex);
const typeLabel = m.category.toUpperCase();
const vault = this.options.vault;
const placeholder = vault ? vault.store(typeLabel, original) : "[REDACTED]";
result = result.slice(0, m.startIndex) + placeholder + result.slice(m.endIndex);
}
return result;
}
};
function secretFamily(id) {
const parts = id.split("-");
if (parts.length >= 3) {
return parts.slice(1, parts.length - 1).join("_").toLowerCase();
}
return id.toLowerCase();
}
function deduplicateMatches(matches) {
const sorted = [...matches].sort((a, b) => a.startIndex - b.startIndex || b.endIndex - a.endIndex);
const result = [];
let lastEnd = -1;
for (const m of sorted) {
if (m.startIndex >= lastEnd) {
result.push(m);
lastEnd = m.endIndex;
} else if (m.endIndex > lastEnd && result.length > 0) {
result[result.length - 1] = m;
lastEnd = m.endIndex;
}
}
return result;
}
function scanSecrets(input, options) {
return new SecretsScanner(options).scan(input);
}
// src/scanners/patterns/pii-patterns.ts
function luhnCheck(value) {
const digits = value.replace(/[\s\-]/g, "");
if (!/^\d+$/.test(digits)) return false;
let sum = 0;
let shouldDouble = false;
for (let i = digits.length - 1; i >= 0; i--) {
let d = parseInt(digits[i], 10);
if (shouldDouble) {
d *= 2;
if (d > 9) d -= 9;
}
sum += d;
shouldDouble = !shouldDouble;
}
return sum % 10 === 0;
}
function ibanCheck(value) {
const cleaned = value.replace(/[\s\-]/g, "").toUpperCase();
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/.test(cleaned)) return false;
const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
const numeric = rearranged.split("").map((c) => {
const code = c.charCodeAt(0);
return code >= 65 && code <= 90 ? String(code - 55) : c;
}).join("");
let remainder = BigInt(0);
for (const ch of numeric) {
remainder = (remainder * BigInt(10) + BigInt(parseInt(ch, 10))) % BigInt(97);
}
return remainder === BigInt(1);
}
var EMAIL_PATTERNS = [
{
id: "PII-EMAIL-001",
type: "email",
// RFC-5321 compliant enough for practical use
pattern: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/
}
];
var PHONE_PATTERNS = [
{
id: "PII-PHONE-001",
type: "phone",
// E.164 format
pattern: /\+[1-9][0-9]{6,14}\b/
},
{
id: "PII-PHONE-002",
type: "phone",
// US/CA NANP: (XXX) XXX-XXXX or XXX-XXX-XXXX or XXX.XXX.XXXX
pattern: /(?:\+1[\s\-.]?)?\(?[2-9][0-9]{2}\)?[\s\-.]?[2-9][0-9]{2}[\s\-.]?[0-9]{4}\b/
},
{
id: "PII-PHONE-003",
type: "phone",
// UK phone: 07xxx xxxxxx or +44 7xxx xxxxxx
pattern: /(?:\+44|0)[0-9]{2,4}[\s\-]?[0-9]{3,4}[\s\-]?[0-9]{4}\b/
}
];
var SSN_PATTERNS = [
{
id: "PII-SSN-001",
type: "ssn",
// US Social Security Number (not 000, not 666, not 900-999)
pattern: /\b(?!000|666|9[0-9][0-9])[0-9]{3}[-\s](?!00)[0-9]{2}[-\s](?!0000)[0-9]{4}\b/
},
{
id: "PII-ITIN-001",
type: "ssn",
// US Individual Taxpayer Identification Number (9XX-7X-XXXX or 9XX-8X-XXXX)
pattern: /\b9[0-9]{2}[-\s](?:7[0-9]|8[0-8])[-\s][0-9]{4}\b/
}
];
var CREDIT_CARD_PATTERNS = [
{
id: "PII-CC-001",
type: "credit_card",
// Visa / Mastercard / Amex / Discover — 13-19 digits with optional separators
pattern: /\b(?:4[0-9]{3}|5[1-5][0-9]{2}|3[47][0-9]{2}|6(?:011|5[0-9]{2}))[0-9 \-]{8,15}[0-9]\b/,
validator: (match) => luhnCheck(match)
},
{
id: "PII-CC-002",
type: "credit_card",
// Generic 16-digit card with separators
pattern: /\b[0-9]{4}[\ \-]?[0-9]{4}[\ \-]?[0-9]{4}[\ \-]?[0-9]{4}\b/,
validator: (match) => luhnCheck(match)
},
{
id: "PII-CC-003",
type: "credit_card",
// Amex: 15-digit, starts with 34 or 37
pattern: /\b3[47][0-9]{2}[\s\-]?[0-9]{6}[\s\-]?[0-9]{5}\b/,
validator: (match) => luhnCheck(match)
}
];
var IBAN_PATTERNS = [
{
id: "PII-IBAN-001",
type: "iban",
// Standard IBAN format: 2-letter country, 2 check digits, up to 30 alphanumeric
pattern: /\b[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}(?:[A-Z0-9]{0,16})\b/,
validator: (match) => ibanCheck(match)
},
{
id: "PII-IBAN-002",
type: "iban",
// IBAN with spaces (printed format: groups of 4)
pattern: /\b[A-Z]{2}[0-9]{2}(?:\s[A-Z0-9]{4}){2,7}\b/,
validator: (match) => ibanCheck(match)
}
];
var IP_PATTERNS = [
{
id: "PII-IPV4-001",
type: "ip",
// IPv4 (non-private ranges included — scanner user decides what to block)
pattern: /\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/
},
{
id: "PII-IPV6-001",
type: "ip",
// Full IPv6
pattern: /\b(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}\b/
},
{
id: "PII-IPV6-002",
type: "ip",
// Compressed IPv6 (e.g. 2001:db8::1)
pattern: /\b(?:[A-Fa-f0-9]{1,4}:){1,7}:\b|\b:[A-Fa-f0-9]{1,4}(?::[A-Fa-f0-9]{1,4}){1,7}\b/
}
];
var MAC_PATTERNS = [
{
id: "PII-MAC-001",
type: "mac",
// Colon-separated MAC
pattern: /\b(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}\b/
},
{
id: "PII-MAC-002",
type: "mac",
// Hyphen-separated MAC
pattern: /\b(?:[0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}\b/
}
];
var POSTCODE_PATTERNS = [
{
id: "PII-ZIP-001",
type: "postcode",
// US ZIP code (5 digit or ZIP+4)
pattern: /\b[0-9]{5}(?:-[0-9]{4})?\b/
},
{
id: "PII-UKPOST-001",
type: "postcode",
// UK postcode
pattern: /\b[A-Z]{1,2}[0-9][0-9A-Z]?\s?[0-9][A-Z]{2}\b/i
},
{
id: "PII-CAPOST-001",
type: "postcode",
// Canadian postal code: A1A 1A1
pattern: /\b[A-CEGHJKLMNPRSTVXYa-cegjklmnprstvxy][0-9][A-Za-z]\s?[0-9][A-Za-z][0-9]\b/
}
];
var SIN_PATTERNS = [
{
id: "PII-SIN-001",
type: "other",
// Canadian Social Insurance Number: NNN NNN NNN (first digit 1-9, not 0)
pattern: /\b[1-9][0-9]{2}[\s\-][0-9]{3}[\s\-][0-9]{3}\b/
}
];
var PASSPORT_PATTERNS = [
{
id: "PII-PASSPORT-US-001",
type: "passport",
// US Passport: 9 digits (newer) or letter + 8 digits
pattern: /\b(?:[A-Z][0-9]{8}|[0-9]{9})\b/
},
{
id: "PII-PASSPORT-UK-001",
type: "passport",
// UK Passport: 9 digits
pattern: /\b[0-9]{9}\b/
},
{
id: "PII-PASSPORT-GENERIC-001",
type: "passport",
// Generic MRZ-style: 2 letters + 6 alphanumeric
pattern: /\b[A-Z]{2}[A-Z0-9]{6,7}\b/
}
];
var DOB_PATTERNS = [
{
id: "PII-DOB-001",
type: "dob",
// ISO format: YYYY-MM-DD
pattern: /\b(?:19|20)[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])\b/
},
{
id: "PII-DOB-002",
type: "dob",
// US format: MM/DD/YYYY
pattern: /\b(?:0[1-9]|1[0-2])\/(?:0[1-9]|[12][0-9]|3[01])\/(?:19|20)[0-9]{2}\b/
},
{
id: "PII-DOB-003",
type: "dob",
// EU format: DD.MM.YYYY
pattern: /\b(?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[0-2])\.(?:19|20)[0-9]{2}\b/
},
{
id: "PII-DOB-004",
type: "dob",
// Written "born on", "date of birth", "dob" keyword patterns
pattern: /(?:born(?:\s+on)?|date\s+of\s+birth|dob)\s*[:\-]?\s*(?:0?[1-9]|[12][0-9]|3[01])[\s\/\-](?:0?[1-9]|1[0-2])[\s\/\-](?:19|20)[0-9]{2}/i
}
];
var PII_PATTERNS = [
...EMAIL_PATTERNS,
...PHONE_PATTERNS,
...SSN_PATTERNS,
...CREDIT_CARD_PATTERNS,
...IBAN_PATTERNS,
...IP_PATTERNS,
...MAC_PATTERNS,
...POSTCODE_PATTERNS,
...SIN_PATTERNS,
...PASSPORT_PATTERNS,
...DOB_PATTERNS
];
// src/scanners/pii.ts
var PiiScanner = class {
constructor(options = {}) {
/** @inheritdoc */
this.name = "pii";
this.patterns = [...PII_PATTERNS, ...options.customPatterns ?? []];
this.options = {
redact: options.redact ?? false,
vault: options.vault,
blockOnAny: options.blockOnAny ?? true,
blockAt: options.blockAt ?? 0.8,
hitlAt: options.hitlAt ?? 0.5
};
}
/** @inheritdoc */
scan(input, _ctx) {
input = coerceScanInput(input);
const matches = [];
const reasons = [];
let sanitized = input;
for (const entry of this.patterns) {
const flags = entry.pattern.flags.includes("g") ? entry.pattern.flags : entry.pattern.flags + "g";
const re = new RegExp(entry.pattern.source, flags);
let m;
while ((m = re.exec(input)) !== null) {
const matched = m[0];
if (entry.validator && !entry.validator(matched)) {
if (matched.length === 0) break;
continue;
}
matches.push({
pattern: entry.id,
startIndex: m.index,
endIndex: m.index + matched.length,
category: entry.type
});
const label = `${entry.type.toUpperCase()} (${entry.id})`;
if (!reasons.includes(label)) {
reasons.push(label);
}
if (matched.length === 0) break;
}
}
if (matches.length === 0) {
return allowResult(this.name, input);
}
if (this.options.redact) {
sanitized = this.redactInput(input, matches);
}
const score = this.options.blockOnAny ? 1 : Math.min(1, matches.length / 5);
const result = blockResult(
this.name,
sanitized,
score,
reasons,
matches,
this.options.blockAt,
this.options.hitlAt
);
return { ...result, sanitized };
}
redactInput(input, matches) {
const deduped = deduplicateMatches2(matches);
const sorted = [...deduped].sort((a, b) => b.startIndex - a.startIndex);
let result = input;
for (const m of sorted) {
const original = input.slice(m.startIndex, m.endIndex);
const typeLabel = m.category.toUpperCase();
const vault = this.options.vault;
const placeholder = vault ? vault.store(typeLabel, original) : `[REDACTED_${typeLabel}]`;
result = result.slice(0, m.startIndex) + placeholder + result.slice(m.endIndex);
}
return result;
}
};
function deduplicateMatches2(matches) {
const sorted = [...matches].sort((a, b) => a.startIndex - b.startIndex || b.endIndex - a.endIndex);
const result = [];
let lastEnd = -1;
for (const m of sorted) {
if (m.startIndex >= lastEnd) {
result.push(m);
lastEnd = m.endIndex;
} else if (m.endIndex > lastEnd && result.length > 0) {
result[result.length - 1] = m;
lastEnd = m.endIndex;
}
}
return result;
}
function scanPii(input, options) {
return new PiiScanner(options).scan(input);
}
// src/scanners/vault.ts
function levenshtein(a, b) {
const m = a.length;
const n = b.length;
const dp = Array.from(
{ length: m + 1 },
(_, i) => Array.from({ length: n + 1 }, (__, j) => i === 0 ? j : j === 0 ? i : 0)
);
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (a[i - 1] === b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
}
return dp[m][n];
}
var Vault = class {
constructor() {
this.entries = [];
/** Counters per type so each new entry gets a unique sequential number. */
this.counters = /* @__PURE__ */ new Map();
}
/**
* Store an original value and return a unique placeholder string.
*
* @param type - PII/secret type label (will be upper-cased), e.g. "email".
* @param original - The raw sensitive value to redact.
* @returns The placeholder token, e.g. "[REDACTED_EMAIL_1]".
*/
store(type, original) {
const normalizedType = type.toUpperCase().replace(/[^A-Z0-9]/g, "_");
const count = (this.counters.get(normalizedType) ?? 0) + 1;
this.counters.set(normalizedType, count);
const placeholder = `[REDACTED_${normalizedType}_${count}]`;
this.entries.push({ placeholder, original, type: normalizedType });
return placeholder;
}
/**
* Replace all placeholders in `text` with their original values.
*
* @param text - The sanitized text containing placeholder tokens.
* @param strategy - How to match placeholders (default: `'exact'`).
* @returns The restored text with originals substituted back.
*/
restore(text, strategy = "exact") {
let result = text;
for (const entry of this.entries) {
switch (strategy) {
case "exact":
result = result.split(entry.placeholder).join(entry.original);
break;
case "caseInsensitive": {
const re = new RegExp(escapeRegex(entry.placeholder), "gi");
result = result.replace(re, entry.original);
break;
}
case "fuzzy": {
result = fuzzyReplace(result, entry.placeholder, entry.original, 3);
break;
}
case "combined": {
if (result.includes(entry.placeholder)) {
result = result.split(entry.placeholder).join(entry.original);
} else {
const reCI = new RegExp(escapeRegex(entry.placeholder), "gi");
if (reCI.test(result)) {
result = result.replace(reCI, entry.original);
} else {
result = fuzzyReplace(result, entry.placeholder, entry.original, 3);
}
}
break;
}
}
}
return result;
}
/** Remove all stored entries and reset counters. */
clear() {
this.entries = [];
this.counters = /* @__PURE__ */ new Map();
}
/** Number of entries currently stored. */
size() {
return this.entries.length;
}
/**
* Retrieve a snapshot of all current entries (read-only copy).
* Useful for serialisation or inspection.
*/
getEntries() {
return this.entries.map((e) => ({ ...e }));
}
};
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function fuzzyReplace(text, needle, replacement, tolerance) {
if (needle.length === 0) return text;
const n = needle.length;
let result = "";
let i = 0;
while (i < text.length) {
const window = text.slice(i, i + n);
if (window.length < Math.floor(n / 2)) {
result += text.slice(i);
break;
}
if (window[0] !== needle[0]) {
result += text[i];
i++;
continue;
}
const dist = levenshtein(window, needle);
if (dist <= tolerance) {
result += replacement;
i += window.length;
} else {
result += text[i];
i++;
}
}
return result;
}
// src/scanners/composite.ts
var ALL_SCANNERS = ["prompt_injection", "secrets", "pii"];
var DECISION_RANK = {
ALLOW: 0,
HITL: 1,
BLOCK: 2
};
function scanAll(text, options) {
const requested = options?.scanners && options.scanners.length > 0 ? options.scanners : ALL_SCANNERS;
const unknown = requested.filter((s) => !ALL_SCANNERS.includes(s));
if (unknown.length > 0) {
throw new Error(
`Unknown scanner(s): ${unknown.join(", ")}. Valid names: ${ALL_SCANNERS.join(", ")}`
);
}
const active = new Set(requested);
const vault = options?.vault;
const redact = vault !== void 0 && vault !== null;
const results = [];
for (const name of ALL_SCANNERS) {
if (!active.has(name)) continue;
if (name === "prompt_injection") {
const scanner = new PromptInjectionScanner();
results.push(scanner.scan(text));
} else if (name === "secrets") {
const scanner = new SecretsScanner({ redact, vault });
results.push(scanner.scan(text));
} else if (name === "pii") {
const scanner = new PiiScanner({ redact, vault });
results.push(scanner.scan(text));
}
}
return results;
}
function worstDecision(results) {
if (results.length === 0) return "ALLOW";
return results.reduce(
(worst, r) => DECISION_RANK[r.decision] > DECISION_RANK[worst] ? r.decision : worst,
"ALLOW"
);
}
exports.INJECTION_PATTERNS = INJECTION_PATTERNS;
exports.PII_PATTERNS = PII_PATTERNS;
exports.PiiScanner = PiiScanner;
exports.PromptInjectionScanner = PromptInjectionScanner;
exports.SECRET_PATTERNS = SECRET_PATTERNS;
exports.SecretsScanner = SecretsScanner;
exports.Vault = Vault;
exports.allowResult = allowResult;
exports.blockResult = blockResult;
exports.checkPromptInjection = checkPromptInjection;
exports.defaultPromptInjectionScanner = defaultPromptInjectionScanner;
exports.ibanCheck = ibanCheck;
exports.luhnCheck = luhnCheck;
exports.scanAll = scanAll;
exports.scanPii = scanPii;
exports.scanSecrets = scanSecrets;
exports.worstDecision = worstDecision;