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
@@ -0,0 +1,60 @@
1
+ require "uri"
2
+
3
+ module Beskar
4
+ module Services
5
+ # Matching input only. Never copy these raw/canonical paths or query values
6
+ # into audit records, state, or logs; those contain rule identifiers instead.
7
+ class WafRequest
8
+ MAX_BYTES = 8192
9
+ attr_reader :path, :problem, :method, :decoding_passes
10
+
11
+ def initialize(request)
12
+ @request = request
13
+ @method = request.respond_to?(:request_method) ? request.request_method.to_s.upcase : "GET"
14
+ @method = "OTHER" unless %w[GET HEAD POST PUT PATCH DELETE OPTIONS CONNECT TRACE].include?(@method)
15
+ raw = request.path.to_s
16
+ @decoding_passes = 0
17
+ if raw.bytesize > MAX_BYTES
18
+ @path, @problem = "", :oversized_path
19
+ return
20
+ end
21
+ @path = raw.b
22
+ @problem = :invalid_encoding if @path.match?(/%(?![0-9a-f]{2})/i)
23
+ 2.times do
24
+ break unless @path.match?(/%[0-9a-f]{2}/i)
25
+ @path = @path.gsub(/%([0-9a-f]{2})/i) { [$1.to_i(16)].pack("C") }
26
+ @decoding_passes += 1
27
+ end
28
+ @problem ||= :excessive_encoding if @path.match?(/%[0-9a-f]{2}/i)
29
+ @path = @path.tr("\\", "/").force_encoding(Encoding::UTF_8)
30
+ if !@path.valid_encoding? || @path.match?(/[[:cntrl:]]/)
31
+ @problem = :invalid_encoding
32
+ @path = ""
33
+ end
34
+ end
35
+
36
+ def suspicious_format?
37
+ return false unless @request.respond_to?(:query_string)
38
+ query = @request.query_string.to_s
39
+ return false if query.bytesize > MAX_BYTES
40
+ URI.decode_www_form(query).any? do |key, value|
41
+ key == "format" && value.match?(/\A(?:exe|bat|cmd|com|scr|vbs|jar|asp|aspx|jsp|php)\z/i)
42
+ end
43
+ rescue ArgumentError
44
+ false
45
+ end
46
+
47
+ def excluded?(category)
48
+ Array((Beskar.configuration.waf || {})[:request_exclusions]).any? do |rule|
49
+ next false unless rule.is_a?(Hash)
50
+ rule = rule.symbolize_keys
51
+ next false unless rule[:path].is_a?(Regexp)
52
+ methods = Array(rule[:methods]).map { |value| value.to_s.upcase }
53
+ categories = Array(rule[:categories]).map(&:to_s)
54
+ (methods.empty? || methods.include?(method)) &&
55
+ (categories.empty? || categories.include?(category.to_s)) && rule[:path].match?(path)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,3 +1,3 @@
1
1
  module Beskar
2
- VERSION = "0.0.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,53 @@
1
+ module Beskar
2
+ # Wrap strategy execution, not before_failure (which also runs on anonymous
3
+ # page visits). Database passwords retain their identity-aware admission hook.
4
+ module WardenStrategyAdmission
5
+ def _run!
6
+ return super if defined?(Devise::Strategies::DatabaseAuthenticatable) && is_a?(Devise::Strategies::DatabaseAuthenticatable)
7
+ model = Beskar.configuration.model_class_for_scope(scope)
8
+ return super unless model&.respond_to?(:track_failed_authentication)
9
+
10
+ request = ActionDispatch::Request.new(env)
11
+ attempt = Services::AuthenticationAttempt.reserve(request, model: model, scope: scope, cache: true)
12
+ unless attempt.allowed?
13
+ model.track_failed_authentication(request, scope, attempt: attempt)
14
+ custom!(attempt.response)
15
+ return self
16
+ end
17
+ super
18
+ rescue Services::AuthenticationAttempt::Unavailable, ActiveRecord::ActiveRecordError
19
+ custom!(Services::AuthenticationAttempt.unavailable_response)
20
+ self
21
+ end
22
+ end
23
+
24
+ # Enforce even if a host uses set_user(..., run_callbacks: false). Includes
25
+ # OAuth sign_in, non-password strategies, stateless Warden and session fetch.
26
+ module WardenSessionAdmission
27
+ def set_user(user, opts = {})
28
+ return super unless user.respond_to?(:track_authentication_event)
29
+ scope = opts[:scope] || config.default_scope
30
+ request = ActionDispatch::Request.new(env)
31
+ if opts[:event] == :fetch
32
+ allowed = Services::SessionRevocation.allowed?(user, request: request,
33
+ token: Services::SessionRevocation.token(user))
34
+ else
35
+ attempt = Services::AuthenticationAttempt.current(request, scope)
36
+ attempt ||= Services::AuthenticationAttempt.reserve(request, model: user.class,
37
+ scope: scope, user: user, cache: true)
38
+ user.track_authentication_event(request, :success, attempt: attempt, persist: false)
39
+ attempt.verify_generation!
40
+ attempt.persist_outcome!
41
+ allowed = attempt.allowed?
42
+ end
43
+ unless allowed
44
+ logout(scope)
45
+ throw :warden, scope: scope, message: :authentication_denied
46
+ end
47
+ super
48
+ rescue Services::AuthenticationAttempt::Unavailable, ActiveRecord::ActiveRecordError
49
+ logout(scope)
50
+ raise Services::AuthenticationAttempt::Unavailable, "Authentication temporarily unavailable"
51
+ end
52
+ end
53
+ end
data/lib/beskar.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  require "beskar/version"
2
2
  require "beskar/configuration"
