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
data/README.md CHANGED
@@ -1,9 +1,32 @@
1
1
  # Beskar
2
2
 
3
- **Beskar** is a comprehensive, Rails-native security engine designed to provide multi-layered, proactive protection for modern web applications. It defends against common threats, bot activity, and account takeovers without requiring external dependencies, integrating seamlessly into your application as a natural extension of the framework.
3
+ Implementation repairs are tracked in [Repair status](docs/audits/repair-status.md). Read [Security hardening and rollout](docs/operations/security-hardening.md) before upgrading: session revocation, explicit admin permissions, export auditing, sealed configuration, and safer availability defaults change integration requirements. The original findings remain in [Original project review](docs/audits/project-review.md).
4
+
5
+ Account deletion retains security events unchanged. Dashboard ban changes now require
6
+ a trusted actor and reason, recorded in a new administrative-history table. See
7
+ [Audit lifecycle](docs/guides/audit-lifecycle.md) for configuration and upgrade requirements.
8
+
9
+ **Beskar** is a Rails-native security engine for authentication admission limits, risk-based account locking, IP bans, scanner-path detection, and an administrative audit dashboard. It requires a shared writer database for coordinated state and explicit host authentication integration. Its heuristic signals do not replace application authorization, input validation, or recovery delivery.
10
+
11
+ ## Documentation
12
+
13
+ The [documentation index](docs/README.md) organizes current guides, operational
14
+ contracts, audit findings, research, and archived reference material. Start with
15
+ [configuration](docs/guides/configuration.md) and
16
+ [authentication](docs/guides/authentication.md) for integration, or the
17
+ [rollout checklist](docs/operations/security-hardening.md#rollout) for an upgrade.
18
+
19
+ ## Screenshots
20
+
21
+ ![Dashboard](https://humadroid-static-assets.s3.amazonaws.com/beskar/beskar-dashboard.png)
22
+
23
+ | Security Events | Banned IPs |
24
+ |:---------------:|:----------:|
25
+ | ![Security Events](https://humadroid-static-assets.s3.amazonaws.com/beskar/beskar-security-event.png) | ![Banned IPs](https://humadroid-static-assets.s3.amazonaws.com/beskar/beskar-banned-ips.png) |
4
26
 
5
27
  ## Table of Contents
6
28
 
29
+ - [Documentation](#documentation)
7
30
  - [Features](#features)
8
31
  - [Installation](#installation)
9
32
  - [Quick Start](#quick-start)
@@ -27,20 +50,30 @@
27
50
 
28
51
  ## Features
29
52
 
30
- - **Devise Integration:** Seamless integration with Devise authentication for automatic login tracking and security analysis.
31
- - **Risk-Based Account Locking:** Automatically locks accounts when authentication risk scores exceed configurable thresholds, preventing compromised account access.
32
- - **Smart Rate Limiting:** Distributed rate limiting using Rails.cache with IP-based and account-based throttling with exponential backoff.
33
- - **Brute Force Detection:** Advanced pattern recognition to detect single account attacks vs credential stuffing attempts, with automatic IP banning.
34
- - **IP Whitelisting:** Allow trusted IPs (office networks, partners, security scanners) to bypass blocking while maintaining full audit logs. Supports individual IPs and CIDR notation.
35
- - **Persistent IP Blocking:** Hybrid cache + database blocking system that survives application restarts. Auto-bans IPs after authentication abuse or excessive rate limiting violations.
36
- - **Web Application Firewall (WAF):** Real-time detection and blocking of vulnerability scanning attempts across 12 attack categories including Rails exception analysis (WordPress scans, WordPress static files, PHP admin panels, config files, path traversal, framework debug, CMS detection, common exploits, UnknownFormat, IP spoofing, InvalidType, RecordNotFound enumeration). Includes escalating ban durations, monitor-only mode, and configurable exclusion patterns.
37
- - **Security Event Tracking:** Comprehensive logging of authentication events with risk scoring and metadata extraction.
53
+ For current reporting bands, search semantics, native-user presentation, UTC ban
54
+ forms, script CSP/native-navigation behavior, and dashboard/export routes, see
55
+ [Dashboard and search](docs/guides/dashboard-and-search.md).
56
+ No versioned administration API is implemented.
57
+
58
+ See [Configuration](docs/guides/configuration.md) for startup validation, supported
59
+ strategies/providers, and opt-in host background analysis. Opt-in lock/reset email
60
+ delivery and its host recovery requirements are documented in
61
+ [Notifications and recovery](docs/guides/notifications-and-recovery.md).
62
+
63
+ - **Devise Integration:** Admission, risk assessment, optional audit tracking and durable session/remember-cookie revocation on protected models.
64
+ - **Risk-Based Account Locking:** Opt-in rules lock supported accounts at configured thresholds. Scores are heuristics, not proof of account takeover.
65
+ - **Rate Limiting:** Database-coordinated IP/account and opt-in global admission limits with enforced backoff deadlines. Supports any Rails.cache backend. See [authentication integration](docs/guides/authentication.md) for required host guards.
66
+ - **Authentication Pattern Helpers:** Bounded account/IP failure-history helpers; automatic background analysis requires an explicitly configured host job.
67
+ - **IP Whitelisting:** Trusted IPs/CIDRs bypass automatic blocking while configured observations remain enabled; optional audit delivery is not guaranteed.
68
+ - **Persistent IP Blocking:** Database-authoritative blocking across application restarts. WAF rules and opt-in request-wide quota-abuse escalation can create automatic bans.
69
+ - **Web Application Firewall (WAF):** Bounded, decoded-path scanner signatures and narrowly scoped Rails exception signals, with cumulative scores, escalating bans, monitor-only mode, and method/path/category exclusions. This is not a general SQL injection or XSS filter.
70
+ - **Security Event Tracking:** Filtered authentication/WAF evidence and required administrative history. Events survive account deletion; ordinary instance rewrites/deletes are rejected.
38
71
  - **IP Geolocation:** MaxMind GeoLite2-City database integration for country/city location, coordinates, timezone, and enhanced risk scoring (configurable, database not included due to licensing).
39
- - **Geographic Anomaly Detection:** Haversine-based impossible travel detection and location-based risk assessment.
40
- - **Advanced Bot Detection:** Multi-layered defense using JavaScript challenges and invisible honeypots to filter out malicious bots while allowing legitimate ones.
72
+ - **Geographic Anomaly Detection:** Timestamped, admitted-login history and Haversine-based travel heuristics with explicit evidence. Mock locations do not trigger geographic risk; see [risk scoring](docs/guides/risk-scoring.md).
73
+ - **User-Agent Heuristics:** Browser and bot-like User-Agent signals contribute to authentication risk. Headers are spoofable; JavaScript challenges and honeypots are not implemented.
41
74
  - **Modular Architecture:** Devise-specific code is isolated in separate services for maintainability and extensibility.
42
- - **Rails-Native Architecture:** Built as a mountable `Rails::Engine`, it leverages `ActiveJob` and `Rails.cache` for high performance and low overhead.
43
- - **Security Dashboard:** A mountable web interface for monitoring security events, managing IP bans, and viewing statistics. Features configurable authentication, real-time filtering, and export capabilities. See [Dashboard Authentication](#dashboard-authentication) section below.
75
+ - **Rails-Native Architecture:** Built as a mountable `Rails::Engine`, with Active Record-backed security state and optional caching for enrichment.
76
+ - **Security Dashboard:** A mountable web interface for monitoring security events, managing IP bans, and viewing statistics. Features configurable authentication, real-time filtering, and export capabilities. See [Dashboard Authentication](#dashboard-authentication-required) section below.
44
77
 
45
78
  ## Installation
46
79
 
@@ -77,7 +110,7 @@ bin/rails db:migrate
77
110
 
78
111
  **1. Configure Dashboard Authentication (Required)**
79
112
 
80
- Before using Beskar, you must configure authentication for the dashboard. See the [Dashboard Authentication](#dashboard-authentication) section below for details and examples.
113
+ Before using Beskar, you must configure authentication for the dashboard. See the [Dashboard Authentication](#dashboard-authentication-required) section below for details and examples.
81
114
 
82
115
  **2. Enable WAF Monitoring**
83
116
 
@@ -116,29 +149,49 @@ Beskar.configure do |config|
116
149
  user = request.env['warden']&.authenticate(scope: :user)
117
150
  user&.admin?
118
151
  end
152
+ # REQUIRED: grant individual capabilities using your host permission store.
153
+ config.authorize_admin = ->(request, permission) do
154
+ user = request.env['warden']&.user(scope: :user)
155
+ user&.admin? && user.beskar_permissions.include?(permission.to_s)
156
+ end
157
+ # Adapt beskar_permissions to your host: read, manage_bans, export, read_audit.
158
+ # REQUIRED for dashboard writes and exports: identify the authenticated operator.
159
+ config.audit_actor = ->(request) do
160
+ user = request.env['warden']&.user(scope: :user)
161
+ "User:#{user.id}" if user&.admin?
162
+ end
119
163
  end
120
164
  ```
121
165
 
122
166
  **Why this is required:** Previous versions allowed unauthenticated access in development/test environments, which could lead to production security issues. Now, authentication must be explicitly configured for all environments to prevent accidental exposure.
123
167
 
168
+ `audit_actor` is separate from authorization. Without it, authenticated reads work
169
+ but ban mutations return 503. Every mutation also requires a nonblank `audit_reason`
170
+ (at most 1,000 characters), supplied by the dashboard forms. Apply the history
171
+ migration and adapt other authentication strategies to return a trusted opaque
172
+ operator ID; never use request parameters or credentials as that identity. See
173
+ [administrative history](docs/guides/audit-lifecycle.md).
174
+
124
175
  **Other Authentication Strategies:**
125
176
 
126
177
  ```ruby
127
178
  # Token-based authentication
128
179
  config.authenticate_admin = ->(request) do
129
- request.headers['Authorization'] == "Bearer #{ENV['BESKAR_ADMIN_TOKEN']}"
180
+ token = ENV['BESKAR_ADMIN_TOKEN']
181
+ token.present? && Beskar::Services::RequestContext.secure_match?(request.headers['Authorization'], "Bearer #{token}")
130
182
  end
131
183
 
132
184
  # HTTP Basic Auth (uses controller method)
133
185
  config.authenticate_admin = ->(request) do
134
186
  authenticate_or_request_with_http_basic do |username, password|
135
- username == ENV['BESKAR_USERNAME'] && password == ENV['BESKAR_PASSWORD']
187
+ Beskar::Services::RequestContext.secure_match?(username, ENV['BESKAR_USERNAME']) &&
188
+ Beskar::Services::RequestContext.secure_match?(password, ENV['BESKAR_PASSWORD'])
136
189
  end
137
190
  end
138
191
 
139
192
  # Cookie-based authentication (uses controller cookies)
140
193
  config.authenticate_admin = ->(request) do
141
- cookies.signed[:admin_token] == ENV['BESKAR_ADMIN_TOKEN']
194
+ Beskar::Services::RequestContext.secure_match?(cookies.signed[:admin_token], ENV['BESKAR_ADMIN_TOKEN'])
142
195
  end
143
196
 
144
197
  # Development/Testing bypass (use with caution!)
@@ -204,7 +257,8 @@ Beskar.configure do |config|
204
257
  enabled: true, # Master switch - disables all tracking when false
205
258
  track_successful_logins: true, # Track successful authentication events
206
259
  track_failed_logins: true, # Track failed authentication attempts
207
- auto_analyze_patterns: true # Enable automatic pattern analysis for threats
260
+ auto_analyze_patterns: false, # Opt in only with a host-owned Active Job
261
+ analysis_job: nil # Example: "SecurityReviewJob"; see docs/guides/configuration.md
208
262
  }
209
263
 
210
264
  # === Rate Limiting ===
@@ -220,6 +274,7 @@ Beskar.configure do |config|
220
274
  exponential_backoff: true
221
275
  },
222
276
  global_attempts: {
277
+ enabled: false, # Opt-in: attackers can exhaust a shared login budget
223
278
  limit: 100, # System-wide limit
224
279
  period: 1.minute,
225
280
  exponential_backoff: false
@@ -255,9 +310,9 @@ Beskar.configure do |config|
255
310
  config.risk_based_locking = {
256
311
  enabled: false, # Master switch for risk-based locking
257
312
  risk_threshold: 75, # Lock account if risk score >= this value (0-100)
258
- lock_strategy: :devise_lockable, # Strategy: :devise_lockable, :custom, :none
259
- auto_unlock_time: 1.hour, # Time until automatic unlock (if supported)
260
- notify_user: true, # Send notification on lock (future feature)
313
+ lock_strategy: :devise_lockable, # Strategy: :devise_lockable, :rails_auth, :none
314
+ auto_unlock_time: 1.hour, # Native locks only; Devise owns unlock_in
315
+ notify_user: false, # Opt-in email; configure notifications first
261
316
  log_lock_events: true # Create security event for locks
262
317
  }
263
318
 
@@ -276,6 +331,9 @@ end
276
331
 
277
332
  ## Usage
278
333
 
334
+ For current admission, account-lock, and recovery behavior—including the required
335
+ Rails-native controller/session-reader upgrade—see [Authentication](docs/guides/authentication.md).
336
+
279
337
  > **Note:** If you haven't already, see the [Add to Your User Model](#add-to-your-user-model) section in Quick Start for setting up `SecurityTrackable`.
280
338
 
281
339
  ### Risk-Based Account Locking (with Devise Lockable)
@@ -316,8 +374,8 @@ Beskar.configure do |config|
316
374
  enabled: true, # Enable the feature
317
375
  risk_threshold: 75, # Lock when risk >= 75
318
376
  lock_strategy: :devise_lockable, # Use Devise's lockable module
319
- auto_unlock_time: 1.hour, # Automatic unlock after 1 hour
320
- notify_user: true, # Log notification intent
377
+ auto_unlock_time: 1.hour, # Native locks only; configure Devise's unlock_in separately
378
+ notify_user: false, # Opt-in email; see docs/guides/notifications-and-recovery.md
321
379
  log_lock_events: true # Create security events
322
380
  }
323
381
  end
@@ -325,37 +383,24 @@ end
325
383
 
326
384
  **How it works:**
327
385
 
328
- - After each successful authentication, Beskar calculates a risk score (0-100) based on:
329
- - Geographic anomalies (impossible travel, country changes)
330
- - Device fingerprints (suspicious user agents, bot signatures)
331
- - Login patterns (velocity, time of day, recent failures)
332
- - IP reputation and geolocation risk
333
-
334
- - **Adaptive Learning:** The system learns from user behavior:
335
- - After 2+ successful logins from an IP, that location becomes "established"
336
- - If a user unlocks and logs in successfully, that pattern is trusted
337
- - Risk scores are reduced to 30% for established patterns (capped at 25)
338
- - This prevents repeated locks after users validate their login context
386
+ - Each authentication assessment records a score (0100), named factors, and evidence: timestamped geographic observations, unverified User-Agent claims, application-local mobile hours, and recent account failures.
387
+ - Repeated IP use and unlock events do not establish verified trust. The former automatic risk discount and geographic bypass have been removed.
339
388
 
340
389
  - If the risk score meets or exceeds the configured threshold, the account is automatically locked
341
- - The user session is terminated immediately to prevent access
342
- - A security event is logged with the lock reason and risk details (always logged for audit trail)
343
- - The account remains locked until manually unlocked or the auto-unlock time expires (if supported)
390
+ - Confirmed locks reject the current attempt and revoke prior Devise sessions/remember cookies. Unlock does not resurrect them. `immediate_signout` defaults true; legacy false no longer bypasses a lock. Unrelated accounts remain signed in.
391
+ - Optional audit events record lock details; missing audit rows do not change enforcement.
392
+ - Devise controls its own unlock policy; Rails-native locks use Beskar's `auto_unlock_time` and persistent session guards.
344
393
 
345
- **Example Adaptive Flow:**
346
- 1. User travels to new location High risk (85) → Account locked
347
- 2. User unlocks account Validates legitimacy
348
- 3. User logs in from same location → Pattern established → Risk reduced to 25 → Login succeeds ✅
349
- 4. Future logins from this location → Normal risk → No more locks
350
-
351
- See `ADAPTIVE_LEARNING.md` for detailed documentation.
394
+ See [Risk scoring](docs/guides/risk-scoring.md) for factor weights, bounded history, provider
395
+ behavior, and rollout limitations. Scores are heuristics, not calibrated
396
+ probabilities; validate the corrected inputs in monitor mode before enforcement.
352
397
 
353
398
  **Lock Reasons:**
354
399
 
355
400
  The system identifies specific reasons for locking:
356
401
  - `:impossible_travel` - Login from location requiring impossible travel speed
357
402
  - `:suspicious_device` - Bot signature or suspicious user agent detected
358
- - `:geographic_anomaly` - Country change or high-risk location
403
+ - `:geographic_anomaly` - Changed known country
359
404
  - `:high_risk_authentication` - General high-risk authentication pattern
360
405
 
361
406
  **Manual Lock/Unlock Operations:**
@@ -476,13 +521,13 @@ if Beskar::Services::IpWhitelist.whitelisted?(request.ip)
476
521
  # IP is trusted - allow but log activity
477
522
  end
478
523
 
479
- # Clear whitelist cache after config changes
524
+ # Optional compatibility method; configuration changes are detected automatically.
480
525
  Beskar::Services::IpWhitelist.clear_cache!
481
526
  ```
482
527
 
483
528
  ### Web Application Firewall (WAF)
484
529
 
485
- Beskar's WAF uses a **score-based blocking system with exponential decay** to intelligently detect and block vulnerability scanning attempts across 12 attack categories:
530
+ Beskar's WAF uses a **score-based blocking system with exponential decay** for scanner-path signatures and selected Rails exceptions. See [the matching and privacy contract](docs/guides/audit-and-waf.md) for canonicalization, exclusions, and limitations.
486
531
 
487
532
  **Attack Categories Detected:**
488
533
  1. **WordPress Scans** (High: 80 points) - `/wp-admin`, `/wp-login.php`, `/wp-content/*.php`, `/xmlrpc.php`
@@ -493,31 +538,31 @@ Beskar's WAF uses a **score-based blocking system with exponential decay** to in
493
538
  6. **Framework Debug** (Medium: 60 points) - `/rails/info/routes`, `/__debug__`, `/telescope`
494
539
  7. **CMS Detection** (Medium: 60 points) - `/joomla`, `/drupal`, `/magento`
495
540
  8. **Common Exploits** (Critical: 95 points) - `/shell.php`, `/c99.php`, `/webshell`
496
- 9. **ActionController::UnknownFormat** (Medium: 60 points) - Detects requests for unusual formats like `/users/1.exe`, `/api/data.bat` that trigger Rails format exceptions, indicating potential scanning
497
- 10. **ActionDispatch::RemoteIp::IpSpoofAttackError** (Critical: 95 points) - Detects IP spoofing attempts when conflicting IP headers are present
498
- 11. **ActionDispatch::Http::MimeNegotiation::InvalidType** (Medium: 60 points) - Detects invalid MIME type requests like `GET "../../../../../../../../etc/passwd{{"` that indicate scanner activity
499
- 12. **ActiveRecord::RecordNotFound** (Low: 30 points) - Detects potential record enumeration scans like `/admin/users/999999`, with configurable exclusions to prevent false positives
541
+ 9. **Rails Format Paths** (Medium: 60 points) - Selected resource/extension signatures such as `/users/1.exe`, plus exact executable `format` query values
542
+ 10. **Record Scanning Paths** (Low: 30 points) - Selected large-ID and scanner-name path signatures
543
+ 11. **Rails Exceptions** - `UnknownFormat` and `InvalidType` (60), `RecordNotFound` (30), and `IpSpoofAttackError` (95). Ordinary Rails exceptions require independent path/format evidence by default; IP-spoof exceptions require safe resolved attribution in middleware. Exceptions alone can be scored by explicitly opting into `exception_detection: :all`.
500
544
 
501
545
  **How Score-Based Blocking Works:**
502
546
 
503
547
  Instead of counting violations (1, 2, 3...), Beskar tracks a **cumulative risk score** that decays over time:
504
548
 
505
- - Each violation adds points based on severity (Critical=95, High=80, Medium=60, Low=30)
549
+ - Each middleware pass records at most one violation, using the highest matched path severity (Critical=95, High=80, Medium=60, Low=30); a downstream exception does not add a second charge
506
550
  - Violations **decay exponentially** based on severity (critical threats persist longer)
507
551
  - IP is blocked when cumulative score reaches threshold (default: 150 points)
508
- - Lower-severity violations decay faster, reducing false positives from legitimate 404s
552
+ - Lower-severity violations decay faster; ordinary 404s do not add points by default, but signature-matching legitimate paths still can
509
553
 
510
554
  **Example Scenarios:**
511
555
  ```ruby
512
- # Scenario 1: Legitimate user hitting 404s
513
- 10 × RecordNotFound (30 points each) = 300 cumulative
514
- BUT: Low severity decays with 15-minute half-life
515
- Score drops quickly, no ban triggered
556
+ # Scenario 1: Ordinary missing records, no matching scanner signature
557
+ # exception_detection: :suspicious (default)
558
+ 10 × RecordNotFound = no WAF points
559
+ # Opting into :all changes this: ten rapid failures can cross the ban threshold.
516
560
 
517
561
  # Scenario 2: Attacker scanning config files
518
- 2 × /.env access (95 points each) = 190 points
519
- → Exceeds threshold (150) → Immediate ban
520
- Critical severity persists for 6 hours
562
+ 2 × /.env access close together (95 points each) 190 points
563
+ → Exceeds threshold (150) → Ban when enforcement and auto-block are enabled
564
+ One request alone is below the default threshold
565
+ → Critical severity has a 6-hour half-life within the configured retention window
521
566
 
522
567
  # Scenario 3: Mixed attack pattern
523
568
  1 × WordPress scan (80) + 1 × Config access (95) = 175
@@ -647,17 +692,19 @@ end
647
692
 
648
693
  ### IP Blocking and Banning
649
694
 
650
- Beskar uses a hybrid cache + database blocking system that persists across application restarts.
695
+ Beskar uses indexed database ban checks. All workers must share the same authoritative database. Cache eviction, cache outages, and stale cache values cannot change ban enforcement.
651
696
 
652
697
  **Automatic IP Banning Thresholds:**
653
698
 
654
699
  | Trigger | Threshold | Time Window | Ban Duration | Configurable |
655
700
  |---------|-----------|-------------|--------------|--------------|
656
- | **Failed Authentication** | 10 attempts | 1 hour | 1 hour (escalating) | Via rate_limiting config |
657
- | **Rate Limit Violations** | 5 violations | 1 hour | 1 hour (escalating) | Via rate_limiting config |
658
- | **WAF Violations** | 3 violations | 1 hour | 1 hour (escalating) | Via waf[:block_threshold] |
701
+ | **Authentication attempt limit** | Configured IP limit (default 10) | Configured period (default 1 hour) | HTTP 429 until the actual retry deadline | Via rate_limiting config |
702
+ | **Rate Limit Violations** | 5 denied requests | Fixed 1 hour window | Adds 1 hour to a temporary ban | Fixed middleware policy |
703
+ | **WAF Violations** | Cumulative score (default 150) | Configured window with optional decay | Score-based durations or permanent | Via waf configuration |
659
704
 
660
- > **Note:** All ban durations escalate on repeat offenses: 1h → 6h 24h 7d permanent
705
+ Explicit extensions without a duration escalate to 6h, 24h, 7d, then permanent.
706
+ Extensions with a duration add that duration; already permanent bans remain permanent.
707
+ Monitor mode does not create automatic bans.
661
708
 
662
709
  **Manual IP Management:**
663
710
 
@@ -711,16 +758,11 @@ Beskar::BannedIp.where(reason: 'rate_limit_abuse')
711
758
  removed_count = Beskar::BannedIp.cleanup_expired!
712
759
  ```
713
760
 
714
- **Preload cache on startup:**
715
-
716
- The cache is automatically preloaded when your app starts, but you can manually trigger it:
761
+ **State cleanup:**
717
762
 
718
- ```ruby
719
- # In config/initializers/beskar.rb (optional - happens automatically)
720
- Rails.application.config.after_initialize do
721
- Beskar::BannedIp.preload_cache!
722
- end
723
- ```
763
+ Schedule `bin/rails beskar:cleanup_security_state` to reclaim expired coordination
764
+ rows. Audit-event retention is separate. Ban cache preloading is no longer required;
765
+ `Beskar::BannedIp.preload_cache!` remains a compatibility no-op.
724
766
 
725
767
  ### Security Events and Monitoring
726
768
 
@@ -833,16 +875,19 @@ Security events are logged to the `beskar_security_events` table for analysis an
833
875
  | Framework Debug | Medium | `/rails/info/routes`, `/__debug__`, `/telescope` | 60 |
834
876
  | CMS Detection | Medium | `/joomla`, `/drupal`, `/magento` | 60 |
835
877
  | Common Exploits | **Critical** | `/shell.php`, `/c99.php`, `/webshell` | **95** |
836
- | UnknownFormat Exception | Medium | `/users/1.exe`, `/api/data.bat` | 60 |
878
+ | Rails Format Paths | Medium | `/users/1.exe`, `/reports?format=exe` | 60 |
879
+ | Record Scanning Paths | Low | `/account/999999` | 30 |
837
880
  | IP Spoofing Exception | **Critical** | Conflicting IP headers | **95** |
838
- | Invalid MIME Type Exception | Medium | `GET "../../../../etc/passwd{{"` | 60 |
839
- | RecordNotFound Exception | Low | `/admin/users/999999` | 30 |
881
+ | UnknownFormat / InvalidType Exceptions | Medium | Requires path/format evidence by default | 60 |
882
+ | RecordNotFound Exception | Low | Requires path/format evidence by default | 30 |
840
883
 
841
- **Pattern matching is:**
842
- - Case-insensitive
843
- - Works on full path including query strings
844
- - Detects URL-encoded variants
845
- - Can match multiple patterns per request
884
+ **Pattern matching:**
885
+
886
+ - Uses an at-most-8-KiB path with up to two percent-decoding passes; backslashes become slashes and dot segments are retained.
887
+ - Uses case-insensitive matching for most signatures, with explicit root/segment boundaries.
888
+ - Ignores arbitrary query text and request bodies; only the exact `format` query key is examined.
889
+ - Can match several rules but charges once per middleware pass; ordinary `.well-known` endpoints are not signatures.
890
+ - Supports explicit method/path/category exclusions. `exception_detection` defaults to `:suspicious`; `:all` opts into broad exception scoring and `:none` disables exception scoring.
846
891
 
847
892
  ## Security Best Practices
848
893
 
@@ -946,10 +991,10 @@ end
946
991
 
947
992
  ### Issue: Legitimate users being blocked
948
993
 
949
- **Solution:** Add their IP to whitelist or reduce WAF `block_threshold`:
994
+ **Solution:** Review matching rules in monitor mode, add narrow exclusions, or raise the cumulative score threshold:
950
995
 
951
996
  ```ruby
952
- config.waf[:block_threshold] = 5 # Increase from default 3
997
+ config.waf[:score_threshold] = 250 # Increase from default 150; not a violation count
953
998
  ```
954
999
 
955
1000
  Or whitelist specific IPs:
@@ -984,50 +1029,25 @@ Beskar::BannedIp.cleanup_expired!
984
1029
 
985
1030
  ### Issue: Performance concerns
986
1031
 
987
- **Solution:** Beskar uses cache-first architecture. Ensure cache is configured:
988
-
989
- ```ruby
990
- # config/environments/production.rb
991
- config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
992
- ```
993
-
994
- Check cache health:
995
- ```ruby
996
- Rails.cache.read("test_key") # Should work
997
- Beskar::BannedIp.preload_cache! # Reload from database if needed
998
- ```
1032
+ **Solution:** Measure database query latency, connection-pool contention, and global
1033
+ counter throughput. Redis is not required and cannot replace the authoritative
1034
+ database. See [state-storage trade-offs](docs/operations/state-storage.md).
999
1035
 
1000
1036
  ## Migration from Previous Versions
1001
1037
 
1002
- If upgrading from a version without WAF/IP blocking features:
1003
-
1004
- ```bash
1005
- # Run new migrations
1006
- rails db:migrate
1007
-
1008
- # Preload cache with existing bans (if any)
1009
- rails runner "Beskar::BannedIp.preload_cache!"
1010
-
1011
- # Test in development first
1012
- RAILS_ENV=development rails server
1013
-
1014
- # Review logs for any issues
1015
- tail -f log/development.log | grep Beskar
1016
- ```
1038
+ Copy engine migrations with `bin/rails beskar:install:migrations`, then run
1039
+ `bin/rails db:migrate` before starting new workers. Drain old cache-based workers:
1040
+ mixed versions do not share counters. Existing cache counters are not imported.
1041
+ Review legacy bans and schedule state cleanup as described in
1042
+ [the upgrade notes](docs/operations/state-storage.md#upgrade).
1017
1043
 
1018
1044
  ## Performance Characteristics
1019
1045
 
1020
- - **Whitelist check**: O(n) where n = whitelist size, cached, < 1ms
1021
- - **Banned IP check**: O(1) cache lookup, < 1ms
1022
- - **Rate limit check**: O(1) cache lookup, < 1ms
1023
- - **WAF analysis**: O(m) where m = number of patterns, < 5ms
1024
- - **Total middleware overhead**: Typically < 10ms per request
1025
-
1026
- **Scalability:**
1027
- - Handles 1000s of requests/second
1028
- - Cache-first architecture minimizes database queries
1029
- - Efficient pattern matching with compiled regexes
1030
- - Parallel test execution: 352 tests run in < 3 seconds
1046
+ Ordinary requests read indexed ban state. Request-wide IP quota checks are opt-in. Authentication
1047
+ attempts update applicable counters transactionally; WAF violations update per-IP
1048
+ history. The global counter is disabled by default; enabling it serializes authentication accounting. No fixed
1049
+ requests-per-second or latency guarantee is asserted; benchmark the deployed
1050
+ database and workload.
1031
1051
 
1032
1052
  ## Development
1033
1053
 
@@ -0,0 +1,46 @@
1
+ module Beskar
2
+ module Channels
3
+ # Prepend to ApplicationCable::Channel to cover every subscription and inbound
4
+ # action. The connection resolves identity and the credential's signed epoch.
5
+ # Standard outbound channel transmissions are checked too. No polling of idle
6
+ # connections is needed; direct connection.transmit bypasses channel policy.
7
+ module SessionSecurity
8
+ def subscribe_to_channel
9
+ unless beskar_session_allowed?
10
+ reject
11
+ reject_subscription
12
+ return
13
+ end
14
+ super
15
+ end
16
+
17
+ def perform_action(data)
18
+ unless beskar_session_allowed?
19
+ stop_all_streams
20
+ connection.close(reason: "authentication_revoked", reconnect: false)
21
+ return
22
+ end
23
+ super
24
+ end
25
+
26
+ private
27
+
28
+ def transmit(data, via: nil)
29
+ unless beskar_session_allowed?
30
+ stop_all_streams
31
+ connection.close(reason: "authentication_revoked", reconnect: false)
32
+ return
33
+ end
34
+ super
35
+ end
36
+
37
+ def beskar_session_allowed?
38
+ return false unless connection.respond_to?(:beskar_authenticated_user) && connection.respond_to?(:beskar_authenticated_generation)
39
+ Services::SessionRevocation.allowed?(connection.beskar_authenticated_user,
40
+ request: connection.request, token: connection.beskar_authenticated_generation)
41
+ rescue Services::AuthenticationAttempt::Unavailable
42
+ false
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,16 @@
1
+ module Beskar
2
+ class AdministrativeActionsController < ApplicationController
3
+ before_action { response.headers["Cache-Control"] = "no-store" }
4
+
5
+ def index
6
+ records = AdministrativeAction.order(id: :desc)
7
+ records = records.where(target_type: "BannedIp", target_id: params[:target_id]) if params[:target_id].present?
8
+ @pagination = paginate(records)
9
+ @administrative_actions = @pagination[:records]
10
+ end
11
+
12
+ def show
13
+ @administrative_action = AdministrativeAction.find(params[:id])
14
+ end
15
+ end
16
+ end