beskar 0.1.0 → 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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +150 -19
  3. data/README.md +142 -122
  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 +69 -25
  7. data/app/controllers/beskar/banned_ips_controller.rb +116 -141
  8. data/app/controllers/beskar/dashboard_controller.rb +20 -28
  9. data/app/controllers/beskar/security_events_controller.rb +37 -55
  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 +91 -132
  17. data/app/models/beskar/security_event.rb +37 -4
  18. data/app/models/beskar/security_state.rb +58 -0
  19. data/app/services/beskar/banned_ip_manager.rb +16 -6
  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 +25 -89
  23. data/app/views/beskar/banned_ips/index.html.erb +20 -62
  24. data/app/views/beskar/banned_ips/new.html.erb +18 -138
  25. data/app/views/beskar/banned_ips/review.html.erb +24 -0
  26. data/app/views/beskar/banned_ips/show.html.erb +9 -15
  27. data/app/views/beskar/dashboard/index.html.erb +4 -4
  28. data/app/views/beskar/security_events/index.html.erb +8 -15
  29. data/app/views/beskar/security_events/show.html.erb +6 -20
  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 +9 -76
  33. data/config/routes.rb +10 -21
  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 +84 -13
  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 +30 -35
  58. data/lib/beskar/middleware/request_analyzer.rb +41 -82
  59. data/lib/beskar/models/security_trackable_authenticable.rb +72 -93
  60. data/lib/beskar/models/security_trackable_devise.rb +32 -23
  61. data/lib/beskar/models/security_trackable_generic.rb +169 -212
  62. data/lib/beskar/risk_level.rb +22 -0
  63. data/lib/beskar/services/account_locker.rb +85 -76
  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 +164 -280
  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 +53 -4
  86. data/lib/generators/beskar/install/install_generator.rb +36 -36
  87. data/lib/generators/beskar/install/templates/initializer.rb.tt +104 -20
  88. data/lib/tasks/beskar_tasks.rake +15 -19
  89. metadata +60 -8
@@ -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.1.0"
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,6 +1,8 @@
1
1
  require "beskar/version"
2
2
  require "beskar/configuration"
3
+ require "beskar/configuration_validator"
3
4
  require "beskar/logger"
5
+ require "beskar/risk_level"
4
6
  require "beskar/middleware"
5
7
  require "beskar/middleware/request_analyzer"
6
8
  require "beskar/models/security_trackable_generic"
@@ -10,19 +12,66 @@ require "beskar/models/security_trackable"
10
12
  require "beskar/services/rate_limiter"
11
13
  require "beskar/services/device_detector"
12
14
  require "beskar/services/geolocation_service"
15
+ require "beskar/services/location_assessment"
16
+ require "beskar/services/risk_assessment"
13
17
  require "beskar/services/account_locker"
14
18
  require "beskar/services/ip_whitelist"
15
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"
16
34
  require "beskar/engine"
17
35
 
18
36
  module Beskar
19
37
  class << self
20
- 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
21
42
  end
22
43
 
23
- def self.configure
24
- self.configuration ||= Configuration.new
25
- 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
26
75
  end
27
76
 
28
77
  # Convenience method to access the rate limiter
@@ -1,25 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rails/generators'
4
- require 'rails/generators/migration'
3
+ require "rails/generators"
4
+ require "rails/generators/active_record/migration"
5
5
 
6
6
  module Beskar
7
7
  module Generators
8
8
  class InstallGenerator < Rails::Generators::Base
9
- include Rails::Generators::Migration
9
+ include ActiveRecord::Generators::Migration
10
10
 
11
- source_root File.expand_path('templates', __dir__)
11
+ source_root File.expand_path("templates", __dir__)
12
12
 
