beskar 0.0.1 → 0.1.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.md +987 -21
  4. data/app/controllers/beskar/application_controller.rb +170 -0
  5. data/app/controllers/beskar/banned_ips_controller.rb +280 -0
  6. data/app/controllers/beskar/dashboard_controller.rb +70 -0
  7. data/app/controllers/beskar/security_events_controller.rb +182 -0
  8. data/app/controllers/concerns/beskar/controllers/security_tracking.rb +70 -0
  9. data/app/models/beskar/banned_ip.rb +193 -0
  10. data/app/models/beskar/security_event.rb +64 -0
  11. data/app/services/beskar/banned_ip_manager.rb +78 -0
  12. data/app/views/beskar/banned_ips/edit.html.erb +259 -0
  13. data/app/views/beskar/banned_ips/index.html.erb +361 -0
  14. data/app/views/beskar/banned_ips/new.html.erb +310 -0
  15. data/app/views/beskar/banned_ips/show.html.erb +310 -0
  16. data/app/views/beskar/dashboard/index.html.erb +280 -0
  17. data/app/views/beskar/security_events/index.html.erb +309 -0
  18. data/app/views/beskar/security_events/show.html.erb +307 -0
  19. data/app/views/layouts/beskar/application.html.erb +647 -5
  20. data/config/locales/en.yml +10 -0
  21. data/config/routes.rb +41 -0
  22. data/db/migrate/20251016000001_create_beskar_security_events.rb +25 -0
  23. data/db/migrate/20251016000002_create_beskar_banned_ips.rb +23 -0
  24. data/lib/beskar/configuration.rb +214 -0
  25. data/lib/beskar/engine.rb +105 -0
  26. data/lib/beskar/logger.rb +293 -0
  27. data/lib/beskar/middleware/request_analyzer.rb +305 -0
  28. data/lib/beskar/middleware.rb +4 -0
  29. data/lib/beskar/models/security_trackable.rb +25 -0
  30. data/lib/beskar/models/security_trackable_authenticable.rb +167 -0
  31. data/lib/beskar/models/security_trackable_devise.rb +82 -0
  32. data/lib/beskar/models/security_trackable_generic.rb +355 -0
  33. data/lib/beskar/services/account_locker.rb +263 -0
  34. data/lib/beskar/services/device_detector.rb +250 -0
  35. data/lib/beskar/services/geolocation_service.rb +392 -0
  36. data/lib/beskar/services/ip_whitelist.rb +113 -0
  37. data/lib/beskar/services/rate_limiter.rb +257 -0
  38. data/lib/beskar/services/waf.rb +551 -0
  39. data/lib/beskar/version.rb +1 -1
  40. data/lib/beskar.rb +32 -1
  41. data/lib/generators/beskar/install/install_generator.rb +158 -0
  42. data/lib/generators/beskar/install/templates/initializer.rb.tt +177 -0
  43. data/lib/tasks/beskar_tasks.rake +121 -4
  44. metadata +138 -5
