Class: Vangrail::Rails::PersonalData
- Inherits:
-
Vangrail::Rail
- Object
- Vangrail::Rail
- Vangrail::Rails::PersonalData
- 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
-
#patterns ⇒ Object
readonly
Returns the value of attribute patterns.
-
#placeholder ⇒ Object
readonly
Returns the value of attribute placeholder.
Instance Method Summary collapse
- #cache_key(text, _context) ⇒ Object
- #call(text, _context) ⇒ Object
-
#initialize(patterns: PATTERNS, placeholder: PLACEHOLDER, name: 'personal_data', sides: [:input]) ⇒ PersonalData
constructor
A new instance of PersonalData.
- #language_agnostic? ⇒ Boolean
- #offline? ⇒ Boolean
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
#patterns ⇒ Object (readonly)
Returns the value of attribute patterns.
74 75 76 |
# File 'lib/vangrail/rails/personal_data.rb', line 74 def patterns @patterns end |
#placeholder ⇒ Object (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
87 88 89 |
# File 'lib/vangrail/rails/personal_data.rb', line 87 def language_agnostic? true end |
#offline? ⇒ Boolean
83 84 85 |
# File 'lib/vangrail/rails/personal_data.rb', line 83 def offline? true end |