beskar 0.0.2 → 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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.md +298 -110
  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 +6 -6
  9. data/app/models/beskar/banned_ip.rb +68 -27
  10. data/app/models/beskar/security_event.rb +14 -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/routes.rb +41 -0
  21. data/lib/beskar/configuration.rb +24 -10
  22. data/lib/beskar/engine.rb +4 -4
  23. data/lib/beskar/logger.rb +293 -0
  24. data/lib/beskar/middleware/request_analyzer.rb +128 -53
  25. data/lib/beskar/models/security_trackable_authenticable.rb +11 -11
  26. data/lib/beskar/models/security_trackable_devise.rb +5 -5
  27. data/lib/beskar/models/security_trackable_generic.rb +12 -12
  28. data/lib/beskar/services/account_locker.rb +12 -12
  29. data/lib/beskar/services/geolocation_service.rb +8 -8
  30. data/lib/beskar/services/ip_whitelist.rb +2 -2
  31. data/lib/beskar/services/waf.rb +307 -78
  32. data/lib/beskar/version.rb +1 -1
  33. data/lib/beskar.rb +1 -0
  34. data/lib/generators/beskar/install/install_generator.rb +158 -0
  35. data/lib/generators/beskar/install/templates/initializer.rb.tt +177 -0
  36. data/lib/tasks/beskar_tasks.rake +11 -2
  37. metadata +35 -6
  38. data/lib/beskar/templates/beskar_initializer.rb +0 -107
@@ -1,10 +1,15 @@
1
1
  module Beskar
2
2
  class Configuration
3
- attr_accessor :rate_limiting, :security_tracking, :risk_based_locking, :geolocation, :ip_whitelist, :waf, :authentication_models, :emergency_password_reset
3
+ attr_accessor :rate_limiting, :security_tracking, :risk_based_locking, :geolocation, :ip_whitelist, :waf, :authentication_models, :emergency_password_reset, :monitor_only, :authenticate_admin
4
4
 
5
5
  def initialize
6
+ @monitor_only = false # Global monitor-only mode - logs everything but doesn't block
6
7
  @ip_whitelist = [] # Array of IP addresses or CIDR ranges
7
8
 
9
+ # Dashboard authentication - configure this to restrict access to the dashboard
10
+ # Example: config.authenticate_admin = proc { authenticate_admin! }
11
+ @authenticate_admin = nil
12
+
8
13
  # Authentication models configuration
9
14
  # Auto-detect by default, or can be explicitly configured
