beskar 0.0.2 → 0.2.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +274 -0
  3. data/README.md +412 -204
  4. data/app/channels/concerns/beskar/channels/session_security.rb +46 -0
  5. data/app/controllers/beskar/administrative_actions_controller.rb +16 -0
  6. data/app/controllers/beskar/application_controller.rb +214 -0
  7. data/app/controllers/beskar/banned_ips_controller.rb +255 -0
  8. data/app/controllers/beskar/dashboard_controller.rb +62 -0
  9. data/app/controllers/beskar/security_events_controller.rb +164 -0
  10. data/app/controllers/concerns/beskar/controllers/audit_export.rb +54 -0
  11. data/app/controllers/concerns/beskar/controllers/security_tracking.rb +76 -48
  12. data/app/controllers/concerns/beskar/controllers/session_security.rb +29 -0
  13. data/app/jobs/beskar/notification_job.rb +33 -0
  14. data/app/mailers/beskar/security_mailer.rb +59 -0
  15. data/app/models/beskar/administrative_action.rb +41 -0
  16. data/app/models/beskar/banned_ip.rb +105 -105
  17. data/app/models/beskar/security_event.rb +51 -4
  18. data/app/models/beskar/security_state.rb +58 -0
  19. data/app/services/beskar/banned_ip_manager.rb +88 -0
  20. data/app/views/beskar/administrative_actions/index.html.erb +33 -0
  21. data/app/views/beskar/administrative_actions/show.html.erb +21 -0
  22. data/app/views/beskar/banned_ips/edit.html.erb +195 -0
  23. data/app/views/beskar/banned_ips/index.html.erb +319 -0
  24. data/app/views/beskar/banned_ips/new.html.erb +190 -0
  25. data/app/views/beskar/banned_ips/review.html.erb +24 -0
  26. data/app/views/beskar/banned_ips/show.html.erb +304 -0
  27. data/app/views/beskar/dashboard/index.html.erb +280 -0
  28. data/app/views/beskar/security_events/index.html.erb +302 -0
  29. data/app/views/beskar/security_events/show.html.erb +293 -0
  30. data/app/views/beskar/shared/_export_form.html.erb +10 -0
  31. data/app/views/layouts/beskar/_behavior.html.erb +121 -0
  32. data/app/views/layouts/beskar/application.html.erb +581 -6
  33. data/config/routes.rb +30 -0
  34. data/db/migrate/20251016000001_create_beskar_security_events.rb +3 -3
  35. data/db/migrate/20260910000001_create_beskar_security_states.rb +14 -0
  36. data/db/migrate/20260911000001_create_beskar_administrative_actions.rb +22 -0
  37. data/db/migrate/20260911000002_expand_administrative_action_targets.rb +6 -0
  38. data/docs/README.md +73 -0
  39. data/docs/archive/project-documentation.md +659 -0
  40. data/docs/audits/project-review.md +437 -0
  41. data/docs/audits/repair-status.md +216 -0
  42. data/docs/guides/audit-and-waf.md +175 -0
  43. data/docs/guides/audit-lifecycle.md +172 -0
  44. data/docs/guides/authentication.md +213 -0
  45. data/docs/guides/configuration.md +182 -0
  46. data/docs/guides/dashboard-and-search.md +251 -0
  47. data/docs/guides/notifications-and-recovery.md +157 -0
  48. data/docs/guides/risk-scoring.md +116 -0
  49. data/docs/operations/monitor-only-mode.md +85 -0
  50. data/docs/operations/security-hardening.md +167 -0
  51. data/docs/operations/state-storage.md +144 -0
  52. data/docs/research/rust-performance-assessment.md +69 -0
  53. data/lib/beskar/configuration.rb +105 -20
  54. data/lib/beskar/configuration_validator.rb +188 -0
  55. data/lib/beskar/devise_authentication.rb +24 -0
  56. data/lib/beskar/engine.rb +21 -88
  57. data/lib/beskar/logger.rb +288 -0
  58. data/lib/beskar/middleware/request_analyzer.rb +133 -99
  59. data/lib/beskar/models/security_trackable_authenticable.rb +76 -97
  60. data/lib/beskar/models/security_trackable_devise.rb +34 -25
  61. data/lib/beskar/models/security_trackable_generic.rb +171 -214
  62. data/lib/beskar/risk_level.rb +22 -0
  63. data/lib/beskar/services/account_locker.rb +90 -81
  64. data/lib/beskar/services/administrative_audit.rb +36 -0
  65. data/lib/beskar/services/administrative_bans.rb +104 -0
  66. data/lib/beskar/services/audit_data.rb +72 -0
  67. data/lib/beskar/services/authentication.rb +31 -0
  68. data/lib/beskar/services/authentication_attempt.rb +141 -0
  69. data/lib/beskar/services/ban_expiry.rb +28 -0
  70. data/lib/beskar/services/device_detector.rb +32 -41
  71. data/lib/beskar/services/event_search.rb +58 -0
  72. data/lib/beskar/services/geolocation_service.rb +83 -114
  73. data/lib/beskar/services/ip_whitelist.rb +31 -40
  74. data/lib/beskar/services/location_assessment.rb +109 -0
  75. data/lib/beskar/services/native_account_lock.rb +82 -0
  76. data/lib/beskar/services/notifications.rb +46 -0
  77. data/lib/beskar/services/rate_limiter.rb +99 -125
  78. data/lib/beskar/services/request_context.rb +64 -0
  79. data/lib/beskar/services/risk_assessment.rb +58 -0
  80. data/lib/beskar/services/session_revocation.rb +62 -0
  81. data/lib/beskar/services/waf.rb +311 -198
  82. data/lib/beskar/services/waf_request.rb +60 -0
  83. data/lib/beskar/version.rb +1 -1
  84. data/lib/beskar/warden_authentication.rb +53 -0
  85. data/lib/beskar.rb +54 -4
  86. data/lib/generators/beskar/install/install_generator.rb +158 -0
  87. data/lib/generators/beskar/install/templates/initializer.rb.tt +261 -0
  88. data/lib/tasks/beskar_tasks.rake +25 -20
  89. metadata +93 -12
  90. data/lib/beskar/templates/beskar_initializer.rb +0 -107