3
+ require "beskar/configuration_validator"
4
+ require "beskar/logger"
5
+ require "beskar/risk_level"
3
6
  require "beskar/middleware"
4
7
  require "beskar/middleware/request_analyzer"
5
8
  require "beskar/models/security_trackable_generic"
@@ -9,19 +12,66 @@ require "beskar/models/security_trackable"
9
12
  require "beskar/services/rate_limiter"
10
13
  require "beskar/services/device_detector"
11
14
  require "beskar/services/geolocation_service"
15
+ require "beskar/services/location_assessment"
16
+ require "beskar/services/risk_assessment"
12
17
  require "beskar/services/account_locker"
13
18
  require "beskar/services/ip_whitelist"
14
19
  require "beskar/services/waf"
20
+ require "beskar/services/waf_request"
21
+ require "beskar/services/audit_data"
22
+ require "beskar/services/event_search"
23
+ require "beskar/services/request_context"
24
+ require "beskar/services/authentication_attempt"
25
+ require "beskar/services/authentication"
26
+ require "beskar/services/native_account_lock"
27
+ require "beskar/services/session_revocation"
28
+ require "beskar/services/notifications"
29
+ require "beskar/services/administrative_bans"
30
+ require "beskar/services/administrative_audit"
31
+ require "beskar/services/ban_expiry"
32
+ require "beskar/devise_authentication"
33
+ require "beskar/warden_authentication"
15
34
  require "beskar/engine"
16
35
 
17
36
  module Beskar
18
37
  class << self
19
- attr_accessor :configuration
38
+ def configuration=(value)
39
+ raise Configuration::Error, "Use audited Beskar.configure for runtime changes" if configuration.frozen?
40
+ @configuration = value
41
+ end
20
42
  end
21
43
 
22
- def self.configure
23
- self.configuration ||= Configuration.new
24
- yield(configuration)
44
+ CONFIGURATION_MUTEX = Mutex.new
45
+
46
+ def self.configure(actor: nil, reason: nil, request_id: nil)
47
+ CONFIGURATION_MUTEX.synchronize do
48
+ original = configuration
49
+ if original.frozen?
50
+ allowed = original.authorize_configuration&.call(actor) == true
51
+ raise Configuration::Error, "Runtime configuration change is not authorized" unless allowed
52
+ raise Configuration::Error, "Runtime configuration cannot run in a database transaction" if AdministrativeAction.connection.transaction_open?
53
+ end
54
+ candidate = original.dup
55
+ yield(candidate)
56
+ candidate.validate!(resolve_jobs: !!Rails.application&.initialized?)
57
+ published = candidate.dup
58
+ if original.frozen?
59
+ published.seal!
60
+ Services::AdministrativeAudit.record!(actor: actor, reason: reason, request_id: request_id,
61
+ action: "configuration_changed", target_type: "Configuration",
62
+ before_state: Services::AdministrativeAudit.configuration_snapshot(original),
63
+ after_state: Services::AdministrativeAudit.configuration_snapshot(published).merge(
64
+ "changed_settings" => original.instance_variables.filter_map do |name|
65
+ name.to_s.delete_prefix("@") unless original.instance_variable_get(name) == published.instance_variable_get(name)
66
+ end, "process_id" => Process.pid
67
+ ))
68
+ end
69
+ @configuration = published
70
+ end
71
+ end
72
+
73
+ def self.configuration
74
+ @configuration ||= Configuration.new
25
75
  end
