auth-sanitizer 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,13 +4,11 @@ module Auth
4
4
  module Sanitizer
5
5
  # Mixin that redacts sensitive instance variables in `#inspect` output.
6
6
  #
7
- # Classes include this module and declare which attribute names should be
8
- # filtered via {.filtered_attributes}. Matching and replacement behavior is
9
- # delegated to {ThingFilter}, which is initialized once per object.
7
+ # Classes include this module and declare attribute names to filter via {.filtered_attributes}.
8
+ # Matching and replacement behavior is delegated to {ThingFilter}, initialized once per object.
10
9
  #
11
- # This means existing objects keep the filter configuration that was present
12
- # when they were initialized, even if global config or class-level filter
13
- # declarations change later.
10
+ # This means existing objects keep the filter configuration that was present when they were
11
+ # initialized, even if global config or class-level filter declarations change later.
14
12
  #
15
13
  # The label used for redaction is resolved from {Auth::Sanitizer.filtered_label}
16
14
  # at object initialization time. Host gems may customize this by installing a
@@ -20,7 +18,7 @@ module Auth
20
18
  # Hook invoked when the module is included. Extends the including class with
21
19
  # class-level helpers and prepends the initializer hook.
22
20
  #
23
- # @param [Class] base The including class
21
+ # @param base [Class] The including class
24
22
  # @return [void]
25
23
  def included(base)
26
24
  base.extend(ClassMethods)
@@ -47,7 +45,8 @@ module Auth
47
45
  class << self
48
46
  # Declare attributes that should be redacted in inspect output.
49
47
  #
50
- # @param [Array<Symbol, String>] attributes One or more attribute names
48
+ # @param base [Class] The class the attributes are declared on
49
+ # @param attributes [Array<Symbol, String>] One or more attribute names
51
50
  # @return [void]
52
51
  def filtered_attributes(base, *attributes)
53
52
  base.instance_variable_set(:@filtered_attribute_names, attributes.map(&:to_sym))
@@ -55,7 +54,7 @@ module Auth
55
54
 
56
55
  # The configured attribute names to filter.
57
56
  #
58
- # @param [Class] base The class to get filtered attributes for
57
+ # @param base [Class] The class to get filtered attributes for
59
58
  # @return [Array<Symbol>]
60
59
  def filtered_attribute_names(base)
61
60
  return [] unless base.instance_variable_defined?(:@filtered_attribute_names)
@@ -66,7 +65,7 @@ module Auth
66
65
 
67
66
  # Declare attributes that should be redacted in inspect output.
68
67
  #
69
- # @param [Array<Symbol, String>] attributes One or more attribute names
68
+ # @param attributes [Array<Symbol, String>] One or more attribute names
70
69
  # @return [void]
71
70
  def filtered_attributes(*attributes)
72
71
  ClassMethods.filtered_attributes(self, *attributes)
@@ -101,7 +100,9 @@ module Auth
101
100
 
102
101
  private
103
102
 
103
+ # Matches a quoted string literal inside `#inspect` output.
104
104
  INSPECTED_STRING_VALUE = /"(?:(?:\\.)|[^"\\])*"/
105
+ # Matches redactable key/value pairs inside `#inspect` output.
105
106
  INSPECTED_REDACTABLE_VALUE = /