13
- desc "Creates a Beskar initializer and runs migrations"
14
-
15
- def self.next_migration_number(path)
16
- if @prev_migration_nr
17
- @prev_migration_nr += 1
18
- else
19
- @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
20
- end
21
- @prev_migration_nr.to_s
22
- end
13
+ desc "Creates a Beskar initializer, mounts the dashboard, and copies migrations"
23
14
 
24
15
  def copy_initializer
25
16
  template "initializer.rb.tt", "config/initializers/beskar.rb"
@@ -29,7 +20,10 @@ module Beskar
29
20
  route_text = "mount Beskar::Engine => '/beskar'"
30
21
 
31
22
  # Check if the route already exists
32
- if File.read("config/routes.rb").include?(route_text)
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/)
33
27
  say "Route already mounted, skipping...", :yellow
34
28
  else
35
29
  route route_text
@@ -39,11 +33,11 @@ module Beskar
39
33
 
40
34
  def copy_migrations
41
35
  # Copy migrations from the engine to the host app
42
- migration_source = File.expand_path("../../../../../db/migrate", __dir__)
36
+ migration_source = File.expand_path("../../../../db/migrate", __dir__)
43
37
 
44
38
  if Dir.exist?(migration_source)
45
39
  Dir.glob("#{migration_source}/*.rb").each do |migration|
46
- migration_name = File.basename(migration).sub(/^\d+_/, '')
40
+ migration_name = File.basename(migration).sub(/^\d+_/, "")
47
41
 
48
42
  # Check if migration already exists
49
43
  if migration_already_exists?(migration_name)
@@ -76,22 +70,28 @@ module Beskar
76
70
 
77
71
  2. Configure authentication for the dashboard in config/initializers/beskar.rb
78
72
 
79
- For Devise users:
80
- config.authenticate_admin = proc do
81
- authenticate_admin!
82
- end
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.
83
76
 
84
- For custom authentication:
85
- config.authenticate_admin = proc do
86
- redirect_to main_app.root_path unless current_user&.admin?
87
- end
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.
88
83
 
89
84
  3. Add Beskar concerns to your User model (or authentication model):
90
85
 
86
+ For Rails built-in authentication:
91
87
  class User < ApplicationRecord
92
- include Beskar::Models::SecurityTrackable
88
+ include Beskar::Models::SecurityTrackableAuthenticable
89
+ end
90
+ Also integrate login admission and session guards from docs/guides/authentication.md.
93
91
 
94
- # For Devise users, also add:
92
+ For Devise:
93
+ class User < ApplicationRecord
94
+ include Beskar::Models::SecurityTrackable
95
95
  include Beskar::Models::SecurityTrackableDevise
96
96
  end
97
97
 
@@ -109,8 +109,9 @@ module Beskar
109
109
  📚 Documentation
110
110
  ===============================================================================
111
111
 
112
- Dashboard Guide: https://github.com/humadroid-io/beskar/blob/main/DASHBOARD.md
113
- Configuration: https://github.com/humadroid-io/beskar/blob/main/README.md
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
114
115
 
115
116
  ===============================================================================
116
117
  ⚠️ Important for Production
@@ -119,10 +120,8 @@ module Beskar
119
120
  1. ALWAYS configure authentication for the dashboard
120
121
  2. Set monitor_only = false when ready to block threats
121
122
  3. Configure your IP whitelist to prevent locking yourself out
122
- 4. Set up database indexes for large-scale deployments:
123
-
124
- $ rails generate beskar:indexes
125
- $ rails db:migrate
123
+ 4. Run the copied migrations; required indexes are included.
124
+ 5. Schedule Beskar::SecurityState.cleanup_expired! to reclaim expired state.
126
125
 
127
126
  ===============================================================================
128
127
  💡 Quick Tips
@@ -144,12 +143,13 @@ module Beskar
144
143
  private
145
144
 
146
145
  def migration_already_exists?(migration_name)
147
- Dir.glob("db/migrate/*_#{migration_name}").any?
146
+ basename = migration_name.delete_suffix(".rb")
147
+ Dir.glob(File.join(destination_root, "db/migrate/*_#{basename}{,.beskar}.rb")).any?
148
148
  end