@@ -2,146 +2,168 @@ module Beskar
2
2
  module Models
3
3
  # Generic security tracking functionality shared by all authentication systems
4
4
  # This module provides the core security event tracking, risk scoring,
5
- # and adaptive learning features that work with any authentication framework
5
+ # and risk evidence shared by the supported authentication adapters
6
6
  module SecurityTrackableGeneric
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- has_many :security_events, class_name: "Beskar::SecurityEvent", as: :user, dependent: :destroy
10
+ # Audit rows outlive the account, retaining the original polymorphic
11
+ # identity and stored evidence without deletion or nullification.
12
+ has_many :security_events, class_name: "Beskar::SecurityEvent", as: :user
11
13
  end
12
14
 
13
15
  module ClassMethods
14
16
  # Track failed authentication attempt without a user context
15
17
  # 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
- Rails.logger.debug "[Beskar] Failed login tracking disabled in configuration"
20
- return
21
- end
22
-
23
- # Extract attempted email from params based on scope
18
+ def track_failed_authentication(request, scope, attempt: nil)
24
19
  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,
20
+ attempt ||= Services::AuthenticationAttempt.current(request, scope)
21
+ unless attempt
22
+ key = attribute_names.include?("email_address") ? :email_address : :email
23
+ credentials = attempted_email ? {key => attempted_email} : {}
24
+ user = if credentials.empty?
25
+ nil
26
+ elsif respond_to?(:find_for_database_authentication)
27
+ find_for_database_authentication(credentials)
28
+ else
29
+ find_by(credentials)
30
+ end
31
+ attempt = Services::AuthenticationAttempt.reserve(request, model: self, scope: scope,
32
+ user: user, credentials: credentials)
33
+ end
34
+ attempted_email ||= attempt.attempted_email
35
+ return attempt.event if attempt.completed
36
+
37
+ attempt.completed = true
38
+ return unless Beskar.configuration.track_failed_logins?
39
+
40
+ assessment = Services::RiskAssessment.new(request, user: attempt.user, result: :failure) if attempt.allowed?
41
+ event = Beskar::SecurityEvent.new(
42
+ user_type: attempt.user&.class&.polymorphic_name, user_id: attempt.user&.id,
43
+ event_type: attempt.allowed? ? "login_failure" : "authentication_blocked",
44
+ ip_address: Services::RequestContext.ip(request),
45
+ user_agent: Services::RequestContext.text(request.user_agent),
46
46
  attempted_email: attempted_email,
47
- metadata: metadata,
48
- risk_score: calculate_failure_risk_score(request)
47
+ metadata: Services::RequestContext.security_metadata(request, enrich: false).merge(assessment&.metadata || {}).merge(scope: scope.to_s,
48
+ request_path: attempt.request_path, authentication: attempt.metadata),
49
+ risk_score: assessment&.score || 0
49
50
  )