10
15
  @authentication_models = {
@@ -16,12 +21,20 @@ module Beskar
16
21
  @waf = {
17
22
  enabled: false, # Master switch for WAF
18
23
  auto_block: true, # Automatically block IPs after threshold
19
- block_threshold: 3, # Number of violations before blocking
20
- violation_window: 1.hour, # Time window to count violations
21
- block_durations: [ 1.hour, 6.hours, 24.hours, 7.days ], # Escalating block durations
22
- permanent_block_after: 5, # Permanent block after N violations (nil = never)
24
+ score_threshold: 150, # Cumulative risk score before blocking (replaces block_threshold)
25
+ violation_window: 6.hours, # Maximum time window to track violations
26
+ block_durations: [1.hour, 6.hours, 24.hours, 7.days], # Escalating block durations
27
+ permanent_block_after: 500, # Permanent block after cumulative score reaches this (nil = never)
23
28
  create_security_events: true, # Create SecurityEvent records
24
- monitor_only: false # If true, log but don't block (even if auto_block is true)
29
+ record_not_found_exclusions: [], # Regex patterns to exclude from RecordNotFound detection
30
+ decay_enabled: true, # Enable exponential decay of violation scores over time
31
+ decay_rates: { # Decay rates by severity (half-life in minutes)
32
+ critical: 360, # Critical violations: 6 hour half-life
33
+ high: 120, # High violations: 2 hour half-life
34
+ medium: 45, # Medium violations: 45 minute half-life
35
+ low: 15 # Low violations: 15 minute half-life
36
+ },
37
+ max_violations_tracked: 50 # Maximum number of violations to track per IP (oldest pruned)
25
38
  }
26
39
  @security_tracking = {
27
40
  enabled: true,
@@ -135,11 +148,12 @@ module Beskar
135
148
  end
136
149
 
137
150
  def waf_auto_block?
138
- waf_enabled? && @waf[:auto_block] && !@waf[:monitor_only]
151
+ waf_enabled? && @waf[:auto_block] && !@monitor_only
139
152
  end
140
153
 
141
- def waf_monitor_only?
142
- @waf[:monitor_only] == true
154
+ # General monitor-only mode check (affects all blocking)
155
+ def monitor_only?
156
+ @monitor_only == true
143
157
  end
144
158
 
145
159
  # IP Whitelist configuration helpers
@@ -179,7 +193,7 @@ module Beskar
179
193
  end
180
194
  rescue => e
181
195
  # Ignore errors during detection
182
- Rails.logger.debug "[Beskar] Error detecting Rails auth model #{model.name}: #{e.message}"
196
+ Beskar::Logger.debug("Error detecting Rails auth model #{model.name}: #{e.message}")
183
197
  end
184
198
  end
185
199
 
data/lib/beskar/engine.rb CHANGED
@@ -11,9 +11,9 @@ module Beskar
11
11
  if defined?(Beskar::BannedIp)
12
12
  Rails.application.executor.wrap do
13
13
  Beskar::BannedIp.preload_cache!
14
- Rails.logger.info "[Beskar] Preloaded banned IPs into cache"
14
+ Beskar::Logger.info("Preloaded banned IPs into cache")
15
15
  rescue => e
16
- Rails.logger.warn "[Beskar] Failed to preload banned IPs: #{e.message}"
16
+ Beskar::Logger.warn("Failed to preload banned IPs: #{e.message}")
17
17
  end
18
18
  end
19
19
  end
@@ -35,7 +35,7 @@ module Beskar
35
35
  security_event &&
36
36
  user_was_just_locked?(user, security_event) &&
37
37
  user.respond_to?(:access_locked?) && user.access_locked?
38
- Rails.logger.warn "[Beskar] Signing out user #{user.id} due to high-risk lock"
38
+ Beskar::Logger.warn("Signing out user #{user.id} due to high-risk lock")
39
39
  auth.logout
40
40
  throw :warden, scope: opts[:scope], message: :account_locked_due_to_high_risk
41
41
  end
@@ -61,7 +61,7 @@ module Beskar
61
61
  if model_class && model_class.respond_to?(:track_failed_authentication)
62
62
  model_class.track_failed_authentication(request, scope)
63
63
  else
64
- Rails.logger.debug "[Beskar] No trackable model found for scope: #{scope}"
64
+ Beskar::Logger.debug("No trackable model found for scope: #{scope}")
65
65
  end
66
66
  end
67
67
  end
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beskar
4
+ # Centralized logging module for consistent log formatting and flexible output handling
5
+ module Logger
6
+ class << self
7
+ # Available log levels
8
+ LOG_LEVELS = %i[debug info warn error fatal].freeze
9
+
10
+ # Generate logging methods for each level
11
+ LOG_LEVELS.each do |level|
12
+ define_method(level) do |message, component: nil|
13
+ log(level, message, component: component)
14
+ end
15
+ end
16
+
17
+ # Main logging method that handles formatting and output
18
+ #
19
+ # @param level [Symbol] The log level (:debug, :info, :warn, :error, :fatal)
20
+ # @param message [String] The message to log
21
+ # @param component [String, Symbol, nil] Optional component name for more specific prefixes
22
+ #
23
+ # @example Basic usage
24
+ # Beskar::Logger.info("User authenticated successfully")
25
+ # # => [Beskar] User authenticated successfully
26
+ #
27
+ # @example With component
28
+ # Beskar::Logger.warn("Rate limit exceeded", component: :WAF)
29
+ # # => [Beskar::WAF] Rate limit exceeded
30
+ #
31
+ # @example With class as component
32
+ # Beskar::Logger.error("Failed to lock account", component: self.class)
33
+ # # => [Beskar::AccountLocker] Failed to lock account
34
+ def log(level, message, component: nil)
35
+ return unless should_log?(level)
36
+
37
+ formatted_message = format_message(message, component)
38
+ logger.send(level, formatted_message)
39
+ rescue StandardError => e
40
+ # Fallback to stderr if logging fails
41
+ $stderr.puts "[Beskar::Logger] Failed to log message: #{e.message}"
42
+ $stderr.puts "[Beskar::Logger] Original message: #{formatted_message}"
43
+ end
44
+
45
+ # Configure the logger instance
46
+ #
47
+ # @param logger_instance [Logger, nil] The logger to use, defaults to Rails.logger
48
+ def logger=(logger_instance)
49
+ @logger = logger_instance
50
+ end
51
+
52
+ # Get the current logger instance
53
+ #
54
+ # @return [Logger] The configured logger or Rails.logger as default
55
+ def logger
56
+ @logger ||= default_logger
57
+ end
58
+
59
+ # Configure log level threshold
60
+ #
61
+ # @param level [Symbol, String] Minimum log level to output
62
+ def level=(level)
63
+ @level = level.to_sym if LOG_LEVELS.include?(level.to_sym)
64
+ end
65
+
66
+ # Get the current log level
67
+ #
68
+ # @return [Symbol] Current log level
69
+ def level
70
+ @level ||= :debug
71
+ end
72
+
73
+ # Reset logger configuration to defaults
74
+ def reset!
75
+ @logger = nil
76
+ @level = nil
77
+ @component_aliases = nil
78
+ end
79
+
80
+ # Configure component name aliases for cleaner output
81
+ #
82
+ # @param aliases [Hash] Mapping of classes/modules to display names
83
+ #
84
+ # @example
85
+ # Beskar::Logger.component_aliases = {
86
+ # 'Beskar::Services::Waf' => 'WAF',
87
+ # 'Beskar::Services::AccountLocker' => 'AccountLocker'
88
+ # }
89
+ def component_aliases=(aliases)
90
+ @component_aliases = aliases
91
+ end
92
+
93
+ # Get component aliases
94
+ #
95
+ # @return [Hash] Current component aliases
96
+ def component_aliases
97
+ @component_aliases ||= default_component_aliases
98
+ end
99
+
100
+ private
101
+
102
+ # Format the log message with appropriate prefix
103
+ #
104
+ # @param message [String] The message to format
105
+ # @param component [String, Symbol, Class, nil] Component identifier
106
+ # @return [String] Formatted message with prefix
107
+ def format_message(message, component)
108
+ prefix = build_prefix(component)
109
+ "#{prefix} #{message}"
110
+ end
111
+
112
+ # Build the log prefix based on component
113
+ #
114
+ # @param component [String, Symbol, Class, nil] Component identifier
115
+ # @return [String] Formatted prefix
116
+ def build_prefix(component)
117
+ return "[Beskar]" if component.nil?
118
+
119
+ component_name = normalize_component_name(component)
120
+
121
+ if component_name.nil? || component_name.empty?
122
+ "[Beskar]"
123
+ else
124
+ "[Beskar::#{component_name}]"
125
+ end
126
+ end
127
+
128
+ # Normalize component name from various input types
129
+ #
130
+ # @param component [String, Symbol, Class] Component identifier
131
+ # @return [String, nil] Normalized component name
132
+ def normalize_component_name(component)
133
+ case component
134
+ when String
135
+ apply_component_alias(component)
136
+ when Symbol
137
+ component.to_s
138
+ when Class
139
+ apply_component_alias(component.name)
140
+ when Module
141
+ apply_component_alias(component.name)
142
+ else
143
+ component.to_s
144
+ end
145
+ end
146
+
147
+ # Apply component alias if configured
148
+ #
149
+ # @param component_name [String] Original component name
150
+ # @return [String] Aliased name or original
151
+ def apply_component_alias(component_name)
152
+ return nil if component_name.nil?
153
+
154
+ # First check exact matches
155
+ aliased = component_aliases[component_name]
156
+ return aliased if aliased
157
+
158
+ # Remove Beskar:: prefix if present for lookup
159
+ clean_name = component_name.sub(/^Beskar::/, '')
160
+ aliased = component_aliases[clean_name]
161
+ return aliased if aliased
162
+
163
+ # Check if it's already a simple component name (no ::)
164
+ return clean_name unless clean_name.include?('::')
165
+
166
+ # Extract the last component for nested classes
167
+ # e.g., "Beskar::Services::Waf" -> "Waf"
168
+ last_component = clean_name.split('::').last
169
+ component_aliases[clean_name] || last_component
170
+ end
171
+
172
+ # Check if message should be logged based on current level
173
+ #
174
+ # @param message_level [Symbol] Level of the message
175
+ # @return [Boolean] True if message should be logged
176
+ def should_log?(message_level)
177
+ level_value(message_level) >= level_value(level)
178
+ end
179
+
180
+ # Convert log level to numeric value for comparison
181
+ #
182
+ # @param level_sym [Symbol] Log level
183
+ # @return [Integer] Numeric value
184
+ def level_value(level_sym)
185
+ LOG_LEVELS.index(level_sym) || 0
186
+ end
187
+
188
+ # Get the default logger instance
189
+ #
190
+ # @return [Logger] Default logger (Rails.logger or stdlib Logger)
191
+ def default_logger
192
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
193
+ Rails.logger
194
+ else
195
+ require 'logger'
196
+ ::Logger.new($stdout)
197
+ end
198
+ end
199
+
200
+ # Default component name aliases for cleaner output
201
+ #
202
+ # @return [Hash] Default aliases
203
+ def default_component_aliases
204
+ {
205
+ 'Beskar::Services::Waf' => 'WAF',
206
+ 'Beskar::Services::WAF' => 'WAF',
207
+ 'Services::Waf' => 'WAF',
208
+ 'Services::WAF' => 'WAF',
209
+ 'Beskar::Services::AccountLocker' => 'AccountLocker',
210
+ 'Services::AccountLocker' => 'AccountLocker',
211
+ 'Beskar::Services::RateLimiter' => 'RateLimiter',
212
+ 'Services::RateLimiter' => 'RateLimiter',
213
+ 'Beskar::Services::IpWhitelist' => 'IpWhitelist',
214
+ 'Services::IpWhitelist' => 'IpWhitelist',
215
+ 'Beskar::Services::GeolocationService' => 'GeolocationService',
216
+ 'Services::GeolocationService' => 'GeolocationService',
217
+ 'Beskar::Services::DeviceDetector' => 'DeviceDetector',
218
+ 'Services::DeviceDetector' => 'DeviceDetector',
219
+ 'Beskar::Middleware::RequestAnalyzer' => 'Middleware',
220
+ 'Middleware::RequestAnalyzer' => 'Middleware',
221
+ 'Beskar::Models::SecurityTrackableDevise' => 'SecurityTracking',
222
+ 'Models::SecurityTrackableDevise' => 'SecurityTracking',
223
+ 'Beskar::Models::SecurityTrackableAuthenticable' => 'SecurityTracking',
224
+ 'Models::SecurityTrackableAuthenticable' => 'SecurityTracking',
225
+ 'Beskar::Models::SecurityTrackableGeneric' => 'SecurityTracking',
226
+ 'Models::SecurityTrackableGeneric' => 'SecurityTracking'
227
+ }
228
+ end
229
+ end
230
+
231
+ # Module to include in classes for instance-level logging
232
+ module ClassMethods
233
+ # Log a debug message with automatic component detection
234
+ def log_debug(message)
235
+ Beskar::Logger.debug(message, component: self)
236
+ end
237
+
238
+ # Log an info message with automatic component detection
239
+ def log_info(message)
240
+ Beskar::Logger.info(message, component: self)
241
+ end
242
+
243
+ # Log a warning message with automatic component detection
244
+ def log_warn(message)
245
+ Beskar::Logger.warn(message, component: self)
246
+ end
247
+
248
+ # Log an error message with automatic component detection
249
+ def log_error(message)
250
+ Beskar::Logger.error(message, component: self)
251
+ end
252
+
253
+ # Log a fatal message with automatic component detection
254
+ def log_fatal(message)
255
+ Beskar::Logger.fatal(message, component: self)
256
+ end
257
+ end
258
+
259
+ # Module to include in classes for instance-level logging
260
+ module InstanceMethods
261
+ # Log a debug message with automatic component detection
262
+ def log_debug(message)
263
+ Beskar::Logger.debug(message, component: self.class)
264
+ end
265
+
266
+ # Log an info message with automatic component detection
267
+ def log_info(message)
268
+ Beskar::Logger.info(message, component: self.class)
269
+ end
270
+
271
+ # Log a warning message with automatic component detection
272
+ def log_warn(message)
273
+ Beskar::Logger.warn(message, component: self.class)
274
+ end
275
+
276
+ # Log an error message with automatic component detection
277
+ def log_error(message)
278
+ Beskar::Logger.error(message, component: self.class)
279
+ end
280
+
281
+ # Log a fatal message with automatic component detection
282
+ def log_fatal(message)
283
+ Beskar::Logger.fatal(message, component: self.class)
284
+ end
285
+ end
286
+
287
+ # Convenience method to include both class and instance methods
288
+ def self.included(base)
289
+ base.extend(ClassMethods)
290
+ base.include(InstanceMethods)
291
+ end
292
+ end
293
+ end
@@ -9,76 +9,116 @@ module Beskar
9
9
  request = ActionDispatch::Request.new(env)
10
10
  ip_address = request.ip
11
11
 
12
+ Beskar::Logger.debug("[RequestAnalyzer] Processing request from IP: #{ip_address}, Path: #{request.path}", component: :Middleware)
13
+
12
14
  # 1. Check if IP is whitelisted (whitelisted IPs skip blocking but still get logged)
13
15
  is_whitelisted = Beskar::Services::IpWhitelist.whitelisted?(ip_address)
14
16
 
15
- # 2. Check if IP is banned (early exit for blocked IPs, unless whitelisted)
17
+ # 2. Check if IP is banned (early exit for blocked IPs, unless whitelisted or in monitor-only mode)
16
18
  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
+ 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
19
25
  end
20
26
 
21
27
  # 3. Check rate limiting (unless whitelisted)
22
28
  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
29
+ # Auto-block after excessive rate limiting violations (even in monitor-only mode - we create the ban record)
26
30
  if should_auto_block_rate_limit?(ip_address)
27
31
  Beskar::BannedIp.ban!(
28
32
  ip_address,
29
- reason: 'rate_limit_abuse',
33
+ reason: "rate_limit_abuse",
30
34
  duration: 1.hour,
31
- details: 'Excessive rate limit violations'
35
+ details: "Excessive rate limit violations"
32
36
  )
33
37
  end
34
-
35
- return rate_limit_response
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
36
45
  end
37
46
 
38
47
  # 4. Check WAF patterns (vulnerability scans)
39
48
  if Beskar.configuration.waf_enabled?
49
+ Beskar::Logger.debug("[RequestAnalyzer] WAF enabled, analyzing request", component: :Middleware)
40
50
  waf_analysis = Beskar::Services::Waf.analyze_request(request)
41
-
51
+
42
52
  if waf_analysis
53
+ Beskar::Logger.debug("[RequestAnalyzer] WAF detected threat: #{waf_analysis[:patterns].map { |p| p[:description] }.join(", ")}", component: :Middleware)
43
54
  # Log the violation (and create security event if configured)
44
55
  # 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
-
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
+
47
59
  # Log even for whitelisted IPs (but don't block)
48
60
  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
- )
61
+ Beskar::Logger.info("WAF violation from whitelisted IP #{ip_address} " \
62
+ "(not blocking): #{waf_analysis[:patterns].map { |p| p[:description] }.join(", ")}", component: :Middleware)
53
63
  else
54
64
  # Check if we should block
55
65
  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
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)
59
70
  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