26
76
 
27
77
  # Convenience method to access the rate limiter
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record/migration"
5
+
6
+ module Beskar
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Creates a Beskar initializer, mounts the dashboard, and copies migrations"
14
+
15
+ def copy_initializer
16
+ template "initializer.rb.tt", "config/initializers/beskar.rb"
17
+ end
18
+
19
+ def mount_engine
20
+ route_text = "mount Beskar::Engine => '/beskar'"
21
+
22
+ # Check if the route already exists
23
+ routes_path = File.join(destination_root, "config/routes.rb")
24
+ if !File.exist?(routes_path)
25
+ say "No config/routes.rb found; mount Beskar::Engine manually at /beskar.", :yellow
26
+ elsif File.read(routes_path).match?(/\bmount\s+Beskar::Engine\b/)
27
+ say "Route already mounted, skipping...", :yellow
28
+ else
29
+ route route_text
30
+ say "Mounted Beskar engine at /beskar", :green
31
+ end
32
+ end
33
+
34
+ def copy_migrations
35
+ # Copy migrations from the engine to the host app
36
+ migration_source = File.expand_path("../../../../db/migrate", __dir__)
37
+
38
+ if Dir.exist?(migration_source)
39
+ Dir.glob("#{migration_source}/*.rb").each do |migration|
40
+ migration_name = File.basename(migration).sub(/^\d+_/, "")
41
+
42
+ # Check if migration already exists
43
+ if migration_already_exists?(migration_name)
44
+ say "Migration #{migration_name} already exists, skipping...", :yellow
45
+ else
46
+ migration_template migration, "db/migrate/#{migration_name}"
47
+ end
48
+ end
49
+ end
50
+
51
+ say "Migrations copied. Run 'rails db:migrate' to create the tables.", :green
52
+ end
53
+
54
+ # CSS Zero is no longer required - styles are embedded in the dashboard
55
+ # def install_css_zero
56
+ # # Removed - dashboard now uses embedded styles
57
+ # end
58
+
59
+ def show_readme
60
+ readme_content = <<~README
61
+
62
+ ===============================================================================
63
+ 🛡️ Beskar Installation Complete!
64
+ ===============================================================================
65
+
66
+ Next steps:
67
+
68
+ 1. Run migrations to create the security tables:
69
+ $ rails db:migrate
70
+
71
+ 2. Configure authentication for the dashboard in config/initializers/beskar.rb
72
+
73
+ Uncomment one complete example in the generated initializer:
74
+ - Rails built-in authentication: signed session_id cookie and Session model.
75
+ - Devise: Warden authentication with your app's Devise mapping scope.
76
+
77
+ Each example includes authenticate_admin, authorize_admin, and audit_actor.
78
+ Adapt admin? and beskar_permissions to your application's roles/policies.
79
+ Permissions: read, manage_bans, export, read_audit. Missing grants deny access.
80
+ Beskar's controller does not inherit your host authentication helpers.
81
+ See docs/guides/authentication.md and docs/guides/audit-lifecycle.md.
82
+ Without audit_actor, separately authorized reads work but writes/exports return 503.
83
+
84
+ 3. Add Beskar concerns to your User model (or authentication model):
85
+
86
+ For Rails built-in authentication:
87
+ class User < ApplicationRecord
88
+ include Beskar::Models::SecurityTrackableAuthenticable
89
+ end
90
+ Also integrate login admission and session guards from docs/guides/authentication.md.
91
+
92
+ For Devise:
93
+ class User < ApplicationRecord
94
+ include Beskar::Models::SecurityTrackable
95
+ include Beskar::Models::SecurityTrackableDevise
96
+ end
97
+
98
+ 4. Access the security dashboard at:
99
+ http://localhost:3000/beskar
100
+
101
+ 5. Optional: Configure additional settings in config/initializers/beskar.rb
102
+ - IP whitelist
103
+ - WAF rules
104
+ - Rate limiting
105
+ - Geolocation
106
+ - Risk-based locking
107
+
108
+ ===============================================================================
109
+ 📚 Documentation
110
+ ===============================================================================
111
+
112
+ Documentation: https://github.com/humadroid-io/beskar/blob/master/docs/README.md
113
+ Dashboard Guide: https://github.com/humadroid-io/beskar/blob/master/docs/guides/dashboard-and-search.md
114
+ Configuration: https://github.com/humadroid-io/beskar/blob/master/docs/guides/configuration.md
115
+
116
+ ===============================================================================
117
+ ⚠️ Important for Production
118
+ ===============================================================================
119
+
120
+ 1. ALWAYS configure authentication for the dashboard
121
+ 2. Set monitor_only = false when ready to block threats
122
+ 3. Configure your IP whitelist to prevent locking yourself out
123
+ 4. Run the copied migrations; required indexes are included.
124
+ 5. Schedule Beskar::SecurityState.cleanup_expired! to reclaim expired state.
125
+
126
+ ===============================================================================
127
+ 💡 Quick Tips
128
+ ===============================================================================
129
+
130
+ - Start with monitor_only = true to observe without blocking
131
+ - Use the dashboard to review security events before enabling blocking
132
+ - Configure email notifications for high-risk events
133
+ - Export security data regularly for analysis
134
+ - Consider implementing custom risk scoring for your use case
135
+
136
+ ===============================================================================
137
+
138
+ README
139
+
140
+ say readme_content, :green
141
+ end
142
+
143
+ private
144
+
145
+ def migration_already_exists?(migration_name)
146
+ basename = migration_name.delete_suffix(".rb")
147
+ Dir.glob(File.join(destination_root, "db/migrate/*_#{basename}{,.beskar}.rb")).any?
148
+ end
149
+
150
+ def migration_template(source, destination)
151
+ migration_number = self.class.next_migration_number(File.join(destination_root, File.dirname(destination)))
152
+ file_name = File.join(File.dirname(destination), "#{migration_number}_#{File.basename(destination)}")
153
+
154
+ copy_file source, file_name
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ Beskar.configure do |config|
4
+ # ============================================================================
5
+ # DASHBOARD AUTHENTICATION (REQUIRED)
6
+ # ============================================================================
7
+ # Configure how to authenticate access to the Beskar security dashboard.
8
+ # This is REQUIRED for all environments.
9
+ #
10
+ # The authenticate_admin callback receives the request object and is executed
11
+ # in the controller context, giving you access to all controller methods
12
+ # (cookies, session, authenticate_or_request_with_http_basic, etc.).
13
+ # The block should return truthy value to allow access, falsey to deny.
14
+
15
+ # Choose one authentication option below. Options 1 and 2 include all three
16
+ # callbacks: authentication, permissions, and the operator recorded in audits.
17
+ # admin? and beskar_permissions are application-defined methods, not Rails or
18
+ # Devise defaults. Adapt them to your roles/policies. Permission names are:
19
+ # read, manage_bans, export, read_audit (beskar_permissions returns strings).
20
+ #
21
+ # Option 1: Rails built-in authentication (Rails 8 authentication generator)
22
+ # Beskar does not inherit your ApplicationController or Authentication concern,
23
+ # so Current.session/current_user may not be populated on dashboard requests.
24
+ # Resolve the host's Session from its signed cookie instead. Adapt the model
25
+ # and cookie name if you customized Rails' generated authentication.
26
+ # First add this to app/models/user.rb (not inside this initializer):
27
+ # include Beskar::Models::SecurityTrackableAuthenticable
28
+ # This supplies beskar_access_allowed? for the revocation guard below.
29
+ # Beskar::Models::SecurityTrackable is Devise-specific and is not a substitute;
30
+ # without the native concern, the guard raises NoMethodError on the user.
31
+ # See docs/guides/authentication.md for the full integration.
32
+ # config.authenticate_admin = ->(request) do
33
+ # @beskar_admin_user = nil
34
+ # session_id = cookies.signed[:session_id]
35
+ # auth_session = ::Session.find_by(id: session_id) if session_id
36
+ # if Beskar::Services::SessionRevocation.native_session_allowed?(auth_session, request: request)
37
+ # @beskar_admin_user = auth_session.user
38
+ # end
39
+ # @beskar_admin_user&.admin?
40
+ # end
41
+ # config.authorize_admin = ->(_request, permission) do
42
+ # @beskar_admin_user&.admin? && @beskar_admin_user.beskar_permissions.include?(permission.to_s)
43
+ # end
44
+ # config.audit_actor = ->(_request) do
45
+ # "User:#{@beskar_admin_user.id}" if @beskar_admin_user&.admin?
46
+ # end
47
+
48
+ # Option 2: Devise (Warden-backed; does not use the Rails Session model)
49
+ # :user is the Devise mapping scope, not a role. Change it to :admin only if
50
+ # your app has a separate Devise Admin model/mapping.
51
+ # authenticate resumes the Warden session or tries configured strategies
52
+ # (including remember-me); user reads the resulting authenticated identity.
53
+ # config.authenticate_admin = ->(request) do
54
+ # user = request.env['warden']&.authenticate(scope: :user)
55
+ # user&.admin?
56
+ # end
57
+ # config.authorize_admin = ->(request, permission) do
58
+ # user = request.env['warden']&.user(scope: :user)
59
+ # user&.admin? && user.beskar_permissions.include?(permission.to_s)
60
+ # end
61
+ # config.audit_actor = ->(request) do
62
+ # user = request.env['warden']&.user(scope: :user)
63
+ # "User:#{user.id}" if user&.admin?
64
+ # end
65
+
66
+ # Option 3: HTTP Basic Authentication (uses controller method)
67
+ # config.authenticate_admin = ->(request) do
68
+ # authenticate_or_request_with_http_basic("Beskar Admin") do |username, password|
69
+ # Beskar::Services::RequestContext.secure_match?(username, ENV['BESKAR_ADMIN_USERNAME']) &&
70
+ # Beskar::Services::RequestContext.secure_match?(password, ENV['BESKAR_ADMIN_PASSWORD'])
71
+ # end
72
+ # end
73
+
74
+ # Option 4: Token-based authentication
75
+ # config.authenticate_admin = ->(request) do
76
+ # token = ENV['BESKAR_ADMIN_TOKEN']
77
+ # token.present? && Beskar::Services::RequestContext.secure_match?(request.headers['Authorization'], "Bearer #{token}")
78
+ # end
79
+
80
+ # Option 5: Cookie-based authentication (uses controller cookies)
81
+ # config.authenticate_admin = ->(request) do
82
+ # Beskar::Services::RequestContext.secure_match?(cookies.signed[:admin_token], ENV['BESKAR_ADMIN_TOKEN'])
83
+ # end
84
+
85
+ # Option 6: Using CanCanCan (requires policy helpers on Beskar's controller)
86
+ # config.authenticate_admin = ->(request) do
87
+ # authorize! :manage, :beskar_dashboard
88
+ # end
89
+
90
+ # Option 7: Using Pundit (requires policy helpers on Beskar's controller)
91
+ # config.authenticate_admin = ->(request) do
92
+ # authorize :beskar_dashboard, :access?
93
+ # end
94
+
95
+ # Option 8: For development/testing ONLY (NOT for production!)
96
+ # config.authenticate_admin = ->(request) do
97
+ # Rails.env.development? || Rails.env.test?
98
+ # end
99
+
100
+ # REQUIRED: grant permissions separately from authentication (nil denies all).
101
+ # Options 1 and 2 show complete examples. For other options, supply your own
102
+ # authorize_admin callback returning true for each permitted operation.
103
+
104
+ # REQUIRED FOR DASHBOARD WRITES AND EXPORTS: resolve an authenticated operator.
105
+ # Use the same trusted identity used by authenticate_admin (as above).
106
+ # Never derive this from request params, emails, passwords, or tokens.
107
+ # Without audit_actor, separately authorized reads work; writes/exports return 503.
108
+ # Forms require a reason and changes require transactional history.
109
+ # Apply the administrative-action migration; see docs/guides/audit-lifecycle.md.
110
+
111
+ # ============================================================================
112
+ # MONITOR-ONLY MODE
113
+ # ============================================================================
114
+ # When enabled, Beskar will log all security events but won't actually block
115
+ # any requests. Useful for testing or initial deployment.
116
+ config.monitor_only = true
117
+
118
+ # ============================================================================
119
+ # IP WHITELIST
120
+ # ============================================================================
121
+ # IPs that should never be blocked (your office, monitoring services, etc.)
122
+ config.ip_whitelist = [
123
+ # '192.168.1.0/24', # Local network
124
+ # '10.0.0.0/8', # Private network
125
+ # '127.0.0.1', # Localhost
126
+ ]
127
+
128
+ # ============================================================================
129
+ # WAF (WEB APPLICATION FIREWALL) - Score-Based Blocking
130
+ # ============================================================================
131
+ # Enable scanner-path detection (not general SQL injection/XSS filtering).
132
+ # Uses score-based blocking with exponential decay for intelligent threat detection.
133
+ config.waf[:enabled] = true
134
+
135
+ # Optionally customize WAF settings (these are the defaults):
136
+ # config.waf[:auto_block] = true # Automatically block IPs after threshold
137
+ # config.waf[:score_threshold] = 150 # Cumulative risk score before blocking
138
+ # config.waf[:violation_window] = 6.hours # Maximum time window to track violations
139
+ # config.waf[:block_durations] = [1.hour, 6.hours, 24.hours, 7.days] # Escalating durations
140
+ # config.waf[:permanent_block_after] = 500 # Permanent block when cumulative score reaches this
141
+ # config.waf[:create_security_events] = true # Create SecurityEvent records
142
+ # config.waf[:exception_detection] = :suspicious # Ordinary Rails errors need scanner evidence
143
+ # config.waf[:request_exclusions] = [
144
+ # {path: %r{\A/wp-content/}, methods: ["GET", "HEAD"], categories: [:wordpress_static]}
145
+ # ]
146
+ # WAF evidence excludes raw URLs and exception messages. Audit fields honor
147
+ # Rails filter_parameters. See docs/guides/audit-and-waf.md before opting into :all errors.
148
+ #
149
+ # === Exponential Decay Configuration ===
150
+ # Violations decay over time based on severity (reduces false positives)
151
+ # config.waf[:decay_enabled] = true
152
+ # config.waf[:decay_rates] = { # Half-life in minutes
153
+ # critical: 360, # 6 hour half-life (config files, path traversal)
154
+ # high: 120, # 2 hour half-life (WordPress, PHP admin scans)
155
+ # medium: 45, # 45 minute half-life (unknown formats)
156
+ # low: 15 # 15 minute half-life (RecordNotFound/404s)
157
+ # }
158
+ # config.waf[:max_violations_tracked] = 50 # Maximum violations to track per IP
159
+ #
160
+ # === RecordNotFound Exclusions ===
161
+ # Exclude legitimate 404-prone paths from triggering violations
162
+ # config.waf[:record_not_found_exclusions] = [
163
+ # %r{/posts/.*}, # Blog posts with slugs
164
+ # %r{/products/[\w-]+}, # Product URLs with slugs
165
+ # %r{/public/.*} # Public content
166
+ # ]
167
+ #
168
+ # === Illustrative thresholds ===
169
+ # See docs/guides/audit-and-waf.md for matching rules and tuning limitations:
170
+ # - STRICT: score_threshold = 100 (high-security)
171
+ # - BALANCED: score_threshold = 150 (recommended default)
172
+ # - PERMISSIVE: score_threshold = 200 (high-traffic sites)
173
+
174
+ # ============================================================================
175
+ # SECURITY TRACKING
176
+ # ============================================================================
177
+ # Security tracking is enabled by default. To disable or customize:
178
+ # config.security_tracking[:enabled] = false
179
+ # config.security_tracking[:track_successful_logins] = true
180
+ # config.security_tracking[:track_failed_logins] = true
181
+ # No background analyzer is built in. Opt in with your own Active Job:
182
+ # config.security_tracking[:analysis_job] = "SecurityReviewJob"
183
+ # config.security_tracking[:auto_analyze_patterns] = true
184
+ # The job receives user_type:, user_id:, event_type: after the outer commit.
185
+ # See docs/guides/configuration.md for the contract and queue/delivery limitations.
186
+
187
+ # ============================================================================
188
+ # RATE LIMITING
189
+ # ============================================================================
190
+ # Rate limiting is configured by default. To customize:
191
+ # config.rate_limiting[:ip_attempts][:limit] = 10
192
+ # config.rate_limiting[:ip_attempts][:period] = 1.hour
193
+ # config.rate_limiting[:ip_attempts][:exponential_backoff] = true
194
+ #
195
+ # config.rate_limiting[:account_attempts][:limit] = 5
196
+ # config.rate_limiting[:account_attempts][:period] = 15.minutes
197
+ # config.rate_limiting[:account_attempts][:exponential_backoff] = true
198
+ #
199
+ # config.rate_limiting[:global_attempts][:enabled] = false # Shared budget is opt-in
200
+ # config.rate_limiting[:ip_attempts][:block_requests] = false # Keep login quotas off unrelated requests
201
+ # config.rate_limiting[:global_attempts][:limit] = 100
202
+ # config.rate_limiting[:global_attempts][:period] = 1.minute
203
+ # config.rate_limiting[:global_attempts][:exponential_backoff] = false
204
+
205
+ # ============================================================================
206
+ # RISK-BASED ACCOUNT LOCKING
207
+ # ============================================================================
208
+ # Risk-based locking is disabled by default. To enable:
209
+ # Review docs/guides/risk-scoring.md: factors are heuristics; repeated IPs do not establish trust.
210
+ # config.risk_based_locking[:enabled] = true
211
+ # Confirmed locks always reject sign-in and revoke old sessions, including with legacy immediate_signout: false.
212
+ # config.risk_based_locking[:risk_threshold] = 75 # Risk score threshold for locking
213
+ # config.risk_based_locking[:lock_strategy] = :devise_lockable # :rails_auth or :none also supported; :custom is rejected
214
+ # config.risk_based_locking[:auto_unlock_time] = 1.hour # Native locks only; nil requires manual unlock
215
+ # Devise owns unlock_strategy/unlock_in. Configure those through Devise.
216
+ # Rails-native controllers/session readers need the guards in docs/guides/authentication.md.
217
+ # config.risk_based_locking[:notify_user] = true # Opt-in email; configure notifications below first
218
+ # config.risk_based_locking[:log_lock_events] = true # Log lock events
219
+
220
+ # ============================================================================
221
+ # GEOLOCATION
222
+ # ============================================================================
223
+ # Geolocation uses a mock provider by default. To use MaxMind:
224
+ # Mock locations are synthetic and never trigger travel/country risk.
225
+ # 1. Download GeoLite2-City database from maxmind.com
226
+ # 2. Place it in config/GeoLite2-City.mmdb
227
+ # 3. Configure:
228
+ # config.geolocation[:provider] = :maxmind
229
+ # config.geolocation[:maxmind_city_db_path] = Rails.root.join('config', 'GeoLite2-City.mmdb').to_s
230
+ # config.geolocation[:cache_ttl] = 4.hours
231
+
232
+ # ============================================================================
233
+ # AUTHENTICATION MODELS
234
+ # ============================================================================
235
+ # Authentication models are auto-detected by default. To customize:
236
+ # config.authentication_models[:auto_detect] = false # Disable auto-detection
237
+ # config.authentication_models[:devise] = [:user, :admin]
238
+ # config.authentication_models[:rails_auth] = [:customer]
239
+
240
+ # ============================================================================
241
+ # EMERGENCY PASSWORD RESET
242
+ # ============================================================================
243
+ # Emergency password reset is disabled by default. To enable:
244
+ # config.emergency_password_reset[:enabled] = true
245
+ # config.emergency_password_reset[:impossible_travel_threshold] = 3
246
+ # config.emergency_password_reset[:suspicious_device_threshold] = 5
247
+ # config.emergency_password_reset[:total_locks_threshold] = 5
248
+ # config.emergency_password_reset[:send_notification] = true # Default false; requires notification settings
249
+ # config.emergency_password_reset[:notify_security_team] = true # Default false; requires team recipients
250
+ # config.emergency_password_reset[:require_manual_unlock] = false
251
+
252
+ # ============================================================================
253
+ # NOTIFICATIONS (OPT-IN)
254
+ # ============================================================================
255
+ # config.notifications[:from] = "security@your-domain.example"
256
+ # config.notifications[:recovery_url] = "https://your-domain.example/account-recovery"
257
+ # config.notifications[:security_team_recipients] = ["security-team@your-domain.example"]
258
+ # Replace these values, configure Action Mailer, and run an Active Job worker
259
+ # consuming beskar_notifications. Recovery URLs are entry pages, never tokens.
260
+ # See docs/guides/notifications-and-recovery.md for retries and host recovery requirements.
261
+ end