scheduling and jobs, new dataclasses and such better UDP handling

This commit is contained in:
2025-10-17 16:49:30 -05:00
parent 9956667c8f
commit 41306801ae
13 changed files with 771 additions and 169 deletions

View File

@@ -6,8 +6,10 @@ import os
import yaml
import logging
from apscheduler.triggers.cron import CronTrigger
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@dataclass
@@ -25,6 +27,7 @@ class ScanOptions:
"""
Feature toggles that affect how scans are executed.
"""
cron: Optional[str] = None
udp_scan: bool = False
tls_security_scan: bool = True
tls_exp_check: bool = True
@@ -38,6 +41,8 @@ class Reporting:
report_name: str = "Scan Report"
report_filename: str = "report.html"
full_details: bool = False
email_to: List[str] = field(default_factory=list)
email_cc: List[str] = field(default_factory=list)
@dataclass
@@ -58,7 +63,7 @@ class ScanConfigRepository:
Search order for the config directory:
1) Explicit path argument to load_all()
2) Environment variable SCAN_TARGETS_DIR
3) Default: /data/scan_targets
3) Default: /app/data/scan_targets
"""
SUPPORTED_EXT = (".yaml", ".yml")
@@ -102,7 +107,7 @@ class ScanConfigRepository:
env = os.getenv("SCAN_TARGETS_DIR")
if env:
return Path(env)
return Path("/data/scan_targets")
return Path("/app/data/scan_targets")
@staticmethod
def _read_yaml(path: Path) -> Dict[str, Any]:
@@ -144,6 +149,7 @@ class ScanConfigRepository:
# Parse scan_options
so_raw = data.get("scan_options", {}) or {}
scan_options = ScanOptions(
cron=self._validate_cron_or_none(so_raw.get("cron")),
udp_scan=bool(so_raw.get("udp_scan", False)),
tls_security_scan=bool(so_raw.get("tls_security_scan", True)),
tls_exp_check=bool(so_raw.get("tls_exp_check", True)),
@@ -155,6 +161,8 @@ class ScanConfigRepository:
report_name=str(rep_raw.get("report_name", "Scan Report")),
report_filename=str(rep_raw.get("report_filename", "report.html")),
full_details=bool(rep_raw.get("full_details", False)),
email_to=self._as_str_list(rep_raw.get("email_to", []), "email_to"),
email_cc=self._as_str_list(rep_raw.get("email_cc", []), "email_cc"),
)
# Parse targets
@@ -179,6 +187,20 @@ class ScanConfigRepository:
scan_targets=targets,
)
@staticmethod
def _validate_cron_or_none(expr: Optional[str]) -> Optional[str]:
"""
Validate a standard 5-field crontab string via CronTrigger.from_crontab.
Return the original string if valid; None if empty/None.
Raise ValueError on invalid expressions.
"""
if not expr:
return None
expr = str(expr).strip()
# Validate now so we fail early on bad configs
CronTrigger.from_crontab(expr) # will raise on invalid
return expr
@staticmethod
def _validate_config(cfg: ScanConfigFile, source: str) -> None:
"""
@@ -192,7 +214,27 @@ class ScanConfigRepository:
if dups:
raise ValueError(f"{source}: duplicate IP(s) in scan_targets: {dups}")
# Optional helpers
@staticmethod
def _as_str_list(value: Any, field_name: str) -> List[str]:
"""
Accept a single string or a list of strings; return List[str].
"""
if value is None or value == []:
return []
if isinstance(value, str):
return [value.strip()] if value.strip() else []
if isinstance(value, (list, tuple)):
out: List[str] = []
for v in value:
if not isinstance(v, str):
raise TypeError(f"'{field_name}' must contain only strings.")
s = v.strip()
if s:
out.append(s)
return out
raise TypeError(f"'{field_name}' must be a string or a list of strings.")
# helpers
def list_configs(self) -> List[str]:
"""