- )
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)
65
74
  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
- )
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)
72
79
  # Block already handled by WAF.record_violation auto-block logic
73
80
  # But we return 403 immediately
74
81
  return blocked_response("Access denied due to suspicious activity.")
75
82
  end
76
83
  end
84
+ else
85
+ Beskar::Logger.debug("[RequestAnalyzer] No WAF threat detected for path: #{request.path}", component: :Middleware)
77
86
  end
87
+ else
88
+ Beskar::Logger.debug("[RequestAnalyzer] WAF is disabled", component: :Middleware)
78
89
  end
79
90
 
80
91
  # 5. Process the request normally (will raise 404 if route not found)
92
+ Beskar::Logger.debug("[RequestAnalyzer] Passing request to application", component: :Middleware)
81
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
82
122
  rescue ActionController::RoutingError => e
83
123
  # If WAF is enabled, log 404s as potential scanning attempts
84
124
  if Beskar.configuration.waf_enabled?
@@ -94,7 +134,7 @@ module Beskar
94
134
  # Check both IP rate limit and authentication abuse
95
135
  ip_check = Beskar::Services::RateLimiter.check_ip_rate_limit(request.ip)
96
136
  auth_abused = authentication_brute_force?(request.ip)
97
-
137
+
98
138
  !ip_check[:allowed] || auth_abused
