beskar 0.0.2 → 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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.md +298 -110
  4. data/app/controllers/beskar/application_controller.rb +170 -0
  5. data/app/controllers/beskar/banned_ips_controller.rb +280 -0
  6. data/app/controllers/beskar/dashboard_controller.rb +70 -0
  7. data/app/controllers/beskar/security_events_controller.rb +182 -0
  8. data/app/controllers/concerns/beskar/controllers/security_tracking.rb +6 -6
  9. data/app/models/beskar/banned_ip.rb +68 -27
  10. data/app/models/beskar/security_event.rb +14 -0
  11. data/app/services/beskar/banned_ip_manager.rb +78 -0
  12. data/app/views/beskar/banned_ips/edit.html.erb +259 -0
  13. data/app/views/beskar/banned_ips/index.html.erb +361 -0
  14. data/app/views/beskar/banned_ips/new.html.erb +310 -0
  15. data/app/views/beskar/banned_ips/show.html.erb +310 -0
  16. data/app/views/beskar/dashboard/index.html.erb +280 -0
  17. data/app/views/beskar/security_events/index.html.erb +309 -0
  18. data/app/views/beskar/security_events/show.html.erb +307 -0
  19. data/app/views/layouts/beskar/application.html.erb +647 -5
  20. data/config/routes.rb +41 -0
  21. data/lib/beskar/configuration.rb +24 -10
  22. data/lib/beskar/engine.rb +4 -4
  23. data/lib/beskar/logger.rb +293 -0
  24. data/lib/beskar/middleware/request_analyzer.rb +128 -53
  25. data/lib/beskar/models/security_trackable_authenticable.rb +11 -11
  26. data/lib/beskar/models/security_trackable_devise.rb +5 -5
  27. data/lib/beskar/models/security_trackable_generic.rb +12 -12
  28. data/lib/beskar/services/account_locker.rb +12 -12
  29. data/lib/beskar/services/geolocation_service.rb +8 -8
  30. data/lib/beskar/services/ip_whitelist.rb +2 -2
  31. data/lib/beskar/services/waf.rb +307 -78
  32. data/lib/beskar/version.rb +1 -1
  33. data/lib/beskar.rb +1 -0
  34. data/lib/generators/beskar/install/install_generator.rb +158 -0
  35. data/lib/generators/beskar/install/templates/initializer.rb.tt +177 -0
  36. data/lib/tasks/beskar_tasks.rake +11 -2
  37. metadata +35 -6
  38. data/lib/beskar/templates/beskar_initializer.rb +0 -107
@@ -3,11 +3,37 @@ module Beskar
3
3
  class Waf
4
4
  # Common vulnerability scan patterns
