standard_audit 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44b12d653a138dc099f16e659e14cd139b612369f2279859b9fcae7eff57827c
4
- data.tar.gz: f36ceca0425ba35b8ad73c1ac91cf63e82eb6da21fdaf7f7117c73f9c09ecc4e
3
+ metadata.gz: c400c65330d8896079475becca2eedc20481027bd23a9e6c35d1aa6011e46762
4
+ data.tar.gz: e5757a3734ccecaa266b0e17f537f53c51d26cebd6b6fdcc0022db758d31935e
5
5
  SHA512:
6
- metadata.gz: e10c0a45d0941ab4284f3f1fd0a8bd25b5b9c83b0052a1e354d3acb43be984eb1997e934ff8ea45beb9df772e12420300f41403bd018d3a75e2080b32df05cd0
7
- data.tar.gz: c0fcae8a3eee766f6a2caf7f01323788e4eeadf3de73ff3b37a7258e3a037b2771e557a7865adf559ebd98afc3d975b2e92bbdfc0b2735b1819fffaef80511e8
6
+ metadata.gz: f03510f5f6f9c6ba1e47e189da9d29e9c1a6d720886fb149c4938f4612581bd4049b87e61d1a67a36c7a27cb4ca1a147c3f9984c17ee090980d236fa632531c3
7
+ data.tar.gz: faf25861c62875fb9e89b97c938a5ed1730982f3f8a48b40c694a3ee26f9941c9832085a1dc75420d4a7085c0cb03aff0b1474ea59e3a29a4386297d9fb6c879
data/CHANGELOG.md CHANGED
@@ -1,8 +1,35 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 (2026-03-03)
3
+ All notable changes to this project will be documented in this file.
4
4
 
5
- Initial release.
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
+ ## [0.2.0] - 2026-03-25
11
+
12
+ ### Added
13
+
14
+ - Batch insert mode via `StandardAudit.batch { }` for high-volume audit logging
15
+ - `StandardAudit::CleanupJob` for automated retention enforcement
16
+ - `config.use_preset(:standard_id)` to subscribe to StandardId auth events in one call
17
+ - GIN index on metadata JSONB column in install generator (PostgreSQL)
18
+ - CI-driven gem publishing via GitHub Actions trusted publisher
19
+
20
+ ### Changed
21
+
22
+ - Migration template uses `jsonb` instead of `json` for metadata column
23
+ - Expanded default `sensitive_keys` to include `api_key`, `access_token`, `refresh_token`, `private_key`, `certificate_chain`, `ssn`, `credit_card`, `authorization`
24
+
25
+ ### Breaking Changes
26
+
27
+ - AuditLog records are now immutable — `update`/`destroy` raises `ActiveRecord::ReadOnlyRecord`. Use `update_columns` for privileged operations like GDPR anonymization. `delete`/`delete_all` still work for bulk cleanup.
28
+ - Removed `auto_cleanup` config attribute. Schedule `StandardAudit::CleanupJob` directly instead.
29
+
30
+ ## [0.1.0] - 2026-03-03
31
+
32
+ ### Added
6
33
 
7
34
  - Core audit log model with UUID primary keys and GlobalID-based polymorphic references
8
35
  - Convenience API: `StandardAudit.record` with sync, async, and block forms
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright TODO: Write your name
1
+ Copyright (c) 2026 Jaryl Sim
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -161,7 +161,7 @@ StandardAudit.configure do |config|
161
161
 
162
162
  # -- Sensitive Data --
163
163
  # Keys automatically stripped from metadata.
164
- config.sensitive_keys = %i[password password_confirmation token secret]
164
+ config.sensitive_keys += %i[my_custom_secret] # added to built-in defaults
165
165
 
166
166
  # -- Metadata Builder --
167
167
  # Optional proc to transform metadata before storage.
@@ -179,9 +179,8 @@ StandardAudit.configure do |config|
179
179
  # Metadata keys to strip during anonymization.