149
149
 
150
150
  def migration_template(source, destination)
151
- migration_number = self.class.next_migration_number(nil)
152
- file_name = "#{migration_number}_#{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
153
 
154
154
  copy_file source, file_name
155
155
  end
@@ -12,51 +12,108 @@ Beskar.configure do |config|
12
12
  # (cookies, session, authenticate_or_request_with_http_basic, etc.).
13
13
  # The block should return truthy value to allow access, falsey to deny.
14
14
 
15
- # Option 1: Using Devise with admin role (recommended for production)
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.
16
53
  # config.authenticate_admin = ->(request) do
17
54
  # user = request.env['warden']&.authenticate(scope: :user)
18
55
  # user&.admin?
19
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
20
65
 
21
- # Option 2: HTTP Basic Authentication (uses controller method)
66
+ # Option 3: HTTP Basic Authentication (uses controller method)
22
67
  # config.authenticate_admin = ->(request) do
23
68
  # authenticate_or_request_with_http_basic("Beskar Admin") do |username, password|
24
- # username == ENV['BESKAR_ADMIN_USERNAME'] &&
25
- # password == ENV['BESKAR_ADMIN_PASSWORD']
69
+ # Beskar::Services::RequestContext.secure_match?(username, ENV['BESKAR_ADMIN_USERNAME']) &&
70
+ # Beskar::Services::RequestContext.secure_match?(password, ENV['BESKAR_ADMIN_PASSWORD'])
26
71
  # end
27
72
  # end
28
73
 
29
- # Option 3: Token-based authentication
74
+ # Option 4: Token-based authentication
30
75
  # config.authenticate_admin = ->(request) do
31
- # request.headers['Authorization'] == "Bearer #{ENV['BESKAR_ADMIN_TOKEN']}"
76
+ # token = ENV['BESKAR_ADMIN_TOKEN']
77
+ # token.present? && Beskar::Services::RequestContext.secure_match?(request.headers['Authorization'], "Bearer #{token}")
32
78
  # end
33
79
 
34
- # Option 4: Cookie-based authentication (uses controller cookies)
80
+ # Option 5: Cookie-based authentication (uses controller cookies)
35
81
  # config.authenticate_admin = ->(request) do
36
- # cookies.signed[:admin_token] == ENV['BESKAR_ADMIN_TOKEN']
82
+ # Beskar::Services::RequestContext.secure_match?(cookies.signed[:admin_token], ENV['BESKAR_ADMIN_TOKEN'])
37
83
  # end
38
84
 
39
- # Option 5: Using CanCanCan (uses controller method)
85
+ # Option 6: Using CanCanCan (requires policy helpers on Beskar's controller)
40
86
  # config.authenticate_admin = ->(request) do
41
87
  # authorize! :manage, :beskar_dashboard
42
88
  # end
43
89
 
44
- # Option 6: Using Pundit (uses controller method)
90
+ # Option 7: Using Pundit (requires policy helpers on Beskar's controller)
45
91
  # config.authenticate_admin = ->(request) do
46
92
  # authorize :beskar_dashboard, :access?
47
93
  # end
48
94
 
49
- # Option 7: For development/testing ONLY (NOT for production!)
95
+ # Option 8: For development/testing ONLY (NOT for production!)
50
96
  # config.authenticate_admin = ->(request) do
51
97
  # Rails.env.development? || Rails.env.test?
52
98
  # end
53
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
+
54
111
  # ============================================================================
55
112
  # MONITOR-ONLY MODE
56
113
  # ============================================================================
57
114
  # When enabled, Beskar will log all security events but won't actually block
58
115
  # any requests. Useful for testing or initial deployment.
59
- config.monitor_only = <%= Rails.env.development? ? 'true' : 'false' %>
116
+ config.monitor_only = true
60
117
 