99
139
  end
100
140
 
@@ -104,7 +144,7 @@ module Beskar
104
144
  violations = Rails.cache.read(cache_key) || 0
105
145
  violations += 1
106
146
  Rails.cache.write(cache_key, violations, expires_in: 1.hour)
107
-
147
+
108
148
  # Block after 5 rate limit violations in an hour
109
149
  violations >= 5
110
150
  end
@@ -112,53 +152,88 @@ module Beskar
112
152
  def authentication_brute_force?(ip_address)
113
153
  # Check authentication failure count from RateLimiter
114
154
  cache_key = "beskar:ip_auth_failures:#{ip_address}"
115
-
155
+
116
156
  # Get current failure count and timestamp
117
157
  failure_data = Rails.cache.read(cache_key)
118
158
  return false unless failure_data.is_a?(Hash)
119
-
159
+
120
160
  # Count recent failures (within the configured period)
121
161
  config = Beskar.configuration.rate_limiting[:ip_attempts] || {}
122
162
  period = config[:period] || 1.hour
123
163
  limit = config[:limit] || 10
124
-
164
+
125
165
  now = Time.current.to_i
126
166
  recent_failures = failure_data.select { |timestamp, _| now - timestamp.to_i < period.to_i }
127
-
167
+
128
168
  # If too many auth failures, it's brute force
