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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -19
- data/README.md +142 -122
- 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 +69 -25
- data/app/controllers/beskar/banned_ips_controller.rb +116 -141
- data/app/controllers/beskar/dashboard_controller.rb +20 -28
- data/app/controllers/beskar/security_events_controller.rb +37 -55
- 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 +91 -132
- data/app/models/beskar/security_event.rb +37 -4
- data/app/models/beskar/security_state.rb +58 -0
- data/app/services/beskar/banned_ip_manager.rb +16 -6
- 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 +25 -89
- data/app/views/beskar/banned_ips/index.html.erb +20 -62
- data/app/views/beskar/banned_ips/new.html.erb +18 -138
- data/app/views/beskar/banned_ips/review.html.erb +24 -0
- data/app/views/beskar/banned_ips/show.html.erb +9 -15
- data/app/views/beskar/dashboard/index.html.erb +4 -4
- data/app/views/beskar/security_events/index.html.erb +8 -15
- data/app/views/beskar/security_events/show.html.erb +6 -20
- 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 +9 -76
- data/config/routes.rb +10 -21
- 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 +84 -13
- 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 +30 -35
- data/lib/beskar/middleware/request_analyzer.rb +41 -82
- data/lib/beskar/models/security_trackable_authenticable.rb +72 -93
- data/lib/beskar/models/security_trackable_devise.rb +32 -23
- data/lib/beskar/models/security_trackable_generic.rb +169 -212
- data/lib/beskar/risk_level.rb +22 -0
- data/lib/beskar/services/account_locker.rb +85 -76
- 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 +164 -280
- 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 +53 -4
- data/lib/generators/beskar/install/install_generator.rb +36 -36
- data/lib/generators/beskar/install/templates/initializer.rb.tt +104 -20
- data/lib/tasks/beskar_tasks.rake +15 -19
- metadata +60 -8
data/README.md
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
# Beskar
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+

|
|
22
|
+
|
|
23
|
+
| Security Events | Banned IPs |
|
|
24
|
+
|:---------------:|:----------:|
|
|
25
|
+
|  |  |
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
40
|
-
- **
|
|
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`,
|
|
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
|
-
|
|
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
|
|
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]
|
|
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:
|
|
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, :
|
|
259
|
-
auto_unlock_time: 1.hour, #
|
|
260
|
-
notify_user:
|
|
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, #
|
|
320
|
-
notify_user:
|
|
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
|
-
-
|
|
329
|
-
|
|
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 (0–100), 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
|
-
-
|
|
342
|
-
-
|
|
343
|
-
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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` -
|
|
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
|
-
#
|
|
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**
|
|
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. **
|
|
497
|
-
10. **
|
|
498
|
-
11. **
|
|
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
|
|
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
|
|
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:
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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)
|
|
519
|
-
→ Exceeds threshold (150) →
|
|
520
|
-
→
|
|
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
|
|
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
|
-
| **
|
|
657
|
-
| **Rate Limit Violations** | 5
|
|
658
|
-
| **WAF Violations** |
|
|
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
|
-
|
|
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
|
-
**
|
|
715
|
-
|
|
716
|
-
The cache is automatically preloaded when your app starts, but you can manually trigger it:
|
|
761
|
+
**State cleanup:**
|
|
717
762
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
|
839
|
-
| RecordNotFound Exception | Low |
|
|
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
|
|
842
|
-
|
|
843
|
-
-
|
|
844
|
-
-
|
|
845
|
-
-
|
|
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:**
|
|
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[:
|
|
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:**
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
-
|
|
1024
|
-
|
|
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
|