standard_audit 0.11.0 → 0.11.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +26 -3
- data/app/models/standard_audit/audit_log.rb +23 -4
- data/lib/standard_audit/event_subscriber.rb +17 -0
- data/lib/standard_audit/subscriber.rb +17 -0
- data/lib/standard_audit/version.rb +1 -1
- data/lib/tasks/standard_audit_tasks.rake +32 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 697434bf73f4588971d9fda7503f13f53a848ab4fe058fbc05be5b567a186d6f
|
|
4
|
+
data.tar.gz: e5bad881dc6a5ab13ae22d13777176120967161f40356dcd6522ab4616492c1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: feb2b3354808c948aff042e0a3354ac2344c3e4ac1b03f0c375645ff7b1aaf1ec27253e87e764652ee7d52710a4c7aecd7748ba13475afa001a0c27f55a2ca79
|
|
7
|
+
data.tar.gz: a83422f4ef6b6ca319c29c450ae7cf82457cc5ad342345947a867e2243ef701c2bb0310a5b1d99f5cbf9c322b756f5094c42e77e53eb6ea3b828a834f94bfacc
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.1] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`rake standard_audit:anonymize_actor[gid]` and `standard_audit:export_actor[gid]` no longer raise `NoMethodError`.** The tasks passed the GlobalID string straight into `AuditLog.anonymize_actor!` / `export_for_actor`, which called `to_global_id` on it. Both methods now accept a record, a `GlobalID`, or a GlobalID string. A string is parsed, not located, so erasure works after the subject's row has been deleted; an invalid string raises `ArgumentError`. What gets anonymized is unchanged.
|
|
15
|
+
- **`rake standard_audit:cleanup` no longer deletes logs older than 90 days when `retention_days` is nil.** nil means keep forever, but the task fell back to a hard-coded 90. `cleanup` and `archive` (which always defaulted to 90) now take the days argument, else `config.retention_days`, else abort with a message saying how to set one.
|
|
16
|
+
- **`cleanup`/`archive` reject a days value that is not a positive integer.** `cleanup[abc]` used to become `0` days, i.e. delete every row. `0`, negatives and non-numeric values now abort. `StandardAudit::CleanupJob` is unchanged.
|
|
17
|
+
- **`StandardAudit::Subscriber` and `StandardAudit::EventSubscriber` report swallowed errors to `Rails.error`.** A failed audit write was only logged, so it never reached error tracking. It is now also reported with `handled: true` and context `{ <config.audit_error_context_key> => event_name, subscriber: <class name> }`. The log line is kept.
|
|
18
|
+
|
|
10
19
|
## [0.11.0] - 2026-07-31
|
|
11
20
|
|
|
12
21
|
### Security
|
data/README.md
CHANGED
|
@@ -499,6 +499,16 @@ This:
|
|
|
499
499
|
- Clears `ip_address`, `user_agent`, and `session_id`
|
|
500
500
|
- Removes metadata keys listed in `anonymizable_metadata_keys`
|
|
501
501
|
|
|
502
|
+
Both `anonymize_actor!` and `export_for_actor` accept the subject as a record,
|
|
503
|
+
a `GlobalID`, or a GlobalID string (`"gid://myapp/User/123"`). A string is
|
|
504
|
+
parsed, never located, so erasure still works after the user's own row has been
|
|
505
|
+
deleted — the usual order for an erasure request. Anything that is not a valid
|
|
506
|
+
GlobalID raises `ArgumentError`.
|
|
507
|
+
|
|
508
|
+
```ruby
|
|
509
|
+
StandardAudit::AuditLog.anonymize_actor!("gid://myapp/User/123")
|
|
510
|
+
```
|
|
511
|
+
|
|
502
512
|
### Right to Access (Export)
|
|
503
513
|
|
|
504
514
|
Export all audit data for a specific user:
|
|
@@ -525,7 +535,9 @@ STANDARD_AUDIT_RETENTION_DAYS=365 # keep 365 days
|
|
|
525
535
|
```
|
|
526
536
|
|
|
527
537
|
Infinite retention (the default) is the compliance-safe behavior: nothing is
|
|
528
|
-
ever auto-deleted.
|
|
538
|
+
ever auto-deleted. The `standard_audit:cleanup` and `standard_audit:archive`
|
|
539
|
+
rake tasks respect this: with no days argument and a nil `retention_days` they
|
|
540
|
+
abort rather than fall back to a default window. For financial/legal domains that is usually what you want;
|
|
529
541
|
enabling a finite window is a deliberate decision.
|
|
530
542
|
|
|
531
543
|
### Production retention warning (StandardHealth)
|
|
@@ -635,10 +647,12 @@ rake standard_audit:verify
|
|
|
635
647
|
# Record the parent digest each existing row was signed against
|
|
636
648
|
rake standard_audit:relink_checksums
|
|
637
649
|
|
|
638
|
-
# Delete logs older than N days
|
|
650
|
+
# Delete logs older than N days
|
|
639
651
|
rake standard_audit:cleanup[180]
|
|
652
|
+
# ...or older than config.retention_days
|
|
653
|
+
rake standard_audit:cleanup
|
|
640
654
|
|
|
641
|
-
# Archive old logs to a JSON file before deleting
|
|
655
|
+
# Archive old logs to a JSON file before deleting (same days rules)
|
|
642
656
|
rake standard_audit:archive[90,audit_backup.json]
|
|
643
657
|
|
|
644
658
|
# Show statistics
|
|
@@ -651,6 +665,15 @@ rake "standard_audit:anonymize_actor[gid://myapp/User/123]"
|
|
|
651
665
|
rake "standard_audit:export_actor[gid://myapp/User/123,export.json]"
|
|
652
666
|
```
|
|
653
667
|
|
|
668
|
+
`cleanup` and `archive` take the window from the days argument, else
|
|
669
|
+
`config.retention_days`; if neither is set they abort (a nil `retention_days`
|
|
670
|
+
means keep forever, so there is no implicit 90-day default). Days must be a
|
|
671
|
+
positive integer — `0`, negatives and non-numeric values abort instead of
|
|
672
|
+
deleting everything.
|
|
673
|
+
|
|
674
|
+
`anonymize_actor` and `export_actor` take a GlobalID string and work even after
|
|
675
|
+
the user record has been deleted.
|
|
676
|
+
|
|
654
677
|
## Database Support
|
|
655
678
|
|
|
656
679
|
The migration uses `json` column type by default, which works across:
|
|
@@ -92,8 +92,11 @@ module StandardAudit
|
|
|
92
92
|
|
|
93
93
|
# -- GDPR methods --
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
# `subject` may be a record, a GlobalID, or a GlobalID string
|
|
96
|
+
# ("gid://app/User/1"). A string is parsed, never located, so erasure and
|
|
97
|
+
# export still work after the subject's own row has been deleted.
|
|
98
|
+
def self.anonymize_actor!(subject)
|
|
99
|
+
gid = subject_gid_for(subject)
|
|
97
100
|
logs = where("actor_gid = ? OR target_gid = ?", gid, gid)
|
|
98
101
|
count = logs.count
|
|
99
102
|
|
|
@@ -122,8 +125,8 @@ module StandardAudit
|
|
|
122
125
|
count
|
|
123
126
|
end
|
|
124
127
|
|
|
125
|
-
def self.export_for_actor(
|
|
126
|
-
gid =
|
|
128
|
+
def self.export_for_actor(subject)
|
|
129
|
+
gid = subject_gid_for(subject)
|
|
127
130
|
logs = where("actor_gid = ? OR target_gid = ?", gid, gid).chronological
|
|
128
131
|
|
|
129
132
|
records = logs.map do |log|
|
|
@@ -149,6 +152,22 @@ module StandardAudit
|
|
|
149
152
|
}
|
|
150
153
|
end
|
|
151
154
|
|
|
155
|
+
# Normalises a GDPR subject to its GlobalID string. Parses rather than
|
|
156
|
+
# locates, so it works when the subject's row no longer exists.
|
|
157
|
+
def self.subject_gid_for(subject)
|
|
158
|
+
gid =
|
|
159
|
+
case subject
|
|
160
|
+
when GlobalID then subject
|
|
161
|
+
when String then GlobalID.parse(subject)
|
|
162
|
+
else subject.respond_to?(:to_global_id) ? subject.to_global_id : nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
raise ArgumentError, "expected a record, a GlobalID, or a GlobalID string (gid://app/Model/id), got #{subject.inspect}" unless gid
|
|
166
|
+
|
|
167
|
+
gid.to_s
|
|
168
|
+
end
|
|
169
|
+
private_class_method :subject_gid_for
|
|
170
|
+
|
|
152
171
|
# Recomputes the checksum from the record's current field values and the
|
|
153
172
|
# given previous checksum. Useful for verification without saving.
|
|
154
173
|
def compute_checksum_value(previous_checksum: nil)
|
|
@@ -48,10 +48,27 @@ module StandardAudit
|
|
|
48
48
|
)
|
|
49
49
|
rescue => e
|
|
50
50
|
Rails.logger.error("[StandardAudit] Error handling Rails.event: #{e.class}: #{e.message}")
|
|
51
|
+
report_error(e, name)
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
private
|
|
54
55
|
|
|
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
|
+
|
|
55
72
|
def matches_subscription?(name)
|
|
56
73
|
StandardAudit.config.subscriptions.any? { |pattern| pattern_match?(pattern, name) }
|
|
57
74
|
end
|
|
@@ -25,6 +25,22 @@ module StandardAudit
|
|
|
25
25
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
|
+
# The event handler rescues so a failed audit write cannot break the
|
|
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
|
+
|
|
28
44
|
def handle_event(event)
|
|
29
45
|
return unless StandardAudit.config.enabled
|
|
30
46
|
|
|
@@ -68,6 +84,7 @@ module StandardAudit
|
|
|
68
84
|
end
|
|
69
85
|
rescue => e
|
|
70
86
|
Rails.logger.error("[StandardAudit] Error creating audit log: #{e.class}: #{e.message}")
|
|
87
|
+
report_error(e, event.name)
|
|
71
88
|
end
|
|
72
89
|
|
|
73
90
|
def extract_metadata(payload, config)
|
|
@@ -1,16 +1,44 @@
|
|
|
1
|
+
module StandardAudit
|
|
2
|
+
module RakeSupport
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
# Resolves the retention window for cleanup/archive: the task argument,
|
|
6
|
+
# else config.retention_days, else abort. nil retention means "keep
|
|
7
|
+
# forever", so there is deliberately no hard-coded fallback. Anything that
|
|
8
|
+
# is not a positive integer aborts — `cleanup[abc]` used to become 0 days,
|
|
9
|
+
# i.e. delete everything.
|
|
10
|
+
def resolve_days!(task_name, arg)
|
|
11
|
+
raw = arg.presence || StandardAudit.config.retention_days
|
|
12
|
+
|
|
13
|
+
if raw.nil?
|
|
14
|
+
abort "standard_audit:#{task_name}: no retention window. Pass days " \
|
|
15
|
+
"(rake \"standard_audit:#{task_name}[90]\") or set config.retention_days " \
|
|
16
|
+
"/ STANDARD_AUDIT_RETENTION_DAYS. A nil retention_days means keep forever."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
days = Integer(raw.to_s.strip, 10, exception: false)
|
|
20
|
+
unless days&.positive?
|
|
21
|
+
abort "standard_audit:#{task_name}: days must be a positive integer, got #{raw.inspect}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
days
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
1
29
|
namespace :standard_audit do
|
|
2
|
-
desc "Delete audit logs older than
|
|
30
|
+
desc "Delete audit logs older than N days (default: config.retention_days; aborts if neither is set)"
|
|
3
31
|
task :cleanup, [:days] => :environment do |_t, args|
|
|
4
|
-
days = (args[:days]
|
|
32
|
+
days = StandardAudit::RakeSupport.resolve_days!("cleanup", args[:days])
|
|
5
33
|
cutoff = days.days.ago
|
|
6
34
|
|
|
7
35
|
deleted = StandardAudit::AuditLog.where("occurred_at < ?", cutoff).delete_all
|
|
8
36
|
puts "Deleted #{deleted} audit logs older than #{days} days"
|
|
9
37
|
end
|
|
10
38
|
|
|
11
|
-
desc "Archive audit logs to JSON file"
|
|
39
|
+
desc "Archive audit logs older than N days to a JSON file (default: config.retention_days; aborts if neither is set)"
|
|
12
40
|
task :archive, [:days, :output] => :environment do |_t, args|
|
|
13
|
-
days = (args[:days]
|
|
41
|
+
days = StandardAudit::RakeSupport.resolve_days!("archive", args[:days])
|
|
14
42
|
output = args[:output] || "audit_logs_archive_#{Date.current}.json"
|
|
15
43
|
cutoff = days.days.ago
|
|
16
44
|
|