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
data/lib/beskar/engine.rb CHANGED
@@ -6,105 +6,38 @@ module Beskar
6
6
  app.config.middleware.use ::Beskar::Middleware::RequestAnalyzer
7
7
  end
8
8
 
9
- # Preload banned IPs into cache on startup
10
- config.after_initialize do
11
- if defined?(Beskar::BannedIp)
12
- Rails.application.executor.wrap do
13
- Beskar::BannedIp.preload_cache!
14
- Rails.logger.info "[Beskar] Preloaded banned IPs into cache"
15
- rescue => e
16
- Rails.logger.warn "[Beskar] Failed to preload banned IPs: #{e.message}"
17
- end
9
+ initializer "beskar.warden_callbacks", after: :load_config_initializers do
10
+ if defined?(Devise)
11
+ require "devise/strategies/database_authenticatable"
12
+ Devise::Strategies::DatabaseAuthenticatable.prepend(Beskar::DeviseAuthentication)
18
13
  end
19
- end
20
14
 
21
- initializer "beskar.warden_callbacks", after: :load_config_initializers do |app|
22
15
  if defined?(Warden)
23
- # Track successful authentication and check for high-risk locks
24
- Warden::Manager.after_set_user except: :fetch do |user, auth, opts|
25
- # Only proceed if Beskar security tracking is available and enabled
26
- if user.respond_to?(:track_authentication_event) && auth.request
27
- # Track the authentication event (creates security event)
28
- security_event = user.track_authentication_event(auth.request, :success)
29
-
30
- # Check if account was locked due to high risk (only if immediate_signout is enabled)
31
- # This happens AFTER successful authentication but BEFORE the request completes
32
- # Requires :lockable module to be enabled on the user model
33
- if Beskar.configuration.immediate_signout? &&
34
- Beskar.configuration.risk_based_locking_enabled? &&
35
- security_event &&
36
- user_was_just_locked?(user, security_event) &&
37
- user.respond_to?(:access_locked?) && user.access_locked?
38
- Rails.logger.warn "[Beskar] Signing out user #{user.id} due to high-risk lock"
39
- auth.logout
40
- throw :warden, scope: opts[:scope], message: :account_locked_due_to_high_risk
41
- end
42
- end
43
- end
44
-
45
- # Alternative approach using after_authentication is available but not enabled by default
46
- # Uncomment this to use the alternative approach (more targeted, only on authentication)
47
- # Warden::Manager.after_authentication do |user, auth, opts|
48
- # if user.respond_to?(:check_high_risk_lock_and_signout)
49
- # user.check_high_risk_lock_and_signout(auth)
50
- # end
51
- # end
16
+ Warden::Strategies::Base.prepend(Beskar::WardenStrategyAdmission)
17
+ Warden::Proxy.prepend(Beskar::WardenSessionAdmission)
52
18
 
53
19
  Warden::Manager.before_failure do |env, opts|
54
- if env
55
- request = ActionDispatch::Request.new(env)
56
- scope = opts[:scope]
57
-
58
- # Try to get model class from configuration
59
- model_class = Beskar.configuration&.model_class_for_scope(scope)
60
-
61
- if model_class && model_class.respond_to?(:track_failed_authentication)
62
- model_class.track_failed_authentication(request, scope)
63
- else
64
- Rails.logger.debug "[Beskar] No trackable model found for scope: #{scope}"
65
- end
66
- end
20
+ next unless env
21
+ request = ActionDispatch::Request.new(env)
22
+ attempt = Services::AuthenticationAttempt.current(request, opts[:scope])
23
+ next unless attempt && !attempt.completed
24
+ model = Beskar.configuration.model_class_for_scope(opts[:scope])
25
+ model.track_failed_authentication(request, opts[:scope], attempt: attempt) if model&.respond_to?(:track_failed_authentication)
67
26
  end
68
27
  end
69
28
  end
70
29
 
71
- # Helper method to check if user was just locked
72
- def self.user_was_just_locked?(user, security_event)
73
- return false unless Beskar.configuration.risk_based_locking_enabled?
74
- return false unless security_event
75
- return false unless user&.respond_to?(:security_events)
76
-
77
- # Check if an account_locked or lock_attempted event was just created
78
- recent_lock = user.security_events
79
- .where(event_type: [ "account_locked", "lock_attempted" ])
80
- .where("created_at >= ?", 10.seconds.ago)
81
- .order(created_at: :desc)
82
- .first
83
-
84
- recent_lock.present?
30
+ initializer "beskar.register_configuration_validation", after: :load_config_initializers do |app|
31
+ # Register after host configuration files, so their after_initialize hooks
32
+ # precede this one. Resolve app/jobs only after the main autoloader is ready.
33
+ app.config.after_initialize { Beskar.configuration.validate!.seal! }
85
34
  end
86
35
 
