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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module StandardAudit
2
- VERSION = "0.7.0"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -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
- previous_checksum = StandardAudit::AuditLog
157
- .order(created_at: :desc, id: :desc)
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 "\nTampered records detected: #{result[:failures].size}"
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.7.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