106
107
  (?:
107
108
  (@([A-Za-z_]\w*[!?=]?)=) |
@@ -113,6 +114,10 @@ module Auth
113
114
  /x
114
115
  private_constant :INSPECTED_STRING_VALUE, :INSPECTED_REDACTABLE_VALUE
115
116
 
117
+ # Redact filtered values from `#inspect` output text.
118
+ #
119
+ # @param inspected [String] The `#inspect` output to redact
120
+ # @return [String] The redacted inspect output
116
121
  def redact_inspected_values(inspected)
117
122
  inspected.gsub(INSPECTED_REDACTABLE_VALUE) do |match|
118
123
  captures = Regexp.last_match.captures
@@ -2,12 +2,11 @@
2
2
 
3
3
  module Auth
4
4
  module Sanitizer
5
- # Logger wrapper that redacts sensitive values from debug output before
6
- # delegating to the underlying logger instance.
5
+ # Logger wrapper that redacts sensitive values from debug output before delegating to the
6
+ # underlying logger instance.
7
7
  #
8
- # This class is intentionally narrow in scope: it only sanitizes string
9
- # messages emitted through the logging path and leaves request/response
10
- # behavior unchanged.
8
+ # This class is intentionally narrow in scope: it only sanitizes string messages emitted
9
+ # through the logging path and leaves request/response behavior unchanged.
11
10
  #
12
11
  # The underlying {ThingFilter} is initialized once when the logger wrapper is
13
12
  # created, so later config changes do not alter the behavior of existing
@@ -15,24 +14,24 @@ module Auth
15
14
  class SanitizedLogger
16
15
  # Create a new sanitized logger wrapper.
17
16
  #
18
- # @param [#add, #debug, #info, #warn, #error, #fatal, #unknown] logger
17
+ # @param logger [#add, #debug, #info, #warn, #error, #fatal, #unknown]
19
18
  # The underlying logger instance that will receive sanitized messages.
20
- # @param [Array<String>] filtered_keys
19
+ # @param filtered_keys [Array<String>]
21
20
  # Key names whose values should be redacted in debug output.
22
21
  # Defaults to {Auth::Sanitizer.default_filtered_keys}.
23
- # @param [String] label
22
+ # @param label [String]
24
23
  # Replacement label for redacted values.
25
24
  # Defaults to {Auth::Sanitizer.filtered_label}.
26
25
  def initialize(logger, filtered_keys: Auth::Sanitizer.default_filtered_keys, label: Auth::Sanitizer.filtered_label)
27
26
  @logger = logger
28
- @thing_filter = ThingFilter.new(filtered_keys, label: label)
27
+ @value_sanitizer = ValueSanitizer.new(filtered_keys: filtered_keys, label: label)
29
28
  end
30
29
 
31
30
  # Add a log entry after sanitizing any string payloads.
32
31
  #
33
- # @param [Integer, Symbol, String, nil] severity Logger severity
34
- # @param [Object, nil] message Optional log message
35
- # @param [Object, nil] progname Optional program name
32
+ # @param severity [Integer, Symbol, String, nil] Logger severity
33
+ # @param message [Object, nil] Optional log message
34
+ # @param progname [Object, nil] Optional program name
36
35
  # @yieldreturn [Object] Deferred log message
37
36
  # @return [Object] The underlying logger result
38
37
  def add(severity, message = nil, progname = nil)
@@ -45,7 +44,7 @@ module Auth
45
44
 
46
45
  # Append a message to the underlying logger after sanitization.
47
46
  #
48
- # @param [String] message Message to append
47
+ # @param message [String] Message to append
49
48
  # @return [Object] The underlying logger result
50
49
  def <<(message)
51
50
  @logger << sanitize(message)
@@ -53,7 +52,7 @@ module Auth
53
52
 
54
53
  # Log a debug message after sanitization.
55
54
  #
56
- # @param [Object, nil] progname Optional program name
55
+ # @param progname [Object, nil] Optional program name
57
56
  # @yieldreturn [Object] Deferred log message
58
57
  # @return [Object] The underlying logger result
59
58
  def debug(progname = nil, &block)
@@ -62,7 +61,7 @@ module Auth
62
61
 
63
62
  # Log an info message after sanitization.
64
63
  #
65
- # @param [Object, nil] progname Optional program name
64
+ # @param progname [Object, nil] Optional program name
66
65
  # @yieldreturn [Object] Deferred log message
67
66
  # @return [Object] The underlying logger result
68
67
  def info(progname = nil, &block)
@@ -71,7 +70,7 @@ module Auth
71
70
 
72
71
  # Log a warning message after sanitization.
73
72
  #
74
- # @param [Object, nil] progname Optional program name
73
+ # @param progname [Object, nil] Optional program name
75
74
  # @yieldreturn [Object] Deferred log message
76
75
  # @return [Object] The underlying logger result
77
76
  def warn(progname = nil, &block)
@@ -80,7 +79,7 @@ module Auth
80
79
 
81
80
  # Log an error message after sanitization.
82
81
  #
83
- # @param [Object, nil] progname Optional program name
82
+ # @param progname [Object, nil] Optional program name
84
83
  # @yieldreturn [Object] Deferred log message
85
84
  # @return [Object] The underlying logger result
86
85
  def error(progname = nil, &block)
@@ -89,7 +88,7 @@ module Auth
89
88
 
90
89
  # Log a fatal message after sanitization.
91
90
  #
92
- # @param [Object, nil] progname Optional program name
91
+ # @param progname [Object, nil] Optional program name
93
92
  # @yieldreturn [Object] Deferred log message
94
93
  # @return [Object] The underlying logger result
95
94
  def fatal(progname = nil, &block)
@@ -98,7 +97,7 @@ module Auth
98
97
 
99
98
  # Log an unknown-severity message after sanitization.
100
99
  #
101
- # @param [Object, nil] progname Optional program name
100
+ # @param progname [Object, nil] Optional program name
102
101
  # @yieldreturn [Object] Deferred log message
103
102
  # @return [Object] The underlying logger result
104
103
  def unknown(progname = nil, &block)
@@ -121,7 +120,7 @@ module Auth
121
120
 
122
121
  # Set the formatter of the underlying logger if supported.
123
122
  #
124
- # @param [Object] formatter Formatter object
123
+ # @param formatter [Object] Formatter object
125
124
  # @return [void]
126
125
  def formatter=(formatter)
127
126
  @logger.formatter = formatter if @logger.respond_to?(:formatter=)
@@ -136,7 +135,7 @@ module Auth
136
135
 
137
136
  # Set the logger level if supported.
138
137
  #
139
- # @param [Object] level Logger level
138
+ # @param level [Object] Logger level
140
139
  # @return [void]
141
140
  def level=(level)
142
141
  @logger.level = level if @logger.respond_to?(:level=)
@@ -151,7 +150,7 @@ module Auth
151
150
 
152
151
  # Set the logger progname if supported.
153
152
  #
154
- # @param [Object] progname Logger progname
153
+ # @param progname [Object] Logger progname
155
154
  # @return [void]
156
155
  def progname=(progname)
157
156
  @logger.progname = progname if @logger.respond_to?(:progname=)
@@ -159,8 +158,8 @@ module Auth
159
158
 
160
159
  # Report support for methods provided by the wrapped logger.
161
160
  #
162
- # @param [Symbol] method_name Method name to check
163
- # @param [Boolean] include_private Whether private methods are considered
161
+ # @param method_name [Symbol] Method name to check
162
+ # @param include_private [Boolean] Whether private methods are considered
164
163
  # @return [Boolean]
165
164
  def respond_to_missing?(method_name, include_private = false)
166
165
  @logger.respond_to?(method_name, include_private) || super
@@ -168,8 +167,8 @@ module Auth
168
167
 
169
168
  # Delegate unsupported methods to the wrapped logger.
170
169
  #
171
- # @param [Symbol] method_name Method to invoke
172
- # @param [Array<Object>] args Method arguments
170
+ # @param method_name [Symbol] Method to invoke
171
+ # @param args [Array<Object>] Method arguments
173
172
  # @yield Deferred block forwarded to the wrapped logger
174
173
  # @return [Object] The delegated result
175
174
  def method_missing(method_name, *args, &block)
@@ -182,8 +181,8 @@ module Auth
182
181
 
183
182
  # Dispatch a severity-specific log call after sanitization.
184
183
  #
185
- # @param [Symbol] level Logger method name
186
- # @param [Object, nil] progname Optional program name
184
+ # @param level [Symbol] Logger method name
185
+ # @param progname [Object, nil] Optional program name
187
186
  # @yieldreturn [Object] Deferred log message
188
187
  # @return [Object] The underlying logger result
189
188
  def log(level, progname = nil)
@@ -196,48 +195,12 @@ module Auth
196
195
 
197
196
  # Sanitize a logger message when it is a String.
198
197
  #
199
- # @param [Object] message Potential logger payload
198
+ # @param message [Object] Potential logger payload
200
199
  # @return [Object] Unchanged non-String payloads, sanitized String payloads
201
200
  def sanitize(message)
202
201
  return message unless message.is_a?(String)
203
202
 
204
- sanitized = message.dup
205
- sanitized = sanitize_authorization_header(sanitized)
206
- sanitized = sanitize_json_pairs(sanitized)
207
- sanitize_form_and_query_pairs(sanitized)
208
- end
209
-
210
- # The initialized thing filter used by this logger.
211
- #
212
- # This is a per-logger snapshot created during initialization.
213
- #
214
- # @return [ThingFilter]
215
- attr_reader :thing_filter
216
-
217
- # Redact Authorization header values.
218
- #
219
- # @param [String] message Logger message
220
- # @return [String] Sanitized logger message
221
- def sanitize_authorization_header(message)
222
- message.gsub(/(Authorization:\s*)(?:"[^"]*"|[^\r\n]+)/i, "\\1\"#{thing_filter.label}\"")
223
- end
224
-
225
- # Redact JSON-style values for configured sensitive key names.
226
- #
227
- # @param [String] message Logger message
228
- # @return [String] Sanitized logger message
229
- def sanitize_json_pairs(message)
230
- message.gsub(/(["'])(#{thing_filter.pattern_source})\1(\s*:\s*)(["'])(.*?)\4/i) do
231
- %(#{$1}#{$2}#{$1}#{$3}#{$4}#{thing_filter.label}#{$4})
232
- end
233
- end
234
-
235
- # Redact form-encoded and query-string values for configured sensitive key names.
236
- #
237
- # @param [String] message Logger message
238
- # @return [String] Sanitized logger message
239
- def sanitize_form_and_query_pairs(message)
240
- message.gsub(/(\b(?:#{thing_filter.pattern_source})=)([^&\s"]+)/i, "\\1#{thing_filter.label}")
203
+ @value_sanitizer.sanitize(message)
241
204
  end
242
205
  end
243
206
  end
@@ -13,8 +13,8 @@ module Auth
13
13
  class ThingFilter
14
14
  # Create a new filter.
15
15
  #
16
- # @param [Enumerable<#to_s>] things Names that should be filtered
17
- # @param [String] label Replacement label to use for filtered values
16
+ # @param things [Enumerable<#to_s>] Names that should be filtered
17
+ # @param label [String] Replacement label to use for filtered values
18
18
  #
19
19
  # The provided values are duplicated and frozen so the filter remains
20
20
  # stable for the lifetime of the object.
@@ -39,7 +39,7 @@ module Auth
39
39
  # Matching is substring-based so it works naturally with instance-variable
40
40
  # names used by `#inspect`, such as `@secret` matching `secret`.
41
41
  #
42
- # @param [#to_s] thing_name Candidate thing name
42
+ # @param thing_name [#to_s] Candidate thing name
43
43
  # @return [Boolean]
44
44
  def filtered?(thing_name)
45
45
  thing_name_str = thing_name.to_s
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module Sanitizer
5
+ # Sanitizes arbitrary values using a configured {ThingFilter}.
6
+ class ValueSanitizer
7
+ # @param filtered_keys [Array<String>] Sensitive key names
8
+ # @param label [String] Replacement label
9
+ def initialize(filtered_keys: Auth::Sanitizer.default_filtered_keys, label: Auth::Sanitizer.filtered_label, **options)
10
+ @thing_filter = ThingFilter.new(filtered_keys, label: label)
11
+ end
12
+
13
+ # Return a sanitized copy. Matching Hash keys are replaced as well as
14
+ # their values, so a secret used as a key cannot leak through inspect.
15
+ #
16
+ # @param value [Object] Value to sanitize
17
+ # @return [Object]
18
+ def sanitize(value)
19
+ case value
20
+ when Hash
21
+ value.each_with_object({}) do |(key, nested), sanitized|
22
+ sanitized_key = sanitize(key)
23
+ sanitized[sanitized_key] = sensitive_key?(key) ? @thing_filter.label : sanitize(nested)
24
+ end
25
+ when Array
26
+ value.map { |nested| sanitize(nested) }
27
+ when String
28
+ sanitize_string(value)
29
+ else
30
+ value
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def sensitive_key?(key)
37
+ @thing_filter.filtered?(key)
38
+ end
39
+
40
+ def sanitize_string(message)
41
+ sanitized = message.dup.scrub
42
+ sanitized = sanitized.gsub(/(Authorization:\s*)(?:"[^"]*"|[^\r\n]+)/i, "\\1\"#{@thing_filter.label}\"")
43
+ sanitized = sanitized.gsub(/(["'])(#{@thing_filter.pattern_source})\1(\s*:\s*)(["'])(.*?)\4/i) do
44
+ %(#{$1}#{$2}#{$1}#{$3}#{$4}#{@thing_filter.label}#{$4})
45
+ end
46
+ key = @thing_filter.pattern_source
47
+ label = @thing_filter.label
48
+ sanitized = sanitized.gsub(/((?:"(?:#{key})"|:#{key})\s*=>\s*")[^"]*(")/i) { "#{$1}#{label}#{$2}" }
49
+ sanitized = sanitized.gsub(/(?<!["'])((?:\b(?:#{key})):\s*")[^"]*(")/i) { "#{$1}#{label}#{$2}" }
50
+ sanitized.gsub(/(\b(?:#{key})=)([^&\s">]+)/i, "\\1#{label}")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -2,9 +2,12 @@
2
2
 
3
3
  module Auth
4
4
  module Sanitizer
5
+ # Version namespace for this gem.
5
6
  module Version
6
- VERSION = "0.2.3"
7
+ # Current gem version.
8
+ VERSION = "0.2.5"
7
9
  end
10
+ # Current gem version exposed at the traditional constant location.
8
11
  VERSION = Version::VERSION # Traditional Constant Location
9
12
  end
10
13
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  require "version_gem"
4
4
 
5
- require_relative "sanitizer/version"
6
5
  require_relative "sanitizer/thing_filter"
7
6
  require_relative "sanitizer/core"
7
+ require_relative "sanitizer/value_sanitizer"
8
8
  require_relative "sanitizer/filtered_attributes"
9
9
  require_relative "sanitizer/sanitized_logger"
10
+ require_relative "sanitizer/version"
10
11
 
11
12
  Auth::Sanitizer::Version.class_eval do
12
13
  extend VersionGem::Basic
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Namespace for the isolated loader surface of the auth-sanitizer gem.
3
4
  module AuthSanitizer
4
5
  # Loader for consumers that need Auth::Sanitizer without defining a top-level
5
6
  # Auth constant in the host application.
6
7
  module Loader
8
+ # Gem source files evaluated into the anonymous namespace, in load order.
7
9
  FILES = %w[
8
10
  auth/sanitizer/version.rb
9
11
  auth/sanitizer/thing_filter.rb
10
12
  auth/sanitizer/core.rb
13
+ auth/sanitizer/value_sanitizer.rb
11
14
  auth/sanitizer/filtered_attributes.rb
12
15
  auth/sanitizer/sanitized_logger.rb
13
16
  ].freeze
@@ -39,6 +42,9 @@ module AuthSanitizer
39
42
  # the anonymous Auth namespace. This keeps the normal files unchanged while
40
43
  # avoiding Object::Auth leakage on runtimes where Module#module_eval still
41
44
  # resolves nested module declarations through Object.
45
+ #
46
+ # @param path [String] Absolute path to the gem source file
47
+ # @return [String] The source with the top-level wrapper removed
42
48
  def isolated_source(path)
43
49
  lines = File.readlines(path)
44
50
  wrapper_index = lines.index("module Auth\n")
@@ -5,3 +5,15 @@ module AuthSanitizer
5
5
  def self.load_isolated: () -> Module
6
6
  end
7
7
  end
8
+
9
+ module Auth
10
+ module Sanitizer
11
+ module Version
12
+ VERSION: String
13
+ end
14
+ VERSION: String
15
+ def self.default_filtered_keys: () -> Array[String]
16
+ def self.filtered_label: () -> String
17
+ def self.sanitize: (untyped value, ?filtered_keys: Array[String], ?label: String, **untyped options) -> untyped
18
+ end
19
+ end
data.tar.gz.sig CHANGED
Binary file