beskar 0.0.1 → 0.0.2

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.
@@ -0,0 +1,230 @@
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
+ # 1. Check if IP is whitelisted (whitelisted IPs skip blocking but still get logged)
13
+ is_whitelisted = Beskar::Services::IpWhitelist.whitelisted?(ip_address)
14
+
15
+ # 2. Check if IP is banned (early exit for blocked IPs, unless whitelisted)
16
+ if !is_whitelisted && Beskar::BannedIp.banned?(ip_address)
17
+ Rails.logger.warn "[Beskar::Middleware] Blocked request from banned IP: #{ip_address}"
18
+ return blocked_response("Your IP address has been blocked due to suspicious activity.")
19
+ end
20
+
21
+ # 3. Check rate limiting (unless whitelisted)
22
+ if !is_whitelisted && rate_limited?(request)
23
+ Rails.logger.warn "[Beskar::Middleware] Rate limit exceeded for IP: #{ip_address}"
24
+
25
+ # Auto-block after excessive rate limiting violations
26
+ if should_auto_block_rate_limit?(ip_address)
27
+ Beskar::BannedIp.ban!(
28
+ ip_address,
29
+ reason: 'rate_limit_abuse',
30
+ duration: 1.hour,
31
+ details: 'Excessive rate limit violations'
32
+ )
33
+ end
34
+
35
+ return rate_limit_response
36
+ end
37
+
38
+ # 4. Check WAF patterns (vulnerability scans)
39
+ if Beskar.configuration.waf_enabled?
40
+ waf_analysis = Beskar::Services::Waf.analyze_request(request)
41
+
42
+ if waf_analysis
43
+ # Log the violation (and create security event if configured)
44
+ # Pass whitelist status to prevent auto-blocking whitelisted IPs
45
+ violation_count = Beskar::Services::Waf.record_violation(ip_address, waf_analysis, whitelisted: is_whitelisted)
46
+
47
+ # Log even for whitelisted IPs (but don't block)
48
+ if is_whitelisted
49
+ Rails.logger.info(
50
+ "[Beskar::Middleware] WAF violation from whitelisted IP #{ip_address} " \
51
+ "(not blocking): #{waf_analysis[:patterns].map { |p| p[:description] }.join(', ')}"
52
+ )
53
+ else
54
+ # Check if we should block
55
+ should_block = Beskar::Services::Waf.should_block?(ip_address)
56
+
57
+ if Beskar.configuration.waf_monitor_only?
58
+ # Monitor-only mode: Just log, don't block
59
+ if should_block
60
+ Rails.logger.warn(
61
+ "[Beskar::Middleware] 🔍 MONITOR-ONLY: Would block IP #{ip_address} " \
62
+ "after #{violation_count} WAF violations, but monitor_only=true. " \
63
+ "Request proceeding normally."
64
+ )
65
+ end
66
+ elsif should_block
67
+ # Actually block the request
68
+ Rails.logger.warn(
69
+ "[Beskar::Middleware] 🔒 Blocking IP #{ip_address} " \
70
+ "after #{violation_count} WAF violations"
71
+ )
72
+ # Block already handled by WAF.record_violation auto-block logic
73
+ # But we return 403 immediately
74
+ return blocked_response("Access denied due to suspicious activity.")
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ # 5. Process the request normally (will raise 404 if route not found)
81
+ @app.call(env)
82
+ rescue ActionController::RoutingError => e
83
+ # If WAF is enabled, log 404s as potential scanning attempts
84
+ if Beskar.configuration.waf_enabled?
85
+ log_404_for_waf(request, e)
86
+ end
87
+ # Re-raise to allow normal 404 handling
88
+ raise
89
+ end
90
+
91
+ private
92
+
93
+ def rate_limited?(request)
94
+ # Check both IP rate limit and authentication abuse
95
+ ip_check = Beskar::Services::RateLimiter.check_ip_rate_limit(request.ip)
96
+ auth_abused = authentication_brute_force?(request.ip)
97
+
98
+ !ip_check[:allowed] || auth_abused
99
+ end
100
+
101
+ def should_auto_block_rate_limit?(ip_address)
102
+ # Check how many times this IP has been rate limited in the past hour
103
+ cache_key = "beskar:rate_limit_violations:#{ip_address}"
104
+ violations = Rails.cache.read(cache_key) || 0
105
+ violations += 1
106
+ Rails.cache.write(cache_key, violations, expires_in: 1.hour)
107
+
108
+ # Block after 5 rate limit violations in an hour
109
+ violations >= 5
110
+ end
111
+
112
+ def authentication_brute_force?(ip_address)
113
+ # Check authentication failure count from RateLimiter
114
+ cache_key = "beskar:ip_auth_failures:#{ip_address}"
115
+
116
+ # Get current failure count and timestamp
117
+ failure_data = Rails.cache.read(cache_key)
118
+ return false unless failure_data.is_a?(Hash)
119
+
120
+ # Count recent failures (within the configured period)
121
+ config = Beskar.configuration.rate_limiting[:ip_attempts] || {}
122
+ period = config[:period] || 1.hour
123
+ limit = config[:limit] || 10
124
+
125
+ now = Time.current.to_i
126
+ recent_failures = failure_data.select { |timestamp, _| now - timestamp.to_i < period.to_i }
127
+
128
+ # If too many auth failures, it's brute force
129
+ if recent_failures.length >= limit
130
+ # Auto-ban for authentication abuse
131
+ Beskar::BannedIp.ban!(
132
+ ip_address,
133
+ reason: 'authentication_abuse',
134
+ duration: 1.hour,
135
+ details: "#{recent_failures.length} failed authentication attempts in #{period / 60} minutes",
136
+ metadata: { failure_count: recent_failures.length, detection_time: Time.current }
137
+ )
138
+
139
+ Rails.logger.warn(
140
+ "[Beskar::Middleware] 🔒 Auto-blocked IP #{ip_address} " \
141
+ "for authentication brute force (#{recent_failures.length} failures)"
142
+ )
143
+
144
+ return true
145
+ end
146
+
147
+ false
148
+ end
149
+
150
+ def log_404_for_waf(request, error)
151
+ # 404s on suspicious paths might indicate scanning
152
+ path = request.fullpath || request.path
153
+
154
+ # Only log if it matches WAF patterns (already analyzed in analyze_request)
155
+ waf_analysis = Beskar::Services::Waf.analyze_request(request)
156
+
157
+ if waf_analysis
158
+ Rails.logger.info(
159
+ "[Beskar::Middleware] 404 on suspicious path from #{request.ip}: #{path} " \
160
+ "(WAF patterns: #{waf_analysis[:patterns].map { |p| p[:description] }.join(', ')})"
161
+ )
162
+ end
163
+ end
164
+
165
+ def blocked_response(message = "Forbidden")
166
+ [
167
+ 403,
168
+ {
169
+ "Content-Type" => "text/html",
170
+ "X-Beskar-Blocked" => "true"
171
+ },
172
+ [render_blocked_page(message)]
173
+ ]
174
+ end
175
+
176
+ def rate_limit_response
177
+ [
178
+ 429,
179
+ {
180
+ "Content-Type" => "text/html",
181
+ "Retry-After" => "3600",
182
+ "X-Beskar-Rate-Limited" => "true"
183
+ },
184
+ [render_rate_limit_page]
185
+ ]
186
+ end
187
+
188
+ def render_blocked_page(message)
189
+ <<~HTML
190
+ <!DOCTYPE html>
191
+ <html>
192
+ <head>
193
+ <title>Access Denied</title>
194
+ <style>
195
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
196
+ h1 { color: #d32f2f; }
197
+ p { color: #666; }
198
+ </style>
199
+ </head>
200
+ <body>
201
+ <h1>Access Denied</h1>
202
+ <p>#{message}</p>
203
+ <p>If you believe this is an error, please contact the site administrator.</p>
204
+ </body>
205
+ </html>
206
+ HTML
207
+ end
208
+
209
+ def render_rate_limit_page
210
+ <<~HTML
211
+ <!DOCTYPE html>
212
+ <html>
213
+ <head>
214
+ <title>Too Many Requests</title>
215
+ <style>
216
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
217
+ h1 { color: #ff9800; }
218
+ p { color: #666; }
219
+ </style>
220
+ </head>
221
+ <body>
222
+ <h1>Too Many Requests</h1>
223
+ <p>You have exceeded the rate limit. Please try again later.</p>
224
+ </body>
225
+ </html>
226
+ HTML
227
+ end
228
+ end
229
+ end
230
+ 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
+ Rails.logger.warn "[Beskar] 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
+ Rails.logger.info "[Beskar] Destroyed #{sessions.count} sessions except current"
43
+ else
44
+ # Destroy ALL sessions including current
45
+ count = sessions.count
46
+ sessions.destroy_all
47
+ Rails.logger.info "[Beskar] Destroyed all #{count} sessions"
48
+ end
49
+ else
50
+ Rails.logger.warn "[Beskar] Model does not have sessions association, cannot destroy sessions"
51
+ end
52
+ rescue => e
53
+ Rails.logger.error "[Beskar] 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
+ Rails.logger.warn "[Beskar] Emergency password reset performed for user #{id}, reason: #{reason}"
129
+
130
+ rescue => e
131
+ Rails.logger.error "[Beskar] 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
+ Rails.logger.info "[Beskar] Would send emergency reset notification to user #{id}"
153
+ rescue => e
154
+ Rails.logger.error "[Beskar] 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
+ Rails.logger.info "[Beskar] Would notify security team about reset for user #{id}"
162
+ rescue => e
163
+ Rails.logger.error "[Beskar] 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
+ Rails.logger.debug "[Beskar] 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
+ Rails.logger.warn "[Beskar] 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
+ Rails.logger.warn "[Beskar] 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
+ Rails.logger.debug "[Beskar] 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
+ Rails.logger.debug "[Beskar] 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