87
- # Add engine migrations to host app's migration paths
88
- initializer "beskar.append_migrations" do |app|
89
- # Don't add migrations if we're inside the engine itself (testing)
90
- if !root.to_s.include?(app.root.to_s) && !app.root.to_s.include?(root.to_s)
91
- engine_migrations = root.join("db", "migrate").to_s
92
-
93
- # Add to Rails paths
94
- app.config.paths["db/migrate"] << engine_migrations
95
- end
36
+ # Compatibility helper: only the current attempt's actual lock qualifies.
37
+ def self.user_was_just_locked?(user, security_event)
38
+ return false unless Beskar.configuration.risk_based_locking_enabled?
39
+ attempt = security_event&.beskar_attempt
40
+ !!(attempt && attempt.user == user && attempt.locked_now)
96
41
  end
97
-
98
- # Ensure ActiveRecord sees the engine migrations after initialization
99
- # config.after_initialize do |app|
100
- # unless root.to_s.include?(app.root.to_s)
101
- # engine_migrations = root.join("db", "migrate").to_s
102
-
103
- # # Update ActiveRecord::Tasks paths
104
- # current_paths = Array(ActiveRecord::Tasks::DatabaseTasks.migrations_paths)
105
- # current_paths << engine_migrations
106
- # ActiveRecord::Tasks::DatabaseTasks.migrations_paths = current_paths.uniq
107
- # end
108
- # end
109
42
  end
110
43
  end
