standard_audit 0.7.0 → 0.9.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 +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +222 -0
- data/app/models/standard_audit/audit_log.rb +259 -41
- data/lib/generators/standard_audit/add_previous_checksum/add_previous_checksum_generator.rb +17 -0
- data/lib/generators/standard_audit/add_previous_checksum/templates/add_previous_checksum_to_audit_logs.rb.erb +23 -0
- data/lib/generators/standard_audit/install/templates/create_audit_logs.rb.erb +15 -0
- data/lib/standard_audit/configuration.rb +47 -1
- data/lib/standard_audit/operation/audit.rb +253 -0
- data/lib/standard_audit/operation.rb +205 -0
- data/lib/standard_audit/rspec/operation.rb +136 -0
- data/lib/standard_audit/version.rb +1 -1
- data/lib/standard_audit.rb +4 -4
- data/lib/tasks/standard_audit_tasks.rake +20 -2
- metadata +6 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require "standard_audit"
|
|
2
|
+
|
|
3
|
+
# RSpec shared examples for the operation-audit contract.
|
|
4
|
+
#
|
|
5
|
+
# A THIN layer over StandardAudit::Operation::Audit — every example below is a
|
|
6
|
+
# one-line call to a plain-Ruby predicate plus a failure message. If the shape
|
|
7
|
+
# of these examples doesn't fit your app, call the predicates directly from a
|
|
8
|
+
# bespoke spec and skip this file; nothing here is load-bearing.
|
|
9
|
+
#
|
|
10
|
+
# require "standard_audit/rspec/operation"
|
|
11
|
+
#
|
|
12
|
+
# RSpec.describe "Operation audit declarations" do
|
|
13
|
+
# it_behaves_like "standard_audit operation declarations",
|
|
14
|
+
# source: "/app/operations/",
|
|
15
|
+
# minimum: 100,
|
|
16
|
+
# orphans_within: -> { AuditCatalogue::OPERATION_ACTIONS }
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Options (all optional):
|
|
20
|
+
#
|
|
21
|
+
# source: path fragment scoping the check to your own operations.
|
|
22
|
+
# Strongly recommended — without it, any class anywhere that
|
|
23
|
+
# includes the module is held to the contract.
|
|
24
|
+
# minimum: registry floor. THE MOST IMPORTANT OPTION. Without it, an
|
|
25
|
+
# app where someone stopped including the module — or where
|
|
26
|
+
# eager loading silently stopped reaching `app/operations/` —
|
|
27
|
+
# passes every other example vacuously against an empty set.
|
|
28
|
+
# Set it just below your real count.
|
|
29
|
+
# expected: Array of class names that must be registered. A second,
|
|
30
|
+
# sharper form of the same protection.
|
|
31
|
+
# orphans_within: the catalogue slice operations are responsible for, as an
|
|
32
|
+
# Array or a callable. Use it when your catalogue also covers
|
|
33
|
+
# writers outside `app/operations/` (a controller concern, a
|
|
34
|
+
# tool server, a notification-bus name), which would otherwise
|
|
35
|
+
# always look orphaned. Omit to skip the orphan check;
|
|
36
|
+
# pass `:catalogue` to check the whole catalogue.
|
|
37
|
+
# eager_load: force `Rails.application.eager_load!` first so the registry
|
|
38
|
+
# is complete regardless of the test env's setting.
|
|
39
|
+
# Defaults to true.
|
|
40
|
+
RSpec.shared_examples "standard_audit operation declarations" do |options = {}|
|
|
41
|
+
source = options[:source]
|
|
42
|
+
minimum = options[:minimum]
|
|
43
|
+
expected = Array(options[:expected])
|
|
44
|
+
orphans_within = options[:orphans_within]
|
|
45
|
+
eager_load = options.fetch(:eager_load, true)
|
|
46
|
+
|
|
47
|
+
audit = StandardAudit::Operation::Audit
|
|
48
|
+
|
|
49
|
+
before(:all) do
|
|
50
|
+
Rails.application.eager_load! if eager_load && defined?(Rails) && Rails.application
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
let(:operations) { audit.operations(source: source) }
|
|
54
|
+
|
|
55
|
+
it "sees the operations it is supposed to check" do
|
|
56
|
+
if expected.any?
|
|
57
|
+
expect(operations.map(&:name)).to include(*expected)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if minimum
|
|
61
|
+
expect(operations.size).to be >= minimum,
|
|
62
|
+
"Expected at least #{minimum} registered operations#{source ? " under #{source}" : ""}, " \
|
|
63
|
+
"found #{operations.size}. Every other example in this group passes vacuously against " \
|
|
64
|
+
"an empty set, so this is almost certainly a wiring failure — an operation that stopped " \
|
|
65
|
+
"including StandardAudit::Operation, or eager loading no longer reaching them."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
skip("no `minimum:` or `expected:` given — nothing to assert") if minimum.nil? && expected.empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "requires every operation to declare `audits` or `audit_none!`" do
|
|
72
|
+
undeclared = audit.undeclared(operations: operations)
|
|
73
|
+
|
|
74
|
+
expect(undeclared).to be_empty,
|
|
75
|
+
"These operations declare neither `audits` nor `audit_none!`:\n " \
|
|
76
|
+
"#{undeclared.map(&:name).sort.join("\n ")}\n" \
|
|
77
|
+
"Add `audits \"some.action\"` if it records an audit, or `audit_none!` " \
|
|
78
|
+
"(with the reason in a comment) if it deliberately doesn't."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "only declares actions present in the audit catalogue" do
|
|
82
|
+
unknown = audit.unknown_actions(operations: operations)
|
|
83
|
+
|
|
84
|
+
skip("no `config.audit_catalogue` configured") if audit.catalogue.nil?
|
|
85
|
+
|
|
86
|
+
expect(unknown).to be_empty,
|
|
87
|
+
"These operations declare audit actions missing from the catalogue " \
|
|
88
|
+
"(add them to it):\n " \
|
|
89
|
+
"#{unknown.map { |klass, actions| "#{klass.name}: #{actions.inspect}" }.join("\n ")}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "has no duplicate catalogue entries" do
|
|
93
|
+
skip("no `config.audit_catalogue` configured") if audit.catalogue.nil?
|
|
94
|
+
|
|
95
|
+
dupes = audit.duplicate_catalogue_entries
|
|
96
|
+
|
|
97
|
+
expect(dupes).to be_empty, "duplicate audit catalogue entries: #{dupes.inspect}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "operations that declare `audits` call `audit!`" do
|
|
101
|
+
missing = audit.missing_write_sites(operations: operations)
|
|
102
|
+
|
|
103
|
+
expect(missing).to be_empty,
|
|
104
|
+
"These operations declare `audits` but never call `audit!`:\n " \
|
|
105
|
+
"#{missing.map(&:name).sort.join("\n ")}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "operations that declare `audit_none!` do not call `audit!`" do
|
|
109
|
+
writing = audit.unexpected_write_sites(operations: operations)
|
|
110
|
+
|
|
111
|
+
expect(writing).to be_empty,
|
|
112
|
+
"These operations declare `audit_none!` but call `audit!`:\n " \
|
|
113
|
+
"#{writing.map(&:name).sort.join("\n ")}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "has no catalogued action that no operation writes" do
|
|
117
|
+
skip("no `orphans_within:` given") if orphans_within.nil?
|
|
118
|
+
|
|
119
|
+
within =
|
|
120
|
+
if orphans_within == :catalogue
|
|
121
|
+
nil
|
|
122
|
+
elsif orphans_within.respond_to?(:call)
|
|
123
|
+
orphans_within.call
|
|
124
|
+
else
|
|
125
|
+
orphans_within
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
skip("no `config.audit_catalogue` configured") if within.nil? && audit.catalogue.nil?
|
|
129
|
+
|
|
130
|
+
orphans = audit.orphan_actions(operations: operations, within: within)
|
|
131
|
+
|
|
132
|
+
expect(orphans).to be_empty,
|
|
133
|
+
"These catalogued actions are declared by no operation — wire them or " \
|
|
134
|
+
"drop them: #{orphans.inspect}"
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/standard_audit.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "standard_audit/sensitive_keys_dry_run"
|
|
|
6
6
|
require "standard_audit/subscriber"
|
|
7
7
|
require "standard_audit/event_subscriber"
|
|
8
8
|
require "standard_audit/reference_preloading"
|
|
9
|
+
require "standard_audit/operation"
|
|
9
10
|
require "standard_audit/auditable"
|
|
10
11
|
require "standard_audit/audit_scope"
|
|
11
12
|
require "standard_audit/checks/retention"
|
|
@@ -153,10 +154,8 @@ module StandardAudit
|
|
|
153
154
|
|
|
154
155
|
def flush_batch(buffer)
|
|
155
156
|
now = Time.current
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.limit(1)
|
|
159
|
-
.pick(:checksum)
|
|
157
|
+
records_parent = StandardAudit::AuditLog.chain_parent_column?
|
|
158
|
+
previous_checksum = StandardAudit::AuditLog.chain_tip_checksum
|
|
160
159
|
|
|
161
160
|
# Generate sorted UUIDs to ensure batch ordering matches id ordering.
|
|
162
161
|
# UUIDv7 within the same millisecond can have non-monotonic lower bits;
|
|
@@ -175,6 +174,7 @@ module StandardAudit
|
|
|
175
174
|
row.stringify_keys,
|
|
176
175
|
previous_checksum: previous_checksum
|
|
177
176
|
)
|
|
177
|
+
row[:previous_checksum] = previous_checksum if records_parent
|
|
178
178
|
row[:checksum] = checksum
|
|
179
179
|
previous_checksum = checksum
|
|
180
180
|
row
|
|
@@ -63,16 +63,34 @@ namespace :standard_audit do
|
|
|
63
63
|
puts "============================="
|
|
64
64
|
puts "Records verified: #{result[:verified]}"
|
|
65
65
|
puts "Chain valid: #{result[:valid]}"
|
|
66
|
+
puts "Forked links recovered: #{result[:recovered]}"
|
|
66
67
|
|
|
67
68
|
if result[:failures].any?
|
|
68
|
-
puts "\
|
|
69
|
+
puts "\nUnverifiable records detected: #{result[:failures].size}"
|
|
69
70
|
result[:failures].each do |failure|
|
|
70
|
-
puts " #{failure[:id]} (#{failure[:event_type]}) at #{failure[:created_at]}"
|
|
71
|
+
puts " #{failure[:id]} (#{failure[:event_type]}) at #{failure[:created_at]} — #{failure[:reason]}"
|
|
71
72
|
end
|
|
72
73
|
abort "Chain verification failed"
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
desc "Record the parent digest each existing row was signed against (never re-signs)"
|
|
78
|
+
task relink_checksums: :environment do
|
|
79
|
+
unless StandardAudit::AuditLog.chain_parent_column?
|
|
80
|
+
abort "audit_logs has no previous_checksum column — run `rails generate standard_audit:add_previous_checksum` first"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
result = StandardAudit::AuditLog.relink_checksums!
|
|
84
|
+
|
|
85
|
+
puts "Relinked: #{result[:relinked]}"
|
|
86
|
+
puts "Left as-is (already linked, or a segment root): #{result[:skipped]}"
|
|
87
|
+
puts "Unresolved: #{result[:unresolved]}"
|
|
88
|
+
puts ""
|
|
89
|
+
puts "No checksum was rewritten. A parent is recorded only when it reproduces"
|
|
90
|
+
puts "the digest the row has held since it was written; unresolved rows keep"
|
|
91
|
+
puts "failing verification, which is the point."
|
|
92
|
+
end
|
|
93
|
+
|
|
76
94
|
desc "Backfill checksums for records that don't have them"
|
|
77
95
|
task backfill_checksums: :environment do
|
|
78
96
|
count = StandardAudit::AuditLog.backfill_checksums!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_audit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -99,6 +99,8 @@ files:
|
|
|
99
99
|
- config/routes.rb
|
|
100
100
|
- lib/generators/standard_audit/add_checksums/add_checksums_generator.rb
|
|
101
101
|
- lib/generators/standard_audit/add_checksums/templates/add_checksum_to_audit_logs.rb.erb
|
|
102
|
+
- lib/generators/standard_audit/add_previous_checksum/add_previous_checksum_generator.rb
|
|
103
|
+
- lib/generators/standard_audit/add_previous_checksum/templates/add_previous_checksum_to_audit_logs.rb.erb
|
|
102
104
|
- lib/generators/standard_audit/install/install_generator.rb
|
|
103
105
|
- lib/generators/standard_audit/install/templates/create_audit_logs.rb.erb
|
|
104
106
|
- lib/generators/standard_audit/install/templates/initializer.rb.erb
|
|
@@ -110,8 +112,11 @@ files:
|
|
|
110
112
|
- lib/standard_audit/engine.rb
|
|
111
113
|
- lib/standard_audit/event_subscriber.rb
|
|
112
114
|
- lib/standard_audit/metadata_filter.rb
|
|
115
|
+
- lib/standard_audit/operation.rb
|
|
116
|
+
- lib/standard_audit/operation/audit.rb
|
|
113
117
|
- lib/standard_audit/reference_preloading.rb
|
|
114
118
|
- lib/standard_audit/rspec.rb
|
|
119
|
+
- lib/standard_audit/rspec/operation.rb
|
|
115
120
|
- lib/standard_audit/sensitive_keys_dry_run.rb
|
|
116
121
|
- lib/standard_audit/subscriber.rb
|
|
117
122
|
- lib/standard_audit/version.rb
|