180
180
  config.anonymizable_metadata_keys = %i[email name ip_address]
181
181
 
182
- # -- Retention --
182
+ # -- Retention (schedule StandardAudit::CleanupJob to enforce) --
183
183
  config.retention_days = 90
184
- config.auto_cleanup = false
185
184
  end
186
185
  ```
187
186
 
@@ -353,7 +352,7 @@ t.jsonb :metadata, default: {}
353
352
  **Sensitive data**: Configure `sensitive_keys` to automatically strip passwords, tokens, and secrets from metadata. Add domain-specific keys as needed:
354
353
 
355
354
  ```ruby
356
- config.sensitive_keys = %i[password token secret ssn credit_card_number]
355
+ config.sensitive_keys += %i[medical_record_number] # extend the built-in defaults
357
356
  ```
358
357
 
359
358
  **Performance**: For high-volume applications, enable async processing and ensure your `audit_logs` table has appropriate indexes (the install generator adds them by default). Consider partitioning by `occurred_at` for very large tables.
@@ -0,0 +1,14 @@
1
+ module StandardAudit
2
+ class CleanupJob < ActiveJob::Base
3
+ queue_as { StandardAudit.config.queue_name }
4
+
5
+ def perform
6
+ days = StandardAudit.config.retention_days
7
+ return unless days
8
+
9
+ cutoff = days.days.ago
10
+ deleted = StandardAudit::AuditLog.before(cutoff).in_batches(of: 10_000).delete_all
11
+ Rails.logger.info("[StandardAudit] CleanupJob deleted #{deleted} audit logs older than #{days} days")
12
+ end
13
+ end
14
+ end
@@ -3,6 +3,14 @@ module StandardAudit
3
3
  self.table_name = "audit_logs"
4
4
 
5
5
  before_create :assign_uuid, if: -> { id.blank? }
6
+ after_create_commit :emit_created_event
7
+
8
+ # Audit logs are append-only. Use update_columns for privileged
9
+ # operations like GDPR anonymization that must bypass this guard.
10
+ # Note: delete/delete_all bypass callbacks and are permitted for
11
+ # bulk cleanup operations (see CleanupJob, rake standard_audit:cleanup).
12
+ before_update { raise ActiveRecord::ReadOnlyRecord }
13
+ before_destroy { raise ActiveRecord::ReadOnlyRecord }
6
14
 
7
15
  validates :event_type, presence: true
8
16
  validates :occurred_at, presence: true
@@ -85,8 +93,8 @@ module StandardAudit
85
93
  scope :for_request, ->(request_id) { where(request_id: request_id) }
86
94
  scope :from_ip, ->(ip_address) { where(ip_address: ip_address) }
87
95
  scope :for_session, ->(session_id) { where(session_id: session_id) }
88
- scope :chronological, -> { order(occurred_at: :asc) }
89
- scope :reverse_chronological, -> { order(occurred_at: :desc) }
96
+ scope :chronological, -> { order(occurred_at: :asc, created_at: :asc) }
97
+ scope :reverse_chronological, -> { order(occurred_at: :desc, created_at: :desc) }
90
98
  scope :recent, ->(n = 10) { reverse_chronological.limit(n) }
91
99
 
92
100
  # -- GDPR methods --
@@ -150,6 +158,18 @@ module StandardAudit
150
158
 
151
159
  private
152
160
 
161
+ def emit_created_event
162
+ ActiveSupport::Notifications.instrument("standard_audit.audit_log.created", {
163
+ id: id,
164
+ event_type: event_type,
165
+ actor_type: actor_type,
166
+ target_type: target_type,
167
+ scope_type: scope_type
168
+ })
169
+ rescue StandardError => e
170
+ Rails.logger.warn("[StandardAudit] Failed to emit event: #{e.class}: #{e.message}")
171
+ end
172
+
153
173
  def assign_uuid
154
174
  self.id = SecureRandom.uuid
155
175
  end
@@ -5,26 +5,28 @@ class CreateAuditLogs < ActiveRecord::Migration[<%= ActiveRecord::Migration.curr
5
5
  t.string :actor_type
6
6
  t.string :target_gid
7
7
  t.string :target_type
8
+ # Multi-tenancy: include StandardAudit::AuditScope in your tenant model
9
+ # (e.g., Organisation) to scope audit logs. Leave nil for single-tenant apps.
8
10
  t.string :scope_gid
9
11
  t.string :scope_type
10
12
  t.string :event_type, null: false
11
13
  t.string :request_id
12
14
  t.string :ip_address
13
- t.string :user_agent
15
+ t.text :user_agent
14
16
  t.string :session_id
15
- t.json :metadata, default: {}
17
+ t.jsonb :metadata, default: {}
16
18
  t.datetime :occurred_at, null: false
17
19
  t.timestamps
18
20
  end
19
21
 
20
22
  add_index :audit_logs, :event_type
21
- add_index :audit_logs, :actor_type
22
- add_index :audit_logs, :target_type
23
+ add_index :audit_logs, [:actor_gid, :occurred_at]
24
+ add_index :audit_logs, [:target_gid, :occurred_at]
23
25
  add_index :audit_logs, [:scope_type, :scope_gid]
24
- add_index :audit_logs, :scope_type
25
26
  add_index :audit_logs, :request_id
26
- add_index :audit_logs, :occurred_at
27
- add_index :audit_logs, :ip_address
27
+ add_index :audit_logs, [:occurred_at, :created_at]
28
28
  add_index :audit_logs, :session_id
29
+ # GIN index requires PostgreSQL; remove if using another database
30
+ add_index :audit_logs, :metadata, using: :gin
29
31
  end
30
32
  end
@@ -1,5 +1,8 @@
1
1
  StandardAudit.configure do |config|
2
- # Subscribe to ActiveSupport::Notifications patterns
2
+ # Use a preset to subscribe to common event patterns
3
+ # config.use_preset(:standard_id)
4
+
5
+ # Or subscribe to ActiveSupport::Notifications patterns manually
3
6
  # config.subscribe_to "audit.**"
4
7
 
5
8
  # Actor extractor from notification payload
@@ -11,6 +14,9 @@ StandardAudit.configure do |config|
11
14
  # Scope extractor from notification payload
12
15
  # config.scope_extractor = ->(payload) { payload[:scope] }
13
16
 
17
+ # Multi-tenancy: include StandardAudit::AuditScope in your tenant model
18
+ # to add a scoped_audit_logs association.
19
+
14
20
  # Fallback resolvers (used when payload values are nil)
15
21
  # config.current_actor_resolver = -> { Current.user }
16
22
  # config.current_request_id_resolver = -> { Current.request_id }
@@ -18,8 +24,10 @@ StandardAudit.configure do |config|
18
24
  # config.current_user_agent_resolver = -> { Current.user_agent }
19
25
  # config.current_session_id_resolver = -> { Current.session_id }
20
26
 
21
- # Keys to strip from metadata
22
- # config.sensitive_keys = %i[password password_confirmation token secret]
27
+ # Keys to strip from metadata (defaults include: password, password_confirmation,
28
+ # token, secret, api_key, access_token, refresh_token, private_key,
29
+ # certificate_chain, ssn, credit_card, authorization)
30
+ # config.sensitive_keys += %i[my_custom_secret]
23
31
 
24
32
  # Run audit log creation in background job
25
33
  # config.async = false
@@ -31,7 +39,6 @@ StandardAudit.configure do |config|
31
39
  # GDPR: metadata keys to strip on anonymization
32
40
  # config.anonymizable_metadata_keys = %i[email name ip_address]
33
41
 
34
- # Data retention
42
+ # Data retention (schedule StandardAudit::CleanupJob to enforce)
35
43
  # config.retention_days = nil
36
- # config.auto_cleanup = false
37
44
  end
@@ -6,10 +6,11 @@ module StandardAudit
6
6
  :current_ip_address_resolver, :current_user_agent_resolver,
7
7
  :current_session_id_resolver,
8
8
  :sensitive_keys, :metadata_builder,
9
- :anonymizable_metadata_keys, :retention_days, :auto_cleanup
9
+ :anonymizable_metadata_keys, :retention_days
10
10
 
11
11
  def initialize
12
12
  @subscriptions = []
13
+ @applied_presets = []
13
14
  @async = false
14
15
  @queue_name = :default
15
16
  @enabled = true
@@ -34,11 +35,18 @@ module StandardAudit
34
35
  defined?(Current) && Current.respond_to?(:session_id) ? Current.session_id : nil
35
36
  }
36
37
 
37
- @sensitive_keys = %i[password password_confirmation token secret]
38
+ # Note: :authorization filters the HTTP Authorization header value.
39
+ # If you use "authorization" as a metadata key for policy decisions,
40
+ # rename it (e.g. :authorization_policy) to avoid accidental filtering.
41
+ @sensitive_keys = %i[
42
+ password password_confirmation token secret
43
+ api_key access_token refresh_token
44
+ private_key certificate_chain
45
+ ssn credit_card authorization
46
+ ]
38
47
  @metadata_builder = nil
39
48
  @anonymizable_metadata_keys = %i[email name ip_address]
40
49
  @retention_days = nil
41
- @auto_cleanup = false
42
50
  end
43
51
 
44
52
  def subscribe_to(pattern)
@@ -48,5 +56,22 @@ module StandardAudit
48
56
  def subscriptions
49
57
  @subscriptions.dup.freeze
50
58
  end
59
+
60
+ def use_preset(name)
61
+ key = name.to_sym
62
+ return self if @applied_presets.include?(key)
63
+
64
+ preset = case key
65
+ when :standard_id
66
+ require "standard_audit/presets/standard_id"
67
+ StandardAudit::Presets::StandardId
68
+ else
69
+ raise ArgumentError, "Unknown preset: #{name}. Available presets: :standard_id"
70
+ end
71
+
72
+ preset.apply(self)
73
+ @applied_presets << key
74
+ self
75
+ end
51
76
  end
52
77
  end
@@ -0,0 +1,22 @@
1
+ module StandardAudit
2
+ module Presets
3
+ module StandardId
4
+ # Regex wildcards capture all events in a namespace. Session uses
5
+ # explicit strings to exclude noisy events like session.validated
6
+ # that fire on every authenticated request.
7
+ SUBSCRIPTIONS = [
8
+ /\Astandard_id\.authentication\./,
9
+ "standard_id.session.created",
10
+ "standard_id.session.revoked",
11
+ "standard_id.session.expired",
12
+ /\Astandard_id\.account\./,
13
+ /\Astandard_id\.social\./,
14
+ /\Astandard_id\.passwordless\./
15
+ ].freeze
16
+
17
+ def self.apply(config)
18
+ SUBSCRIPTIONS.each { |pattern| config.subscribe_to(pattern) }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module StandardAudit
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -44,15 +44,20 @@ module StandardAudit
44
44
  return
45
45
  end
46
46
 
47
- if config.async
48
- job_attrs = attrs.dup
49
- job_attrs[:actor_gid] = actor&.to_global_id&.to_s
50
- job_attrs[:target_gid] = target&.to_global_id&.to_s
51
- job_attrs[:scope_gid] = scope&.to_global_id&.to_s
52
- job_attrs[:actor_type] = actor&.class&.name
53
- job_attrs[:target_type] = target&.class&.name
54
- job_attrs[:scope_type] = scope&.class&.name
55
- StandardAudit::CreateAuditLogJob.perform_later(job_attrs.stringify_keys)
47
+ gid_attrs = {
48
+ actor_gid: actor&.to_global_id&.to_s,
49
+ actor_type: actor&.class&.name,
50
+ target_gid: target&.to_global_id&.to_s,
51
+ target_type: target&.class&.name,
52
+ scope_gid: scope&.to_global_id&.to_s,
53
+ scope_type: scope&.class&.name
54
+ }
55
+
56
+ if batching?
57
+ Thread.current[:standard_audit_batch] << attrs.merge(gid_attrs)
58
+ nil
59
+ elsif config.async
60
+ StandardAudit::CreateAuditLogJob.perform_later(attrs.merge(gid_attrs).stringify_keys)
56
61
  else
57
62
  log = StandardAudit::AuditLog.new(attrs)
58
63
  log.actor = actor
@@ -63,6 +68,24 @@ module StandardAudit
63
68
  end
64
69
  end
65
70
 
71
+ # Buffers record calls and flushes them via insert_all! on block exit.
72
+ # If the block raises, buffered records are dropped — only successful
73
+ # batches are persisted. Nested batches flush independently.
74
+ # Block-form record calls (with AS::Notifications) bypass the buffer
75
+ # and are processed normally since they don't persist records directly.
76
+ # Note: uses Thread.current for storage, which is not fiber-safe.
77
+ # Apps using async adapters (Falcon) should avoid concurrent batches.
78
+ def batch
79
+ previous = Thread.current[:standard_audit_batch]
80
+ buffer = Thread.current[:standard_audit_batch] = []
81
+
82
+ yield
83
+
84
+ flush_batch(buffer) if buffer.any?
85
+ ensure
86
+ Thread.current[:standard_audit_batch] = previous
87
+ end
88
+
66
89
  def subscriber
67
90
  @subscriber ||= Subscriber.new
68
91
  end
@@ -70,5 +93,24 @@ module StandardAudit
70
93
  def reset_configuration!
71
94
  @configuration = nil
72
95
  end
96
+
97
+ private
98
+
99
+ def batching?
100
+ Thread.current[:standard_audit_batch].is_a?(Array)
101
+ end
102
+
103
+ def flush_batch(buffer)
104
+ now = Time.current
105
+ rows = buffer.map do |attrs|
106
+ attrs.merge(
107
+ id: SecureRandom.uuid,
108
+ created_at: now,
109
+ updated_at: now
110
+ )
111
+ end
112
+
113
+ StandardAudit::AuditLog.insert_all!(rows)
114
+ end
73
115
  end
74
116
  end
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -77,6 +77,7 @@ files:
77
77
  - MIT-LICENSE
78
78
  - README.md
79
79
  - Rakefile
80
+ - app/jobs/standard_audit/cleanup_job.rb
80
81
  - app/jobs/standard_audit/create_audit_log_job.rb
81
82
  - app/models/standard_audit/application_record.rb
82
83
  - app/models/standard_audit/audit_log.rb
@@ -89,6 +90,7 @@ files:
89
90
  - lib/standard_audit/auditable.rb
90
91
  - lib/standard_audit/configuration.rb
91
92
  - lib/standard_audit/engine.rb
93
+ - lib/standard_audit/presets/standard_id.rb
92
94
  - lib/standard_audit/subscriber.rb
93
95
  - lib/standard_audit/version.rb
94
96
  - lib/tasks/standard_audit_tasks.rake
@@ -99,6 +101,7 @@ metadata:
99
101
  homepage_uri: https://github.com/rarebit-one/standard_audit
100
102
  source_code_uri: https://github.com/rarebit-one/standard_audit
101
103
  changelog_uri: https://github.com/rarebit-one/standard_audit/blob/main/CHANGELOG.md
104
+ bug_tracker_uri: https://github.com/rarebit-one/standard_audit/issues
102
105
  rdoc_options: []
103
106
  require_paths:
104
107
  - lib