Class: CreditCardSanitizer

Inherits:
Object
  • Object
show all
Defined in:
lib/credit_card_sanitizer.rb

Defined Under Namespace

Classes: Candidate

Constant Summary collapse

CARD_COMPANIES =
{
  "visa" => /^4\d{12}(\d{3})?(\d{3})?$/,
  "master" => /^(5[1-5]\d{4}|677189|222[1-9]\d{2}|22[3-9]\d{3}|2[3-6]\d{4}|27[01]\d{3}|2720\d{2})\d{10}$/,
  "discover" => /^((6011\d{12})|(65[4-9]\d{13})|(64[4-9]\d{13})|(622(?:12[6-9]|1[3-9]\d|[2-8]\d{2}|9[01]\d|92[0-5])\d{10}))$/,
  "american_express" => /^3[47]\d{13}$/,
  "diners_club" => /^3(0[0-5]|[68]\d)\d{11}$/,
  "jcb" => /^35(28|29|[3-8]\d)\d{12}$/,
  "switch" => /^(6759\d{12}(\d{2,3})?|(4903|4905|4911|4936|6333|6759)\d{12}|(4903|4905|4911|4936|6333|6759)\d{14}|(4903|4905|4911|4936|6333|6759)\d{15}|564182\d{10}|564182\d{12}|564182\d{13}|633110\d{10}|633110\d{12}|633110\d{13})$/,
  "solo" => /^(6767\d{12}(\d{2,3})?|6334\d{12}|6334\d{14}|6334\d{15}|6767\d{14}|6767\d{15})$/,
  "dankort" => /^5019\d{12}$/,
  "maestro" => /^(5[06-8]\d{10,17}|6\d\d{10,17}|5018|5020|5038|5893|6304|6759|6761|6762|6763\d{8,15})$/,
  "forbrugsforeningen" => /^600722\d{10}$/,
  "laser" => /^(6304|6706|6709|6771(?!89))(\d{12,15}|\d{8}(\d{4}|\d{6,7})?)$/,
  "bc_global" => /^(6541|6556)\d{12}$/,
  "carte_blanche" => /^389\d{11}$/,
  "insta_payment" => /^63[7-9]\d{13}$/,
  "korean_local" => /^9\d{15}$/,
  "union_pay" => /^62\d{14,17}$/,
  "visa_master" => /^(4\d{12}(\d{3})?|5[1-5]\d{14})$/
}.freeze
CARD_NUMBER_GROUPINGS =
{
  "visa" => [[4, 4, 4, 4]],
  "master" => [[4, 4, 4, 4]],
  "discover" => [[4, 4, 4, 4]],
  "american_express" => [[4, 6, 5]],
  "diners_club" => [[4, 6, 4]],
  "jcb" => [[4, 4, 4, 4]],
  "switch" => [[4, 4, 4, 4]],
  "solo" => [[4, 4, 4, 4], [4, 4, 4, 4, 2], [4, 4, 4, 4, 3]],
  "dankort" => [[4, 4, 4, 4]],
  "maestro" => [[4], [5], [4, 4, 4, 4], [4, 4, 4, 4, 1], [4, 4, 4, 4, 2], [4, 4, 4, 4, 3]],
  "forbrugsforeningen" => [[4, 4, 4, 4]],
  "laser" => [[4, 4, 4, 4], [4, 4, 4, 4, 1], [4, 4, 4, 4, 2], [4, 4, 4, 4, 3]],
  "bc_global" => [[4, 4, 4, 4]],
  "carte_blanche" => [[4, 6, 4]],
  "insta_payment" => [[4, 4, 4, 4]],
  "korean_local" => [[4, 4, 4, 4]],
  "union_pay" => [[4, 4, 4, 4], [4, 4, 4, 4, 1], [4, 4, 4, 4, 2], [4, 4, 4, 4, 3]],
  "visa_master" => [[4, 4, 4, 4], [4, 4, 4, 4, 3]]
}.freeze
ACCEPTED_PREFIX =
/(?:cc|card|visa|amex)\z/i
ACCEPTED_POSTFIX =
/\Aex/i
ALPHANUMERIC =
/[[:alnum:]]/i
VALID_COMPANY_PREFIXES =
Regexp.union(*CARD_COMPANIES.values)
EXPIRATION_DATE =
/\s(?:0?[1-9]|1[0-2])(?:\/|-)(?:\d{4}|\d{2})(?:\D|$)/
LINE_NOISE_CHAR =
/[^\w\n,()&.\/:;<>]/
LINE_NOISE =
/#{LINE_NOISE_CHAR}{,5}/
NONEMPTY_LINE_NOISE =
/#{LINE_NOISE_CHAR}{1,5}/
SCHEME_OR_PLUS =
/((?:&#43;|\+|\/)|(?:[a-zA-Z][-+.a-zA-Z\d]{,9}):[^\s>]+)/
NUMBERS_WITH_LINE_NOISE =
/#{SCHEME_OR_PLUS}?\d(?:#{LINE_NOISE}\d){10,30}/
DEFAULT_OPTIONS =
{
  replacement_token: "▇",
  expose_first: 6,
  expose_last: 4,
  use_groupings: false,
  exclude_tracking_numbers: false,
  parse_flanking: false
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ CreditCardSanitizer

Create a new CreditCardSanitizer

Options

:replacement_character - the character that will replace digits for redaction. :expose_first - the number of leading digits that will not be redacted. :expose_last - the number of ending digits that will not be redacted. :use_groupings - require card number groupings to match to redact. :exclude_tracking_numbers - do not redact valid shipping company tracking numbers.



82
83
84
# File 'lib/credit_card_sanitizer.rb', line 82

def initialize(options = {})
  @settings = DEFAULT_OPTIONS.merge(options)
end

Instance Attribute Details

#settingsObject (readonly)

Returns the value of attribute settings.



68
69
70
# File 'lib/credit_card_sanitizer.rb', line 68

def settings
  @settings
end

Class Method Details

.parameter_filter(options = {}) ⇒ Object

A proc that can be used

text - the text containing potential credit card numbers

Examples

Rails.app.config.filter_parameters = [:password, CreditCardSanitizer.parameter_filter]

env = {
  "action_dispatch.request.parameters" => {"credit_card_number" => "4111 1111 1111 1111", "password" => "123"},
  "action_dispatch.parameter_filter" => Rails.app.config.filter_parameters
}

>> ActionDispatch::Request.new(env).filtered_parameters
=> {"credit_card_number" => "4111 11▇▇ ▇▇▇▇ 1111", "password" => "[FILTERED]"}

Returns a Proc that takes the key/value of the request parameter.



153
154
155
# File 'lib/credit_card_sanitizer.rb', line 153

def self.parameter_filter(options = {})
  proc { |_, value| new(options).sanitize!(value) if value.is_a?(String) }
end

Instance Method Details

#sanitize!(text, options = {}) ⇒ Object

Finds credit card numbers and redacts digits from them

text - the text containing potential credit card numbers

Examples

# If the text contains a credit card number:
sanitize!("4111 1111 1111 1111")
#=> "4111 11▇▇ ▇▇▇▇ 1111"

# If the text does not contain a credit card number:
sanitize!("I want all your credit card numbers!")
#=> nil

If options is false, returns nil if no redaction happened, else the full text after redaction.

If options is true, returns nil if no redaction happened, else an array of [old_text, new_text] indicating what substrings were redacted.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/credit_card_sanitizer.rb', line 105

def sanitize!(text, options = {})
  options = @settings.merge(options)

  text.force_encoding(Encoding::UTF_8)
  text.scrub!("�")
  changes = nil

  without_expiration(text) do
    text.gsub!(NUMBERS_WITH_LINE_NOISE) do |match|
      next match if $1

      candidate = Candidate.new(match, match.tr("^0-9", ""), $`, $')

      if valid_context?(candidate, options) && valid_numbers?(candidate, options)
        redact_numbers(candidate, options).tap do |redacted_text|
          changes ||= []
          changes << [candidate.text, redacted_text]
        end
      else
        match
      end
    end
  end

  if options[:return_changes]
    changes
  else
    changes && text
  end
end