50
-
51
- # Trigger rate limiting check
52
- Beskar::Services::RateLimiter.check_authentication_attempt(request, :failure)
51
+ event.beskar_attempt = attempt
52
+ attempt.event = event
53
+ Beskar::SecurityEvent.transaction(requires_new: true) { event.save! }
54
+ event
55
+ rescue Services::AuthenticationAttempt::Unavailable
56
+ raise
57
+ rescue => error
58
+ Beskar::Logger.warn("Failed authentication audit unavailable (#{error.class})")
59
+ attempt&.event
53
60
  end
54
61
 
55
62
  private
56
63
 
57
64
  def extract_attempted_email(request, scope)
58
65
  # Try different param patterns based on scope
59
- request.params.dig("devise_user", "email") ||
60
- request.params.dig(scope.to_s, "email") ||
66
+ request.params.dig(scope.to_s, "email") ||
67
+ request.params.dig(scope.to_s, "email_address") ||
68
+ request.params.dig("devise_user", "email") ||
61
69
  request.params.dig("user", "email") ||
70
+ request.params.dig("user", "email_address") ||
62
71
  request.params.dig("email_address") ||
63
72
  request.params["email"]
64
73
  end
65
74
 
66
75
  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
- Rails.logger.info "[Beskar] 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
- Rails.logger.info "[Beskar] Geolocation risk: #{geo_score}"
88
-
89
- [score, 100].min # Cap at 100
76
+ Services::RiskAssessment.new(request, result: :failure).score
90
77
  end
91
78
  end
92
79
 
93
80
  # Track authentication event (success or failure) for a specific user
94
- def track_authentication_event(request, result)
81
+ def track_authentication_event(request, result, attempt: nil, persist: true)
95
82
  return unless request
96
83
 
97
- # Check if tracking is enabled for this event type
98
- if result == :success && !Beskar.configuration.track_successful_logins?
99
- Rails.logger.debug "[Beskar] Successful login tracking disabled in configuration"
100
- return
101
- elsif result == :failure && !Beskar.configuration.track_failed_logins?
102
- Rails.logger.debug "[Beskar] Failed login tracking disabled in configuration"
84
+ attempt ||= Services::AuthenticationAttempt.reserve(request, model: self.class,
85
+ scope: self.class.name.underscore, user: self)
86
+ attempt.bind_user!(self)
87
+ return attempt.event if attempt.completed
88
+ tracking = (result == :success) ? Beskar.configuration.track_successful_logins? : Beskar.configuration.track_failed_logins?
89
+ assessment_required = tracking || (result == :success && Beskar.configuration.risk_based_locking_enabled?)
90
+ unless assessment_required
91
+ attempt.completed = true
103
92
  return
104
93
  end
105
94
 
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,
95
+ # Build the decision even when audit persistence is disabled. A rejected
96
+ # credential attempt is never recorded as a trusted successful login.
97
+ # Do not build through has_many: saving/locking the user can autosave its
98
+ # unsaved children and accidentally make optional audit writes mandatory.
99
+ assessment = assess_authentication_risk(request, result)
100
+ security_event = Beskar::SecurityEvent.new(
101
+ user_type: self.class.polymorphic_name, user_id: id,
102
+ event_type: (result == :success) ? "login_success" : "login_failure",
103
+ ip_address: Services::RequestContext.ip(request),
104
+ user_agent: Services::RequestContext.text(request.user_agent),
112
105
  attempted_email: extract_user_email,
113
- metadata: extract_security_context(request),
114
- risk_score: calculate_risk_score(request, result)
106
+ metadata: Services::RequestContext.security_metadata(request, enrich: false).merge(assessment.metadata),
107
+ risk_score: assessment.score
115
108
  )
109
+ security_event.beskar_attempt = attempt
110
+ attempt.event = security_event
116
111
 
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)
112
+ if result == :success && attempt.allowed?
113
+ attempt.locked_now = !!check_and_lock_if_high_risk(security_event, request)
114
+ if attempt.locked_now
115
+ attempt.deny!(:account_locked)
128
116
  end
129
117
  end
130
118
 
119
+ attempt.verify_generation!
120
+ security_event.event_type = "authentication_blocked" unless attempt.allowed?
121
+ security_event.metadata = (security_event.metadata || {}).merge("authentication" => attempt.metadata)
122
+ attempt.completed = true
123
+ return unless tracking && persist
124
+
125
+ persist_authentication_audit(security_event)
126
+ analyze_suspicious_patterns_async if result == :success && attempt.allowed? && Beskar.configuration.auto_analyze_patterns?
131
127
  security_event
