standard_audit 0.11.1 → 0.12.1

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.
@@ -21,6 +21,9 @@ class CreateAuditLogs < ActiveRecord::Migration[<%= ActiveRecord::Migration.curr
21
21
  # share a parent, so the log is a DAG rather than a strict line; storing
22
22
  # the parent is what keeps every row verifiable when it forks.
23
23
  t.string :previous_checksum, limit: 64
24
+ # Set by AuditLog.anonymize_actor! (GDPR erasure). verify_chain counts a
25
+ # stamped row as `redacted` instead of a digest failure.
26
+ t.datetime :anonymized_at
24
27
  t.timestamps
25
28
  end
26
29
 
@@ -4,7 +4,8 @@ module StandardAudit
4
4
  :actor_extractor, :target_extractor, :scope_extractor,
5
5
  :current_actor_resolver, :current_request_id_resolver,
6
6
  :current_ip_address_resolver, :current_user_agent_resolver,
7
- :current_session_id_resolver,
7
+ :current_session_id_resolver, :current_scope_resolver,
8
+ :before_write,
8
9
  :sensitive_keys, :sensitive_key_patterns,
9
10
  :sensitive_key_exceptions, :filter_nested_metadata,
10
11
  :dereference_record_metadata,
@@ -24,8 +25,11 @@ module StandardAudit
24
25
  @target_extractor = ->(payload) { payload[:target] }
25
26
  @scope_extractor = ->(payload) { payload[:scope] }
26
27
 
28
+ # Tries StandardId's `Current.account` first, then the Rails-generator
29
+ # convention `Current.user`. Each is respond_to?-guarded, so an app that
30
+ # defines only one of them (or no `Current` at all) resolves cleanly.
27
31
  @current_actor_resolver = -> {
28
- defined?(Current) && Current.respond_to?(:user) ? Current.user : nil
32
+ Configuration.current_attribute(:account) || Configuration.current_attribute(:user)
29
33
  }
30
34
  @current_request_id_resolver = -> {
31
35
  defined?(Current) && Current.respond_to?(:request_id) ? Current.request_id : nil
@@ -36,10 +40,35 @@ module StandardAudit
36
40
  @current_user_agent_resolver = -> {
37
41
  defined?(Current) && Current.respond_to?(:user_agent) ? Current.user_agent : nil
38
42
  }
43
+ # Tries StandardId's `Current.session&.id` first, then a plain
44
+ # `Current.session_id` attribute.
39
45
  @current_session_id_resolver = -> {
40
- defined?(Current) && Current.respond_to?(:session_id) ? Current.session_id : nil
46
+ session = Configuration.current_attribute(:session)
47
+ session_id = session.id if session.respond_to?(:id)
48
+ session_id || Configuration.current_attribute(:session_id)
41
49
  }
42
50
 
51
+ # Fallback for the audit scope (tenant) when a write names none —
52
+ # explicit `scope:` and `scope_extractor` results always win. Applied on
53
+ # every write path. nil (the default) leaves unscoped writes unscoped.
54
+ #
55
+ # config.current_scope_resolver = -> { Current.organisation }
56
+ @current_scope_resolver = nil
57
+
58
+ # A callable run on EVERY write path (direct `record`, `audit!`,
59
+ # `record_audit`, both subscribers; sync, async and batched) after the
60
+ # Current resolvers and `metadata_builder`, and BEFORE redaction. It
61
+ # receives the mutable entry Hash — :event_type, :actor, :target, :scope,
62
+ # :metadata, :request_id, :ip_address, :user_agent, :session_id — and may
63
+ # change it in place; the return value is ignored. Raising aborts the
64
+ # write (the subscribers rescue and report; direct callers see the error).
65
+ #
66
+ # config.before_write = ->(entry) {
67
+ # AuditMetadataPii.verify!(entry[:metadata]) if Rails.env.local?
68
+ # entry[:metadata] = entry[:metadata].merge("surface" => Current.surface) if Current.surface
69
+ # }
70
+ @before_write = nil
71
+
43
72
  # Note: :authorization filters the HTTP Authorization header value.
44
73
  # If you use "authorization" as a metadata key for policy decisions,
45
74
  # rename it (e.g. :authorization_policy) to avoid accidental filtering.
@@ -169,6 +198,14 @@ module StandardAudit
169
198
  @retention_days = self.class.retention_days_from_env
170
199
  end
171
200
 
201
+ # Reads `::Current.<name>` when a top-level `Current` exists and responds
202
+ # to it; nil otherwise. The building block of the default resolvers.
203
+ def self.current_attribute(name)
204
+ return nil unless defined?(::Current) && ::Current.respond_to?(name)
205
+
206
+ ::Current.public_send(name)
207
+ end
208
+
172
209
  # Parses STANDARD_AUDIT_RETENTION_DAYS into a positive Integer, or nil when
173
210
  # unset/blank/zero/negative/non-numeric (=> infinite retention).
174
211
  def self.retention_days_from_env
@@ -199,9 +236,9 @@ module StandardAudit
199
236
  # supplied by a concern mixed into the model). A callable is passed the
200
237
  # instance.
201
238
  #
202
- # NOTE: batched writes (`StandardAudit.batch { … }` → `insert_all!`) never
203
- # instantiate a model, so hooks do not run there. A batched writer that
204
- # needs a derived column has to set it on the buffered attrs.
239
+ # Batched writes (`StandardAudit.batch { … }` → `insert_all!`) run the same
240
+ # hooks at flush time, against an unsaved instance built from each
241
+ # buffered row, before that row's checksum is computed (since 0.12.0).
205
242
  def before_checksum(hook = nil, &block)
206
243
  hook ||= block
207
244
  raise ArgumentError, "before_checksum needs a callable, a Symbol, or a block" if hook.nil?
@@ -33,42 +33,28 @@ module StandardAudit
33
33
  target = config.target_extractor.call(payload)
34
34
  scope = config.scope_extractor.call(payload)
35
35
 
36
- metadata = build_metadata(payload, event[:tags], event[:source_location], config)
36
+ reserved = RESERVED_PAYLOAD_KEYS.map(&:to_s)
37
37
 
38
- StandardAudit.record(
38
+ StandardAudit.write_entry(
39
39
  name,
40
40
  actor: actor,
41
41
  target: target,
42
42
  scope: scope,
43
- metadata: metadata,
44
- request_id: context[:request_id] || payload[:request_id],
45
- ip_address: context[:ip_address] || payload[:ip_address],
46
- user_agent: context[:user_agent] || payload[:user_agent],
47
- session_id: context[:session_id] || payload[:session_id]
43
+ metadata: payload.reject { |k, _| reserved.include?(k.to_s) },
44
+ reserved: reserved_metadata(event[:tags], event[:source_location]),
45
+ context: {
46
+ request_id: context[:request_id] || payload[:request_id],
47
+ ip_address: context[:ip_address] || payload[:ip_address],
48
+ user_agent: context[:user_agent] || payload[:user_agent],
49
+ session_id: context[:session_id] || payload[:session_id]
50
+ }
48
51
  )
49
52
  rescue => e
50
- Rails.logger.error("[StandardAudit] Error handling Rails.event: #{e.class}: #{e.message}")
51
- report_error(e, name)
53
+ StandardAudit.report_write_error(e, name, subscriber: self.class.name)
52
54
  end
53
55
 
54
56
  private
55
57
 
56
- # The event handler rescues so a failed audit write cannot break the
57
- # instrumented code path, but a log line alone is invisible to error
58
- # tracking — so report it as handled too.
59
- def report_error(error, event_name)
60
- return unless Rails.respond_to?(:error) && Rails.error
61
-
62
- Rails.error.report(
63
- error,
64
- handled: true,
65
- context: { StandardAudit.config.audit_error_context_key => event_name,
66
- subscriber: self.class.name }
67
- )
68
- rescue => report_failure
69
- Rails.logger.error("[StandardAudit] Error reporting audit failure: #{report_failure.class}: #{report_failure.message}")
70
- end
71
-
72
58
  def matches_subscription?(name)
73
59
  StandardAudit.config.subscriptions.any? { |pattern| pattern_match?(pattern, name) }
74
60
  end
@@ -99,20 +85,18 @@ module StandardAudit
99
85
  end
100
86
 
101
87
  # `_tags` and `_source` are reserved metadata keys owned by this
102
- # subscriber. Sensitive-key filtering is handled downstream by
103
- # `StandardAudit.record`, so we don't re-run it here.
104
- def build_metadata(payload, tags, source_location, config)
105
- reserved = RESERVED_PAYLOAD_KEYS.map(&:to_s)
106
- raw = payload.reject { |k, _| reserved.include?(k.to_s) }
107
- raw = config.metadata_builder.call(raw) if config.metadata_builder
88
+ # subscriber. They are merged in AFTER `metadata_builder` runs (see
89
+ # StandardAudit.write_entry), so a builder never sees or strips them.
90
+ def reserved_metadata(tags, source_location)
91
+ reserved = {}
108
92
 
109
93
  if tags.is_a?(Hash) && tags.any?
110
- raw[:_tags] = tags
94
+ reserved[:_tags] = tags
111
95
  elsif tags && !tags.is_a?(Hash)
112
96
  Rails.logger.warn("[StandardAudit] Dropping Rails.event tags of unexpected type: #{tags.class}")
113
97
  end
114
- raw[:_source] = source_location if source_location
115
- raw
98
+ reserved[:_source] = source_location if source_location
99
+ reserved
116
100
  end
117
101
  end
118
102
  end
@@ -0,0 +1,81 @@
1
+ require "standard_audit"
2
+
3
+ # Shared example guarding the `configure(baseline: true)` contract: that the
4
+ # app's StandardAudit configuration survives `StandardAudit.reset_configuration!`
5
+ # (which `require "standard_audit/rspec"` runs before every example).
6
+ #
7
+ # require "standard_audit/rspec/baseline" # or "standard_audit/rspec"
8
+ #
9
+ # RSpec.describe "StandardAudit configuration baseline" do
10
+ # it_behaves_like "a standard_audit baseline",
11
+ # subscriptions: [/\Astandard_id\./, /\Aauthorization\./],
12
+ # settings: {
13
+ # retention_days: 1826,
14
+ # raise_on_audit_write_error: true,
15
+ # filter_nested_metadata: true,
16
+ # queue_name: :maintenance
17
+ # },
18
+ # catalogue: -> { AuditCatalogue::ACTIONS },
19
+ # sensitive_keys: %i[source_payload],
20
+ # sensitive_key_patterns: [/secret/i],
21
+ # present: %i[metadata_builder before_write current_scope_resolver]
22
+ # end
23
+ #
24
+ # Every option is optional. Behaviour held in lambdas (resolvers, builders)
25
+ # can't be compared by value, so list them under `present:` to assert they
26
+ # survive a reset, and keep an app-specific example for what they return.
27
+ RSpec.shared_examples "a standard_audit baseline" do |options = {}|
28
+ subscriptions = Array(options[:subscriptions])
29
+ settings = options.fetch(:settings, {})
30
+ catalogue = options[:catalogue]
31
+ sensitive_keys = Array(options[:sensitive_keys])
32
+ sensitive_key_patterns = Array(options[:sensitive_key_patterns])
33
+ present = Array(options[:present])
34
+
35
+ def standard_audit_baseline_assertions(subscriptions:, settings:, catalogue:, sensitive_keys:,
36
+ sensitive_key_patterns:, present:)
37
+ config = StandardAudit.config
38
+
39
+ expect(config.subscriptions).to include(*subscriptions) if subscriptions.any?
40
+ settings.each do |name, value|
41
+ expect(config.public_send(name)).to eq(value), "expected config.#{name} to be #{value.inspect} after a reset"
42
+ end
43
+ if catalogue
44
+ resolved = config.audit_catalogue.respond_to?(:call) ? config.audit_catalogue.call : config.audit_catalogue
45
+ expect(Array(resolved).map(&:to_s)).to match_array(Array(catalogue.call).map(&:to_s))
46
+ end
47
+ expect(config.sensitive_keys).to include(*sensitive_keys) if sensitive_keys.any?
48
+ expect(config.sensitive_key_patterns).to include(*sensitive_key_patterns) if sensitive_key_patterns.any?
49
+ present.each do |name|
50
+ expect(config.public_send(name)).not_to be_nil, "expected config.#{name} to survive a reset"
51
+ end
52
+ end
53
+
54
+ let(:standard_audit_baseline_options) do
55
+ { subscriptions: subscriptions, settings: settings, catalogue: catalogue,
56
+ sensitive_keys: sensitive_keys, sensitive_key_patterns: sensitive_key_patterns, present: present }
57
+ end
58
+
59
+ it "is registered with configure(baseline: true)" do
60
+ expect(StandardAudit.baseline_configured?).to be(true),
61
+ "config/initializers/standard_audit.rb must call StandardAudit.configure(baseline: true), " \
62
+ "or reset_configuration! silently reverts every example to gem defaults"
63
+ end
64
+
65
+ it "carries the app's configuration" do
66
+ standard_audit_baseline_assertions(**standard_audit_baseline_options)
67
+ end
68
+
69
+ it "restores it after a mutation and a reset" do
70
+ StandardAudit.config.sensitive_keys += %i[standard_audit_isolation_canary]
71
+ StandardAudit.config.subscribe_to "standard_audit.isolation_canary"
72
+ settings.each_key { |name| StandardAudit.config.public_send(:"#{name}=", nil) }
73
+ present.each { |name| StandardAudit.config.public_send(:"#{name}=", nil) }
74
+
75
+ StandardAudit.reset_configuration!
76
+
77
+ expect(StandardAudit.config.sensitive_keys).not_to include(:standard_audit_isolation_canary)
78
+ expect(StandardAudit.config.subscriptions).not_to include("standard_audit.isolation_canary")
79
+ standard_audit_baseline_assertions(**standard_audit_baseline_options)
80
+ end
81
+ end
@@ -0,0 +1,187 @@
1
+ require "standard_audit"
2
+
3
+ # Block matcher asserting that a block wrote an audit row.
4
+ #
5
+ # require "standard_audit/rspec/matchers" # or "standard_audit/rspec"
6
+ #
7
+ # expect { operation.call }
8
+ # .to have_audited("order.created")
9
+ # .by(account) # actor: a record, an RSpec matcher, or no arg for "any actor"
10
+ # .on(order) # target: same forms
11
+ # .within(organisation) # scope: same forms
12
+ # .with_metadata(total: 100) # subset match; values may be RSpec matchers
13
+ # .once # or .times(n); default is "at least one"
14
+ #
15
+ # expect { noop }.not_to have_audited("order.created")
16
+ #
17
+ # Only rows written DURING the block are considered, and only persisted ones:
18
+ # under `config.async = true` run jobs inline (or use `perform_enqueued_jobs`),
19
+ # and a `StandardAudit.batch` must be flushed inside the block.
20
+ #
21
+ # `.by` / `.on` / `.within` with no argument assert presence — a well-formed
22
+ # row with a resolvable GlobalID — which is what per-app helpers such as
23
+ # `expect_well_formed_audit` checked by hand.
24
+ module StandardAudit
25
+ module RSpec
26
+ module Matchers
27
+ class HaveAudited
28
+ include ::RSpec::Matchers::Composable
29
+
30
+ ANY = Object.new.freeze
31
+ private_constant :ANY
32
+
33
+ def initialize(event_type)
34
+ @event_type = event_type.to_s
35
+ @references = {}
36
+ @metadata = nil
37
+ @count = nil
38
+ end
39
+
40
+ def by(actor = ANY)
41
+ @references[:actor] = actor
42
+ self
43
+ end
44
+
45
+ def on(target = ANY)
46
+ @references[:target] = target
47
+ self
48
+ end
49
+
50
+ def within(scope = ANY)
51
+ @references[:scope] = scope
52
+ self
53
+ end
54
+ alias_method :scoped_to, :within
55
+
56
+ def with_metadata(expected)
57
+ @metadata = expected.to_h.deep_stringify_keys
58
+ self
59
+ end
60
+
61
+ def once
62
+ times(1)
63
+ end
64
+
65
+ def times(count)
66
+ @count = count
67
+ self
68
+ end
69
+ alias_method :exactly, :times
70
+
71
+ def supports_block_expectations?
72
+ true
73
+ end
74
+
75
+ def supports_value_expectations?
76
+ false
77
+ end
78
+
79
+ def matches?(block)
80
+ capture(block)
81
+ @count ? matching.size == @count : matching.any?
82
+ end
83
+
84
+ def does_not_match?(block)
85
+ raise ArgumentError, "have_audited(...).times/once is not supported with not_to" if @count
86
+
87
+ capture(block)
88
+ matching.empty?
89
+ end
90
+
91
+ def description
92
+ parts = ["have audited #{@event_type.inspect}"]
93
+ @references.each { |role, value| parts << "#{preposition(role)} #{describe_value(value)}" }
94
+ parts << "with metadata including #{@metadata.inspect}" if @metadata
95
+ parts << (@count == 1 ? "once" : "#{@count} times") if @count
96
+ parts.join(" ")
97
+ end
98
+
99
+ def failure_message
100
+ "expected block to #{description}, but #{found_summary}"
101
+ end
102
+
103
+ def failure_message_when_negated
104
+ "expected block not to #{description}, but it wrote:\n#{format_rows(matching)}"
105
+ end
106
+
107
+ private
108
+
109
+ def capture(block)
110
+ before_ids = StandardAudit::AuditLog.pluck(:id)
111
+ block.call
112
+ @written = StandardAudit::AuditLog.where.not(id: before_ids).order(:created_at, :id).to_a
113
+ @candidates = @written.select { |log| log.event_type == @event_type }
114
+ end
115
+
116
+ def matching
117
+ @matching ||= @candidates.select { |log| row_matches?(log) }
118
+ end
119
+
120
+ def row_matches?(log)
121
+ @references.all? { |role, expected| reference_matches?(log, role, expected) } &&
122
+ metadata_matches?(log.metadata || {})
123
+ end
124
+
125
+ def metadata_matches?(actual)
126
+ return true if @metadata.nil?
127
+
128
+ @metadata.all? { |key, value| actual.key?(key) && values_match?(value, actual[key]) }
129
+ end
130
+
131
+ def reference_matches?(log, role, expected)
132
+ gid = log.public_send(:"#{role}_gid")
133
+
134
+ if expected.equal?(ANY)
135
+ gid.present? && GlobalID.parse(gid)&.model_id.present?
136
+ elsif expected.respond_to?(:to_global_id) && !matcher?(expected)
137
+ gid == expected.to_global_id.to_s
138
+ else
139
+ values_match?(expected, log.public_send(role))
140
+ end
141
+ end
142
+
143
+ def matcher?(object)
144
+ ::RSpec::Matchers.is_a_matcher?(object)
145
+ end
146
+
147
+ def preposition(role)
148
+ { actor: "by", target: "on", scope: "within" }.fetch(role)
149
+ end
150
+
151
+ def describe_value(value)
152
+ return "any record" if value.equal?(ANY)
153
+ return value.description if matcher?(value) && value.respond_to?(:description)
154
+ return value.to_global_id.to_s if value.respond_to?(:to_global_id)
155
+
156
+ value.inspect
157
+ end
158
+
159
+ def found_summary
160
+ if @candidates.empty?
161
+ others = @written.map(&:event_type).uniq
162
+ others.empty? ? "no audit rows were written" : "it wrote only: #{others.join(", ")}"
163
+ elsif @count
164
+ "#{matching.size} matching row(s) were written:\n#{format_rows(@candidates)}"
165
+ else
166
+ "no #{@event_type.inspect} row matched. Rows written:\n#{format_rows(@candidates)}"
167
+ end
168
+ end
169
+
170
+ def format_rows(rows)
171
+ rows.map { |log|
172
+ " #{log.event_type} actor=#{log.actor_gid.inspect} target=#{log.target_gid.inspect} " \
173
+ "scope=#{log.scope_gid.inspect} metadata=#{log.metadata.inspect}"
174
+ }.join("\n")
175
+ end
176
+ end
177
+
178
+ def have_audited(event_type)
179
+ HaveAudited.new(event_type)
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ RSpec.configure do |config|
186
+ config.include StandardAudit::RSpec::Matchers
187
+ end
@@ -1,5 +1,12 @@
1
1
  require "standard_audit"
2
+ require "standard_audit/rspec/matchers"
3
+ require "standard_audit/rspec/baseline"
2
4
 
5
+ # StandardAudit test support. Loads the `have_audited` matcher
6
+ # (standard_audit/rspec/matchers) and the "a standard_audit baseline" shared
7
+ # example (standard_audit/rspec/baseline), and resets StandardAudit state
8
+ # between examples.
9
+ #
3
10
  # StandardAudit state reset between examples.
4
11
  #
5
12
  # - Clears the thread-local batch buffer so a spec that exits inside a
@@ -27,90 +27,32 @@ module StandardAudit
27
27
 
28
28
  # The event handler rescues so a failed audit write cannot break the
29
29
  # instrumented code path, but a log line alone is invisible to error
30
- # tracking — so report it as handled too.
31
- def report_error(error, event_name)
32
- return unless Rails.respond_to?(:error) && Rails.error
33
-
34
- Rails.error.report(
35
- error,
36
- handled: true,
37
- context: { StandardAudit.config.audit_error_context_key => event_name,
38
- subscriber: self.class.name }
39
- )
40
- rescue => report_failure
41
- Rails.logger.error("[StandardAudit] Error reporting audit failure: #{report_failure.class}: #{report_failure.message}")
42
- end
43
-
30
+ # tracking — so it is reported as handled too.
31
+ #
32
+ # The write itself goes through StandardAudit.write_entry, the same path
33
+ # `StandardAudit.record` uses, so batching, `current_scope_resolver`,
34
+ # `metadata_builder` and `before_write` behave identically here. Until
35
+ # 0.12.0 this method carried its own copy of the write logic, which ignored
36
+ # `StandardAudit.batch`.
44
37
  def handle_event(event)
45
- return unless StandardAudit.config.enabled
46
-
47
38
  config = StandardAudit.config
48
- payload = event.payload
49
-
50
- actor = config.actor_extractor.call(payload)
51
- target = config.target_extractor.call(payload)
52
- scope = config.scope_extractor.call(payload)
53
-
54
- # Fall back to Current attributes when payload values are nil
55
- actor ||= config.current_actor_resolver.call
56
-
57
- metadata = extract_metadata(payload, config)
39
+ return unless config.enabled
58
40
 
59
- attrs = {
60
- event_type: event.name,
61
- occurred_at: Time.current,
62
- request_id: payload[:request_id] || config.current_request_id_resolver.call,
63
- ip_address: payload[:ip_address] || config.current_ip_address_resolver.call,
64
- user_agent: payload[:user_agent] || config.current_user_agent_resolver.call,
65
- session_id: payload[:session_id] || config.current_session_id_resolver.call,
66
- metadata: metadata
67
- }
41
+ payload = event.payload
68
42
 
69
- if config.async
70
- job_attrs = attrs.dup
71
- job_attrs[:actor_gid] = actor&.to_global_id&.to_s
72
- job_attrs[:target_gid] = target&.to_global_id&.to_s
73
- job_attrs[:scope_gid] = scope&.to_global_id&.to_s
74
- job_attrs[:actor_type] = actor&.class&.name
75
- job_attrs[:target_type] = target&.class&.name
76
- job_attrs[:scope_type] = scope&.class&.name
77
- StandardAudit::CreateAuditLogJob.perform_later(job_attrs.stringify_keys)
78
- else
79
- log = StandardAudit::AuditLog.new(attrs)
80
- log.actor = actor
81
- log.target = target
82
- log.scope = scope
83
- log.save!
84
- end
43
+ StandardAudit.write_entry(
44
+ event.name,
45
+ actor: config.actor_extractor.call(payload),
46
+ target: config.target_extractor.call(payload),
47
+ scope: config.scope_extractor.call(payload),
48
+ metadata: payload.except(*EXCLUDED_PAYLOAD_KEYS),
49
+ context: payload.slice(:request_id, :ip_address, :user_agent, :session_id)
50
+ )
85
51
  rescue => e
86
- Rails.logger.error("[StandardAudit] Error creating audit log: #{e.class}: #{e.message}")
87
- report_error(e, event.name)
52
+ StandardAudit.report_write_error(e, event.name, subscriber: self.class.name)
88
53
  end
89
54
 
90
- def extract_metadata(payload, config)
91
- # Remove known non-metadata keys
92
- excluded_keys = %i[actor target scope request_id ip_address user_agent session_id]
93
- raw_metadata = payload.except(*excluded_keys)
94
-
95
- if config.metadata_builder
96
- raw_metadata = config.metadata_builder.call(raw_metadata)
97
- end
98
-
99
- # ActiveRecord objects in the payload are replaced by a reference BEFORE
100
- # any key-based redaction, because the leak they cause is not a
101
- # sensitive *key* — it is a value that serialises as an entire database
102
- # row (rarebit-one/rarebit-ops#296). Runs after `metadata_builder` so a
103
- # host that derives fields from a record (`payload[:account].email`)
104
- # still sees the record.
105
- if config.dereference_record_metadata
106
- raw_metadata = StandardAudit::RecordReference.call(raw_metadata)
107
- end
108
-
109
- # Redaction lives in MetadataFilter, shared with StandardAudit.record.
110
- # This path previously carried its own copy that did *not* subtract
111
- # RESERVED_METADATA_KEYS, so `_tags`/`_source` were strippable here and
112
- # not there.
113
- StandardAudit::MetadataFilter.call(raw_metadata, config: config)
114
- end
55
+ EXCLUDED_PAYLOAD_KEYS = %i[actor target scope request_id ip_address user_agent session_id].freeze
56
+ private_constant :EXCLUDED_PAYLOAD_KEYS
115
57
  end
116
58
  end
@@ -1,3 +1,3 @@
1
1
  module StandardAudit
2
- VERSION = "0.11.1"
2
+ VERSION = "0.12.1"
3
3
  end