standard_audit 0.6.0 → 0.8.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 +70 -0
- data/README.md +133 -2
- data/app/models/standard_audit/audit_log.rb +336 -79
- 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/generators/standard_audit/install/templates/initializer.rb.erb +46 -4
- data/lib/standard_audit/configuration.rb +64 -1
- data/lib/standard_audit/metadata_filter.rb +155 -0
- data/lib/standard_audit/reference_preloading.rb +298 -0
- data/lib/standard_audit/rspec.rb +17 -3
- data/lib/standard_audit/sensitive_keys_dry_run.rb +177 -0
- data/lib/standard_audit/subscriber.rb +5 -3
- data/lib/standard_audit/version.rb +1 -1
- data/lib/standard_audit.rb +50 -11
- data/lib/tasks/standard_audit_tasks.rake +61 -2
- metadata +6 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
3
|
+
module StandardAudit
|
|
4
|
+
# Answers "is this redaction rule safe for MY data?" against the rows an app
|
|
5
|
+
# already has, before the rule is switched on.
|
|
6
|
+
#
|
|
7
|
+
# This matters more here than in most gems: audit rows are append-only, so a
|
|
8
|
+
# rule that swallows real audit content cannot be undone after the fact — the
|
|
9
|
+
# content is simply never written from then on. Reading the historical rows
|
|
10
|
+
# first turns the judgement call into a command.
|
|
11
|
+
#
|
|
12
|
+
# Keys are extracted **in Ruby**, not with `jsonb_object_keys`, so this works
|
|
13
|
+
# against SQLite (the dummy app), MySQL, and Postgres alike. The install
|
|
14
|
+
# template's migration uses jsonb + GIN, but the gem itself stays
|
|
15
|
+
# backend-neutral.
|
|
16
|
+
#
|
|
17
|
+
# StandardAudit::SensitiveKeysDryRun.call(sensitive_key_patterns: [/secret/i])
|
|
18
|
+
# # => #<Report rows_scanned=1204 stripped={"client_secret"=>18, ...} ...>
|
|
19
|
+
#
|
|
20
|
+
class SensitiveKeysDryRun
|
|
21
|
+
# [rows_scanned] how many audit rows were read
|
|
22
|
+
# [stripped] key path => row count, for keys the candidate rule
|
|
23
|
+
# would redact
|
|
24
|
+
# [kept] key path => row count, for keys it would keep
|
|
25
|
+
# [nested_unfiltered] key path => row count, for *nested* keys the rule
|
|
26
|
+
# matches but which are NOT redacted because
|
|
27
|
+
# `filter_nested_metadata` is off. This is the
|
|
28
|
+
# exposure `filter_nested_metadata` exists to close;
|
|
29
|
+
# empty when nested filtering is enabled.
|
|
30
|
+
Report = Struct.new(:rows_scanned, :stripped, :kept, :nested_unfiltered, :nested, keyword_init: true) do
|
|
31
|
+
def any_stripped?
|
|
32
|
+
stripped.any?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
lines = []
|
|
37
|
+
lines << "Rows scanned: #{rows_scanned}"
|
|
38
|
+
lines << "Nested filtering: #{nested ? 'on' : 'off'}"
|
|
39
|
+
lines << ""
|
|
40
|
+
|
|
41
|
+
lines << section("WOULD BE STRIPPED", stripped, "Nothing would be stripped.")
|
|
42
|
+
lines << ""
|
|
43
|
+
lines << section("KEPT", kept, "No metadata keys found.")
|
|
44
|
+
|
|
45
|
+
if nested_unfiltered.any?
|
|
46
|
+
lines << ""
|
|
47
|
+
lines << section(
|
|
48
|
+
"MATCHES BUT NOT STRIPPED (nested; set config.filter_nested_metadata = true to redact)",
|
|
49
|
+
nested_unfiltered,
|
|
50
|
+
nil
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
lines.join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def section(title, counts, empty_message)
|
|
60
|
+
out = [title, "=" * title.length]
|
|
61
|
+
|
|
62
|
+
if counts.empty?
|
|
63
|
+
out << (empty_message || "None.")
|
|
64
|
+
else
|
|
65
|
+
width = counts.keys.map(&:length).max
|
|
66
|
+
counts.sort_by { |key, count| [-count, key] }.each do |key, count|
|
|
67
|
+
out << format(" %-#{width}s %d row(s)", key, count)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
out.join("\n")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
# Every keyword defaults to the app's live configuration, so calling it
|
|
77
|
+
# with no arguments reports what the *current* config does.
|
|
78
|
+
def call(sensitive_keys: nil, sensitive_key_patterns: nil, sensitive_key_exceptions: nil,
|
|
79
|
+
nested: nil, relation: nil, batch_size: 1000)
|
|
80
|
+
new(
|
|
81
|
+
sensitive_keys: sensitive_keys,
|
|
82
|
+
sensitive_key_patterns: sensitive_key_patterns,
|
|
83
|
+
sensitive_key_exceptions: sensitive_key_exceptions,
|
|
84
|
+
nested: nested
|
|
85
|
+
).call(relation: relation, batch_size: batch_size)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def initialize(sensitive_keys: nil, sensitive_key_patterns: nil, sensitive_key_exceptions: nil, nested: nil)
|
|
90
|
+
live = StandardAudit.config
|
|
91
|
+
|
|
92
|
+
candidate = Configuration.new
|
|
93
|
+
candidate.sensitive_keys = sensitive_keys || live.sensitive_keys
|
|
94
|
+
candidate.sensitive_key_patterns = sensitive_key_patterns || live.sensitive_key_patterns
|
|
95
|
+
candidate.sensitive_key_exceptions = sensitive_key_exceptions || live.sensitive_key_exceptions
|
|
96
|
+
|
|
97
|
+
@nested = nested.nil? ? live.filter_nested_metadata : nested
|
|
98
|
+
@filter = MetadataFilter.new(config: candidate)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def call(relation: nil, batch_size: 1000)
|
|
102
|
+
relation ||= StandardAudit::AuditLog.all
|
|
103
|
+
|
|
104
|
+
rows = 0
|
|
105
|
+
stripped = Hash.new(0)
|
|
106
|
+
kept = Hash.new(0)
|
|
107
|
+
nested_unfiltered = Hash.new(0)
|
|
108
|
+
|
|
109
|
+
relation.select(:id, :metadata).find_each(batch_size: batch_size) do |log|
|
|
110
|
+
rows += 1
|
|
111
|
+
metadata = log.metadata
|
|
112
|
+
next unless metadata.respond_to?(:each_pair)
|
|
113
|
+
|
|
114
|
+
# Collect distinct paths per row first, then increment once each. A row
|
|
115
|
+
# whose `charges[]` array holds two matching hashes is ONE affected row,
|
|
116
|
+
# not two — the counts are documented as row counts.
|
|
117
|
+
row = { stripped: Set.new, kept: Set.new, nested_unfiltered: Set.new }
|
|
118
|
+
walk(metadata, nil, row, top_level: true)
|
|
119
|
+
|
|
120
|
+
row[:stripped].each { |path| stripped[path] += 1 }
|
|
121
|
+
row[:kept].each { |path| kept[path] += 1 }
|
|
122
|
+
row[:nested_unfiltered].each { |path| nested_unfiltered[path] += 1 }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
Report.new(
|
|
126
|
+
rows_scanned: rows,
|
|
127
|
+
stripped: stripped,
|
|
128
|
+
kept: kept,
|
|
129
|
+
nested_unfiltered: nested_unfiltered,
|
|
130
|
+
nested: @nested
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def walk(hash, prefix, row, top_level:)
|
|
137
|
+
hash.each_pair do |key, value|
|
|
138
|
+
name = key.to_s
|
|
139
|
+
path = prefix ? "#{prefix}.#{name}" : name
|
|
140
|
+
|
|
141
|
+
# Reserved keys are immune at every depth, and their subtree is
|
|
142
|
+
# gem-owned — never descended into. Mirrors MetadataFilter exactly, so
|
|
143
|
+
# the dry run cannot misrepresent the rule it is modelling.
|
|
144
|
+
if StandardAudit::RESERVED_METADATA_KEYS.include?(name)
|
|
145
|
+
row[:kept] << path
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
matches = @filter.filter?(name)
|
|
150
|
+
|
|
151
|
+
if matches && (top_level || @nested)
|
|
152
|
+
row[:stripped] << path
|
|
153
|
+
next
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A nested match that survives because nested filtering is off. Reported
|
|
157
|
+
# separately rather than silently counted as "kept" — this is exactly
|
|
158
|
+
# the leak `filter_nested_metadata` closes.
|
|
159
|
+
if matches
|
|
160
|
+
row[:nested_unfiltered] << path
|
|
161
|
+
else
|
|
162
|
+
row[:kept] << path
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
descend(value, path, row)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def descend(value, path, row)
|
|
170
|
+
if value.respond_to?(:each_pair)
|
|
171
|
+
walk(value, path, row, top_level: false)
|
|
172
|
+
elsif value.is_a?(Array)
|
|
173
|
+
value.each { |element| descend(element, "#{path}[]", row) }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -79,9 +79,11 @@ module StandardAudit
|
|
|
79
79
|
raw_metadata = config.metadata_builder.call(raw_metadata)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
# Redaction lives in MetadataFilter, shared with StandardAudit.record.
|
|
83
|
+
# This path previously carried its own copy that did *not* subtract
|
|
84
|
+
# RESERVED_METADATA_KEYS, so `_tags`/`_source` were strippable here and
|
|
85
|
+
# not there.
|
|
86
|
+
StandardAudit::MetadataFilter.call(raw_metadata, config: config)
|
|
85
87
|
end
|
|
86
88
|
end
|
|
87
89
|
end
|
data/lib/standard_audit.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require "standard_audit/version"
|
|
2
2
|
require "standard_audit/engine"
|
|
3
3
|
require "standard_audit/configuration"
|
|
4
|
+
require "standard_audit/metadata_filter"
|
|
5
|
+
require "standard_audit/sensitive_keys_dry_run"
|
|
4
6
|
require "standard_audit/subscriber"
|
|
5
7
|
require "standard_audit/event_subscriber"
|
|
8
|
+
require "standard_audit/reference_preloading"
|
|
6
9
|
require "standard_audit/auditable"
|
|
7
10
|
require "standard_audit/audit_scope"
|
|
8
11
|
require "standard_audit/checks/retention"
|
|
@@ -13,8 +16,30 @@ module StandardAudit
|
|
|
13
16
|
RESERVED_METADATA_KEYS = %w[_tags _source].freeze
|
|
14
17
|
|
|
15
18
|
class << self
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
# Applies configuration to the single mutable Configuration instance.
|
|
20
|
+
#
|
|
21
|
+
# `baseline: true` also *remembers* the block, so `reset_configuration!`
|
|
22
|
+
# replays it. That matters because the config object holds behaviour, not
|
|
23
|
+
# just data — `before_checksum_hooks` in particular. Without a baseline, a
|
|
24
|
+
# suite that installs the rspec plugin (which calls `reset_configuration!`
|
|
25
|
+
# before every example) silently loses write-time hooks after the first
|
|
26
|
+
# example, and the specs that would notice pass vacuously.
|
|
27
|
+
#
|
|
28
|
+
# Idiomatic host usage, in config/initializers/standard_audit.rb:
|
|
29
|
+
#
|
|
30
|
+
# StandardAudit.configure(baseline: true) do |config|
|
|
31
|
+
# config.subscribe_to "myapp.**"
|
|
32
|
+
# config.before_checksum :backfill_scope
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# Only the most recent `baseline: true` block is remembered; call it once,
|
|
36
|
+
# from the initializer.
|
|
37
|
+
def configure(baseline: false, &block)
|
|
38
|
+
return config unless block
|
|
39
|
+
|
|
40
|
+
@baseline_configuration = block if baseline
|
|
41
|
+
block.call(config)
|
|
42
|
+
config
|
|
18
43
|
end
|
|
19
44
|
|
|
20
45
|
def config
|
|
@@ -26,10 +51,9 @@ module StandardAudit
|
|
|
26
51
|
|
|
27
52
|
actor ||= config.current_actor_resolver.call
|
|
28
53
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
filtered_metadata = metadata.reject { |k, _| sensitive.include?(k.to_s) }
|
|
54
|
+
# Redaction lives in MetadataFilter, shared with Subscriber, so the two
|
|
55
|
+
# write paths cannot drift apart.
|
|
56
|
+
filtered_metadata = MetadataFilter.call(metadata, config: config)
|
|
33
57
|
|
|
34
58
|
attrs = {
|
|
35
59
|
event_type: event_type,
|
|
@@ -101,8 +125,24 @@ module StandardAudit
|
|
|
101
125
|
@event_subscriber ||= EventSubscriber.new
|
|
102
126
|
end
|
|
103
127
|
|
|
104
|
-
|
|
128
|
+
# Drops the memoized Configuration. Any block registered with
|
|
129
|
+
# `configure(baseline: true)` is replayed onto the fresh instance, so a
|
|
130
|
+
# per-example reset restores the app's real configuration rather than the
|
|
131
|
+
# gem defaults.
|
|
132
|
+
def reset_configuration!(replay_baseline: true)
|
|
105
133
|
@configuration = nil
|
|
134
|
+
@baseline_configuration&.call(config) if replay_baseline
|
|
135
|
+
config
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Forgets the `configure(baseline: true)` block. Mainly for the gem's own
|
|
139
|
+
# specs and for a host that needs a genuinely pristine configuration.
|
|
140
|
+
def clear_baseline_configuration!
|
|
141
|
+
@baseline_configuration = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def baseline_configured?
|
|
145
|
+
!@baseline_configuration.nil?
|
|
106
146
|
end
|
|
107
147
|
|
|
108
148
|
private
|
|
@@ -113,10 +153,8 @@ module StandardAudit
|
|
|
113
153
|
|
|
114
154
|
def flush_batch(buffer)
|
|
115
155
|
now = Time.current
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.limit(1)
|
|
119
|
-
.pick(:checksum)
|
|
156
|
+
records_parent = StandardAudit::AuditLog.chain_parent_column?
|
|
157
|
+
previous_checksum = StandardAudit::AuditLog.chain_tip_checksum
|
|
120
158
|
|
|
121
159
|
# Generate sorted UUIDs to ensure batch ordering matches id ordering.
|
|
122
160
|
# UUIDv7 within the same millisecond can have non-monotonic lower bits;
|
|
@@ -135,6 +173,7 @@ module StandardAudit
|
|
|
135
173
|
row.stringify_keys,
|
|
136
174
|
previous_checksum: previous_checksum
|
|
137
175
|
)
|
|
176
|
+
row[:previous_checksum] = previous_checksum if records_parent
|
|
138
177
|
row[:checksum] = checksum
|
|
139
178
|
previous_checksum = checksum
|
|
140
179
|
row
|
|
@@ -63,22 +63,81 @@ 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!
|
|
79
97
|
puts "Backfilled checksums for #{count} audit log records"
|
|
80
98
|
end
|
|
81
99
|
|
|
100
|
+
namespace :sensitive_keys do
|
|
101
|
+
desc "Report which historical metadata keys a redaction rule would strip (read-only)"
|
|
102
|
+
task :dry_run, [:pattern] => :environment do |_t, args|
|
|
103
|
+
# Audit rows are append-only, so a redaction rule that swallows real
|
|
104
|
+
# content cannot be undone. This reads the rows you already have and
|
|
105
|
+
# reports, per key, what the rule would have stripped.
|
|
106
|
+
#
|
|
107
|
+
# rake standard_audit:sensitive_keys:dry_run
|
|
108
|
+
# rake "standard_audit:sensitive_keys:dry_run[secret]"
|
|
109
|
+
# NESTED=1 rake "standard_audit:sensitive_keys:dry_run[secret|token]"
|
|
110
|
+
#
|
|
111
|
+
# The argument is a Regexp source, matched case-insensitively, and is
|
|
112
|
+
# applied *in addition to* the app's configured sensitive_keys.
|
|
113
|
+
# NESTED=1/0 overrides config.filter_nested_metadata for the run.
|
|
114
|
+
pattern = args[:pattern].presence || ENV["PATTERN"].presence
|
|
115
|
+
patterns = StandardAudit.config.sensitive_key_patterns.dup
|
|
116
|
+
patterns << Regexp.new(pattern, Regexp::IGNORECASE) if pattern
|
|
117
|
+
|
|
118
|
+
nested =
|
|
119
|
+
case ENV["NESTED"]
|
|
120
|
+
when nil, "" then nil
|
|
121
|
+
when "0", "false" then false
|
|
122
|
+
else true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
report = StandardAudit::SensitiveKeysDryRun.call(
|
|
126
|
+
sensitive_key_patterns: patterns,
|
|
127
|
+
nested: nested,
|
|
128
|
+
batch_size: (ENV["BATCH_SIZE"] || 1000).to_i
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
puts "StandardAudit sensitive-key dry run"
|
|
132
|
+
puts "==================================="
|
|
133
|
+
puts "Candidate pattern: #{pattern ? Regexp.new(pattern, Regexp::IGNORECASE).inspect : '(none — reporting current config)'}"
|
|
134
|
+
puts ""
|
|
135
|
+
puts report
|
|
136
|
+
puts ""
|
|
137
|
+
puts "Nothing was written. Rows are append-only; a rule you enable applies only to future writes."
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
82
141
|
desc "Export audit logs for a specific actor (GDPR right to access)"
|
|
83
142
|
task :export_actor, [:actor_gid, :output] => :environment do |_t, args|
|
|
84
143
|
raise "actor_gid is required" unless args[:actor_gid].present?
|
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.8.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
|
|
@@ -109,7 +111,10 @@ files:
|
|
|
109
111
|
- lib/standard_audit/configuration.rb
|
|
110
112
|
- lib/standard_audit/engine.rb
|
|
111
113
|
- lib/standard_audit/event_subscriber.rb
|
|
114
|
+
- lib/standard_audit/metadata_filter.rb
|
|
115
|
+
- lib/standard_audit/reference_preloading.rb
|
|
112
116
|
- lib/standard_audit/rspec.rb
|
|
117
|
+
- lib/standard_audit/sensitive_keys_dry_run.rb
|
|
113
118
|
- lib/standard_audit/subscriber.rb
|
|
114
119
|
- lib/standard_audit/version.rb
|
|
115
120
|
- lib/tasks/standard_audit_tasks.rake
|