61
118
  # ============================================================================
62
119
  # IP WHITELIST
@@ -71,7 +128,7 @@ Beskar.configure do |config|
71
128
  # ============================================================================
72
129
  # WAF (WEB APPLICATION FIREWALL) - Score-Based Blocking
73
130
  # ============================================================================
74
- # Enable WAF to protect against common attacks and vulnerability scans.
131
+ # Enable scanner-path detection (not general SQL injection/XSS filtering).
75
132
  # Uses score-based blocking with exponential decay for intelligent threat detection.
76
133
  config.waf[:enabled] = true
77
134
 
@@ -82,6 +139,12 @@ Beskar.configure do |config|
82
139
  # config.waf[:block_durations] = [1.hour, 6.hours, 24.hours, 7.days] # Escalating durations
83
140
  # config.waf[:permanent_block_after] = 500 # Permanent block when cumulative score reaches this
84
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.
85
148
  #
86
149
  # === Exponential Decay Configuration ===
87
150
  # Violations decay over time based on severity (reduces false positives)
@@ -102,8 +165,8 @@ Beskar.configure do |config|
102
165
  # %r{/public/.*} # Public content
103
166
  # ]
104
167
  #
105
- # === Pre-configured Profiles ===
106
- # See WAF_CONFIGURATION_PROFILES.md for complete profile examples:
168
+ # === Illustrative thresholds ===
169
+ # See docs/guides/audit-and-waf.md for matching rules and tuning limitations:
107
170
  # - STRICT: score_threshold = 100 (high-security)
108
171
  # - BALANCED: score_threshold = 150 (recommended default)
109
172
  # - PERMISSIVE: score_threshold = 200 (high-traffic sites)
@@ -115,7 +178,11 @@ Beskar.configure do |config|
115
178
  # config.security_tracking[:enabled] = false
116
179
  # config.security_tracking[:track_successful_logins] = true
117
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"
118
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.
119
186
 
120
187
  # ============================================================================
121
188
  # RATE LIMITING
@@ -129,6 +196,8 @@ Beskar.configure do |config|
129
196
  # config.rate_limiting[:account_attempts][:period] = 15.minutes
130
197
  # config.rate_limiting[:account_attempts][:exponential_backoff] = true
131
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
132
201
  # config.rate_limiting[:global_attempts][:limit] = 100
133
202
  # config.rate_limiting[:global_attempts][:period] = 1.minute
134
203
  # config.rate_limiting[:global_attempts][:exponential_backoff] = false
@@ -137,17 +206,22 @@ Beskar.configure do |config|
137
206
  # RISK-BASED ACCOUNT LOCKING
138
207
  # ============================================================================
139
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.
140
210
  # config.risk_based_locking[:enabled] = true
141
- # config.risk_based_locking[:immediate_signout] = false # Sign out users immediately when locked
211
+ # Confirmed locks always reject sign-in and revoke old sessions, including with legacy immediate_signout: false.
142
212
  # config.risk_based_locking[:risk_threshold] = 75 # Risk score threshold for locking
143
- # config.risk_based_locking[:auto_unlock_time] = 1.hour # How long to lock the account
144
- # config.risk_based_locking[:notify_user] = true # Notify user on lock
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
145
218
  # config.risk_based_locking[:log_lock_events] = true # Log lock events
146
219
 
147
220
  # ============================================================================
148
221
  # GEOLOCATION
149
222
  # ============================================================================
150
223
  # Geolocation uses a mock provider by default. To use MaxMind:
224
+ # Mock locations are synthetic and never trigger travel/country risk.
151
225
  # 1. Download GeoLite2-City database from maxmind.com
152
226
  # 2. Place it in config/GeoLite2-City.mmdb
153
227
  # 3. Configure:
@@ -171,7 +245,17 @@ Beskar.configure do |config|
171
245
  # config.emergency_password_reset[:impossible_travel_threshold] = 3
