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,305 @@
1
+ module Beskar
2
+ module Middleware
3
+ class RequestAnalyzer
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ request = ActionDispatch::Request.new(env)
10
+ ip_address = request.ip
11
+
12
+ Beskar::Logger.debug("[RequestAnalyzer] Processing request from IP: #{ip_address}, Path: #{request.path}", component: :Middleware)
13
+
14
+ # 1. Check if IP is whitelisted (whitelisted IPs skip blocking but still get logged)
15
+ is_whitelisted = Beskar::Services::IpWhitelist.whitelisted?(ip_address)
16
+
17
+ # 2. Check if IP is banned (early exit for blocked IPs, unless whitelisted or in monitor-only mode)
18
+ if !is_whitelisted && Beskar::BannedIp.banned?(ip_address)
19
+ if Beskar.configuration.monitor_only?
20
+ Beskar::Logger.warn("🔍 MONITOR-ONLY: Would block request from banned IP: #{ip_address}, but monitor_only=true. Request proceeding normally.", component: :Middleware)
21
+ else
22
+ Beskar::Logger.warn("Blocked request from banned IP: #{ip_address}", component: :Middleware)
23
+ return blocked_response("Your IP address has been blocked due to suspicious activity.")
24
+ end
25
+ end
26
+
27
+ # 3. Check rate limiting (unless whitelisted)
28
+ if !is_whitelisted && rate_limited?(request)
29
+ # Auto-block after excessive rate limiting violations (even in monitor-only mode - we create the ban record)
30
+ if should_auto_block_rate_limit?(ip_address)
31
+ Beskar::BannedIp.ban!(
32
+ ip_address,
33
+ reason: "rate_limit_abuse",
34
+ duration: 1.hour,
35
+ details: "Excessive rate limit violations"
36
+ )
37
+ end
38
+
39
+ if Beskar.configuration.monitor_only?
40
+ Beskar::Logger.warn("🔍 MONITOR-ONLY: Would block rate limit exceeded for IP: #{ip_address}, but monitor_only=true. Request proceeding normally.", component: :Middleware)
41
+ else
42
+ Beskar::Logger.warn("Rate limit exceeded for IP: #{ip_address}", component: :Middleware)
43
+ return rate_limit_response
44
+ end
45
+ end
46
+
47
+ # 4. Check WAF patterns (vulnerability scans)
48
+ if Beskar.configuration.waf_enabled?
49
+ Beskar::Logger.debug("[RequestAnalyzer] WAF enabled, analyzing request", component: :Middleware)
50
+ waf_analysis = Beskar::Services::Waf.analyze_request(request)
51
+
52
+ if waf_analysis
53
+ Beskar::Logger.debug("[RequestAnalyzer] WAF detected threat: #{waf_analysis[:patterns].map { |p| p[:description] }.join(", ")}", component: :Middleware)
54
+ # Log the violation (and create security event if configured)
55
+ # Pass whitelist status to prevent auto-blocking whitelisted IPs
56
+ current_score = Beskar::Services::Waf.record_violation(ip_address, waf_analysis, whitelisted: is_whitelisted)
57
+ Beskar::Logger.debug("[RequestAnalyzer] Current score after recording: #{current_score.round(2)}", component: :Middleware)
58
+
59
+ # Log even for whitelisted IPs (but don't block)
60
+ if is_whitelisted
61
+ Beskar::Logger.info("WAF violation from whitelisted IP #{ip_address} " \
62
+ "(not blocking): #{waf_analysis[:patterns].map { |p| p[:description] }.join(", ")}", component: :Middleware)
63
+ else
64
+ # Check if we should block
65
+ should_block = Beskar::Services::Waf.should_block?(ip_address)
66
+ Beskar::Logger.debug("[RequestAnalyzer] Should block IP #{ip_address}?: #{should_block}", component: :Middleware)
67
+
68
+ if Beskar.configuration.monitor_only?
69
+ # Monitor-only mode: Just log, don't block (but ban record was created by WAF.record_violation)
70
+ if should_block
71
+ Beskar::Logger.warn("🔍 MONITOR-ONLY: Would block IP #{ip_address} " \
72
+ "with score #{current_score.round(2)}, but monitor_only=true. " \
73
+ "Request proceeding normally.", component: :Middleware)
74
+ end
75
+ elsif should_block && !Beskar.configuration.monitor_only?
76
+ # Actually block the request (not in monitor-only mode)
77
+ Beskar::Logger.warn("🔒 Blocking IP #{ip_address} " \
78
+ "with WAF score #{current_score.round(2)}", component: :Middleware)
79
+ # Block already handled by WAF.record_violation auto-block logic
80
+ # But we return 403 immediately
81
+ return blocked_response("Access denied due to suspicious activity.")
82
+ end
83
+ end
84
+ else
85
+ Beskar::Logger.debug("[RequestAnalyzer] No WAF threat detected for path: #{request.path}", component: :Middleware)
86
+ end
87
+ else
88
+ Beskar::Logger.debug("[RequestAnalyzer] WAF is disabled", component: :Middleware)
89
+ end
90
+
91
+ # 5. Process the request normally (will raise 404 if route not found)
92
+ Beskar::Logger.debug("[RequestAnalyzer] Passing request to application", component: :Middleware)
93
+ @app.call(env)
94
+ rescue ActionController::UnknownFormat => e
95
+ # Analyze unknown format as potential scanner
96
+ if Beskar.configuration.waf_enabled?
97
+ handle_rails_exception(request, e, ip_address, is_whitelisted)
98
+ end
99
+ # Re-raise to allow normal error handling
100
+ raise
101
+ rescue ActionDispatch::RemoteIp::IpSpoofAttackError => e
102
+ # Handle IP spoofing attack
103
+ if Beskar.configuration.waf_enabled?
104
+ handle_rails_exception(request, e, ip_address, is_whitelisted)
105
+ end
106
+ # Re-raise to allow normal error handling
107
+ raise
108
+ rescue ActiveRecord::RecordNotFound => e
109
+ # Analyze record not found as potential enumeration scan
110
+ if Beskar.configuration.waf_enabled?
111
+ handle_rails_exception(request, e, ip_address, is_whitelisted)
112
+ end
113
+ # Re-raise to allow normal error handling
114
+ raise
115
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType => e
116
+ # Analyze invalid MIME type as potential scanner
117
+ if Beskar.configuration.waf_enabled?
118
+ handle_rails_exception(request, e, ip_address, is_whitelisted)
119
+ end
120
+ # Re-raise to allow normal error handling
121
+ raise
122
+ rescue ActionController::RoutingError => e
123
+ # If WAF is enabled, log 404s as potential scanning attempts
124
+ if Beskar.configuration.waf_enabled?
125
+ log_404_for_waf(request, e)
126
+ end
127
+ # Re-raise to allow normal 404 handling
128
+ raise
129
+ end
130
+
131
+ private
132
+
133
+ def rate_limited?(request)
134
+ # Check both IP rate limit and authentication abuse
135
+ ip_check = Beskar::Services::RateLimiter.check_ip_rate_limit(request.ip)
136
+ auth_abused = authentication_brute_force?(request.ip)
137
+
138
+ !ip_check[:allowed] || auth_abused
139
+ end
140
+
141
+ def should_auto_block_rate_limit?(ip_address)
142
+ # Check how many times this IP has been rate limited in the past hour
143
+ cache_key = "beskar:rate_limit_violations:#{ip_address}"
144
+ violations = Rails.cache.read(cache_key) || 0
145
+ violations += 1
146
+ Rails.cache.write(cache_key, violations, expires_in: 1.hour)
147
+
148
+ # Block after 5 rate limit violations in an hour
149
+ violations >= 5
150
+ end
151
+
152
+ def authentication_brute_force?(ip_address)
153
+ # Check authentication failure count from RateLimiter
154
+ cache_key = "beskar:ip_auth_failures:#{ip_address}"
155
+
156
+ # Get current failure count and timestamp
157
+ failure_data = Rails.cache.read(cache_key)
158
+ return false unless failure_data.is_a?(Hash)
159
+
160
+ # Count recent failures (within the configured period)
161
+ config = Beskar.configuration.rate_limiting[:ip_attempts] || {}
162
+ period = config[:period] || 1.hour
163
+ limit = config[:limit] || 10
164
+
165
+ now = Time.current.to_i
166
+ recent_failures = failure_data.select { |timestamp, _| now - timestamp.to_i < period.to_i }
167
+
168
+ # If too many auth failures, it's brute force
169
+ if recent_failures.length >= limit
170
+ # Auto-ban for authentication abuse (create ban record even in monitor-only mode)
171
+ Beskar::BannedIp.ban!(
172
+ ip_address,
173
+ reason: "authentication_abuse",
174
+ duration: 1.hour,
175
+ details: "#{recent_failures.length} failed authentication attempts in #{period / 60} minutes",
176
+ metadata: {failure_count: recent_failures.length, detection_time: Time.current}
177
+ )
178
+
179
+ if Beskar.configuration.monitor_only?
180
+ Beskar::Logger.warn("🔍 MONITOR-ONLY: Would auto-block IP #{ip_address} " \
181
+ "for authentication brute force (#{recent_failures.length} failures), but monitor_only=true", component: :Middleware)
182
+ else
183
+ Beskar::Logger.warn("🔒 Auto-blocked IP #{ip_address} " \
184
+ "for authentication brute force (#{recent_failures.length} failures)", component: :Middleware)
185
+ end
186
+
187
+ return true
188
+ end
189
+
190
+ false
191
+ end
192
+
193
+ def handle_rails_exception(request, exception, ip_address, is_whitelisted)
194
+ # Analyze the exception using WAF
195
+ waf_analysis = Beskar::Services::Waf.analyze_exception(exception, request)
196
+
197
+ if waf_analysis
198
+ Beskar::Logger.debug("[RequestAnalyzer] WAF detected threat from exception: #{exception.class.name}", component: :Middleware)
199
+
200
+ # Record the violation (similar to regular WAF violations)
201
+ current_score = Beskar::Services::Waf.record_violation(ip_address, waf_analysis, whitelisted: is_whitelisted)
202
+
203
+ # Log for whitelisted IPs
204
+ if is_whitelisted
205
+ Beskar::Logger.info("Exception-based WAF violation from whitelisted IP #{ip_address} " \
206
+ "(not blocking): #{exception.class.name} - #{waf_analysis[:patterns].first[:description]}", component: :Middleware)
207
+ else
208
+ # Check if we should block
209
+ should_block = Beskar::Services::Waf.should_block?(ip_address)
210
+
211
+ if Beskar.configuration.monitor_only?
212
+ if should_block
213
+ Beskar::Logger.warn("🔍 MONITOR-ONLY: Would block IP #{ip_address} " \
214
+ "with score #{current_score.round(2)} (exception: #{exception.class.name}), " \
215
+ "but monitor_only=true. Request proceeding normally.", component: :Middleware)
216
+ end
217
+ elsif should_block && !Beskar.configuration.monitor_only?
218
+ Beskar::Logger.warn("🔒 Blocking IP #{ip_address} " \
219
+ "with score #{current_score.round(2)} (exception: #{exception.class.name})", component: :Middleware)
220
+ # Note: We don't return blocked response here as exception is already raised
221
+ # The ban record is created by WAF.record_violation
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ def log_404_for_waf(request, error)
228
+ # 404s on suspicious paths might indicate scanning
229
+ path = request.fullpath || request.path
230
+
231
+ # Only log if it matches WAF patterns (already analyzed in analyze_request)
232
+ waf_analysis = Beskar::Services::Waf.analyze_request(request)
233
+
234
+ if waf_analysis
235
+ Beskar::Logger.info("404 on suspicious path from #{request.ip}: #{path} " \
236
+ "(WAF patterns: #{waf_analysis[:patterns].map { |p| p[:description] }.join(", ")})", component: :Middleware)
237
+ end
238
+ end
239
+
240
+ def blocked_response(message = "Forbidden")
241
+ [
242
+ 403,
243
+ {
244
+ "Content-Type" => "text/html",
245
+ "X-Beskar-Blocked" => "true"
246
+ },
247
+ [render_blocked_page(message)]
248
+ ]
249
+ end
250
+
251
+ def rate_limit_response
252
+ [
253
+ 429,
254
+ {
255
+ "Content-Type" => "text/html",
256
+ "Retry-After" => "3600",
257
+ "X-Beskar-Rate-Limited" => "true"
258
+ },
259
+ [render_rate_limit_page]
260
+ ]
261
+ end
262
+
263
+ def render_blocked_page(message)
264
+ <<~HTML
265
+ <!DOCTYPE html>
266
+ <html>
267
+ <head>
268
+ <title>Access Denied</title>
269
+ <style>
270
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
271
+ h1 { color: #d32f2f; }
272
+ p { color: #666; }
273
+ </style>
274
+ </head>
275
+ <body>
276
+ <h1>Access Denied</h1>
277
+ <p>#{message}</p>
278
+ <p>If you believe this is an error, please contact the site administrator.</p>
279
+ </body>
280
+ </html>
281
+ HTML
282
+ end
283
+
284
+ def render_rate_limit_page
285
+ <<~HTML
286
+ <!DOCTYPE html>
287
+ <html>
288
+ <head>
289
+ <title>Too Many Requests</title>
290
+ <style>
291
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
292
+ h1 { color: #ff9800; }
293
+ p { color: #666; }
294
+ </style>
295
+ </head>
296
+ <body>
297
+ <h1>Too Many Requests</h1>
298
+ <p>You have exceeded the rate limit. Please try again later.</p>
299
+ </body>
300
+ </html>
301
+ HTML
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,4 @@
1
+ module Beskar
2
+ module Middleware
3
+ end
4
+ end
@@ -0,0 +1,25 @@
1
+ module Beskar
2
+ module Models
3
+ # Main SecurityTrackable module - delegates to Devise-specific implementation
4
+ # for backward compatibility with existing Devise integrations
5
+ #
6
+ # This is now a thin wrapper around the modular implementation:
7
+ # - SecurityTrackableGeneric: Shared functionality for all auth systems
8
+ # - SecurityTrackableDevise: Devise/Warden specific (included by default here)
9
+ # - SecurityTrackableAuthenticable: Rails 8 has_secure_password specific
10
+ #
11
+ # Usage:
12
+ # For Devise models:
13
+ # include Beskar::Models::SecurityTrackable # (this module)
14
+ # For Rails 8 auth models:
15
+ # include Beskar::Models::SecurityTrackableAuthenticable
16
+ module SecurityTrackable
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ # Include Devise-specific tracking by default for backward compatibility
21
+ include Beskar::Models::SecurityTrackableDevise
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,167 @@
1
+ module Beskar
2
+ module Models
3
+ # Rails 8 authentication-specific security tracking functionality
4
+ # This module provides security tracking for models using has_secure_password
5
+ # and session-based authentication (Rails 8 built-in authentication)
6
+ module SecurityTrackableAuthenticable
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Include the generic functionality first
11
+ include Beskar::Models::SecurityTrackableGeneric
12
+
13
+ # No automatic callbacks like Devise - tracking is done explicitly
14
+ # in controllers via the Beskar::Controllers::SecurityTracking concern
15
+ end
16
+
17
+ # Make handle_high_risk_lock public (it's private in Generic)
18
+ public
19
+
20
+ # Rails 8 auth-specific: Handle high risk lock by destroying sessions
21
+ # Public method called when high-risk event is detected
22
+ def handle_high_risk_lock(security_event, request)
23
+ reason = determine_lock_reason(security_event)
24
+
25
+ Beskar::Logger.warn("Rails auth high-risk lock detected: #{reason}")
26
+
27
+ # Destroy all sessions to immediately lock out attacker
28
+ destroy_all_sessions(except: request.session.id)
29
+
30
+ # Check if this warrants emergency password reset
31
+ if should_reset_password?(security_event, reason)
32
+ perform_emergency_password_reset(security_event, reason)
33
+ end
34
+ end
35
+
36
+ # Destroy all user sessions (for impossible travel / high-risk scenarios)
37
+ def destroy_all_sessions(except: nil)
38
+ if respond_to?(:sessions) && sessions.respond_to?(:destroy_all)
39
+ if except
40
+ # Keep current session but destroy all others
41
+ sessions.where.not(id: except).destroy_all
42
+ Beskar::Logger.info("Destroyed #{sessions.count} sessions except current")
43
+ else
44
+ # Destroy ALL sessions including current
45
+ count = sessions.count
46
+ sessions.destroy_all
47
+ Beskar::Logger.info("Destroyed all #{count} sessions")
48
+ end
49
+ else
50
+ Beskar::Logger.warn("Model does not have sessions association, cannot destroy sessions")
51
+ end
52
+ rescue => e
53
+ Beskar::Logger.error("Failed to destroy sessions: #{e.message}")
54
+ end
55
+
56
+ # Determine if emergency password reset is warranted
57
+ def should_reset_password?(security_event, reason)
58
+ config = Beskar.configuration.emergency_password_reset
59
+ return false unless config[:enabled]
60
+
61
+ case reason
62
+ when :impossible_travel
63
+ # Count impossible travel events in recent history
64
+ recent_impossible_travel = security_events
65
+ .where(event_type: ["account_locked", "login_success"])
66
+ .where("created_at >= ?", 24.hours.ago)
67
+ .where("metadata->>'geolocation' LIKE ?", '%impossible_travel%')
68
+ .count
69
+
70
+ recent_impossible_travel >= (config[:impossible_travel_threshold] || 3)
71
+
72
+ when :suspicious_device
73
+ # Multiple suspicious device logins
74
+ recent_suspicious = security_events
75
+ .where(event_type: "account_locked")
76
+ .where("created_at >= ?", 24.hours.ago)
77
+ .where("metadata->>'device_info' LIKE ?", '%suspicious%')
78
+ .count
79
+
80
+ recent_suspicious >= (config[:suspicious_device_threshold] || 5)
81
+
82
+ else
83
+ # For other reasons, check total lock count
84
+ recent_locks = security_events
85
+ .where(event_type: "account_locked")
86
+ .where("created_at >= ?", 24.hours.ago)
87
+ .count
88
+
89
+ recent_locks >= (config[:total_locks_threshold] || 5)
90
+ end
91
+ end
92
+
93
+ # Perform emergency password reset
94
+ def perform_emergency_password_reset(security_event, reason)
95
+ config = Beskar.configuration.emergency_password_reset
96
+
97
+ # Generate a cryptographically secure random password
98
+ new_password = SecureRandom.base58(32)
99
+
100
+ begin
101
+ # Update password
102
+ update!(password: new_password, password_confirmation: new_password)
103
+
104
+ # Log the reset event
105
+ security_events.create!(
106
+ event_type: "emergency_password_reset",
107
+ ip_address: security_event.ip_address,
108
+ user_agent: security_event.user_agent,
109
+ metadata: {
110
+ reason: reason.to_s,
111
+ triggering_event_id: security_event.id,
112
+ timestamp: Time.current.iso8601,
113
+ reset_method: "automatic"
114
+ },
115
+ risk_score: 100
116
+ )
117
+
118
+ # Send notification to user
119
+ if config[:send_notification]
120
+ send_emergency_reset_notification(reason)
121
+ end
122
+
123
+ # Notify security team
124
+ if config[:notify_security_team]
125
+ notify_security_team_of_reset(reason, security_event)
126
+ end
127
+
128
+ Beskar::Logger.warn("Emergency password reset performed for user #{id}, reason: #{reason}")
129
+
130
+ rescue => e
131
+ Beskar::Logger.error("Failed to perform emergency password reset: #{e.message}")
132
+
133
+ # Create failed reset event
134
+ security_events.create!(
135
+ event_type: "emergency_password_reset_failed",
136
+ ip_address: security_event.ip_address,
137
+ user_agent: security_event.user_agent,
138
+ metadata: {
139
+ reason: reason.to_s,
140
+ error: e.message,
141
+ timestamp: Time.current.iso8601
142
+ },
143
+ risk_score: 100
144
+ )
145
+ end
146
+ end
147
+
148
+ # Send notification to user about emergency password reset
149
+ def send_emergency_reset_notification(reason)
150
+ # This should be implemented by the application
151
+ # Example: UserMailer.emergency_password_reset(self, reason).deliver_later
152
+ Beskar::Logger.info("Would send emergency reset notification to user #{id}")
153
+ rescue => e
154
+ Beskar::Logger.error("Failed to send emergency reset notification: #{e.message}")
155
+ end
156
+
157
+ # Notify security team about emergency password reset
158
+ def notify_security_team_of_reset(reason, security_event)
159
+ # This should be implemented by the application
160
+ # Example: SecurityMailer.emergency_reset_alert(self, reason, security_event).deliver_later
161
+ Beskar::Logger.info("Would notify security team about reset for user #{id}")
162
+ rescue => e
163
+ Beskar::Logger.error("Failed to notify security team: #{e.message}")
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,82 @@
1
+ module Beskar
2
+ module Models
3
+ # Devise-specific security tracking functionality
4
+ # This module hooks into Devise/Warden callbacks and provides
5
+ # Devise-specific authentication tracking and account locking
6
+ module SecurityTrackableDevise
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Include the generic functionality first
11
+ include Beskar::Models::SecurityTrackableGeneric
12
+
13
+ # Hook into Devise callbacks if Devise is present and available
14
+ if defined?(Devise) && respond_to?(:after_database_authentication)
15
+ # Track successful authentications
16
+ after_database_authentication :track_successful_login
17
+ end
18
+ end
19
+
20
+ # Track successful login via Devise callback
21
+ def track_successful_login
22
+ # Skip tracking if disabled in configuration
23
+ unless Beskar.configuration.track_successful_logins?
24
+ Beskar::Logger.debug("Successful login tracking disabled in configuration")
25
+ return
26
+ end
27
+
28
+ if current_request = request_from_context
29
+ track_authentication_event(current_request, :success)
30
+ end
31
+ rescue => e
32
+ Beskar::Logger.warn("Failed to track successful login: #{e.message}")
33
+ nil
34
+ end
35
+
36
+ # PUBLIC method called from Warden callback in engine.rb
37
+ # Checks if account was just locked due to high risk and signs out if needed
38
+ def check_high_risk_lock_and_signout(auth)
39
+ return unless Beskar.configuration.risk_based_locking_enabled?
40
+
41
+ # Check if there's a very recent lock event (within last 5 seconds)
42
+ recent_lock = security_events
43
+ .where(event_type: ["account_locked", "lock_attempted"])
44
+ .where("created_at >= ?", 5.seconds.ago)
45
+ .exists?
46
+
47
+ if recent_lock
48
+ Beskar::Logger.warn("High-risk lock detected, signing out user #{id}")
49
+ auth.logout
50
+ throw :warden, message: :account_locked_due_to_high_risk
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Devise-specific: Try to get request from various Warden/Devise contexts
57
+ def request_from_context
58
+ # Try to get request from various contexts
59
+ if defined?(Current) && Current.respond_to?(:request)
60
+ Current.request
61
+ elsif Thread.current[:request]
62
+ Thread.current[:request]
63
+ elsif defined?(ActionController::Base) && ActionController::Base.respond_to?(:current_request)
64
+ ActionController::Base.current_request
65
+ elsif defined?(Warden) && Warden::Manager.respond_to?(:current_request)
66
+ Warden::Manager.current_request
67
+ end
68
+ rescue => e
69
+ Beskar::Logger.debug("Could not get request from context: #{e.message}")
70
+ nil
71
+ end
72
+
73
+ # Devise-specific: Handle high risk lock by creating lock event
74
+ # The actual sign-out is handled by Warden callback in engine.rb
75
+ def handle_high_risk_lock(security_event, request)
76
+ Beskar::Logger.debug("Devise account locked - Warden callback will handle sign-out")
77
+ # The lock event is already created by AccountLocker service
78
+ # The Warden callback will detect it and perform the actual sign-out
79
+ end
80
+ end
81
+ end
82
+ end