beskar 0.0.1 → 0.1.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 +143 -0
- data/README.md +987 -21
- data/app/controllers/beskar/application_controller.rb +170 -0
- data/app/controllers/beskar/banned_ips_controller.rb +280 -0
- data/app/controllers/beskar/dashboard_controller.rb +70 -0
- data/app/controllers/beskar/security_events_controller.rb +182 -0
- data/app/controllers/concerns/beskar/controllers/security_tracking.rb +70 -0
- data/app/models/beskar/banned_ip.rb +193 -0
- data/app/models/beskar/security_event.rb +64 -0
- data/app/services/beskar/banned_ip_manager.rb +78 -0
- data/app/views/beskar/banned_ips/edit.html.erb +259 -0
- data/app/views/beskar/banned_ips/index.html.erb +361 -0
- data/app/views/beskar/banned_ips/new.html.erb +310 -0
- data/app/views/beskar/banned_ips/show.html.erb +310 -0
- data/app/views/beskar/dashboard/index.html.erb +280 -0
- data/app/views/beskar/security_events/index.html.erb +309 -0
- data/app/views/beskar/security_events/show.html.erb +307 -0
- data/app/views/layouts/beskar/application.html.erb +647 -5
- data/config/locales/en.yml +10 -0
- data/config/routes.rb +41 -0
- data/db/migrate/20251016000001_create_beskar_security_events.rb +25 -0
- data/db/migrate/20251016000002_create_beskar_banned_ips.rb +23 -0
- data/lib/beskar/configuration.rb +214 -0
- data/lib/beskar/engine.rb +105 -0
- data/lib/beskar/logger.rb +293 -0
- data/lib/beskar/middleware/request_analyzer.rb +305 -0
- data/lib/beskar/middleware.rb +4 -0
- data/lib/beskar/models/security_trackable.rb +25 -0
- data/lib/beskar/models/security_trackable_authenticable.rb +167 -0
- data/lib/beskar/models/security_trackable_devise.rb +82 -0
- data/lib/beskar/models/security_trackable_generic.rb +355 -0
- data/lib/beskar/services/account_locker.rb +263 -0
- data/lib/beskar/services/device_detector.rb +250 -0
- data/lib/beskar/services/geolocation_service.rb +392 -0
- data/lib/beskar/services/ip_whitelist.rb +113 -0
- data/lib/beskar/services/rate_limiter.rb +257 -0
- data/lib/beskar/services/waf.rb +551 -0
- data/lib/beskar/version.rb +1 -1
- data/lib/beskar.rb +32 -1
- data/lib/generators/beskar/install/install_generator.rb +158 -0
- data/lib/generators/beskar/install/templates/initializer.rb.tt +177 -0
- data/lib/tasks/beskar_tasks.rake +121 -4
- metadata +138 -5
data/README.md
CHANGED
|
@@ -2,13 +2,45 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#features)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Dashboard Authentication (REQUIRED)](#dashboard-authentication-required)
|
|
11
|
+
- [Add to Your User Model](#add-to-your-user-model)
|
|
12
|
+
- [Configuration](#configuration)
|
|
13
|
+
- [Usage](#usage)
|
|
14
|
+
- [Risk-Based Account Locking](#risk-based-account-locking-with-devise-lockable)
|
|
15
|
+
- [Rate Limiting](#rate-limiting)
|
|
16
|
+
- [IP Whitelisting](#ip-whitelisting)
|
|
17
|
+
- [Web Application Firewall (WAF)](#web-application-firewall-waf)
|
|
18
|
+
- [IP Blocking and Banning](#ip-blocking-and-banning)
|
|
19
|
+
- [Security Events](#security-events)
|
|
20
|
+
- [Middleware Integration](#middleware-integration)
|
|
21
|
+
- [WAF Pattern Reference](#waf-pattern-reference)
|
|
22
|
+
- [Security Best Practices](#security-best-practices)
|
|
23
|
+
- [Troubleshooting](#troubleshooting)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
- [Contributing](#contributing)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
5
28
|
## Features
|
|
6
29
|
|
|
7
|
-
- **
|
|
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.
|
|
38
|
+
- **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.
|
|
8
40
|
- **Advanced Bot Detection:** Multi-layered defense using JavaScript challenges and invisible honeypots to filter out malicious bots while allowing legitimate ones.
|
|
9
|
-
- **
|
|
41
|
+
- **Modular Architecture:** Devise-specific code is isolated in separate services for maintainability and extensibility.
|
|
10
42
|
- **Rails-Native Architecture:** Built as a mountable `Rails::Engine`, it leverages `ActiveJob` and `Rails.cache` for high performance and low overhead.
|
|
11
|
-
- **
|
|
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.
|
|
12
44
|
|
|
13
45
|
## Installation
|
|
14
46
|
|
|
@@ -16,52 +48,987 @@ Add this line to your application's Gemfile:
|
|
|
16
48
|
|
|
17
49
|
```ruby
|
|
18
50
|
gem 'beskar'
|
|
19
|
-
|
|
51
|
+
```
|
|
20
52
|
|
|
21
53
|
And then execute:
|
|
22
54
|
|
|
23
55
|
```bash
|
|
24
|
-
|
|
56
|
+
bundle install
|
|
25
57
|
```
|
|
26
58
|
|
|
27
|
-
|
|
59
|
+
Run the installation task to set up Beskar:
|
|
28
60
|
|
|
29
61
|
```bash
|
|
30
|
-
|
|
62
|
+
bin/rails beskar:install
|
|
31
63
|
```
|
|
32
64
|
|
|
33
|
-
|
|
65
|
+
This will:
|
|
66
|
+
- Copy all necessary migrations to your application
|
|
67
|
+
- Create `config/initializers/beskar.rb` with sensible defaults
|
|
68
|
+
- Display next steps for completing the setup
|
|
69
|
+
|
|
70
|
+
Then run the database migrations:
|
|
34
71
|
|
|
35
72
|
```bash
|
|
36
|
-
|
|
73
|
+
bin/rails db:migrate
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Quick Start
|
|
77
|
+
|
|
78
|
+
**1. Configure Dashboard Authentication (Required)**
|
|
79
|
+
|
|
80
|
+
Before using Beskar, you must configure authentication for the dashboard. See the [Dashboard Authentication](#dashboard-authentication) section below for details and examples.
|
|
81
|
+
|
|
82
|
+
**2. Enable WAF Monitoring**
|
|
83
|
+
|
|
84
|
+
By default, Beskar enables the **Web Application Firewall (WAF) in monitor-only mode**. This means:
|
|
85
|
+
- ✅ Vulnerability scans are detected and logged
|
|
86
|
+
- ✅ Security events are created for analysis
|
|
87
|
+
- ⚠️ No requests are blocked yet (safe to enable in production)
|
|
88
|
+
|
|
89
|
+
After monitoring for 24-48 hours, review the logs and disable monitor-only mode to enable active blocking:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# config/initializers/beskar.rb
|
|
93
|
+
Beskar.configure do |config|
|
|
94
|
+
config.monitor_only = true # Change this to false to enable blocking
|
|
95
|
+
config.waf[:enabled] = true
|
|
96
|
+
# ... rest of configuration
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Dashboard Authentication (REQUIRED)
|
|
101
|
+
|
|
102
|
+
**⚠️ IMPORTANT: Dashboard authentication must be configured for all environments.**
|
|
103
|
+
|
|
104
|
+
The Beskar dashboard requires authentication to prevent unauthorized access. You must configure how users authenticate to access the dashboard by setting up the `authenticate_admin` callback:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# config/initializers/beskar.rb
|
|
108
|
+
Beskar.configure do |config|
|
|
109
|
+
# REQUIRED: Configure dashboard authentication
|
|
110
|
+
# The block is executed in the controller context and receives the request object.
|
|
111
|
+
# You have access to all controller methods (cookies, session, etc.) and helpers.
|
|
112
|
+
config.authenticate_admin = ->(request) do
|
|
113
|
+
# Return truthy to allow access, falsey to deny
|
|
114
|
+
|
|
115
|
+
# Example 1: Devise with admin role (recommended for production)
|
|
116
|
+
user = request.env['warden']&.authenticate(scope: :user)
|
|
117
|
+
user&.admin?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**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
|
+
|
|
124
|
+
**Other Authentication Strategies:**
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# Token-based authentication
|
|
128
|
+
config.authenticate_admin = ->(request) do
|
|
129
|
+
request.headers['Authorization'] == "Bearer #{ENV['BESKAR_ADMIN_TOKEN']}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# HTTP Basic Auth (uses controller method)
|
|
133
|
+
config.authenticate_admin = ->(request) do
|
|
134
|
+
authenticate_or_request_with_http_basic do |username, password|
|
|
135
|
+
username == ENV['BESKAR_USERNAME'] && password == ENV['BESKAR_PASSWORD']
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Cookie-based authentication (uses controller cookies)
|
|
140
|
+
config.authenticate_admin = ->(request) do
|
|
141
|
+
cookies.signed[:admin_token] == ENV['BESKAR_ADMIN_TOKEN']
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Development/Testing bypass (use with caution!)
|
|
145
|
+
config.authenticate_admin = ->(request) do
|
|
146
|
+
Rails.env.development? || Rails.env.test?
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Accessing the Dashboard:**
|
|
151
|
+
|
|
152
|
+
After configuring authentication, mount the engine in your routes:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# config/routes.rb
|
|
156
|
+
Rails.application.routes.draw do
|
|
157
|
+
mount Beskar::Engine => "/beskar"
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Then visit `http://localhost:3000/beskar` to access the dashboard.
|
|
162
|
+
|
|
163
|
+
**Dashboard Features:**
|
|
164
|
+
- 📊 Security event monitoring with filtering and search
|
|
165
|
+
- 🚫 IP ban management (view, extend, unban)
|
|
166
|
+
- 📈 Statistics and risk distribution analysis
|
|
167
|
+
- 📥 Export capabilities (CSV/JSON)
|
|
168
|
+
- 🔒 CSRF protection and secure by default
|
|
169
|
+
|
|
170
|
+
### Add to Your User Model
|
|
171
|
+
|
|
172
|
+
Include the `SecurityTrackable` concern in your Devise user model:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# app/models/user.rb
|
|
176
|
+
class User < ApplicationRecord
|
|
177
|
+
include Beskar::Models::SecurityTrackable
|
|
178
|
+
|
|
179
|
+
devise :database_authenticatable, :registerable,
|
|
180
|
+
:recoverable, :rememberable, :validatable
|
|
181
|
+
# ... other Devise modules
|
|
182
|
+
end
|
|
37
183
|
```
|
|
38
184
|
|
|
39
185
|
## Configuration
|
|
40
186
|
|
|
41
|
-
You can configure
|
|
187
|
+
You can configure Beskar in the initializer file created by the installer.
|
|
188
|
+
|
|
189
|
+
> **Note:** Dashboard authentication setup is covered in the [Dashboard Authentication](#dashboard-authentication-required) section above.
|
|
42
190
|
|
|
43
191
|
```ruby
|
|
44
192
|
# config/initializers/beskar.rb
|
|
45
|
-
|
|
193
|
+
Beskar.configure do |config|
|
|
194
|
+
# === Dashboard Authentication (REQUIRED) ===
|
|
195
|
+
# See "Dashboard Authentication" section above for examples and details
|
|
196
|
+
config.authenticate_admin = ->(request) do
|
|
197
|
+
user = request.env['warden']&.authenticate(scope: :user)
|
|
198
|
+
user&.admin?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# === Security Tracking ===
|
|
202
|
+
# Controls what security events are tracked and analyzed
|
|
203
|
+
config.security_tracking = {
|
|
204
|
+
enabled: true, # Master switch - disables all tracking when false
|
|
205
|
+
track_successful_logins: true, # Track successful authentication events
|
|
206
|
+
track_failed_logins: true, # Track failed authentication attempts
|
|
207
|
+
auto_analyze_patterns: true # Enable automatic pattern analysis for threats
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# === Rate Limiting ===
|
|
211
|
+
config.rate_limiting = {
|
|
212
|
+
ip_attempts: {
|
|
213
|
+
limit: 10, # Max attempts per IP
|
|
214
|
+
period: 1.hour, # Time window
|
|
215
|
+
exponential_backoff: true # Enable exponential backoff
|
|
216
|
+
},
|
|
217
|
+
account_attempts: {
|
|
218
|
+
limit: 5, # Max attempts per account
|
|
219
|
+
period: 15.minutes, # Time window
|
|
220
|
+
exponential_backoff: true
|
|
221
|
+
},
|
|
222
|
+
global_attempts: {
|
|
223
|
+
limit: 100, # System-wide limit
|
|
224
|
+
period: 1.minute,
|
|
225
|
+
exponential_backoff: false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# === IP Whitelisting ===
|
|
230
|
+
# See "IP Whitelisting" section below for detailed examples
|
|
231
|
+
config.ip_whitelist = [] # Add trusted IPs here (supports CIDR notation)
|
|
232
|
+
|
|
46
233
|
# === Web Application Firewall (WAF) ===
|
|
47
|
-
#
|
|
48
|
-
|
|
234
|
+
# See "Web Application Firewall" section below for production examples
|
|
235
|
+
# Defaults shown here - use [:key] syntax to preserve other defaults
|
|
236
|
+
config.waf[:enabled] = true # Master switch for WAF
|
|
237
|
+
# config.waf[:auto_block] = true # Default: true
|
|
238
|
+
# config.waf[:score_threshold] = 150 # Default: 150 (cumulative risk score before blocking)
|
|
239
|
+
# config.waf[:violation_window] = 6.hours # Default: 6 hours (max time to track violations)
|
|
240
|
+
# config.waf[:block_durations] = [1.hour, 6.hours, 24.hours, 7.days] # Escalating bans
|
|
241
|
+
# config.waf[:permanent_block_after] = 500 # Permanent after cumulative score reaches 500
|
|
242
|
+
# config.waf[:create_security_events] = true # Log to SecurityEvent table
|
|
243
|
+
# config.waf[:record_not_found_exclusions] = [] # Regex patterns for false positives
|
|
244
|
+
# config.waf[:decay_enabled] = true # Enable exponential decay of violation scores
|
|
245
|
+
# config.waf[:decay_rates] = { # Decay rates by severity (half-life in minutes)
|
|
246
|
+
# critical: 360, # 6 hour half-life
|
|
247
|
+
# high: 120, # 2 hour half-life
|
|
248
|
+
# medium: 45, # 45 minute half-life
|
|
249
|
+
# low: 15 # 15 minute half-life
|
|
250
|
+
# }
|
|
251
|
+
# config.waf[:max_violations_tracked] = 50 # Maximum violations to track per IP
|
|
49
252
|
|
|
50
|
-
# === Account
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
253
|
+
# === Risk-Based Account Locking ===
|
|
254
|
+
# Automatically lock accounts when authentication risk score exceeds threshold
|
|
255
|
+
config.risk_based_locking = {
|
|
256
|
+
enabled: false, # Master switch for risk-based locking
|
|
257
|
+
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)
|
|
261
|
+
log_lock_events: true # Create security event for locks
|
|
262
|
+
}
|
|
54
263
|
|
|
55
|
-
#
|
|
264
|
+
# === IP Geolocation ===
|
|
265
|
+
# Configure IP geolocation for enhanced risk assessment
|
|
266
|
+
# Note: You must provide your own MaxMind GeoLite2-City database due to licensing
|
|
267
|
+
# Download from: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
|
268
|
+
config.geolocation = {
|
|
269
|
+
provider: :maxmind, # Provider: :maxmind or :mock (for testing)
|
|
270
|
+
maxmind_city_db_path: Rails.root.join('db', 'geoip', 'GeoLite2-City.mmdb').to_s,
|
|
271
|
+
cache_ttl: 4.hours # How long to cache geolocation results
|
|
272
|
+
}
|
|
56
273
|
end
|
|
274
|
+
|
|
57
275
|
```
|
|
58
276
|
|
|
59
277
|
## Usage
|
|
60
278
|
|
|
61
|
-
|
|
279
|
+
> **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
|
+
|
|
281
|
+
### Risk-Based Account Locking (with Devise Lockable)
|
|
282
|
+
|
|
283
|
+
Beskar can automatically lock user accounts when the calculated risk score exceeds a configured threshold. This prevents compromised accounts from being accessed even after successful authentication.
|
|
284
|
+
|
|
285
|
+
**Setup with Devise Lockable:**
|
|
286
|
+
|
|
287
|
+
1. Enable the `:lockable` module in your User model:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
class User < ApplicationRecord
|
|
291
|
+
devise :database_authenticatable, :registerable,
|
|
292
|
+
:recoverable, :rememberable, :validatable,
|
|
293
|
+
:lockable # Add this for risk-based locking
|
|
294
|
+
|
|
295
|
+
include Beskar::Models::SecurityTrackable
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
2. Generate and run the migration to add lockable columns:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
rails generate devise User # This will add lockable columns if not present
|
|
303
|
+
# Or manually add:
|
|
304
|
+
# - failed_attempts (integer)
|
|
305
|
+
# - unlock_token (string)
|
|
306
|
+
# - locked_at (datetime)
|
|
307
|
+
rails db:migrate
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
3. Enable risk-based locking in your initializer:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
# config/initializers/beskar.rb
|
|
314
|
+
Beskar.configure do |config|
|
|
315
|
+
config.risk_based_locking = {
|
|
316
|
+
enabled: true, # Enable the feature
|
|
317
|
+
risk_threshold: 75, # Lock when risk >= 75
|
|
318
|
+
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
|
|
321
|
+
log_lock_events: true # Create security events
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**How it works:**
|
|
327
|
+
|
|
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
|
|
339
|
+
|
|
340
|
+
- 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)
|
|
344
|
+
|
|
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.
|
|
352
|
+
|
|
353
|
+
**Lock Reasons:**
|
|
354
|
+
|
|
355
|
+
The system identifies specific reasons for locking:
|
|
356
|
+
- `:impossible_travel` - Login from location requiring impossible travel speed
|
|
357
|
+
- `:suspicious_device` - Bot signature or suspicious user agent detected
|
|
358
|
+
- `:geographic_anomaly` - Country change or high-risk location
|
|
359
|
+
- `:high_risk_authentication` - General high-risk authentication pattern
|
|
360
|
+
|
|
361
|
+
**Manual Lock/Unlock Operations:**
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
# Manually lock an account based on risk
|
|
365
|
+
locker = Beskar::Services::AccountLocker.new(
|
|
366
|
+
user,
|
|
367
|
+
risk_score: 85,
|
|
368
|
+
reason: :suspicious_device,
|
|
369
|
+
metadata: { ip_address: request.ip }
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if locker.should_lock?
|
|
373
|
+
locker.lock! # Lock the account
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Check if account is locked
|
|
377
|
+
locker.locked? # => true/false
|
|
378
|
+
|
|
379
|
+
# Manually unlock
|
|
380
|
+
locker.unlock!
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Security Event Tracking
|
|
384
|
+
|
|
385
|
+
Beskar automatically tracks login attempts and creates security events with rich metadata:
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
# Check recent failed attempts for a user
|
|
389
|
+
user.recent_failed_attempts(within: 1.hour)
|
|
390
|
+
|
|
391
|
+
# Check if user has suspicious login patterns
|
|
392
|
+
user.suspicious_login_pattern?
|
|
393
|
+
|
|
394
|
+
# Get recent successful logins
|
|
395
|
+
user.recent_successful_logins(within: 24.hours)
|
|
396
|
+
|
|
397
|
+
# Access security events
|
|
398
|
+
user.security_events.login_failures.recent
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Rate Limiting
|
|
402
|
+
|
|
403
|
+
Check if a request should be rate limited:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# In a controller or middleware
|
|
407
|
+
if Beskar.rate_limited?(request, current_user)
|
|
408
|
+
render json: { error: 'Rate limit exceeded' }, status: 429
|
|
409
|
+
return
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Manual rate limiting check
|
|
413
|
+
rate_limiter = Beskar::Services::RateLimiter.new(request.ip, current_user)
|
|
414
|
+
unless rate_limiter.allowed?
|
|
415
|
+
# Handle rate limiting
|
|
416
|
+
time_until_reset = rate_limiter.time_until_reset
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Security Events Analysis
|
|
421
|
+
|
|
422
|
+
Security events are automatically created and include:
|
|
423
|
+
|
|
424
|
+
- **Event Type**: `login_success`, `login_failure`
|
|
425
|
+
- **IP Address**: Client IP with proxy detection
|
|
426
|
+
- **User Agent**: Browser and device information
|
|
427
|
+
- **Risk Score**: 0-100 based on various factors
|
|
428
|
+
- **Metadata**: Device info, geolocation, timestamps
|
|
429
|
+
- **Attack Patterns**: Detection of brute force, credential stuffing, etc.
|
|
430
|
+
|
|
431
|
+
### Attack Pattern Detection
|
|
432
|
+
|
|
433
|
+
Beskar can identify different types of attacks:
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
rate_limiter = Beskar::Services::RateLimiter.new(ip_address, user)
|
|
437
|
+
attack_type = rate_limiter.attack_pattern_type
|
|
438
|
+
|
|
439
|
+
case attack_type
|
|
440
|
+
when :brute_force_single_account
|
|
441
|
+
# Single IP attacking one account
|
|
442
|
+
when :distributed_single_account
|
|
443
|
+
# Multiple IPs attacking one account
|
|
444
|
+
when :single_ip_multiple_accounts
|
|
445
|
+
# One IP attacking multiple accounts (credential stuffing)
|
|
446
|
+
when :mixed_attack_pattern
|
|
447
|
+
# Complex attack pattern
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### IP Whitelisting
|
|
452
|
+
|
|
453
|
+
Whitelist trusted IPs to bypass all security blocking while maintaining full audit logs:
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
# In config/initializers/beskar.rb
|
|
457
|
+
Beskar.configure do |config|
|
|
458
|
+
config.ip_whitelist = [
|
|
459
|
+
"203.0.113.0/24", # Office network (CIDR notation)
|
|
460
|
+
"198.51.100.50", # VPN gateway (single IP)
|
|
461
|
+
"2001:db8::1" # IPv6 address
|
|
462
|
+
]
|
|
463
|
+
end
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**How it works:**
|
|
467
|
+
- Whitelisted IPs bypass **all blocking** (banned IPs, rate limits, WAF violations)
|
|
468
|
+
- All requests from whitelisted IPs are **still logged** for audit purposes
|
|
469
|
+
- Supports individual IPs and CIDR notation (IPv4 and IPv6)
|
|
470
|
+
- Configuration is validated on startup
|
|
471
|
+
- Efficient caching for high-performance checks
|
|
472
|
+
|
|
473
|
+
**Check if an IP is whitelisted:**
|
|
474
|
+
```ruby
|
|
475
|
+
if Beskar::Services::IpWhitelist.whitelisted?(request.ip)
|
|
476
|
+
# IP is trusted - allow but log activity
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Clear whitelist cache after config changes
|
|
480
|
+
Beskar::Services::IpWhitelist.clear_cache!
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Web Application Firewall (WAF)
|
|
484
|
+
|
|
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:
|
|
486
|
+
|
|
487
|
+
**Attack Categories Detected:**
|
|
488
|
+
1. **WordPress Scans** (High: 80 points) - `/wp-admin`, `/wp-login.php`, `/wp-content/*.php`, `/xmlrpc.php`
|
|
489
|
+
2. **WordPress Static Files** (Low: 30 points) - `/wp-content/*.css`, `/wp-content/*.js`, `/wp-content/*.jpg` (broken links, not attacks)
|
|
490
|
+
3. **PHP Admin Panels** (High: 80 points) - `/phpmyadmin`, `/admin.php`, `/phpinfo.php`
|
|
491
|
+
4. **Config Files** (Critical: 95 points) - `/.env`, `/.git`, `/database.yml`
|
|
492
|
+
5. **Path Traversal** (Critical: 95 points) - `/../../../etc/passwd`, URL encoded variants
|
|
493
|
+
6. **Framework Debug** (Medium: 60 points) - `/rails/info/routes`, `/__debug__`, `/telescope`
|
|
494
|
+
7. **CMS Detection** (Medium: 60 points) - `/joomla`, `/drupal`, `/magento`
|
|
495
|
+
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
|
|
500
|
+
|
|
501
|
+
**How Score-Based Blocking Works:**
|
|
502
|
+
|
|
503
|
+
Instead of counting violations (1, 2, 3...), Beskar tracks a **cumulative risk score** that decays over time:
|
|
504
|
+
|
|
505
|
+
- Each violation adds points based on severity (Critical=95, High=80, Medium=60, Low=30)
|
|
506
|
+
- Violations **decay exponentially** based on severity (critical threats persist longer)
|
|
507
|
+
- IP is blocked when cumulative score reaches threshold (default: 150 points)
|
|
508
|
+
- Lower-severity violations decay faster, reducing false positives from legitimate 404s
|
|
509
|
+
|
|
510
|
+
**Example Scenarios:**
|
|
511
|
+
```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
|
|
516
|
+
|
|
517
|
+
# 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
|
|
521
|
+
|
|
522
|
+
# Scenario 3: Mixed attack pattern
|
|
523
|
+
1 × WordPress scan (80) + 1 × Config access (95) = 175
|
|
524
|
+
→ Exceeds threshold → Ban triggered
|
|
525
|
+
→ Different decay rates for each violation type
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Configuration Profiles:**
|
|
529
|
+
|
|
530
|
+
```ruby
|
|
531
|
+
# 🔥 STRICT - High-security environment (financial, healthcare)
|
|
532
|
+
Beskar.configure do |config|
|
|
533
|
+
config.waf[:enabled] = true
|
|
534
|
+
config.waf[:auto_block] = true
|
|
535
|
+
config.waf[:score_threshold] = 100 # Lower threshold = faster blocking
|
|
536
|
+
config.waf[:violation_window] = 12.hours # Longer memory
|
|
537
|
+
config.waf[:permanent_block_after] = 300 # Permanent ban at 300 cumulative score
|
|
538
|
+
config.waf[:block_durations] = [6.hours, 24.hours, 7.days, 30.days]
|
|
539
|
+
|
|
540
|
+
# Slower decay = violations persist longer
|
|
541
|
+
config.waf[:decay_rates] = {
|
|
542
|
+
critical: 720, # 12 hour half-life (very persistent)
|
|
543
|
+
high: 360, # 6 hour half-life
|
|
544
|
+
medium: 120, # 2 hour half-life
|
|
545
|
+
low: 30 # 30 minute half-life
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
# Exclude legitimate 404-prone paths
|
|
549
|
+
config.waf[:record_not_found_exclusions] = [
|
|
550
|
+
%r{/posts/.*}, %r{/articles/\d+}, %r{/public/.*}
|
|
551
|
+
]
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# ⚖️ BALANCED - Default production (recommended for most apps)
|
|
555
|
+
Beskar.configure do |config|
|
|
556
|
+
config.waf[:enabled] = true
|
|
557
|
+
config.waf[:auto_block] = true
|
|
558
|
+
config.waf[:score_threshold] = 150 # Default threshold
|
|
559
|
+
config.waf[:violation_window] = 6.hours # Standard window
|
|
560
|
+
config.waf[:permanent_block_after] = 500 # Permanent at 500 cumulative
|
|
561
|
+
config.waf[:decay_enabled] = true
|
|
562
|
+
# Uses default decay rates (critical: 360, high: 120, medium: 45, low: 15)
|
|
563
|
+
|
|
564
|
+
config.waf[:record_not_found_exclusions] = [
|
|
565
|
+
%r{/posts/.*}, %r{/products/[\\w-]+}
|
|
566
|
+
]
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# 🧪 PERMISSIVE - High-traffic public site with many 404s
|
|
570
|
+
Beskar.configure do |config|
|
|
571
|
+
config.waf[:enabled] = true
|
|
572
|
+
config.waf[:auto_block] = true
|
|
573
|
+
config.waf[:score_threshold] = 200 # Higher tolerance
|
|
574
|
+
config.waf[:violation_window] = 3.hours # Shorter memory
|
|
575
|
+
config.waf[:permanent_block_after] = 800 # Rare permanent bans
|
|
576
|
+
|
|
577
|
+
# Faster decay = violations forgotten quickly
|
|
578
|
+
config.waf[:decay_rates] = {
|
|
579
|
+
critical: 180, # 3 hour half-life
|
|
580
|
+
high: 60, # 1 hour half-life
|
|
581
|
+
medium: 20, # 20 minute half-life
|
|
582
|
+
low: 5 # 5 minute half-life (very forgiving)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# Extensive exclusions for public content
|
|
586
|
+
config.waf[:record_not_found_exclusions] = [
|
|
587
|
+
%r{/posts/.*}, %r{/articles/.*}, %r{/tags/.*},
|
|
588
|
+
%r{/search/.*}, %r{/public/.*}, %r{/assets/.*}
|
|
589
|
+
]
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# 🔍 MONITOR ONLY - Testing/staging (recommended before going live)
|
|
593
|
+
Beskar.configure do |config|
|
|
594
|
+
config.monitor_only = true # Log violations but NEVER block
|
|
595
|
+
config.waf[:enabled] = true
|
|
596
|
+
config.waf[:create_security_events] = true
|
|
597
|
+
config.ip_whitelist = ["127.0.0.1", "::1"] # Whitelist localhost
|
|
598
|
+
end
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Blocking Behavior:**
|
|
602
|
+
|
|
603
|
+
With **default settings** (score_threshold: 150):
|
|
604
|
+
- **Violations accumulate**: Each violation adds points based on severity
|
|
605
|
+
- **Score threshold reached**: IP automatically banned when cumulative score ≥ 150
|
|
606
|
+
- **Exponential decay**: Violations lose impact over time based on severity
|
|
607
|
+
- **Ban duration escalates**: Based on total score accumulated:
|
|
608
|
+
- 150-300 points → 1 hour ban
|
|
609
|
+
- 300-450 points → 6 hour ban
|
|
610
|
+
- 450-600 points → 24 hour ban
|
|
611
|
+
- 600+ points → 7 day ban
|
|
612
|
+
- 500+ cumulative score → **permanent ban**
|
|
613
|
+
|
|
614
|
+
**Key Advantages:**
|
|
615
|
+
- **Fewer false positives**: Low-severity violations (404s) decay quickly
|
|
616
|
+
- **Faster response to serious threats**: Critical violations persist longer
|
|
617
|
+
- **Adaptive blocking**: Mixed attack patterns properly weighted
|
|
618
|
+
- **Monitor mode compatible**: Set `config.monitor_only = true` to log without blocking
|
|
619
|
+
|
|
620
|
+
> **Production Tip:** Start with monitor mode for 24-48 hours to observe your traffic patterns, then adjust thresholds and exclusions before enabling blocking.
|
|
621
|
+
|
|
622
|
+
**Check WAF status:**
|
|
623
|
+
```ruby
|
|
624
|
+
# Get current risk score (with decay applied)
|
|
625
|
+
current_score = Beskar::Services::Waf.get_current_score(ip_address)
|
|
626
|
+
# => 145.3 (below threshold, not blocked)
|
|
627
|
+
|
|
628
|
+
# Get number of violations tracked
|
|
629
|
+
violation_count = Beskar::Services::Waf.get_violation_count(ip_address)
|
|
630
|
+
# => 3 (number of violations being tracked)
|
|
631
|
+
|
|
632
|
+
# Get detailed violation records
|
|
633
|
+
violations = Beskar::Services::Waf.get_violations(ip_address)
|
|
634
|
+
# => [{timestamp: ..., score: 95, severity: :critical, category: :config_files}, ...]
|
|
635
|
+
|
|
636
|
+
# Reset violations (admin action)
|
|
637
|
+
Beskar::Services::Waf.reset_violations(ip_address)
|
|
638
|
+
|
|
639
|
+
# Analyze a request without blocking
|
|
640
|
+
waf_analysis = Beskar::Services::Waf.analyze_request(request)
|
|
641
|
+
if waf_analysis
|
|
642
|
+
puts "Detected: #{waf_analysis[:patterns].map { |p| p[:description] }}"
|
|
643
|
+
puts "Severity: #{waf_analysis[:highest_severity]}"
|
|
644
|
+
puts "Risk Score: #{waf_analysis[:risk_score]}"
|
|
645
|
+
end
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### IP Blocking and Banning
|
|
649
|
+
|
|
650
|
+
Beskar uses a hybrid cache + database blocking system that persists across application restarts.
|
|
651
|
+
|
|
652
|
+
**Automatic IP Banning Thresholds:**
|
|
653
|
+
|
|
654
|
+
| Trigger | Threshold | Time Window | Ban Duration | Configurable |
|
|
655
|
+
|---------|-----------|-------------|--------------|--------------|
|
|
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] |
|
|
659
|
+
|
|
660
|
+
> **Note:** All ban durations escalate on repeat offenses: 1h → 6h → 24h → 7d → permanent
|
|
661
|
+
|
|
662
|
+
**Manual IP Management:**
|
|
663
|
+
|
|
664
|
+
```ruby
|
|
665
|
+
# Ban an IP address
|
|
666
|
+
Beskar::BannedIp.ban!(
|
|
667
|
+
"203.0.113.50",
|
|
668
|
+
reason: "manual_block",
|
|
669
|
+
duration: 24.hours,
|
|
670
|
+
details: "Suspicious activity reported by admin",
|
|
671
|
+
metadata: { reporter: "admin@example.com", ticket: "#12345" }
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# Permanent ban
|
|
675
|
+
Beskar::BannedIp.ban!(
|
|
676
|
+
"203.0.113.51",
|
|
677
|
+
reason: "confirmed_attack",
|
|
678
|
+
permanent: true,
|
|
679
|
+
details: "Confirmed malicious actor"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Check if IP is banned
|
|
683
|
+
Beskar::BannedIp.banned?("203.0.113.50") # => true
|
|
684
|
+
|
|
685
|
+
# Unban an IP
|
|
686
|
+
Beskar::BannedIp.unban!("203.0.113.50")
|
|
687
|
+
|
|
688
|
+
# Extend existing ban
|
|
689
|
+
ban = Beskar::BannedIp.find_by(ip_address: "203.0.113.50")
|
|
690
|
+
ban.extend_ban!(12.hours) # Add 12 hours to current expiry
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**Query banned IPs:**
|
|
694
|
+
|
|
695
|
+
```ruby
|
|
696
|
+
# Get all active bans
|
|
697
|
+
Beskar::BannedIp.active
|
|
698
|
+
|
|
699
|
+
# Get permanent bans
|
|
700
|
+
Beskar::BannedIp.permanent
|
|
701
|
+
|
|
702
|
+
# Get expired bans (not enforced but in database)
|
|
703
|
+
Beskar::BannedIp.expired
|
|
704
|
+
|
|
705
|
+
# Find bans by reason
|
|
706
|
+
Beskar::BannedIp.where(reason: 'waf_violation')
|
|
707
|
+
Beskar::BannedIp.where(reason: 'authentication_abuse')
|
|
708
|
+
Beskar::BannedIp.where(reason: 'rate_limit_abuse')
|
|
709
|
+
|
|
710
|
+
# Cleanup expired bans from database
|
|
711
|
+
removed_count = Beskar::BannedIp.cleanup_expired!
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
**Preload cache on startup:**
|
|
715
|
+
|
|
716
|
+
The cache is automatically preloaded when your app starts, but you can manually trigger it:
|
|
717
|
+
|
|
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
|
+
```
|
|
724
|
+
|
|
725
|
+
### Security Events and Monitoring
|
|
726
|
+
|
|
727
|
+
**Query WAF violations:**
|
|
728
|
+
|
|
729
|
+
```ruby
|
|
730
|
+
# Recent WAF violations
|
|
731
|
+
waf_events = Beskar::SecurityEvent
|
|
732
|
+
.where(event_type: 'waf_violation')
|
|
733
|
+
.where('created_at > ?', 24.hours.ago)
|
|
734
|
+
.order(created_at: :desc)
|
|
735
|
+
|
|
736
|
+
# High-risk WAF events
|
|
737
|
+
high_risk = Beskar::SecurityEvent
|
|
738
|
+
.where(event_type: 'waf_violation')
|
|
739
|
+
.where('risk_score >= ?', 80)
|
|
740
|
+
.includes(:user)
|
|
741
|
+
|
|
742
|
+
# Group by IP to find repeat offenders
|
|
743
|
+
repeat_offenders = Beskar::SecurityEvent
|
|
744
|
+
.where(event_type: 'waf_violation')
|
|
745
|
+
.where('created_at > ?', 7.days.ago)
|
|
746
|
+
.group(:ip_address)
|
|
747
|
+
.having('COUNT(*) >= ?', 5)
|
|
748
|
+
.count
|
|
749
|
+
|
|
750
|
+
# WAF violations by pattern type
|
|
751
|
+
waf_events.each do |event|
|
|
752
|
+
patterns = event.metadata['waf_analysis']['patterns']
|
|
753
|
+
patterns.each do |pattern|
|
|
754
|
+
puts "#{event.ip_address}: #{pattern['category']} - #{pattern['description']}"
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
**Scheduled maintenance:**
|
|
760
|
+
|
|
761
|
+
```ruby
|
|
762
|
+
# In a background job (e.g., daily)
|
|
763
|
+
class SecurityCleanupJob < ApplicationJob
|
|
764
|
+
def perform
|
|
765
|
+
# Remove expired bans from database
|
|
766
|
+
removed = Beskar::BannedIp.cleanup_expired!
|
|
767
|
+
Rails.logger.info "Cleaned up #{removed} expired IP bans"
|
|
768
|
+
|
|
769
|
+
# Archive old security events (optional)
|
|
770
|
+
Beskar::SecurityEvent.where('created_at < ?', 90.days.ago).delete_all
|
|
771
|
+
|
|
772
|
+
# Generate security report (example)
|
|
773
|
+
report = {
|
|
774
|
+
active_bans: Beskar::BannedIp.active.count,
|
|
775
|
+
permanent_bans: Beskar::BannedIp.permanent.count,
|
|
776
|
+
waf_violations_today: Beskar::SecurityEvent.where(
|
|
777
|
+
event_type: 'waf_violation',
|
|
778
|
+
created_at: 24.hours.ago..Time.current
|
|
779
|
+
).count
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
# Send to monitoring service
|
|
783
|
+
Rails.logger.info "Security Report: #{report}"
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Middleware Integration
|
|
789
|
+
|
|
790
|
+
Beskar automatically injects its middleware (`Beskar::Middleware::RequestAnalyzer`) into the Rails stack to provide comprehensive request-level protection.
|
|
791
|
+
|
|
792
|
+
**Request Processing Order:**
|
|
793
|
+
|
|
794
|
+
Every request passes through these security checks in order:
|
|
795
|
+
|
|
796
|
+
1. **Whitelist Check** - Determine if IP is whitelisted (bypasses blocking but still logs)
|
|
797
|
+
2. **Banned IP Check** - Block immediately if IP is banned (403 Forbidden)
|
|
798
|
+
3. **Rate Limiting** - Check rate limits (429 Too Many Requests if exceeded)
|
|
799
|
+
4. **WAF Analysis** - Scan for vulnerability patterns (403 Forbidden if detected and threshold met)
|
|
800
|
+
5. **Request Processing** - Continue to application if all checks pass
|
|
801
|
+
|
|
802
|
+
**Features:**
|
|
803
|
+
- **Early exit** - Banned IPs are blocked immediately for performance
|
|
804
|
+
- **Whitelist bypass** - Trusted IPs bypass all blocking but activity is logged
|
|
805
|
+
- **Auto-blocking** - See [Automatic IP Banning Thresholds](#ip-blocking-and-banning) section for details
|
|
806
|
+
- **Custom error pages** - Returns helpful 403/429 error responses
|
|
807
|
+
- **Response headers** - Adds `X-Beskar-Blocked` and `X-Beskar-Rate-Limited` headers
|
|
808
|
+
- **Graceful degradation** - Continues working if cache is unavailable
|
|
809
|
+
|
|
810
|
+
**Middleware Logs:**
|
|
811
|
+
|
|
812
|
+
The middleware generates structured log messages for monitoring:
|
|
813
|
+
|
|
814
|
+
```
|
|
815
|
+
[Beskar::Middleware] Blocked request from banned IP: 203.0.113.50
|
|
816
|
+
[Beskar::Middleware] Rate limit exceeded for IP: 203.0.113.51
|
|
817
|
+
[Beskar::Middleware] WAF violation from whitelisted IP 192.168.1.100 (not blocking): WordPress vulnerability scan
|
|
818
|
+
[Beskar::Middleware] 🔒 Auto-blocked IP 203.0.113.52 after 3 WAF violations (duration: 1 hours)
|
|
819
|
+
[Beskar::Middleware] 🔒 Auto-blocked IP 203.0.113.53 for authentication brute force (15 failures)
|
|
820
|
+
```
|
|
62
821
|
|
|
63
822
|
Security events are logged to the `beskar_security_events` table for analysis and will be visualized in the forthcoming security dashboard.
|
|
64
823
|
|
|
824
|
+
## WAF Pattern Reference
|
|
825
|
+
|
|
826
|
+
| Category | Severity | Example Patterns | Risk Score |
|
|
827
|
+
|----------|----------|------------------|------------|
|
|
828
|
+
| WordPress Scans | High | `/wp-admin`, `/wp-login.php`, `/wp-content/*.php` | 80 |
|
|
829
|
+
| WordPress Static Files | Low | `/wp-content/*.css`, `/wp-content/*.jpg` | 30 |
|
|
830
|
+
| PHP Admin Panels | High | `/phpmyadmin`, `/admin.php`, `/phpinfo.php` | 80 |
|
|
831
|
+
| Config Files | **Critical** | `/.env`, `/.git`, `/database.yml`, `/config.php` | **95** |
|
|
832
|
+
| Path Traversal | **Critical** | `/../../../etc/passwd`, `%2e%2e/` | **95** |
|
|
833
|
+
| Framework Debug | Medium | `/rails/info/routes`, `/__debug__`, `/telescope` | 60 |
|
|
834
|
+
| CMS Detection | Medium | `/joomla`, `/drupal`, `/magento` | 60 |
|
|
835
|
+
| Common Exploits | **Critical** | `/shell.php`, `/c99.php`, `/webshell` | **95** |
|
|
836
|
+
| UnknownFormat Exception | Medium | `/users/1.exe`, `/api/data.bat` | 60 |
|
|
837
|
+
| 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 |
|
|
840
|
+
|
|
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
|
|
846
|
+
|
|
847
|
+
## Security Best Practices
|
|
848
|
+
|
|
849
|
+
### 1. Start with Monitor Mode
|
|
850
|
+
|
|
851
|
+
When first enabling WAF, use monitor-only mode to tune thresholds:
|
|
852
|
+
|
|
853
|
+
```ruby
|
|
854
|
+
config.monitor_only = true # Log but don't block
|
|
855
|
+
config.waf[:enabled] = true
|
|
856
|
+
config.waf[:create_security_events] = true
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
After reviewing logs for false positives, enable blocking:
|
|
860
|
+
|
|
861
|
+
```ruby
|
|
862
|
+
config.monitor_only = false
|
|
863
|
+
config.waf[:enabled] = true
|
|
864
|
+
config.waf[:auto_block] = true
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### 2. Whitelist Carefully
|
|
868
|
+
|
|
869
|
+
Only whitelist truly trusted IPs:
|
|
870
|
+
|
|
871
|
+
```ruby
|
|
872
|
+
# ✅ Good - Documented, legitimate sources
|
|
873
|
+
config.ip_whitelist = [
|
|
874
|
+
"203.0.113.0/24", # Office network - IT approved
|
|
875
|
+
"198.51.100.50" # VPN gateway - documented in wiki
|
|
876
|
+
]
|
|
877
|
+
|
|
878
|
+
# ❌ Bad - Whitelisting unknown IPs
|
|
879
|
+
config.ip_whitelist = ["0.0.0.0/0"] # Never do this!
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### 3. Regular Maintenance
|
|
883
|
+
|
|
884
|
+
Set up a scheduled job to clean up old data:
|
|
885
|
+
|
|
886
|
+
```ruby
|
|
887
|
+
# Schedule daily via cron or Sidekiq
|
|
888
|
+
SecurityCleanupJob.perform_later
|
|
889
|
+
|
|
890
|
+
# Or in initializer for quick cleanup on restart
|
|
891
|
+
Rails.application.config.after_initialize do
|
|
892
|
+
Beskar::BannedIp.cleanup_expired! if Rails.env.production?
|
|
893
|
+
end
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### 4. Monitor Security Events
|
|
897
|
+
|
|
898
|
+
Set up alerts for high-risk events:
|
|
899
|
+
|
|
900
|
+
```ruby
|
|
901
|
+
# Example monitoring
|
|
902
|
+
high_risk_count = Beskar::SecurityEvent
|
|
903
|
+
.where('created_at > ?', 1.hour.ago)
|
|
904
|
+
.where('risk_score >= ?', 80)
|
|
905
|
+
.count
|
|
906
|
+
|
|
907
|
+
alert_service.notify if high_risk_count > 10
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
### 5. Document Whitelist Changes
|
|
911
|
+
|
|
912
|
+
Keep a record of why each IP is whitelisted:
|
|
913
|
+
|
|
914
|
+
```ruby
|
|
915
|
+
# config/initializers/beskar.rb
|
|
916
|
+
config.ip_whitelist = [
|
|
917
|
+
"203.0.113.0/24", # Office HQ network (added 2024-01-15, ticket #1234)
|
|
918
|
+
"198.51.100.50", # Partner API server (added 2024-02-01, contract #5678)
|
|
919
|
+
"192.0.2.10" # Security scanner (added 2024-03-01, vendor: SecurityCo)
|
|
920
|
+
]
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### 6. Test WAF in Staging
|
|
924
|
+
|
|
925
|
+
Before deploying to production, test WAF rules in staging to catch false positives.
|
|
926
|
+
|
|
927
|
+
### 7. Review Ban Reasons
|
|
928
|
+
|
|
929
|
+
Periodically review banned IPs to ensure blocking is working correctly:
|
|
930
|
+
|
|
931
|
+
```ruby
|
|
932
|
+
# Check recent auto-bans
|
|
933
|
+
recent_bans = Beskar::BannedIp
|
|
934
|
+
.where('created_at > ?', 7.days.ago)
|
|
935
|
+
.group(:reason)
|
|
936
|
+
.count
|
|
937
|
+
|
|
938
|
+
# Review specific ban details
|
|
939
|
+
waf_bans = Beskar::BannedIp.where(reason: 'waf_violation')
|
|
940
|
+
waf_bans.each do |ban|
|
|
941
|
+
puts "#{ban.ip_address}: #{ban.details} (violations: #{ban.violation_count})"
|
|
942
|
+
end
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
## Troubleshooting
|
|
946
|
+
|
|
947
|
+
### Issue: Legitimate users being blocked
|
|
948
|
+
|
|
949
|
+
**Solution:** Add their IP to whitelist or reduce WAF `block_threshold`:
|
|
950
|
+
|
|
951
|
+
```ruby
|
|
952
|
+
config.waf[:block_threshold] = 5 # Increase from default 3
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Or whitelist specific IPs:
|
|
956
|
+
```ruby
|
|
957
|
+
config.ip_whitelist = ["user.ip.address.here"]
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
### Issue: Too many false positives
|
|
961
|
+
|
|
962
|
+
**Solution:** Enable monitor-only mode and review patterns:
|
|
963
|
+
|
|
964
|
+
```ruby
|
|
965
|
+
config.monitor_only = true # This is a global setting, not WAF-specific
|
|
966
|
+
|
|
967
|
+
# Review what's being flagged
|
|
968
|
+
Beskar::SecurityEvent.where(event_type: 'waf_violation').last(20).each do |event|
|
|
969
|
+
puts "Path: #{event.metadata['request_path']}"
|
|
970
|
+
puts "Patterns: #{event.metadata['waf_analysis']['patterns']}"
|
|
971
|
+
end
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
### Issue: Banned IPs persist after restart
|
|
975
|
+
|
|
976
|
+
**Solution:** This is intentional (database persistence). To unban:
|
|
977
|
+
|
|
978
|
+
```ruby
|
|
979
|
+
Beskar::BannedIp.unban!("ip.address.here")
|
|
980
|
+
|
|
981
|
+
# Or unban all expired
|
|
982
|
+
Beskar::BannedIp.cleanup_expired!
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### Issue: Performance concerns
|
|
986
|
+
|
|
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
|
+
```
|
|
999
|
+
|
|
1000
|
+
## Migration from Previous Versions
|
|
1001
|
+
|
|
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
|
+
```
|
|
1017
|
+
|
|
1018
|
+
## Performance Characteristics
|
|
1019
|
+
|
|
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
|
|
1031
|
+
|
|
65
1032
|
## Development
|
|
66
1033
|
|
|
67
1034
|
After checking out the repo, run `bundle install` to install dependencies. The gem contains a dummy Rails application in `test/dummy` for development and testing.
|
|
@@ -75,7 +1042,7 @@ $ bin/rails test
|
|
|
75
1042
|
|
|
76
1043
|
## Contributing
|
|
77
1044
|
|
|
78
|
-
Bug reports and pull requests are welcome on GitHub at [https://github.com/prograis/beskar](https://github.com/prograils/beskar).
|
|
1045
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/prograis/beskar](https://github.com/prograils/beskar).
|
|
79
1046
|
|
|
80
1047
|
## License
|
|
81
1048
|
|
|
@@ -84,4 +1051,3 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
84
1051
|
## Code of Conduct
|
|
85
1052
|
|
|
86
1053
|
Just be nice to each other.
|
|
87
|
-
|