# 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