172
246
  # config.emergency_password_reset[:suspicious_device_threshold] = 5
173
247
  # config.emergency_password_reset[:total_locks_threshold] = 5
174
- # config.emergency_password_reset[:send_notification] = true
175
- # config.emergency_password_reset[:notify_security_team] = true
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
176
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.
177
261
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :beskar do
4
+ desc "Remove expired coordination state (does not remove audit events or active bans)"
5
+ task cleanup_security_state: :environment do
6
+ Beskar::SecurityState.cleanup_expired!
7
+ end
8
+
4
9
  desc "Install Beskar: copy migrations and create initializer"
5
10
  task install: :environment do
6
11
  puts "=" * 80
@@ -10,25 +15,19 @@ namespace :beskar do
10
15
 
11
16
  # Copy migrations
12
17
  puts "📦 Copying migrations..."
13
- begin
14
- Rake::Task["beskar:install:migrations"].invoke
15
- rescue RuntimeError
16
- # In development/test within the gem, this task might not exist
17
- # In a real app using the gem, it will work fine
18
- puts " (Skipping migration copy in gem development mode)"
19
- end
18
+ Rake::Task["beskar:install:migrations"].invoke
20
19
  puts "✓ Migrations ready"
21
20
  puts
22
21
 
23
22
  # Create initializer
24
23
  initializer_path = Rails.root.join("config/initializers/beskar.rb")
25
-
24
+
26
25
  if File.exist?(initializer_path)
27
26
  puts "⚠️ Initializer already exists at config/initializers/beskar.rb"
28
27
  print " Overwrite? (y/N): "
29
- response = $stdin.gets.chomp.downcase
30
-
31
- unless response == 'y' || response == 'yes'
28
+ response = $stdin.gets.to_s.strip.downcase
29
+
30
+ unless response == "y" || response == "yes"
32
31
  puts " Skipping initializer creation"
33
32
  puts
34
33
  next_steps
@@ -41,7 +40,7 @@ namespace :beskar do
41
40
 
42
41
  # Read the template and process ERB (Rails will be available in the rake task context)
43
42
  template_content = File.read(template_path)
44
- erb = ERB.new(template_content, trim_mode: '-')
43
+ erb = ERB.new(template_content, trim_mode: "-")
45
44
 
46
45
  # Evaluate the ERB template in a context where Rails is available
47
46
  processed_content = erb.result(binding)
@@ -69,7 +68,7 @@ namespace :beskar do
69
68
  puts
70
69
  puts " # app/models/user.rb"
71
70
  puts " class User < ApplicationRecord"
72
- puts " include Beskar::SecurityTrackable"
71
+ puts " include Beskar::Models::SecurityTrackable"
73
72
  puts " "
74
73
  puts " devise :database_authenticatable, :registerable,"
75
74
  puts " :recoverable, :rememberable, :validatable"
@@ -98,11 +97,8 @@ namespace :beskar do
98
97
  puts "5. When ready to enable blocking:"
99
98
  puts
100
99
  puts " # config/initializers/beskar.rb"
101
- puts " config.waf = {"
102
- puts " enabled: true,"
103
- puts " monitor_only: false, # <-- Change this to false"
104
- puts " # ... rest of config"
105
- puts " }"
100
+ puts " config.waf[:enabled] = true"
101
+ puts " config.monitor_only = false"
106
102
  puts
107
103
  puts "6. Optional: Add IP whitelist for trusted sources:"
108
104
  puts
@@ -115,7 +111,7 @@ namespace :beskar do
115
111
  puts "=" * 80
116
112
  puts "Documentation:"
117
113
  puts " - README: https://github.com/humadroid-io/beskar"
118
- puts " - WAF Monitor Mode: See MONITOR_ONLY_MODE.md"
114
+ puts " - WAF Monitor Mode: See docs/operations/monitor-only-mode.md"
119
115
  puts "=" * 80
120
116
  end
121
117
  end