Class: PromptSanitizer::Engines::RegexEngine

Inherits:
Object
  • Object
show all
Defined in:
lib/prompt_sanitizer/engines/regex_engine.rb

Overview

Regex Engine — Layer 1 of prompt-sanitizer.

Detects structured PII (email, phone, SSN, credit cards, IBANs, IPs, crypto addresses, MAC addresses, URLs, passport numbers, driving licences, and date patterns) using regular expressions with optional checksum validation (Luhn for credit cards, IBAN mod-97).

All patterns run on every sanitize() call regardless of Mode.

Defined Under Namespace

Classes: Pattern

Constant Summary collapse

PATTERNS =

── Pattern registry ─────────────────────────────────────────────────────

[
  # ── Email ──────────────────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::EMAIL,
    regex:       /(?<![a-zA-Z0-9._%+\-])[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]{1,253}\.[a-zA-Z]{2,}(?![a-zA-Z0-9._%+\-@])/i,
    confidence:  0.99
  ),

  # ── US phone ───────────────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::PHONE,
    regex:       /(?<!\d)(?:\+?1[\s.\-]?)?(?:\([2-9]\d{2}\)|[2-9]\d{2})[\s.\-]?\d{3}[\s.\-]?\d{4}(?!\d)/,
    confidence:  0.85
  ),

  # ── International phone — compact E.164 e.g. +447946123456 ────────────
  Pattern.new(
    entity_type: EntityType::PHONE,
    regex:       /(?<!\d)\+[1-9]\d{6,14}(?!\d)/,
    confidence:  0.80
  ),

  # ── International phone — spaced/dashed e.g. +44 20 7946 0958 ─────────
  Pattern.new(
    entity_type: EntityType::PHONE,
    regex:       /(?<!\d)\+[1-9]\d{0,3}(?:[\s.\-]\d{2,4}){2,4}(?!\d)/,
    confidence:  0.78
  ),

  # ── US SSN ─────────────────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::SSN,
    regex:       /(?<!\d)(?!000|666|9\d{2})\d{3}[\s\-](?!00)\d{2}[\s\-](?!0000)\d{4}(?!\d)/,
    confidence:  0.95
  ),

  # ── Credit / debit card (Luhn-validated) ──────────────────────────────
  Pattern.new(
    entity_type: EntityType::CREDIT_CARD,
    regex:       /(?<!\d)(?:4[0-9]{3}|5[1-5][0-9]{2}|3[47][0-9]{2}|3(?:0[0-5]|[68][0-9])[0-9]|6(?:011|5[0-9]{2})|(?:2131|1800|35\d{3}))[\s\-]?(?:\d{4}[\s\-]?){2}\d{1,4}(?!\d)/,
    confidence:  0.95,
    validator:   ->(m) { luhn_valid?(m) }
  ),

  # ── IBAN (mod-97 validated) ────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::IBAN,
    regex:       /\b[A-Z]{2}\d{2}(?:\s?[A-Z0-9]{4}){2,7}\s?[A-Z0-9]{1,4}\b/i,
    confidence:  0.92,
    validator:   ->(m) { iban_valid?(m) }
  ),

  # ── IPv4 ───────────────────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::IP_ADDRESS,
    regex:       /(?<!\d)(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?!\d)/,
    confidence:  0.90
  ),

  # ── IPv6 (full and compressed forms) ──────────────────────────────────
  Pattern.new(
    entity_type: EntityType::IP_ADDRESS,
    regex:       /(?<![:\w])(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}|[0-9a-fA-F]{1,4}::(?:[0-9a-fA-F]{1,4}:){0,4}[0-9a-fA-F]{1,4})(?![:\w])/i,
    confidence:  0.90
  ),

  # ── MAC address ────────────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::MAC_ADDRESS,
    regex:       /(?<![:\w])(?:[0-9a-fA-F]{2}[:\-]){5}[0-9a-fA-F]{2}(?![:\w])/i,
    confidence:  0.90
  ),

  # ── URL (http/https) ───────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::URL,
    regex:       /https?:\/\/(?:[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]|(?:%[0-9a-fA-F]{2}))+/i,
    confidence:  0.85
  ),

  # ── Bitcoin address (P2PKH, P2SH, Bech32) ─────────────────────────────
  Pattern.new(
    entity_type: EntityType::CRYPTO_ADDRESS,
    regex:       /(?<![a-zA-Z0-9])(?:[13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87})(?![a-zA-Z0-9])/,
    confidence:  0.88
  ),

  # ── Ethereum address ───────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::CRYPTO_ADDRESS,
    regex:       /(?<![a-fA-F0-9])0x[0-9a-fA-F]{40}(?![0-9a-fA-F])/,
    confidence:  0.92
  ),

  # ── US Passport ────────────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::PASSPORT,
    regex:       /(?<![A-Z0-9])[A-Z]{1,2}\d{7,9}(?![A-Z0-9])/,
    confidence:  0.72
  ),

  # ── US ZIP code ────────────────────────────────────────────────────────
  Pattern.new(
    entity_type: EntityType::ZIP_CODE,
    regex:       /(?<!\d)\d{5}(?:-\d{4})?(?!\d)/,
    confidence:  0.55
  ),

  # ── Date patterns (DD/MM/YYYY, YYYY-MM-DD, Month DD YYYY, etc.) ────────
  Pattern.new(
    entity_type: EntityType::DATE,
    regex:       /(?<!\d)(?:\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4}|\d{4}[\/\-\.]\d{1,2}[\/\-\.]\d{1,2}|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},?\s+\d{4}|\d{1,2}\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{4})(?!\d)/i,
    confidence:  0.75
  ),
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeRegexEngine

── Instance ─────────────────────────────────────────────────────────────



168
169
170
# File 'lib/prompt_sanitizer/engines/regex_engine.rb', line 168

def initialize
  @patterns = PATTERNS.dup
end

Class Method Details

.iban_valid?(iban) ⇒ Boolean

IBAN mod-97 validation.

Returns:

  • (Boolean)


37
38
39
40
41
42
43
44
45
46
# File 'lib/prompt_sanitizer/engines/regex_engine.rb', line 37

def self.iban_valid?(iban)
  raw = iban.gsub(/[\s\-]/, "").upcase
  return false unless raw.length.between?(15, 34)

  rearranged = raw[4..] + raw[0..3]
  numeric    = rearranged.chars.map { |c| c =~ /[A-Z]/ ? (c.ord - 55).to_s : c }.join
  numeric.to_i % 97 == 1
rescue ArgumentError
  false
end

.luhn_valid?(card) ⇒ Boolean

Luhn algorithm — validates credit/debit card numbers.

Returns:

  • (Boolean)


19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/prompt_sanitizer/engines/regex_engine.rb', line 19

def self.luhn_valid?(card)
  digits = card.gsub(/\D/, "").chars.map(&:to_i)
  return false if digits.size < 13

  total = digits.reverse.each_with_index.sum do |d, i|
    i.odd? ? [d * 2 - 9, d * 2].min + (d * 2 >= 10 ? 0 : 0) : d
    # Standard Luhn: double every second digit from the right
    if i.odd?
      doubled = d * 2
      doubled > 9 ? doubled - 9 : doubled
    else
      d
    end
  end
  total % 10 == 0
end

Instance Method Details

#add_pattern(entity_type, regex, confidence: 0.80, validator: nil) ⇒ Object

Register a custom regex pattern at runtime.

engine.add_pattern(:custom, /\bACME-\d{6}\b/, confidence: 0.90)
engine.add_pattern(:custom, "ACME-\\d{6}", confidence: 0.90)


176
177
178
179
180
181
182
183
184
# File 'lib/prompt_sanitizer/engines/regex_engine.rb', line 176

def add_pattern(entity_type, regex, confidence: 0.80, validator: nil)
  regex = Regexp.new(regex) if regex.is_a?(String)
  @patterns << Pattern.new(
    entity_type: entity_type,
    regex:       regex,
    confidence:  confidence,
    validator:   validator
  )
end

#detect(text) ⇒ Object

Run all patterns against text and return an Array of DetectedEntity. Overlapping matches from different patterns are kept — deduplication happens in the Sanitizer, which has the full multi-engine view.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/prompt_sanitizer/engines/regex_engine.rb', line 189

def detect(text)
  safe_text = text.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
  entities  = []

  @patterns.each do |pat|
    safe_text.scan(pat.regex) do
      m     = Regexp.last_match
      value = m[0]

      next if pat.validator && !self.class.instance_exec(value, &pat.validator)

      entities << DetectedEntity.new(
        entity_type: pat.entity_type,
        original:    value,
        replacement: nil,
        start_pos:   m.begin(0),
        end_pos:     m.end(0),
        confidence:  pat.confidence,
        layer:       :regex
      )
    end
  end

  entities
end