128
+ rescue Services::AuthenticationAttempt::Unavailable
129
+ raise
130
+ rescue => error
131
+ Beskar::Logger.error("Authentication risk assessment unavailable (#{error.class})")
132
+ # A broken risk assessment must not silently admit a potentially locked
133
+ # account. Audit writes have their own non-fatal boundary below.
134
+ if Beskar.configuration.risk_based_locking_enabled? && Services::RequestContext.enforce?(attempt&.ip_address)
135
+ raise Services::AuthenticationAttempt::Unavailable, "Authentication temporarily unavailable"
136
+ end
137
+ attempt.completed = true if attempt
138
+ nil
132
139
  end
133
140
 
134
141
  def analyze_suspicious_patterns_async
135
- # Skip analysis if disabled in configuration
136
- unless Beskar.configuration.auto_analyze_patterns?
137
- Rails.logger.debug "[Beskar] Auto pattern analysis disabled in configuration"
138
- return
142
+ return unless Beskar.configuration.auto_analyze_patterns?
143
+ job = Beskar.configuration.analysis_job_class
144
+ arguments = {user_type: self.class.base_class.name, user_id: id, event_type: "login_success"}
145
+ # Never enqueue for an outer transaction that later rolls back. The job
146
+ # is optional enrichment, not delayed authentication enforcement.
147
+ ActiveRecord.after_all_transactions_commit do
148
+ result = job.perform_later(**arguments)
149
+ Beskar::Logger.warn("Security analysis job was not enqueued") unless result
150
+ rescue => error
151
+ Beskar::Logger.warn("Failed to queue security analysis (#{error.class})")
139
152
  end
140
-
141
- # Queue background job for detailed analysis
142
- Beskar::SecurityAnalysisJob.perform_later(self.id, "login_success") if defined?(Beskar::SecurityAnalysisJob)
143
153
  rescue => e
144
- Rails.logger.warn "[Beskar] Failed to queue security analysis: #{e.message}"
154
+ Beskar::Logger.warn("Failed to prepare security analysis (#{e.class})")
155
+ end
156
+
157
+ def beskar_session_token
158
+ Services::SessionRevocation.token(self)
159
+ end
160
+
161
+ def revoke_beskar_sessions!
162
+ Services::SessionRevocation.revoke!(self)
163
+ end
164
+
165
+ def beskar_session_valid?(request, token:)
166
+ Services::SessionRevocation.allowed?(self, request: request, token: token)
145
167
  end
146
168
 
147
169
  def recent_failed_attempts(within: 1.hour)
@@ -164,81 +186,48 @@ module Beskar
164
186
  return true if recent_attempts.count >= 3
165
187
 
166
188
  # 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)
189
+ return true if geographic_anomaly_detected?
169
190
 
170
191
  false
171
192
  end
172
193
 
173
194
  private
174
195
 
196
+ def persist_authentication_audit(event)
197
+ Beskar::SecurityEvent.transaction(requires_new: true) { event.save }
198
+ rescue => error
199
+ Beskar::Logger.warn("Authentication audit unavailable (#{error.class})")
200
+ false
201
+ end
202
+
175
203
  def extract_user_email
176
204
  # Try different email attribute names
177
- respond_to?(:email) ? email : (respond_to?(:email_address) ? email_address : nil)
205
+ if respond_to?(:email)
206
+ email
207
+ else
208
+ (respond_to?(:email_address) ? email_address : nil)
209
+ end
178
210
  end
179
211
 
180
212
  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
- }
213
+ Services::RequestContext.security_metadata(request)
192
214
  end
193
215
 
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
- Rails.logger.info "[Beskar] 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
- Rails.logger.info "[Beskar] 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
- Rails.logger.info "[Beskar] 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
216
+ def assess_authentication_risk(request, result)
217
+ Services::RiskAssessment.new(request, user: self, result: result)
218
+ end
234
219
 
235
- [score, 100].min # Cap at 100
220
+ # Numeric compatibility helper; authentication uses the complete assessment.
221
+ def calculate_risk_score(request, result)
222
+ assess_authentication_risk(request, result).score
236
223
  end
237
224
 
238
- def geographic_anomaly_detected?(recent_logins)
239
- # Placeholder for geographic anomaly detection
240
- # Would implement haversine formula and impossible travel detection
241
- false
225
+ def geographic_anomaly_detected?
226
+ observations = Services::RiskAssessment.observations(self)
227
+ observations.sort_by { |entry| [entry[:occurred_at], entry[:event_id]] }.each_cons(2).any? do |previous, current|
228
+ assessment = Services::LocationAssessment.new(current[:location], observations: [previous], at: current[:occurred_at]).call
229
+ assessment[:location][:impossible_travel]
230
+ end
242
231
  end
243
232
 
244
233
  # Check if the account should be locked based on risk score
