Migrate from file-based configs to database with per-IP site configuration

Major architectural changes:
   - Replace YAML config files with database-stored ScanConfig model
   - Remove CIDR block support in favor of individual IP addresses per site
   - Each IP now has its own expected_ping, expected_tcp_ports, expected_udp_ports
   - AlertRule now uses config_id FK instead of config_file string

   API changes:
   - POST /api/scans now requires config_id instead of config_file
   - Alert rules API uses config_id with validation
   - All config dropdowns fetch from /api/configs dynamically

   Template updates:
   - scans.html, dashboard.html, alert_rules.html load configs via API
   - Display format: Config Title (X sites) in dropdowns
   - Removed Jinja2 config_files loops

   Migrations:
   - 008: Expand CIDRs to individual IPs with per-IP port configs
   - 009: Remove CIDR-related columns
   - 010: Add config_id to alert_rules, remove config_file
This commit is contained in:
2025-11-19 19:40:34 -06:00
parent 034f146fa1
commit 0ec338e252
21 changed files with 2004 additions and 686 deletions

View File

@@ -261,14 +261,14 @@ class SneakyScanner:
"""
Load a site definition from the database.
IPs are pre-expanded in the database, so we just load them directly.
Args:
site_name: Name of the site to load
Returns:
Site definition dict with expanded IPs, or None if not found
Site definition dict with IPs, or None if not found
"""
import ipaddress
try:
# Import database modules
import os
@@ -281,7 +281,7 @@ class SneakyScanner:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, joinedload
from web.models import Site, SiteCIDR
from web.models import Site
# Get database URL from environment
database_url = os.environ.get('DATABASE_URL', 'sqlite:///./sneakyscanner.db')
@@ -291,12 +291,10 @@ class SneakyScanner:
Session = sessionmaker(bind=engine)
session = Session()
# Query site with CIDRs and IP overrides
# Query site with all IPs (CIDRs are already expanded)
site = (
session.query(Site)
.options(
joinedload(Site.cidrs).joinedload(SiteCIDR.ips)
)
.options(joinedload(Site.ips))
.filter(Site.name == site_name)
.first()
)
@@ -305,60 +303,25 @@ class SneakyScanner:
session.close()
return None
# Expand CIDRs to IP list
# Load all IPs directly from database (already expanded)
expanded_ips = []
for cidr_obj in site.cidrs:
cidr = cidr_obj.cidr
expected_ping = cidr_obj.expected_ping
expected_tcp_ports = json.loads(cidr_obj.expected_tcp_ports) if cidr_obj.expected_tcp_ports else []
expected_udp_ports = json.loads(cidr_obj.expected_udp_ports) if cidr_obj.expected_udp_ports else []
for ip_obj in site.ips:
# Get settings from IP (no need to merge with CIDR defaults)
expected_ping = ip_obj.expected_ping if ip_obj.expected_ping is not None else False
expected_tcp_ports = json.loads(ip_obj.expected_tcp_ports) if ip_obj.expected_tcp_ports else []
expected_udp_ports = json.loads(ip_obj.expected_udp_ports) if ip_obj.expected_udp_ports else []
# Build IP override map
override_map = {}
for ip_override in cidr_obj.ips:
override_map[ip_override.ip_address] = {
'expected_ping': ip_override.expected_ping if ip_override.expected_ping is not None else expected_ping,
'expected_tcp_ports': json.loads(ip_override.expected_tcp_ports) if ip_override.expected_tcp_ports else expected_tcp_ports,
'expected_udp_ports': json.loads(ip_override.expected_udp_ports) if ip_override.expected_udp_ports else expected_udp_ports
ip_config = {
'address': ip_obj.ip_address,
'expected': {
'ping': expected_ping,
'tcp_ports': expected_tcp_ports,
'udp_ports': expected_udp_ports
}
}
# Expand CIDR to IP list
try:
network = ipaddress.ip_network(cidr, strict=False)
ip_list = [str(ip) for ip in network.hosts()]
if not ip_list:
ip_list = [str(network.network_address)]
for ip_address in ip_list:
# Check if this IP has an override
if ip_address in override_map:
override = override_map[ip_address]
ip_config = {
'address': ip_address,
'expected': {
'ping': override['expected_ping'],
'tcp_ports': override['expected_tcp_ports'],
'udp_ports': override['expected_udp_ports']
}
}
else:
# Use CIDR-level defaults
ip_config = {
'address': ip_address,
'expected': {
'ping': expected_ping if expected_ping is not None else False,
'tcp_ports': expected_tcp_ports,
'udp_ports': expected_udp_ports
}
}
expanded_ips.append(ip_config)
except ValueError as e:
print(f"WARNING: Invalid CIDR '{cidr}' in site '{site_name}': {e}", file=sys.stderr)
continue
expanded_ips.append(ip_config)
session.close()