@@ -0,0 +1,355 @@
1
+ module Beskar
2
+ module Models
3
+ # Generic security tracking functionality shared by all authentication systems
4
+ # This module provides the core security event tracking, risk scoring,
5
+ # and adaptive learning features that work with any authentication framework
6
+ module SecurityTrackableGeneric
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ has_many :security_events, class_name: "Beskar::SecurityEvent", as: :user, dependent: :destroy
11
+ end
12
+
13
+ module ClassMethods
14
+ # Track failed authentication attempt without a user context
15
+ # Used when authentication fails and we don't have a user object
16
+ def track_failed_authentication(request, scope)
17
+ # Skip tracking if disabled in configuration
18
+ unless Beskar.configuration.track_failed_logins?
19
+ Beskar::Logger.debug("Failed login tracking disabled in configuration")
20
+ return
21
+ end
22
+
23
+ # Extract attempted email from params based on scope
24
+ attempted_email = extract_attempted_email(request, scope)
25
+
26
+ # Create a security event for failed authentication
27
+ metadata = {
28
+ scope: scope.to_s,
29
+ attempted_email: attempted_email,
30
+ timestamp: Time.current.iso8601,
31
+ session_id: request.session.id,
32
+ request_path: request.path,
33
+ referer: request.referer,
34
+ accept_language: request.headers["Accept-Language"],
35
+ x_forwarded_for: request.headers["X-Forwarded-For"],
36
+ x_real_ip: request.headers["X-Real-IP"],
37
+ device_info: Beskar::Services::DeviceDetector.detect(request.user_agent),
38
+ geolocation: Beskar::Services::GeolocationService.locate(request.ip)
39
+ }
40
+
41
+ Beskar::SecurityEvent.create!(
42
+ user: nil,
43
+ event_type: "login_failure",
44
+ ip_address: request.ip,
45
+ user_agent: request.user_agent,
46
+ attempted_email: attempted_email,
47
+ metadata: metadata,
48
+ risk_score: calculate_failure_risk_score(request)
49
+ )
50
+
51
+ # Trigger rate limiting check
52
+ Beskar::Services::RateLimiter.check_authentication_attempt(request, :failure)
53
+ end
54
+
55
+ private
56
+
57
+ def extract_attempted_email(request, scope)
58
+ # Try different param patterns based on scope
59
+ request.params.dig("devise_user", "email") ||
60
+ request.params.dig(scope.to_s, "email") ||
61
+ request.params.dig("user", "email") ||
62
+ request.params.dig("email_address") ||
63
+ request.params["email"]
64
+ end
65
+
66
+ def calculate_failure_risk_score(request)
67
+ score = 10 # Base score for failed login
68
+
69
+ # Use device detector for comprehensive risk assessment
70
+ device_detector = Beskar::Services::DeviceDetector.new
71
+ score += device_detector.calculate_user_agent_risk(request.user_agent)
72
+
73
+ # Additional failure-specific risk factors
74
+ password = request.params.dig("user", "password") ||
75
+ request.params.dig("devise_user", "password") ||
76
+ request.params["password"]
77
+
78
+ if password&.length.to_i > 50
79
+ score += 10
80
+ Beskar::Logger.info("Suspicious password length: #{password.length}, adding 10 risk")
81
+ end
82
+
83
+ # Use geolocation service for location-based risk
84
+ geolocation_service = Beskar::Services::GeolocationService.new
85
+ geo_score = geolocation_service.calculate_location_risk(request.ip)
86
+ score += geo_score
87
+ Beskar::Logger.info("Geolocation risk: #{geo_score}")
88
+
89
+ [score, 100].min # Cap at 100
90
+ end
91
+ end
92
+
93
+ # Track authentication event (success or failure) for a specific user
94
+ def track_authentication_event(request, result)
95
+ return unless request
96
+
97
+ # Check if tracking is enabled for this event type
98
+ if result == :success && !Beskar.configuration.track_successful_logins?
99
+ Beskar::Logger.debug("Successful login tracking disabled in configuration")
100
+ return
101
+ elsif result == :failure && !Beskar.configuration.track_failed_logins?
102
+ Beskar::Logger.debug("Failed login tracking disabled in configuration")
103
+ return
104
+ end
105
+
106
+ event_type = result == :success ? "login_success" : "login_failure"
107
+
108
+ security_event = security_events.build(
109
+ event_type: event_type,
110
+ ip_address: request.ip,
111
+ user_agent: request.user_agent,
112
+ attempted_email: extract_user_email,
113
+ metadata: extract_security_context(request),
114
+ risk_score: calculate_risk_score(request, result)
115
+ )
116
+
117
+ if security_event.save
118
+ # Perform background security analysis
119
+ analyze_suspicious_patterns_async if result == :success && Beskar.configuration.auto_analyze_patterns?
120
+
121
+ # Update rate limiting
122
+ Beskar::Services::RateLimiter.check_authentication_attempt(request, result, self)
123
+
124
+ # Check risk-based locking after successful authentication
125
+ # This prevents compromised accounts from being used even after successful login
126
+ if result == :success
127
+ check_and_lock_if_high_risk(security_event, request)
128
+ end
129
+ end
130
+
131
+ security_event
132
+ end
133
+
134
+ def analyze_suspicious_patterns_async
135
+ # Skip analysis if disabled in configuration
136
+ unless Beskar.configuration.auto_analyze_patterns?
137
+ Beskar::Logger.debug("Auto pattern analysis disabled in configuration")
138
+ return
139
+ end
140
+
141
+ # Queue background job for detailed analysis
142
+ Beskar::SecurityAnalysisJob.perform_later(self.id, "login_success") if defined?(Beskar::SecurityAnalysisJob)
143
+ rescue => e
144
+ Beskar::Logger.warn("Failed to queue security analysis: #{e.message}")
145
+ end
146
+
147
+ def recent_failed_attempts(within: 1.hour)
148
+ security_events.where(
149
+ event_type: "login_failure",
150
+ created_at: within.ago..Time.current
151
+ )
152
+ end
153
+
154
+ def recent_successful_logins(within: 24.hours)
155
+ security_events.where(
156
+ event_type: "login_success",
157
+ created_at: within.ago..Time.current
158
+ )
159
+ end
160
+
161
+ def suspicious_login_pattern?
162
+ # Check for rapid successive attempts
163
+ recent_attempts = recent_failed_attempts(within: 5.minutes)
164
+ return true if recent_attempts.count >= 3
165
+
166
+ # Check for geographic anomalies
167
+ recent_logins = recent_successful_logins(within: 4.hours).includes(:security_events)
168
+ return true if geographic_anomaly_detected?(recent_logins)
169
+
170
+ false
171
+ end
172
+
173
+ private
174
+
175
+ def extract_user_email
176
+ # Try different email attribute names
177
+ respond_to?(:email) ? email : (respond_to?(:email_address) ? email_address : nil)
178
+ end
179
+
180
+ def extract_security_context(request)
181
+ {
182
+ timestamp: Time.current.iso8601,
183
+ session_id: request.session.id,
184
+ request_path: request.path,
185
+ referer: request.referer,
186
+ accept_language: request.headers["Accept-Language"],
187
+ x_forwarded_for: request.headers["X-Forwarded-For"],
188
+ x_real_ip: request.headers["X-Real-IP"],
189
+ device_info: Beskar::Services::DeviceDetector.detect(request.user_agent),
190
+ geolocation: Beskar::Services::GeolocationService.locate(request.ip)
191
+ }
192
+ end
193
+
194
+ def calculate_risk_score(request, result)
195
+ base_score = result == :success ? 1 : 25
196
+ score = base_score
197
+
198
+ # Use dedicated services for risk assessment
199
+ device_detector = Beskar::Services::DeviceDetector.new
200
+ score += device_detector.calculate_user_agent_risk(request.user_agent)
201
+
202
+ # Mobile device login during late hours
203
+ if device_detector.mobile?(request.user_agent) && Time.current.hour.between?(22, 6)
204
+ score += 5
205
+ Beskar::Logger.info("Mobile device login during late hours, adding 5 risk")
206
+ end
207
+
208
+ # Account-specific risk factors
209
+ if recent_failed_attempts(within: 10.minutes).count >= 2
210
+ score += 20
211
+ Beskar::Logger.info("Recent failed attempts: #{recent_failed_attempts(within: 10.minutes).count}, adding 20 risk")
212
+ end
213
+
214
+ # ADAPTIVE LEARNING: Check if this is an established pattern
215
+ if result == :success && established_pattern?(request)
216
+ Beskar::Logger.info("Established pattern detected, reducing risk score")
217
+ score = [score * 0.3, 25].min.to_i # Reduce to 30% of original, cap at 25
218
+ end
219
+
220
+ # Geographic risk assessment
221
+ geolocation_service = Beskar::Services::GeolocationService.new
222
+ recent_locations = recent_successful_logins(within: 4.hours).map do |event|
223
+ event.metadata&.dig("geolocation")
224
+ end.compact
225
+
226
+ # Don't apply geographic risk if this location is established
227
+ unless location_established?(request.ip)
228
+ score += geolocation_service.calculate_location_risk(
229
+ request.ip,
230
+ recent_locations,
231
+ recent_successful_logins(within: 4.hours).last&.created_at&.to_i
232
+ )
233
+ end
234
+
235
+ [score, 100].min # Cap at 100
236
+ end
237
+
238
+ def geographic_anomaly_detected?(recent_logins)
239
+ # Placeholder for geographic anomaly detection
240
+ # Would implement haversine formula and impossible travel detection
241
+ false
242
+ end
243
+
244
+ # Check if the account should be locked based on risk score
245
+ def check_and_lock_if_high_risk(security_event, request)
246
+ return unless Beskar.configuration.risk_based_locking_enabled?
247
+ return unless security_event.risk_score
248
+
249
+ locker = Beskar::Services::AccountLocker.new(
250
+ self,
251
+ risk_score: security_event.risk_score,
252
+ reason: determine_lock_reason(security_event),
253
+ metadata: {
254
+ ip_address: request.ip,
255
+ user_agent: request.user_agent,
256
+ security_event_id: security_event.id,
257
+ geolocation: security_event.geolocation,
258
+ device_info: security_event.device_info
259
+ }
260
+ )
261
+
262
+ if locker.lock_if_necessary!
263
+ Beskar::Logger.warn("Account locked due to high risk score: #{security_event.risk_score}")
264
+
265
+ # Trigger auth-system-specific lock handling
266
+ handle_high_risk_lock(security_event, request)
267
+ end
268
+ end
269
+
270
+ # Determine the specific reason for locking
271
+ def determine_lock_reason(security_event)
272
+ metadata = security_event.metadata || {}
273
+
274
+ # Check for impossible travel
275
+ if metadata.dig("geolocation", "impossible_travel")
276
+ return :impossible_travel
277
+ end
278
+
279
+ # Check for suspicious device
280
+ device_info = metadata["device_info"] || {}
281
+ if device_info["bot_signature"] || device_info["suspicious"]
282
+ return :suspicious_device
283
+ end
284
+
285
+ # Check for geographic anomaly
286
+ geolocation = metadata["geolocation"] || {}
287
+ if geolocation["country_change"] || geolocation["high_risk_country"]
288
+ return :geographic_anomaly
289
+ end
290
+
291
+ # Default to high risk authentication
292
+ :high_risk_authentication
293
+ end
294
+
295
+ # Override this in auth-system-specific modules
296
+ def handle_high_risk_lock(security_event, request)
297
+ Beskar::Logger.debug("High risk lock handled - override in specific module")
298
+ end
299
+
300
+ # ADAPTIVE LEARNING: Check if this login pattern is established
301
+ def established_pattern?(request)
302
+ return false unless security_events.any?
303
+
304
+ current_ip = request.ip
305
+
306
+ # Look for successful logins from this IP in the past 30 days
307
+ historical_logins = security_events
308
+ .where(event_type: "login_success")
309
+ .where(ip_address: current_ip)
310
+ .where("created_at >= ?", 30.days.ago)
311
+ .where("created_at < ?", 5.minutes.ago)
312
+
313
+ # Need at least 2 successful logins from this context
314
+ return false if historical_logins.count < 2
315
+
316
+ # Check if there was an unlock event followed by successful logins
317
+ recent_unlock_or_lock = security_events
318
+ .where(event_type: ["account_locked", "account_unlocked", "lock_attempted"])
319
+ .where("created_at >= ?", 7.days.ago)
320
+ .order(created_at: :desc)
321
+ .first
322
+
323
+ if recent_unlock_or_lock
324
+ # Check for successful logins after unlock/lock from same IP
325
+ logins_after_unlock = security_events
326
+ .where(event_type: "login_success")
327
+ .where(ip_address: current_ip)
328
+ .where("created_at > ?", recent_unlock_or_lock.created_at)
329
+ .count
330
+
331
+ # If user unlocked and successfully logged in, that's legitimate
332
+ return true if logins_after_unlock >= 1
333
+ end
334
+
335
+ # Pattern is established if there are 3+ successful logins
336
+ historical_logins.count >= 3
337
+ end
338
+
339
+ # Check if a location (IP) is established/trusted
340
+ def location_established?(ip_address)
341
+ return false unless security_events.any?
342
+
343
+ successful_logins_from_ip = security_events
344
+ .where(event_type: "login_success")
345
+ .where(ip_address: ip_address)
346
+ .where("created_at >= ?", 30.days.ago)
347
+ .where("created_at < ?", 5.minutes.ago)
348
+ .count
349
+
350
+ # Location is established if there are 2+ successful logins
351
+ successful_logins_from_ip >= 2
352
+ end
353
+ end
354
+ end
355
+ end
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beskar
4
+ module Services
5
+ # Service for locking user accounts based on risk scores
6
+ #
7
+ # This service provides a modular approach to account locking that can work
8
+ # with Devise's lockable module or custom locking implementations. It keeps
9
+ # Devise-specific code isolated for maintainability.
10
+ #
11
+ # @example Basic usage with Devise lockable
12
+ # locker = Beskar::Services::AccountLocker.new(user, risk_score: 85, reason: :high_risk_login)
13
+ # locker.lock_if_necessary!
14
+ #
15
+ # @example Check if account should be locked
16
+ # if locker.should_lock?
17
+ # locker.lock!
18
+ # end
19
+ #
20
+ class AccountLocker
21
+ attr_reader :user, :risk_score, :reason, :metadata
22
+
23
+ # Initialize the account locker
24
+ #
25
+ # @param user [ActiveRecord::Base] The user to potentially lock
26
+ # @param risk_score [Integer] The calculated risk score (0-100)
27
+ # @param reason [Symbol] The reason for potential lock (:high_risk_login, :impossible_travel, etc.)
28
+ # @param metadata [Hash] Additional context for the lock decision
29
+ def initialize(user, risk_score:, reason: :high_risk_authentication, metadata: {})
30
+ @user = user
31
+ @risk_score = risk_score
32
+ @reason = reason
33
+ @metadata = metadata
34
+ end
35
+
36
+ # Check if account should be locked based on configuration
37
+ #
38
+ # @return [Boolean] true if account should be locked
39
+ def should_lock?
40
+ return false unless Beskar.configuration.risk_based_locking_enabled?
41
+ return false unless user
42
+ return false if user_already_locked?
43
+
44
+ risk_score >= Beskar.configuration.risk_threshold
45
+ end
46
+
47
+ # Lock the account if necessary (based on should_lock? check)
48
+ #
49
+ # @return [Boolean] true if account was locked, false otherwise
50
+ def lock_if_necessary!
51
+ return false unless should_lock?
52
+ lock!
53
+ end
54
+
55
+ # Lock the account using the configured strategy
56
+ #
57
+ # @return [Boolean] true if lock was successful, false otherwise
58
+ def lock!
59
+ return false unless user
60
+
61
+ strategy = Beskar.configuration.lock_strategy
62
+
63
+ result = case strategy
64
+ when :devise_lockable
65
+ lock_with_devise_lockable
66
+ when :custom
67
+ lock_with_custom_strategy
68
+ else
69
+ Beskar::Logger.warn("Unknown lock strategy: #{strategy}", component: :AccountLocker)
70
+ false
71
+ end
72
+
73
+ # Always log lock events when risk-based locking is enabled
74
+ # This creates an audit trail even if actual locking fails
75
+ if Beskar.configuration.log_lock_events?
76
+ log_lock_event(result)
77
+ end
78
+
79
+ if result
80
+ notify_user if Beskar.configuration.notify_user_on_lock?
81
+ end
82
+
83
+ result
84
+ end
85
+
86
+ # Unlock the account using the configured strategy
87
+ #
88
+ # @return [Boolean] true if unlock was successful
89
+ def unlock!
90
+ return false unless user
91
+
92
+ strategy = Beskar.configuration.lock_strategy
93
+
94
+ result = case strategy
95
+ when :devise_lockable
96
+ unlock_with_devise_lockable
97
+ when :custom
98
+ unlock_with_custom_strategy
99
+ else
100
+ false
101
+ end
102
+
103
+ # Log unlock event for adaptive learning
104
+ if result && Beskar.configuration.log_lock_events?
105
+ log_unlock_event
106
+ end
107
+
108
+ result
109
+ end
110
+
111
+ # Check if user is currently locked
112
+ #
113
+ # @return [Boolean] true if user is locked
114
+ def locked?
115
+ user_already_locked?
116
+ end
117
+
118
+ private
119
+
120
+ # Check if user is already locked
121
+ def user_already_locked?
122
+ return false unless user
123
+
124
+ if user.respond_to?(:access_locked?)
125
+ user.access_locked?
126
+ elsif user.respond_to?(:locked_at)
127
+ user.locked_at.present?
128
+ else
129
+ false
130
+ end
131
+ end
132
+
133
+ # Lock account using Devise's lockable module
134
+ def lock_with_devise_lockable
135
+ unless devise_lockable_available?
136
+ Beskar::Logger.warn("Devise lockable not available for #{user.class.name}", component: :AccountLocker)
137
+ return false
138
+ end
139
+
140
+ begin
141
+ # Use Devise's lock_access! method
142
+ user.lock_access!(send_instructions: false)
143
+
144
+ # Set automatic unlock time if configured and supported
145
+ if Beskar.configuration.auto_unlock_time && user.respond_to?(:locked_at=)
146
+ user.update_column(:locked_at, Time.current)
147
+ end
148
+
149
+ Beskar::Logger.info("Locked account #{user.id} (#{user.class.name}) - Risk: #{risk_score}, Reason: #{reason}", component: :AccountLocker)
150
+ true
151
+ rescue => e
152
+ Beskar::Logger.error("Failed to lock account: #{e.message}", component: :AccountLocker)
153
+ false
154
+ end
155
+ end
156
+
157
+ # Unlock account using Devise's lockable module
158
+ def unlock_with_devise_lockable
159
+ unless devise_lockable_available?
160
+ Beskar::Logger.warn("Devise lockable not available for #{user.class.name}", component: :AccountLocker)
161
+ return false
162
+ end
163
+
164
+ begin
165
+ user.unlock_access!
166
+ Beskar::Logger.info("Unlocked account #{user.id} (#{user.class.name})", component: :AccountLocker)
167
+ true
168
+ rescue => e
169
+ Beskar::Logger.error("Failed to unlock account: #{e.message}", component: :AccountLocker)
170
+ false
171
+ end
172
+ end
173
+
174
+ # Lock account using custom strategy (to be implemented by application)
175
+ def lock_with_custom_strategy
176
+ # Applications can implement this by:
177
+ # 1. Adding a locked_by_beskar column to users table
178
+ # 2. Checking this in authentication callbacks
179
+ # 3. Implementing unlock logic
180
+
181
+ Beskar::Logger.warn("Custom lock strategy not implemented", component: :AccountLocker)
182
+ false
183
+ end
184
+
185
+ # Unlock using custom strategy
186
+ def unlock_with_custom_strategy
187
+ Beskar::Logger.warn("Custom unlock strategy not implemented", component: :AccountLocker)
188
+ false
189
+ end
190
+
191
+ # Check if Devise lockable is available for this user
192
+ def devise_lockable_available?
193
+ defined?(Devise) &&
194
+ user.class.respond_to?(:devise_modules) &&
195
+ user.class.devise_modules.include?(:lockable) &&
196
+ user.respond_to?(:lock_access!)
197
+ end
198
+
199
+ # Log the lock event to security events
200
+ # Always logs, even if actual lock fails, to maintain audit trail
201
+ def log_lock_event(lock_succeeded = true)
202
+ return unless user.respond_to?(:security_events)
203
+
204
+ begin
205
+ event_type = lock_succeeded ? 'account_locked' : 'lock_attempted'
206
+
207
+ user.security_events.create!(
208
+ event_type: event_type,
209
+ ip_address: metadata[:ip_address] || 'system',
210
+ user_agent: metadata[:user_agent] || 'beskar_system',
211
+ risk_score: risk_score,
212
+ metadata: {
213
+ reason: reason,
214
+ risk_threshold: Beskar.configuration.risk_threshold,
215
+ lock_strategy: Beskar.configuration.lock_strategy,
216
+ auto_unlock_time: Beskar.configuration.auto_unlock_time,
217
+ locked_at: Time.current.iso8601,
218
+ lock_succeeded: lock_succeeded,
219
+ additional_context: metadata
220
+ }
221
+ )
222
+ rescue => e
223
+ Beskar::Logger.warn("Failed to log lock event: #{e.message}", component: :AccountLocker)
224
+ end
225
+ end
226
+
227
+ # Log unlock event for adaptive learning
228
+ # This helps establish patterns - if user unlocks and logs in successfully,
229
+ # that context becomes "established" and trusted
230
+ def log_unlock_event
231
+ return unless user.respond_to?(:security_events)
232
+
233
+ begin
234
+ user.security_events.create!(
235
+ event_type: 'account_unlocked',
236
+ ip_address: metadata[:ip_address] || 'system',
237
+ user_agent: metadata[:user_agent] || 'beskar_system',
238
+ risk_score: 0, # Unlock has no risk
239
+ metadata: {
240
+ unlocked_at: Time.current.iso8601,
241
+ unlock_method: 'manual',
242
+ additional_context: metadata
243
+ }
244
+ )
245
+ rescue => e
246
+ Beskar::Logger.warn("Failed to log unlock event: #{e.message}", component: :AccountLocker)
247
+ end
248
+ end
249
+
250
+ # Notify user about account lock
251
+ def notify_user
252
+ # This would integrate with ActionMailer or notification system
253
+ # For now, just log it
254
+ Beskar::Logger.info("User #{user.id} should be notified of account lock", component: :AccountLocker)
255
+
256
+ # Future implementation:
257
+ # if defined?(Beskar::AccountLockMailer)
258
+ # Beskar::AccountLockMailer.account_locked(user, risk_score, reason).deliver_later
259
+ # end
260
+ end
261
+ end
262
+ end
263
+ end