orfeas_lyra 0.6.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 +7 -0
- data/CHANGELOG.md +222 -0
- data/LICENSE +21 -0
- data/README.md +1165 -0
- data/Rakefile +728 -0
- data/app/controllers/lyra/application_controller.rb +23 -0
- data/app/controllers/lyra/dashboard_controller.rb +624 -0
- data/app/controllers/lyra/flow_controller.rb +224 -0
- data/app/controllers/lyra/privacy_controller.rb +182 -0
- data/app/views/lyra/dashboard/audit_trail.html.erb +324 -0
- data/app/views/lyra/dashboard/discrepancies.html.erb +125 -0
- data/app/views/lyra/dashboard/event_graph_view.html.erb +525 -0
- data/app/views/lyra/dashboard/heatmap_view.html.erb +155 -0
- data/app/views/lyra/dashboard/index.html.erb +119 -0
- data/app/views/lyra/dashboard/model_overview.html.erb +115 -0
- data/app/views/lyra/dashboard/projections.html.erb +302 -0
- data/app/views/lyra/dashboard/schema.html.erb +283 -0
- data/app/views/lyra/dashboard/schema_history.html.erb +78 -0
- data/app/views/lyra/dashboard/schema_version.html.erb +340 -0
- data/app/views/lyra/dashboard/verification.html.erb +370 -0
- data/app/views/lyra/flow/crud_mapping.html.erb +125 -0
- data/app/views/lyra/flow/timeline.html.erb +260 -0
- data/app/views/lyra/privacy/pii_detection.html.erb +148 -0
- data/app/views/lyra/privacy/policy.html.erb +188 -0
- data/app/workflows/es_async_mode_workflow.rb +80 -0
- data/app/workflows/es_sync_mode_workflow.rb +64 -0
- data/app/workflows/hijack_mode_workflow.rb +54 -0
- data/app/workflows/lifecycle_workflow.rb +43 -0
- data/app/workflows/monitor_mode_workflow.rb +39 -0
- data/config/privacy_policies.rb +273 -0
- data/config/routes.rb +48 -0
- data/lib/lyra/aggregate.rb +131 -0
- data/lib/lyra/associations/event_aware.rb +225 -0
- data/lib/lyra/command.rb +81 -0
- data/lib/lyra/command_handler.rb +155 -0
- data/lib/lyra/configuration.rb +124 -0
- data/lib/lyra/consistency/read_your_writes.rb +91 -0
- data/lib/lyra/correlation.rb +144 -0
- data/lib/lyra/dual_view.rb +231 -0
- data/lib/lyra/engine.rb +67 -0
- data/lib/lyra/event.rb +71 -0
- data/lib/lyra/event_analyzer.rb +135 -0
- data/lib/lyra/event_flow.rb +449 -0
- data/lib/lyra/event_mapper.rb +106 -0
- data/lib/lyra/event_store_adapter.rb +72 -0
- data/lib/lyra/id_generator.rb +137 -0
- data/lib/lyra/interceptors/association_interceptor.rb +169 -0
- data/lib/lyra/interceptors/crud_interceptor.rb +543 -0
- data/lib/lyra/privacy/gdpr_compliance.rb +161 -0
- data/lib/lyra/privacy/pii_detector.rb +85 -0
- data/lib/lyra/privacy/pii_masker.rb +66 -0
- data/lib/lyra/privacy/policy_integration.rb +253 -0
- data/lib/lyra/projection.rb +94 -0
- data/lib/lyra/projections/async_projection_job.rb +63 -0
- data/lib/lyra/projections/cached_projection.rb +322 -0
- data/lib/lyra/projections/cached_relation.rb +757 -0
- data/lib/lyra/projections/event_store_reader.rb +127 -0
- data/lib/lyra/projections/model_projection.rb +143 -0
- data/lib/lyra/schema/diff.rb +331 -0
- data/lib/lyra/schema/event_class_registrar.rb +63 -0
- data/lib/lyra/schema/generator.rb +190 -0
- data/lib/lyra/schema/reporter.rb +188 -0
- data/lib/lyra/schema/store.rb +156 -0
- data/lib/lyra/schema/validator.rb +100 -0
- data/lib/lyra/strict_data_access.rb +363 -0
- data/lib/lyra/verification/crud_lifecycle_workflow.rb +456 -0
- data/lib/lyra/verification/workflow_generator.rb +540 -0
- data/lib/lyra/version.rb +3 -0
- data/lib/lyra/visualization/activity_heatmap.rb +215 -0
- data/lib/lyra/visualization/event_graph.rb +310 -0
- data/lib/lyra/visualization/timeline.rb +398 -0
- data/lib/lyra.rb +150 -0
- data/lib/tasks/dist.rake +391 -0
- data/lib/tasks/gems.rake +185 -0
- data/lib/tasks/lyra_schema.rake +231 -0
- data/lib/tasks/lyra_workflows.rake +452 -0
- data/lib/tasks/public_release.rake +351 -0
- data/lib/tasks/stats.rake +175 -0
- data/lib/tasks/testbed.rake +479 -0
- data/lib/tasks/version.rake +159 -0
- metadata +221 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
module Lyra
|
|
2
|
+
class FlowController < ApplicationController
|
|
3
|
+
# GET /lyra/flow/timeline
|
|
4
|
+
def timeline
|
|
5
|
+
subject_id = params[:subject_id]
|
|
6
|
+
subject_type = params[:subject_type]
|
|
7
|
+
|
|
8
|
+
flow = EventFlow.new(
|
|
9
|
+
subject_id: subject_id,
|
|
10
|
+
subject_type: subject_type
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
@flow_data = flow.flow_data
|
|
14
|
+
@timeline = @flow_data[:timeline] || []
|
|
15
|
+
@flows = @flow_data[:flows] || []
|
|
16
|
+
@statistics = @flow_data[:statistics] || {}
|
|
17
|
+
@privacy_impact = @flow_data[:privacy_impact] || {}
|
|
18
|
+
|
|
19
|
+
# Get available filter options
|
|
20
|
+
@available_models = @timeline.map { |e| e[:model_class] }.compact.uniq.sort
|
|
21
|
+
@available_operations = @timeline.map { |e| e[:operation] }.compact.uniq.sort
|
|
22
|
+
|
|
23
|
+
# Apply filters
|
|
24
|
+
@filter_model = params[:model]
|
|
25
|
+
@filter_operation = params[:operation]
|
|
26
|
+
|
|
27
|
+
if @filter_model.present?
|
|
28
|
+
@timeline = @timeline.select { |e| e[:model_class] == @filter_model }
|
|
29
|
+
end
|
|
30
|
+
if @filter_operation.present?
|
|
31
|
+
@timeline = @timeline.select { |e| e[:operation].to_s == @filter_operation }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
respond_to do |format|
|
|
35
|
+
format.html
|
|
36
|
+
format.json { render json: @flow_data }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# GET /lyra/flow/event_chain/:model_class/:model_id
|
|
41
|
+
def event_chain
|
|
42
|
+
model_class = params[:model_class].constantize
|
|
43
|
+
model_id = params[:model_id]
|
|
44
|
+
|
|
45
|
+
flow = EventFlow.new
|
|
46
|
+
chain = flow.reconstruct_state_chain(model_class.name, model_id)
|
|
47
|
+
|
|
48
|
+
render json: chain
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# GET /lyra/flow/crud_mapping
|
|
52
|
+
def crud_mapping
|
|
53
|
+
model_class = params[:model_class]
|
|
54
|
+
operation = params[:operation]&.to_sym
|
|
55
|
+
model_id = params[:model_id]
|
|
56
|
+
|
|
57
|
+
if model_class && operation
|
|
58
|
+
# Specific mapping lookup
|
|
59
|
+
flow = EventFlow.new
|
|
60
|
+
mapping = flow.crud_to_event_mapping(model_class, operation, model_id)
|
|
61
|
+
respond_to do |format|
|
|
62
|
+
format.html { render json: mapping }
|
|
63
|
+
format.json { render json: mapping }
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
# Show summary of all CRUD operations
|
|
67
|
+
events = Lyra.config.event_store.read.to_a
|
|
68
|
+
@models_summary = events.group_by { |e| event_model_class(e) }.transform_values do |model_events|
|
|
69
|
+
{
|
|
70
|
+
total: model_events.count,
|
|
71
|
+
operations: model_events.group_by { |e| event_operation(e) }.transform_values(&:count)
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
@total_events = events.count
|
|
75
|
+
@operations_summary = events.group_by { |e| event_operation(e) }.transform_values(&:count)
|
|
76
|
+
|
|
77
|
+
respond_to do |format|
|
|
78
|
+
format.html
|
|
79
|
+
format.json do
|
|
80
|
+
render json: {
|
|
81
|
+
total_events: @total_events,
|
|
82
|
+
operations: @operations_summary,
|
|
83
|
+
models: @models_summary
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# GET /lyra/flow/visualization/:model_class/:model_id
|
|
91
|
+
def visualization
|
|
92
|
+
model_class = params[:model_class].constantize
|
|
93
|
+
model_id = params[:model_id]
|
|
94
|
+
format = params[:format] || 'json'
|
|
95
|
+
|
|
96
|
+
stream_name = "#{model_class.name}$#{model_id}"
|
|
97
|
+
events = Lyra.config.event_store.read.stream(stream_name).to_a
|
|
98
|
+
|
|
99
|
+
timeline = Visualization::Timeline.new(events)
|
|
100
|
+
|
|
101
|
+
case format
|
|
102
|
+
when 'html'
|
|
103
|
+
render html: timeline.to_html.html_safe
|
|
104
|
+
when 'mermaid'
|
|
105
|
+
render plain: timeline.to_mermaid
|
|
106
|
+
when 'ascii'
|
|
107
|
+
render plain: timeline.to_ascii
|
|
108
|
+
when 'd3'
|
|
109
|
+
render json: JSON.parse(timeline.to_d3_json)
|
|
110
|
+
else
|
|
111
|
+
render json: timeline.to_data
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# GET /lyra/flow/correlation/:correlation_id
|
|
116
|
+
def correlation
|
|
117
|
+
correlation_id = params[:correlation_id]
|
|
118
|
+
|
|
119
|
+
events = Lyra.config.event_store.read.to_a.select do |event|
|
|
120
|
+
event.metadata[:correlation_id] == correlation_id
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
flow = EventFlow.new
|
|
124
|
+
timeline = flow.build_timeline(events)
|
|
125
|
+
|
|
126
|
+
render json: {
|
|
127
|
+
correlation_id: correlation_id,
|
|
128
|
+
events_count: events.count,
|
|
129
|
+
started_at: events.min_by(&:timestamp)&.timestamp,
|
|
130
|
+
completed_at: events.max_by(&:timestamp)&.timestamp,
|
|
131
|
+
events: timeline,
|
|
132
|
+
privacy_impact: pam_dsl_available? ? events.count { |e| Privacy::PIIDetector.detect(event_attributes(e)).any? } : nil
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# GET /lyra/flow/user_actions/:user_id
|
|
137
|
+
def user_actions
|
|
138
|
+
user_id = params[:user_id]
|
|
139
|
+
|
|
140
|
+
events = Lyra.config.event_store.read.to_a.select do |event|
|
|
141
|
+
event.metadata[:user_id] == user_id.to_i
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
actions = events.group_by { |e| e.metadata[:action_id] }
|
|
145
|
+
|
|
146
|
+
render json: {
|
|
147
|
+
user_id: user_id,
|
|
148
|
+
total_actions: actions.count,
|
|
149
|
+
total_events: events.count,
|
|
150
|
+
actions: actions.map do |action_id, action_events|
|
|
151
|
+
{
|
|
152
|
+
action_id: action_id,
|
|
153
|
+
user_action: action_events.first.metadata[:user_action],
|
|
154
|
+
timestamp: event_timestamp(action_events.min_by { |e| event_timestamp(e) }),
|
|
155
|
+
events_count: action_events.count,
|
|
156
|
+
models_affected: action_events.map { |e| event_model_class(e) }.uniq,
|
|
157
|
+
pii_affected: pam_dsl_available? ? action_events.any? { |e| Privacy::PIIDetector.detect(event_attributes(e)).any? } : nil
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def pam_dsl_available?
|
|
166
|
+
defined?(PAM_DSL_AVAILABLE) && PAM_DSL_AVAILABLE
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Helper methods to extract data from events (works with both Lyra::Event and RubyEventStore::Event)
|
|
170
|
+
def event_operation(event)
|
|
171
|
+
return event.operation if event.respond_to?(:operation)
|
|
172
|
+
data = event.respond_to?(:data) ? event.data : nil
|
|
173
|
+
return nil unless data
|
|
174
|
+
op = data[:operation] || data["operation"]
|
|
175
|
+
op.is_a?(String) ? op.to_sym : op
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def event_model_class(event)
|
|
179
|
+
return event.model_class if event.respond_to?(:model_class)
|
|
180
|
+
data = event.respond_to?(:data) ? event.data : nil
|
|
181
|
+
return nil unless data
|
|
182
|
+
data[:model_class] || data["model_class"]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def event_model_id(event)
|
|
186
|
+
return event.model_id if event.respond_to?(:model_id)
|
|
187
|
+
data = event.respond_to?(:data) ? event.data : nil
|
|
188
|
+
return nil unless data
|
|
189
|
+
data[:model_id] || data["model_id"]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def event_timestamp(event)
|
|
193
|
+
return event.timestamp if event.respond_to?(:timestamp) && event.timestamp
|
|
194
|
+
data = event.respond_to?(:data) ? event.data : nil
|
|
195
|
+
return nil unless data
|
|
196
|
+
data[:timestamp] || data["timestamp"] || event.metadata[:timestamp]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def event_attributes(event)
|
|
200
|
+
# First try Lyra::Event accessor method
|
|
201
|
+
if event.respond_to?(:attributes)
|
|
202
|
+
attrs = event.attributes
|
|
203
|
+
return attrs if attrs.is_a?(Hash)
|
|
204
|
+
end
|
|
205
|
+
# Fall back to data hash for RubyEventStore events
|
|
206
|
+
data = event.respond_to?(:data) ? event.data : nil
|
|
207
|
+
return {} unless data
|
|
208
|
+
data[:attributes] || data["attributes"] || {}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def load_events(subject_id, subject_type)
|
|
212
|
+
events = Lyra.config.event_store.read.to_a
|
|
213
|
+
|
|
214
|
+
if subject_id && subject_type
|
|
215
|
+
events = events.select do |event|
|
|
216
|
+
event.metadata[:user_id] == subject_id.to_i ||
|
|
217
|
+
(event_model_class(event) == subject_type && event_model_id(event) == subject_id.to_i)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
events
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
module Lyra
|
|
2
|
+
class PrivacyController < ApplicationController
|
|
3
|
+
before_action :require_pam_dsl, except: [:policy]
|
|
4
|
+
|
|
5
|
+
# GET /lyra/privacy/subject/:subject_type/:subject_id
|
|
6
|
+
def subject_data
|
|
7
|
+
subject_type = params[:subject_type]
|
|
8
|
+
subject_id = params[:subject_id]
|
|
9
|
+
|
|
10
|
+
compliance = Privacy::GDPRCompliance.new(
|
|
11
|
+
subject_id: subject_id,
|
|
12
|
+
subject_type: subject_type
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
render json: {
|
|
16
|
+
subject: { type: subject_type, id: subject_id },
|
|
17
|
+
data_export: compliance.data_export,
|
|
18
|
+
generated_at: Time.current
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# GET /lyra/privacy/gdpr_report/:subject_type/:subject_id
|
|
23
|
+
def gdpr_report
|
|
24
|
+
subject_type = params[:subject_type]
|
|
25
|
+
subject_id = params[:subject_id]
|
|
26
|
+
|
|
27
|
+
compliance = Privacy::GDPRCompliance.new(
|
|
28
|
+
subject_id: subject_id,
|
|
29
|
+
subject_type: subject_type
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
render json: {
|
|
33
|
+
subject: { type: subject_type, id: subject_id },
|
|
34
|
+
right_to_access: compliance.data_export,
|
|
35
|
+
right_to_be_forgotten: compliance.right_to_be_forgotten_report,
|
|
36
|
+
rectification_history: compliance.rectification_history,
|
|
37
|
+
processing_activities: compliance.processing_activities,
|
|
38
|
+
retention_compliance: compliance.retention_compliance_check,
|
|
39
|
+
consent_audit: compliance.consent_audit
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# GET /lyra/privacy/portable_export/:subject_type/:subject_id
|
|
44
|
+
def portable_export
|
|
45
|
+
subject_type = params[:subject_type]
|
|
46
|
+
subject_id = params[:subject_id]
|
|
47
|
+
format = params[:format]&.to_sym || :json
|
|
48
|
+
|
|
49
|
+
compliance = Privacy::GDPRCompliance.new(
|
|
50
|
+
subject_id: subject_id,
|
|
51
|
+
subject_type: subject_type
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
data = compliance.portable_export(format: format)
|
|
55
|
+
|
|
56
|
+
case format
|
|
57
|
+
when :json
|
|
58
|
+
render json: data
|
|
59
|
+
when :csv
|
|
60
|
+
send_data data, filename: "data_export_#{subject_id}.csv", type: 'text/csv'
|
|
61
|
+
when :xml
|
|
62
|
+
send_data data, filename: "data_export_#{subject_id}.xml", type: 'application/xml'
|
|
63
|
+
else
|
|
64
|
+
render json: { error: "Unsupported format" }, status: :bad_request
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# GET /lyra/privacy/pii_inventory/:subject_type/:subject_id
|
|
69
|
+
def pii_inventory
|
|
70
|
+
subject_type = params[:subject_type]
|
|
71
|
+
subject_id = params[:subject_id]
|
|
72
|
+
|
|
73
|
+
flow = EventFlow.new(subject_id: subject_id, subject_type: subject_type)
|
|
74
|
+
data = flow.privacy_impact_analysis
|
|
75
|
+
|
|
76
|
+
render json: data
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# GET /lyra/privacy/data_lineage/:field_name
|
|
80
|
+
def data_lineage
|
|
81
|
+
field_name = params[:field_name]
|
|
82
|
+
model_class = params[:model_class]
|
|
83
|
+
|
|
84
|
+
flow = EventFlow.new
|
|
85
|
+
lineage = flow.data_lineage(field_name, model_class)
|
|
86
|
+
|
|
87
|
+
render json: lineage
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# GET /lyra/privacy/policy
|
|
91
|
+
def policy
|
|
92
|
+
@pam_dsl_available = pam_dsl_available?
|
|
93
|
+
@policies = @pam_dsl_available && PamDsl.respond_to?(:registry) ? PamDsl.registry.policies : {}
|
|
94
|
+
@default_policy_name = Rails.application.config.respond_to?(:pam_dsl) ?
|
|
95
|
+
Rails.application.config.pam_dsl.default_policy : nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# GET /lyra/privacy/pii_detection
|
|
99
|
+
def pii_detection
|
|
100
|
+
# Detect PII across all events
|
|
101
|
+
events = Lyra.config.event_store.read.to_a
|
|
102
|
+
@pii_inventory = extract_pii_from_events(events)
|
|
103
|
+
|
|
104
|
+
@total_events = events.count
|
|
105
|
+
@events_with_pii = events.count { |e| Privacy::PIIDetector.detect(event_attributes(e)).any? }
|
|
106
|
+
@pii_categories = @pii_inventory.keys
|
|
107
|
+
@sensitive_pii = @pii_inventory.select { |k, _|
|
|
108
|
+
[:ssn, :credit_card, :health, :biometric].include?(k)
|
|
109
|
+
}.keys
|
|
110
|
+
@contact_pii = @pii_inventory.select { |k, _|
|
|
111
|
+
[:email, :phone, :address].include?(k)
|
|
112
|
+
}.keys
|
|
113
|
+
|
|
114
|
+
respond_to do |format|
|
|
115
|
+
format.html
|
|
116
|
+
format.json do
|
|
117
|
+
render json: {
|
|
118
|
+
total_events: @total_events,
|
|
119
|
+
events_with_pii: @events_with_pii,
|
|
120
|
+
pii_categories: @pii_categories,
|
|
121
|
+
pii_inventory: @pii_inventory,
|
|
122
|
+
summary: {
|
|
123
|
+
sensitive_pii: @sensitive_pii,
|
|
124
|
+
contact_pii: @contact_pii
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def require_pam_dsl
|
|
134
|
+
unless defined?(PAM_DSL_AVAILABLE) && PAM_DSL_AVAILABLE
|
|
135
|
+
respond_to do |format|
|
|
136
|
+
format.html { render plain: "Privacy features require PAM DSL. Add 'pam_dsl' to your Gemfile.", status: :service_unavailable }
|
|
137
|
+
format.json { render json: { error: "Privacy features unavailable", message: "PAM DSL is not installed" }, status: :service_unavailable }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def pam_dsl_available?
|
|
143
|
+
defined?(PAM_DSL_AVAILABLE) && PAM_DSL_AVAILABLE
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def event_attributes(event)
|
|
147
|
+
# First try Lyra::Event accessor method
|
|
148
|
+
if event.respond_to?(:attributes)
|
|
149
|
+
attrs = event.attributes
|
|
150
|
+
return attrs if attrs.is_a?(Hash)
|
|
151
|
+
end
|
|
152
|
+
# Fall back to data hash for RubyEventStore events
|
|
153
|
+
data = event.respond_to?(:data) ? event.data : nil
|
|
154
|
+
return {} unless data
|
|
155
|
+
data[:attributes] || data["attributes"] || {}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def event_model_class(event)
|
|
159
|
+
return event.model_class if event.respond_to?(:model_class)
|
|
160
|
+
data = event.respond_to?(:data) ? event.data : nil
|
|
161
|
+
return nil unless data
|
|
162
|
+
data[:model_class] || data["model_class"]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def extract_pii_from_events(events)
|
|
166
|
+
inventory = Hash.new { |h, k| h[k] = [] }
|
|
167
|
+
events.each do |event|
|
|
168
|
+
pii = Privacy::PIIDetector.detect(event_attributes(event))
|
|
169
|
+
next unless pii.is_a?(Hash)
|
|
170
|
+
pii.each do |field, info|
|
|
171
|
+
next unless info.is_a?(Hash) && info[:type]
|
|
172
|
+
inventory[info[:type]] << {
|
|
173
|
+
field: field,
|
|
174
|
+
model_class: event_model_class(event),
|
|
175
|
+
value: info[:value]
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
inventory
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|