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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +222 -0
  3. data/LICENSE +21 -0
  4. data/README.md +1165 -0
  5. data/Rakefile +728 -0
  6. data/app/controllers/lyra/application_controller.rb +23 -0
  7. data/app/controllers/lyra/dashboard_controller.rb +624 -0
  8. data/app/controllers/lyra/flow_controller.rb +224 -0
  9. data/app/controllers/lyra/privacy_controller.rb +182 -0
  10. data/app/views/lyra/dashboard/audit_trail.html.erb +324 -0
  11. data/app/views/lyra/dashboard/discrepancies.html.erb +125 -0
  12. data/app/views/lyra/dashboard/event_graph_view.html.erb +525 -0
  13. data/app/views/lyra/dashboard/heatmap_view.html.erb +155 -0
  14. data/app/views/lyra/dashboard/index.html.erb +119 -0
  15. data/app/views/lyra/dashboard/model_overview.html.erb +115 -0
  16. data/app/views/lyra/dashboard/projections.html.erb +302 -0
  17. data/app/views/lyra/dashboard/schema.html.erb +283 -0
  18. data/app/views/lyra/dashboard/schema_history.html.erb +78 -0
  19. data/app/views/lyra/dashboard/schema_version.html.erb +340 -0
  20. data/app/views/lyra/dashboard/verification.html.erb +370 -0
  21. data/app/views/lyra/flow/crud_mapping.html.erb +125 -0
  22. data/app/views/lyra/flow/timeline.html.erb +260 -0
  23. data/app/views/lyra/privacy/pii_detection.html.erb +148 -0
  24. data/app/views/lyra/privacy/policy.html.erb +188 -0
  25. data/app/workflows/es_async_mode_workflow.rb +80 -0
  26. data/app/workflows/es_sync_mode_workflow.rb +64 -0
  27. data/app/workflows/hijack_mode_workflow.rb +54 -0
  28. data/app/workflows/lifecycle_workflow.rb +43 -0
  29. data/app/workflows/monitor_mode_workflow.rb +39 -0
  30. data/config/privacy_policies.rb +273 -0
  31. data/config/routes.rb +48 -0
  32. data/lib/lyra/aggregate.rb +131 -0
  33. data/lib/lyra/associations/event_aware.rb +225 -0
  34. data/lib/lyra/command.rb +81 -0
  35. data/lib/lyra/command_handler.rb +155 -0
  36. data/lib/lyra/configuration.rb +124 -0
  37. data/lib/lyra/consistency/read_your_writes.rb +91 -0
  38. data/lib/lyra/correlation.rb +144 -0
  39. data/lib/lyra/dual_view.rb +231 -0
  40. data/lib/lyra/engine.rb +67 -0
  41. data/lib/lyra/event.rb +71 -0
  42. data/lib/lyra/event_analyzer.rb +135 -0
  43. data/lib/lyra/event_flow.rb +449 -0
  44. data/lib/lyra/event_mapper.rb +106 -0
  45. data/lib/lyra/event_store_adapter.rb +72 -0
  46. data/lib/lyra/id_generator.rb +137 -0
  47. data/lib/lyra/interceptors/association_interceptor.rb +169 -0
  48. data/lib/lyra/interceptors/crud_interceptor.rb +543 -0
  49. data/lib/lyra/privacy/gdpr_compliance.rb +161 -0
  50. data/lib/lyra/privacy/pii_detector.rb +85 -0
  51. data/lib/lyra/privacy/pii_masker.rb +66 -0
  52. data/lib/lyra/privacy/policy_integration.rb +253 -0
  53. data/lib/lyra/projection.rb +94 -0
  54. data/lib/lyra/projections/async_projection_job.rb +63 -0
  55. data/lib/lyra/projections/cached_projection.rb +322 -0
  56. data/lib/lyra/projections/cached_relation.rb +757 -0
  57. data/lib/lyra/projections/event_store_reader.rb +127 -0
  58. data/lib/lyra/projections/model_projection.rb +143 -0
  59. data/lib/lyra/schema/diff.rb +331 -0
  60. data/lib/lyra/schema/event_class_registrar.rb +63 -0
  61. data/lib/lyra/schema/generator.rb +190 -0
  62. data/lib/lyra/schema/reporter.rb +188 -0
  63. data/lib/lyra/schema/store.rb +156 -0
  64. data/lib/lyra/schema/validator.rb +100 -0
  65. data/lib/lyra/strict_data_access.rb +363 -0
  66. data/lib/lyra/verification/crud_lifecycle_workflow.rb +456 -0
  67. data/lib/lyra/verification/workflow_generator.rb +540 -0
  68. data/lib/lyra/version.rb +3 -0
  69. data/lib/lyra/visualization/activity_heatmap.rb +215 -0
  70. data/lib/lyra/visualization/event_graph.rb +310 -0
  71. data/lib/lyra/visualization/timeline.rb +398 -0
  72. data/lib/lyra.rb +150 -0
  73. data/lib/tasks/dist.rake +391 -0
  74. data/lib/tasks/gems.rake +185 -0
  75. data/lib/tasks/lyra_schema.rake +231 -0
  76. data/lib/tasks/lyra_workflows.rake +452 -0
  77. data/lib/tasks/public_release.rake +351 -0
  78. data/lib/tasks/stats.rake +175 -0
  79. data/lib/tasks/testbed.rake +479 -0
  80. data/lib/tasks/version.rake +159 -0
  81. 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