provenance 2.0.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +19 -0
  3. data/LICENSE +21 -0
  4. data/README.md +358 -0
  5. data/lib/generators/provenance/install/install_generator.rb +34 -0
  6. data/lib/generators/provenance/install/templates/create_provenance_outbox.rb.tt +21 -0
  7. data/lib/generators/provenance/install/templates/initializer.rb.tt +12 -0
  8. data/lib/provenance/action.rb +160 -0
  9. data/lib/provenance/actor.rb +86 -0
  10. data/lib/provenance/configuration.rb +152 -0
  11. data/lib/provenance/context.rb +30 -0
  12. data/lib/provenance/controller.rb +93 -0
  13. data/lib/provenance/emitter.rb +66 -0
  14. data/lib/provenance/entity_change.rb +73 -0
  15. data/lib/provenance/errors.rb +12 -0
  16. data/lib/provenance/event.rb +42 -0
  17. data/lib/provenance/integrity.rb +75 -0
  18. data/lib/provenance/job.rb +62 -0
  19. data/lib/provenance/json_safe.rb +41 -0
  20. data/lib/provenance/middleware.rb +44 -0
  21. data/lib/provenance/model.rb +123 -0
  22. data/lib/provenance/outbox/record.rb +23 -0
  23. data/lib/provenance/outbox/relay.rb +81 -0
  24. data/lib/provenance/outbox.rb +67 -0
  25. data/lib/provenance/railtie.rb +35 -0
  26. data/lib/provenance/rake.rb +27 -0
  27. data/lib/provenance/recorder.rb +206 -0
  28. data/lib/provenance/redactor.rb +63 -0
  29. data/lib/provenance/registry.rb +48 -0
  30. data/lib/provenance/relay_job.rb +15 -0
  31. data/lib/provenance/rspec.rb +139 -0
  32. data/lib/provenance/serializers/cloud_events.rb +26 -0
  33. data/lib/provenance/serializers/native.rb +16 -0
  34. data/lib/provenance/serializers/ocsf.rb +127 -0
  35. data/lib/provenance/serializers.rb +31 -0
  36. data/lib/provenance/sinks/http.rb +81 -0
  37. data/lib/provenance/sinks/io.rb +24 -0
  38. data/lib/provenance/sinks/kafka.rb +45 -0
  39. data/lib/provenance/sinks/logger.rb +22 -0
  40. data/lib/provenance/sinks/memory.rb +29 -0
  41. data/lib/provenance/sinks/proc.rb +20 -0
  42. data/lib/provenance/sinks.rb +35 -0
  43. data/lib/provenance/testing.rb +36 -0
  44. data/lib/provenance/uuid.rb +20 -0
  45. data/lib/provenance/version.rb +6 -0
  46. data/lib/provenance.rb +166 -0
  47. data/lib/tasks/provenance.rake +22 -0
  48. data/schema/event-2.json +126 -0
  49. metadata +150 -0
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Provenance
4
+ module Sinks
5
+ # Calls a block or callable with each batch.
6
+ class Proc
7
+ # @param callable [#call, nil]
8
+ def initialize(callable = nil, &block)
9
+ @callable = callable || block
10
+ raise ArgumentError, "proc sink needs a callable or a block" unless @callable.respond_to?(:call)
11
+ end
12
+
13
+ # @param batch [Array<Hash>]
14
+ # @return [void]
15
+ def deliver(batch)
16
+ @callable.call(batch)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "provenance/sinks/logger"
4
+ require "provenance/sinks/io"
5
+ require "provenance/sinks/http"
6
+ require "provenance/sinks/proc"
7
+ require "provenance/sinks/memory"
8
+
9
+ module Provenance
10
+ # Destinations for serialized events. A sink is any object responding to
11
+ # +#deliver(batch)+, where +batch+ is an Array of serialized events (Hashes).
12
+ module Sinks
13
+ module_function
14
+
15
+ # Builds a sink from a type symbol or returns a sink object unchanged.
16
+ #
17
+ # @param type [Symbol, #deliver]
18
+ # @return [#deliver]
19
+ def build(type, *args, **options, &block)
20
+ return type if type.respond_to?(:deliver)
21
+
22
+ case type.to_sym
23
+ when :logger then Logger.new(*args, **options)
24
+ when :io then IO.new(*args, **options)
25
+ when :http then HTTP.new(*args, **options)
26
+ when :proc then Proc.new(*args, &block)
27
+ when :memory then Memory.new
28
+ when :kafka
29
+ require "provenance/sinks/kafka"
30
+ Kafka.new(*args, **options)
31
+ else raise ConfigurationError, "unknown sink #{type.inspect}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Provenance
4
+ # Test helpers. While {capture} runs, tracking is enabled regardless of
5
+ # +config.enabled+ and events go to the capture instead of the outbox or sinks.
6
+ module Testing
7
+ @captures = []
8
+ @lock = Mutex.new
9
+
10
+ class << self
11
+ # Collects the native events emitted while the block runs.
12
+ #
13
+ # @return [Array<Hash>] native events
14
+ def capture
15
+ events = []
16
+ @lock.synchronize { @captures << events }
17
+ begin
18
+ yield
19
+ ensure
20
+ @lock.synchronize { @captures.delete_if { |c| c.equal?(events) } }
21
+ end
22
+ events
23
+ end
24
+
25
+ # @return [Boolean] whether a capture is active
26
+ def capturing?
27
+ !@captures.empty?
28
+ end
29
+
30
+ # @api private
31
+ def record(event)
32
+ @lock.synchronize { @captures.each { |c| c << event } }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Provenance
4
+ # UUIDv7 generation (RFC 9562): 48-bit Unix millisecond timestamp followed by
5
+ # random bits, so ids sort by creation time.
6
+ module UUID
7
+ module_function
8
+
9
+ # @param time [Time] timestamp to embed
10
+ # @return [String] a UUIDv7 in canonical 8-4-4-4-12 form
11
+ def v7(time = Time.now)
12
+ ms = (time.to_r * 1000).to_i
13
+ bytes = [ms >> 16, ms & 0xffff].pack("Nn").b + SecureRandom.random_bytes(10)
14
+ bytes.setbyte(6, (bytes.getbyte(6) & 0x0f) | 0x70)
15
+ bytes.setbyte(8, (bytes.getbyte(8) & 0x3f) | 0x80)
16
+ hex = bytes.unpack1("H*")
17
+ [hex[0, 8], hex[8, 4], hex[12, 4], hex[16, 4], hex[20, 12]].join("-")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Provenance
4
+ # Current gem version.
5
+ VERSION = "2.0.0"
6
+ end
data/lib/provenance.rb ADDED
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+ require "active_support"
6
+ require "active_support/core_ext"
7
+ require "active_support/isolated_execution_state"
8
+ require "active_record"
9
+
10
+ require "provenance/version"
11
+ require "provenance/errors"
12
+ require "provenance/uuid"
13
+ require "provenance/json_safe"
14
+ require "provenance/configuration"
15
+ require "provenance/actor"
16
+ require "provenance/context"
17
+ require "provenance/entity_change"
18
+ require "provenance/action"
19
+ require "provenance/registry"
20
+ require "provenance/redactor"
21
+ require "provenance/event"
22
+ require "provenance/serializers"
23
+ require "provenance/sinks"
24
+ require "provenance/integrity"
25
+ require "provenance/emitter"
26
+ require "provenance/recorder"
27
+ require "provenance/model"
28
+ require "provenance/middleware"
29
+ require "provenance/controller"
30
+ require "provenance/job"
31
+ require "provenance/rake"
32
+ require "provenance/outbox"
33
+
34
+ # Provenance emits one action-level audit event per HTTP request, background job,
35
+ # rake task or explicit block, and delivers it to security and observability tooling.
36
+ #
37
+ # @example
38
+ # Provenance.configure do |c|
39
+ # c.sink :logger
40
+ # end
41
+ module Provenance
42
+ autoload :RelayJob, "provenance/relay_job"
43
+
44
+ STATE_KEY = :provenance_action
45
+ SUPPRESS_KEY = :provenance_suppressed
46
+ private_constant :STATE_KEY, :SUPPRESS_KEY
47
+
48
+ class << self
49
+ # Yields the global configuration and validates it afterwards.
50
+ #
51
+ # @yieldparam config [Configuration]
52
+ # @return [Configuration]
53
+ # @raise [ConfigurationError] when the resulting configuration is invalid
54
+ def configure
55
+ yield config
56
+ config.validate!
57
+ config
58
+ end
59
+
60
+ # @return [Configuration] the global configuration
61
+ def config
62
+ @config ||= Configuration.new
63
+ end
64
+
65
+ # Replaces the global configuration with defaults. Mostly useful in tests.
66
+ #
67
+ # @return [Configuration]
68
+ def reset_config!
69
+ @config = Configuration.new
70
+ end
71
+
72
+ # @return [Registry] the registry of models tracked with +has_provenance+
73
+ def registry
74
+ @registry ||= Registry.new
75
+ end
76
+
77
+ # @return [Boolean] whether tracking is active for the current execution context
78
+ def enabled?
79
+ (config.enabled || Testing.capturing?) && !suppressed?
80
+ end
81
+
82
+ # @return [Boolean] whether tracking is suppressed by {without}
83
+ def suppressed?
84
+ ActiveSupport::IsolatedExecutionState[SUPPRESS_KEY].to_i.positive?
85
+ end
86
+
87
+ # The action currently open in this thread or fiber, if any.
88
+ #
89
+ # @return [Action, nil]
90
+ def current
91
+ ActiveSupport::IsolatedExecutionState[STATE_KEY]
92
+ end
93
+
94
+ # Opens an explicit action. Explicit actions are always auditable. Nested scopes
95
+ # attach their changes and metadata to the outermost action.
96
+ #
97
+ # @param name [String] action name, e.g. "users.import"
98
+ # @param actor [Object, Hash, Actor, nil] who performs the action
99
+ # @param metadata [Hash] arbitrary JSON-safe data
100
+ # @yieldparam action [Action]
101
+ # @return [Object] the block's return value
102
+ def action(name, actor: nil, metadata: {}, &block)
103
+ raise ArgumentError, "a block is required" unless block
104
+
105
+ track(kind: :custom, name: name, actor: actor, metadata: metadata, explicit: true, &block)
106
+ end
107
+
108
+ # Runs the block with change tracking and emission suppressed.
109
+ #
110
+ # @return [Object] the block's return value
111
+ def without
112
+ state = ActiveSupport::IsolatedExecutionState
113
+ state[SUPPRESS_KEY] = state[SUPPRESS_KEY].to_i + 1
114
+ yield
115
+ ensure
116
+ state[SUPPRESS_KEY] -= 1
117
+ end
118
+
119
+ # Runs the block inside an action of the given kind, or inside the already open
120
+ # action when there is one. Used by the HTTP, job and rake integrations.
121
+ #
122
+ # @param kind [Symbol] :http, :job, :task or :custom
123
+ # @param name [String, nil]
124
+ # @param actor [Object, nil]
125
+ # @param metadata [Hash]
126
+ # @param explicit [Boolean]
127
+ # @param caused_by [String, nil] id of the parent action
128
+ # @param context [Context, nil]
129
+ # @yieldparam action [Action]
130
+ # @return [Object] the block's return value
131
+ def track(kind:, name:, actor: nil, metadata: {}, explicit: false, caused_by: nil, context: nil)
132
+ if (outer = current)
133
+ outer.annotate(**metadata) if metadata.present?
134
+ outer.actor = actor if actor && outer.explicit_actor.nil?
135
+ return yield(outer)
136
+ end
137
+
138
+ action = Action.new(kind: kind, name: name, actor: actor, metadata: metadata,
139
+ explicit: explicit, caused_by: caused_by, context: context)
140
+ within(action) { yield(action) }
141
+ end
142
+
143
+ # Makes +action+ current for the duration of the block, records an unhandled
144
+ # exception as the outcome and hands the action to the emitter afterwards.
145
+ #
146
+ # @param action [Action]
147
+ # @return [Object] the block's return value
148
+ def within(action)
149
+ state = ActiveSupport::IsolatedExecutionState
150
+ previous = state[STATE_KEY]
151
+ state[STATE_KEY] = action
152
+ begin
153
+ yield
154
+ rescue Exception => e # rubocop:disable Lint/RescueException
155
+ action.fail!(e)
156
+ raise
157
+ ensure
158
+ state[STATE_KEY] = previous
159
+ Emitter.finish(action)
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ require "provenance/testing"
166
+ require "provenance/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :provenance do
4
+ desc "Verify the integrity chain of events stored in the outbox"
5
+ task verify: :environment do
6
+ key = Provenance.config.integrity.key
7
+ abort "provenance:verify: integrity.key is not configured" if key.blank?
8
+
9
+ broken = Provenance::Outbox::Record.where.not(seq: nil).distinct.pluck(:app).filter_map do |app|
10
+ events = Provenance::Outbox::Record.where(app: app).where.not(seq: nil).order(:seq).map(&:event)
11
+ seq = Provenance::Integrity.verify(events, key: key)
12
+ puts "#{app}: #{seq ? "broken at seq #{seq}" : "ok (#{events.size} events)"}"
13
+ seq
14
+ end
15
+ abort "provenance:verify: integrity check failed" if broken.any?
16
+ end
17
+
18
+ desc "Deliver pending outbox events now"
19
+ task relay: :environment do
20
+ puts "delivered #{Provenance::Outbox::Relay.run} events"
21
+ end
22
+ end
@@ -0,0 +1,126 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:provenance:event:2",
4
+ "title": "Provenance action event",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema", "id", "occurred_at", "app", "action", "actor", "request", "outcome", "changes", "metadata"],
8
+ "properties": {
9
+ "schema": {"const": "provenance/event@2"},
10
+ "id": {
11
+ "type": "string",
12
+ "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
13
+ },
14
+ "occurred_at": {
15
+ "type": "string",
16
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$"
17
+ },
18
+ "app": {"type": "string", "minLength": 1},
19
+ "action": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "required": ["kind", "name", "caused_by"],
23
+ "properties": {
24
+ "kind": {"enum": ["http", "job", "task", "custom"]},
25
+ "name": {"type": "string"},
26
+ "caused_by": {"type": ["string", "null"]}
27
+ }
28
+ },
29
+ "actor": {
30
+ "oneOf": [{"type": "null"}, {"$ref": "#/$defs/actor"}]
31
+ },
32
+ "request": {
33
+ "oneOf": [
34
+ {"type": "null"},
35
+ {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["id", "method", "path", "status", "ip", "user_agent"],
39
+ "properties": {
40
+ "id": {"type": ["string", "null"]},
41
+ "method": {"type": "string"},
42
+ "path": {"type": "string"},
43
+ "status": {"type": ["integer", "null"]},
44
+ "ip": {"type": ["string", "null"]},
45
+ "user_agent": {"type": ["string", "null"]}
46
+ }
47
+ }
48
+ ]
49
+ },
50
+ "outcome": {
51
+ "type": "object",
52
+ "additionalProperties": false,
53
+ "required": ["result", "error"],
54
+ "properties": {
55
+ "result": {"enum": ["success", "failure", "denied"]},
56
+ "error": {
57
+ "oneOf": [
58
+ {"type": "null"},
59
+ {
60
+ "type": "object",
61
+ "additionalProperties": false,
62
+ "required": ["class", "message"],
63
+ "properties": {
64
+ "class": {"type": "string"},
65
+ "message": {"type": "string", "maxLength": 500}
66
+ }
67
+ }
68
+ ]
69
+ }
70
+ }
71
+ },
72
+ "changes": {"type": "array", "items": {"$ref": "#/$defs/change"}},
73
+ "metadata": {"type": "object"},
74
+ "integrity": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["seq", "prev", "mac"],
78
+ "properties": {
79
+ "seq": {"type": "integer", "minimum": 1},
80
+ "prev": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"},
81
+ "mac": {"type": "string", "pattern": "^[0-9a-f]{64}$"}
82
+ }
83
+ }
84
+ },
85
+ "$defs": {
86
+ "actor": {
87
+ "type": "object",
88
+ "additionalProperties": false,
89
+ "required": ["id", "type", "display", "roles", "impersonator"],
90
+ "properties": {
91
+ "id": {"type": ["string", "null"]},
92
+ "type": {"type": ["string", "null"]},
93
+ "display": {"type": ["string", "null"]},
94
+ "roles": {"type": "array", "items": {"type": "string"}},
95
+ "impersonator": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/actor"}]}
96
+ }
97
+ },
98
+ "change": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "required": ["entity", "entity_id", "operation", "diff"],
102
+ "properties": {
103
+ "entity": {"type": "string"},
104
+ "entity_id": {"type": ["string", "null"]},
105
+ "operation": {
106
+ "enum": ["create", "update", "destroy", "link", "unlink", "bulk_update", "bulk_delete", "bulk_insert"]
107
+ },
108
+ "diff": {"type": ["object", "null"]},
109
+ "association": {"type": "string"},
110
+ "target": {
111
+ "type": "object",
112
+ "additionalProperties": false,
113
+ "required": ["type", "id"],
114
+ "properties": {
115
+ "type": {"type": "string"},
116
+ "id": {"type": ["string", "null"]}
117
+ }
118
+ },
119
+ "count": {"type": ["integer", "null"]},
120
+ "where": {"type": ["string", "null"]},
121
+ "ids": {"type": "array", "items": {"type": "string"}},
122
+ "truncated": {"type": "boolean"}
123
+ }
124
+ }
125
+ }
126
+ }
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: provenance
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ivan Nikolaev
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activejob
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activesupport
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: railties
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '7.2'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '7.2'
68
+ description: 'Provenance records who did what, through which entry point, with what
69
+ outcome and which records changed: one event per HTTP request, job, rake task or
70
+ explicit block, delivered to SIEM, log pipelines and webhooks in native, OCSF or
71
+ CloudEvents format.'
72
+ email:
73
+ - ivan_n6_20@icloud.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - CHANGELOG.md
79
+ - LICENSE
80
+ - README.md
81
+ - lib/generators/provenance/install/install_generator.rb
82
+ - lib/generators/provenance/install/templates/create_provenance_outbox.rb.tt
83
+ - lib/generators/provenance/install/templates/initializer.rb.tt
84
+ - lib/provenance.rb
85
+ - lib/provenance/action.rb
86
+ - lib/provenance/actor.rb
87
+ - lib/provenance/configuration.rb
88
+ - lib/provenance/context.rb
89
+ - lib/provenance/controller.rb
90
+ - lib/provenance/emitter.rb
91
+ - lib/provenance/entity_change.rb
92
+ - lib/provenance/errors.rb
93
+ - lib/provenance/event.rb
94
+ - lib/provenance/integrity.rb
95
+ - lib/provenance/job.rb
96
+ - lib/provenance/json_safe.rb
97
+ - lib/provenance/middleware.rb
98
+ - lib/provenance/model.rb
99
+ - lib/provenance/outbox.rb
100
+ - lib/provenance/outbox/record.rb
101
+ - lib/provenance/outbox/relay.rb
102
+ - lib/provenance/railtie.rb
103
+ - lib/provenance/rake.rb
104
+ - lib/provenance/recorder.rb
105
+ - lib/provenance/redactor.rb
106
+ - lib/provenance/registry.rb
107
+ - lib/provenance/relay_job.rb
108
+ - lib/provenance/rspec.rb
109
+ - lib/provenance/serializers.rb
110
+ - lib/provenance/serializers/cloud_events.rb
111
+ - lib/provenance/serializers/native.rb
112
+ - lib/provenance/serializers/ocsf.rb
113
+ - lib/provenance/sinks.rb
114
+ - lib/provenance/sinks/http.rb
115
+ - lib/provenance/sinks/io.rb
116
+ - lib/provenance/sinks/kafka.rb
117
+ - lib/provenance/sinks/logger.rb
118
+ - lib/provenance/sinks/memory.rb
119
+ - lib/provenance/sinks/proc.rb
120
+ - lib/provenance/testing.rb
121
+ - lib/provenance/uuid.rb
122
+ - lib/provenance/version.rb
123
+ - lib/tasks/provenance.rake
124
+ - schema/event-2.json
125
+ homepage: https://github.com/inikalaev/provenance
126
+ licenses:
127
+ - MIT
128
+ metadata:
129
+ homepage_uri: https://github.com/inikalaev/provenance
130
+ source_code_uri: https://github.com/inikalaev/provenance
131
+ changelog_uri: https://github.com/inikalaev/provenance/blob/v2/CHANGELOG.md
132
+ rubygems_mfa_required: 'true'
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '3.2'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubygems_version: 3.6.9
148
+ specification_version: 4
149
+ summary: Action-level, tamper-evident audit events for Rails applications
150
+ test_files: []