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.

Deliberately not included: national identity numbers. The Dutch BSN is nine digits with a checksum, a Slurm job id is six to eight digits, and one in eleven job ids passes the checksum by accident. A rail that redacts job ids from a cluster support question is unusable, and the trade is not close.

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/,
  'card' => /\b(?:\d[ -]?){13,19}\b/
}.freeze

Constants inherited from Vangrail::Rail

Vangrail::Rail::DEFAULT_SIDES, Vangrail::Rail::SIDES

Instance Attribute Summary collapse

Attributes inherited from Vangrail::Rail

#name, #sides

Instance Method Summary collapse

Methods inherited from Vangrail::Rail

#applies_to?, #placeholder?, #to_s

Constructor Details

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

Returns a new instance of PersonalData.



63
64
65
66
67
68
# File 'lib/vangrail/rails/personal_data.rb', line 63

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.



61
62
63
# File 'lib/vangrail/rails/personal_data.rb', line 61

def patterns
  @patterns
end

#placeholderObject (readonly)

Returns the value of attribute placeholder.



61
62
63
# File 'lib/vangrail/rails/personal_data.rb', line 61

def placeholder
  @placeholder
end

Instance Method Details

#cache_key(text, _context) ⇒ Object



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

def cache_key(text, _context)
  text
end

#call(text, _context) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
# File 'lib/vangrail/rails/personal_data.rb', line 78

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

#offline?Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/vangrail/rails/personal_data.rb', line 70

def offline?
  true
end