Class: StandardAudit::MetadataFilter

Inherits:
Object
  • Object
show all
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

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 HashActionController::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.

Returns:

  • (Boolean)


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