provenance 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 272ac0a7de2193660ff6e31bc2e5e1ef79f0809837057c3cce8df4b56c00e5f1
4
+ data.tar.gz: e34d95e42e38eb2c1e40d238cc981b2ee0d7c06829d24b9c134d9eae5b7cc41d
5
+ SHA512:
6
+ metadata.gz: f789e8800b2bd3082424a4f6567ba981f3b0d7f732ab8ec1bfde3f1f0d66dbed31290b4ad30a067491d27e6d2c6ed58490ffad662321a1083f34ee2b4fa30bd8
7
+ data.tar.gz: '03285aa742eb534df8d103659467b004dc5a040991351072d37e7120a16e144697172987cf54742ec11a259cd3482d21dd214d89df8f271e64806cfbb52518c2'
data/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2026-06-05
11
+
12
+ ### Added
13
+
14
+ - Initial public release.
15
+ - Model change tracking (create / update / destroy) via ActiveRecord callbacks,
16
+ grouped per request and per transaction.
17
+ - Controller-level auditing with automatic event-type generation.
18
+ - Error reporting through `audit_error(error, status)`.
19
+ - Bulk operation tracking for `update_all` / `delete_all` (opt-in).
20
+ - `has_and_belongs_to_many` join-table change tracking.
21
+ - Recursive sensitive-data filtering with global and per-model attribute lists.
22
+ - Pluggable value providers (username, roles, remote IP, origin IP, session id).
23
+ - Configurable delivery hooks for shipping audit events to any sink.
24
+ - Transaction-aware delivery: events are flushed only after every transaction
25
+ has committed, and discarded on rollback.
26
+
27
+ [Unreleased]: https://github.com/inikalaev/provenance/compare/v1.0.0...HEAD
28
+ [1.0.0]: https://github.com/inikalaev/provenance/releases/tag/v1.0.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Nikolaev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,333 @@
1
+ <div align="center">
2
+
3
+ # ๐Ÿ“œ Provenance
4
+
5
+ ### A drop-in audit trail for Rails โ€” every user action and model change, captured.
6
+
7
+ [![CI](https://github.com/inikalaev/provenance/actions/workflows/ci.yml/badge.svg)](https://github.com/inikalaev/provenance/actions/workflows/ci.yml)
8
+ [![Gem Version](https://img.shields.io/gem/v/provenance.svg)](https://rubygems.org/gems/provenance)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
10
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D.svg)](.ruby-version)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ Provenance watches your Rails app and records **who did what, to which record, when** โ€” then ships
17
+ a structured event anywhere you want: a log pipeline, a data warehouse, an external SIEM, or just
18
+ `Rails.logger`. You wire it in once; it stays out of your controllers and models.
19
+
20
+ ```ruby
21
+ # An audit event Provenance produces for a successful request
22
+ {
23
+ "event_type": "create_users",
24
+ "status": 201,
25
+ "username": "admin@example.com",
26
+ "remote_ip": "203.0.113.1",
27
+ "message": {
28
+ "count": 1,
29
+ "changes": [
30
+ { "model": "User", "model_id": 42, "action": "create",
31
+ "changes": { "attributes": { "email": "user@example.com", "password": "[FILTERED]" } } }
32
+ ]
33
+ },
34
+ "source": "myapp_production"
35
+ }
36
+ ```
37
+
38
+ ## โœจ Features
39
+
40
+ - ๐Ÿงพ **Model change tracking** โ€” `create` / `update` / `destroy` captured straight from ActiveRecord callbacks.
41
+ - ๐ŸŒ **Controller auditing** โ€” one `around_action` records every change made during a request and emits a single event.
42
+ - ๐Ÿงฌ **Transaction-aware** โ€” changes are grouped per transaction, flushed only after every transaction commits, and **discarded on rollback**.
43
+ - ๐Ÿ’ฅ **Error reporting** โ€” emit a dedicated audit event for failed requests with `audit_error`.
44
+ - ๐Ÿ—‚๏ธ **Bulk operations** โ€” opt-in tracking for `update_all` / `delete_all`, which normally bypass callbacks.
45
+ - ๐Ÿ”— **`has_and_belongs_to_many`** โ€” join-table writes are tracked through SQL notifications.
46
+ - ๐Ÿ›ก๏ธ **Sensitive-data filtering** โ€” recursive `[FILTERED]` redaction with global and per-model attribute lists.
47
+ - ๐Ÿ”Œ **Pluggable providers & hooks** โ€” decide how to resolve the actor and where events are delivered.
48
+ - ๐Ÿชถ **Fail-safe** โ€” auditing never breaks the underlying request or operation.
49
+
50
+ ## ๐Ÿ“ฆ Installation
51
+
52
+ Add it to your `Gemfile`:
53
+
54
+ ```ruby
55
+ gem "provenance"
56
+ ```
57
+
58
+ Then install:
59
+
60
+ ```bash
61
+ bundle install
62
+ ```
63
+
64
+ ## ๐Ÿš€ Quick start
65
+
66
+ ### 1. Configure the initializer
67
+
68
+ ```ruby
69
+ # config/initializers/provenance.rb
70
+ require "provenance"
71
+
72
+ Provenance.configure do |config|
73
+ config.source_name = "myapp_#{Rails.env}"
74
+ config.sensitive_attributes = %w[password password_confirmation token secret_key api_key]
75
+
76
+ # Auditing is disabled in the test environment by default. Override if needed:
77
+ # config.enabled = true
78
+ end
79
+
80
+ # How to resolve the actor and request metadata (each receives the controller):
81
+ Provenance.setup_username_provider(->(controller) { controller.current_user&.email })
82
+ Provenance.setup_roles_provider(->(controller) { controller.current_user&.roles || [] })
83
+ Provenance.setup_remote_ip_provider(->(controller) { controller.request.remote_ip })
84
+ Provenance.setup_origin_ip_provider(->(controller) { ENV["SERVER_IP"] || "127.0.0.1" })
85
+ Provenance.setup_session_id_provider(->(controller) { controller.request.headers["Authorization"]&.split(" ")&.last })
86
+
87
+ # Where audit events go (you can register more than one hook):
88
+ Provenance.config.add_audit_hook do |audit_data|
89
+ Rails.logger.info("AUDIT: #{audit_data.to_json}")
90
+ end
91
+ ```
92
+
93
+ ### 2. Mix the concerns in
94
+
95
+ ```ruby
96
+ # app/controllers/application_controller.rb
97
+ class ApplicationController < ActionController::API
98
+ include Provenance::Auditable # records changes per request
99
+ include Provenance::ErrorReporting # adds audit_error(error, status)
100
+ end
101
+ ```
102
+
103
+ ```ruby
104
+ # app/models/application_record.rb
105
+ class ApplicationRecord < ActiveRecord::Base
106
+ self.abstract_class = true
107
+ include Provenance::Trackable
108
+ end
109
+ ```
110
+
111
+ That's it. Every write that happens inside a request now produces an audit event.
112
+
113
+ ## ๐Ÿงญ How it works
114
+
115
+ ```
116
+ Request โ”€โ–ถ Auditable (around_action)
117
+ โ”‚ opens a Journal for this request
118
+ โ–ผ
119
+ Trackable callbacks โ”€โ”€โ–ถ Journal โ—€โ”€โ”€ BulkOperations / HABTM SQL
120
+ (create/update/destroy) (grouped by transaction)
121
+ โ”‚
122
+ โ–ผ
123
+ all transactions committed?
124
+ โ”‚ yes โ”‚ rollback
125
+ โ–ผ โ–ผ
126
+ audit hooks receive changes discarded
127
+ one assembled event
128
+ ```
129
+
130
+ The `Journal` lives in fiber-local storage for the duration of the request, so concurrent requests
131
+ never see each other's changes.
132
+
133
+ ## ๐Ÿ’ฅ Error logging
134
+
135
+ Call `audit_error` from your rescue handlers to record failures:
136
+
137
+ ```ruby
138
+ def render_errors(errors, status: :unprocessable_entity)
139
+ audit_error(errors, status)
140
+ render json: { errors: Array(errors) }, status: status
141
+ end
142
+
143
+ rescue_from ActiveRecord::RecordNotFound do
144
+ audit_error("Not found", :not_found)
145
+ head :not_found
146
+ end
147
+ ```
148
+
149
+ Symbolic statuses (`:not_found`, `:unauthorized`, `:forbidden`, `:unprocessable_entity`, `:conflict`)
150
+ are mapped to their numeric codes automatically; anything else defaults to `500`.
151
+
152
+ ## ๐Ÿ›ก๏ธ Sensitive data filtering
153
+
154
+ Filtered values are replaced with `[FILTERED]` โ€” in model attributes, request params, and nested
155
+ hashes/arrays alike.
156
+
157
+ ```ruby
158
+ # Global (applies everywhere)
159
+ Provenance.configure do |config|
160
+ config.sensitive_attributes = %w[password token api_key]
161
+ end
162
+
163
+ # Per-model (takes priority over the global list)
164
+ class Payment < ApplicationRecord
165
+ sensitive_attributes :card_number, :cvv, :token
166
+ end
167
+ ```
168
+
169
+ ```ruby
170
+ # in # out
171
+ { user: { { user: {
172
+ email: "user@example.com", email: "user@example.com",
173
+ password: "secret", password: "[FILTERED]",
174
+ profile: { api_key: "abc123" } profile: { api_key: "[FILTERED]" }
175
+ } } } }
176
+ ```
177
+
178
+ ## ๐Ÿ—‚๏ธ Bulk operations
179
+
180
+ `update_all` and `delete_all` skip ActiveRecord callbacks, so they are tracked separately and must be
181
+ enabled explicitly:
182
+
183
+ ```ruby
184
+ Provenance.configure do |config|
185
+ config.track_bulk_operations = true # default: false
186
+ config.bulk_operations_max_ids = 1000 # cap ids per record; over the cap sets truncated: true
187
+ end
188
+ ```
189
+
190
+ The affected ids are collected with a `pluck` **before** the statement runs, so factor in one extra
191
+ query on large result sets. Operations outside an HTTP request (migrations, rake tasks, background
192
+ jobs) are not tracked. A bulk change looks like:
193
+
194
+ ```ruby
195
+ {
196
+ model: "Comment",
197
+ model_ids: ["101", "102"],
198
+ action: "bulk_update", # or "bulk_delete"
199
+ count: 2,
200
+ changes: { status: "deleted", deleted_at: "2026-06-05T12:00:00Z" }
201
+ }
202
+ ```
203
+
204
+ ## ๐Ÿ”— has_and_belongs_to_many
205
+
206
+ Join-table inserts and deletes never trigger model callbacks, so Provenance observes them through
207
+ `sql.active_record` notifications and folds them into the owner's change as an `*_ids` update. No extra
208
+ setup is required beyond including `Provenance::Trackable` in the participating models.
209
+
210
+ > **Note:** the SQL reconstruction for HABTM is tuned for PostgreSQL bind placeholders (`$1`, `$2`).
211
+ > Insert tracking is portable; delete tracking depends on that placeholder style.
212
+
213
+ ## โš™๏ธ Fine-tuning
214
+
215
+ ### Skip auditing per action
216
+
217
+ ```ruby
218
+ class UsersController < ApplicationController
219
+ skip_audit_logging :index, :show # no event at all
220
+ skip_model_change_tracking :index # event, but without model diffs
221
+ end
222
+ ```
223
+
224
+ ### Custom event types
225
+
226
+ ```ruby
227
+ class SessionsController < ApplicationController
228
+ custom_audit_event_type :create, "user_login"
229
+ custom_audit_event_type :destroy, "user_logout"
230
+ end
231
+ ```
232
+
233
+ ### Automatic event-type generation
234
+
235
+ When you don't override it, Provenance derives the event type from the controller and action:
236
+
237
+ | Action | Event type | Example (`UsersController`) |
238
+ | ----------------- | ------------------------- | ---------------------------------- |
239
+ | `index` | `read_{controller}` | `read_users` |
240
+ | `show` | `show_{singular}` | `show_user` |
241
+ | `create` | `create_{controller}` | `create_users` |
242
+ | `update` | `update_{singular}` | `update_user` |
243
+ | `destroy` | `destroy_{singular}` | `destroy_user` |
244
+ | _custom action_ | `{action}_{controller}` | `archive_users` |
245
+
246
+ Namespaced controllers are flattened with `_`, e.g. `Admin::UsersController#index` โ†’ `read_admin_users`.
247
+
248
+ ### Delivery hooks
249
+
250
+ ```ruby
251
+ Provenance.config.add_audit_hook { |event| ExternalAuditService.deliver(event) }
252
+ Provenance.config.add_audit_hook { |event| Rails.logger.info("AUDIT: #{event.to_json}") }
253
+ Provenance.config.clear_audit_hooks # remove all hooks
254
+ ```
255
+
256
+ ## ๐Ÿ”ง Configuration reference
257
+
258
+ | Option | Default | Description |
259
+ | --------------------------- | -------------------- | ------------------------------------------------------- |
260
+ | `source_name` | `"app_#{Rails.env}"` | Identifies the emitting application in every event. |
261
+ | `sensitive_attributes` | `[]` | Global attribute names to redact. |
262
+ | `enabled` | `!Rails.env.test?` | Master switch for the whole pipeline. |
263
+ | `track_bulk_operations` | `false` | Track `update_all` / `delete_all`. |
264
+ | `bulk_operations_max_ids` | `1000` | Max ids recorded per bulk change. |
265
+ | `audit_hooks` | `[]` | Delivery callbacks (use `add_audit_hook`). |
266
+
267
+ Providers: `username`, `roles`, `remote_ip`, `origin_ip`, `session_id` โ€” each set via
268
+ `Provenance.setup_<name>_provider(callable)`.
269
+
270
+ ## ๐Ÿ“ Event structure
271
+
272
+ **Successful request**
273
+
274
+ ```json
275
+ {
276
+ "timestamp": "2026-06-05T12:00:00.000Z",
277
+ "event_type": "create_users",
278
+ "status": 201,
279
+ "message": {
280
+ "count": 1,
281
+ "changes": [
282
+ {
283
+ "model": "User",
284
+ "model_id": 123,
285
+ "action": "create",
286
+ "changes": { "attributes": { "email": "user@example.com", "name": "John Doe" } },
287
+ "timestamp": "2026-06-05T12:00:00.000Z"
288
+ }
289
+ ],
290
+ "params": { "user": { "email": "user@example.com" } }
291
+ },
292
+ "username": "admin@example.com",
293
+ "remote_ip": "203.0.113.1",
294
+ "origin_ip": "192.168.1.100",
295
+ "session_id": "token123",
296
+ "roles": ["admin"],
297
+ "request_id": "req-123",
298
+ "source": "myapp_production"
299
+ }
300
+ ```
301
+
302
+ **Failed request** (via `audit_error`)
303
+
304
+ ```json
305
+ {
306
+ "timestamp": "2026-06-05T12:00:00.000Z",
307
+ "event_type": "create_users",
308
+ "status": "422",
309
+ "message": {
310
+ "error_type": "ActiveRecord::RecordInvalid",
311
+ "error_message": "Validation failed: Email has already been taken",
312
+ "params": { "user": { "email": "invalid" } }
313
+ },
314
+ "username": "admin@example.com",
315
+ "source": "myapp_production"
316
+ }
317
+ ```
318
+
319
+ ## ๐Ÿงช Development
320
+
321
+ ```bash
322
+ bundle install
323
+ bundle exec rspec # run the test suite
324
+ bundle exec rubocop # lint
325
+ ```
326
+
327
+ ## ๐Ÿค Contributing
328
+
329
+ Bug reports and pull requests are welcome โ€” see [CONTRIBUTING.md](CONTRIBUTING.md).
330
+
331
+ ## ๐Ÿ“„ License
332
+
333
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Provenance
4
+ # Holds the global configuration: the data source name, the list of sensitive
5
+ # attributes, the delivery hooks and the value providers used to enrich every
6
+ # audit event.
7
+ class Configuration
8
+ attr_accessor :source_name, :sensitive_attributes, :audit_hooks, :enabled,
9
+ :username_provider, :roles_provider, :remote_ip_provider, :origin_ip_provider, :session_id_provider,
10
+ :track_bulk_operations, :bulk_operations_max_ids
11
+
12
+ def initialize
13
+ @source_name = "app_#{Rails.env}"
14
+ @sensitive_attributes = []
15
+ @audit_hooks = []
16
+ @enabled = !Rails.env.test?
17
+ @track_bulk_operations = false
18
+ @bulk_operations_max_ids = 1000
19
+ @username_provider = nil
20
+ @roles_provider = nil
21
+ @remote_ip_provider = nil
22
+ @origin_ip_provider = nil
23
+ @session_id_provider = nil
24
+ end
25
+
26
+ def add_audit_hook(&block)
27
+ @audit_hooks << block
28
+ end
29
+
30
+ def clear_audit_hooks
31
+ @audit_hooks.clear
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Provenance
6
+ # Per-request scratch space backed by fiber-local storage. Holds the active
7
+ # journal, the deferred delivery callback and the request/response metadata
8
+ # for the duration of a single request.
9
+ class Context
10
+ class << self
11
+ def journal
12
+ Thread.current[:provenance_journal]
13
+ end
14
+
15
+ def journal=(value)
16
+ Thread.current[:provenance_journal] = value
17
+ end
18
+
19
+ def pending_audit_log
20
+ Thread.current[:provenance_pending_log]
21
+ end
22
+
23
+ def pending_audit_log=(callback)
24
+ Thread.current[:provenance_pending_log] = callback
25
+ end
26
+
27
+ def request_id
28
+ Thread.current[:provenance_request_id]
29
+ end
30
+
31
+ def request_id=(value)
32
+ Thread.current[:provenance_request_id] = value
33
+ end
34
+
35
+ def response_status
36
+ Thread.current[:provenance_response_status]
37
+ end
38
+
39
+ def response_status=(value)
40
+ Thread.current[:provenance_response_status] = value
41
+ Thread.current[:provenance_request_completed] = true
42
+ end
43
+
44
+ def request_completed?
45
+ Thread.current[:provenance_request_completed] || false
46
+ end
47
+
48
+ def cleanup
49
+ Thread.current[:provenance_journal] = nil
50
+ Thread.current[:provenance_pending_log] = nil
51
+ Thread.current[:provenance_request_id] = nil
52
+ Thread.current[:provenance_send_scheduled] = nil
53
+ Thread.current[:provenance_response_status] = nil
54
+ Thread.current[:provenance_request_completed] = nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Provenance
4
+ # Accumulates model and bulk changes for the current request, grouped by
5
+ # transaction key so they can be discarded on rollback and flushed together
6
+ # once every transaction has completed.
7
+ class Journal
8
+ attr_reader :changes
9
+
10
+ def initialize
11
+ @changes = []
12
+ @active_transactions = Set.new
13
+ end
14
+
15
+ def register_transaction(transaction_id)
16
+ return if transaction_id.nil?
17
+
18
+ @active_transactions.add(transaction_id)
19
+ end
20
+
21
+ def complete_transaction(transaction_id)
22
+ return if transaction_id.nil?
23
+
24
+ @active_transactions.delete(transaction_id)
25
+ end
26
+
27
+ def add_change(model, action, data)
28
+ change_data = {
29
+ model: model.class.name,
30
+ model_id: model.id,
31
+ action: action.to_s,
32
+ changes: filter_sensitive_data(data, model.class),
33
+ timestamp: Time.current.utc.iso8601(3)
34
+ }
35
+
36
+ change_data[:transaction_id] = data[:transaction_id] if data.is_a?(Hash) && data[:transaction_id]
37
+
38
+ @changes << change_data
39
+ end
40
+
41
+ def add_bulk_change(model_class, action, ids, updates, truncated: false, transaction_id: nil)
42
+ change_data = {
43
+ model: model_class.name,
44
+ model_ids: ids,
45
+ action: action.to_s,
46
+ count: ids.size,
47
+ changes: updates.nil? ? {} : filter_sensitive_data(updates, model_class),
48
+ timestamp: Time.current.utc.iso8601(3)
49
+ }
50
+ change_data[:truncated] = true if truncated
51
+ change_data[:transaction_id] = transaction_id if transaction_id
52
+
53
+ @changes << change_data
54
+ end
55
+
56
+ def remove_changes_for_transaction(transaction_id)
57
+ return if transaction_id.nil?
58
+
59
+ @changes.reject! { |change| change[:transaction_id] == transaction_id }
60
+ @active_transactions.delete(transaction_id)
61
+ end
62
+
63
+ def all_transactions_completed?
64
+ @active_transactions.empty?
65
+ end
66
+
67
+ def to_h
68
+ {
69
+ count: @changes.size,
70
+ changes: @changes
71
+ }
72
+ end
73
+
74
+ def clear!
75
+ @changes.clear
76
+ @active_transactions.clear
77
+ end
78
+
79
+ def empty?
80
+ @changes.empty?
81
+ end
82
+
83
+ def present?
84
+ @changes.any?
85
+ end
86
+
87
+ private
88
+
89
+ def filter_sensitive_data(data, model_class = nil)
90
+ sensitive_attrs = model_sensitive_attributes(model_class)
91
+ return data if sensitive_attrs.empty?
92
+
93
+ case data
94
+ when Hash
95
+ filtered_data = {}
96
+ data.each do |key, value|
97
+ filtered_data[key] = if key == "transaction_id"
98
+ value
99
+ elsif sensitive_attrs.include?(key.to_s)
100
+ "[FILTERED]"
101
+ else
102
+ filter_sensitive_data(value, model_class)
103
+ end
104
+ end
105
+ filtered_data
106
+ when Array
107
+ data.map { |item| filter_sensitive_data(item, model_class) }
108
+ else
109
+ data
110
+ end
111
+ end
112
+
113
+ def model_sensitive_attributes(model_class)
114
+ attributes = model_class&.attribute_names
115
+ if model_class.respond_to?(:sensitive_attributes_list)
116
+ attrs = model_class.sensitive_attributes_list
117
+ attributes = attrs.map(&:to_s)
118
+ end
119
+
120
+ (Provenance.config.sensitive_attributes + attributes).map(&:to_s)
121
+ end
122
+ end
123
+ end