129
169
  if recent_failures.length >= limit
130
- # Auto-ban for authentication abuse
170
+ # Auto-ban for authentication abuse (create ban record even in monitor-only mode)
131
171
  Beskar::BannedIp.ban!(
132
172
  ip_address,
133
- reason: 'authentication_abuse',
173
+ reason: "authentication_abuse",
134
174
  duration: 1.hour,
135
175
  details: "#{recent_failures.length} failed authentication attempts in #{period / 60} minutes",
136
- metadata: { failure_count: recent_failures.length, detection_time: Time.current }
176
+ metadata: {failure_count: recent_failures.length, detection_time: Time.current}
137
177
  )
138
-
139
- Rails.logger.warn(
140
- "[Beskar::Middleware] 🔒 Auto-blocked IP #{ip_address} " \
141
- "for authentication brute force (#{recent_failures.length} failures)"
142
- )
143
-
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
+
144
187
  return true
145
188
  end
146
-
189
+
147
190
  false
148
191
  end
149
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
+
150
227
  def log_404_for_waf(request, error)
151
228
  # 404s on suspicious paths might indicate scanning
152
229
  path = request.fullpath || request.path
153
-
230
+
154
231
  # Only log if it matches WAF patterns (already analyzed in analyze_request)
155
232
  waf_analysis = Beskar::Services::Waf.analyze_request(request)
156
-
233
+
157
234
  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
- )
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)
162
237
  end
163
238
  end
164
239