@@ -0,0 +1,288 @@
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 => e
40
+ # Fallback to stderr if logging fails
41
+ Kernel.warn "[Beskar::Logger] Logging unavailable (#{e.class})"
42
+ end
43
+
44
+ # Configure the logger instance
45
+ #
46
+ # @param logger_instance [Logger, nil] The logger to use, defaults to Rails.logger
47
+ attr_writer :logger
48
+
49
+ # Get the current logger instance
50
+ #
51
+ # @return [Logger] The configured logger or Rails.logger as default
52
+ def logger
53
+ @logger ||= default_logger
54
+ end
55
+
56
+ # Configure log level threshold
57
+ #
58
+ # @param level [Symbol, String] Minimum log level to output
59
+ def level=(level)
60
+ @level = level.to_sym if LOG_LEVELS.include?(level.to_sym)
61
+ end
62
+
63
+ # Get the current log level
64
+ #
65
+ # @return [Symbol] Current log level
66
+ def level
67
+ @level ||= :debug
68
+ end
69
+
70
+ # Reset logger configuration to defaults
71
+ def reset!
72
+ @logger = nil
73
+ @level = nil
74
+ @component_aliases = nil
75
+ end
76
+
77
+ # Configure component name aliases for cleaner output
78
+ #
79
+ # @param aliases [Hash] Mapping of classes/modules to display names
80
+ #
81
+ # @example
82
+ # Beskar::Logger.component_aliases = {
83
+ # 'Beskar::Services::Waf' => 'WAF',
84
+ # 'Beskar::Services::AccountLocker' => 'AccountLocker'
85
+ # }
86
+ attr_writer :component_aliases
87
+
88
+ # Get component aliases
89
+ #
90
+ # @return [Hash] Current component aliases
91
+ def component_aliases
92
+ @component_aliases ||= default_component_aliases
93
+ end
94
+
95
+ private
96
+
97
+ # Format the log message with appropriate prefix
98
+ #
99
+ # @param message [String] The message to format
100
+ # @param component [String, Symbol, Class, nil] Component identifier
101
+ # @return [String] Formatted message with prefix
102
+ def format_message(message, component)
103
+ prefix = build_prefix(component)
104
+ "#{prefix} #{message}"
105
+ end
106
+
107
+ # Build the log prefix based on component
108
+ #
109
+ # @param component [String, Symbol, Class, nil] Component identifier
110
+ # @return [String] Formatted prefix
111
+ def build_prefix(component)
112
+ return "[Beskar]" if component.nil?
113
+
114
+ component_name = normalize_component_name(component)
115
+
116
+ if component_name.nil? || component_name.empty?
117
+ "[Beskar]"
118
+ else
119
+ "[Beskar::#{component_name}]"
120
+ end
121
+ end
122
+
123
+ # Normalize component name from various input types
124
+ #
125
+ # @param component [String, Symbol, Class] Component identifier
126
+ # @return [String, nil] Normalized component name
127
+ def normalize_component_name(component)
128
+ case component
129
+ when String
130
+ apply_component_alias(component)
131
+ when Symbol
132
+ component.to_s
133
+ when Class
134
+ apply_component_alias(component.name)
135
+ when Module
136
+ apply_component_alias(component.name)
137
+ else
138
+ component.to_s
139
+ end
140
+ end
141
+
142
+ # Apply component alias if configured
143
+ #
144
+ # @param component_name [String] Original component name
145
+ # @return [String] Aliased name or original
146
+ def apply_component_alias(component_name)
147
+ return nil if component_name.nil?
148
+
149
+ # First check exact matches
150
+ aliased = component_aliases[component_name]
151
+ return aliased if aliased
152
+
153
+ # Remove Beskar:: prefix if present for lookup
154
+ clean_name = component_name.sub(/^Beskar::/, "")
155
+ aliased = component_aliases[clean_name]
156
+ return aliased if aliased
157
+
158
+ # Check if it's already a simple component name (no ::)
159
+ return clean_name unless clean_name.include?("::")
160
+
161
+ # Extract the last component for nested classes
162
+ # e.g., "Beskar::Services::Waf" -> "Waf"
163
+ last_component = clean_name.split("::").last
164
+ component_aliases[clean_name] || last_component
165
+ end
166
+
167
+ # Check if message should be logged based on current level
168
+ #
169
+ # @param message_level [Symbol] Level of the message
170
+ # @return [Boolean] True if message should be logged
171
+ def should_log?(message_level)
172
+ level_value(message_level) >= level_value(level)
173
+ end
174
+
175
+ # Convert log level to numeric value for comparison
176
+ #
177
+ # @param level_sym [Symbol] Log level
178
+ # @return [Integer] Numeric value
179
+ def level_value(level_sym)
180
+ LOG_LEVELS.index(level_sym) || 0
181
+ end
182
+
183
+ # Get the default logger instance
184
+ #
185
+ # @return [Logger] Default logger (Rails.logger or stdlib Logger)
186
+ def default_logger
187
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
188
+ Rails.logger
189
+ else
190
+ require "logger"
191
+ ::Logger.new($stdout)
192
+ end
193
+ end
194
+
195
+ # Default component name aliases for cleaner output
196
+ #
197
+ # @return [Hash] Default aliases
198
+ def default_component_aliases
199
+ {
200
+ "Beskar::Services::Waf" => "WAF",
201
+ "Beskar::Services::WAF" => "WAF",
202
+ "Services::Waf" => "WAF",
203
+ "Services::WAF" => "WAF",
204
+ "Beskar::Services::AccountLocker" => "AccountLocker",
205
+ "Services::AccountLocker" => "AccountLocker",
206
+ "Beskar::Services::RateLimiter" => "RateLimiter",
207
+ "Services::RateLimiter" => "RateLimiter",
208
+ "Beskar::Services::IpWhitelist" => "IpWhitelist",
209
+ "Services::IpWhitelist" => "IpWhitelist",
210
+ "Beskar::Services::GeolocationService" => "GeolocationService",
211
+ "Services::GeolocationService" => "GeolocationService",
212
+ "Beskar::Services::DeviceDetector" => "DeviceDetector",
213
+ "Services::DeviceDetector" => "DeviceDetector",
214
+ "Beskar::Middleware::RequestAnalyzer" => "Middleware",
215
+ "Middleware::RequestAnalyzer" => "Middleware",
216
+ "Beskar::Models::SecurityTrackableDevise" => "SecurityTracking",
217
+ "Models::SecurityTrackableDevise" => "SecurityTracking",
218
+ "Beskar::Models::SecurityTrackableAuthenticable" => "SecurityTracking",
219
+ "Models::SecurityTrackableAuthenticable" => "SecurityTracking",
220
+ "Beskar::Models::SecurityTrackableGeneric" => "SecurityTracking",
221
+ "Models::SecurityTrackableGeneric" => "SecurityTracking"
222
+ }
223
+ end
224
+ end
225
+
226
+ # Module to include in classes for instance-level logging
227
+ module ClassMethods
228
+ # Log a debug message with automatic component detection
229
+ def log_debug(message)
230
+ Beskar::Logger.debug(message, component: self)
231
+ end
232
+
233
+ # Log an info message with automatic component detection
234
+ def log_info(message)
235
+ Beskar::Logger.info(message, component: self)
236
+ end
237
+
238
+ # Log a warning message with automatic component detection
239
+ def log_warn(message)
240
+ Beskar::Logger.warn(message, component: self)
241
+ end
242
+
243
+ # Log an error message with automatic component detection
244
+ def log_error(message)
245
+ Beskar::Logger.error(message, component: self)
246
+ end
247
+
248
+ # Log a fatal message with automatic component detection
249
+ def log_fatal(message)
250
+ Beskar::Logger.fatal(message, component: self)
251
+ end
252
+ end
253
+
254
+ # Module to include in classes for instance-level logging
255
+ module InstanceMethods
256
+ # Log a debug message with automatic component detection
257
+ def log_debug(message)
258
+ Beskar::Logger.debug(message, component: self.class)
259
+ end
260
+
261
+ # Log an info message with automatic component detection
262
+ def log_info(message)
263
+ Beskar::Logger.info(message, component: self.class)
264
+ end
265
+
266
+ # Log a warning message with automatic component detection
267
+ def log_warn(message)
268
+ Beskar::Logger.warn(message, component: self.class)
269
+ end
270
+
271
+ # Log an error message with automatic component detection
272
+ def log_error(message)
273
+ Beskar::Logger.error(message, component: self.class)
274
+ end
275
+
276
+ # Log a fatal message with automatic component detection
277
+ def log_fatal(message)
278
+ Beskar::Logger.fatal(message, component: self.class)
279
+ end
280
+ end
281
+
282
+ # Convenience method to include both class and instance methods
283
+ def self.included(base)
284
+ base.extend(ClassMethods)
285
+ base.include(InstanceMethods)
286
+ end
287
+ end
288
+ end