class CreditCardSanitizer
Constants
- ACCEPTED_POSTFIX
- ACCEPTED_PREFIX
- ALPHANUMERIC
- CARD_COMPANIES
- CARD_NUMBER_GROUPINGS
- Candidate
- DEFAULT_OPTIONS
- EXPIRATION_DATE
- LINE_NOISE
- LINE_NOISE_CHAR
- NONEMPTY_LINE_NOISE
- NUMBERS_WITH_LINE_NOISE
- SCHEME_OR_PLUS
- VALID_COMPANY_PREFIXES
Attributes
Public Class Methods
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.
# File lib/credit_card_sanitizer.rb, line 82 def initialize(options = {}) @settings = DEFAULT_OPTIONS.merge(options) end
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.
# 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
Public Instance Methods
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.
# 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
Private Instance Methods
# File lib/credit_card_sanitizer.rb, line 163 def find_company(numbers) CARD_COMPANIES.each do |company, pattern| return company if pattern.match?(numbers) end end
# File lib/credit_card_sanitizer.rb, line 208 def redact_numbers(candidate, options) candidate.text.gsub(/\d/).with_index do |number, digit_index| if within_redaction_range?(candidate, digit_index, options) options[:replacement_token] else number end end end
# File lib/credit_card_sanitizer.rb, line 186 def tracking?(candidate, options) options[:exclude_tracking_numbers] && TrackingNumber.new(candidate.numbers).valid? end
# File lib/credit_card_sanitizer.rb, line 159 def valid_company_prefix?(numbers) !!(numbers =~ VALID_COMPANY_PREFIXES) end
# File lib/credit_card_sanitizer.rb, line 194 def valid_context?(candidate, options) !options[:parse_flanking] || valid_prefix?(candidate.prefix) && valid_postfix?(candidate.postfix) end
# File lib/credit_card_sanitizer.rb, line 169 def valid_grouping?(candidate, options) if options[:use_groupings] if (company = find_company(candidate.numbers)) groupings = candidate.text.split(NONEMPTY_LINE_NOISE).map(&:length) return true if groupings.length == 1 if (company_groupings = CARD_NUMBER_GROUPINGS[company]) company_groupings.each do |company_grouping| return true if groupings.take(company_grouping.length) == company_grouping end end end false else true end end
# File lib/credit_card_sanitizer.rb, line 190 def valid_numbers?(candidate, options) LuhnChecksum.valid?(candidate.numbers) && valid_company_prefix?(candidate.numbers) && valid_grouping?(candidate, options) && !tracking?(candidate, options) end
# File lib/credit_card_sanitizer.rb, line 203 def valid_postfix?(postfix) return true if postfix.nil? || !!ACCEPTED_POSTFIX.match(postfix) !ALPHANUMERIC.match(postfix[0]) end
# File lib/credit_card_sanitizer.rb, line 198 def valid_prefix?(prefix) return true if prefix.nil? || !!ACCEPTED_PREFIX.match(prefix) !ALPHANUMERIC.match(prefix[-1]) end
# File lib/credit_card_sanitizer.rb, line 218 def within_redaction_range?(candidate, digit_index, options) digit_index >= options[:expose_first] && digit_index < candidate.numbers.size - options[:expose_last] end
# File lib/credit_card_sanitizer.rb, line 222 def without_expiration(text) expiration_date_boundary = SecureRandom.hex.tr("0123456789", "ABCDEFGHIJ") text.gsub!(EXPIRATION_DATE) do |expiration_date| match = expiration_date.match(/(?<whitespace>\s*)(?<rest>.*)/m) "#{match[:whitespace]}#{expiration_date_boundary}#{match[:rest]}#{expiration_date_boundary}" end yield text.gsub!(expiration_date_boundary, "") end