Class: SensitiveDataFilter
- Inherits:
-
Logger::Formatter
- Object
- Logger::Formatter
- SensitiveDataFilter
- Defined in:
- lib/AuthenticationSDK/logging/sensitive_logging.rb
Constant Summary collapse
- SENSITIVE_KEYS =
%w[ securitycode number cardnumber expirationmonth expirationyear expiration expirationdate account routingnumber email firstname lastname phonenumber type token signature prefix suffix bin pan encryptedrequest encryptedresponse password secret apikey accesstoken refreshtoken ssn creditcard debitcard cvv pin cardholder nameoncard holdername track1 track2 trackdata iban swift bic dateofbirth birthdate taxid privatekey publickey authorization bearer cavv xid pares micr jwt jwe paymentinformation paymentinstrument card billto shipto customer driverlicense ].freeze
- REDACTED_PAYLOAD =
'[REDACTED]'.freeze
- REDACTED_VALUE =
('X' * 10).freeze
Instance Method Summary collapse
-
#call(severity, time, progname, msg) ⇒ Object
Central defense-in-depth entry point.
-
#find_balanced_json_end(str, start_idx) ⇒ Object
Returns the index of the matching closing brace/bracket for the JSON span beginning at start_idx, honoring JSON string and escape rules.
- #formatLogEntry(severity, _time, progname, msg) ⇒ Object
- #isSensitiveKey?(key) ⇒ Boolean
- #json_object?(str) ⇒ Boolean
-
#maskEmbeddedJson(msg_str) ⇒ Object
Walk the string, extract every top-level balanced ... or [...] span (respecting JSON string/escape rules), and mask each in place.
- #maskJsonObject(obj) ⇒ Object
- #maskSensitiveDataInJson(input) ⇒ Object
-
#maskSensitiveValue(value) ⇒ Object
When a sensitive key wraps a value, redact scalars directly.
- #maskValue(value) ⇒ Object
-
#rubyInspectToJson(str) ⇒ Object
Best-effort, eval-free conversion of Ruby Hash/Array#inspect output to JSON.
Instance Method Details
#call(severity, time, progname, msg) ⇒ Object
Central defense-in-depth entry point. Regardless of how the log message is shaped (Ruby Hash/Array, pure JSON string, "label: json", "prefix json suffix", or even multiple embedded JSON blobs), every JSON Object/Array substring is extracted, parsed, and masked in-place. Any JSON-shaped substring that fails to parse is replaced with REDACTED_PAYLOAD (fail-closed) so a partially-decrypted or truncated body cannot leak through the wrapper.
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 190 def call(severity, time, progname, msg) maskedMessage = case msg when Hash, Array # Ruby Hash/Array passed directly to logger.<level>(obj). Walk # the structure with maskJsonObject so individual sensitive # fields are masked; Hash#to_s produces "key=>value" syntax # which is NOT valid JSON, so we must not route it through # the string-based parser path. JSON.generate(maskJsonObject(msg)) else msg_str = msg.to_s begin JSON.parse(msg_str) maskSensitiveDataInJson(msg_str) rescue JSON::ParserError, TypeError maskEmbeddedJson(msg_str) end end formatLogEntry(severity, time, progname, maskedMessage) end |
#find_balanced_json_end(str, start_idx) ⇒ Object
Returns the index of the matching closing brace/bracket for the JSON span beginning at start_idx, honoring JSON string and escape rules. Returns nil if unbalanced.
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 251 def find_balanced_json_end(str, start_idx) depth = 0 in_string = false escape = false i = start_idx len = str.length while i < len ch = str[i] if in_string if escape escape = false elsif ch == '\\' escape = true elsif ch == '"' in_string = false end else case ch when '"' in_string = true when '{', '[' depth += 1 when '}', ']' depth -= 1 return i if depth.zero? return nil if depth < 0 end end i += 1 end nil end |
#formatLogEntry(severity, _time, progname, msg) ⇒ Object
285 286 287 288 289 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 285 def formatLogEntry(severity, _time, progname, msg) datetime = DateTime.now date_format = datetime.strftime('%Y-%m-%d %H:%M:%S') "[#{date_format}] #{severity.ljust(10)} (#{progname}): #{msg}\n" end |
#isSensitiveKey?(key) ⇒ Boolean
155 156 157 158 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 155 def isSensitiveKey?(key) normalized = key.to_s.downcase.gsub(/[^a-z0-9]/, '') SENSITIVE_KEYS.any? { |sensitive| normalized.include?(sensitive) } end |
#json_object?(str) ⇒ Boolean
21 22 23 24 25 26 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 21 def json_object?(str) parsed = JSON.parse(str) parsed.is_a?(Hash) rescue JSON::ParserError, TypeError false end |
#maskEmbeddedJson(msg_str) ⇒ Object
Walk the string, extract every top-level balanced ... or [...] span (respecting JSON string/escape rules), and mask each in place. If no JSON-shaped span is present, the original text is returned unchanged. If a span is found but cannot be parsed, the entire message is redacted.
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 216 def maskEmbeddedJson(msg_str) return msg_str unless msg_str.include?('{') || msg_str.include?('[') result = +'' i = 0 len = msg_str.length found_any = false while i < len ch = msg_str[i] if ch == '{' || ch == '[' span_end = find_balanced_json_end(msg_str, i) if span_end.nil? # Unbalanced JSON-looking content remaining: fail closed. return REDACTED_PAYLOAD end candidate = msg_str[i..span_end] masked = maskSensitiveDataInJson(candidate) # maskSensitiveDataInJson returns REDACTED_PAYLOAD on parse failure, # which is the safe outcome we want to propagate. result << masked i = span_end + 1 found_any = true else result << ch i += 1 end end found_any ? result : msg_str end |
#maskJsonObject(obj) ⇒ Object
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 138 def maskJsonObject(obj) case obj when Hash obj.each_with_object({}) do |(key, value), result| result[key] = if isSensitiveKey?(key) maskSensitiveValue(value) else maskJsonObject(value) end end when Array obj.map { |item| maskJsonObject(item) } else obj end end |
#maskSensitiveDataInJson(input) ⇒ Object
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 28 def maskSensitiveDataInJson(input) str = input.to_s parsed = begin JSON.parse(str) rescue JSON::ParserError # Fall back to Ruby Hash#inspect syntax (e.g. interpolated # "#{hash}" in a log string produces {"k"=>"v"} or {:k=>"v"}). # Convert heuristically to JSON without eval, then retry. converted = rubyInspectToJson(str) converted.nil? ? (raise) : JSON.parse(converted) end unless parsed.is_a?(Hash) || parsed.is_a?(Array) return REDACTED_PAYLOAD end masked_data = maskJsonObject(parsed) JSON.generate(masked_data) rescue JSON::ParserError, TypeError, EncodingError REDACTED_PAYLOAD end |
#maskSensitiveValue(value) ⇒ Object
When a sensitive key wraps a value, redact scalars directly. For nested Hash/Array containers, recurse via maskJsonObject so individual inner values are masked based on their own key names rather than blanket- redacting the entire container.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 164 def maskSensitiveValue(value) case value when Hash, Array maskJsonObject(value) when String REDACTED_VALUE when Integer, Float 9_999 when TrueClass, FalseClass false else value end end |
#maskValue(value) ⇒ Object
179 180 181 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 179 def maskValue(value) maskSensitiveValue(value) end |
#rubyInspectToJson(str) ⇒ Object
Best-effort, eval-free conversion of Ruby Hash/Array#inspect output to JSON. Returns nil if the input cannot plausibly be Ruby inspect syntax (so the caller can fail closed). Handles:
:symbol => "v" -> "symbol": "v" (legacy inspect)
"k" => "v" -> "k": "v" (string-key inspect)
{ bareword: "v" } -> { "bareword": "v" } (Ruby 3.4+ shorthand)
bare nil tokens -> null
Does NOT attempt to handle every Ruby literal (Procs, Time, custom #inspect, etc.); anything unrecognized will cause JSON.parse to fail downstream and the span will be redacted.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/AuthenticationSDK/logging/sensitive_logging.rb', line 59 def rubyInspectToJson(str) out = +'' i = 0 len = str.length in_string = false escape = false while i < len ch = str[i] if in_string out << ch if escape escape = false elsif ch == '\\' escape = true elsif ch == '"' in_string = false end i += 1 next end if ch == '"' in_string = true out << ch i += 1 next end # :symbol_key (legacy inspect): preceded by { , [ or whitespace if ch == ':' && i + 1 < len && str[i + 1] =~ /[A-Za-z_]/ && (out.empty? || out[-1] =~ /[\{\[,\s]/) j = i + 1 j += 1 while j < len && str[j] =~ /[A-Za-z0-9_]/ sym = str[(i + 1)...j] out << '"' << sym << '"' i = j next end # bareword: shorthand (Ruby 3.4+ inspect): preceded by { , or # whitespace; followed by ':' then whitespace (so we don't # confuse with the '::' constant-resolution operator). if ch =~ /[A-Za-z_]/ && (out.empty? || out[-1] =~ /[\{\[,\s]/) j = i j += 1 while j < len && str[j] =~ /[A-Za-z0-9_]/ if j < len && str[j] == ':' && str[j + 1] != ':' && (j + 1 >= len || str[j + 1] =~ /\s/) word = str[i...j] out << '"' << word << '":' i = j + 1 next end end # => operator if ch == '=' && str[i + 1] == '>' out << ':' i += 2 next end # bare nil -> null if ch == 'n' && str[i, 3] == 'nil' && (i.zero? || str[i - 1] =~ /[\s,\{\[:]/) && (i + 3 >= len || str[i + 3] =~ /[\s,\}\]]/) out << 'null' i += 3 next end out << ch i += 1 end out end |