Class: Vangrail::Rails::PersonalData

Inherits:
Vangrail::Rail show all
Defined in:
lib/vangrail/rails/personal_data.rb

Overview

Redacts a reader's own details before the question leaves the building.

This is a privacy rail rather than a security one, and it exists because of where the text goes next. A question typed into a documentation desk is about to be sent to a model endpoint, which may be a third party, may log, and may sit in another jurisdiction. A reader pasting a support thread into it has not thought about any of that, and nothing in the answer needs their phone number.

Redacts rather than blocks, for the same reason the secrets rail does: the question is answerable, and one span in it should not have been sent.

The hard part on a cluster desk is not detection. It is that ssh rgoswami@snellius.example.org is an email address by every syntactic measure, and redacting it destroys the answer to the most commonly asked question there is. So an address is left alone when it is inside backticks or a fence, when its line carries a command that takes a user@host argument or an ssh config keyword, or when a remote path follows it. All three are in the corpus, because a rail that eats login examples is worse for a handbook than no rail at all.

National identity numbers are matched only beside their own name, and the reason is the same false-positive budget. The Dutch BSN is nine digits with a checksum that one number in eleven passes by accident, so a rail reading bare nine-digit runs redacts job ids and project numbers out of cluster questions, which makes it unusable.

A label changes that trade completely. "Mijn BSN is 123456782" carries the word and the checksum, and nothing on a cluster desk writes both by accident; a bare 123456782 keeps passing through untouched. It is the same pair-of-signals rule the rest of this gem uses, and it is what makes the most sensitive identifier a Dutch reader can paste into a support question something this rail can actually catch.

Constant Summary collapse

PLACEHOLDER =
'[redacted]'
HOST_COMMANDS =

Commands whose argument is a login target rather than a mailbox. Read over the line rather than the character before the match: scp puts a source path in between, and an ssh config line has no command on it at all, only the User keyword.

/\b(?:ssh|scp|sftp|rsync|mosh|ssh-copy-id|ssh:\/\/|sftp:\/\/|User)\b/i
REMOTE_PATH =

The other half of scp and rsync syntax: an address followed by a remote path is a target, not a mailbox.

/\A:[~\/\w.]/
PLACEHOLDER_USERS =

Local parts that are documentation rather than a person.

/\A(?:user|username|your[._-]?name|login|account|me|example|
firstname|lastname|name|admin|root)\z/xi
PATTERNS =
{
  'email' => /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/,
  # International or national, with a separator, long enough to be a
  # phone number and not a job id: a leading + or 00, or a leading zero
  # with grouping.
  'phone' => /(?:\+|\b00)[1-9]\d{0,2}[\s.-]?(?:\(?\d{1,4}\)?[\s.-]?){2,5}\d{2,4}\b
              |\b0\d{1,3}[\s.-]\d{3}[\s.-]?\d{3,4}\b/x,
  'iban' => /\b[A-Z]{2}\d{2}\s?(?:[A-Z0-9]{4}\s?){2,7}[A-Z0-9]{1,4}\b/,
  # Separators between the digits rather than after them: the trailing
  # form eats the space before the next word and redacts it away.
  'card' => /\b\d(?:[ -]?\d){12,18}\b/,
  # The number beside its own name, in the words a Dutch reader uses for
  # it. The digits may carry the dots or spaces a form prints them with.
  'bsn' => /\b(?:bsn|burgerservicenummer|sofinummer|sofi[\s-]?nummer)\b
            [^\n]{0,24}?((?:\d[\s.-]?){8}\d)\b/xi,
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(patterns: PATTERNS, placeholder: PLACEHOLDER, name: 'personal_data', sides: [:input]) ⇒ PersonalData

Returns a new instance of PersonalData.



76
77
78
79
80
81
# File 'lib/vangrail/rails/personal_data.rb', line 76

def initialize(patterns: PATTERNS, placeholder: PLACEHOLDER,
               name: 'personal_data', sides: [:input])
  super(name: name, sides: sides)
  @patterns = patterns
  @placeholder = placeholder
end

Instance Attribute Details

#patternsObject (readonly)

Returns the value of attribute patterns.



74
75
76
# File 'lib/vangrail/rails/personal_data.rb', line 74

def patterns
  @patterns
end

#placeholderObject (readonly)

Returns the value of attribute placeholder.



74
75
76
# File 'lib/vangrail/rails/personal_data.rb', line 74

def placeholder
  @placeholder
end

Instance Method Details

#cache_key(text, _context) ⇒ Object



91
92
93
# File 'lib/vangrail/rails/personal_data.rb', line 91

def cache_key(text, _context)
  text
end

#call(text, _context) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/vangrail/rails/personal_data.rb', line 95

def call(text, _context)
  body = text.to_s
  found = []
  redacted = patterns.reduce(body) do |acc, (label, pattern)|
    replace(acc, label, pattern, found)
  end
  return pass if found.empty?

  modify(redacted, categories: found.uniq,
                   reason: "redacted #{found.uniq.join(', ')} before sending")
end

#language_agnostic?Boolean

Returns:

  • (Boolean)


87
88
89
# File 'lib/vangrail/rails/personal_data.rb', line 87

def language_agnostic?
  true
end

#offline?Boolean

Returns:

  • (Boolean)


83
84
85
# File 'lib/vangrail/rails/personal_data.rb', line 83

def offline?
  true
end