5
5
  VULNERABILITY_PATTERNS = {
6
+ rails_exceptions: {
7
+ patterns: [
8
+ %r{/(?:users?|posts?|articles?|comments?|api/v\d+/\w+)/\d+\.(?:exe|bat|cmd|com|scr|vbs|jar|app|deb|rpm)$}i, # Rails resources with executable extensions
9
+ %r{/(?:users?|posts?|articles?|comments?|api/v\d+/\w+)\.(?:asp|aspx|jsp|do|action|cgi|pl|py|rb)$}i, # Rails routes with server-side script extensions
10
+ %r{\?format=(?:exe|bat|cmd|com|scr|vbs|jar|asp|aspx|jsp|php)$}i, # Suspicious format in query params
11
+ ],
12
+ severity: :medium,
13
+ description: "Potential Rails exception triggering attempt"
14
+ },
15
+ ip_spoofing: {
16
+ patterns: [
17
+ %r{X-Forwarded-For.*X-Forwarded-For}i, # Multiple X-Forwarded-For headers in path (suspicious)
18
+ %r{Client-IP.*X-Forwarded-For}i, # Conflicting IP headers in path
19
+ ],
20
+ severity: :high,
21
+ description: "Potential IP spoofing attempt"
22
+ },
23
+ record_scanning: {
24
+ patterns: [
25
+ %r{/(?:user|admin|account|profile|order|payment|invoice|document|file|download)/\d{6,}}i, # Large IDs that likely don't exist
26
+ %r{/(?:user|admin|account|profile)/(?:test|admin|root|administrator|superuser)}i, # Common test usernames
27
+ %r{/api/v\d+/(?:users?|accounts?|orders?|payments?)/(?:999999|123456|0|null|undefined)}i, # Obviously fake API IDs
28
+ ],
29
+ severity: :low,
30
+ description: "Potential record enumeration/scanning"
31
+ },
6
32
  wordpress: {
7
33
  patterns: [
8
34
  %r{/wp-admin}i,
9
35
  %r{/wp-login\.php}i,
10
- %r{/wp-content}i,
36
+ %r{/wp-content/.*\.php}i, # PHP files in wp-content are suspicious
11
37
  %r{/wp-includes}i,
12
38
  %r{/xmlrpc\.php}i,
13
39
  %r{/wp-config\.php}i,
@@ -17,6 +43,14 @@ module Beskar
17
43
  severity: :high,
18
44
  description: "WordPress vulnerability scan"
19
45
  },
46
+ wordpress_static: {
47
+ patterns: [
48
+ %r{/wp-content/.*\.(?:css|js|jpe?g|png|gif|svg|webp|ico|woff2?|ttf|eot|map)$}i, # Static files in wp-content
49
+ %r{/wp-content/(?:uploads|themes|plugins)/[^.]*$}i, # Directory listing attempts
50
+ ],
51
+ severity: :low,
52
+ description: "WordPress static file probe"
53
+ },
20
54
  php_admin: {
21
55
  patterns: [
22
56
  %r{/phpmyadmin}i,
@@ -92,6 +126,13 @@ module Beskar
92
126
  }
93
127
  }.freeze
94
128
 
129
+ # Configuration for RecordNotFound exclusion patterns
130
+ # These patterns will not trigger WAF violations
131
+ RECORD_NOT_FOUND_EXCLUSIONS = [
132
+ # Add default exclusions here if needed
133
+ # %r{/posts/.*}, # Example: exclude all posts paths
134
+ ].freeze
135
+
95
136
  class << self
96
137
  # Analyze a request for vulnerability scanning patterns
97
138
  def analyze_request(request)
@@ -128,17 +169,109 @@ module Beskar
128
169
  end
129
170
  end
130
171
 
172
+ # Analyze Rails exceptions as potential security threats
173
+ def analyze_exception(exception, request)
174
+ case exception
175
+ when ActionController::UnknownFormat
176
+ {
177
+ threat_detected: true,
178
+ patterns: [{
179
+ category: :unknown_format,
180
+ pattern: "ActionController::UnknownFormat",
181
+ severity: :medium,
182
+ description: "Unknown format requested - potential scanner",
183
+ matched_path: request.fullpath
184
+ }],
185
+ highest_severity: :medium,
186
+ ip_address: request.ip,
187
+ user_agent: request.user_agent,
188
+ timestamp: Time.current,
189
+ exception_class: exception.class.name,
190
+ exception_message: exception.message
191
+ }
192
+ when ActionDispatch::RemoteIp::IpSpoofAttackError
193
+ {
194
+ threat_detected: true,
195
+ patterns: [{
196
+ category: :ip_spoof,
197
+ pattern: "ActionDispatch::RemoteIp::IpSpoofAttackError",
198
+ severity: :critical,
199
+ description: "IP spoofing attack detected",
200
+ matched_path: request.fullpath
201
+ }],
202
+ highest_severity: :critical,
203
+ ip_address: request.ip,
204
+ user_agent: request.user_agent,
205
+ timestamp: Time.current,
206
+ exception_class: exception.class.name,
207
+ exception_message: exception.message
208
+ }
209
+ when ActiveRecord::RecordNotFound
210
+ # Check if this path should be excluded from WAF
211
+ if should_exclude_record_not_found?(request.fullpath)
212
+ return nil
213
+ end
214
+
215
+ {
216
+ threat_detected: true,
217
+ patterns: [{
218
+ category: :record_not_found,
219
+ pattern: "ActiveRecord::RecordNotFound",
220
+ severity: :low,
221
+ description: "Record not found - potential enumeration scan",
222
+ matched_path: request.fullpath
223
+ }],
224
+ highest_severity: :low,
225
+ ip_address: request.ip,
226
+ user_agent: request.user_agent,
227
+ timestamp: Time.current,
228
+ exception_class: exception.class.name,
229
+ exception_message: exception.message
230
+ }
231
+ when ActionDispatch::Http::MimeNegotiation::InvalidType
232
+ {
233
+ threat_detected: true,
234
+ patterns: [{
235
+ category: :invalid_mime_type,
236
+ pattern: "ActionDispatch::Http::MimeNegotiation::InvalidType",
237
+ severity: :medium,
238
+ description: "Invalid MIME type requested - potential scanner",
239
+ matched_path: request.fullpath
240
+ }],
241
+ highest_severity: :medium,
242
+ ip_address: request.ip,
243
+ user_agent: request.user_agent,
244
+ timestamp: Time.current,
245
+ exception_class: exception.class.name,
246
+ exception_message: exception.message
247
+ }
248
+ else
249
+ nil
250
+ end
251
+ end
252
+
253
+ # Check if a RecordNotFound exception should be excluded from WAF
254
+ def should_exclude_record_not_found?(path)
255
+ return false if path.blank?
256
+
257
+ # Check configured exclusions
258
+ exclusions = waf_config[:record_not_found_exclusions] || []
259
+ all_exclusions = RECORD_NOT_FOUND_EXCLUSIONS + exclusions
260
+
261
+ all_exclusions.any? { |pattern| path.match?(pattern) }
262
+ end
263
+
131
264
  # Check if request should be blocked based on violation history
132
265
  def should_block?(ip_address)
133
266
  config = waf_config
134
267
  return false unless config[:enabled]
135
268
  return false unless config[:auto_block]
136
269
 
137
- # Check violation count in cache
138
- violation_count = get_violation_count(ip_address)
139
- threshold = config[:block_threshold] || 3
270
+ # Get current risk score (with decay applied)
271
+ current_score = get_current_score(ip_address)
272
+ threshold = config[:score_threshold] || 150
140
273
 
141
- violation_count >= threshold
274
+ current_score >= threshold
142
275
  end
143
276
 
144
277
  # Record a WAF violation
@@ -146,42 +279,83 @@ module Beskar
146
279
  config = waf_config
147
280
  return unless config[:enabled]
148
281
 
149
- # Increment violation count
282
+ Beskar::Logger.debug("[WAF] Recording violation for IP: #{ip_address}, whitelisted: #{whitelisted}", component: :WAF)
283
+
284
+ # Get current violations and add new one
150
285
  cache_key = "beskar:waf_violations:#{ip_address}"
151
- current_count = Rails.cache.read(cache_key) || 0
152
- new_count = current_count + 1
153
-
154
- # Store with TTL from config (default 1 hour)
155
- ttl = config[:violation_window] || 1.hour
156
- Rails.cache.write(cache_key, new_count, expires_in: ttl)
286
+ violations = Rails.cache.read(cache_key) || []
287
+
288
+ # Calculate risk score for this violation
289
+ risk_score = severity_to_risk_score(analysis_result[:highest_severity])
290
+
291
+ # Create new violation record
292
+ new_violation = {
293
+ timestamp: Time.current.to_i,
294
+ score: risk_score,
295
+ severity: analysis_result[:highest_severity],
296
+ category: analysis_result[:patterns].first[:category],
297
+ description: analysis_result[:patterns].first[:description],
298
+ path: analysis_result[:patterns].first[:matched_path]
299
+ }
300
+
301
+ violations << new_violation
302
+
303
+ # Prune old violations (outside violation window and max tracked)
304
+ violations = prune_violations(violations, config)
305
+
306
+ # Calculate current score with decay
307
+ current_score = calculate_current_score(violations, config)
308
+
309
+ Beskar::Logger.debug("[WAF] Violation recorded for #{ip_address}: score=#{risk_score}, current_total=#{current_score.round(2)}, violations_count=#{violations.size}", component: :WAF)
310
+
311
+ # Store violations with TTL from config
312
+ ttl = config[:violation_window] || 6.hours
313
+ Rails.cache.write(cache_key, violations, expires_in: ttl)
157
314
 
158
315
  # Log the violation
159
- log_violation(ip_address, analysis_result, new_count)
316
+ log_violation(ip_address, analysis_result, current_score, violations.size)
160
317
 
161
318
  # Create security event if configured
162
319
  if config[:create_security_events]
163
- create_security_event(ip_address, analysis_result)
320
+ create_security_event(ip_address, analysis_result, current_score)
164
321
  end
165
322
 
166
- # Check if we should auto-block (skip if whitelisted, in monitor-only mode)
167
- threshold = config[:block_threshold] || 3
168
-
169
- if !whitelisted && config[:auto_block] && new_count >= threshold
170
- if config[:monitor_only]
171
- # Monitor-only mode: Log what WOULD happen but don't block
172
- log_monitor_only_action(ip_address, analysis_result, new_count, threshold)
173
- else
174
- auto_block_ip(ip_address, analysis_result, new_count)
323
+ # Check if we should auto-block (skip if whitelisted)
324
+ threshold = config[:score_threshold] || 150
325
+
326
+ Beskar::Logger.debug("[WAF] Auto-block check: whitelisted=#{whitelisted}, auto_block=#{config[:auto_block]}, score=#{current_score.round(2)}, threshold=#{threshold}", component: :WAF)
327
+
328
+ if !whitelisted && config[:auto_block] && current_score >= threshold
329
+ Beskar::Logger.info("[WAF] Score threshold reached for #{ip_address}, creating ban record", component: :WAF)
330
+ # Always create the ban record (even in monitor-only mode)
331
+ auto_block_ip(ip_address, analysis_result, current_score)
332
+
333
+ # But also log monitor-only message if in monitor mode
334
+ if Beskar.configuration.monitor_only?
335
+ log_monitor_only_action(ip_address, analysis_result, current_score, threshold)
175
336
  end
337
+ else
338
+ Beskar::Logger.debug("[WAF] Not auto-blocking: conditions not met", component: :WAF)
176
339
  end
177
340
 
178
- new_count
341
+ current_score
179
342
  end
180
343
 
181
- # Get current violation count for an IP
182
- def get_violation_count(ip_address)
344
+ # Get current risk score for an IP (with decay applied)
345
+ def get_current_score(ip_address)
346
+ violations = get_violations(ip_address)
347
+ calculate_current_score(violations, waf_config)
348
+ end
349
+
350
+ # Get violations for an IP
351
+ def get_violations(ip_address)
183
352
  cache_key = "beskar:waf_violations:#{ip_address}"
184
- Rails.cache.read(cache_key) || 0
353
+ Rails.cache.read(cache_key) || []
354
+ end
355
+
356
+ # Get violation count for an IP (number of violations tracked)
357
+ def get_violation_count(ip_address)
358
+ get_violations(ip_address).size
185
359
  end
186
360
 
187
361
  # Reset violations for an IP
@@ -192,6 +366,48 @@ module Beskar
192
366
 
193
367
  private
194
368
 
369
+ # Calculate current cumulative score with decay applied
370
+ def calculate_current_score(violations, config)
371
+ return 0.0 if violations.empty?
372
+ return violations.sum { |v| v[:score] } unless config[:decay_enabled]
373
+
374
+ now = Time.current.to_i
375
+ decay_rates = config[:decay_rates] || {}
376
+
377
+ violations.sum do |v|
378
+ age_seconds = now - v[:timestamp]
379
+ age_minutes = age_seconds / 60.0
380
+
381
+ # Get half-life for this severity (in minutes)
382
+ half_life = decay_rates[v[:severity]] || 60
383
+
384
+ # Exponential decay: score * (1/2)^(age/half_life)
385
+ # Equivalent to: score * e^(-ln(2) * age / half_life)
386
+ decay_factor = Math.exp(-Math.log(2) * age_minutes / half_life)
387
+
388
+ v[:score] * decay_factor
389
+ end
390
+ end
391
+
392
+ # Prune violations that are outside the window or exceed max tracked
393
+ def prune_violations(violations, config)
394
+ now = Time.current.to_i
395
+ window_seconds = (config[:violation_window] || 6.hours).to_i
396
+ max_tracked = config[:max_violations_tracked] || 50
397
+
398
+ # Remove violations outside the time window
399
+ recent = violations.select do |v|
400
+ (now - v[:timestamp]) <= window_seconds
401
+ end
402
+
403
+ # Keep only the most recent violations if we exceed max_tracked
404
+ if recent.size > max_tracked
405
+ recent.sort_by { |v| -v[:timestamp] }.first(max_tracked)
406
+ else
407
+ recent
408
+ end
409
+ end
410
+
195
411
  # Get WAF configuration
196
412
  def waf_config
197
413
  Beskar.configuration.waf || {}
@@ -207,7 +423,7 @@ module Beskar
207
423
  end
208
424
 
209
425
  # Log WAF violation
210
- def log_violation(ip_address, analysis_result, violation_count)
426
+ def log_violation(ip_address, analysis_result, current_score, violation_count)
211
427
  severity_emoji = {
212
428
  critical: "🚨",
213
429
  high: "⚠️",
@@ -217,40 +433,36 @@ module Beskar
217
433
 
218
434
  emoji = severity_emoji[analysis_result[:highest_severity]] || "🔍"
219
435
  config = waf_config
220
- monitor_mode_notice = config[:monitor_only] ? " [MONITOR-ONLY MODE]" : ""
221
-
222
- Rails.logger.warn(
223
- "[Beskar::WAF] #{emoji} Vulnerability scan detected#{monitor_mode_notice} " \
224
- "(#{violation_count} violations) - " \
436
+ monitor_mode_notice = Beskar.configuration.monitor_only? ? " [MONITOR-ONLY MODE]" : ""
437
+
438
+ Beskar::Logger.warn("#{emoji} Vulnerability scan detected#{monitor_mode_notice} " \
439
+ "(score: #{current_score.round(2)}, violations: #{violation_count}) - " \
225
440
  "IP: #{ip_address}, " \
226
441
  "Severity: #{analysis_result[:highest_severity]}, " \
227
442
  "Patterns: #{analysis_result[:patterns].map { |p| p[:description] }.join(', ')}, " \
228
- "Path: #{analysis_result[:patterns].first[:matched_path]}"
229
- )
443
+ "Path: #{analysis_result[:patterns].first[:matched_path]}", component: :WAF)
230
444
  end
231
445
 
232
446
  # Log what would happen in monitor-only mode (but don't actually block)
233
- def log_monitor_only_action(ip_address, analysis_result, violation_count, threshold)
447
+ def log_monitor_only_action(ip_address, analysis_result, current_score, threshold)
234
448
  config = waf_config
235
- duration = calculate_block_duration(violation_count, config)
236
-
237
- Rails.logger.warn(
238
- "[Beskar::WAF] 🔍 MONITOR-ONLY: IP #{ip_address} WOULD BE BLOCKED " \
239
- "(threshold reached: #{violation_count}/#{threshold} violations) - " \
449
+ duration = calculate_block_duration(current_score, config)
450
+
451
+ Beskar::Logger.warn("🔍 MONITOR-ONLY: IP #{ip_address} WOULD BE BLOCKED " \
452
+ "(score threshold reached: #{current_score.round(2)}/#{threshold}) - " \
240
453
  "Duration would be: #{duration ? "#{duration / 3600.0} hours" : 'PERMANENT'}, " \
241
454
  "Severity: #{analysis_result[:highest_severity]}, " \
242
455
  "Patterns: #{analysis_result[:patterns].map { |p| p[:description] }.join(', ')}. " \
243
- "To enable blocking, set config.waf[:monitor_only] = false"
244
- )
456
+ "To enable blocking, set config.monitor_only = false", component: :WAF)
245
457
  end
246
458
 
247
459
  # Create security event for WAF violation
248
- def create_security_event(ip_address, analysis_result)
460
+ def create_security_event(ip_address, analysis_result, current_score)
249
461
  config = waf_config
250
462
  violation_count = get_violation_count(ip_address)
251
- threshold = config[:block_threshold] || 3
252
- would_be_blocked = violation_count >= threshold
253
-
463
+ threshold = config[:score_threshold] || 150
464
+ would_be_blocked = current_score >= threshold
465
+
254
466
  Beskar::SecurityEvent.create!(
255
467
  event_type: 'waf_violation',
256
468
  ip_address: ip_address,
@@ -260,50 +472,67 @@ module Beskar
260
472
  waf_analysis: analysis_result,
261
473
  patterns_matched: analysis_result[:patterns].map { |p| p[:description] },
262
474
  severity: analysis_result[:highest_severity],
263
- monitor_only_mode: config[:monitor_only],
475
+ monitor_only_mode: Beskar.configuration.monitor_only?,
264
476
  would_be_blocked: would_be_blocked,
265
477
  violation_count: violation_count,
266
- block_threshold: threshold
478
+ current_score: current_score.round(2),
479
+ score_threshold: threshold
267
480
  }
268
481
  )
269
482
  rescue => e
270
- Rails.logger.error "[Beskar::WAF] Failed to create security event: #{e.message}"
483
+ Beskar::Logger.error("Failed to create security event: #{e.message}", component: :WAF)
271
484
  end
272
485
 
273
486
  # Auto-block an IP after threshold violations
274
- def auto_block_ip(ip_address, analysis_result, violation_count)
487
+ def auto_block_ip(ip_address, analysis_result, current_score)
275
488
  config = waf_config
276
- duration = calculate_block_duration(violation_count, config)
277
-
278
- Beskar::BannedIp.ban!(
279
- ip_address,
280
- reason: 'waf_violation',
281
- duration: duration,
282
- permanent: duration.nil?,
283
- details: "WAF violations: #{violation_count} - #{analysis_result[:patterns].map { |p| p[:description] }.join(', ')}",
284
- metadata: {
285
- violation_count: violation_count,
286
- patterns: analysis_result[:patterns],
287
- highest_severity: analysis_result[:highest_severity],
288
- blocked_at: Time.current
289
- }
290
- )
489
+ duration = calculate_block_duration(current_score, config)
490
+ violation_count = get_violation_count(ip_address)
291
491
 
292
- Rails.logger.warn(
293
- "[Beskar::WAF] 🔒 Auto-blocked IP #{ip_address} " \
294
- "after #{violation_count} violations " \
295
- "(duration: #{duration ? "#{duration / 3600} hours" : 'permanent'})"
296
- )
492
+ Beskar::Logger.debug("[WAF] Attempting to ban IP #{ip_address} with duration: #{duration.inspect}", component: :WAF)
493
+
494
+ begin
495
+ banned_ip = Beskar::BannedIp.ban!(
496
+ ip_address,
497
+ reason: 'waf_violation',
498
+ duration: duration,
499
+ permanent: duration.nil?,
500
+ details: "WAF score: #{current_score.round(2)} (#{violation_count} violations) - #{analysis_result[:patterns].map { |p| p[:description] }.join(', ')}",
501
+ metadata: {
502
+ violation_count: violation_count,
503
+ risk_score: current_score.round(2),
504
+ patterns: analysis_result[:patterns],
505
+ highest_severity: analysis_result[:highest_severity],
506
+ blocked_at: Time.current
507
+ }
508
+ )
509
+
510
+ Beskar::Logger.warn("🔒 Auto-blocked IP #{ip_address} " \
511
+ "with score #{current_score.round(2)} (#{violation_count} violations) " \
512
+ "(duration: #{duration ? "#{duration / 3600} hours" : 'permanent'}), " \
513
+ "Ban ID: #{banned_ip.id}", component: :WAF)
514
+ rescue => e
515
+ Beskar::Logger.error("[WAF] Failed to create ban for #{ip_address}: #{e.class} - #{e.message}", component: :WAF)
516
+ raise
517
+ end
297
518
  end
298
519
 
299
- # Calculate block duration based on violation count
300
- def calculate_block_duration(violation_count, config)
301
- return nil if config[:permanent_block_after] && violation_count >= config[:permanent_block_after]
520
+ # Calculate block duration based on cumulative score
521
+ def calculate_block_duration(current_score, config)
522
+ permanent_threshold = config[:permanent_block_after]
523
+ return nil if permanent_threshold && current_score >= permanent_threshold
302
524
 
303
- # Default escalating durations: 1h, 6h, 24h, 7d, permanent
525
+ # Default escalating durations: 1h, 6h, 24h, 7d
304
526
  base_durations = config[:block_durations] || [1.hour, 6.hours, 24.hours, 7.days]
305
- index = [violation_count - (config[:block_threshold] || 3), base_durations.length - 1].min
306
- base_durations[index]
527
+ score_threshold = config[:score_threshold] || 150
528
+
529
+ # Calculate how many times over the threshold we are
530
+ # 150-300 = 1x = 1 hour
531
+ # 300-450 = 2x = 6 hours
532
+ # 450-600 = 3x = 24 hours
533
+ # 600+ = 4x = 7 days
534
+ multiplier = ((current_score / score_threshold).floor - 1).clamp(0, base_durations.length - 1)
535
+ base_durations[multiplier]
307
536
  end
308
537
 
309
538
  # Convert severity level to risk score
@@ -312,7 +541,7 @@ module Beskar
312
541
  when :critical then 95
313
542
  when :high then 80
314
543
  when :medium then 60
315
- when :low then 40
544
+ when :low then 30
316
545
  else 50
317
546
  end
318
547
  end
@@ -1,3 +1,3 @@
1
1
  module Beskar
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/beskar.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "beskar/version"
2
2
  require "beskar/configuration"
3
+ require "beskar/logger"
3
4
  require "beskar/middleware"
4
5
  require "beskar/middleware/request_analyzer"
5
6
  require "beskar/models/security_trackable_generic"
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+
6
+ module Beskar
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ desc "Creates a Beskar initializer and runs migrations"
14
+
15
+ def self.next_migration_number(path)
16
+ if @prev_migration_nr
17
+ @prev_migration_nr += 1
18
+ else
19
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
20
+ end
21
+ @prev_migration_nr.to_s
22
+ end
23
+
24
+ def copy_initializer
25
+ template "initializer.rb.tt", "config/initializers/beskar.rb"
26
+ end
27
+
28
+ def mount_engine
29
+ route_text = "mount Beskar::Engine => '/beskar'"
30
+
31
+ # Check if the route already exists
32
+ if File.read("config/routes.rb").include?(route_text)
33
+ say "Route already mounted, skipping...", :yellow
34
+ else
35
+ route route_text
36
+ say "Mounted Beskar engine at /beskar", :green
37
+ end
38
+ end
39
+
40
+ def copy_migrations
41
+ # Copy migrations from the engine to the host app
42
+ migration_source = File.expand_path("../../../../../db/migrate", __dir__)
43
+
44
+ if Dir.exist?(migration_source)
45
+ Dir.glob("#{migration_source}/*.rb").each do |migration|
46
+ migration_name = File.basename(migration).sub(/^\d+_/, '')
47
+
48
+ # Check if migration already exists
49
+ if migration_already_exists?(migration_name)
50
+ say "Migration #{migration_name} already exists, skipping...", :yellow
51
+ else
52
+ migration_template migration, "db/migrate/#{migration_name}"
53
+ end
54
+ end
55
+ end
56
+
57
+ say "Migrations copied. Run 'rails db:migrate' to create the tables.", :green
58
+ end
59
+
60
+ # CSS Zero is no longer required - styles are embedded in the dashboard
61
+ # def install_css_zero
62
+ # # Removed - dashboard now uses embedded styles
63
+ # end
64
+
65
+ def show_readme
66
+ readme_content = <<~README
67
+
68
+ ===============================================================================
69
+ 🛡️ Beskar Installation Complete!
70
+ ===============================================================================
71
+
72
+ Next steps:
73
+
74
+ 1. Run migrations to create the security tables:
75
+ $ rails db:migrate
76
+
77
+ 2. Configure authentication for the dashboard in config/initializers/beskar.rb
78
+
79
+ For Devise users:
80
+ config.authenticate_admin = proc do
81
+ authenticate_admin!
82
+ end
83
+
84
+ For custom authentication:
85
+ config.authenticate_admin = proc do
86
+ redirect_to main_app.root_path unless current_user&.admin?
87
+ end
88
+
89
+ 3. Add Beskar concerns to your User model (or authentication model):
90
+
91
+ class User < ApplicationRecord
92
+ include Beskar::Models::SecurityTrackable
93
+
94
+ # For Devise users, also add:
95
+ include Beskar::Models::SecurityTrackableDevise
96
+ end
97
+
98
+ 4. Access the security dashboard at:
99
+ http://localhost:3000/beskar
100
+
101
+ 5. Optional: Configure additional settings in config/initializers/beskar.rb
102
+ - IP whitelist
103
+ - WAF rules
104
+ - Rate limiting
105
+ - Geolocation
106
+ - Risk-based locking
107
+
108
+ ===============================================================================
109
+ 📚 Documentation
110
+ ===============================================================================
111
+
112
+ Dashboard Guide: https://github.com/humadroid-io/beskar/blob/main/DASHBOARD.md
113
+ Configuration: https://github.com/humadroid-io/beskar/blob/main/README.md
114
+
115
+ ===============================================================================
116
+ ⚠️ Important for Production
117
+ ===============================================================================
118
+
119
+ 1. ALWAYS configure authentication for the dashboard
120
+ 2. Set monitor_only = false when ready to block threats
121
+ 3. Configure your IP whitelist to prevent locking yourself out
122
+ 4. Set up database indexes for large-scale deployments:
123
+
124
+ $ rails generate beskar:indexes
125
+ $ rails db:migrate
126
+
127
+ ===============================================================================
128
+ 💡 Quick Tips
129
+ ===============================================================================
130
+
131
+ - Start with monitor_only = true to observe without blocking
132
+ - Use the dashboard to review security events before enabling blocking
133
+ - Configure email notifications for high-risk events
134
+ - Export security data regularly for analysis
135
+ - Consider implementing custom risk scoring for your use case
136
+
137
+ ===============================================================================
138
+
139
+ README
140
+
141
+ say readme_content, :green
142
+ end
143
+
144
+ private
145
+
146
+ def migration_already_exists?(migration_name)
147
+ Dir.glob("db/migrate/*_#{migration_name}").any?
148
+ end
149
+
150
+ def migration_template(source, destination)
151
+ migration_number = self.class.next_migration_number(nil)
152
+ file_name = "#{migration_number}_#{destination}"
153
+
154
+ copy_file source, file_name
155
+ end
156
+ end
157
+ end
158
+ end