UNPKG

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
'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;