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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +274 -0
- data/README.md +412 -204
- data/app/channels/concerns/beskar/channels/session_security.rb +46 -0
- data/app/controllers/beskar/administrative_actions_controller.rb +16 -0
- data/app/controllers/beskar/application_controller.rb +214 -0
- data/app/controllers/beskar/banned_ips_controller.rb +255 -0
- data/app/controllers/beskar/dashboard_controller.rb +62 -0
- data/app/controllers/beskar/security_events_controller.rb +164 -0
- data/app/controllers/concerns/beskar/controllers/audit_export.rb +54 -0
- data/app/controllers/concerns/beskar/controllers/security_tracking.rb +76 -48
- data/app/controllers/concerns/beskar/controllers/session_security.rb +29 -0
- data/app/jobs/beskar/notification_job.rb +33 -0
- data/app/mailers/beskar/security_mailer.rb +59 -0
- data/app/models/beskar/administrative_action.rb +41 -0
- data/app/models/beskar/banned_ip.rb +105 -105
- data/app/models/beskar/security_event.rb +51 -4
- data/app/models/beskar/security_state.rb +58 -0
- data/app/services/beskar/banned_ip_manager.rb +88 -0
- data/app/views/beskar/administrative_actions/index.html.erb +33 -0
- data/app/views/beskar/administrative_actions/show.html.erb +21 -0
- data/app/views/beskar/banned_ips/edit.html.erb +195 -0
- data/app/views/beskar/banned_ips/index.html.erb +319 -0
- data/app/views/beskar/banned_ips/new.html.erb +190 -0
- data/app/views/beskar/banned_ips/review.html.erb +24 -0
- data/app/views/beskar/banned_ips/show.html.erb +304 -0
- data/app/views/beskar/dashboard/index.html.erb +280 -0
- data/app/views/beskar/security_events/index.html.erb +302 -0
- data/app/views/beskar/security_events/show.html.erb +293 -0
- data/app/views/beskar/shared/_export_form.html.erb +10 -0
- data/app/views/layouts/beskar/_behavior.html.erb +121 -0
- data/app/views/layouts/beskar/application.html.erb +581 -6
- data/config/routes.rb +30 -0
- data/db/migrate/20251016000001_create_beskar_security_events.rb +3 -3
- data/db/migrate/20260910000001_create_beskar_security_states.rb +14 -0
- data/db/migrate/20260911000001_create_beskar_administrative_actions.rb +22 -0
- data/db/migrate/20260911000002_expand_administrative_action_targets.rb +6 -0
- data/docs/README.md +73 -0
- data/docs/archive/project-documentation.md +659 -0
- data/docs/audits/project-review.md +437 -0
- data/docs/audits/repair-status.md +216 -0
- data/docs/guides/audit-and-waf.md +175 -0
- data/docs/guides/audit-lifecycle.md +172 -0
- data/docs/guides/authentication.md +213 -0
- data/docs/guides/configuration.md +182 -0
- data/docs/guides/dashboard-and-search.md +251 -0
- data/docs/guides/notifications-and-recovery.md +157 -0
- data/docs/guides/risk-scoring.md +116 -0
- data/docs/operations/monitor-only-mode.md +85 -0
- data/docs/operations/security-hardening.md +167 -0
- data/docs/operations/state-storage.md +144 -0
- data/docs/research/rust-performance-assessment.md +69 -0
- data/lib/beskar/configuration.rb +105 -20
- data/lib/beskar/configuration_validator.rb +188 -0
- data/lib/beskar/devise_authentication.rb +24 -0
- data/lib/beskar/engine.rb +21 -88
- data/lib/beskar/logger.rb +288 -0
- data/lib/beskar/middleware/request_analyzer.rb +133 -99
- data/lib/beskar/models/security_trackable_authenticable.rb +76 -97
- data/lib/beskar/models/security_trackable_devise.rb +34 -25
- data/lib/beskar/models/security_trackable_generic.rb +171 -214
- data/lib/beskar/risk_level.rb +22 -0
- data/lib/beskar/services/account_locker.rb +90 -81
- data/lib/beskar/services/administrative_audit.rb +36 -0
- data/lib/beskar/services/administrative_bans.rb +104 -0
- data/lib/beskar/services/audit_data.rb +72 -0
- data/lib/beskar/services/authentication.rb +31 -0
- data/lib/beskar/services/authentication_attempt.rb +141 -0
- data/lib/beskar/services/ban_expiry.rb +28 -0
- data/lib/beskar/services/device_detector.rb +32 -41
- data/lib/beskar/services/event_search.rb +58 -0
- data/lib/beskar/services/geolocation_service.rb +83 -114
- data/lib/beskar/services/ip_whitelist.rb +31 -40
- data/lib/beskar/services/location_assessment.rb +109 -0
- data/lib/beskar/services/native_account_lock.rb +82 -0
- data/lib/beskar/services/notifications.rb +46 -0
- data/lib/beskar/services/rate_limiter.rb +99 -125
- data/lib/beskar/services/request_context.rb +64 -0
- data/lib/beskar/services/risk_assessment.rb +58 -0
- data/lib/beskar/services/session_revocation.rb +62 -0
- data/lib/beskar/services/waf.rb +311 -198
- data/lib/beskar/services/waf_request.rb +60 -0
- data/lib/beskar/version.rb +1 -1
- data/lib/beskar/warden_authentication.rb +53 -0
- data/lib/beskar.rb +54 -4
- data/lib/generators/beskar/install/install_generator.rb +158 -0
- data/lib/generators/beskar/install/templates/initializer.rb.tt +261 -0
- data/lib/tasks/beskar_tasks.rake +25 -20
- metadata +93 -12
- 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
|
data/lib/beskar/version.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|