@@ -251,40 +240,52 @@ module Beskar
251
240
  risk_score: security_event.risk_score,
252
241
  reason: determine_lock_reason(security_event),
253
242
  metadata: {
254
- ip_address: request.ip,
255
- user_agent: request.user_agent,
243
+ ip_address: Beskar::Services::RequestContext.ip(request),
244
+ user_agent: Services::RequestContext.text(request.user_agent),
256
245
  security_event_id: security_event.id,
246
+ authentication_attempt_id: security_event.beskar_attempt&.id,
257
247
  geolocation: security_event.geolocation,
258
- device_info: security_event.device_info
248
+ device_info: security_event.device_info,
249
+ risk_assessment: security_event.metadata["risk_assessment"]
259
250
  }
260
251
  )
261
252
 
253
+ security_event.metadata["lock_decision"] = {
254
+ "threshold" => Beskar.configuration.risk_threshold,
255
+ "strategy_available" => locker.supported?,
256
+ "would_lock" => locker.supported? && security_event.risk_score >= Beskar.configuration.risk_threshold && !locker.locked?,
257
+ "enforcement_enabled" => Services::RequestContext.enforce?(Services::RequestContext.ip(request))
258
+ }
259
+
262
260
  if locker.lock_if_necessary!
263
- Rails.logger.warn "[Beskar] Account locked due to high risk score: #{security_event.risk_score}"
264
-
261
+ Beskar::Logger.warn("Account locked due to high risk score: #{security_event.risk_score}")
262
+
265
263
  # Trigger auth-system-specific lock handling
266
264
  handle_high_risk_lock(security_event, request)
265
+ true
266
+ else
267
+ false
267
268
  end
268
269
  end
269
270
 
270
271
  # Determine the specific reason for locking
271
272
  def determine_lock_reason(security_event)
272
- metadata = security_event.metadata || {}
273
+ metadata = (security_event.metadata || {}).deep_stringify_keys
273
274
 
274
275
  # Check for impossible travel
275
- if metadata.dig("geolocation", "impossible_travel")
276
+ if metadata.dig("geolocation", "impossible_travel") == true
276
277
  return :impossible_travel
277
278
  end
278
279
 
279
280
  # Check for suspicious device
280
281
  device_info = metadata["device_info"] || {}
281
- if device_info["bot_signature"] || device_info["suspicious"]
282
+ if device_info["bot"] == true || device_info["bot_signature"] == true || device_info["suspicious"] == true
282
283
  return :suspicious_device
283
284
  end
284
285
 
285
286
  # Check for geographic anomaly
286
287
  geolocation = metadata["geolocation"] || {}
287
- if geolocation["country_change"] || geolocation["high_risk_country"]
288
+ if geolocation["country_change"] == true || geolocation["high_risk_country"] == true
288
289
  return :geographic_anomaly
289
290
  end
290
291
 
@@ -294,61 +295,17 @@ module Beskar
294
295
 
295
296
  # Override this in auth-system-specific modules
296
297
  def handle_high_risk_lock(security_event, request)
297
- Rails.logger.debug "[Beskar] High risk lock handled - override in specific module"
298
+ Beskar::Logger.debug("High risk lock handled - override in specific module")
298
299
  end
299
300
 
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
301
+ # Kept for integrations that called these former private helpers. Repeated
302
+ # IP use, automatic unlocks, and account-lock attempts do not establish trust.
303
+ def established_pattern?(_request)
304
+ false
337
305
  end
338
306
 
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
307
+ def location_established?(_ip_address)
308
+ false
352
309
  end
353
310
  end
354
311
  end
@@ -0,0 +1,22 @@
1
+ module Beskar
2
+ # Reporting bands, not configurable authentication-lock thresholds.
3
+ module RiskLevel
4
+ RANGES = {low: 0...30, medium: 30...70, high: 70...90, critical: 90..100}.freeze
5
+ BADGES = {low: "success", medium: "warning", high: "danger", critical: "critical"}.freeze
6
+ COLORS = {low: "#4CAF50", medium: "#FF9800", high: "#E91E63", critical: "#D32F2F"}.freeze
7
+
8
+ module_function
9
+
10
+ def for(score)
11
+ return unless score.is_a?(Numeric) && score.real? && score.finite?
12
+ RANGES.find { |_, range| range.cover?(score) }&.first
13
+ end
14
+
15
+ def filter_options
16
+ RANGES.map do |level, range|
17
+ last = range.exclude_end? ? range.end - 1 : range.end
18
+ ["#{level.to_s.capitalize} (#{range.begin}-#{last})", level.to_s]
19
+ end
20
+ end
21
+ end
22
+ end