118 lines
3.0 KiB
Ruby
118 lines
3.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# PathScannerDetectorJob - Detects IPs hitting scanner paths and auto-bans them
|
|
#
|
|
# This job analyzes recent events to find IPs hitting common scanner/bot paths
|
|
# like /.env, /.git, /wp-admin, etc. When detected, it creates temporary IP
|
|
# block rules that expire after 24 hours.
|
|
#
|
|
# Schedule: Every 5 minutes (configured in initializer or cron)
|
|
class PathScannerDetectorJob < ApplicationJob
|
|
queue_as :default
|
|
|
|
# Common paths that scanners/bots hit
|
|
SCANNER_PATHS = %w[
|
|
/.env
|
|
/.git
|
|
/wp-admin
|
|
/wp-login.php
|
|
/phpMyAdmin
|
|
/phpmyadmin
|
|
/.aws
|
|
/.ssh
|
|
/admin
|
|
/administrator
|
|
/.config
|
|
/backup
|
|
/db_backup
|
|
/.DS_Store
|
|
/web.config
|
|
].freeze
|
|
|
|
# Minimum hits to be considered a scanner
|
|
MIN_SCANNER_HITS = 3
|
|
|
|
# Look back window
|
|
LOOKBACK_WINDOW = 5.minutes
|
|
|
|
# Ban duration
|
|
BAN_DURATION = 24.hours
|
|
|
|
def perform
|
|
scanner_ips = detect_scanner_ips
|
|
|
|
Rails.logger.info "PathScannerDetectorJob: Found #{scanner_ips.count} scanner IPs"
|
|
|
|
scanner_ips.each do |ip_data|
|
|
create_ban_rule(ip_data)
|
|
end
|
|
|
|
scanner_ips.count
|
|
end
|
|
|
|
private
|
|
|
|
def detect_scanner_ips
|
|
# Find IPs that have hit scanner paths multiple times recently
|
|
Event
|
|
.where("timestamp > ?", LOOKBACK_WINDOW.ago)
|
|
.where("request_path IN (?)", SCANNER_PATHS)
|
|
.group(:ip_address)
|
|
.select("ip_address, COUNT(*) as hit_count, GROUP_CONCAT(DISTINCT request_path) as paths")
|
|
.having("COUNT(*) >= ?", MIN_SCANNER_HITS)
|
|
.map do |event|
|
|
{
|
|
ip: event.ip_address,
|
|
hit_count: event.hit_count,
|
|
paths: event.paths.to_s.split(",")
|
|
}
|
|
end
|
|
end
|
|
|
|
def create_ban_rule(ip_data)
|
|
ip = ip_data[:ip]
|
|
|
|
# Check if rule already exists for this IP
|
|
existing_rule = Rule.active.network_rules.find_by(
|
|
"conditions ->> 'cidr' = ?", "#{ip}/32"
|
|
)
|
|
|
|
if existing_rule
|
|
Rails.logger.info "PathScannerDetectorJob: Rule already exists for #{ip}, skipping"
|
|
return
|
|
end
|
|
|
|
# Determine if IPv4 or IPv6
|
|
addr = IPAddr.new(ip)
|
|
rule_type = addr.ipv4? ? "network_v4" : "network_v6"
|
|
|
|
# Create the ban rule
|
|
rule = Rule.create!(
|
|
rule_type: rule_type,
|
|
action: "deny",
|
|
conditions: { cidr: "#{ip}/32" },
|
|
priority: 32,
|
|
expires_at: BAN_DURATION.from_now,
|
|
source: "auto:scanner_detected",
|
|
enabled: true,
|
|
metadata: {
|
|
reason: "Scanner detected: hit #{ip_data[:paths].join(', ')}",
|
|
hit_count: ip_data[:hit_count],
|
|
paths: ip_data[:paths],
|
|
detected_at: Time.current.iso8601,
|
|
auto_generated: true
|
|
}
|
|
)
|
|
|
|
Rails.logger.info "PathScannerDetectorJob: Created ban rule #{rule.id} for #{ip} (expires: #{rule.expires_at})"
|
|
|
|
rule
|
|
rescue IPAddr::InvalidAddressError => e
|
|
Rails.logger.error "PathScannerDetectorJob: Invalid IP address #{ip}: #{e.message}"
|
|
nil
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
Rails.logger.error "PathScannerDetectorJob: Failed to create rule for #{ip}: #{e.message}"
|
|
nil
|
|
end
|
|
end
|