Class: StandardAudit::MetadataFilter
- Inherits:
-
Object
- Object
- StandardAudit::MetadataFilter
- Defined in:
- lib/standard_audit/metadata_filter.rb
Overview
The single implementation of sensitive-key redaction for audit metadata.
There used to be two: one in StandardAudit.record and one in
Subscriber#extract_metadata. They had already diverged — the subscriber
copy did not subtract RESERVED_METADATA_KEYS, so an app that (reasonably)
added :_tags to sensitive_keys got the reserved key preserved on the
record path and stripped on the ActiveSupport::Notifications path. Two
copies of a security filter drifting apart is exactly the failure mode
worth designing out, so both paths now call here.
The divergence is resolved in favour of record's behaviour: reserved keys
are subtracted from the sensitive set and can never be filtered.
Matching
A key is redacted when it matches config.sensitive_keys exactly (by
name, string/symbol insensitive) or matches one of
config.sensitive_key_patterns (Regexps, always applied), unless it is
listed in config.sensitive_key_exceptions or is a reserved key.
Why there is no substring mode
Matching sensitive_keys by substring instead of exactly is the obvious
"fix" for client_secret not matching :secret, and it is a trap. Against
the current default key list it would strip real audit content across the
estate:
:token => input_tokens, output_tokens (live LLM cost accounting),
token_digest (rendered in a staff audit UI)
:password => password_reset_sent_at, onepassword
:authorization => authorization_endpoint
Audit rows are append-only, so a bad default cannot be undone — the content
is simply never written. sensitive_key_patterns is the supported tool
instead: /secret/i solves the motivating client_secret case exactly,
opt-in and per-app, and rake standard_audit:sensitive_keys:dry_run turns
"is this rule safe for my data?" into a command rather than a guess.
Defined Under Namespace
Classes: UnfilterableMetadataError
Class Method Summary collapse
Instance Method Summary collapse
-
#call(metadata) ⇒ Object
Filters anything hash-like, not just
Hash—ActionController::Parametersand othereach_pair-able wrappers are filtered rather than waved through. -
#filter?(key) ⇒ Boolean
True when
keywould be redacted from metadata. -
#initialize(config: StandardAudit.config) ⇒ MetadataFilter
constructor
A new instance of MetadataFilter.
Constructor Details
#initialize(config: StandardAudit.config) ⇒ MetadataFilter
Returns a new instance of MetadataFilter.
55 56 57 |
# File 'lib/standard_audit/metadata_filter.rb', line 55 def initialize(config: StandardAudit.config) @config = config end |
Class Method Details
.call(metadata, config: StandardAudit.config) ⇒ Object
50 51 52 |
# File 'lib/standard_audit/metadata_filter.rb', line 50 def call(, config: StandardAudit.config) new(config: config).call() end |
Instance Method Details
#call(metadata) ⇒ Object
Filters anything hash-like, not just Hash — ActionController::Parameters
and other each_pair-able wrappers are filtered rather than waved
through. nil passes (there is nothing to leak); anything else raises.
Descends into nested Hashes (and Hashes inside Arrays) only when
config.filter_nested_metadata is true. Reserved keys are preserved and
their values are left entirely alone, at every depth — _tags and
_source are gem-owned, not host payload, and immunity that held only at
the top level would be a confusing half-guarantee.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/standard_audit/metadata_filter.rb', line 68 def call() return nil if .nil? unless hash_like?() raise UnfilterableMetadataError, "audit metadata must be nil or hash-like (got #{.class}); " \ "refusing to write it unfiltered" end filtered = .reject { |key, _| filter?(key) } return filtered unless @config. filter_values(filtered) end |
#filter?(key) ⇒ Boolean
True when key would be redacted from metadata. Public so hosts (and the
dry-run tooling) can ask the question without writing a row.
85 86 87 88 89 90 91 92 93 94 |
# File 'lib/standard_audit/metadata_filter.rb', line 85 def filter?(key) key = key.to_s # `_tags` and `_source` are owned by EventSubscriber and are never # stripped, even if a consumer lists them in `sensitive_keys`. return false if StandardAudit::RESERVED_METADATA_KEYS.include?(key) return false if exception?(key) sensitive_keys.include?(key) || sensitive_key_patterns.any? { |pattern| pattern.match?(key) } end |