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 73 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 135 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
Returns a String of the redacted text if a credit card number was detected. Returns nil if no credit card numbers were detected.
# File lib/credit_card_sanitizer.rb, line 93 def sanitize!(text, options = {}) options = @settings.merge(options) text.force_encoding(Encoding::UTF_8) text.scrub!('�') redacted = 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) redacted = true redact_numbers(candidate, options) else match end end end redacted && text end
Private Instance Methods
# File lib/credit_card_sanitizer.rb, line 145 def find_company(numbers) CARD_COMPANIES.each do |company, pattern| return company if numbers =~ pattern end end
# File lib/credit_card_sanitizer.rb, line 190 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 168 def tracking?(candidate, options) options[:exclude_tracking_numbers] && TrackingNumber.new(candidate.numbers).valid? end
# File lib/credit_card_sanitizer.rb, line 141 def valid_company_prefix?(numbers) !!(numbers =~ VALID_COMPANY_PREFIXES) end
# File lib/credit_card_sanitizer.rb, line 176 def valid_context?(candidate, options) !options[:parse_flanking] || valid_prefix?(candidate.prefix) && valid_postfix?(candidate.postfix) end
# File lib/credit_card_sanitizer.rb, line 151 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 172 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 185 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 180 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 200 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 204 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