Module: Legion::Phi

Defined in:
lib/legion/phi.rb,
lib/legion/phi/erasure.rb,
lib/legion/phi/access_log.rb

Defined Under Namespace

Modules: AccessLog, Erasure

Constant Summary collapse

PHI_TAG =
:phi
DEFAULT_PHI_PATTERNS =
%w[
  ssn
  social_security
  mrn
  medical_record
  dob
  date_of_birth
  patient_name
  first_name
  last_name
  full_name
  phone
  phone_number
  email
  address
  zip
  zipcode
  postal_code
  diagnosis
  icd_code
  npi
  insurance_id
  member_id
  account_number
  credit_card
  passport
  drivers_license
  ip_address
  device_id
].freeze

Class Method Summary collapse

Class Method Details

.auto_detect_fields(data) ⇒ Object

Auto-detect PHI fields by matching field names against configurable patterns.



95
96
97
98
99
100
101
102
103
# File 'lib/legion/phi.rb', line 95

def auto_detect_fields(data)
  return [] unless data.is_a?(Hash)

  patterns = phi_patterns
  data.keys.select do |key|
    key_str = key.to_s.downcase
    patterns.any? { |pat| key_str.match?(pat) }
  end
end

.compiled_defaultsObject



116
117
118
# File 'lib/legion/phi.rb', line 116

def compiled_defaults
  DEFAULT_PHI_PATTERNS.map { |p| Regexp.new("\\b#{Regexp.escape(p)}\\b", Regexp::IGNORECASE) }
end

.erase(data, key_id:) ⇒ Object

Cryptographic erasure: re-encrypt PHI fields with a throwaway key, then destroy the key. Returns the erased record (PHI fields replaced with erasure markers).



85
86
87
88
89
90
91
92
# File 'lib/legion/phi.rb', line 85

def erase(data, key_id:)
  return data unless data.is_a?(Hash)

  fields = phi_fields(data) + auto_detect_fields(data)
  fields = fields.uniq

  Erasure.erase_record(record: data, phi_fields: fields, key_id: key_id)
end

.phi_fields(data) ⇒ Object

Returns the list of PHI-tagged field names.



63
64
65
66
67
# File 'lib/legion/phi.rb', line 63

def phi_fields(data)
  return [] unless tagged?(data)

  data[:__phi_fields] || []
end

.phi_patternsObject

Returns the configured PHI field patterns (regex strings).



106
107
108
109
110
111
112
113
114
# File 'lib/legion/phi.rb', line 106

def phi_patterns
  configured = settings_patterns
  return compiled_defaults if configured.nil? || configured.empty?

  configured.map { |p| Regexp.new(p, Regexp::IGNORECASE) }
rescue StandardError => e
  Legion::Logging.warn "Phi#phi_patterns failed to compile configured patterns: #{e.message}" if defined?(Legion::Logging)
  compiled_defaults
end

.redact(data) ⇒ Object

Returns a copy of data with all PHI fields replaced with [REDACTED].



70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/legion/phi.rb', line 70

def redact(data)
  return data unless data.is_a?(Hash)

  fields = phi_fields(data) + auto_detect_fields(data)
  fields = fields.uniq

  result = data.dup
  fields.each do |field|
    result[field] = '[REDACTED]' if result.key?(field)
  end
  result
end

.settings_patternsObject



120
121
122
123
124
125
126
127
# File 'lib/legion/phi.rb', line 120

def settings_patterns
  return nil unless defined?(Legion::Settings)

  Legion::Settings.dig(:phi, :field_patterns)
rescue StandardError => e
  Legion::Logging.debug "Phi#settings_patterns failed: #{e.message}" if defined?(Legion::Logging)
  nil
end

.tag(data, fields:) ⇒ Object

Marks specific hash fields as containing PHI by adding __phi_fields metadata.

Raises:

  • (ArgumentError)


45
46
47
48
49
50
51
52
53
# File 'lib/legion/phi.rb', line 45

def tag(data, fields:)
  raise ArgumentError, 'data must be a Hash' unless data.is_a?(Hash)
  raise ArgumentError, 'fields must be an Array' unless fields.is_a?(Array)

  result = data.dup
  existing = result[:__phi_fields] || []
  result[:__phi_fields] = (existing + fields.map(&:to_sym)).uniq
  result
end

.tagged?(data) ⇒ Boolean

Returns true if the hash has a PHI tag.

Returns:

  • (Boolean)


56
57
58
59
60
# File 'lib/legion/phi.rb', line 56

def tagged?(data)
  return false unless data.is_a?(Hash)

  data.key?(:__phi_fields) && !data[:__phi_fields].nil?
end