From 9fc1c23b4bce8b29f894fed9e9c12e32840ba3cc Mon Sep 17 00:00:00 2001 From: Sumner Robinson Date: Wed, 4 Mar 2026 22:10:20 +0000 Subject: [PATCH 1/3] Resolve older crs v3 issues --- src/nssec/cli/waf_commands.py | 176 +++++++++++++++- src/nssec/modules/waf/__init__.py | 124 +++++++++-- src/nssec/modules/waf/config.py | 49 ++++- src/nssec/modules/waf/status.py | 113 ++++++++++ src/nssec/modules/waf/utils.py | 65 ++++++ tests/unit/test_waf_commands.py | 162 ++++++++++++++ tests/unit/test_waf_module.py | 337 +++++++++++++++++++++++++++++- tests/unit/test_waf_status.py | 133 ++++++++++++ tests/unit/test_waf_utils.py | 255 ++++++++++++++++++++++ 9 files changed, 1392 insertions(+), 22 deletions(-) create mode 100644 tests/unit/test_waf_status.py create mode 100644 tests/unit/test_waf_utils.py diff --git a/src/nssec/cli/waf_commands.py b/src/nssec/cli/waf_commands.py index e9d87f7..c1f05ef 100644 --- a/src/nssec/cli/waf_commands.py +++ b/src/nssec/cli/waf_commands.py @@ -116,7 +116,29 @@ def _build_status_table(status): table.add_column("Property", style="cyan") table.add_column("Value") - table.add_row("ModSecurity installed", _yn(status.modsec_installed)) + if status.apache_version: + apache_val = f"v{status.apache_version}" + if status.apache_ppa: + apache_val += " [cyan](ondrej PPA)[/cyan]" + table.add_row("Apache", apache_val) + + if status.modsec_installed and status.modsec_version: + from nssec.modules.waf.utils import version_gte + + ver_str = f"v{status.modsec_version}" + if not version_gte(status.modsec_version, "2.9.6") and status.disabled_crs_rules > 0: + table.add_row( + "ModSecurity installed", + f"[yellow]yes ({ver_str} — {status.disabled_crs_rules} CRS rule(s) disabled, " + f"run [cyan]nssec waf update[/cyan] to upgrade)[/yellow]", + ) + else: + table.add_row( + "ModSecurity installed", + f"[green]yes ({ver_str})[/green]", + ) + else: + table.add_row("ModSecurity installed", _yn(status.modsec_installed)) table.add_row("Module enabled", _yn(status.modsec_enabled)) if status.modsec_mode: @@ -130,6 +152,10 @@ def _build_status_table(status): table.add_row("OWASP CRS", f"[green]{crs}[/green]") if status.crs_path: table.add_row("CRS path", status.crs_path) + setup_val = _yn(status.crs_setup_present) + if not status.crs_setup_present: + setup_val += " [red](rule 901001 will flag all traffic! run [cyan]nssec waf init[/cyan])[/red]" + table.add_row("crs-setup.conf", setup_val) else: table.add_row("OWASP CRS", "[red]not installed[/red]") @@ -139,7 +165,35 @@ def _build_status_table(status): else: table.add_row("mod_evasive", "[dim]not installed[/dim]") - table.add_row("NS exclusions", _yn(status.exclusions_present, "yellow")) + # NS exclusions detail + if status.exclusions_present: + if not status.exclusions_included: + excl_val = ( + "[red]not loaded[/red] — security2.conf does not include exclusions file, " + "run [cyan]nssec waf init[/cyan] to fix" + ) + elif not status.crs_path_valid: + excl_val = ( + "[yellow]loaded but ineffective[/yellow] — " + "CRS misconfigured (missing crs-setup.conf), " + "run [cyan]nssec waf init[/cyan] to fix" + ) + elif not status.exclusions_current: + v = status.exclusions_version or "unknown" + excl_val = ( + f"[yellow]outdated (v{v})[/yellow] — " + "run [cyan]nssec waf update-exclusions[/cyan]" + ) + else: + excl_val = "[green]active (v{})[/green]".format(status.exclusions_version) + table.add_row("NS exclusions", excl_val) + if status.exclusions_admin_ips: + table.add_row(" Admin IPs", str(status.exclusions_admin_ips)) + if status.exclusions_nodeping_ips: + table.add_row(" NodePing IPs", str(status.exclusions_nodeping_ips)) + else: + table.add_row("NS exclusions", "[yellow]not deployed[/yellow]") + table.add_row("Audit log", _yn(status.audit_log_exists, "dim")) return table @@ -194,8 +248,17 @@ def waf_init(mode, skip_evasive, yes, dry_run): console.print("[yellow]Aborted.[/yellow]") return + # Fetch NodePing monitoring probe IPs for WAF allowlisting + from nssec.modules.waf import fetch_nodeping_probe_ips + + nodeping_ips, np_err = fetch_nodeping_probe_ips() + if np_err: + console.print(f" [yellow]Warning:[/yellow] {np_err}") + elif nodeping_ips: + console.print(f" Fetched {len(nodeping_ips)} NodePing probe IPs for WAF allowlisting") + console.print() - result = installer.run() + result = installer.run(nodeping_ips=nodeping_ips) _print_install_results(result) if not result.success: @@ -348,7 +411,16 @@ def waf_update_exclusions(yes, dry_run): console.print("[bold]Updating NetSapiens WAF exclusions...[/bold]") - result = installer.install_exclusions() + # Fetch NodePing monitoring probe IPs for WAF allowlisting + from nssec.modules.waf import fetch_nodeping_probe_ips + + nodeping_ips, np_err = fetch_nodeping_probe_ips() + if np_err: + console.print(f" [yellow]Warning:[/yellow] {np_err}") + elif nodeping_ips: + console.print(f" Fetched {len(nodeping_ips)} NodePing probe IPs for WAF allowlisting") + + result = installer.install_exclusions(nodeping_ips=nodeping_ips) if not result.success: console.print(f" [red]Error:[/red] {result.error}") raise SystemExit(1) @@ -383,6 +455,102 @@ def waf_status(): console.print(f" [dim]{line}[/dim]") +@waf.command("update") +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts") +def waf_update(yes): + """Check ModSecurity version and re-enable CRS rules after upgrade. + + \b + If ModSecurity < 2.9.6, shows instructions for adding the Digitalwave + ModSecurity repository to get a compatible version. + + \b + If ModSecurity >= 2.9.6 (user already upgraded), re-enables any CRS + rules that were disabled during init and validates the Apache config. + """ + from nssec.modules.waf import ModSecurityInstaller + from nssec.modules.waf.utils import detect_modsec_version, version_gte + from nssec.modules.waf.config import ( + CRS_INSTALL_DIR, + DIGITALWAVE_KEY_URL, + DIGITALWAVE_KEYRING, + DIGITALWAVE_LIST, + DIGITALWAVE_REPO_URL, + ) + + installer = ModSecurityInstaller() + pf = installer.preflight() + _require_root_and_modsec(pf, "sudo nssec waf update") + + current_ver = detect_modsec_version() + console.print(f"[bold]Current ModSecurity version:[/bold] {current_ver or 'unknown'}") + + if not version_gte(current_ver, "2.9.6"): + console.print() + console.print( + "[yellow]ModSecurity < 2.9.6 — some CRS v4 rules are disabled.[/yellow]" + ) + console.print( + "Ubuntu 22.04 ships ModSecurity 2.9.5 which lacks support for " + "multipart rules introduced in 2.9.6." + ) + console.print() + console.print("[bold]To upgrade, add the Digitalwave ModSecurity repository:[/bold]") + console.print() + keyring = DIGITALWAVE_KEYRING + console.print( + f" [cyan]curl -fsSL {DIGITALWAVE_KEY_URL} " + f"| sudo gpg --dearmor -o {keyring}[/cyan]" + ) + console.print() + # Escape square brackets so Rich doesn't treat [signed-by=...] as markup + signed = f"\\[signed-by={keyring}]" + repo = DIGITALWAVE_REPO_URL + lst = DIGITALWAVE_LIST + console.print( + f' [cyan]echo "deb {signed} {repo} $(lsb_release -sc) main" ' + f"| sudo tee {lst}[/cyan]" + ) + console.print( + f' [cyan]echo "deb {signed} {repo} $(lsb_release -sc)-backports main" ' + f"| sudo tee -a {lst}[/cyan]" + ) + console.print() + console.print( + " [cyan]sudo apt-get update[/cyan]" + ) + console.print( + " [cyan]sudo apt-get install -t $(lsb_release -sc)-backports " + "libapache2-mod-security2[/cyan]" + ) + console.print() + console.print( + "After upgrading, run [cyan]nssec waf update[/cyan] again to " + "re-enable the disabled CRS rules." + ) + return + + # ModSec >= 2.9.6 — re-enable any disabled rules + crs_path = pf.crs_path or CRS_INSTALL_DIR + reenabled = installer._reenable_crs_rules(crs_path) + if not reenabled: + console.print("[green]ModSecurity >= 2.9.6 and all CRS rules are active. Nothing to do.[/green]") + return + + console.print( + f" [green]Done:[/green] Re-enabled {len(reenabled)} CRS rule(s): " + + ", ".join(reenabled) + ) + + val = installer.validate_config() + if not val.success: + console.print(f" [red]Error:[/red] {val.error}") + raise SystemExit(1) + console.print(f" [green]Done:[/green] {val.message}") + + _prompt_and_reload_apache(installer, yes) + + @waf.group("allowlist", invoke_without_command=True) @click.pass_context def waf_allowlist(ctx): diff --git a/src/nssec/modules/waf/__init__.py b/src/nssec/modules/waf/__init__.py index 352bb52..f228af2 100644 --- a/src/nssec/modules/waf/__init__.py +++ b/src/nssec/modules/waf/__init__.py @@ -17,6 +17,7 @@ CRS_APT_PACKAGE, CRS_GITHUB_DOWNLOAD, CRS_INSTALL_DIR, + CRS_RULES_REQUIRE_296, CRS_SEARCH_PATHS, CRS_SETUP_OVERRIDES_TEMPLATE, EVASIVE_CONF, @@ -36,7 +37,9 @@ MODSEC_PACKAGE, MODSEC_TMP_DIR, NS_EXCLUSIONS_CONF, + NS_EXCLUSIONS_HASH, NS_EXCLUSIONS_TEMPLATE, + NS_EXCLUSIONS_VERSION, PINNED_CRS_VERSION, SECURITY2_CONF, SECURITY2_CONF_TEMPLATE, @@ -47,17 +50,26 @@ append_crs_to_security2, backup_file, detect_modsec_mode, + detect_modsec_version, file_exists, package_installed, parse_security2_conf, read_file, render, run_cmd, + version_gte, write_file, write_security2_full, ) +def fetch_nodeping_probe_ips() -> tuple[list[str], str]: + """Fetch NodePing monitoring probe IPs for WAF allowlisting.""" + from nssec.modules.mtls.utils import fetch_nodeping_ips + + return fetch_nodeping_ips() + + def get_allowlisted_ips() -> list[str]: """Parse allowlisted admin IPs from the deployed exclusions conf.""" content = read_file(NS_EXCLUSIONS_CONF) @@ -82,7 +94,12 @@ def add_allowlisted_ip(ip: str) -> StepResult: if file_exists(NS_EXCLUSIONS_CONF): backup_file(NS_EXCLUSIONS_CONF) - content = render(NS_EXCLUSIONS_TEMPLATE, admin_ips=new_ips) + content = render( + NS_EXCLUSIONS_TEMPLATE, + admin_ips=new_ips, + version=NS_EXCLUSIONS_VERSION, + template_hash=NS_EXCLUSIONS_HASH, + ) if not write_file(NS_EXCLUSIONS_CONF, content): return StepResult(success=False, error=f"Failed to write {NS_EXCLUSIONS_CONF}") @@ -100,7 +117,12 @@ def remove_allowlisted_ip(ip: str) -> StepResult: if file_exists(NS_EXCLUSIONS_CONF): backup_file(NS_EXCLUSIONS_CONF) - content = render(NS_EXCLUSIONS_TEMPLATE, admin_ips=new_ips) + content = render( + NS_EXCLUSIONS_TEMPLATE, + admin_ips=new_ips, + version=NS_EXCLUSIONS_VERSION, + template_hash=NS_EXCLUSIONS_HASH, + ) if not write_file(NS_EXCLUSIONS_CONF, content): return StepResult(success=False, error=f"Failed to write {NS_EXCLUSIONS_CONF}") @@ -317,7 +339,12 @@ def install_crs_v4(self) -> StepResult: has_v4 = pf.crs_installed and pf.crs_version and pf.crs_version.startswith("4") if has_v4: - msg = f"CRS v{pf.crs_version} already installed at {pf.crs_path}" + # Still update crs-setup.conf with latest template values + self._update_crs_setup(pf.crs_path) + disabled = self._disable_incompatible_crs_rules(pf.crs_path) + msg = f"CRS v{pf.crs_version} at {pf.crs_path} (crs-setup.conf updated)" + if disabled: + msg += f"; disabled {len(disabled)} rule(s) incompatible with ModSec < 2.9.6" return StepResult(skipped=True, message=msg) if self.dry_run: @@ -351,6 +378,39 @@ def _try_crs_from_apt(self) -> StepResult | None: return StepResult(message=msg) return None + def _update_crs_setup(self, crs_path: str) -> bool: + """Write crs-setup.conf from template. Returns True on success.""" + setup_content = render( + CRS_SETUP_OVERRIDES_TEMPLATE, + paranoia_level=1, + inbound_threshold=5, + outbound_threshold=4, + ) + setup_path = f"{crs_path}/crs-setup.conf" + return write_file(setup_path, setup_content) + + def _disable_incompatible_crs_rules(self, crs_path: str) -> list[str]: + """Disable CRS rules incompatible with the installed ModSecurity version. + + On ModSecurity < 2.9.6, renames rule files in CRS_RULES_REQUIRE_296 + from .conf to .conf.disabled to prevent Apache startup failures. + + Returns list of disabled filenames (empty if >= 2.9.6 or nothing to do). + """ + ver = detect_modsec_version() + if version_gte(ver, "2.9.6"): + return [] + + disabled: list[str] = [] + rules_dir = Path(f"{crs_path}/rules") + for rule_file in CRS_RULES_REQUIRE_296: + src = rules_dir / rule_file + dst = rules_dir / (rule_file + ".disabled") + if src.exists() and not dst.exists(): + src.rename(dst) + disabled.append(rule_file) + return disabled + def _download_crs_from_github(self) -> StepResult: """Download and extract CRS v4 from GitHub releases.""" tarball = f"/tmp/crs-v{PINNED_CRS_VERSION}.tar.gz" @@ -377,21 +437,24 @@ def _download_crs_from_github(self) -> StepResult: return StepResult(success=False, error=f"Failed to extract CRS: {stderr}") Path(tarball).unlink(missing_ok=True) - setup_content = render( - CRS_SETUP_OVERRIDES_TEMPLATE, - paranoia_level=1, - inbound_threshold=5, - outbound_threshold=4, - ) - setup_path = f"{CRS_INSTALL_DIR}/crs-setup.conf" - if not write_file(setup_path, setup_content): - return StepResult(success=False, error=f"Failed to write {setup_path}") + if not self._update_crs_setup(CRS_INSTALL_DIR): + return StepResult( + success=False, error=f"Failed to write {CRS_INSTALL_DIR}/crs-setup.conf" + ) + + # Refresh preflight cache so write_security2_conf() uses the new CRS path + self._preflight = None + + disabled = self._disable_incompatible_crs_rules(CRS_INSTALL_DIR) msg = f"Installed CRS v{PINNED_CRS_VERSION} to {CRS_INSTALL_DIR}" + if disabled: + msg += f"; disabled {len(disabled)} rule(s) incompatible with ModSec < 2.9.6" return StepResult(message=msg) def install_exclusions( self, admin_ips: list[str] | None = None, + nodeping_ips: list[str] | None = None, ) -> StepResult: """Write NetSapiens-specific ModSecurity exclusions.""" if self.dry_run: @@ -400,7 +463,13 @@ def install_exclusions( if file_exists(NS_EXCLUSIONS_CONF): backup_file(NS_EXCLUSIONS_CONF) - content = render(NS_EXCLUSIONS_TEMPLATE, admin_ips=admin_ips or []) + content = render( + NS_EXCLUSIONS_TEMPLATE, + admin_ips=admin_ips or [], + nodeping_ips=nodeping_ips or [], + version=NS_EXCLUSIONS_VERSION, + template_hash=NS_EXCLUSIONS_HASH, + ) if not write_file(NS_EXCLUSIONS_CONF, content): return StepResult( success=False, @@ -500,7 +569,11 @@ def verify(self) -> list[StepResult]: # Full install orchestration # ------------------------------------------------------------------ - def run(self, admin_ips: list[str] | None = None) -> InstallResult: + def run( + self, + admin_ips: list[str] | None = None, + nodeping_ips: list[str] | None = None, + ) -> InstallResult: """Run the full installation sequence.""" result = InstallResult(mode=self.mode) pf = self.preflight() @@ -519,7 +592,10 @@ def run(self, admin_ips: list[str] | None = None) -> InstallResult: ("Configure mod_evasive", self.setup_evasive_config), ("Enable mod_evasive", lambda: self.set_evasive_state(True)), ("Install OWASP CRS v4", self.install_crs_v4), - ("Install NS exclusions", lambda: self.install_exclusions(admin_ips)), + ( + "Install NS exclusions", + lambda: self.install_exclusions(admin_ips, nodeping_ips), + ), ("Update security2.conf", self.write_security2_conf), ("Validate Apache config", self.validate_config), ] @@ -582,3 +658,21 @@ def set_mode(self, mode: str) -> StepResult: msg = f"SecRuleEngine set to {mode} and Apache reloaded" return StepResult(message=msg) + + def _reenable_crs_rules(self, crs_path: str) -> list[str]: + """Re-enable CRS rules previously disabled for ModSec < 2.9.6. + + Renames .conf.disabled files back to .conf for rules in + CRS_RULES_REQUIRE_296. + + Returns list of re-enabled filenames. + """ + reenabled: list[str] = [] + rules_dir = Path(f"{crs_path}/rules") + for rule_file in CRS_RULES_REQUIRE_296: + disabled = rules_dir / (rule_file + ".disabled") + target = rules_dir / rule_file + if disabled.exists() and not target.exists(): + disabled.rename(target) + reenabled.append(rule_file) + return reenabled diff --git a/src/nssec/modules/waf/config.py b/src/nssec/modules/waf/config.py index 4db0dbe..38b6073 100644 --- a/src/nssec/modules/waf/config.py +++ b/src/nssec/modules/waf/config.py @@ -34,6 +34,16 @@ # CRS version pinning (used when apt ships v3.x) PINNED_CRS_VERSION = "4.8.0" + +# CRS rule files that require ModSecurity >= 2.9.6 (MULTIPART_PART_HEADERS). +# These are renamed to .conf.disabled on older ModSecurity versions. +CRS_RULES_REQUIRE_296 = [ + "REQUEST-922-MULTIPART-ATTACK.conf", +] +DIGITALWAVE_REPO_URL = "http://modsecurity.digitalwave.hu/ubuntu/" +DIGITALWAVE_KEY_URL = "https://modsecurity.digitalwave.hu/archive.key" +DIGITALWAVE_KEYRING = "/usr/share/keyrings/digitalwave-modsecurity.gpg" +DIGITALWAVE_LIST = "/etc/apt/sources.list.d/digitalwave-modsecurity.list" CRS_GITHUB_DOWNLOAD = ( f"https://github.com/coreruleset/coreruleset/archive/refs/tags/v{PINNED_CRS_VERSION}.tar.gz" ) @@ -50,16 +60,20 @@ MODSEC_TMP_DIR = "/tmp/" MODSEC_DATA_DIR = "/tmp/" -# CRS locations to check (apt-installed or manual) +# CRS locations to check — nssec-managed path first so v4 is preferred +# over the apt v3 package when both exist. CRS_SEARCH_PATHS = [ - "/usr/share/modsecurity-crs", "/etc/modsecurity/crs", + "/usr/share/modsecurity-crs", "/etc/apache2/modsecurity-crs", ] # Backup suffix for nssec-managed files BACKUP_SUFFIX = ".bak.nssec" +# Exclusions template version — human-readable label for the template revision. +NS_EXCLUSIONS_VERSION = "2" + # --------------------------------------------------------------------------- # Jinja2 Templates # --------------------------------------------------------------------------- @@ -188,6 +202,8 @@ # NetSapiens-specific ModSecurity Exclusions # Managed by nssec # Generated: {{ timestamp }} +# nssec-exclusions-version: {{ version }} +# nssec-exclusions-hash: {{ template_hash }} # # These rules prevent false positives on the NetSapiens management UI # and API endpoints while keeping CRS protection active for everything else. @@ -276,6 +292,20 @@ ctl:ruleRemoveByTag=OWASP_CRS" {% endfor %} {% endif %} + +{% if nodeping_ips %} +# ---- NodePing monitoring probe IPs ---- +# Health check probes should bypass CRS to avoid false positives. +# Source: https://nodeping.com/content/txt/pinghosts.txt +{% for ip in nodeping_ips %} +SecRule REMOTE_ADDR "@ipMatch {{ ip }}" \\ + "id:{{ 1000200 + loop.index }},\\ + phase:1,\\ + pass,\\ + nolog,\\ + ctl:ruleRemoveByTag=OWASP_CRS" +{% endfor %} +{% endif %} """ CRS_SETUP_OVERRIDES_TEMPLATE = """\ @@ -444,3 +474,18 @@ DOSWhitelist 192.168.*.* """ + + +def _exclusions_template_hash() -> str: + """Compute MD5 hash of NS_EXCLUSIONS_TEMPLATE source. + + Any change to the template (new rules, modified rules, structural changes) + automatically produces a different hash. Deployed files embed this hash + so 'waf status' can detect drift without manual version bumping. + """ + import hashlib + + return hashlib.md5(NS_EXCLUSIONS_TEMPLATE.encode()).hexdigest()[:12] + + +NS_EXCLUSIONS_HASH = _exclusions_template_hash() diff --git a/src/nssec/modules/waf/status.py b/src/nssec/modules/waf/status.py index 22d089d..72f02f1 100644 --- a/src/nssec/modules/waf/status.py +++ b/src/nssec/modules/waf/status.py @@ -2,11 +2,13 @@ from __future__ import annotations +import re from dataclasses import dataclass, field from pathlib import Path from typing import Optional from nssec.modules.waf.config import ( + CRS_RULES_REQUIRE_296, CRS_SEARCH_PATHS, EVASIVE_LOAD, EVASIVE_PACKAGE, @@ -14,6 +16,9 @@ MODSEC_CONF, MODSEC_PACKAGE, NS_EXCLUSIONS_CONF, + NS_EXCLUSIONS_HASH, + NS_EXCLUSIONS_VERSION, + SECURITY2_CONF, SECURITY2_LOAD, ) @@ -22,15 +27,26 @@ class WafStatus: """Current state of ModSecurity / CRS.""" + apache_version: Optional[str] = None + apache_ppa: bool = False modsec_installed: bool = False modsec_enabled: bool = False modsec_mode: Optional[str] = None crs_installed: bool = False crs_version: Optional[str] = None crs_path: Optional[str] = None + crs_setup_present: bool = False evasive_installed: bool = False evasive_enabled: bool = False exclusions_present: bool = False + exclusions_version: Optional[str] = None + exclusions_current: bool = False + exclusions_included: bool = False + crs_path_valid: bool = False + exclusions_admin_ips: int = 0 + exclusions_nodeping_ips: int = 0 + modsec_version: Optional[str] = None + disabled_crs_rules: int = 0 audit_log_exists: bool = False recent_log_lines: list[str] = field(default_factory=list) @@ -50,6 +66,36 @@ def _pkg_installed(package: str) -> bool: return False +def _get_pkg_version(package: str) -> Optional[str]: + """Get the upstream version of an installed deb package.""" + import subprocess + + try: + result = subprocess.run( + ["dpkg-query", "-W", "-f=${Version}", package], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0 or not result.stdout.strip(): + return None + # Strip Debian suffix (e.g. "2.9.5-3" → "2.9.5") + return result.stdout.strip().split("-")[0] + except (subprocess.TimeoutExpired, FileNotFoundError): + return None + + +def _is_ondrej_apache_ppa() -> bool: + """Check whether the ondrej/apache2 PPA is configured.""" + import glob + + patterns = [ + "/etc/apt/sources.list.d/ondrej-ubuntu-apache2-*", + "/etc/apt/sources.list.d/ondrej-*apache2*", + ] + return any(glob.glob(p) for p in patterns) + + def _read_file(path: str) -> Optional[str]: try: return Path(path).read_text() @@ -68,11 +114,37 @@ def _tail_file(path: str, lines: int = 10) -> list[str]: return [] +def _parse_security2_crs_path(content: str) -> Optional[str]: + """Extract the CRS path referenced in security2.conf.""" + match = re.search(r"IncludeOptional\s+(\S+)/crs-setup\.conf", content) + if match: + return match.group(1) + return None + + +def _parse_exclusions_meta(content: str) -> tuple[Optional[str], Optional[str], int, int]: + """Parse exclusions file for version, hash, admin IP count, NodePing IP count.""" + version = None + template_hash = None + for line in content.splitlines(): + if line.startswith("# nssec-exclusions-version:"): + version = line.split(":", 1)[1].strip() + elif line.startswith("# nssec-exclusions-hash:"): + template_hash = line.split(":", 1)[1].strip() + + admin_ips = len(re.findall(r'"id:10001\d+', content)) + nodeping_ips = len(re.findall(r'"id:10002\d+', content)) + return version, template_hash, admin_ips, nodeping_ips + + def get_waf_status() -> WafStatus: """Collect comprehensive WAF status information.""" status = WafStatus() + status.apache_version = _get_pkg_version("apache2") + status.apache_ppa = _is_ondrej_apache_ppa() status.modsec_installed = _pkg_installed(MODSEC_PACKAGE) + status.modsec_version = _get_pkg_version(MODSEC_PACKAGE) status.modsec_enabled = Path(SECURITY2_LOAD).exists() # Detect mode @@ -93,12 +165,53 @@ def get_waf_status() -> WafStatus: version_file = Path(search_path) / "VERSION" if version_file.exists(): status.crs_version = version_file.read_text().strip() + # Check crs-setup.conf exists at this path + status.crs_setup_present = (Path(search_path) / "crs-setup.conf").exists() + # Count disabled CRS rules (files renamed .conf.disabled for ModSec compat) + rules_dir = Path(search_path) / "rules" + if rules_dir.is_dir(): + status.disabled_crs_rules = sum( + 1 for f in CRS_RULES_REQUIRE_296 + if (rules_dir / (f + ".disabled")).exists() + ) break + # Check security2.conf references the correct CRS path and includes exclusions + sec2_content = _read_file(SECURITY2_CONF) + if sec2_content: + sec2_crs_path = _parse_security2_crs_path(sec2_content) + + # Exclusions are included if security2.conf either: + # 1. Explicitly includes the exclusions file path, OR + # 2. Uses a wildcard IncludeOptional /etc/modsecurity/*.conf + # (the default Debian config) which picks up all .conf in that dir + has_explicit = NS_EXCLUSIONS_CONF in sec2_content + has_wildcard = "/etc/modsecurity/*.conf" in sec2_content + status.exclusions_included = has_explicit or has_wildcard + + status.crs_path_valid = ( + sec2_crs_path is not None + and Path(sec2_crs_path).is_dir() + and (Path(sec2_crs_path) / "crs-setup.conf").exists() + ) + status.evasive_installed = _pkg_installed(EVASIVE_PACKAGE) status.evasive_enabled = Path(EVASIVE_LOAD).exists() + # Parse exclusions file status.exclusions_present = Path(NS_EXCLUSIONS_CONF).exists() + if status.exclusions_present: + excl_content = _read_file(NS_EXCLUSIONS_CONF) + if excl_content: + version, deployed_hash, admin_count, np_count = _parse_exclusions_meta( + excl_content + ) + status.exclusions_version = version + # Use hash for drift detection — automatically catches any template change + status.exclusions_current = deployed_hash == NS_EXCLUSIONS_HASH + status.exclusions_admin_ips = admin_count + status.exclusions_nodeping_ips = np_count + status.audit_log_exists = Path(MODSEC_AUDIT_LOG).exists() if status.audit_log_exists: status.recent_log_lines = _tail_file(MODSEC_AUDIT_LOG, 10) diff --git a/src/nssec/modules/waf/utils.py b/src/nssec/modules/waf/utils.py index 7fdf72a..a2c8033 100644 --- a/src/nssec/modules/waf/utils.py +++ b/src/nssec/modules/waf/utils.py @@ -68,6 +68,51 @@ def render(template_str: str, **kwargs: object) -> str: ) +def _parse_version(s: str) -> tuple[int, ...]: + """Parse a version string into an int tuple, stripping pre-release suffixes. + + Handles versions like "2.9.13~pre", "2.9.5-1", "2.9.6". + """ + import re + + parts = [] + for component in s.split("."): + # Extract leading digits, ignore suffixes like ~pre, -rc1, etc. + m = re.match(r"(\d+)", component) + if m: + parts.append(int(m.group(1))) + else: + raise ValueError(f"Cannot parse version component: {component}") + return tuple(parts) + + +def version_gte(version_str: str | None, target: str) -> bool: + """Compare version strings using tuple comparison. + + Returns True if version_str >= target. Returns False on parse errors. + Handles pre-release suffixes like ~pre, -rc1, etc. + """ + try: + v = _parse_version(version_str) + t = _parse_version(target) + return v >= t + except (ValueError, AttributeError, TypeError): + return False + + +def detect_modsec_version() -> str | None: + """Detect installed ModSecurity version from dpkg metadata. + + Returns version string (e.g. "2.9.5") or None if not installed. + """ + stdout, _, rc = run_cmd(["dpkg-query", "-W", "-f=${Version}", "libapache2-mod-security2"]) + if rc != 0 or not stdout.strip(): + return None + # dpkg version may have suffixes like "2.9.5-3" — keep only the upstream part + version = stdout.strip().split("-")[0] + return version + + def detect_modsec_mode(config_paths: list[str]) -> str | None: """Read SecRuleEngine mode from the first config that has it.""" for config_path in config_paths: @@ -105,10 +150,30 @@ def parse_security2_conf(path: str) -> tuple[bool, bool]: def append_crs_to_security2(crs_path: str) -> bool: """Append CRS IncludeOptional directives to an existing security2.conf. + Comments out any existing CRS load lines (e.g., apt v3 + ``IncludeOptional /usr/share/modsecurity-crs/*.load``) to prevent + dual-loading when nssec manages CRS v4 separately. + Returns True on success, False on write failure. """ sec2_content = read_file(SECURITY2_CONF) or "" backup_file(SECURITY2_CONF) + + # Comment out old CRS include lines to prevent dual-loading. + new_lines = [] + for line in sec2_content.splitlines(): + stripped = line.strip() + if ( + not stripped.startswith("#") + and "IncludeOptional" in stripped + and "modsecurity-crs" in stripped + ): + new_lines.append(" # Disabled by nssec (CRS v4 managed below)") + new_lines.append(f" # {stripped}") + else: + new_lines.append(line) + sec2_content = "\n".join(new_lines) + crs_block = ( f"\n # OWASP CRS (added by nssec)\n" f" IncludeOptional {crs_path}/crs-setup.conf\n" diff --git a/tests/unit/test_waf_commands.py b/tests/unit/test_waf_commands.py index 2041709..28bb500 100644 --- a/tests/unit/test_waf_commands.py +++ b/tests/unit/test_waf_commands.py @@ -324,3 +324,165 @@ def test_default_subcommand_shows_status(self, runner): assert result.exit_code == 0 assert "mod_evasive Status" in result.output + + +class TestWafInitNodeping: + """Tests for waf init command with NodePing IP fetching.""" + + def test_fetches_nodeping_ips_during_init(self, runner, mock_installer): + """Should fetch NodePing IPs and pass to run().""" + pf = mock_installer.preflight.return_value + pf.can_proceed = True + pf.crs_installed = True + pf.crs_version = "4.8.0" + pf.warnings = [] + + run_result = MagicMock() + run_result.success = True + run_result.steps_completed = ["Done"] + run_result.steps_skipped = [] + run_result.warnings = [] + run_result.errors = [] + mock_installer.run.return_value = run_result + mock_installer.reload_apache.return_value = MagicMock(success=True, message="Reloaded") + + with patch( + "nssec.modules.waf.fetch_nodeping_probe_ips", + return_value=(["52.71.195.82"], ""), + ): + result = runner.invoke(waf, ["init", "-y"]) + + assert result.exit_code == 0 + # Verify NodePing IPs were passed to run() + mock_installer.run.assert_called_once() + call_kwargs = mock_installer.run.call_args + assert call_kwargs.kwargs.get("nodeping_ips") == ["52.71.195.82"] + + def test_warns_on_nodeping_fetch_failure(self, runner, mock_installer): + """Should warn but continue when NodePing fetch fails.""" + pf = mock_installer.preflight.return_value + pf.can_proceed = True + pf.crs_installed = True + pf.crs_version = "4.8.0" + pf.warnings = [] + + run_result = MagicMock() + run_result.success = True + run_result.steps_completed = ["Done"] + run_result.steps_skipped = [] + run_result.warnings = [] + run_result.errors = [] + mock_installer.run.return_value = run_result + mock_installer.reload_apache.return_value = MagicMock(success=True, message="Reloaded") + + with patch( + "nssec.modules.waf.fetch_nodeping_probe_ips", + return_value=([], "Connection error"), + ): + result = runner.invoke(waf, ["init", "-y"]) + + assert result.exit_code == 0 + assert "Warning" in result.output + + +class TestWafUpdateExclusionsNodeping: + """Tests for waf update-exclusions command with NodePing IPs.""" + + def test_fetches_nodeping_ips_during_update(self, runner, mock_installer): + """Should fetch NodePing IPs and pass to install_exclusions().""" + step = MagicMock() + step.success = True + step.message = "Updated" + mock_installer.install_exclusions.return_value = step + mock_installer.validate_config.return_value = step + mock_installer.reload_apache.return_value = step + + with patch( + "nssec.modules.waf.fetch_nodeping_probe_ips", + return_value=(["52.71.195.82", "3.21.118.250"], ""), + ): + result = runner.invoke(waf, ["update-exclusions", "-y"]) + + assert result.exit_code == 0 + mock_installer.install_exclusions.assert_called_once() + call_kwargs = mock_installer.install_exclusions.call_args + assert call_kwargs.kwargs.get("nodeping_ips") == ["52.71.195.82", "3.21.118.250"] + + def test_warns_on_nodeping_fetch_failure_update(self, runner, mock_installer): + """Should warn but continue when NodePing fetch fails during update.""" + step = MagicMock() + step.success = True + step.message = "Updated" + mock_installer.install_exclusions.return_value = step + mock_installer.validate_config.return_value = step + mock_installer.reload_apache.return_value = step + + with patch( + "nssec.modules.waf.fetch_nodeping_probe_ips", + return_value=([], "Timeout"), + ): + result = runner.invoke(waf, ["update-exclusions", "-y"]) + + assert result.exit_code == 0 + assert "Warning" in result.output + + +class TestWafUpdate: + """Tests for waf update command.""" + + def test_requires_root(self, runner, mock_installer): + """Should fail if not root.""" + mock_installer.preflight.return_value.is_root = False + + with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.5"), \ + patch("nssec.modules.waf.utils.version_gte", return_value=False): + result = runner.invoke(waf, ["update", "-y"]) + + assert result.exit_code == 1 + assert "root" in result.output.lower() + + def test_shows_instructions_when_old(self, runner, mock_installer): + """Should show Digitalwave repo instructions when ModSec < 2.9.6.""" + with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.5"), \ + patch("nssec.modules.waf.utils.version_gte", return_value=False): + result = runner.invoke(waf, ["update", "-y"]) + + assert result.exit_code == 0 + assert "Digitalwave" in result.output + assert "signed-by=" in result.output + assert "apt-get update" in result.output + + def test_nothing_to_do_when_current_no_disabled(self, runner, mock_installer): + """Should report nothing to do when >= 2.9.6 and no disabled rules.""" + mock_installer._reenable_crs_rules.return_value = [] + mock_installer.preflight.return_value.crs_path = "/etc/modsecurity/crs" + + with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.7"), \ + patch("nssec.modules.waf.utils.version_gte", return_value=True): + result = runner.invoke(waf, ["update", "-y"]) + + assert result.exit_code == 0 + assert "Nothing to do" in result.output + + def test_reenables_rules_after_upgrade(self, runner, mock_installer): + """Should re-enable disabled rules when ModSec >= 2.9.6.""" + mock_installer._reenable_crs_rules.return_value = ["REQUEST-922-MULTIPART-ATTACK.conf"] + mock_installer.preflight.return_value.crs_path = "/etc/modsecurity/crs" + + validate_result = MagicMock() + validate_result.success = True + validate_result.message = "Apache config test passed" + mock_installer.validate_config.return_value = validate_result + + reload_result = MagicMock() + reload_result.success = True + reload_result.message = "Apache reloaded" + mock_installer.reload_apache.return_value = reload_result + + with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.7"), \ + patch("nssec.modules.waf.utils.version_gte", return_value=True): + result = runner.invoke(waf, ["update", "-y"]) + + assert result.exit_code == 0 + mock_installer._reenable_crs_rules.assert_called_once() + assert "Re-enabled" in result.output diff --git a/tests/unit/test_waf_module.py b/tests/unit/test_waf_module.py index 8fd8095..703b88c 100644 --- a/tests/unit/test_waf_module.py +++ b/tests/unit/test_waf_module.py @@ -1,7 +1,8 @@ """Tests for WAF module functions.""" import pytest -from unittest.mock import patch, MagicMock, call +from pathlib import Path +from unittest.mock import patch, MagicMock, call, ANY from jinja2 import Template @@ -425,3 +426,337 @@ def test_detect_mode_does_not_toggle_evasive(self, mock_file_ops): assert not any("evasive" in c for c in run_calls) # Message should NOT mention mod_evasive assert "mod_evasive" not in result.message + + +class TestFetchNodepingProbeIps: + """Tests for fetch_nodeping_probe_ips function.""" + + def test_returns_ips_from_mtls_util(self): + """Should delegate to mTLS fetch_nodeping_ips utility.""" + from nssec.modules.waf import fetch_nodeping_probe_ips + + with patch( + "nssec.modules.mtls.utils.fetch_nodeping_ips", + return_value=(["52.71.195.82", "3.21.118.250"], ""), + ): + ips, err = fetch_nodeping_probe_ips() + + assert ips == ["52.71.195.82", "3.21.118.250"] + assert err == "" + + def test_returns_error_on_failure(self): + """Should propagate error from mTLS utility.""" + from nssec.modules.waf import fetch_nodeping_probe_ips + + with patch( + "nssec.modules.mtls.utils.fetch_nodeping_ips", + return_value=([], "Failed to fetch NodePing IPs: connection error"), + ): + ips, err = fetch_nodeping_probe_ips() + + assert ips == [] + assert "Failed to fetch" in err + + +class TestInstallExclusionsWithNodeping: + """Tests for install_exclusions with nodeping_ips parameter.""" + + def test_passes_nodeping_ips_to_template(self, mock_file_ops): + """Should pass nodeping_ips to the exclusions template.""" + from nssec.modules.waf import ModSecurityInstaller + + installer = ModSecurityInstaller() + nodeping = ["52.71.195.82", "3.21.118.250"] + result = installer.install_exclusions(nodeping_ips=nodeping) + + assert result.success + render_call = mock_file_ops["render"].call_args + assert render_call[1].get("nodeping_ips") == nodeping or \ + nodeping in render_call[0] if render_call[0] else False + + def test_defaults_to_empty_nodeping_list(self, mock_file_ops): + """Should default to empty list when no nodeping_ips provided.""" + from nssec.modules.waf import ModSecurityInstaller + + installer = ModSecurityInstaller() + result = installer.install_exclusions() + + assert result.success + render_call = mock_file_ops["render"].call_args + # nodeping_ips should be an empty list + assert render_call[1].get("nodeping_ips") == [] or \ + render_call.kwargs.get("nodeping_ips") == [] + + def test_passes_both_admin_and_nodeping_ips(self, mock_file_ops): + """Should pass both admin_ips and nodeping_ips to template.""" + from nssec.modules.waf import ModSecurityInstaller + + installer = ModSecurityInstaller() + admin = ["192.168.1.100"] + nodeping = ["52.71.195.82"] + result = installer.install_exclusions(admin_ips=admin, nodeping_ips=nodeping) + + assert result.success + render_call = mock_file_ops["render"].call_args + assert render_call.kwargs.get("admin_ips") == admin + assert render_call.kwargs.get("nodeping_ips") == nodeping + + +class TestNodepingExclusionsTemplate: + """Tests for NS_EXCLUSIONS_TEMPLATE with NodePing IPs.""" + + def test_renders_nodeping_section(self): + """Should render NodePing IP rules in the exclusions template.""" + from nssec.modules.waf.config import NS_EXCLUSIONS_TEMPLATE + + rendered = Template(NS_EXCLUSIONS_TEMPLATE).render( + timestamp="test", + admin_ips=[], + nodeping_ips=["52.71.195.82", "3.21.118.250"], + ) + assert "NodePing monitoring probe IPs" in rendered + assert "52.71.195.82" in rendered + assert "3.21.118.250" in rendered + assert "id:1000201" in rendered + assert "id:1000202" in rendered + + def test_omits_nodeping_section_when_empty(self): + """Should not render NodePing section when list is empty.""" + from nssec.modules.waf.config import NS_EXCLUSIONS_TEMPLATE + + rendered = Template(NS_EXCLUSIONS_TEMPLATE).render( + timestamp="test", + admin_ips=[], + nodeping_ips=[], + ) + assert "NodePing" not in rendered + + def test_nodeping_rule_ids_separate_from_admin(self): + """NodePing rules should use 1000200+ range, admin uses 1000100+.""" + from nssec.modules.waf.config import NS_EXCLUSIONS_TEMPLATE + + rendered = Template(NS_EXCLUSIONS_TEMPLATE).render( + timestamp="test", + admin_ips=["192.168.1.100"], + nodeping_ips=["52.71.195.82"], + ) + assert "id:1000101" in rendered # admin IP + assert "id:1000201" in rendered # NodePing IP + + def test_nodeping_rules_bypass_crs(self): + """NodePing rules should bypass all CRS rules.""" + from nssec.modules.waf.config import NS_EXCLUSIONS_TEMPLATE + + rendered = Template(NS_EXCLUSIONS_TEMPLATE).render( + timestamp="test", + admin_ips=[], + nodeping_ips=["52.71.195.82"], + ) + # Find the NodePing rule section + lines = rendered.split("\n") + nodeping_rule_lines = [l for l in lines if "1000201" in l] + assert len(nodeping_rule_lines) > 0 + assert "ruleRemoveByTag=OWASP_CRS" in rendered + + +class TestInstallCrsV4UpdatesSetup: + """Tests for install_crs_v4 updating crs-setup.conf when v4 already present.""" + + def test_updates_crs_setup_when_v4_present(self, mock_file_ops): + """Should update crs-setup.conf even when CRS v4 is already installed.""" + from nssec.modules.waf import ModSecurityInstaller + + installer = ModSecurityInstaller() + pf = MagicMock() + pf.crs_installed = True + pf.crs_version = "4.8.0" + pf.crs_path = "/etc/modsecurity/crs" + pf.can_proceed = True + installer._preflight = pf + + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.7"), \ + patch("nssec.modules.waf.version_gte", return_value=True): + result = installer.install_crs_v4() + + assert result.skipped + assert "crs-setup.conf updated" in result.message + # Should have called write_file for crs-setup.conf + mock_file_ops["write"].assert_called_once() + write_args = mock_file_ops["write"].call_args + assert "crs-setup.conf" in write_args[0][0] + + def test_skips_setup_update_renders_template(self, mock_file_ops): + """Should render the CRS_SETUP_OVERRIDES_TEMPLATE when updating.""" + from nssec.modules.waf import ModSecurityInstaller + + installer = ModSecurityInstaller() + pf = MagicMock() + pf.crs_installed = True + pf.crs_version = "4.8.0" + pf.crs_path = "/etc/modsecurity/crs" + pf.can_proceed = True + installer._preflight = pf + + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.7"), \ + patch("nssec.modules.waf.version_gte", return_value=True): + installer.install_crs_v4() + + # render should have been called for crs-setup.conf + mock_file_ops["render"].assert_called_once() + + +class TestPreflightCacheRefresh: + """Tests for preflight cache being cleared after CRS download.""" + + def test_clears_preflight_after_download(self, mock_file_ops): + """Should clear _preflight cache after successful CRS download.""" + from nssec.modules.waf import ModSecurityInstaller + + with patch("nssec.modules.waf.run_cmd", return_value=("", "", 0)), \ + patch("nssec.modules.waf.Path"): + installer = ModSecurityInstaller() + pf = MagicMock() + pf.crs_installed = False + pf.crs_version = None + pf.crs_path = None + pf.can_proceed = True + installer._preflight = pf + + result = installer._download_crs_from_github() + + assert result.success + assert installer._preflight is None + + def test_does_not_clear_preflight_on_download_failure(self, mock_file_ops): + """Should not clear preflight cache if download fails.""" + from nssec.modules.waf import ModSecurityInstaller + + with patch("nssec.modules.waf.run_cmd", return_value=("", "error", 1)): + installer = ModSecurityInstaller() + pf = MagicMock() + installer._preflight = pf + + result = installer._download_crs_from_github() + + assert not result.success + assert installer._preflight is pf # unchanged + + +class TestDisableIncompatibleCrsRules: + """Tests for ModSecurityInstaller._disable_incompatible_crs_rules.""" + + def test_disables_rules_on_old_modsec(self, tmp_path, mock_file_ops): + """Should rename .conf to .conf.disabled when ModSec < 2.9.6.""" + from nssec.modules.waf import ModSecurityInstaller + + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + rule_file = rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf" + rule_file.write_text("# rule content") + + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), \ + patch("nssec.modules.waf.version_gte", side_effect=lambda v, t: False): + installer = ModSecurityInstaller() + disabled = installer._disable_incompatible_crs_rules(str(tmp_path)) + + assert disabled == ["REQUEST-922-MULTIPART-ATTACK.conf"] + assert (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf.disabled").exists() + assert not rule_file.exists() + + def test_skips_on_new_modsec(self, tmp_path, mock_file_ops): + """Should not disable rules when ModSec >= 2.9.6.""" + from nssec.modules.waf import ModSecurityInstaller + + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + rule_file = rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf" + rule_file.write_text("# rule content") + + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.6"), \ + patch("nssec.modules.waf.version_gte", side_effect=lambda v, t: True): + installer = ModSecurityInstaller() + disabled = installer._disable_incompatible_crs_rules(str(tmp_path)) + + assert disabled == [] + assert rule_file.exists() + + def test_skips_already_disabled(self, tmp_path, mock_file_ops): + """Should not rename if .conf.disabled already exists.""" + from nssec.modules.waf import ModSecurityInstaller + + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + # Both files exist — should not touch them + (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf").write_text("# rule") + (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf.disabled").write_text("# disabled") + + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), \ + patch("nssec.modules.waf.version_gte", side_effect=lambda v, t: False): + installer = ModSecurityInstaller() + disabled = installer._disable_incompatible_crs_rules(str(tmp_path)) + + assert disabled == [] + + def test_handles_missing_rule_file(self, tmp_path, mock_file_ops): + """Should handle case where rule file doesn't exist.""" + from nssec.modules.waf import ModSecurityInstaller + + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + # No rule files created + + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), \ + patch("nssec.modules.waf.version_gte", side_effect=lambda v, t: False): + installer = ModSecurityInstaller() + disabled = installer._disable_incompatible_crs_rules(str(tmp_path)) + + assert disabled == [] + + +class TestReenableCrsRules: + """Tests for ModSecurityInstaller._reenable_crs_rules.""" + + def test_reenables_disabled_rules(self, tmp_path, mock_file_ops): + """Should rename .conf.disabled back to .conf.""" + from nssec.modules.waf import ModSecurityInstaller + + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + disabled_file = rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf.disabled" + disabled_file.write_text("# rule content") + + installer = ModSecurityInstaller() + reenabled = installer._reenable_crs_rules(str(tmp_path)) + + assert reenabled == ["REQUEST-922-MULTIPART-ATTACK.conf"] + assert (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf").exists() + assert not disabled_file.exists() + + def test_skips_when_target_exists(self, tmp_path, mock_file_ops): + """Should not rename if .conf already exists (avoid overwrite).""" + from nssec.modules.waf import ModSecurityInstaller + + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf").write_text("# active") + (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf.disabled").write_text("# old") + + installer = ModSecurityInstaller() + reenabled = installer._reenable_crs_rules(str(tmp_path)) + + assert reenabled == [] + + def test_returns_empty_when_nothing_disabled(self, tmp_path, mock_file_ops): + """Should return empty list when no disabled files found.""" + from nssec.modules.waf import ModSecurityInstaller + + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf").write_text("# active") + + installer = ModSecurityInstaller() + reenabled = installer._reenable_crs_rules(str(tmp_path)) + + assert reenabled == [] + + diff --git a/tests/unit/test_waf_status.py b/tests/unit/test_waf_status.py new file mode 100644 index 0000000..e04bb15 --- /dev/null +++ b/tests/unit/test_waf_status.py @@ -0,0 +1,133 @@ +"""Tests for WAF status reporting.""" + +import pytest +from unittest.mock import patch, MagicMock +from jinja2 import Template + + +class TestParseExclusionsMeta: + """Tests for _parse_exclusions_meta.""" + + def test_parses_version_and_hash(self): + from nssec.modules.waf.status import _parse_exclusions_meta + + content = """\ +# nssec-exclusions-version: 2 +# nssec-exclusions-hash: abc123def456 +SecRule REMOTE_ADDR "@ipMatch 192.168.1.1" "id:1000101,phase:1" +SecRule REMOTE_ADDR "@ipMatch 52.71.195.82" "id:1000201,phase:1" +""" + version, template_hash, admin, nodeping = _parse_exclusions_meta(content) + assert version == "2" + assert template_hash == "abc123def456" + assert admin == 1 + assert nodeping == 1 + + def test_counts_multiple_ips(self): + from nssec.modules.waf.status import _parse_exclusions_meta + + content = """\ +# nssec-exclusions-version: 2 +# nssec-exclusions-hash: abc123 +"id:1000101,phase:1" +"id:1000102,phase:1" +"id:1000201,phase:1" +"id:1000202,phase:1" +"id:1000203,phase:1" +""" + version, _, admin, nodeping = _parse_exclusions_meta(content) + assert admin == 2 + assert nodeping == 3 + + def test_handles_missing_version_and_hash(self): + from nssec.modules.waf.status import _parse_exclusions_meta + + content = "# Old exclusions file without version\nSecRule something" + version, template_hash, admin, nodeping = _parse_exclusions_meta(content) + assert version is None + assert template_hash is None + assert admin == 0 + assert nodeping == 0 + + +class TestParseSecurity2CrsPath: + """Tests for _parse_security2_crs_path.""" + + def test_extracts_crs_path(self): + from nssec.modules.waf.status import _parse_security2_crs_path + + content = """\ + + IncludeOptional /etc/modsecurity/modsecurity.conf + IncludeOptional /etc/modsecurity/crs/crs-setup.conf + IncludeOptional /etc/modsecurity/crs/rules/*.conf + +""" + assert _parse_security2_crs_path(content) == "/etc/modsecurity/crs" + + def test_extracts_apt_crs_path(self): + from nssec.modules.waf.status import _parse_security2_crs_path + + content = "IncludeOptional /usr/share/modsecurity-crs/crs-setup.conf" + assert _parse_security2_crs_path(content) == "/usr/share/modsecurity-crs" + + def test_returns_none_when_no_crs(self): + from nssec.modules.waf.status import _parse_security2_crs_path + + content = "IncludeOptional /etc/modsecurity/modsecurity.conf" + assert _parse_security2_crs_path(content) is None + + +class TestExclusionsHashDrift: + """Tests for template hash drift detection.""" + + def test_matching_hash_means_current(self): + from nssec.modules.waf.config import ( + NS_EXCLUSIONS_HASH, + NS_EXCLUSIONS_TEMPLATE, + NS_EXCLUSIONS_VERSION, + ) + from nssec.modules.waf.status import _parse_exclusions_meta + + # Render the template with the current hash + rendered = Template(NS_EXCLUSIONS_TEMPLATE).render( + timestamp="test", + admin_ips=[], + nodeping_ips=[], + version=NS_EXCLUSIONS_VERSION, + template_hash=NS_EXCLUSIONS_HASH, + ) + _, deployed_hash, _, _ = _parse_exclusions_meta(rendered) + assert deployed_hash == NS_EXCLUSIONS_HASH + + def test_old_hash_means_outdated(self): + from nssec.modules.waf.config import NS_EXCLUSIONS_HASH + from nssec.modules.waf.status import _parse_exclusions_meta + + content = "# nssec-exclusions-hash: stale_old_hash\n" + _, deployed_hash, _, _ = _parse_exclusions_meta(content) + assert deployed_hash != NS_EXCLUSIONS_HASH + + def test_missing_hash_means_outdated(self): + from nssec.modules.waf.config import NS_EXCLUSIONS_HASH + from nssec.modules.waf.status import _parse_exclusions_meta + + content = "# Old file without hash\n" + _, deployed_hash, _, _ = _parse_exclusions_meta(content) + assert deployed_hash is None + assert deployed_hash != NS_EXCLUSIONS_HASH + + +class TestExclusionsTemplateHash: + """Tests for the template hash computation.""" + + def test_hash_is_deterministic(self): + from nssec.modules.waf.config import _exclusions_template_hash + + assert _exclusions_template_hash() == _exclusions_template_hash() + + def test_hash_is_12_chars(self): + from nssec.modules.waf.config import NS_EXCLUSIONS_HASH + + assert len(NS_EXCLUSIONS_HASH) == 12 + assert all(c in "0123456789abcdef" for c in NS_EXCLUSIONS_HASH) diff --git a/tests/unit/test_waf_utils.py b/tests/unit/test_waf_utils.py new file mode 100644 index 0000000..2f80b19 --- /dev/null +++ b/tests/unit/test_waf_utils.py @@ -0,0 +1,255 @@ +"""Tests for WAF utility functions.""" + +from unittest.mock import patch + +import pytest + + +class TestVersionGte: + """Tests for version_gte helper.""" + + def test_equal_versions(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte("2.9.6", "2.9.6") is True + + def test_greater_patch(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte("2.9.7", "2.9.6") is True + + def test_greater_minor(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte("2.10.0", "2.9.6") is True + + def test_greater_major(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte("3.0.0", "2.9.6") is True + + def test_less_than(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte("2.9.5", "2.9.6") is False + + def test_none_returns_false(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte(None, "2.9.6") is False + + def test_empty_string_returns_false(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte("", "2.9.6") is False + + def test_invalid_version_returns_false(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte("not.a.version", "2.9.6") is False + + def test_two_component_versions(self): + from nssec.modules.waf.utils import version_gte + + assert version_gte("2.10", "2.9") is True + assert version_gte("2.8", "2.9") is False + + +class TestDetectModsecVersion: + """Tests for detect_modsec_version.""" + + def test_returns_version_string(self): + from nssec.modules.waf.utils import detect_modsec_version + + with patch("nssec.modules.waf.utils.run_cmd", return_value=("2.9.5-3", "", 0)): + ver = detect_modsec_version() + assert ver == "2.9.5" + + def test_returns_none_when_not_installed(self): + from nssec.modules.waf.utils import detect_modsec_version + + with patch("nssec.modules.waf.utils.run_cmd", return_value=("", "not installed", 1)): + ver = detect_modsec_version() + assert ver is None + + def test_strips_debian_suffix(self): + from nssec.modules.waf.utils import detect_modsec_version + + with patch("nssec.modules.waf.utils.run_cmd", return_value=("2.9.7-1ubuntu2", "", 0)): + ver = detect_modsec_version() + assert ver == "2.9.7" + + def test_returns_none_on_empty_stdout(self): + from nssec.modules.waf.utils import detect_modsec_version + + with patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + ver = detect_modsec_version() + assert ver is None + + +DEBIAN_DEFAULT_SEC2 = """\ + + SecDataDir /var/cache/modsecurity + IncludeOptional /etc/modsecurity/*.conf + IncludeOptional /usr/share/modsecurity-crs/*.load + +""" + +NSSEC_MANAGED_SEC2 = """\ + + IncludeOptional /etc/modsecurity/modsecurity.conf + IncludeOptional /etc/modsecurity/crs/crs-setup.conf + IncludeOptional /etc/modsecurity/crs/rules/*.conf + +""" + + +class TestParseSecurity2Conf: + """Tests for parse_security2_conf utility.""" + + def test_detects_wildcard_include(self): + from nssec.modules.waf.utils import parse_security2_conf + + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2): + has_wildcard, has_crs_load = parse_security2_conf("/fake") + assert has_wildcard is True + assert has_crs_load is True + + def test_detects_no_wildcard(self): + from nssec.modules.waf.utils import parse_security2_conf + + with patch("nssec.modules.waf.utils.read_file", return_value=NSSEC_MANAGED_SEC2): + has_wildcard, has_crs_load = parse_security2_conf("/fake") + assert has_wildcard is False + assert has_crs_load is False + + def test_ignores_commented_lines(self): + from nssec.modules.waf.utils import parse_security2_conf + + content = """\ + + # IncludeOptional /etc/modsecurity/*.conf + # IncludeOptional /usr/share/modsecurity-crs/*.load + +""" + with patch("nssec.modules.waf.utils.read_file", return_value=content): + has_wildcard, has_crs_load = parse_security2_conf("/fake") + assert has_wildcard is False + assert has_crs_load is False + + def test_returns_false_when_file_missing(self): + from nssec.modules.waf.utils import parse_security2_conf + + with patch("nssec.modules.waf.utils.read_file", return_value=None): + has_wildcard, has_crs_load = parse_security2_conf("/fake") + assert has_wildcard is False + assert has_crs_load is False + + +class TestAppendCrsToSecurity2: + """Tests for append_crs_to_security2 utility.""" + + def test_appends_crs_includes(self): + from nssec.modules.waf.utils import append_crs_to_security2 + + written = {} + + def capture_write(path, content): + written["content"] = content + return True + + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), \ + patch("nssec.modules.waf.utils.backup_file"), \ + patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): + result = append_crs_to_security2("/etc/modsecurity/crs") + + assert result is True + content = written["content"] + assert "IncludeOptional /etc/modsecurity/crs/crs-setup.conf" in content + assert "IncludeOptional /etc/modsecurity/crs/rules/*.conf" in content + assert "added by nssec" in content + + def test_comments_out_old_v3_load_line(self): + """Should comment out the apt v3 *.load line to prevent dual-loading.""" + from nssec.modules.waf.utils import append_crs_to_security2 + + written = {} + + def capture_write(path, content): + written["content"] = content + return True + + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), \ + patch("nssec.modules.waf.utils.backup_file"), \ + patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): + append_crs_to_security2("/etc/modsecurity/crs") + + content = written["content"] + # The old v3 line should be commented out + assert "# IncludeOptional /usr/share/modsecurity-crs/*.load" in content + assert "Disabled by nssec" in content + # It should NOT appear as an active (uncommented) directive + for line in content.splitlines(): + stripped = line.strip() + if stripped.startswith("#"): + continue + assert "modsecurity-crs/*.load" not in stripped + + def test_preserves_wildcard_modsecurity_conf(self): + """Should NOT touch the /etc/modsecurity/*.conf wildcard.""" + from nssec.modules.waf.utils import append_crs_to_security2 + + written = {} + + def capture_write(path, content): + written["content"] = content + return True + + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), \ + patch("nssec.modules.waf.utils.backup_file"), \ + patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): + append_crs_to_security2("/etc/modsecurity/crs") + + content = written["content"] + # The modsecurity/*.conf line should remain active (not commented) + active_lines = [ + l.strip() for l in content.splitlines() + if not l.strip().startswith("#") and l.strip() + ] + assert any("/etc/modsecurity/*.conf" in l for l in active_lines) + + def test_does_not_comment_out_already_commented_lines(self): + """Should not double-comment already commented CRS lines.""" + from nssec.modules.waf.utils import append_crs_to_security2 + + content_with_comment = """\ + + SecDataDir /var/cache/modsecurity + IncludeOptional /etc/modsecurity/*.conf + # IncludeOptional /usr/share/modsecurity-crs/*.load + +""" + written = {} + + def capture_write(path, content): + written["content"] = content + return True + + with patch("nssec.modules.waf.utils.read_file", return_value=content_with_comment), \ + patch("nssec.modules.waf.utils.backup_file"), \ + patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): + append_crs_to_security2("/etc/modsecurity/crs") + + content = written["content"] + # Should not have "Disabled by nssec" since the line was already commented + assert "Disabled by nssec" not in content + + def test_returns_false_on_write_failure(self): + from nssec.modules.waf.utils import append_crs_to_security2 + + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), \ + patch("nssec.modules.waf.utils.backup_file"), \ + patch("nssec.modules.waf.utils.write_file", return_value=False): + result = append_crs_to_security2("/etc/modsecurity/crs") + + assert result is False From aa523783f9ffb0e605f53d031cff49907cd26b77 Mon Sep 17 00:00:00 2001 From: Sumner Robinson Date: Thu, 5 Mar 2026 18:07:55 +0000 Subject: [PATCH 2/3] Add SSH Login iNSight dashboard and fix CRS exclusion load order - Create insight/sshLogin.json: SSH login monitor dashboard with failed/successful login stats, unique logins (user+IP pairs), login trends, top offending IPs, targeted usernames, and auth log stream - Fix security2.conf template: move netsapiens-exclusions.conf include to after CRS rules so SecRuleUpdateTargetById directives can find the rules they modify (fixes 932270 cookie false positives) - Update README with sshLogin.json in iNSight Templates list --- README.md | 23 +- insight/sshLogin.json | 1045 +++++++++++++++++++++++++++++++ src/nssec/modules/waf/config.py | 20 +- 3 files changed, 1071 insertions(+), 17 deletions(-) create mode 100644 insight/sshLogin.json diff --git a/README.md b/README.md index c3bfe33..ad4af5f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ pip install -e . |----|-------------------|--------| | Ubuntu 24.04 LTS | v44.x | Tested | | Ubuntu 22.04 LTS | v44.x | Tested | -| Ubuntu 20.04 LTS | v44.x | Binary/.deb only | +| Ubuntu 20.04 LTS | v44.x | Source Only | Other Debian-based distributions may work but are untested. Contributions and test reports for additional platforms are welcome. @@ -97,9 +97,11 @@ sudo nssec waf enable The WAF module includes: - OWASP CRS v4 with paranoia level 1 (low false positive rate) -- NetSapiens exclusion rules for admin UI, ns-api, SiPbx, NqsProxy, and iNSight health checks +- NetSapiens exclusion rules for admin UI, ns-api, SiPbx, NqsProxy, portal login, phone provisioning, iNSight health checks, and localhost traffic - CRS tuning for allowed HTTP methods and content types used by NetSapiens +WAF rule templates (exclusions and CRS setup overrides) are defined in `src/nssec/modules/waf/config.py` and deployed to `/etc/modsecurity/` by `nssec waf init`. + ### Path Restrictions (.htaccess) Restrict access to sensitive NetSapiens paths (admin UI, API, NDP, recording) using `.htaccess` IP allowlists: @@ -167,24 +169,21 @@ Start with `standard` and review the Apache API Usage dashboard and mod_evasive | Component | Core | NDP | Recording | QoS | |-----------|:----:|:---:|:---------:|:---:| -| WAF — Admin UI | Yes | — | — | — | -| WAF — Endpoints | — | Yes | — | — | -| WAF — Large Upload | — | — | Yes | — | +| WAF | Yes | Yes | Yes | Yes | | mTLS Provisioning | — | Yes | — | — | -| MySQL Hardening | Yes | — | — | — | - -## Grafana Dashboards & Insight Templates +| MySQL Hardening | Yes | Yes | Yes | Yes | -Pre-built dashboards are available for import into your Grafana/iNSight instance: +## iNSight Templates -**Dashboards** (`dashboards/`): -- `security/apacheHttpServerLogs.json` — Apache error and access logs with HTTP status breakdown +Pre-built dashboards for import into your iNSight/Grafana instance (`insight/`): -**Insight Templates** (`insight/`): - `api.json` — API v1/v2 request rate monitoring (Prometheus) - `apacheApiUsage.json` — Apache access log analysis by IP and path (Loki) - `modsecurityWaf.json` — ModSecurity WAF event analysis: severity, attacking IPs, triggered rules, targeted URIs (Loki) - `modEvasive.json` — mod_evasive HTTP flood protection: blocked IPs, block rate, repeat offenders (Loki) +- `sshLogin.json` — SSH login monitor: failed/successful logins, brute-force source IPs, targeted usernames (Loki) + +![WAF iNSight Dashboard](docs/img/waf-insight.png) ## Related Projects diff --git a/insight/sshLogin.json b/insight/sshLogin.json new file mode 100644 index 0000000..9d453ac --- /dev/null +++ b/insight/sshLogin.json @@ -0,0 +1,1045 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Dashboard for monitoring SSH login activity: failed/successful logins, brute-force sources, and targeted usernames from /var/log/auth.log", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 100, + "panels": [], + "title": "Overview Statistics", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 200 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 0, + "y": 1 + }, + "id": 101, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "sum(count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |= \"Failed password\" |~ `(?i)$ip_filter` [$__range]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Failed Logins", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 5, + "y": 1 + }, + "id": 102, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "sum(count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |~ \"Accepted (password|publickey|keyboard-interactive) for\" |~ `(?i)$ip_filter` [$__range]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "SSH Sessions", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 10, + "y": 1 + }, + "id": 105, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "count(sum by (username, src_ip) (count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |~ \"Accepted (password|publickey|keyboard-interactive) for\" |~ `(?i)$ip_filter` | regexp `for (?P\\S+) from (?P\\d+\\.\\d+\\.\\d+\\.\\d+)` [$__range])))", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Unique Logins", + "description": "Distinct user + source IP combinations", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "red", + "value": 20 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 15, + "y": 1 + }, + "id": 103, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "count(sum by (src_ip) (count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |= \"Failed password\" |~ `(?i)$ip_filter` | regexp `from (?P\\d+\\.\\d+\\.\\d+\\.\\d+)` [$__range])))", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Unique Source IPs (Failed)", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 10 + }, + { + "color": "red", + "value": 50 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "id": 104, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "sum(count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |= \"Invalid user\" |~ `(?i)$ip_filter` [$__range]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Invalid Users", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 200, + "panels": [], + "title": "Login Trends", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "count", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 50, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Failed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Accepted" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 201, + "options": { + "legend": { + "calcs": [ + "sum", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "sum(count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |= \"Failed password\" |~ `(?i)$ip_filter` [$__interval]))", + "legendFormat": "Failed", + "queryType": "range", + "refId": "A" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "sum(count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |~ \"Accepted (password|publickey|keyboard-interactive) for\" |~ `(?i)$ip_filter` [$__interval]))", + "legendFormat": "Accepted", + "queryType": "range", + "refId": "B" + } + ], + "title": "Login Attempts Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 202, + "options": { + "legend": { + "calcs": [ + "sum", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "topk(10, sum by (src_ip) (count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |= \"Failed password\" |~ `(?i)$ip_filter` | regexp `from (?P\\d+\\.\\d+\\.\\d+\\.\\d+)` [$__interval])))", + "legendFormat": "{{src_ip}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Failed Logins by IP Over Time", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 300, + "panels": [], + "title": "Top Offenders", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 10 + }, + { + "color": "red", + "value": 50 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "IP Address" + }, + "properties": [ + { + "id": "custom.width", + "value": 200 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Failed Count" + }, + "properties": [ + { + "id": "custom.width", + "value": 150 + }, + { + "id": "custom.displayMode", + "value": "gradient-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 15 + }, + "id": 301, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Failed Count" + } + ] + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "topk(20, sum by (src_ip) (count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |= \"Failed password\" |~ `(?i)$ip_filter` | regexp `from (?P\\d+\\.\\d+\\.\\d+\\.\\d+)` [$__range])))", + "legendFormat": "{{src_ip}}", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Top 20 Failed IPs", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "src_ip": 0, + "Value": 1 + }, + "renameByName": { + "Value": "Failed Count", + "src_ip": "IP Address" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 10 + }, + { + "color": "red", + "value": 50 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Username" + }, + "properties": [ + { + "id": "custom.width", + "value": 200 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Attempt Count" + }, + "properties": [ + { + "id": "custom.width", + "value": 150 + }, + { + "id": "custom.displayMode", + "value": "gradient-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 15 + }, + "id": 302, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Attempt Count" + } + ] + }, + "pluginVersion": "9.1.6", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "topk(20, sum by (username) (count_over_time({instance=~\"$node\", filename=\"/var/log/auth.log\"} |= \"Failed password\" |~ `(?i)$ip_filter` | regexp `Failed password for (invalid user )?(?P\\S+)` [$__range])))", + "legendFormat": "{{username}}", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Top 20 Targeted Usernames", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "username": 0, + "Value": 1 + }, + "renameByName": { + "Value": "Attempt Count", + "username": "Username" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 400, + "panels": [], + "title": "Auth Logs", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "gridPos": { + "h": 15, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 401, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki_datasource}" + }, + "editorMode": "code", + "expr": "{instance=~\"$node\", filename=\"/var/log/auth.log\"} |~ `(?i)$ip_filter`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Auth Log Stream", + "type": "logs" + } + ], + "refresh": "30s", + "schemaVersion": 37, + "style": "dark", + "tags": [ + "ssh", + "auth", + "security" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "hide": 2, + "includeAll": false, + "label": "", + "multi": false, + "name": "prometheus_datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "hide": 2, + "includeAll": false, + "label": "", + "multi": false, + "name": "loki_datasource", + "options": [], + "query": "loki", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${prometheus_datasource}" + }, + "definition": "label_values(insight_log_rate,instance)", + "hide": 0, + "includeAll": true, + "label": "Host:", + "multi": true, + "name": "node", + "options": [], + "query": { + "query": "label_values(insight_log_rate,instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "hide": 0, + "label": "IP Filter (regex):", + "name": "ip_filter", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "skipUrlSync": false, + "type": "textbox" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h" + ] + }, + "timezone": "", + "title": "SSH Login Monitor", + "uid": "ssh-login-monitor", + "version": 1, + "weekStart": "" +} diff --git a/src/nssec/modules/waf/config.py b/src/nssec/modules/waf/config.py index 38b6073..0b28ede 100644 --- a/src/nssec/modules/waf/config.py +++ b/src/nssec/modules/waf/config.py @@ -72,7 +72,7 @@ BACKUP_SUFFIX = ".bak.nssec" # Exclusions template version — human-readable label for the template revision. -NS_EXCLUSIONS_VERSION = "2" +NS_EXCLUSIONS_VERSION = "3" # --------------------------------------------------------------------------- # Jinja2 Templates @@ -186,15 +186,16 @@ # Main ModSecurity config IncludeOptional /etc/modsecurity/modsecurity.conf - # NetSapiens-specific exclusions - IncludeOptional /etc/modsecurity/netsapiens-exclusions.conf - # OWASP CRS setup and rules IncludeOptional {{ crs_path }}/crs-setup.conf IncludeOptional {{ crs_path }}/plugins/*-config.conf IncludeOptional {{ crs_path }}/plugins/*-before.conf IncludeOptional {{ crs_path }}/rules/*.conf IncludeOptional {{ crs_path }}/plugins/*-after.conf + + # NetSapiens-specific exclusions (MUST load after CRS rules so that + # SecRuleUpdateTargetById directives can find the rules they modify) + IncludeOptional /etc/modsecurity/netsapiens-exclusions.conf """ @@ -263,6 +264,15 @@ nolog,\\ ctl:responseBodyAccess=Off" +# ---- Portal login password false positives ---- +# Passwords with shell metacharacters ($, ~, ^, |) trigger RCE rule 932270. +SecRule REQUEST_URI "@beginsWith /portal/login/login" \\ + "id:1000008,\\ + phase:2,\\ + pass,\\ + nolog,\\ + ctl:ruleRemoveTargetById=932270;ARGS:data[Login][password]" + # ---- iNSight health checks ---- SecRule REQUEST_URI "@beginsWith /cfg/insight_healthcheck" \\ "id:1000006,\\ @@ -322,7 +332,7 @@ nolog,\\ pass,\\ t:none,\\ - setvar:tx.crs_setup_version=400,\\ + setvar:tx.crs_setup_version=480,\\ setvar:tx.paranoia_level={{ paranoia_level }},\\ setvar:tx.blocking_paranoia_level={{ paranoia_level }},\\ setvar:tx.detection_paranoia_level={{ paranoia_level }}" From a4156e337b852fa4d9ecfe33ad24eddc8ecbc949 Mon Sep 17 00:00:00 2001 From: Sumner Robinson Date: Thu, 5 Mar 2026 18:37:39 +0000 Subject: [PATCH 3/3] Remove static CRS v3 rule files, refactor WAF modules, fix ruff lint - Delete rules/crs/ and rules/netsapiens/ (now Jinja2 templates in config.py) - Refactor waf_commands, restrict, and status modules - Replace Optional[X]/Union[X,Y] with X | None / X | Y across codebase - Fix ambiguous variable names and unused assignments in tests - Add WAF iNSight dashboard screenshot --- docs/img/waf-insight.png | Bin 0 -> 226618 bytes rules/crs/crs-setup-netsapiens.conf | 47 ---- rules/netsapiens/netsapiens-exclusions.conf | 77 ------ src/nssec/cli/__init__.py | 3 +- src/nssec/cli/main.py | 1 + src/nssec/cli/mtls_commands.py | 8 +- src/nssec/cli/waf_commands.py | 90 +++---- src/nssec/core/cache.py | 31 ++- src/nssec/core/checklist.py | 25 +- src/nssec/core/server_types.py | 5 +- src/nssec/core/ssh.py | 19 +- src/nssec/modules/waf/__init__.py | 3 +- src/nssec/modules/waf/restrict.py | 264 ++++++++++++++------ src/nssec/modules/waf/status.py | 29 +-- src/nssec/modules/waf/types.py | 7 +- tests/conftest.py | 14 +- tests/unit/test_waf_commands.py | 63 +++-- tests/unit/test_waf_module.py | 86 ++++--- tests/unit/test_waf_restrict.py | 117 ++++++--- tests/unit/test_waf_restrict_commands.py | 181 ++++++++------ tests/unit/test_waf_status.py | 2 - tests/unit/test_waf_utils.py | 39 ++- 22 files changed, 591 insertions(+), 520 deletions(-) create mode 100644 docs/img/waf-insight.png delete mode 100644 rules/crs/crs-setup-netsapiens.conf delete mode 100644 rules/netsapiens/netsapiens-exclusions.conf diff --git a/docs/img/waf-insight.png b/docs/img/waf-insight.png new file mode 100644 index 0000000000000000000000000000000000000000..5d667809d104ccf7c9cc1d9bb38caa62bcf47100 GIT binary patch literal 226618 zcmd?Qbx_>R_bvz_L4pMfF2UVhli)!UNN{)e!9BPIcP6+y!QI{6WnhrObr@v%{(kpX zy?OU;)va6m&(?O;)byM=-P3aVoTty{nJ{HVY4o>5Z{gtJ&}C&LzQMsEv%|r?9z{lc zk$A4}*1o*GcKR%nU67MY@TKxefkIM6 zm02ES1_wt0CoAzu)hz?E=IVw!_cU;RdqTT(rAYB14-SRp>gW0!T*+RmE8J&R^6%o9%9WIyyx>auuiinNW@W|26{mv#>VME=Tzv!Y-QS&KeTDn? zIA(?3u>Y&6Oq&z`9EO>hnfB@`I}uws0aE&yl%xh>Vq)TejjNMOPtOc~{K2&41bNvH zVw%1;35m&%#a^s)M;=XgE$bbW*T-HW;*$RuC#YM%1THWlo%nsko6!{s#^-ajT1iwaJF zN0Z7uk0SxqYg(}r=_oGSauTu~bZwL{j<`ebpT4zKBu8`{Ju-dU7+>I0{l`!w#3OC2;}@93Cg&?7~F7EHCGt*z|<0DKUF zbvjsF3zYn8(X`*abUN^c?V(GbeRFr$WtI2;wyuM3&If9+T$T#q5dSfo|5TeOEYrQW zRAIQC6o2nJ<+UO_+P_5KPG8h~#m5OUN0S|(e}9=w_J)17pCj^<9kbz9!jy#PV)VwG ziSUf?9XVAxVRT;lwS=CR3+>Uk5kDzVW_$U=BYWm)D=DS9^L4{jOAcG~wTr^){mey$ z-NAIlHPmjyLSTDo>qo!n6+b5MY=i1nS0OL`F=u}7!Le89{>KU7RERQ|tFC8XesUA$ zzPZPGHwSTfX)G@1!@w_;`RB}I<;HqxxzWC*n|DiT+K3d&x#4 z=M#m70${B{#4hE+Z=cjxC%G&8zB_G}uTgzQ3a3KdudH_*j62G?gCN(ZwH9-12|q}k zepjPJDaoMfFN*~QZP*r^+puB%;*U-|A2@O8CjroHZ6$(w6$*b}Ow6tlCP(A!&vK?` zK5KotZd&yOi!V6>Di4*OOX((sZ++r!lC@{It(cdrb?~G$CC8?szDtr6v1e?_NNcZP zf}{5te~hJ!Mb~iUcchdsj4XC14P#$eK7F~y{%a8~cPpVBRZRj1AdtMWt#F3wlPx2(6Zx4({4bHcDRDqN> zTU(omo)&K<^L5=!X|_|65x9j}+Zc@bZ9K?&>)jFGB!17}0+0W6K9(5kpUT5~3iEoZ z`@%dlv!r`Qc%0i(3to_<%gjX`*6i-OqW&iLq>J!4hg6T_`@7xh!(V2GKOBUq&yKh` z<3y2-* z^8L1*{TshSbK1_8#_dc(u;00NF0Q+sW}?PNhh)xei-;?hEHGrtOz8vNjGvwP8zIrA z#A8^g14c^>#zGY3?1Vfti_rQ3iibg9mw)NahTUoSZ^^d_;#D*TXziq16qLZwrdCQFt z_Ed*0JFDl!8e1BWU|eY2Z=Gn&hWL8#btV+BEJk5I3>JxsjnY@Ym6GYx?pqATHpb?8 zMl|BTb&@ya4hj6K7eC37bc3r%5(e0%4CIU<71_kasMMdQ961-CQpa~wU5~Va@$EZ&7b5{EnC@KgEas5qf_ZxE9nQK7 zSB&Z+oGCr~n5eD!-Z{?^*WE6S-e{1=eTSbDTBim;-!dl(B$(310n`1DUtvBFefFHn zm8Dr(urpcd=LbNq3hbCFoyI4+y8QVzGUSaVBB)l7 zU$td7(<2=Lo5^+)`Z~Io6$q8%KOZVp{_^_$7Ceu0zybL(>RC`@F~&=hLpNP67a~5z zE5!~|{Z$LGiJQKR9vUHZ(kE^$nPFfRHN6Qknm+x)x9QsF=^S-a=zNW2t;F%p?OI|v z1ZrgIMLs1SvX#jf$TE_waxjB7v(wpo8#b-nToemGO6I+G7jDZD{w#3=Z=W{xO*-PNCG;*@Wikt)9sOW4^J@k^g=n9DWJ0xkDwF3>`wHa?%L@O-9O zTR5k3tK`SJbWRBxE4syYb4=98`*+D-9khwjK5?KCz6-Ltw|lsnTVNh`rm@`o-us%w zJ1^BM4M#JxJ8hRq-L*69S60tH-l5|(l;4T?Xfn$ZGwr%9Y2Ck%n)M-)4Q%tfYUpFd z$r?#Rt=I5!CebwX(CXl@Cgx9V!P(NbjcV|9o?XJTp7Sut-`;s=;w6@eoRfl4SLxk`PKtgNJZsSh3Rk>Ux2Fz7Ux|LwTJW>Y~y zFa%<)>-#9uFLLF(cq~QR_9RNoS5bK1Ta5bDjfQTUy)n~30=1Ea1-Oj7|E4=4ARuak zmAav)09jwY{DW6U%hy0)HTLQ4j3Gn0g-qbh+WBC5*Dz3zrjRz^LP~6^{JYX>TxpkS zw*7TFb=i_v_cWzxWR|XSyi{TvHvdEZef7aw@Vhxdf|dCJ^_Dxe;k9q}$Uq5w39D5` z*i2{I;EkTH0r<&M+cL+TzZb#2zZm-cwE%66_k6AOhFxzUJrRKXqu&T7WSu`XZ$HLD zq)wneuvVju_hir!Yd-w5wcFOAX)r?A3qC+ZTG&Ht1fw95EPWeVhpIr`7qa!fBhBM9 zumt8<(y#scI8v~NeC8O?inAc=&`XuW@*%05IaH3KbQq2}83*4HHIwQUI~ZP_ey!P0 z(m{i6%v!$+RK?d8s;JO?<}?iQ)1o(6-!--yET~ydB^~K7^YnQS#rt?7h)qd(K(rs? zuJz%&UmIm2kKB){>dCkK+rIMa{oQM2sp+4){7GKC;rou*LDNU%3)i65w4P(_o(Gd^ zkZPc}EAKq99c76I+}>!7(ZsyP%6>+od!A0ARZJgZlh)p)lRKh@;2DDQ4(g6w_!X>9))BUAT?{o99mW0yE477vMg3lO$$4$W z`pdY;)e6a{!MSJJ*PZ)eLt|(`u%aS(`hx9FQD)LMb|JNIjTa8vV>n?%95JDXBo_@z zbOxUwz>4XnnwFn4`O?UVC^IsQ9EMfLW+Tha8AzCUZLm{jHr`B?_S4o&#yvW{%_!MZ zBhJ*tR)MSqx;E!5D_;Y`FUhg&V9&S1Uy26TzBpK%6VAN<;XkJaeR^f>dBIxV9DtR6)yQz*i3A&9#PE-{5OSlUV)H zs3Pk0iB%&nt~Rc9`A%Yi7lL4H&65D0wFgbC+46HbN&Q(1AH%@-qeGrcsHDfH{m|_2 zm|{C{TvBn>->2$U)QU54JEaObT@#lbVgKB^@D+p~?rTEJRLjJ!`u&?Fdj3jhm&8xR zO+cfU+z?)gzw0Hc8p{L2vBFnJ+g>v8^~=d zq7FO;jGRSGTe5qJU6F(Y<}{@~|MLd&lr`QH33|_1%lE6+Y9O0!Tc_l_o`kIT&#v-! z)z|Il_S~;hIABQnCCic)si>x6Y{T19oW5CHx*RwM4%Z##$?XYvLQh`h<3S(tkPy>n9O)Oa zr4OPOaOO-fTfU^dTc%Ap%zOT&X4~&MNqiM8LuE4M?8s2iZ<^ga{5h)pHM8kxwm7nF zP5PC!-7BQ0h)#rOlbwDA?V{}Gvy(?;R*gP=ZNST1PqgGBwv2Zldy8 zxTe5Qq{zRnKVba2OrH;i1v&a?;_<}6#RykFJkwAeecVMNQ)R{@fU>fbu7$zB?NG8Z zryH106sd^8-&tX&@1pzhV{^xMXFUXPwvt)>KuVW+k3Ff#g;kD9BW z-+EOPDmY_~Dk_|XL8Np0(b;u7iCiNrewvI~@0F_$H8=9l_w`jjH3&R3auSNe{3T#| z*gfOPDPKweigfOk@Ey9hSld=THOGX8VCKBK0~tYGp`;{q)FC-J0Sk6$ua{+w;ZQ@! zWRb%e!!+nu$K8i{+rggv5hDpqC|ws%7zw{}0FM*nxsy-!)$s8SJzSK@SmM%SA52k45 z0d((gh8pQOvM}`Qt;O9OYfcO2h^)bG3xd)S2FM_@MMB;l&c7u`6t9T5e~#<+<(&)_ zTr_4_j@Lx}X$UzC#MRU`Uu?$tiWMsXAdz?mpxXoNJDm+F#1*gvmh#=WkG?pU%B1}{ z>iI>6)kwx#w)YWRRx(RMS0I3Cg{MobQAFg4BU}qnVjB>fO!c=lq&&@{q19g{Yur4H z4A_;a4@W=OZSFI^6CjBs9gADkvm-28h5%vh%XuJ8F$QV#q4qkz|BEpHBfEHLYMLx(#ud-aoc4AQG|+l)DinhZw*^l1RM9<+|}aiR&oswyuCj z*4t$5u~C{3-^Oc7b^=GWvBDz6qEz3nCkAjxPU3Y;j3QZg zqn~=ONPk|&*`>5tv7q`EXDgkaNMN?5r(9T1V<%U07ylw!Jya#vT37sbA-J6sFEPJG z^A4g>n%Xcc@tmc`&OIzK_hdtDy&*2%l8s^TeGZpy+$?K^;$8~VsHiC}u%*7%B1X)% zJ~yT=9725|oO`e^j;|(1)}l6Hq+mKG3thp?sEBg0ipi#o;z3Q`nXMFhtT+iavS3_^ z1L$AZWUP{D(ICyUnPp1*3ZlKtphgu?kpGozJhpZI@d4qxH_~MBM0s5W1E@1gp)7Pw zA=Om>pLx{S4vxSWyB(x~HzqXdj2L6oXN51LgqBwC z$X!v}5ky6PK8bGlz)7r0Je*mU$;i3_+f2sdJWr+X3uEy#H7g+oW4dK)~E$sO*(fu;{awp+7 z#s2i#*>>MPJE8)o#TCufyD{_)P-J9NsvttWCbhFYH}z~oFSUU_i6&%X45vqP+rhtN zYxSN?_tEE#n20Y+AK9{|bD2#Fdm{}eh{n8n}e^1855bPJN^vWE=FHw$26 z!X*P%1!KudTAWc*>k_kEm42km!c?Zb!}XU>4`sj-6`FxB7tUux__Gz?peBXxwE6#8Fd5@MCZl;d;C@yXDr{vBwn}TsG7;5xMaf-Q#7ueWdHbF%d>1_N+|v`7Myi9+15)a( z8gP5`Mg5PRLdCqsRxr`&mM6}I*Bt(33=A#$@Myt^EXbF2MxzS)PL@yy7~}NLQkqro z-df5a917IV$Ugma5J z&Gxrk&wJ!7+VJd21s5x7mUyo2%d2+6&=94_?~rS&!JZy&kr5yVz*54^5E$Grz^rJa+XK@$D>4QMyh zP~?d`MK~%5t)rvpdfq9vSoL3aFP0*=*kAhnm3j#sq}dL5!_MF2xoPoA+=A6TX(xh1 z6%?CAJtzPe&Hv z1Wt^?CfJ$^-D6GuS$~Ja-EOOt1A)*7OhC)BCfR-Hc7dKH;T+n#fV_8Rz6@O~Uad)p za-WYsv=LL$h#Fpu|0Tr}6=qHpnkz?LA`eItf$564`rZbi41fXk5*V9 ztz}?!BBD}c>?gjciA}!$=6+TkJARz7>sxXb#q8JgU()*OCQPe8JcF+3Thkh7R+kiq z;uhTQ^HGG)H+5QTt%+r6bbr29Kgx*V05JyN+J;u^Gmr~&Bo*qB3=EzFYx4UeQnJ;i zyJs?KZ79!wr$hmx!3X#Q4$tCjoLnDL(ZKqz)(#!e|8_xGP`M%}xE_}tTM&3D0vXc| zV5}u0LoYU_-HFb(dSA$BP_#I*{KsN*F#+Q%x&x~DC;1|(f~)TYt`#`{lM7HL%QP^> z7fhx=%JH7I%Gu`acfV?iu*Tyzi#nRqA4adu1K-}(FjYbZPiu}xG+ZbB)Q;Bc&g|)m z#JQu6`>ge9{ae%Gj~w@E7GuJ5pU>41H1h@)dWud0?(xDo@eDRfFw-D*+)xL3m z8?C|0d^ErCJaTuF(Q;u96GWys)HCZfAeNFMcz^+?skZ#KSSjgG3gpUbl++G<&JdCE za`fK-Mdj7ZgaC9(8KlQ9qNlq}8o0!rMw=?~1Uwlh{!Pr@kD~pNq!U-icrN1uwUTC< zLMz3K=dh{YzY@ry6FV{Ipahe=q(tQ^G4iv#nY9UylY;fS677Cp@|Fk~@IIw62lc)5 zzXD8hsG!n%hy4@@B6@SUZ2bC8Y{)6nW;q|76NRVPCHUF#+kMG2c6hnItDRrR5~4{# zuF#mzUDK-sWo1ot^_L4uKxRW!j=M9O!WTpH0~0zU3uajwDa!!iGw#5#a(qk$y%F(& z^ zcz?gao;A)FSx9LDFQ%B@9eJ0bIWaLb%HJrkg@y|@`FbIpS>9(%T|v8ZHW61zzPlCf zAZ*|BvEs3V;N`{0spi`U2KtPLN5-2K|Bd5fN8~aQ2pE}|+%Jly-EK2u+;*aF;ptgf z_rmD(Q+VP`Q>8RS;9E3M)-r4rMeO z2s+`c=zVVF_sSItUbBBsP*z0>)60^ zJ`hW3RhDMNPR0VNz<#^y43{vJo32#6D)Tnn7kjzswbGQWHAlsfzKPuD@$-)&GGxAs ziYmtQJNSt`dWS|ZfW!AE*oFMOAODD<%AbWR$xiIy=iaHvr-{uQ?puJq`1;UwEp%YA zA)c+jo5t&``}Ouv8ee}AQK|G$=#fq1K6ouRpiRf zG;iL-0tuDJ$Pw@(w?99hM)>KDzw8GSZ+g@~e@uQ}-i-iaWYhVmUfe70y^^oDGi$fXWk1LG zf{CRt@?XNCnvat9Q#QPhc%SZkncPeMAn3@GyEg$UR2qlLVx+9{tG7PV62iJA(U?~& zz#8r1;LuB#{PJW|&eF%F+~rOjI%nn>T*BZJsmj&xfmsy}d82LHe;5!5Ihr_D&VtdhAEizhPojbiT z(m=_dLxf20ePj+4N~XmW0S1A@DlP#HyOETu>TEyZxu^j;`T>g{2{LsF|c8yxP@8 zmvVLL299Ez@MoQ2%><)teugX&9vzEQWeLZJA75;gtC^A%BoPSrE$BoXMi>IAhj}MJ zFcxpZ8C&EXv52i;mN4VV6kbxwGp^;KsPwTz5++0y4h@0)cVy#YS*cPH8=~8Zgr2hN zY6_J}ZEm1r%>Argg@Kz7{LAptbR?L1Up+t3HX)iX!wlBFI+(;B!6thoT48C1-`=ws zgFA%|t~z0@i>6eB(ObP&*DvQV55iQbCHhhF;+0Q0>6g-VwX^*^#49si z6$PD$s%F)ud-B-BXemTLr)F2+zhI|u9K0#0T#}A;6>1JzmPvsMjMPLe3KQY%d7C4? z9;qi!t>z{^qoxqX<%@^rd8pbxMuPP+6<2-OmNA?Ck6=GLgUO+84>MjZE4pB>B1IFZ zmXW*nIA9oI1#715Wlj4aY1e2I<1)W{H6>40)!c7t^C28{InRIi*!jErqnl2-wt_6I z>A#)@{I#lsyW^WXE0aEZ#(!gLN=h?2YU-;x80zqsDpeBA4CK6MO_9$Pb9CGk4Y8-MB<2N7PuMmX71t^M(EL_-Yfaw-n6G z8Hb0A)&LPJNr}T@bN221eF@9M^%+sCxt9(F_{>%c=W2iaw}S&+IPsoh9%g`x0V$bd zs^2xPEYHxIO?`i!hiT!Z8 z#DON=-$kjtp=i$PqP$ zR<{H0mt6pmmuQw+GDM8+alW~z{8U{fMd~3S=I$y49a1UqcwTr!46GF!*GX4-RAG(S zWm_9rIUf*Pj8v%Dsy36tsfOZZr8Fl6%m6PuOli*wT%l*t6!onxLTnX`LYM!=#shEO z#3Vbl%)HX|1@ZVW@fuzpjHmGFnl$b*(F$Lq5X(iT1O#6|R$`{zPhpYXw!9WRA@}ZI zbHxm%6LqbhLAJD`0`_$=hXliaDiutedxswXOq)n=_-8ScdxKT`74M?!xUK})LNyOf ziU#Ri#$3GwAMttdv@^(>hB|~?BZ&uo6~V14e8)Z37=pyC?}UKDQ8g>X!6DFns1=GJ z6ps_`#wOm~+>(b+)TQ{U@`bF$%JNpf5e;Mw9C39@-$ZQWJ?`=QA&P6K)ooX7;uqA~ zyE@bQnit38md=ZRwX+*6&Ea0Vn{i|?qC_h)@o}I(`*ri;qTY6Z*(L8!;;tqQt$51M?T4>}42V8#6mXG9M+XV67iX-E76ak)M}=Yp%a?Ni63|0*WE}p_W&})e*c* zOT_V>u11(Q2z!M*(Cxh=juZdwe5St{RKO=BZWu~%e_zX z?*03(4MO$39wT1ZbCbqM~2ksKmfPJnL8r$2A zL2$gNzQR;cdRejPc$A0{f)xN?6w_{0ohzoW;*x*F5+8cz6B$xy+q}=lcwGN#TFqxdso0jEFcbOEizo9{ zxGc;YPb;E?L;XT_R9JwrtU1T;5efBdgy|!P)_=lYNM0VEegsBdSOWs?3Oje?@1PF! z(myO?tt3n8hJEiAGbs3kkZ!5teeX7+02A~cF4oA4=5zVPpHNK~u+4u>%6o9qyS)X#&mu>%V6;Ywoy*;r}yC>YXSm9LqsG)gHl?Y+)U5S43bSqb< zCq#+~W|O@J9Ehh>{|tDm)SUyv=4DFlz1R^N5_^r&2Q9V5hh!2mp-g#d zJ9x{%6jb_Xxh_Rq#d(RfaJ>=XQF%ZBlH-1Z8M zzrX`^W4I2^nVNQQo*tf-Te-$V0TtF79p{(e4tRY#f$+1Fu`0UfVKrcxAgiCoqZJlb zEUGv>TEN;=44@$zoL$;HIXL$D>xJFu{R5Sc{Dr8k$1;;%hBRzx$IZ<{P}KWmvw}Ey zvN(*G2ez&L&?D_t&v{fgRST7E z$QS7@3t=rmj9gs%d@$F9QV*(x@-{Kuq91;$tg4h*_3;7ARq+xH;@Al*05c}su2K*3 zR0U`B_mAYE9roWuTY)GM7@Mo6uW#2qalF%^`t*uM;EUo`K5ulwb#J%3OnicbXC@~{ zEsozRT-p~qskcyKdAXC$o>O53F*YII`-?L$SC~6gS9j?|?o^9>`vjg8*_n~nX-B&q zcHzssRtV1>D;G9KmlhinlkYj@n1s)rYjs51h-+)K3$b{pgHp`@E_J^POGjQ2OiV89 zi`~JMSU8eCk4dp_wb8KF>L6aoYCDHM;?a&X7On-dad7hc+vz@Hc93#s35y2^gx0HV zbcd21pSc$2aJwML`J|0^i~LcNv$dY|-`7WayYMtx;wMBf<?!U}Md|L9Y9jiU97^ zN_X?I=nwV1OKy1!sSrn`*RII2l6w-v3gmS^n$g|w^*US7XN4Qn?f0Xbh1W~7Z~&)w zI=0Qp#DctiBh`@^q#Yc-^Oa=+_79h%CzE~{4Z=f@Yom~?OIj7|6?V&qY$q-UkGEL4 zdWOb>wn=tXScEgt{WtRUY>xqKNsyc(M93auUE7VWl(XF@1ji-34g|sLe1~mg-83@a z`A>xXIk?yW{24x4zhF^}U#Idim0FD!`h01yWd?<{?(jDoHSE>DvrH%6HF@y^yizcM zE$@F!7)pN>yXSVw6?Vtc^X3kMTI{-^r}ZXOAIm&LSZPPc`-x^leAmT-x4yy?IPGX8 z>F2;UO(AYOfGnlM@tv0q-G~$`xD$aMaT?{GXuD24M(*QAB0(W*?8$5|hjCd@KFErG zg3`p7;pXPkYqv%mQ8&(byBlMuoa-6DYwlbxt?gk&c3?&4a~L5;|OVzwo~!z1<{_@EtXh?*jl6oos;D)+-a*Df{#|xFOtRNc_{Gwt`JeG z&7GWOp=~(Oj`hO`iOkjUPLr=PQUFB5`q3NbCTrnc`-wZ5n=c+z(QGjWNda+vq{Fr~ zoEJV?6|y0<8gt6-F3T9KIsAO6$|1rp#yA|-Q~%_)8nz+L=WWkDwQErh-1yQ?6te9rYFdFIW7YC~DubE*k)-bB2L zdyN8OwU&IH-RW?%Yya6zb2E|&fwHwN)Gllf{rkH2DHUd3bd zk}N!0D*)N%6$I2bvAOf?Gq#)|ZbvArcwPbh{KoX-f%&H6;%IM<>Qhkai zJ0v@7b!OZvc$yhRTNrP5Rwx@U)p#$BwWT(cwR^d!c_>>O%Kq|4ER^e7m1;YD7!$@? z)}E>N{(NKHC^}QKQJ9H&TzJ*5q7OgvHwVQUy|_(D1?@F2+l}GOY`DZE)NV5A|=@?m~-E0s7HRK?7uM z5sGBK{u4QppfbV$&*>Ux(#vEU!wx0lipDJ|LAT61nldVd3W7c{&!8P6St{!hUsEXTgKhuU;D$!g#w zG>-d=mHXLiKj`E83_|mjAcsio%rsSzd&B@<_QN5Wk2g{A*P|Xz` zK0q#$ZdQeUX-*B1wqT_3tb)$@m&}n-y?Mt@wqUE%b|BN|Cr!Q1&l#AgRrF2h6;>Uf_Jr~`^b`gvgUn6Lm4K6QP zZ)UC?xH& zf*eO-)|{IoY!8}mZph7%W`5CY&MjK4j~#Wdt{h5v^BPYwfs!;SAHrHSa>L6dbp7y@ zlF!`D`#zYo{d8d@IU|(|++N95Yjz?47FV_T2C}uQu z$hrLE&Lz%uT?x7yv~xhdI5?e{Pn}^eYP9yF5I7+MQ+#q83BtYmQGJ&yX7vbo2S3WUbt9a5`fT%Azu0uIK@(4&G%S_(ha)#1mLonm&R@iWNog8hLA7 zw5@0qwnm>;pl6B68W6(KpOw#{Od)fmXBl=aL#TcxPSbQPn2Hax?&&Dt%&xJ9=U zyj2f>z$qOfQ5I@zW5>xD0SZp;peX^|AT(KjNm2d(Qw1v)2`8h5rP`sIL}K4jigl7e~SQrD63zf5RJxQ2@VF;Ixc_;Qk04(NH;oM+4YsjyF4s6(iDcvW-2L330(kosShZwH=jxiH!mwu z8wqZ}dQSO|5-dP?l3LX^?*3w9KK|QDs`Jz_n_FB`?6fl@1Eoo|<;7dpbG~sm|J>WT5$P~oIE#z;q z-gcyZ(iNG>jvHdf<3#Qq+Az|?->*HQ(~vP;+#+wIv6ITni}QTnah^ONW@xr3+n?%s zmX2D;={_lhi~pxw-l*w(5*_ze>ogM$!PXwDzDim%Uot_App8+UJGCLRM0e>rsKW^USF*4?;J&jpk ziM0F9VrKBIpe(T~Lo!CxJB_9kyg%8jCO=mk3!RqFo9@&eV)fP<{NjE^jE;~S-pm!U zJ1uq;9W2B!*M_l)`Xn|1c`kLoawID{s}Mu(?07o|Z=Qd$fYRw$JBD-kjH@ zRQ$I{9b2pOl06Lx1Gcas_`V4i$dc!so*bCEG30q1WS$Z7h zVPQxh#RV1WrGlEhr|?2*VpCUcPjlHN8}&y=yW7De5VC%~pcS45YS*j3Ozx3%iftzOoY=JOyfeQn|pm zS4MzfJ1FXA=UGVP{pq<9@d}U#ENVPj{VW_^Uunc zr9TGv^)qxFw(8hRKX4*==Wt$AT2S#jY=0cxlXR;oWT#mRDr`G0LM6HDsiZ1hj4uW<)f@AR|(cE>iFd9;37 zFa!`WWK8lQSlf=b+rgWiMj@wHl531Bz!4aTO$qM6f6jti%%G<6xjxaSQXelwUEgj6 zdPk-EJfndVlB-+}(uKKhaTvY6=)XHi{QLnK;WY|0Kwr?Ig?W+BzbjGdM_f>o73q;x z&I}VlHibWOrkwuZzLH{2kk#z<>NWW2*1cc6BU0k`F$z-d#_Ix%x~-MNJ`MJ)sRBs_ zu1MyL{hjHcls- zo=$KXplC!H!@EdXzmPeg?-vIoS7uoGKJMqOkAA`$k){gtgcV6h`n$6|9y3C2 zDeH&Cocj)eSaVheb`Pq0~IUJsl;bW0}gUK$2O1 zwzp0DzEf?hx8iw$&|1?Mln<{btBONQ zB3=T7<*V_eYirgR-E$|5^u8;2EXsMb3h0pNE}jhduDHj|=U5W}oW4=5Fr5RFj?T?9 z&Ne0{$|Wh+Y@q0rS`w1mT-k3A5{|x&rd`qNMP({MhO~F^X^$h-J`YR75^%ZF8>)Gf z7mM$1cXcj09zZa1$L%mWj@>(r4Cm{%$dg;E6E}#_m=mpI0(-zz1l`R~Z!2NJDw3&P z{oht%j!`mK(X--@IJb5g1DE7BeG48L^~lqz_0LliLw-B7(uIWt0J04-09ngIG<%xt zn;~YMtpf=w#rrG|Cb4EWiWN71O<^$Ktf58 z5UG)Fq@)K0=>};Sy1TnXLQ(}mx*1|%=nw`3q`RA;8Tvh3Ue|r!&;7jbTJQ76v)1n{ z9FTL^XYX%(zOlc1?}(orLWdVdA{jz~;n|Fw?ww<2E%Wa)y|#(fcqnV(EpNl1a~T8o z)^z*o%=GX2iT-MxvW1I*e#qO>qP6%@+b!5tWkC8Cq8MJ@H3oSmN%;-&QZ3f0;(5mC zzWEZqlQB+8Nk+;cu;TO|=G12rWrI#Ou`W{)}MQT(I%gZkb)Z7DS z+jU9~ZI5`BJCh<_(;3s6MJZwprMNFO_D!3q+#3ed+(Z*bXgexJzvk9f_inGic&{@) z=Rd{``eCp#gs8e4`(UboY)i>u7-iQ5yLW$~-ifj*Y>nCozZy=}C>P@U=m>$O=2vG` z*hvwKDWoq!=4S=j%-&8&!+0*3ctWHdIuzAGus}sBYnJI;O@02AXw3MccwBJ!+vyc$QH)4 zve7R$%L!x&J&Jd!-pc!5V;2{e6K) zco1W3r+g$Hv+~n){R>OjZzf|L9T@jM?(%m$&GK%SuVlY($^kLs zl5K^|dCrN~La-7=FkB-rl=kIDi}X4+7cIpRiQChq>}p0L#?q&4=6b!J142KIm4w=F z|1CL^l()&{*)7Tpod^T#t-K;O7LBLG`+@`5SnjemE@jT^_Kt&S6@6%qHTgFjXVivZr3z*h2GBIN)6a;~`f= zX6QyS(>L?um$zL?b>2El86mkv`d#BR;Benb5Ph&%&ex=|#}w`6Z?ApL`TSR0oCwS9NEi#wXqP+?5i)W%pvzuu*isj7N zEA-h2KYIY#?4y|Z)Kj@ehOhOsYN}5^JR}d|tW8*~SVyRRHU=A7 z*Gru>Jlt=XS$m1Rp&TeZG9pR;*@NM64$I6sU>7x(aJ!$U<3{VAsp?gc8z0kIz0#Cv z;q7YFu^2qCKXyjGx8fUJ2&Rt0Ahie#NigFnKSxNr`Ly_=ypGwPPA%_44T20^5j`$l zt=u-gL}bxc8dXN5@lhK(c0Xl4YE!!^3B__^N{1EccdrpI1ny?VbY>xDnC(U#1=h2G z8MksUFt)T z4$JVtU_RzEjUMPILXjdbFXmXR8n^l?iraXvmBAv$m|d}6soXQ+g0`6IvndiPA5LX0 zMe&&Rjr%{oOdVr23_Nwqt!4e$g!=T61jF2DxZzO4WAY*2wIF<@WN_=p09o(veGH3C zS7vDAtXN>Y>E>o?c$84gaA}i&w`Z??bi=38XravM9N&X?h}Wjk-Oc7_9|dgV0`+Ad z$3JIMZv3c}qz1BIWzunVZ0`p2SZ`w^xzvYQ+jFOg_l^zQ$p{dZy#+NUgTx z5U;`)v(P^xbko&^s`Ws!?*;iRJrVpB;=AIDGMI39>Y{EXa}>|JM8`(O6a8*Oe^1j< z*qLNGw!x1%b$cPx%tn)0nr8SnRa6iBs#HjD{{E2E0ZYE!h6OdsGd+*z` zh)2p{Iz4iEG`ggrvO(*6AX6c?$^o@^RhB~2Hzxj0uJ@s3T;HLi&(^NL0L!he9LF}q z7Z_U+quV|iTip&@NGV%KnrLuK4Ns+$lm1=n2_n9$qsk|VtEShT_uZEcWn1M{9lKbo zs!%ELd2SZwb6bLmLiZVT5^JFh+y3)=r+jn9?!~evwVWwQL*1LcjXjfWY9#knS&avu zvUBF@b(K=RSUyxVJTQGcPv`OWRVuXHCoL2wPp|(Jf-p>Ej4aQ?MRR#`QMt)Y@;IxT zSLx#3DT7a6hz!*dgHjH1l@W!UgNNjo z)MaGFFg@owVlk~{#eWZ{soIjbKHewpku^;B^KfV`xb);my>x!PN zbI#3R!mvO6^ZwazXuJZ^ieg6ot5Nq5XwdgawMVGY$0cy&t4=niPR`2@Z^nzwxjFTv z2W*W7$sns(5tx8Qs6X5{T5@ObMY9+e|NvH>ic7QKzGxpJ&fA*-FWq zQP@}hxmUb&fXN(M(sChrr)&$NLnvu(A=KXdNXP3S26>^?6hvE>;Q6zr5e!SmKY3}i zD~LR_ul8zMpT$vLQcse~0bE?0bF^pVT#DVwsWC?XI7C59v z*(=mQITvCMVKUcsb#~FHZigDC}B!-gm~YmBG}?);o#cqAPwzPOQEH6-H`IrQ%zoeomB? zJGa(cWV7)IygkwPF|OVeb}-G?#Y)!olF@aR#_V)lekqdBwlt``HCOitXH z-xH~d_qJhOO7GknXF+WEGdB+rGFA2cj!2)B&|`#m5TmGuqDuc`Qw3(a$`hPfw_Sb` zb)@ITYeG%qF3$G7GLs!sp^L|bue<~`_20)N3IKk*cbS~%@=Pr6uTPx8nnWQV!NTMu zO&voINZZW%4BqEv?ch)?^aPWeKjF)GGA#X3Lr+{dE+z4ea!v&CVdhH4G50uDtl7^` z{u)+m`$vvAO%4RV_H33C#^y}ZMzV(4`9lZ8b@_d;exTMZu50WtRAao}C}nlnzY)&( zd^g)s$XatxJ=4n;uVr7|aTssRPHywfY}yqKd-2XR`Oe|lCm4+bnrYk9Yh)Ak-h9Au zN;cbkfTNO`&IhK_n+=j(ZyS}o+#5?EQSl9aOeV{@-fP{O}@`(SEYtdap99nZhWQjvtE3TohU$t5Oc`Y>pK0nb2lIQ1)PQW zRjCiAvt##WO9XgePSg)!=lA@He45#PP9)f5v^xsn@Ic0-lcP^ucU;&|)Yqs^ITt>h z&rYlE3a>0r+}Q;@s4Pi^`!Zr1TS%jpmn*@>MUFVseNC~9Nt7r`8(Wqc3Kp}CVfe}! zabggb!dg(e90|OkX)Eh?p-nslB=p_72p3&&qRdcztFD=$rj$Lm6U}ym;iosl!ghF{ z02{_s|M#Nt5gy}8izTz7gQvKe92o?=yGOJM<$1fFB?m@qf;JTHJUDIYt*PHlUi%@k zSj|qYF?OP@c&n?X$n=HR(xw)c&1*L4VJD^R7JUMdg={Z^h<#UPyXFkPI|BOS{FLZY z%KG5xxXNL-LX-g)i`li6Z8_8X?jAD=B_DBlLF0u|wc09G_SMe~kVSuW_!pmMYz4|C z)3wmbxq@WzKVzLVs>RCpE@JI4S}ENhs3IO-9e9|`Q+woXlp4JmaXh@WNR z=MPRTC~z6;>FZ~Bo_%$2L^zioJU4G9%9pSl>(kp<`uKGI-WkfcuHs25=~pK!yk|7d zlQx%^X+$8A-%Wx^BO@c#o<$=mCmQqL=7=Bek8`^hUAB!(ABM+ZW*E|+7$1GP-$9J)V*x}3x~Hu$^cP>zky#mPmX(75a8HJPp3Y!Wv;wtN2-@|E!?m7vc&I0-8HQzwy`IHO z^G0}E69N?+)#bB(tc@fpwHLeWBq_D`H`Ajt_p5ERzL)TsR=>ao-jY|L3=Iip~H~UXX+ik6Z&lfebFZ7lIP~nRIk}Zevs}*1B)gFG2bvZSC;X)B+5V%*4-9#>Q=#E9doE z(-2-Zb#x!!-uTT-;Gu3q?(Hi7qpUay$M3PuhrCOLr(N~(fX^mP|1Tww>EojSv(S1b zNi2ZS*plV{>KY+wr;bMRIP(K+Pl@H2ApfpYlUbXa7-uLT)G>1ZQiJ#u?7NraeT{jH z+b8Srt80w8#C4a3ss1esO#`yf>e!i)JSEej2D@=!-dxoI4qC%Ogfol4D9m4C|!~IlVR{F8!0=BAZ-s0K-|Xom;WG)OkYr|A(hYasn^A1+F1wX>D#%t zUAnGwR9le0{x~I~dIaXQB=`Xr+@Z4ad>*^=4|`bu#hz}0!*yfd^Pk4dYAj2k%#a8% z+go6XNw2qwF5d-~lXSmR`U|7~1A)S1{_g|Y|2Rna2X5(B|9^JP6X0*7zJD*`ySD?M zC~F$2C3A|drc3vf0>~OB=)mDkSt1-;Y+`1}X8VY+$ ziGbO&HzX-uI~0OzS(avKhi@7B=PPWT3{(1O6nijepek+Alo>$~30sLh{yqls_J9jKxOn}ie^ z|7AeK@#sM^&}9J~ozzT(ySD?`hTP`<&fEn2i{gQ}*$RKd^;3d)W2u`aO!Z8wHJHlI zWBhjn374sV=11wRMpaU4v~87L{t*2+4{OyTdS-Y=)N0K1Pf z-t@3R^sip^A^%CK_Br^*xiZdU07 z#~meaT@OUipv(1zM;EZhPMej44coQQn;dc~K)c#jK|En0}rt;k$`<93ke z-GFe>vXq}FU!?b{rsw-u+4GVwzOR+@LbJ$gG21@0O$5Mil5rBvGw7bbIMNBm@>f1r70;mDAc(%sX5avnn zyZzWY0g^}=uks0^?iYOJ(olZ$b3B`ud{w_~%sL$BXy@}M4mCQG9m(u>XO7bBIx@Q) z9t;?$7zsSn_l`XQwhx(n>N5HIT&eqO-B4HkS=#=H)dOk*eh?))KvN^7xvs`!hUO%; zDJt`Y&zqDw)$a6CteHFmnxS@c6L(!7%>C?}s-)QLtU2G|43=z;c;PN4nKKgeAks_^ z?@%9lHeTfnU$S~Qunnqc{xG`l=}5OlP76bMd@&;@AmIO5^&4q$K{~L%B3jy^tvY=o zTeyT#{G2JU$S|+Uy~uiho2Lh-!RC5CRx;q?ednv4Y(ycMRW-jnEk?(9LH&m#ynwgrRTUw?j2>tM}1Yr21(XE>`x_Mzk4 z<>iZ`z3tafy|rf0iC&TCiN0aLLYv^d_%In3Q~{5QCM>Q14>_N&;k!p19TV%Hf7Mx? z-}GqTGhajS#&0VGL$81bCcQfzdD0|eD(cCzu!U zq4)_2_<2lXVZ?x|EUGQ4dwG)I%u2hj6GlF9ABBOyfkpf z?x`tB6d7==$q@O*GCuU&8vvc?&8M{KCyGb{Td1#oprg{HAZ!l{H}g_dwT&PiMPzL; ze>jnRG=8Fd9s{Y|E@Xa2DBvbul2>n&7`i+?h^46_Dd{0DpPh9jpL~|a-A^1}`hW{- z@UM>5{N1rv>M3#U&`PXrO}*ivKF$$Al+W%LHk-VxMjLbq8&LNc=Y_qY2Y#Rq+xd9L z_Wi22tEZK@-^244ex=`7wV*97sLk*ZCAb zyjt#MMtjyTX_}kksQCsTnq+NImshSbMZE+z-7O18yUh1uj#am@J@m?*X(4Z8j9;-M z>|C5g6rx}WQzQi8Sl&Ym6_pv={npWvEZ670sr}NDIh+a&l?aDl+KzGD*_Q}anG50q}M_BC9kmi=CisvQnRBjaB25i2Fe`boRmhIjjqKrDa#TNe5> zTwll7K$mfIK+oI<4xST#w?1|)U3Q0%Dow7A>%!iGVm>o%%vjp$nD&2;%kHovMl%NH*q64rR)00#XboU*TTC5@wm7 zKMzcu+|+QD$dPcKdg^?jA}JvC7cIw!PwkO8oH~S@fx6*dwpkXmJSbryRn^g*o#RSq zar2|jMHMAs$E}y{^_GIRqv|RoN7b5LT(~|Hwl8uGof>rgF0i5v6pS9AmGZtnutvm4 zOlF43*e-#C3fV2vWOy#<6PE!U_y$OQqlT6iCMP(h=MY#gtWV`0@hlL>Xof!gdG5|H zpTj@#U3Yvo6CE7`?)+D=V4K_+lx7cRtJ>+B>x1nq00e5-ly0^O=M+6&2cJcTzYG1GlsCH=S6N$33Zn^+bUE>IzI-iW< zWCIMM5)_+k*>4FLx{dT`^09@7sI&Rg(>+9C=~_(I4C2(s4kJFSlgMFx=$uhSee&@Z z;c z3Yl1gGhES-*bL%kO3XT<#C`1yYL3<(BNPgv!zGAs+c;(Hg{B4AU#rBi1Hu|1W>~v9|5w|B8qBs>Wed?0U$rx`jJTI}D&&MRL8F z9U5V4KTdyb$HP+=+hb4noNvovCky#MIjAW390nYG)z_3hZ1XgKZeEh;G-#s<>QI~- zRVEq|5}%PN%j_zqSkNynD32e`oLjkBur|3?(@cYv_STLXj%e-F#6 zeRP*DU)FR$Z~oC8)bN*hKJB%&YCq~^2%S?0utgEK8>aj##NE(T3#o` zuc6GEq`=-Ov30DUV$pskngqB>$qz!?6w*P#^6X9MdBYvDmEaCTXr;+kl7_3|-(F2j z89p;8>?o(y?8G?06Zx9}b8m7lJi}wh9=qppblSbw_ag@jNt_#~RWDTKKze2(g?j#s^a6QfHoj|?%b08KYLzB_^^4DH9#FW**$dZM`-gL|RS!A2n*F@c8^5_Z z!auReZTLtq>VqCwJcEu$tIN}#=%!JVwx(bZW`)$X9BH8;*A~Lk74027fh>fXlym+* zOEA7*U`M<3F?eQXqY)vkQUb&#EE9MQmMq&mfi=!1^~fZvNqqWM7;&rTo(?EJ1@!3? zbK`6Is~DbMd{>W#t%i>klPni$F2`GG#bjzId$ zd!MQ)(^C4f&$3TgY%TU3z=IWmC8BolkFcpgR!XpO`AubSV>An-VCkan?88*XWyQ@~ zA3e3-E5Jf}3a0RIOk!06YvH3~pP~2U*&(})APW38BKG|A0199!PS6pi;>y+}OGD*@ zvWAcM&mN|KN}KfcN;9!VthSBv0t#6|_Fq2Mh4IXHDx!gMS+8Gaz?m*NW)$*s`O?Nk zlnT$jKENHS)GH~RcU1Du8*Ky$hxT$foU$aNye~b>UZKTbRbY$Q6m%IOxgOrbh8$#s zf4YnBMZPC>VNGgK2w$2CKFts}(r)fm)E-?dZ%vO+k~I|PNH$yMfH!?qN);f@3hRzv zwq~VJ(HwSEk(oZgGfP`J6~%s6qjFfuDtL zM8Xts$ski^q1Csg4;imkThTl9-pP!U)4I~(3N^7GLM~j38b+Qk^7Dj0JdcV9YcCy! zWc`Hu-nbVv*!S@zHL1joYN+~)#r&YWCUJ3cpc=;B{ms*>sD}E|b4M}$5xEy4N={++ za2$yhGcPzb`s*=7u(-{&63gz5Gf4wK9 zb@(I)=Gb#%wIu(-Xd3oZ-<-qPVG8dG=~&Q2Bh5JM8M^gshbuwpDmXAgtdG1RSgj=e%j>A#1(&!<-tf+d zK&8V8?RH0NLh7R@#025`SDmKLu#ZP6#v6ji=F!zx4{(L;gC&;lyPKQMI3m(9t5_R(?X+ay`o(KEoNCvjwwd4E7 zF0__YBN4_*PoP$W)H)8XG@D2PO@@q_EgU{^)22mfv4C)=!!dX7;<$d34cICyy>ie^ zaTQUk&=3;zR6BCCNSz1g9CO|Q=M9=Rc4Z>lJFS5vV`E#Auc%~a+y+^(EHAb<=Btg= zwQ(T4(b|-g-4G1fezz8v!e5Kns$uu^mw9oras_~v*3+wPQS3Z8zX)N*(m&D=b`H6wo6@Cm`L|N|DLlA&Gamv`R%HRFIoDD#MBN`vAZu9rmGY%8>V%V z;(N(c<-{dpIAp^y_4K(~zLfXS3fmwZ&#v$Yi4+3F?g!(ci$%U2rzON}Lu;T+cfn}D z+Xl-xwf^$Afo7#no_uqV4#DGb-vg6&&=ZJ@MH-CLJm$a-W-V*p1=)=QsDmNB1#gQt zC&$OqanHU_`{{{hZPQ&WvdfY=xz*}3V{$F`NVTVG2ahFD|vdD52mmL|+T=k1+uqc_y^sN~G* z1=BH_D=;_8Oqlt0snmrNtjz>6)31Hm3idj-cH4qj@?5XoKLG<{@+P(_bl>Q7?Iuf) z$onbXOmxuz^^gb4=pORJdSIH9` zhZg;Yown+@)?qXt!qncb2c@$F)`#>LH^9s89pjPkd*i;eYJlSL*B|$W3{eW7&K95t z9`RIblawFjN0RaFcmR2gxD*k;Adxd5P|maOdij)DO{MW#IH0Ls!&McSfAQO_;qI)* z303?vl)>!YvSG|2UvYdyqR1#>E~@l;UIp%Kq6?Vv30lY0YeiK?g5c7JZKNq)Yq^x7YZu@rf6WC zQ=S|XxeX(Lx-N<-6Qb&~pufJR3Peu#^(#Fe7L&L=B|GP={@3yyS{KZHy|7zLcoBx_%|>uvz!!U@RZBl z1Ckc9v0fK_n>qz9r`FtuKg&*-L^NS#xW10_abveFx$cY(D-O+A0@`Q)jK1d!{VzxQ zvs3lCj-Y^ag@n}eU8uUsL+{GO=0~v(mAD049yMBFhL#G)^v;#732z)j--{(2_j9|i?m1?##1LmxaK~* z)$1C;0FcWx*56PA0Q`mZ%X?0xyyEiB?S@bpFnZhiv{joSPAqBbGZ`7bcFDk@3<93y zZN)cy8aEhXII&&mZ8Yp-IZkT}@&-SOp=A8iow6!6ZbFY9kk=Bi5)$H5n1OG32hd!F z+HmqNmc$`~=R(}0bS7(w;2r~&?EnKP#k4^}@9#OPwp@tzlA$KhaXP&Y6Lg7~whlg5 zjtw5cDGXM(=LCcbL`1a=g`)X9&8_ITxNS5aH1E<9h)dqrBJmHqhslRW)L=Mo zeg$mb9Teyo0iw8Bykim(ceb5YI4KpApXVD3XmZ>5N zD!Sh?dIQ7`no~o#BotYnz#kFKCMkS)36Ds)zdlMbS%{Q0FKVMGvsLy>wPJf0F5Bl$c(Qf z1?XZij*R8Z>t^4fpX=@7PRT7}j=FO3(hR*!e29lZVHzX^G}e?&?#og+jy`*eSiD{c zG1bQ4nY(ESnnxA)?|t*lPBM{2p>PS5AyZaSxIXrY^SmKs?Ktfc)cJ7i zzq+~}>{{sDFP3(PN04vZfD^BkAkuED+9B=96+1=9Yckqrk@qnrSwPuc1PyJi7QAIF zfn_EH;eO>8)jWX)-L#K`4BF9|J-*I?Sgd(4LT6eHd9g#Y=P1?g_KosfbfhYuNk{j!eFG}_z!_I7~h zmwZ>%A59thFZi7G9iL=M;fYJFNT;${lR4H0vP|sW>PNsDIrvL+z1(!+oro3wJxo zzm3Z-l_5L?4;QDm0)!h#(o6dYnnC`>Qie-ww8Z2Xm73IlcupEYK&J7zKIE-SiP z&=+DG&ZuC2|2yN+>%lI*bmEWf(lFJwI?USwA`JWr4C3sTn_aDHGzhe`j4jJ)Uvmx8 zv~bCQ6bW&O@a?V`1H|O5zO|Af@VpjmEM2=8$uJpnKNvi(mypmv(Hf?Qi1{LnpLwKTsDE!pN||p#bTplIgCndxB}$Op2}Yk$FEY1X)Njxk>Q!Ea?}{cP z=)P7{qS^9^``yR;N<%UT4-6Ygy>G0EVu?Jri`%&)I`S0___~(4n`g1eqd1*QnLKfw zF*_@hqI*Xa$=k^jFR^Ss%06Y#KlM!S#&wL{DU)ZWvrNqM2EX#6**o^$czK~K9pj8k zvEzUf9=~Q>`d#QXLk&iiiB%g*!$mjV51wSL%M<_mGQV>5$Al*#-IUlEvB+N7)V25cWO$XN4E4v~#yb@G*O6f|$nVIA*6 zuKa0XtdNMcqKZw-Aj z&G*6Ry0%OAA875lNT?Py3@**u7VO6F961Y=YcdRf9JV84kj)LB0X<2Llc6DeCeB6B zDaIw%W|7JVvj2>EGCCj&B7Ei-9C){14$*2FP+469=o2?%ANDScddJPK(wFO<0=l0i-r#+R$9F?@pSb5%0HN0xjft%{SNZDbRf* zLqpK`=jMljK1m)=-KxHQIxkxlCYn%()bvzv&omh5shGWT{Mm$!i&>2)JW|xpCQGNRpW1m7#*)yT2*x^0ynnpGvoDCn&R%CTSeJ zR#vrSH-)AhTkV*0&JLu|)NQXzeoShRZ?UycUn8mgvPogdjc1Oe@<5F4Mr$Y(ZwwU* zZU;-zr1~9b>B?hSxnx#yFSLY+pgOxLHD3nQDv6ys&uuJ@N=Yg82BY5YCUh zYnY0)Mi~vs?@Che!^UBiyHW$Y;dqY+9?;4lhvZ*-Bp$_8*sPv*h1|!|D_KCfjG70-SGSa1T$e zcX2JfpQ*|sg}}18TZX$E5t%h!s3NQR1Nw>|JL1TPy~HH<$uf0^iHIWipGo5&RkigM z-@J_D-TZ6zVT$4_ib9kWFb&diI2ZBcIjOW@&=0}!{VDY#s@^tXqzL>ukJ%?aHLk3Z zO9;xETt8(I^Ywh$8cDv(9qS5M#|O$KwjzbRP^aXypaXh#z}BB=Dld0dbqZCC&>0lg z$$n$z9HBV{d4@Y2vJ9z6kaBSj^VuA|jTy z$bMcw%_U!^M0}R43c4-m36s&VVzmAGE)Vy`jq*}`zV;r_`6rOrQJ)yqOIlh&_z{%0 z=?V$-nuS_E!!=YoYc-W}K2V=WN|2DB5N`-K$&9iQ&Yb)7 zPwh?XWF2iW7z~Z_y$t zFh%nnPP}h3w?wMc#gS|;`_YQuw-=?4xIw?s0CMn4+cOZfIZ;%X|6L>kxam3P>o0S{-iHs= z*pa`aB-Fiq%w5{0oD)iOgt!XX*yqIvythmeloa2sz}ekX`HfQf_cHM~my9}w1guwv zyGOV+H^+|ab`X-$l=?`{Ldm+0tkhJ}-_z5UmU>^N7vCu&a6V}Z738~%ArQs6tM|I- zpdT+QvYTQ93-Y^I@ec&Hjj6Eeq{IPM%Os*$$)Ui(67xj~l(0(;^ttQhwQGW=D=PA{ zl+gYi#Rm^;0>m(;YFf}jE#{s4hJiGg%pTcq@LkZjis=$-CDXBZvn^2-E;@<96j_LW z+Z`}gt!|v!fWhdu=Nh8uFIZlorW&a(MbgGPJLl?EdMjD355>_%^nZL_IDN@Gx43%a z$T|NIJk7Ru&qhuj_ovtV@x2u={+9}>wbUi&hxkoW?mdF$v{ znD6fI5Gl>g&Hfg${{tWmH!l<*-3zGOdzHh0rwM6*jONIB)kI5kg5DQ);t@^MWOGd# zzSlHO6&uBjR8xE?L@iLo;|__+Qk*h!HTC1|Ja;p;uFB@~8fe@VJWBAv%0)m^cy9LU z-w8rrZOo0SA_%Zwo4#bxc>3WtR{0=B&XYe{07wEL`6voji z)BE&x+KfgqnHx#HvGi)n#2YreU`y+(x>_ntG@rK@dnfw|ohbY*BvYzW?5W*yH@5Ggc)zDbN%bZ!M3IkX)2(29B!TcW)6^ zRl6%HDvXIk!`iX4Y?gSO%S#2mkKnx#40;J@e|LXnxjzg&4buWHJW7M0 z;SKq#)my}QUm4{TxJ{PoeMh{P`tLaW7XP;+;iXp*GVkxPkVC&dAuCYHc$~oAMYre> zFoh?EQ$(Y|lY*IoZ%6iaHSjg)sFt#-1#OtiPQXS7jFE>CkmOHXRbDO;%THrYj`AXD znV;ipP@YMsJ|ySfMX4T zO?TWgs{@r;e6l#`>}q<^m`bafpl{$foXA#E|EtG}`uxd-D$V4x4*FQ~PN9s~8IJ=6 zdg3z31uNOn_!Ok8Bmgntb6r{SU4=Qa=cpA+uJ&ah3XSzTH|+NI$Jrmc?Jm+sx-FL* zUd$waR1R%{;(nDrO&HH;jJ)wev2mIyVu)Aj&x*LcX!JPAE&CBy3jf?pN+cr4;NB9y zR#VDI?VIG`yv4b^tL>K>KNX6MWu*TO?%2WBk6DW5m6qm)2Cl4t=9_W;sRcMrG9<$c zgpl>|o1K3DQWjTkJZHRF)q`bC&32+8WUAVO_oZhaQM=xHhpXGVJzHs7-`-bj5kG0kS8Q|n*ExX~r5I+5im1WTJ z(*!HZeyZ5@orXs97wzlTo<{IWlP8tY47=v`WOYI$TWwHhbgbEUpoEN4ufcfZ!*Wx^ zOSNJ>0@Au2XF24sg`<(6%g&^xy*_Nslnk@liji}E$dOqDG|jA47u>t`@lNt^ z<@gns%}{zn2(EEk=dtx9?AhYD@o|b&>Xb>cUJv~hyV+Jvv71my*kQ~u408rV=GU;> z6s%UP#p%=y8ZEsEuHI7Xx_TAabxvjh@$69cE{cH}mGe-&gu->4S!HEqr(b<|L@DTE zx*m6-S!PG2hrH%|+w~VG-3nvlSN|~BsQgwAfIqh>Dw#H_7OAlr&dkdsYl%pdw}_kA zD_e}{IM<+b?=brN{u{Zh-;sZr5$dDX8j6x)K~@qLobA&}hhnMGH<84eEf`cdr}Q|Bl!9G~5HtuhGz|ia2Gpdr#@EUt=5$(FW%$L*mjd$`=3#n}g#*ip#k;0` zDL>iaMJvEr8JefPT9fJHCw{Qj4ywW~mZ9m~Yp%~$B@OAwrVHeqrLMzQj&(Y))9W;k+^r#LHA(TdEOQ0W;LC2p~RYxjJ>>^HPnX=+K`#%L!n^X;kb)ofTGs4=M(a zd~~pItp{tF;ouvZ`$@B^9UDollDHx5D4NXDZPs zjU9Eu$NMY=27I5x{ayWoT z@7~7g+c$in$pU_++=I!p*99=CFfJXs-$~(g#iXaFM~foAdG+YL>;zDCWoD*ulU`Dc zw@`@i0nWk8P9_$XFObG_Y1S-nRcQ#?*yW0cDdKZ~Ookxn#KJ8WpypPdQ7Yy@tlQ^Y z#Z*p<11M}CIr#d>0Vi2qZPc=#e@)1=KNH-B8Ex+1MKoHh@A+D9!6gwKw-mm zzK%LZMzm|`BgtP+r`7D|rrj$~J;4_z8D#r?k!Y~f7VXE6AEV8?)Rh;{UB_S+(?F6N zCJ*^iHoJFFOKEnUPs|**Y{rJNFJv!l$dPr4pOVU)CX70LjzoHTdM=OsyUtdUoiF)J zjD7?38*8U-T3C!^Tlr3^hcOR`CUCPFYnd1@4#KAKt0Z8+7Bu3v&HEe1a@by{M=(LH zT3dtQ@wWgz5`iWbCOJap4mE4Muo-J05@T#L8jT(+cm9wUYG<_XRb;8R|DL+= zqju3q*Jd{}iPhf(Y{Mp17y8}xQ!;gIA%;Lv6~Q_R7z`(3WyykeEIP8(2yIErq-n3n zsPjL#Ob4f_Ns1niy**wo{`vgj8Hf_@?X~D60F--N2>B12(-$ z2qD2mSssYiK*fqY>O2iQ-QI(m#^V-!SH)c8GOn0iXPt@KvfMvGbUhu;Exhr`p6hIv zd-TyFSkv7^Og(jdcMx=pjGUVzzN*veL@}DMk$o<(G17hxV?TrZ zb!lcJfBqN~F*GtF!tN8iG)(Ol3TMtGn@|oEdIT#TP7(4Xc40cVATCAZTo$y%(IIG9gj0=chsO^ z&Ucf&!^L2%D1yIWuQj~mu>7TahxT8tM-kvss2ANrn=#PFt8|&Ah>>%R&zoUPB!>Y7 z9mJ8xuaxT_Fad5xTc^zrr!W~rLI%#~{LZggFR;v0ARkp8HOR}5*-tjl@6in}FOyri zpLlbLE`d`sMN(G1Lw;?hUs!n0W>?i2@Y!65Dus$RO6D=^e_vY3HuvqpscJ-Nf`xAE zwT`{xNW6D*CoG_YM^!CVd{!@t&qbQU*F@h;MP`1T`Of}p|KOcd{eh~$<%Y>59tz8b z4MkpjAuLzuxSQvm)nM*2X^@&>kHtFBl&t1Wz1S`)A~h9x1HS0Jmb!hP1yXF#=QA+1 zR9|iwR`FAtNeVF_&^&QzrGUTkRT!>r$>u>7hA8_-d{$!U0aNn z>Ua6|_D5b!qosoxd3AhoqRFXar*;<7D+f$E3QXQ1SatNa#!9n2p_wVUfjK#V)vIUK z)GU11&HSJGVgt7BZMK3em&Q)Z8ybo!EUW7Ng&lN%e8#@N+!9_tzXJ)c@>{3v6dhT8Gb1gx64+l6hz8GDTg*cVT{)C@QZoHp!iMSTHhGbXfo#cd zYrUT&&0*aIDUjmlVJVR24o6$mztUyHZNsin{I$`7dtRm+&WG+(^yQ)8dRu3wlN*R) zWr}~A49tr^d@m&20;>#Nghd_Q1r%@aEB5(3MsKW*fcSu8P>my>#8hf-WNzMEXHAyq zv$twwJIV5r%ReTn+%b$reXVXdUu1yV zEeb<5EAxggmWRnW&nZu~nLf;3@_W5n82+(J!dCiF#4GgHi%ZT@x@$1Hy`k{=yzP#A zaHOzWtY%JKFcDx-bH!YL(NsP|HXYk{)ROZSuf;s$?fA=`M0tu*8{ayTyndcYpSVw- z>REL7Vq-A7EwVd3%VVM2?s|Xp%R`+!t-s6vW z6^E?TY(!8ZmZ{DfGzM_&VeT!%;#!)2Q4#_q1lfTE zmn67ba0?_raCZ%EgS!j?5=d}&cXtLEV36SM?(WXuoW*|kKJVWD`{_RCxzD-%0hnQ} zUcI`itE+xhUFH2Pc=YR%o%g8Gs#E-H9Cg04hEQF5IM@G}qvXbE=Aq?&0+2YaU zklUy{c1s_#SUvY{W43{Uu=vBm{xBEiiu(pyJ)en+#*ezPd@4lf-$p(Rid^!37V4ep zk&BzMzjnoWyLX>z{C(7oCofu$&AI$Xr%~7v%Mtk$c`bv?;qqIy`RB9EN4a2`GQE92 zxdsx<^<$D+01=}(or$_e>uLA{O@pg{NC2A;nItrL^aDcTA-E!Aq~VWz7j5Cz=qQQi z8mgJE-<37WqO-=}Rw+LraY0imD_yn`Eo-S-9!s=H}$Cy;eS zyfM*G^AIy)ObH~O72a&bnDBw4TZ&B`vaw|F)G#Z)Po2ncqxDjol8gx!^s)Ipf3eE% zS-g--kKSDwiFREDn=L9)dG!{Kh!IT@UbMd-{VM<6e-sq|A7TKi^j=I^=80hrG1siL z?}x}FAK$^UFjc+qa00MjQs0NJ?!tzacO)OIT?wd?TtuTGEWwu`z2{Nw7g2lJSDNBI zT<>PS{)zKE{`i9a*IQkJG$p?PHm8v*5=VSKW!Zz&3w$%=I#(2Hy)LFIvoIX+8~|I- z-I}Aarf?Arhb)!VT8vZ4;moozM{R&w;^q9UCcVCYt+(OgkCVZ~aAsg8ni3)v(xp*J zLjxofUe3bI zOhlQp+-g(LOq}jXfPo2F?>ve9dL>!zBLGOeSx8xV3o;_&sDmEYT%3%vYr`)+uJWfw z=^#X%y>FPk7Jmiw^uLGP-%Jpv>hxz8PL}X-H zlkX4No2eg?ZNKtprP~iBKf{~yR`9u3@L7kkL5P1t=*vLN&CI=nLZd&nPxN@Og*VfQ z$+zg<;T>V2#e79U)Kgc3{CHACEXdU}{nw%3uESq{f~&WXYzlHg zYj!U&1bl^^NIr0Naok80T1P~B9U=cr=rgO&Aa4C?J6;e9D`Ey%cAcr)sGl=blsAZA zaJxkMMzE1+pP4)WU?u%pEVa{W@Zz7aDgd$@D*{TJ(yv8`E#dK%w92b+m+)jvbk+;l z;D>kBB|FYxU$ednPk76@ znDc4(ZZXFwCw>ped!3jBDC%Q9q=R2LyIxd!7Mmizkzf1m;F>?)SD*9lVL2LMCgUY z%O?e5`_^MnS+xm8w25bIznwia3WNnUd=s2`XK%dkR#m7>82fc(YCru)E)GJ=$@K;c z+78cr)YOyP7-uu`BHHsQj%=qe%hb?@wA2;7Z{N(YK1k6wb6C>%BKxc% zf%cy7@C1ALAi& z5(FPDHJin)A$dOAA3{kcX#;{AZev6c64Q+2ftVZ1m!(F_)eJqBiL7*AJaYq$Q$8m^ zE#&o)sDUqw-ag?SGXz!~RvaD|x(xv(3E78Pp^UG9i7iC;K?NUYAuX5`h>j>nNA&-? z)Y{ydGla@q&P?cab;^L)<0Its%p*!IS{JVfb(GnrZqB8*!QZ2#rZ2E}U8VR<_#MEi zA1n-+ja{DVq)O-G5&-o@VwD!s!c?E1@8C5&2)_rzf+t@Z=@6D;N;GmXB=S&G7jWao zyf3a^U}YNkZa8!9m68T~O}A8hS?()@mWw^Q-LfpzY~x0IKkL9#4tm*p*pBM<0=Yr$ zzG4UhX2JuuY2W3OB?n49uKD#@S3sPp4CywJ>R!Qmg$a`ST~k)Sz09XKgR zYev0fPPQu2J4-~MaEe+7QB>|<)ne}&AoY}$vk);7$zbM?J!xBK6bzl`B4VVc`$_yf z8S0c4lm>`I`nHRkG8UU!;mx$|6Qv|*4%?G9=I6+_>$5R;9Da*|MprCEGDIOKpxzq$ zTD~|LP6$Z#7L0VaeuM5(j;Lcfi`u>ch#)MMWJW2~r@*+kZTwcw-CrR0(t;;_o{&EC znDX=9b}*1n7Z zweBpcUR=o#3r*<@cLQ0gDQ^b*Medx8$VIrZV21~VC4v%v3PalG5L1^Lm-s13*h6LKFQ;hjGrl`8g%a!9F_B{3o75gSI+(4{sKGd za4>i%OMJ8}bQ1@6nStR6#_<0m9e`bJ!AP?nN#M7?1;XFITjl@b2>st1CE!p@{>w-E z_cf1a#U7F3e>eHuf4H9hvxEQQ`KNp3&n~}=JsQyeZW%IpWcOli16OAwV716sAmwyFdG74weSnV`ozG6J&Bb#8kE0KBqp2$! zFwemY-&`j=z7Kq-JXWIxcyZPJ-a?awsPN_KujE?uK1)i^8%oX^0nwYFD6$_o zs-Y|W{wO24BA$H>M0WI?jtTH3Oy`E4pL80LMC*T`&t;L<+EUc@9*U>8@u#wxt-RLR zTSRc4D*mtU`A=eC?C142A0?toxmo>DO3%c54Gs9Da(94MTT5KBL@Us^>7=8pj2!Yh z;S3tO>6g;>3s*-dv^3RFBAv{fC8oq9n3oY;d>do@c>3udEqupW%2n;Zu?_At;@5Nz52btQdKk zA08&@U#BHc`ALf2|IYe_?Xnj%gA%k}G#}UTLhlllW^VQ6JU+eQs;o83^?bPpt1A*z zI@6=bIEiQN%>todpg49e^YY@L?Wp*6?eWlM!O-r-@cz6w^*sSD7$;oNhHl^av570cbocZ?(~v3=)ny&v(#KdBKBQS z{D(*yrib_m^nukD*{0QYF#fkhjGE9@XpO&(C@lBVbmabK=Em)o`Cf$40;^H+?_*1U zEi--Hvl7}Hdo?~2Zjr2^bc#lvwX5x~$Xex^C@~@ZB2>Gh-uBV*xGiORA9$Vri=Gz9d8R|u7 zc!mx;LY2=5kHTisQ5dAA>TPfc1JUQ^{g3uWeYpzrBNrC@b=f2a8>&yDZMh6yN9APpTtWc+?;m8|pqGE`k z_c0^ho~Urz()Usk2=Vi*-H5OvzfnEy_VvZPxcuY5<%C~Fo#t$BG8DTPwvDOBMBHBn zJy^+Kh97rXjMB79S6)ylSaiM?aI+_3^J0Hnn~ap&)Pheu)2NYcQkgyi+?>gX~{NM#hpu_wN0O=N@y?E=hJ}8~bb81Ho5JMt=_G$F{W>^M;KEf&BdT z#klO-;-%Nz+!7QQ;p@EU z63D^4?>l*A=t{C=a9TC#dOBLS*QMSumg?-2$=&Ei6xtgtdK$?SCiSk+cbIYFdw;#h4bW7A{E5_{G2&?oo`yk;{_MpNmWzlgX;rz)`w?HHy2`a%J}<` z;1R`y(>g>1E`vS!^iyV7mQ#Kz`7#x%w_Ob$h?)J{4yI#a5Pn`@8P|=*MrwAGKg+fd zShe;oUGZiiWpM;C((Dhc^q?%RReTx1b-;YT{a=tc`b_LoCUZEB>%3dAei#s%K^sr zbpwBD5snSZuwDcQ;T>Z+V>Z6|=EWx%0qB57`!%|L({_lFDTKf2dVB9T!H8X7yMx}m zcH9<@Wh{b3d{|lhV_!DiNq@Z;E9wam*K&*X zE#cL{q4QGxwQ9hRw^cflg+&rgnRnZF5xi2Xo4_dFLyHc&$Rm&2n0Uh1p zmU2hU-?0D>Xmk`+c_M9v3tw-+ckelz^igR_SSvPUjm}e1h6}`GLzs5N6L~l^sRj+D z!^P9&S!qbhB4k!e;2U<6V0ZX(eln5MbkLW}iX2w^-fS(~R9HgfkVglJC_J?iDzv!iAhfdpH-^!8NB2697ylXR{feib<49a z7IVn0cIxBrW`eow-8|t{+z^-5 zGU=oy8W(Gfo?>neldaa!kj*&~L}Hc?}6ifG~u81o}o_P?~<4(~>zAd40f=Z9N-Dx&frvbvc~bu+MPfKqjr9X47ux7-tve9#3_D zg+xe}2>R{yEZmG6XgHL9JaCm?u{ak;%D4^C8B4pM~)-{6sa%T zkdJ##Don*3`qZ{_?IwSEF*l#Gw;g&!(9|XT4^wpwUH~WtUr7fU7+W% zp9PX`{3;wsb1Zo2zO6=CLWlG|iROJu9f9o*v8>&x`o39|Uiot73wpWJe^- zCM>L{4tsZ$dHjb#XJ`;UdrZ-n*t)zYNsfdg)GawS%cl3|(-Dg?)&I7uUU5JBSKQ&B zyQ`Rpa!lBx2jbuD|2MYu{}nUeq!uO8oN874NDr^0JOIeH1fUo6vykH$SDFRI*Tbjh zKxL7?Tb7&hx&6!+<(cL%C0_j8UU4(C7coZ4yE1=DK#bT&Xoe)hoQm|MzkFfzvGCD< zni8zQLq;>(md?F7a;atWbjSD4qkO+;fa7wfuE*pq7*|@2f7hi4tmAyJ@>LRqmKLPP zjP{B2?6IRSQWf-c85Hl%{xO`jt)FN1GX;57|AP%5bCdqxfN2Ks_Ypp?!8>IvB}yer zmt{TOh6TueeO#uDk#>vq@?wpaHtHx3Yzvo4gxw~KUB5nEKSH^3Xmp%1KqAoYGI(h4 zS2UkqhhKIrHiUH_!hWNiI<2Dgtuie&iCHawZ}(dDp4^NeELymv8f%KiO=$>?r1$>t+YJLE=aP1oehhLle2jV-v#UddIe(uP}c!?LAdHbI8p&3@V!UEIx> zx|E)}RX3qGq(Mue(*Bs({H^Nfrw+T1x7CG22Vfs`#sNDJ<2lgV~PJKx1`L}il0f!V|Lxu(rZ^K%It`nXS_ z2aUB4P5wtWjeW_SP@YeM&9M)}p`+VR({y1k5iSp)rB9we`$YQ3{z4-m>;cbH?J*ts z`#*t0?wq2iiKL0@7oshpK?E1{i+iYgRFI8ZDp9-6Zd^O3Owy~t4FkvYxn4*q$@)ZT zk9B>B$M2;Jbr04J0}dnruKz=p|JH;N&jqP+qM;>YZ@S?@&+$I=;r2NyVR2-UEQy?Bl%~rWtEaWC!+4P zvaccb-#r+2dd-XrC&YlM*{i0j9FI2f^vZp@gun*!R_&A zo+d<%IyX<@%|c04ryxK6TasQ$EYtf~dElYJ6ikcFZISkEIXk+?+`9CaWW=(&ZTbBI zdbZ0HXkN?4GkxxUv#Af>`=YE=Nt|(sjNFMA_#8+Un@8iFnO6>ci*a*G{*XCQp>GN9)dCx& z$7F&jc4fo0K9kM*4ZvBi8yAA=;p!Y($13fY)+LN>1(V6W$e1)Ia-#fBullZ&l<_^eGL5h(ET~Rewpcu%`D>;r%KxE0mXYf1_8?lrhG@$DEZG~} z#zl`iK{ZIbbd8p8+w#&5Y_56QbKo5fP5iip3aj0i;4~2L;8lM{TLN|Z!y0yjyqV<4 zxV;Ue3*!uo6}|VBMAm9)E-XdSTNT1-L^fzTbc{PR(@2A6kCO&@$XKs-lltAotTuvOR9#Y_a3onNmUw&$X~Cv}Cu zU~u~dg!me*ZQV|FPE|pn6V8Hh{O|G(ZeR_3uvGt(C|82<`(ryLY?;UUFP3-@Lfpe z494|*XA)c`9sCg!zBN&eq6-!XAB5p`0^!XqM<%2O14;3Cel1i6551*OXStyFAeqWEKYcc5#dV2?rX3p}pT!g%Umlx~A(8jo#I?!%iJ!KMDF9Py#f>$PQ*cm>r zM8__^t#d-1T(B`}cUknf(dDT+{A_GYS*TKKxYv1TJKvmWwortm39&L9D`YCS(#Aoc zp>eF;pMY-7Wb=Z(zFocCIN6DdJ-TX822NN2Ed-ry=E={l!@xQdGRfHe))ITv8ov8y zoOj1c8soO|5VKb=af%HUe|hKjeas0><# z+r8F=(IMNN-m$)byBWDIh>b-e_H_N_;89BhX?(b{)Y0XEgZUDmol5wZbQ-*7GT1n2 z{X|7L*A{cmdmP?zEWNvYpXwj!hPi&9IFO+;p|;@hAOK=y+v{IbMlKj!@HG$(7 z3QOAm7FFVDrU*%LEj-B^&tT%5bL5|oXd5(N))h>N6$6W^G&h%T!-72Si8s~`9;jMn zcA6>R{4K#$Msf-4{;s=-A9&2($_hA~TCeh^9gAz{H?ENh#MQqMOMHp{p$rp!oRR9! zMWT>-rzU_XG|9;S#B?g{ReU^p9H%t~g6hLMG9zs7K@Eth&bdEB>`&m>Oh-wXvJSEb zWaF`}$L}8H-YxqL?%U(rz&H*NE3jO65P&f06d;@`7>FoyT{$*kLGuNK<7ZoOmYM|* zl-x6c%#q7`mwS;StvVmjEX3xdJJ)U%-Biu^ ze0-k9FltK8a_qaD9K#-?s%&|Zi!`c}fI7K9`dRSMSiP3{G^#B{iZ!Ycwnpmcy!FDi z*}(59vM~lR9;nV0N8?x>Sz!kY?bV1VwkP7bN_#NSEC%^_Nq^P0B0SLIF(mbV?b9<~ zp~rBqNLj(YP({k8HVT777s)(M?^syEx$QBNq|4Oegx@6NKuS| z`$8Qqvsa52#A;S!o24KCgP7;!>U*eqWOV{#GL&T{-m4rmtFqv7sys4EU~{vS{$|kg zYTtXPUPu!<~j(#ozvV6aE&Crfb+9o zCUnjewA8+1a&}i+JPaCG?R>EOlA!+kc6;^GV}OLxsCYG^q`Hft|LdyXoxya~CRc*r zFE1vt=OS#W%jsUyCeY!q$V1N`V(##bF=OkB4^mm8!^ko8v%(O}r&sDl8W%6Qomg*5 zB;=uOq;+Wq40bTjpYFP;k|FUu-JcehhDWb$RA%Ao8<_k|CM$Femo0;@DHz+QEzu@P zCKD*DoAkxf-8TNC^qc$Srg~VxW|?kxxxq;%{Dd60!kaSMPqxMjIT6^5@4kPwC7z5L<6Utby4V z$=lo8&919Z))%w#9aAV$nZ^$0p;exp9G-XUl+|XQ!5o%ag=AtGBRJ}pTzPkVh?(Hdq=O)Evn4VH*p&~zkMSn z_@MXG&EYV!c$TA3v6GR<<>(tKDyqZke6Y#G0bht`w<97TeCzr2 z1o0cK3AK#k?}Yez=g86-^d=$B5W?J)#@rnde|r84p)!vhR|!#*V2^$G;HnVXXqH7) z9g*2LuRXM!wI`*EcdesM{Qc}CrZbYoP}f*SR1b{B#_R0!Cl*~V+_Kx73O3D4BO}m6 z)Fpa(9#9C3-q3Pd`U{`BwQ;NOGFU#9v+6;(l zut1!fE&kvKO~r+RvaK-+N@QhqT`NBzJWiZgrubM=ifr(#k*uEC@AM! z3zCT(mNaatZGO^44~&czx8>SON_i_Q@9XO8quuWeIy2r>QGHzPoG-fA=b_=g%9<$A zE#BH9+YILBpFjDyV95H&xya~v@7>+qL5p=FZT`Vy-@u=gYDz)@?toa`WY2L21fZTT zcOGs9t_QNaov8&Afr}{@IK(O1+h-uFlP?zxmJX6>sj1~+2ehNZ@R&F`oh@vAtoa4S zN0OX3qw!-`cP;LmRh;T2B&9bSVX&DuwThgwwaN?$H42-+EpaYd<+d!FXT3cG5il<{ zd^BJ!qmhu1n4B0-3!F-L)p)JJD&nMKwAR)PB_mi_S&KIV0s;(8OsF_HG1>DL)gm0= zS^4$uBV!W5Pk<>@ML|MCC{O|t2s)Z?6hb zk`deAA@m8k`(6dF%X)t)TYe1*xY$?Oi!0w*F!`oH2&HDHmJ_o>w~H`T315ME>h^wO zJMWb7^UJWL$ch`wczT>5b1(@=i=A1!@1hf?Z)@g0%@Bs?OVWBX*B8jMk#f^&c8lkp zPh$CvWc9l}9NT~nIHf$H7pjZPm|N>O)gRm^gpt-&->PZS8ZCZ#B>!a%7lEf*%e$Ka zv&DK)r4{sC=6q*bb-!dgZQrEn*xK4Ue`N8-J&eP%CcQ6iYAUmU*Yvkh=h4ny{LXM= zERWM(0oe1pP;(M~wuKF+quCyTP%|g?SJc#~e(3`Da$wE7b&Xa+GBCtaDm}lGUtLYa zmRchca}(sB1&wOdna4NjKs9=CN+-&`W>(SJbRr#=*VcZy@r4&C=2}U7kx~LA2(R;9 zIBBm~m38A?!pX@1_$IBaY-REDRJ~)RRf8xB_YIacLeREvQ8yV!p*9Db&sO)8jA6shzFZdNlk`Ly2Fbzff_W;RVQA$Tq(-f>tRl zatzRNHAuJQGVT*%mJoOB3eNKzNudsv`d9R%e5fzD>T>@8lCpSnaH*BrB=CuC z$H383wKM?9-OtyZxNcg~*nh@+rX-w5+DzgLOLtsiCQ;R!gn=Q45v#k zg1pntm_g~HTrHbmCr^6QLTQ#do_HtEaT8*vT#%8;_W1=M5fKn>p%j!9)B*xBlcA0B z0|Nsybryx>+iTs}wDk00p`oD{`kh@8G~M(-n&Q<6J#*7K zjgcDi210emO|zSbla$>I3!R8$hr?50!-tYFcKiNXZxjody(yVWT9hxD&S zljg9$#G#|^m2pb(+A(GA+M1Q*41$Sk@6?)Jc`L9V%xjhOO#Ip-${&;Ch#{>>t@Qci z+9%pw{BY{l=oFT*r%_;%sKMIdS(HTcBLav$h zO3n7M?tsPkyaU>RFo;{eK#*lsh&q>!i_jo?) zF|C-yQK`C~z<#yupV|c7;>UUY+7zCmP0Prr;0Z&%zKSI+viEve6`kk0$KkhG%rsvp ziuTyO%GZSJXh07nJ4(uoT~ehGavIIf3uLnaMY~<6Vo^u4yw;`h1hzv9Vw&3TQ&MKA_fEkO($e&cHva-e?M(7~a--A>qJijl zoJvnn6d7whKj*viq7$)bd`g_6GwaoAyuHNMAc|0y8MG7uUnMMPK~+T>Nk&d(2Z!)N z<%&Ib)fp}ux8~Fun2Hzr}=4}9nMs5ddG(3hv^yg%IiI~IWV4}zkiA}QkFkO~~hTV)|3 zZ=JZSEFWKZ_aZF51ZtJEGf`X^T_`vSED_z2M^4=OXnGVc{EK0tj*WHXeD7uGw%Q&` zFbTU#FahVV^CD8Y3eE!oP{ZH zol2f>o%$poggYZQ)$_Ca#R22!q4}*20YUH+_;-Lp7 zEy=&75)xY1fbH1O7#$g@J${Vvv08CwSQ*}M9MX~Hnb)bSQ(F77s2KqvkEf)>NSIXvdTkyd!lk$|pyzTH{}!9W47t);ho z2Pg;MU=)zB|J;gzFk7(4K(6NfsPp~ZricLD@4u~igIe#upAZl@#Gd@Qzy9@I88<54 zKmPr#U%vSt9}z4=o+AI_v!4A0F3A->QHf+r^X2wA6V$vfsk^7(^~=b)azdw)km@AteMc)7yU}`j#PG+ zv+4q~yq&;I*6~Ky!ji<3@8;aM>)%gps@?Qb{d766-@^=?>&|X8GIL->JnW_VY1P)M zx$Xk;WSCUbobJkqlmlE;#z5-wf(JJ5o$>T;WwdAieTjQ+T9xLdA#wDM#%w1BqNq0~ z>8)V3oK^hbF^5|ZN6T%^?sqz$#RDo@RcRP$g++|T*^%?Fsy?D`&o=D@(%6WMWWHCD zD~#P4w`CZ$DwxrIU*F6 zS%HDnt>zWiFh4;@)i`AQ3K8L3U?RIc#KU7pJR%}eHi}!dw@q;urk*(%(^n&6`qwaz zO6AKB{cw$X4P%nD<_)9hF%tsGqR-|HdddYIH#AQU3Z~8LHO?Ak&OPU{Mq2`I93dGY zIE53J>pL5@pP@}UIZ0UJRt95MBQHT-__*o$sa$>cTyAoF? z(aR8}^od3p%9UIVxvE!Zg9(UC2Ff4pMEo76ii&3fNC9&P!nO z-nt$|LHgZ$6OzFO*A2eiYbz!5nu|FHY(co}S##pYgMy zAPEdNUo3Irl!=raLz#hcOLWArjinz(n!DYGwMJ6-wn)p|gOuuTi1!0A6r9S17`=YK zVN;x4n9ta7ZcG%G5O7-4`5W*m?Rf;7b2=z4rxQmRvDcA(kswuQ*LE2{Et=V_8E)vgCTVIe=N%=2%)lehMa* zY3keMc2HO?w=OU`uHmWDhp_Tf=Iw87o2ORjdJCm{(QtJs$TUmOROJ@mT7f0cg>$w) z>Z?{a0(In@>U4+AL}ILvt?Q}}@)PpuqLLcKI#xx@M+$Pagvl6?0Nf)@0}%GyhuoN$ zvgh05vH&`uy^U|?25;nappQ9+{Y3&T;+h65hUsKzFvv|m4jj*U(Xcv$;r7Ms#a9V| z`h9ahXXR;SQzh{#1uqw+mUrcL4aW66GFqAPp&+GB3}VA5@=u=Q+he*i!-0;>E>OEs zXFA)?r^(DtF{!DA0U_3k1y=-DW2Fq;n(TMH`Nrm93asdMmfWHXKLqLQCf$s3NJq9k zNw>Siov$F~WMbmR_7K#I#+;%3LI+xwjg>`nrm51kICAa}`%Lfz8N0ku*>m$8YVK>f zA6CSwiY@UV@`QYy)g0CyZT7^oC`~XBW=O74Z)mMJ7K>>*XuTU957DH$C>j@6L>#}! zx1UzE&*KygZoqmCJ6EKj=%j`pJ^exYCCa|=iJdLi;M>k?JLf(H9XCiHW53d9QrcXr zAP94LM)8~gN^|%n>?BYz3fuGJkA=4N{cV@4ZD10|g_XwMA?>4&`C=|+=H)%-zd4hv zpxX?6++ZJD9@=cI^aLc^nMS{pPh4SWhN=p*tvyq7-2O7eV%j~xBLSWt@JDGv0O;Mj zZn-})eji$^uE5#X*4&c`&=b;C3^E5!-b4Qpe!py0jBi&W7!w3#u`ATb|@ z_qO%*m(B|P2HPiAdpQk=yipc*vCx{-BoCfE_-ajTT}oY9Q;=VmAcm4#ceru>TiP#m z#BSZCDXFMC&Jp$-93naEN@o+c>_&cDhQjGQ)qD8~uTYe7T|MLV|Fn(7Y5Rlazh-Wm zv)QHWT)A}*(~JL*s3}bEbq`u1znM8--kiOb2z(7D)I@DX= z87zL#DLcY0zVs+gUiIEL%@9yo4oA7e)FL*rQWZt#_F%L-^Ff z{42YDAi0*k#l%@#rnmblL_*)?i{#1`sHGN$^72Gah-BbBF3ws@2J2~9LWBm*Y1&Rw z5M+;1uB~4)A_@l%wUg--k7yu-%WamIb~VaiXH$MR9h{+xiVe>rG!f}qNYBxLCo)cI zpuaA=7OI;Br87?6sX7ukDO~^h#8Z3anaqQuS9w$a8W3P*h2xSWjPX6<0dnPaAAe9y z>~u{I)7E;X@Ko*4OvXi4RMIIerz7#;5s;0>4_ZoZ=tpOfcN;RQI4NaGd7^ZWGKmX> zh($Zli&Bn955J8c<)FEp?C-fzj25<4`5w@>@TR_dC8}SduUmPhXx3n31zU_Kb)ksc>2uD-&G_eO_ydZ1bI#$hV?M@8l3<;fxaxT>gO z@_JX<(7*>+zRI>ys-dwpxL!*Ee7D|wNg|AYk_sT5N%+e@Nk!k)roPtd15gzpoefUi zpHzLap1-Uki%7{ZzMaLqKYf%mgYKs0OUX2eNs2LA!gQc~`0F&6H5Qc3U_{Q{c-&xhKAv4b7I9;XMd4^e zO4`9Df1;NcyGmy-Br99AojT~F6CPOACp3s}mv}|sNrfpo7O&3~^R!BmOJl@kX|~{B zdUv}$gLc?$6%U#wUxUx~9@6nVtW}{Ti0i5*`4uu1CxDFdqN0ox2ePv%%E*LpctB-< zY;obfjg9&t?NdA-{v*K<9$14*@ELgh2~gGU!cgrMl;Z3)5UH3VRohA=LZ&IAU(<1t z$;WqxvrgexTdzv?@Zo3sBpKF?n`LnL8c$v0Fct_Sp&^N>(v;jJntS%%upV6N;MYa? z!auTGENA;Hed=NQqGapxGoWK3Hw8QCb%kS~l=Oj0;((5Xj-a-`?8cXAAqk0;^aW4? z?dc|Q4?}ns{W|GscMn@A21;`-t{ zg^oS~JCLBdv=(u+`T`v{JWPQtd940s-aHMF)je3Q5(8`}Ri4Bsek~(zHd)YLi^TeH z5)zWdE_Vlx9Dlp(iRAI%d-9<1ThAskG8QT!Lsc*}BsY+P)bmo3TMJ4l(KWeXXRc1WQBo}vWw5a&K>_PsT;S+>!g`kC8((NB6x)aIHbH%l_YK@$mUbOUv z+)RJbMY(r)T(bQNpSG`@@2F1#Vrjpt)vFpnH#@0i9#QEZT7rL_$LCoZKD;1KTYRs3 zS_<%fh1c-1;{j`ELb|a5a!;P{n;a2Z*IlvzjCCRpb7^^LmAKI@Ql|x4GtSl&_8%(bKz=d`rw>JPy+zNH(c&|Ye8XAsd9?W; zD_0DYuGo`3RStSzn&+>WZufqIX2V92JC-MfSJw6?7Kc}X23ci zA6VwC+Dlx8bJAt=uDa^X<&yHM+H~n$fb%6dOkW~AJ?q@LXbx=(`WPFPXYZp}*#;U? z(9hSW3w)Dgiif(-_9|BGgrkUJ86H&bvb7%#MY&|pvu^jzDv{4bW5kh^Q*R0WnrSXN zrn_1q^NEPY$U+HJ`?Za6rHH6#w}XIk_IZyn&$&b%qT0{EKCf^}f<69qN>Q2k#Hdj1 ziEq}(SapC#lZyQ2et34nP&_JXvTMpI5-rbJ8H9wVWPGN12ra6BpL*tiw8e|?R} z%4nr`W#7rsXaXt6#WGIg8!_|bm3YZsIX9^z`z8k;F0B-JeOK*SVu+>09y7hvKIyqs z>5Nna5x@FtE+2ozW@j;rK?Pu9j)`X~FkYOV;cjWD%A5(mm+5Y&UGe-!Y3O{xZ69{P zOtlZ~^Ju}|FuDLY% ziu7-+<$sU2S#YfW6}|@N`?iU%k5|X8`TV!<`zE)W*Rwp2n8Q-UztEdJJN|n3NHTVA zo{+B}k0Y(Rp!L0w#iS>Vc!Jv?B7yVWGjRoZ`ScJH{-SldSlSQtpChW=+Bx0M&*OKd z(gUqv=OuQVUQ6ljW0~&Ojyq!_OigD|bJ^8p_4^r^Fo<4;_Xe*#o#66_We!m@(gim43orGT5=d^9KdAoZe5CYdLjnAa7R6UO4p#l@>EeQlOaL)JRd(a7;vHaZz2MQ-g{Mg?>#|9KtYOB=^(xLPKe6TJ0S!@6X}E^ zCA3iP8+GRA%sK0xd)B)718|vqWqmJ}xewWkf%_1h=xl zv|Cat^pHhNaMs9{aoPvAwT)AUA`S_KLkOf3r68TXxwA7e!=&nS)JfDO=6Lt&{n?MQ zvBoMYt#1v5MfCrI5QHbR?|DsNS7wcPw1g3Mq?M46fPwqQ!^Vy>5BF%@l%@%l#&Vl{ zm+h_LBI`n8V`O)?zL?L>2%fPi8x*YFc@ADK^qoF{HRDiurk8CIrY$nI{ZkKEm~Z77 zr#+`?HqtCq&ka{fQxe%Yz_w52&HmON8x>^`sA|Xn==uM=Vr=cZm28Y#)+*XwQkmI) znu#RY&0m}?q!+1;x7KfVfz(6AL`Co0%;|X7`QGN|I;?ndP#HuTHTDxVJ1gUIkNmJ$u}ssi}JF{qjyRvawEgOj01bqjw+b*T2^`Fqdz+Xm24Lmm-$tL?H5pw)ro?8 zvq38v!MR<7gQ}p;YcR-HS;;#;r!o5i8bo$2V|wp=ClCqn@kwuPmeYt@?Nb@n7OWDF z=6I8ct6-MzAMYb?(T51;JwFc415y;5siy2^qgu?!4I|2*nb>D8fmCM3vG-V&nDaHe zN6azn^>}fy)79?9ov^S_RZyaFXxOaj4xjG_b;ao1++0xM(H*xLDav;oImwbCOX{$? zU!c5kvV!(Z=eHkyuSi?d0XX{$cjRTUAGYZ8Lun zUKi*iRHNGzaYvQh*uK~5c2`!t%L0EbDDI5TS;?VwL_Jj&O=ptLWQMG-Gx(UbMH4pv z5U-N0_6HAWxnH@$t?01Kzc`>XXHL9t(UI4)Z#zs>X)!hlD8E&`glU>*Mdii6uS;VQu&J-ZMHu} zvSh;1n%OO>hJ3K!zI^U3uTwNk2;yqDqQ<@N5%?CkxMApMf1 z23PU@v_GQ$*jZQ=I&?wFpz_sa9pU!6IMc7`8He`c7Pgx&m_d#3_S^?p7>&>fW_P*C ze1wXI@U*CV^$k@OV$D%2+#3Du+8pA>x!T4DX(N7cwM%bjYRIt_S4C~{@47`)+sE~V zgyIo&7oBc^az^}4gAW!b)WzMQURxqo%yIWV&ar@Ek#jhU?Nn7yuYh^Elhr6PxG~tG z&*#u47=ZGEYuefZ+1UnlUlR7JwA;GJ<@4F{Ysgy`{(FcmJU8=aO59#gv$5AWXV6My z@XCvSq3{=?pEtg!iRao6=WU+vPwVT|GKHlwYneRYyKlF2k8yFhe|H1mVq0gjmsep$AL}i+u$_>i$aj=3_-s!me9V!mSz~6#} z{S1a*i!UJkDfe)q8XXrt^SM7qAvGuK^6SIp_zeI`Pbxg5E<_B>Mu;;(h%ZiLge8q= zo4@+ciJlccqq?eMUj>S?0Yqd-;8Yq+5a)Dgr1^A4zc)ofHY&zLSkc<|7ZynNt>{}5UWL@$hAQ)$KcyJ# z6z56v_(zHi^Y0)|%)~rAJ<*pV#BvGMuYod?>c$-Stx50FRM@&IP#r*`0x|>S

U% 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 MR#hhVj{fWa1#g0Aga7~l literal 0 HcmV?d00001 diff --git a/rules/crs/crs-setup-netsapiens.conf b/rules/crs/crs-setup-netsapiens.conf deleted file mode 100644 index a93c0fe..0000000 --- a/rules/crs/crs-setup-netsapiens.conf +++ /dev/null @@ -1,47 +0,0 @@ -# OWASP CRS Setup Overrides for NetSapiens -# Source file — used as basis for /etc/modsecurity/crs/crs-setup.conf by nssec waf init -# -# Paranoia level 1 is conservative — increase after tuning false positives. -# See https://coreruleset.org/docs/concepts/paranoia_levels/ for guidance. - -# Paranoia level (1 = low FP, less coverage; 4 = max coverage, many FPs) -SecAction \ - "id:900000,\ - phase:1,\ - nolog,\ - pass,\ - t:none,\ - setvar:tx.crs_setup_version=400,\ - setvar:tx.paranoia_level=1,\ - setvar:tx.blocking_paranoia_level=1,\ - setvar:tx.detection_paranoia_level=1" - -# Anomaly scoring thresholds -# Inbound 5 = block after a single critical rule match (default) -# Outbound 4 = block on data leakage detection (default) -SecAction \ - "id:900110,\ - phase:1,\ - nolog,\ - pass,\ - t:none,\ - setvar:tx.inbound_anomaly_score_threshold=5,\ - setvar:tx.outbound_anomaly_score_threshold=4" - -# Allowed HTTP methods for NetSapiens admin UI and API -SecAction \ - "id:900200,\ - phase:1,\ - nolog,\ - pass,\ - t:none,\ - setvar:'tx.allowed_methods=GET HEAD POST OPTIONS PUT PATCH DELETE'" - -# Allowed content types -SecAction \ - "id:900220,\ - phase:1,\ - nolog,\ - pass,\ - t:none,\ - setvar:'tx.allowed_request_content_type=|application/x-www-form-urlencoded| |multipart/form-data| |multipart/related| |multipart/mixed| |text/xml| |application/xml| |application/soap+xml| |application/json| |application/cloudevents+json| |application/cloudevents-batch+json|'" diff --git a/rules/netsapiens/netsapiens-exclusions.conf b/rules/netsapiens/netsapiens-exclusions.conf deleted file mode 100644 index dd3c20b..0000000 --- a/rules/netsapiens/netsapiens-exclusions.conf +++ /dev/null @@ -1,77 +0,0 @@ -# NetSapiens-specific ModSecurity Exclusions -# Source file — installed to /etc/modsecurity/netsapiens-exclusions.conf by nssec waf init -# -# These rules prevent false positives on the NetSapiens management UI -# and API endpoints while keeping CRS protection active for everything else. -# -# Rule ID range: 1000001–1000999 (reserved for nssec) - -# ---- Admin UI form submissions trigger SQL injection false positives ---- -# The admin search/filter fields send SQL-like syntax that CRS flags incorrectly. -SecRuleUpdateTargetById 942100 "!REQUEST_COOKIES" -SecRuleUpdateTargetById 942200 "!REQUEST_COOKIES" - -# ---- NS API endpoints use base64 in query strings ---- -SecRule REQUEST_URI "@beginsWith /ns-api/" \ - "id:1000001,\ - phase:1,\ - pass,\ - nolog,\ - ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:filter" - -# ---- Admin UI session handling ---- -# Rule 921180 (HTTP Parameter Pollution) false-positives on NS session cookies. -SecRule REQUEST_URI "@beginsWith /SiPbx/" \ - "id:1000002,\ - phase:1,\ - pass,\ - nolog,\ - ctl:ruleRemoveById=921180" - -# ---- QoS proxy (NqsProxy) ---- -# The QoS proxy sends multipart/mixed which triggers rule 920420 -# (content type not allowed by policy). This is normal NS internal traffic. -SecRule REQUEST_URI "@beginsWith /NqsProxy/" \ - "id:1000003,\ - phase:1,\ - pass,\ - nolog,\ - ctl:ruleRemoveById=920420" - -# ---- QoS proxy (NqsProxy) ---- -# The QoS proxy sends multipart/mixed which triggers rule 920420 -# (content type not allowed by policy). This is normal NS internal traffic. -SecRule REQUEST_URI "@beginsWith /NqsProxy/" \ - "id:1000003,\ - phase:1,\ - pass,\ - nolog,\ - ctl:ruleRemoveById=920420" - -# ---- iNSight health checks ---- -# The iNSight monitoring system polls /cfg/insight_healthcheck.html via AJP proxy. -# Apache denies these with 403 (mod_authz_core), and ModSecurity audit-logs them -# because it captures all 4xx/5xx responses. Bypass CRS for this endpoint. -SecRule REQUEST_URI "@beginsWith /cfg/insight_healthcheck" \ - "id:1000004,\ - phase:1,\ - pass,\ - nolog,\ - ctl:ruleRemoveByTag=OWASP_CRS" - -# ---- Localhost internal traffic ---- -# NetSapiens services (NmsSBus, ns-api, cfg) communicate over localhost. -# These internal calls trigger false positives: -# 921110 — NmsSBus RegistrarEvents body contains "connect " which -# matches the HTTP Request Smuggling pattern (connect is a SIP field, -# not an HTTP CONNECT method) -# 920180 — ns-api internal POSTs lack Content-Length header -# 920350 — internal requests use Host: 127.0.0.1 instead of a hostname -# Bypassing CRS for localhost is safe — if an attacker has local access, -# the WAF is already irrelevant. -SecRule REMOTE_ADDR "@ipMatch 127.0.0.1" \ - "id:1000005,\ - phase:1,\ - pass,\ - nolog,\ - ctl:ruleRemoveByTag=OWASP_CRS" diff --git a/src/nssec/cli/__init__.py b/src/nssec/cli/__init__.py index a3e51fa..df23551 100644 --- a/src/nssec/cli/__init__.py +++ b/src/nssec/cli/__init__.py @@ -6,7 +6,6 @@ from __future__ import annotations from pathlib import Path -from typing import Optional import click from rich.console import Console @@ -36,7 +35,7 @@ def _is_within_allowed_bases(resolved: Path, bases: tuple[Path, ...]) -> bool: def validate_path( path_str: str, param_name: str, - allowed_bases: Optional[tuple[Path, ...]] = None, + allowed_bases: tuple[Path, ...] | None = None, must_be_within_cwd: bool = False, ) -> Path: """Validate a path to prevent path traversal attacks. diff --git a/src/nssec/cli/main.py b/src/nssec/cli/main.py index 46f5e25..cb1ba01 100644 --- a/src/nssec/cli/main.py +++ b/src/nssec/cli/main.py @@ -70,6 +70,7 @@ def cli(ctx, host, sudo): # Set up sudo for local execution if no host specified if sudo and not host: from nssec.core.ssh import set_use_sudo + set_use_sudo(True) if host: diff --git a/src/nssec/cli/mtls_commands.py b/src/nssec/cli/mtls_commands.py index 701058e..4e2d71e 100644 --- a/src/nssec/cli/mtls_commands.py +++ b/src/nssec/cli/mtls_commands.py @@ -23,11 +23,15 @@ def mtls(ctx): console.print("[bold]Allowlist Commands:[/bold]") console.print(" [cyan]nssec mtls allowlist show[/cyan] Show all whitelisted IPs") console.print(" [cyan]nssec mtls allowlist add[/cyan] Add an IP to the allowlist") - console.print(" [cyan]nssec mtls allowlist remove[/cyan] Remove an IP from the allowlist") + console.print( + " [cyan]nssec mtls allowlist remove[/cyan] Remove an IP from the allowlist" + ) console.print() console.print("[bold]NodePing Commands:[/bold]") console.print(" [cyan]nssec mtls nodeping show[/cyan] Show current NodePing IPs") - console.print(" [cyan]nssec mtls nodeping fetch[/cyan] Fetch IPs from NodePing (dry run)") + console.print( + " [cyan]nssec mtls nodeping fetch[/cyan] Fetch IPs from NodePing (dry run)" + ) console.print(" [cyan]nssec mtls nodeping update[/cyan] Fetch and apply NodePing IPs") console.print(" [cyan]nssec mtls nodeping remove[/cyan] Remove NodePing IPs section") diff --git a/src/nssec/cli/waf_commands.py b/src/nssec/cli/waf_commands.py index c1f05ef..dbe0e61 100644 --- a/src/nssec/cli/waf_commands.py +++ b/src/nssec/cli/waf_commands.py @@ -154,13 +154,17 @@ def _build_status_table(status): table.add_row("CRS path", status.crs_path) setup_val = _yn(status.crs_setup_present) if not status.crs_setup_present: - setup_val += " [red](rule 901001 will flag all traffic! run [cyan]nssec waf init[/cyan])[/red]" + setup_val += ( + " [red](rule 901001 will flag all traffic! run [cyan]nssec waf init[/cyan])[/red]" + ) table.add_row("crs-setup.conf", setup_val) else: table.add_row("OWASP CRS", "[red]not installed[/red]") if status.evasive_installed: - evasive_state = "[green]enabled[/green]" if status.evasive_enabled else "[yellow]disabled[/yellow]" + evasive_state = ( + "[green]enabled[/green]" if status.evasive_enabled else "[yellow]disabled[/yellow]" + ) table.add_row("mod_evasive", evasive_state) else: table.add_row("mod_evasive", "[dim]not installed[/dim]") @@ -181,11 +185,10 @@ def _build_status_table(status): elif not status.exclusions_current: v = status.exclusions_version or "unknown" excl_val = ( - f"[yellow]outdated (v{v})[/yellow] — " - "run [cyan]nssec waf update-exclusions[/cyan]" + f"[yellow]outdated (v{v})[/yellow] — run [cyan]nssec waf update-exclusions[/cyan]" ) else: - excl_val = "[green]active (v{})[/green]".format(status.exclusions_version) + excl_val = f"[green]active (v{status.exclusions_version})[/green]" table.add_row("NS exclusions", excl_val) if status.exclusions_admin_ips: table.add_row(" Admin IPs", str(status.exclusions_admin_ips)) @@ -301,9 +304,7 @@ def waf_enable(yes): if result.success: console.print(f"[green]{result.message}[/green]") console.print() - console.print( - "[bold]Tip:[/bold] To also enable HTTP flood protection, run:" - ) + console.print("[bold]Tip:[/bold] To also enable HTTP flood protection, run:") console.print(" [cyan]sudo nssec waf evasive enable[/cyan]") else: console.print(f"[red]Error: {result.error}[/red]") @@ -350,10 +351,9 @@ def waf_remove(yes): This disables the security2 module in Apache, effectively turning off the WAF completely. Use 'nssec waf init' to re-enable. """ - from nssec.modules.waf import ModSecurityInstaller - from nssec.modules.waf.utils import run_cmd, file_exists - from nssec.modules.waf.config import SECURITY2_LOAD from nssec.core.ssh import is_root + from nssec.modules.waf.config import SECURITY2_LOAD + from nssec.modules.waf.utils import file_exists, run_cmd if not is_root(): console.print("[red]Error: Must run as root (sudo nssec waf remove)[/red]") @@ -469,7 +469,6 @@ def waf_update(yes): rules that were disabled during init and validates the Apache config. """ from nssec.modules.waf import ModSecurityInstaller - from nssec.modules.waf.utils import detect_modsec_version, version_gte from nssec.modules.waf.config import ( CRS_INSTALL_DIR, DIGITALWAVE_KEY_URL, @@ -477,6 +476,7 @@ def waf_update(yes): DIGITALWAVE_LIST, DIGITALWAVE_REPO_URL, ) + from nssec.modules.waf.utils import detect_modsec_version, version_gte installer = ModSecurityInstaller() pf = installer.preflight() @@ -487,9 +487,7 @@ def waf_update(yes): if not version_gte(current_ver, "2.9.6"): console.print() - console.print( - "[yellow]ModSecurity < 2.9.6 — some CRS v4 rules are disabled.[/yellow]" - ) + console.print("[yellow]ModSecurity < 2.9.6 — some CRS v4 rules are disabled.[/yellow]") console.print( "Ubuntu 22.04 ships ModSecurity 2.9.5 which lacks support for " "multipart rules introduced in 2.9.6." @@ -499,8 +497,7 @@ def waf_update(yes): console.print() keyring = DIGITALWAVE_KEYRING console.print( - f" [cyan]curl -fsSL {DIGITALWAVE_KEY_URL} " - f"| sudo gpg --dearmor -o {keyring}[/cyan]" + f" [cyan]curl -fsSL {DIGITALWAVE_KEY_URL} | sudo gpg --dearmor -o {keyring}[/cyan]" ) console.print() # Escape square brackets so Rich doesn't treat [signed-by=...] as markup @@ -508,17 +505,14 @@ def waf_update(yes): repo = DIGITALWAVE_REPO_URL lst = DIGITALWAVE_LIST console.print( - f' [cyan]echo "deb {signed} {repo} $(lsb_release -sc) main" ' - f"| sudo tee {lst}[/cyan]" + f' [cyan]echo "deb {signed} {repo} $(lsb_release -sc) main" | sudo tee {lst}[/cyan]' ) console.print( f' [cyan]echo "deb {signed} {repo} $(lsb_release -sc)-backports main" ' f"| sudo tee -a {lst}[/cyan]" ) console.print() - console.print( - " [cyan]sudo apt-get update[/cyan]" - ) + console.print(" [cyan]sudo apt-get update[/cyan]") console.print( " [cyan]sudo apt-get install -t $(lsb_release -sc)-backports " "libapache2-mod-security2[/cyan]" @@ -534,12 +528,13 @@ def waf_update(yes): crs_path = pf.crs_path or CRS_INSTALL_DIR reenabled = installer._reenable_crs_rules(crs_path) if not reenabled: - console.print("[green]ModSecurity >= 2.9.6 and all CRS rules are active. Nothing to do.[/green]") + console.print( + "[green]ModSecurity >= 2.9.6 and all CRS rules are active. Nothing to do.[/green]" + ) return console.print( - f" [green]Done:[/green] Re-enabled {len(reenabled)} CRS rule(s): " - + ", ".join(reenabled) + f" [green]Done:[/green] Re-enabled {len(reenabled)} CRS rule(s): " + ", ".join(reenabled) ) val = installer.validate_config() @@ -593,7 +588,7 @@ def waf_allowlist_add(ip, yes): IP can be a single address (192.168.1.1) or CIDR notation (10.0.0.0/8). Allowlisted IPs bypass OWASP CRS rules for reduced false positives. """ - from nssec.modules.waf import ModSecurityInstaller, get_allowlisted_ips, add_allowlisted_ip + from nssec.modules.waf import ModSecurityInstaller, add_allowlisted_ip, get_allowlisted_ips installer = ModSecurityInstaller() pf = installer.preflight() @@ -678,7 +673,7 @@ def waf_evasive(ctx): "--profile", type=click.Choice(["standard", "strict"]), default="standard", - help="Threshold profile: standard (high thresholds, safe default) or strict (tuned for NS traffic)", + help="Threshold profile: standard (safe default) or strict (tuned for NS traffic)", ) def waf_evasive_enable(yes, profile): """Enable mod_evasive HTTP flood protection. @@ -708,15 +703,12 @@ def waf_evasive_enable(yes, profile): raise SystemExit(1) if not package_installed(EVASIVE_PACKAGE): - console.print( - "[red]Error: mod_evasive is not installed. " - "Run 'nssec waf init' first.[/red]" - ) + console.print("[red]Error: mod_evasive is not installed. Run 'nssec waf init' first.[/red]") raise SystemExit(1) thresholds = EVASIVE_PROFILES[profile] console.print( - f"[bold yellow]Warning:[/bold yellow] mod_evasive has no detection-only mode. " + "[bold yellow]Warning:[/bold yellow] mod_evasive has no detection-only mode. " "When enabled it [bold]will block[/bold] IPs that exceed thresholds (HTTP 403)." ) console.print(f" Profile: [cyan]{profile}[/cyan]") @@ -756,8 +748,8 @@ def waf_evasive_enable(yes, profile): @click.option("--yes", "-y", is_flag=True, help="Skip confirmation") def waf_evasive_disable(yes): """Disable mod_evasive HTTP flood protection.""" - from nssec.modules.waf import ModSecurityInstaller from nssec.core.ssh import is_root + from nssec.modules.waf import ModSecurityInstaller if not is_root(): console.print("[red]Error: Must run as root (sudo nssec waf evasive disable)[/red]") @@ -824,9 +816,7 @@ def waf_evasive_status(): console.print(f" Profile: [cyan]{profile}[/cyan]") if not enabled: - console.print( - "\n Enable with: [cyan]sudo nssec waf evasive enable[/cyan]" - ) + console.print("\n Enable with: [cyan]sudo nssec waf evasive enable[/cyan]") # ─── RESTRICT SUBCOMMANDS ─── @@ -916,7 +906,9 @@ def waf_restrict_show(): console.print(" Run [cyan]nssec waf restrict reapply[/cyan] after NS upgrades to restore") elif first_ips_managed: console.print("\n[bold]IP cache:[/bold] [yellow]not saved[/yellow]") - console.print(" Run [cyan]nssec waf restrict init[/cyan] to save IPs for reapply after upgrades") + console.print( + " Run [cyan]nssec waf restrict init[/cyan] to save IPs for reapply after upgrades" + ) @waf_restrict.command("init") @@ -940,9 +932,9 @@ def waf_restrict_init(ips, dry_run, yes): """ import ipaddress - from nssec.core.ssh import is_root from nssec.core.server_types import detect_server_type - from nssec.modules.waf.restrict import init_restrictions, collect_existing_ips + from nssec.core.ssh import is_root + from nssec.modules.waf.restrict import collect_existing_ips, init_restrictions if not is_root(): console.print("[red]Error: Must run as root (sudo nssec waf restrict init)[/red]") @@ -974,9 +966,7 @@ def waf_restrict_init(ips, dry_run, yes): "[bold]Enter IP addresses to allow access[/bold] " "(one per line, or space/comma separated)." ) - console.print( - " Include NetSapiens TAC IPs and your admin office IPs." - ) + console.print(" Include NetSapiens TAC IPs and your admin office IPs.") console.print(" 127.0.0.1 is always included automatically.") console.print(" Press Enter on a blank line when done.") console.print() @@ -1010,8 +1000,9 @@ def waf_restrict_init(ips, dry_run, yes): console.print() if dry_run: - results = init_restrictions(server_type, ip_list, dry_run=True, - merge_existing=merge_existing) + results = init_restrictions( + server_type, ip_list, dry_run=True, merge_existing=merge_existing + ) for name, result in results: label = f"[cyan]{name}:[/cyan] " if name else "" console.print(f" {label}{result.message}") @@ -1022,8 +1013,7 @@ def waf_restrict_init(ips, dry_run, yes): console.print("[yellow]Aborted.[/yellow]") return - results = init_restrictions(server_type, ip_list, - merge_existing=merge_existing) + results = init_restrictions(server_type, ip_list, merge_existing=merge_existing) any_error = False for name, result in results: label = f"[cyan]{name}:[/cyan] " if name else "" @@ -1051,8 +1041,8 @@ def waf_restrict_add(ip, yes): """ import ipaddress - from nssec.core.ssh import is_root from nssec.core.server_types import detect_server_type + from nssec.core.ssh import is_root from nssec.modules.waf.restrict import add_restricted_ip if not is_root(): @@ -1097,8 +1087,8 @@ def waf_restrict_remove(ip, yes): Cannot remove 127.0.0.1 (localhost is always required). """ - from nssec.core.ssh import is_root from nssec.core.server_types import detect_server_type + from nssec.core.ssh import is_root from nssec.modules.waf.restrict import remove_restricted_ip if not is_root(): @@ -1139,9 +1129,9 @@ def waf_restrict_reapply(dry_run, yes): Reads the saved IP list from /etc/nssec/restrict-ips.json and re-creates all managed .htaccess files. """ - from nssec.core.ssh import is_root from nssec.core.server_types import detect_server_type - from nssec.modules.waf.restrict import reapply_restrictions, load_cached_ips + from nssec.core.ssh import is_root + from nssec.modules.waf.restrict import load_cached_ips, reapply_restrictions if not is_root(): console.print("[red]Error: Must run as root (sudo nssec waf restrict reapply)[/red]") diff --git a/src/nssec/core/cache.py b/src/nssec/core/cache.py index da19ff6..0482031 100644 --- a/src/nssec/core/cache.py +++ b/src/nssec/core/cache.py @@ -22,14 +22,13 @@ import threading import time from pathlib import Path -from typing import Optional, Union from nssec.core import ssh def _remove_suffix(text: str, suffix: str) -> str: """Remove suffix from string (Python 3.8 compatible).""" - return text[:-len(suffix)] if suffix and text.endswith(suffix) else text + return text[: -len(suffix)] if suffix and text.endswith(suffix) else text def _run_subprocess(cmd: list[str], timeout: int = 30) -> tuple[str, int]: @@ -46,7 +45,7 @@ def _run_subprocess(cmd: list[str], timeout: int = 30) -> tuple[str, int]: return stdout, rc -def _parse_dpkg_line(line: str) -> Optional[str]: +def _parse_dpkg_line(line: str) -> str | None: """Parse a dpkg -l output line to extract package name if installed. Args: @@ -64,7 +63,7 @@ def _parse_dpkg_line(line: str) -> Optional[str]: return parts[1].split(":")[0] -def _parse_service_line(line: str) -> tuple[Optional[str], Optional[str]]: +def _parse_service_line(line: str) -> tuple[str | None, str | None]: """Parse a systemctl list-units output line to extract service names. Args: @@ -99,7 +98,7 @@ def _read_ufw_files() -> str: return content -def _safe_read_file(path: Union[str, Path]) -> Optional[str]: +def _safe_read_file(path: str | Path) -> str | None: """Safely read a file, returning None on any error (locally or via SSH). Args: @@ -138,15 +137,15 @@ def __init__(self, ttl: float = 0) -> None: self._dpkg_time: float = 0 # Service (systemctl) cache - self._active_services: Optional[set[str]] = None + self._active_services: set[str] | None = None self._services_time: float = 0 # File content cache - self._files: dict[str, Optional[str]] = {} + self._files: dict[str, str | None] = {} self._file_times: dict[str, float] = {} # UFW rules cache - self._ufw_rules: Optional[str] = None + self._ufw_rules: str | None = None self._ufw_rules_loaded: bool = False self._ufw_time: float = 0 @@ -286,7 +285,7 @@ def cached_service_active(self, service_name: str) -> bool: or f"{normalized}.service" in self._active_services ) - def cached_file_read(self, path: Union[str, Path]) -> Optional[str]: + def cached_file_read(self, path: str | Path) -> str | None: """Read file contents with caching. Caches file contents by path. Subsequent reads return cached content @@ -306,7 +305,7 @@ def cached_file_read(self, path: Union[str, Path]) -> Optional[str]: return content return self._load_and_cache_file(path, path_str) - def _get_valid_cached_file(self, path_str: str) -> tuple[bool, Optional[str]]: + def _get_valid_cached_file(self, path_str: str) -> tuple[bool, str | None]: """Get cached file content if valid and not expired. Args: @@ -322,7 +321,7 @@ def _get_valid_cached_file(self, path_str: str) -> tuple[bool, Optional[str]]: return False, None return True, self._files[path_str] - def _load_and_cache_file(self, path: Union[str, Path], path_str: str) -> Optional[str]: + def _load_and_cache_file(self, path: str | Path, path_str: str) -> str | None: """Load a file and store it in cache (internal, called with lock held). Args: @@ -338,7 +337,7 @@ def _load_and_cache_file(self, path: Union[str, Path], path_str: str) -> Optiona self._file_times[path_str] = time.time() return content - def cached_ufw_rules(self) -> Optional[str]: + def cached_ufw_rules(self) -> str | None: """Read UFW rules from config files with caching. Reads UFW rules from /etc/ufw/user.rules and /etc/ufw/user6.rules. @@ -352,7 +351,7 @@ def cached_ufw_rules(self) -> Optional[str]: return self._ufw_rules return self._load_ufw_rules() - def _load_ufw_rules(self) -> Optional[str]: + def _load_ufw_rules(self) -> str | None: """Load UFW rules into cache (internal, called with lock held). Returns: @@ -370,7 +369,7 @@ def _load_ufw_rules(self) -> Optional[str]: return self._cache_ufw_result(None) - def _cache_ufw_result(self, content: Optional[str]) -> Optional[str]: + def _cache_ufw_result(self, content: str | None) -> str | None: """Store UFW result in cache and return it. Args: @@ -433,7 +432,7 @@ def cached_service_active(service_name: str) -> bool: return session_cache.cached_service_active(service_name) -def cached_file_read(path: Union[str, Path]) -> Optional[str]: +def cached_file_read(path: str | Path) -> str | None: """Read file contents with caching (uses session cache). This is a convenience wrapper around session_cache.cached_file_read(). @@ -447,7 +446,7 @@ def cached_file_read(path: Union[str, Path]) -> Optional[str]: return session_cache.cached_file_read(path) -def cached_ufw_rules() -> Optional[str]: +def cached_ufw_rules() -> str | None: """Read UFW rules with caching (uses session cache). This is a convenience wrapper around session_cache.cached_ufw_rules(). diff --git a/src/nssec/core/checklist.py b/src/nssec/core/checklist.py index da6fe51..9b2a83e 100644 --- a/src/nssec/core/checklist.py +++ b/src/nssec/core/checklist.py @@ -6,7 +6,6 @@ from dataclasses import dataclass, field from enum import Enum from pathlib import Path -from typing import Optional from nssec.core import ssh @@ -40,9 +39,9 @@ class CheckResult: status: CheckStatus severity: Severity message: str - details: Optional[str] = None - remediation: Optional[str] = None - reference: Optional[str] = None + details: str | None = None + remediation: str | None = None + reference: str | None = None @dataclass @@ -87,15 +86,15 @@ class BaseCheck(ABC): name: str = "" description: str = "" severity: Severity = Severity.MEDIUM - applies_to: Optional[list[str]] = None # Server types this check applies to - reference: Optional[str] = None # Documentation reference + applies_to: list[str] | None = None # Server types this check applies to + reference: str | None = None # Documentation reference @abstractmethod def run(self) -> CheckResult: """Execute the check and return result.""" pass - def _pass(self, message: str, details: Optional[str] = None) -> CheckResult: + def _pass(self, message: str, details: str | None = None) -> CheckResult: return CheckResult( check_id=self.check_id, name=self.name, @@ -107,7 +106,7 @@ def _pass(self, message: str, details: Optional[str] = None) -> CheckResult: ) def _fail( - self, message: str, details: Optional[str] = None, remediation: Optional[str] = None + self, message: str, details: str | None = None, remediation: str | None = None ) -> CheckResult: return CheckResult( check_id=self.check_id, @@ -121,7 +120,7 @@ def _fail( ) def _warn( - self, message: str, details: Optional[str] = None, remediation: Optional[str] = None + self, message: str, details: str | None = None, remediation: str | None = None ) -> CheckResult: return CheckResult( check_id=self.check_id, @@ -143,7 +142,7 @@ def _skip(self, message: str) -> CheckResult: message=message, ) - def _error(self, message: str, details: Optional[str] = None) -> CheckResult: + def _error(self, message: str, details: str | None = None) -> CheckResult: return CheckResult( check_id=self.check_id, name=self.name, @@ -154,7 +153,7 @@ def _error(self, message: str, details: Optional[str] = None) -> CheckResult: ) -def run_command(cmd: list[str], timeout: int = 30) -> tuple[Optional[str], Optional[str], int]: +def run_command(cmd: list[str], timeout: int = 30) -> tuple[str | None, str | None, int]: """Run a command locally or remotely (if SSH host is configured). Args: @@ -187,7 +186,7 @@ def file_contains(path: Path, pattern: str, ignore_comments: bool = True) -> boo return False -def _extract_config_value(line: str, key: str, separator: str) -> Optional[str]: +def _extract_config_value(line: str, key: str, separator: str) -> str | None: """Extract a value from a config line if it matches the key.""" line = line.strip() if line.startswith("#"): @@ -200,7 +199,7 @@ def _extract_config_value(line: str, key: str, separator: str) -> Optional[str]: return None -def get_file_value(path: Path, key: str, separator: str = " ") -> Optional[str]: +def get_file_value(path: Path, key: str, separator: str = " ") -> str | None: """Get a configuration value from a file (works locally or via SSH).""" content = ssh.read_file(str(path)) if content is None: diff --git a/src/nssec/core/server_types.py b/src/nssec/core/server_types.py index 8e423a7..8d05e2e 100644 --- a/src/nssec/core/server_types.py +++ b/src/nssec/core/server_types.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional from nssec.core.checklist import run_command @@ -144,7 +143,7 @@ class ServiceInfo: ] -def _run_command(cmd: list[str], timeout: int = 10) -> Optional[str]: +def _run_command(cmd: list[str], timeout: int = 10) -> str | None: """Run a command and return stdout, or None on error. This is a convenience wrapper around run_command from checklist.py @@ -352,7 +351,7 @@ def get_server_info() -> dict: } -def get_applicable_security_modules(server_type: Optional[ServerType] = None) -> list[str]: +def get_applicable_security_modules(server_type: ServerType | None = None) -> list[str]: """Get security modules applicable to this server type. Args: diff --git a/src/nssec/core/ssh.py b/src/nssec/core/ssh.py index 91291bf..5458bac 100644 --- a/src/nssec/core/ssh.py +++ b/src/nssec/core/ssh.py @@ -21,16 +21,15 @@ import os import subprocess from pathlib import Path -from typing import Optional # Global remote host - when set, all commands execute via SSH -_remote_host: Optional[str] = None +_remote_host: str | None = None # Global sudo flag - when set, commands are prefixed with sudo _use_sudo: bool = False -def set_remote_host(host: Optional[str]) -> None: +def set_remote_host(host: str | None) -> None: """Set the remote host for SSH execution. Args: @@ -41,7 +40,7 @@ def set_remote_host(host: Optional[str]) -> None: _remote_host = host -def get_remote_host() -> Optional[str]: +def get_remote_host() -> str | None: """Get the current remote host, or None if running locally.""" return _remote_host @@ -119,8 +118,8 @@ def __init__(self, host: str, timeout: int = 30, use_sudo: bool = False): def run_command( self, cmd: list[str], - timeout: Optional[int] = None, - use_sudo: Optional[bool] = None, + timeout: int | None = None, + use_sudo: bool | None = None, ) -> tuple[str, str, int]: """Run a command on the remote host via SSH. @@ -164,7 +163,7 @@ def run_command( except Exception as e: return "", str(e), -1 - def read_file(self, path: str) -> Optional[str]: + def read_file(self, path: str) -> str | None: """Read a file from the remote host. Args: @@ -232,10 +231,10 @@ def _shell_quote(s: str) -> str: # Global executor instance (created when remote host is set) -_executor: Optional[SSHExecutor] = None +_executor: SSHExecutor | None = None -def get_executor() -> Optional[SSHExecutor]: +def get_executor() -> SSHExecutor | None: """Get the global SSH executor, or None if running locally.""" global _executor if _remote_host: @@ -283,7 +282,7 @@ def run_command(cmd: list[str], timeout: int = 30) -> tuple[str, str, int]: return "", str(e), -1 -def read_file(path: str) -> Optional[str]: +def read_file(path: str) -> str | None: """Read a file locally or remotely depending on configuration. Args: diff --git a/src/nssec/modules/waf/__init__.py b/src/nssec/modules/waf/__init__.py index f228af2..1688e9c 100644 --- a/src/nssec/modules/waf/__init__.py +++ b/src/nssec/modules/waf/__init__.py @@ -11,7 +11,6 @@ from pathlib import Path from nssec.core.ssh import is_directory, is_root - from nssec.modules.waf.config import ( BACKUP_SUFFIX, CRS_APT_PACKAGE, @@ -22,8 +21,8 @@ CRS_SETUP_OVERRIDES_TEMPLATE, EVASIVE_CONF, EVASIVE_CONF_TEMPLATE, - EVASIVE_LOAD, EVASIVE_DEFAULT_PROFILE, + EVASIVE_LOAD, EVASIVE_LOG_DIR, EVASIVE_LOG_FILE, EVASIVE_PACKAGE, diff --git a/src/nssec/modules/waf/restrict.py b/src/nssec/modules/waf/restrict.py index 9ef89ed..8afa78d 100644 --- a/src/nssec/modules/waf/restrict.py +++ b/src/nssec/modules/waf/restrict.py @@ -146,13 +146,15 @@ def get_restrict_status(server_type: str) -> list[dict]: managed = is_nssec_managed(path) if exists else False ips = parse_htaccess_ips(path) if exists else [] - statuses.append({ - "name": target["name"], - "path": path, - "exists": exists, - "managed": managed, - "ips": ips, - }) + statuses.append( + { + "name": target["name"], + "path": path, + "exists": exists, + "managed": managed, + "ips": ips, + } + ) return statuses @@ -246,10 +248,15 @@ def init_restrictions( results: list[tuple[str, StepResult]] = [] if not targets: - results.append(("", StepResult( - skipped=True, - message="No applicable targets found for this server type", - ))) + results.append( + ( + "", + StepResult( + skipped=True, + message="No applicable targets found for this server type", + ), + ) + ) return results for target in targets: @@ -257,9 +264,14 @@ def init_restrictions( name = target["name"] if dry_run: - results.append((name, StepResult( - message=f"Would create {path} with {len(all_ips)} IP(s)", - ))) + results.append( + ( + name, + StepResult( + message=f"Would create {path} with {len(all_ips)} IP(s)", + ), + ) + ) continue if file_exists(path): @@ -267,15 +279,25 @@ def init_restrictions( content = _render_htaccess(target, all_ips) if not write_file(path, content): - results.append((name, StepResult( - success=False, - error=f"Failed to write {path}", - ))) + results.append( + ( + name, + StepResult( + success=False, + error=f"Failed to write {path}", + ), + ) + ) continue - results.append((name, StepResult( - message=f"Created {path} with {len(all_ips)} IP(s)", - ))) + results.append( + ( + name, + StepResult( + message=f"Created {path} with {len(all_ips)} IP(s)", + ), + ) + ) # Save the full IP set to cache for reapply after upgrades if not dry_run: @@ -305,40 +327,65 @@ def add_restricted_ip( name = target["name"] if not file_exists(path): - results.append((name, StepResult( - skipped=True, - message=f"No .htaccess at {path} (run init first)", - ))) + results.append( + ( + name, + StepResult( + skipped=True, + message=f"No .htaccess at {path} (run init first)", + ), + ) + ) continue if not is_nssec_managed(path): - results.append((name, StepResult( - skipped=True, - message=f"Skipping unmanaged {path}", - ))) + results.append( + ( + name, + StepResult( + skipped=True, + message=f"Skipping unmanaged {path}", + ), + ) + ) continue current_ips = parse_htaccess_ips(path) if ip in current_ips: - results.append((name, StepResult( - skipped=True, - message=f"{ip} already in {path}", - ))) + results.append( + ( + name, + StepResult( + skipped=True, + message=f"{ip} already in {path}", + ), + ) + ) continue new_ips = current_ips + [ip] backup_file(path) content = _render_htaccess(target, new_ips) if not write_file(path, content): - results.append((name, StepResult( - success=False, - error=f"Failed to write {path}", - ))) + results.append( + ( + name, + StepResult( + success=False, + error=f"Failed to write {path}", + ), + ) + ) continue - results.append((name, StepResult( - message=f"Added {ip} to {path}", - ))) + results.append( + ( + name, + StepResult( + message=f"Added {ip} to {path}", + ), + ) + ) # Update cache with new IP cached = load_cached_ips() @@ -365,10 +412,15 @@ def remove_restricted_ip( List of (target_name, StepResult) tuples. """ if ip == "127.0.0.1": - return [("", StepResult( - success=False, - error="Cannot remove 127.0.0.1 (localhost must always be allowed)", - ))] + return [ + ( + "", + StepResult( + success=False, + error="Cannot remove 127.0.0.1 (localhost must always be allowed)", + ), + ) + ] targets = get_applicable_targets(server_type) results: list[tuple[str, StepResult]] = [] @@ -378,40 +430,65 @@ def remove_restricted_ip( name = target["name"] if not file_exists(path): - results.append((name, StepResult( - skipped=True, - message=f"No .htaccess at {path}", - ))) + results.append( + ( + name, + StepResult( + skipped=True, + message=f"No .htaccess at {path}", + ), + ) + ) continue if not is_nssec_managed(path): - results.append((name, StepResult( - skipped=True, - message=f"Skipping unmanaged {path}", - ))) + results.append( + ( + name, + StepResult( + skipped=True, + message=f"Skipping unmanaged {path}", + ), + ) + ) continue current_ips = parse_htaccess_ips(path) if ip not in current_ips: - results.append((name, StepResult( - skipped=True, - message=f"{ip} not found in {path}", - ))) + results.append( + ( + name, + StepResult( + skipped=True, + message=f"{ip} not found in {path}", + ), + ) + ) continue new_ips = [existing for existing in current_ips if existing != ip] backup_file(path) content = _render_htaccess(target, new_ips) if not write_file(path, content): - results.append((name, StepResult( - success=False, - error=f"Failed to write {path}", - ))) + results.append( + ( + name, + StepResult( + success=False, + error=f"Failed to write {path}", + ), + ) + ) continue - results.append((name, StepResult( - message=f"Removed {ip} from {path}", - ))) + results.append( + ( + name, + StepResult( + message=f"Removed {ip} from {path}", + ), + ) + ) # Update cache — remove this IP cached = load_cached_ips() @@ -439,10 +516,15 @@ def reapply_restrictions( """ cached_ips = load_cached_ips() if not cached_ips: - return [("", StepResult( - skipped=True, - message=f"No cached IPs found in {RESTRICT_CACHE_PATH} (run init first)", - ))] + return [ + ( + "", + StepResult( + skipped=True, + message=f"No cached IPs found in {RESTRICT_CACHE_PATH} (run init first)", + ), + ) + ] # Ensure 127.0.0.1 is first ips = ["127.0.0.1"] + [ip for ip in cached_ips if ip != "127.0.0.1"] @@ -451,10 +533,15 @@ def reapply_restrictions( results: list[tuple[str, StepResult]] = [] if not targets: - results.append(("", StepResult( - skipped=True, - message="No applicable targets found for this server type", - ))) + results.append( + ( + "", + StepResult( + skipped=True, + message="No applicable targets found for this server type", + ), + ) + ) return results for target in targets: @@ -462,9 +549,14 @@ def reapply_restrictions( name = target["name"] if dry_run: - results.append((name, StepResult( - message=f"Would write {path} with {len(ips)} cached IP(s)", - ))) + results.append( + ( + name, + StepResult( + message=f"Would write {path} with {len(ips)} cached IP(s)", + ), + ) + ) continue if file_exists(path): @@ -472,14 +564,24 @@ def reapply_restrictions( content = _render_htaccess(target, ips) if not write_file(path, content): - results.append((name, StepResult( - success=False, - error=f"Failed to write {path}", - ))) + results.append( + ( + name, + StepResult( + success=False, + error=f"Failed to write {path}", + ), + ) + ) continue - results.append((name, StepResult( - message=f"Restored {path} with {len(ips)} cached IP(s)", - ))) + results.append( + ( + name, + StepResult( + message=f"Restored {path} with {len(ips)} cached IP(s)", + ), + ) + ) return results diff --git a/src/nssec/modules/waf/status.py b/src/nssec/modules/waf/status.py index 72f02f1..25d31f9 100644 --- a/src/nssec/modules/waf/status.py +++ b/src/nssec/modules/waf/status.py @@ -5,7 +5,6 @@ import re from dataclasses import dataclass, field from pathlib import Path -from typing import Optional from nssec.modules.waf.config import ( CRS_RULES_REQUIRE_296, @@ -17,7 +16,6 @@ MODSEC_PACKAGE, NS_EXCLUSIONS_CONF, NS_EXCLUSIONS_HASH, - NS_EXCLUSIONS_VERSION, SECURITY2_CONF, SECURITY2_LOAD, ) @@ -27,25 +25,25 @@ class WafStatus: """Current state of ModSecurity / CRS.""" - apache_version: Optional[str] = None + apache_version: str | None = None apache_ppa: bool = False modsec_installed: bool = False modsec_enabled: bool = False - modsec_mode: Optional[str] = None + modsec_mode: str | None = None crs_installed: bool = False - crs_version: Optional[str] = None - crs_path: Optional[str] = None + crs_version: str | None = None + crs_path: str | None = None crs_setup_present: bool = False evasive_installed: bool = False evasive_enabled: bool = False exclusions_present: bool = False - exclusions_version: Optional[str] = None + exclusions_version: str | None = None exclusions_current: bool = False exclusions_included: bool = False crs_path_valid: bool = False exclusions_admin_ips: int = 0 exclusions_nodeping_ips: int = 0 - modsec_version: Optional[str] = None + modsec_version: str | None = None disabled_crs_rules: int = 0 audit_log_exists: bool = False recent_log_lines: list[str] = field(default_factory=list) @@ -66,7 +64,7 @@ def _pkg_installed(package: str) -> bool: return False -def _get_pkg_version(package: str) -> Optional[str]: +def _get_pkg_version(package: str) -> str | None: """Get the upstream version of an installed deb package.""" import subprocess @@ -96,7 +94,7 @@ def _is_ondrej_apache_ppa() -> bool: return any(glob.glob(p) for p in patterns) -def _read_file(path: str) -> Optional[str]: +def _read_file(path: str) -> str | None: try: return Path(path).read_text() except (OSError, PermissionError): @@ -114,7 +112,7 @@ def _tail_file(path: str, lines: int = 10) -> list[str]: return [] -def _parse_security2_crs_path(content: str) -> Optional[str]: +def _parse_security2_crs_path(content: str) -> str | None: """Extract the CRS path referenced in security2.conf.""" match = re.search(r"IncludeOptional\s+(\S+)/crs-setup\.conf", content) if match: @@ -122,7 +120,7 @@ def _parse_security2_crs_path(content: str) -> Optional[str]: return None -def _parse_exclusions_meta(content: str) -> tuple[Optional[str], Optional[str], int, int]: +def _parse_exclusions_meta(content: str) -> tuple[str | None, str | None, int, int]: """Parse exclusions file for version, hash, admin IP count, NodePing IP count.""" version = None template_hash = None @@ -171,8 +169,7 @@ def get_waf_status() -> WafStatus: rules_dir = Path(search_path) / "rules" if rules_dir.is_dir(): status.disabled_crs_rules = sum( - 1 for f in CRS_RULES_REQUIRE_296 - if (rules_dir / (f + ".disabled")).exists() + 1 for f in CRS_RULES_REQUIRE_296 if (rules_dir / (f + ".disabled")).exists() ) break @@ -203,9 +200,7 @@ def get_waf_status() -> WafStatus: if status.exclusions_present: excl_content = _read_file(NS_EXCLUSIONS_CONF) if excl_content: - version, deployed_hash, admin_count, np_count = _parse_exclusions_meta( - excl_content - ) + version, deployed_hash, admin_count, np_count = _parse_exclusions_meta(excl_content) status.exclusions_version = version # Use hash for drift detection — automatically catches any template change status.exclusions_current = deployed_hash == NS_EXCLUSIONS_HASH diff --git a/src/nssec/modules/waf/types.py b/src/nssec/modules/waf/types.py index 05a3739..be984fb 100644 --- a/src/nssec/modules/waf/types.py +++ b/src/nssec/modules/waf/types.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Optional @dataclass @@ -25,10 +24,10 @@ class PreflightResult: apache_running: bool = False modsec_installed: bool = False modsec_enabled: bool = False - modsec_mode: Optional[str] = None + modsec_mode: str | None = None crs_installed: bool = False - crs_version: Optional[str] = None - crs_path: Optional[str] = None + crs_version: str | None = None + crs_path: str | None = None security2_has_wildcard: bool = False security2_has_crs_load: bool = False errors: list[str] = field(default_factory=list) diff --git a/tests/conftest.py b/tests/conftest.py index 5c64131..43cd785 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ """Shared pytest fixtures for nssec tests.""" +from unittest.mock import patch + import pytest -from unittest.mock import MagicMock, patch @pytest.fixture @@ -18,11 +19,11 @@ def mock_file_ops(): Patches at the point of use (nssec.modules.waf) not definition (utils). """ - with patch("nssec.modules.waf.file_exists") as exists, \ - patch("nssec.modules.waf.read_file") as read, \ - patch("nssec.modules.waf.write_file") as write, \ - patch("nssec.modules.waf.backup_file") as backup, \ - patch("nssec.modules.waf.render") as render_mock: + with patch("nssec.modules.waf.file_exists") as exists, patch( + "nssec.modules.waf.read_file" + ) as read, patch("nssec.modules.waf.write_file") as write, patch( + "nssec.modules.waf.backup_file" + ) as backup, patch("nssec.modules.waf.render") as render_mock: exists.return_value = True read.return_value = "" write.return_value = True @@ -55,4 +56,5 @@ def mock_preflight(): def cli_runner(): """Click CLI test runner.""" from click.testing import CliRunner + return CliRunner() diff --git a/tests/unit/test_waf_commands.py b/tests/unit/test_waf_commands.py index 28bb500..6565407 100644 --- a/tests/unit/test_waf_commands.py +++ b/tests/unit/test_waf_commands.py @@ -1,7 +1,8 @@ """Tests for WAF CLI commands.""" +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import patch, MagicMock from click.testing import CliRunner from nssec.cli.waf_commands import waf @@ -90,9 +91,9 @@ class TestWafRemove: def test_disables_security2_module(self, runner): """Should disable security2 Apache module.""" - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.modules.waf.utils.file_exists", return_value=True), \ - patch("nssec.modules.waf.utils.run_cmd") as mock_run: + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.modules.waf.utils.file_exists", return_value=True + ), patch("nssec.modules.waf.utils.run_cmd") as mock_run: mock_run.return_value = ("", "", 0) result = runner.invoke(waf, ["remove", "-y"]) @@ -104,8 +105,9 @@ def test_disables_security2_module(self, runner): def test_skips_if_already_disabled(self, runner): """Should skip if module already disabled.""" - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.modules.waf.utils.file_exists", return_value=False): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.modules.waf.utils.file_exists", return_value=False + ): result = runner.invoke(waf, ["remove", "-y"]) assert result.exit_code == 0 @@ -121,8 +123,9 @@ def test_requires_root(self, runner): def test_prompts_without_yes_flag(self, runner): """Should prompt for confirmation without -y flag.""" - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.modules.waf.utils.file_exists", return_value=True): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.modules.waf.utils.file_exists", return_value=True + ): result = runner.invoke(waf, ["remove"], input="n\n") assert "Aborted" in result.output @@ -133,8 +136,9 @@ class TestWafAllowlistAdd: def test_adds_ip_to_allowlist(self, runner, mock_installer): """Should add IP to allowlist.""" - with patch("nssec.modules.waf.get_allowlisted_ips", return_value=[]), \ - patch("nssec.modules.waf.add_allowlisted_ip") as mock_add: + with patch("nssec.modules.waf.get_allowlisted_ips", return_value=[]), patch( + "nssec.modules.waf.add_allowlisted_ip" + ) as mock_add: mock_add.return_value = MagicMock(success=True, message="Added") result = runner.invoke(waf, ["allowlist", "add", "192.168.1.100", "-y"]) @@ -164,8 +168,9 @@ class TestWafAllowlistDelete: def test_removes_ip_from_allowlist(self, runner, mock_installer): """Should remove IP from allowlist.""" - with patch("nssec.modules.waf.get_allowlisted_ips", return_value=["192.168.1.100"]), \ - patch("nssec.modules.waf.remove_allowlisted_ip") as mock_remove: + with patch("nssec.modules.waf.get_allowlisted_ips", return_value=["192.168.1.100"]), patch( + "nssec.modules.waf.remove_allowlisted_ip" + ) as mock_remove: mock_remove.return_value = MagicMock(success=True, message="Removed") result = runner.invoke(waf, ["allowlist", "delete", "192.168.1.100", "-y"]) @@ -195,7 +200,9 @@ class TestWafAllowlistShow: def test_shows_allowlisted_ips(self, runner): """Should display allowlisted IPs.""" - with patch("nssec.modules.waf.get_allowlisted_ips", return_value=["192.168.1.100", "10.0.0.0/8"]): + with patch( + "nssec.modules.waf.get_allowlisted_ips", return_value=["192.168.1.100", "10.0.0.0/8"] + ): result = runner.invoke(waf, ["allowlist", "show"]) assert result.exit_code == 0 @@ -301,8 +308,9 @@ class TestWafEvasiveStatus: def test_shows_enabled_status(self, runner): """Should show enabled status when evasive is active.""" - with patch("nssec.modules.waf.utils.package_installed", return_value=True), \ - patch("nssec.modules.waf.utils.file_exists", return_value=True): + with patch("nssec.modules.waf.utils.package_installed", return_value=True), patch( + "nssec.modules.waf.utils.file_exists", return_value=True + ): result = runner.invoke(waf, ["evasive", "status"]) assert result.exit_code == 0 @@ -318,8 +326,9 @@ def test_shows_not_installed(self, runner): def test_default_subcommand_shows_status(self, runner): """Running 'waf evasive' without subcommand should show status.""" - with patch("nssec.modules.waf.utils.package_installed", return_value=True), \ - patch("nssec.modules.waf.utils.file_exists", return_value=True): + with patch("nssec.modules.waf.utils.package_installed", return_value=True), patch( + "nssec.modules.waf.utils.file_exists", return_value=True + ): result = runner.invoke(waf, ["evasive"]) assert result.exit_code == 0 @@ -434,8 +443,9 @@ def test_requires_root(self, runner, mock_installer): """Should fail if not root.""" mock_installer.preflight.return_value.is_root = False - with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.5"), \ - patch("nssec.modules.waf.utils.version_gte", return_value=False): + with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.5"), patch( + "nssec.modules.waf.utils.version_gte", return_value=False + ): result = runner.invoke(waf, ["update", "-y"]) assert result.exit_code == 1 @@ -443,8 +453,9 @@ def test_requires_root(self, runner, mock_installer): def test_shows_instructions_when_old(self, runner, mock_installer): """Should show Digitalwave repo instructions when ModSec < 2.9.6.""" - with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.5"), \ - patch("nssec.modules.waf.utils.version_gte", return_value=False): + with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.5"), patch( + "nssec.modules.waf.utils.version_gte", return_value=False + ): result = runner.invoke(waf, ["update", "-y"]) assert result.exit_code == 0 @@ -457,8 +468,9 @@ def test_nothing_to_do_when_current_no_disabled(self, runner, mock_installer): mock_installer._reenable_crs_rules.return_value = [] mock_installer.preflight.return_value.crs_path = "/etc/modsecurity/crs" - with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.7"), \ - patch("nssec.modules.waf.utils.version_gte", return_value=True): + with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.7"), patch( + "nssec.modules.waf.utils.version_gte", return_value=True + ): result = runner.invoke(waf, ["update", "-y"]) assert result.exit_code == 0 @@ -479,8 +491,9 @@ def test_reenables_rules_after_upgrade(self, runner, mock_installer): reload_result.message = "Apache reloaded" mock_installer.reload_apache.return_value = reload_result - with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.7"), \ - patch("nssec.modules.waf.utils.version_gte", return_value=True): + with patch("nssec.modules.waf.utils.detect_modsec_version", return_value="2.9.7"), patch( + "nssec.modules.waf.utils.version_gte", return_value=True + ): result = runner.invoke(waf, ["update", "-y"]) assert result.exit_code == 0 diff --git a/tests/unit/test_waf_module.py b/tests/unit/test_waf_module.py index 703b88c..4e1749a 100644 --- a/tests/unit/test_waf_module.py +++ b/tests/unit/test_waf_module.py @@ -1,8 +1,7 @@ """Tests for WAF module functions.""" -import pytest -from pathlib import Path -from unittest.mock import patch, MagicMock, call, ANY +from unittest.mock import MagicMock, patch + from jinja2 import Template @@ -304,8 +303,9 @@ def test_enables_evasive_module(self, mock_file_ops): """Should run a2enmod evasive when enabling.""" from nssec.modules.waf import ModSecurityInstaller - with patch("nssec.modules.waf.package_installed", return_value=True), \ - patch("nssec.modules.waf.run_cmd", return_value=("", "", 0)) as mock_run: + with patch("nssec.modules.waf.package_installed", return_value=True), patch( + "nssec.modules.waf.run_cmd", return_value=("", "", 0) + ) as mock_run: mock_file_ops["exists"].return_value = False # not currently enabled installer = ModSecurityInstaller() result = installer.set_evasive_state(enable=True) @@ -318,8 +318,9 @@ def test_disables_evasive_module(self, mock_file_ops): """Should run a2dismod evasive when disabling.""" from nssec.modules.waf import ModSecurityInstaller - with patch("nssec.modules.waf.package_installed", return_value=True), \ - patch("nssec.modules.waf.run_cmd", return_value=("", "", 0)) as mock_run: + with patch("nssec.modules.waf.package_installed", return_value=True), patch( + "nssec.modules.waf.run_cmd", return_value=("", "", 0) + ) as mock_run: mock_file_ops["exists"].return_value = True # currently enabled installer = ModSecurityInstaller() result = installer.set_evasive_state(enable=False) @@ -367,8 +368,9 @@ def test_dry_run_does_not_change_state(self, mock_file_ops): """Should not run commands in dry run mode.""" from nssec.modules.waf import ModSecurityInstaller - with patch("nssec.modules.waf.package_installed", return_value=True), \ - patch("nssec.modules.waf.run_cmd") as mock_run: + with patch("nssec.modules.waf.package_installed", return_value=True), patch( + "nssec.modules.waf.run_cmd" + ) as mock_run: mock_file_ops["exists"].return_value = False installer = ModSecurityInstaller(dry_run=True) result = installer.set_evasive_state(enable=True) @@ -381,8 +383,9 @@ def test_returns_error_on_command_failure(self, mock_file_ops): """Should return error if a2enmod/a2dismod fails.""" from nssec.modules.waf import ModSecurityInstaller - with patch("nssec.modules.waf.package_installed", return_value=True), \ - patch("nssec.modules.waf.run_cmd", return_value=("", "error", 1)): + with patch("nssec.modules.waf.package_installed", return_value=True), patch( + "nssec.modules.waf.run_cmd", return_value=("", "error", 1) + ): mock_file_ops["exists"].return_value = False installer = ModSecurityInstaller() result = installer.set_evasive_state(enable=True) @@ -399,8 +402,9 @@ def test_enable_mode_does_not_toggle_evasive(self, mock_file_ops): from nssec.modules.waf import ModSecurityInstaller mock_file_ops["read"].return_value = "SecRuleEngine DetectionOnly\n" - with patch("nssec.modules.waf.package_installed", return_value=True), \ - patch("nssec.modules.waf.run_cmd", return_value=("", "", 0)) as mock_run: + with patch("nssec.modules.waf.package_installed", return_value=True), patch( + "nssec.modules.waf.run_cmd", return_value=("", "", 0) + ) as mock_run: mock_file_ops["exists"].return_value = False installer = ModSecurityInstaller() result = installer.set_mode("On") @@ -415,8 +419,9 @@ def test_detect_mode_does_not_toggle_evasive(self, mock_file_ops): from nssec.modules.waf import ModSecurityInstaller mock_file_ops["read"].return_value = "SecRuleEngine On\n" - with patch("nssec.modules.waf.package_installed", return_value=True), \ - patch("nssec.modules.waf.run_cmd", return_value=("", "", 0)) as mock_run: + with patch("nssec.modules.waf.package_installed", return_value=True), patch( + "nssec.modules.waf.run_cmd", return_value=("", "", 0) + ) as mock_run: mock_file_ops["exists"].return_value = True installer = ModSecurityInstaller() result = installer.set_mode("DetectionOnly") @@ -471,8 +476,11 @@ def test_passes_nodeping_ips_to_template(self, mock_file_ops): assert result.success render_call = mock_file_ops["render"].call_args - assert render_call[1].get("nodeping_ips") == nodeping or \ - nodeping in render_call[0] if render_call[0] else False + assert ( + render_call[1].get("nodeping_ips") == nodeping or nodeping in render_call[0] + if render_call[0] + else False + ) def test_defaults_to_empty_nodeping_list(self, mock_file_ops): """Should default to empty list when no nodeping_ips provided.""" @@ -484,8 +492,9 @@ def test_defaults_to_empty_nodeping_list(self, mock_file_ops): assert result.success render_call = mock_file_ops["render"].call_args # nodeping_ips should be an empty list - assert render_call[1].get("nodeping_ips") == [] or \ - render_call.kwargs.get("nodeping_ips") == [] + assert ( + render_call[1].get("nodeping_ips") == [] or render_call.kwargs.get("nodeping_ips") == [] + ) def test_passes_both_admin_and_nodeping_ips(self, mock_file_ops): """Should pass both admin_ips and nodeping_ips to template.""" @@ -554,7 +563,7 @@ def test_nodeping_rules_bypass_crs(self): ) # Find the NodePing rule section lines = rendered.split("\n") - nodeping_rule_lines = [l for l in lines if "1000201" in l] + nodeping_rule_lines = [line for line in lines if "1000201" in line] assert len(nodeping_rule_lines) > 0 assert "ruleRemoveByTag=OWASP_CRS" in rendered @@ -574,8 +583,9 @@ def test_updates_crs_setup_when_v4_present(self, mock_file_ops): pf.can_proceed = True installer._preflight = pf - with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.7"), \ - patch("nssec.modules.waf.version_gte", return_value=True): + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.7"), patch( + "nssec.modules.waf.version_gte", return_value=True + ): result = installer.install_crs_v4() assert result.skipped @@ -597,8 +607,9 @@ def test_skips_setup_update_renders_template(self, mock_file_ops): pf.can_proceed = True installer._preflight = pf - with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.7"), \ - patch("nssec.modules.waf.version_gte", return_value=True): + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.7"), patch( + "nssec.modules.waf.version_gte", return_value=True + ): installer.install_crs_v4() # render should have been called for crs-setup.conf @@ -612,8 +623,9 @@ def test_clears_preflight_after_download(self, mock_file_ops): """Should clear _preflight cache after successful CRS download.""" from nssec.modules.waf import ModSecurityInstaller - with patch("nssec.modules.waf.run_cmd", return_value=("", "", 0)), \ - patch("nssec.modules.waf.Path"): + with patch("nssec.modules.waf.run_cmd", return_value=("", "", 0)), patch( + "nssec.modules.waf.Path" + ): installer = ModSecurityInstaller() pf = MagicMock() pf.crs_installed = False @@ -654,8 +666,9 @@ def test_disables_rules_on_old_modsec(self, tmp_path, mock_file_ops): rule_file = rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf" rule_file.write_text("# rule content") - with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), \ - patch("nssec.modules.waf.version_gte", side_effect=lambda v, t: False): + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), patch( + "nssec.modules.waf.version_gte", side_effect=lambda v, t: False + ): installer = ModSecurityInstaller() disabled = installer._disable_incompatible_crs_rules(str(tmp_path)) @@ -672,8 +685,9 @@ def test_skips_on_new_modsec(self, tmp_path, mock_file_ops): rule_file = rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf" rule_file.write_text("# rule content") - with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.6"), \ - patch("nssec.modules.waf.version_gte", side_effect=lambda v, t: True): + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.6"), patch( + "nssec.modules.waf.version_gte", side_effect=lambda v, t: True + ): installer = ModSecurityInstaller() disabled = installer._disable_incompatible_crs_rules(str(tmp_path)) @@ -690,8 +704,9 @@ def test_skips_already_disabled(self, tmp_path, mock_file_ops): (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf").write_text("# rule") (rules_dir / "REQUEST-922-MULTIPART-ATTACK.conf.disabled").write_text("# disabled") - with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), \ - patch("nssec.modules.waf.version_gte", side_effect=lambda v, t: False): + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), patch( + "nssec.modules.waf.version_gte", side_effect=lambda v, t: False + ): installer = ModSecurityInstaller() disabled = installer._disable_incompatible_crs_rules(str(tmp_path)) @@ -705,8 +720,9 @@ def test_handles_missing_rule_file(self, tmp_path, mock_file_ops): rules_dir.mkdir() # No rule files created - with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), \ - patch("nssec.modules.waf.version_gte", side_effect=lambda v, t: False): + with patch("nssec.modules.waf.detect_modsec_version", return_value="2.9.5"), patch( + "nssec.modules.waf.version_gte", side_effect=lambda v, t: False + ): installer = ModSecurityInstaller() disabled = installer._disable_incompatible_crs_rules(str(tmp_path)) @@ -758,5 +774,3 @@ def test_returns_empty_when_nothing_disabled(self, tmp_path, mock_file_ops): reenabled = installer._reenable_crs_rules(str(tmp_path)) assert reenabled == [] - - diff --git a/tests/unit/test_waf_restrict.py b/tests/unit/test_waf_restrict.py index 37bfbb7..bdd491a 100644 --- a/tests/unit/test_waf_restrict.py +++ b/tests/unit/test_waf_restrict.py @@ -1,8 +1,8 @@ """Tests for WAF restrict module functions.""" import json -import pytest -from unittest.mock import patch, MagicMock +from unittest.mock import patch + from jinja2 import Template @@ -44,7 +44,6 @@ class TestSaveCachedIps: def test_saves_ips_to_json(self): """Should write IPs as JSON to cache file.""" from nssec.modules.waf.restrict import save_cached_ips - from nssec.modules.waf.config import RESTRICT_CACHE_PATH with patch("nssec.modules.waf.restrict.write_file", return_value=True) as mock_write: result = save_cached_ips(["127.0.0.1", "10.0.0.1"]) @@ -219,8 +218,8 @@ class TestIsNssecManaged: def test_returns_true_for_managed_file(self): """Should return True when marker is present.""" - from nssec.modules.waf.restrict import is_nssec_managed from nssec.modules.waf.config import RESTRICT_MANAGED_MARKER + from nssec.modules.waf.restrict import is_nssec_managed content = f"{RESTRICT_MANAGED_MARKER}\n\n\n" with patch("nssec.modules.waf.restrict.read_file", return_value=content): @@ -252,7 +251,9 @@ class TestInitRestrictions: @patch("nssec.modules.waf.restrict.file_exists", return_value=False) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_creates_htaccess_files(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect): + def test_creates_htaccess_files( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect + ): """Should create .htaccess files for applicable targets.""" from nssec.modules.waf.restrict import init_restrictions @@ -270,7 +271,9 @@ def test_creates_htaccess_files(self, mock_render, mock_isdir, mock_exists, mock @patch("nssec.modules.waf.restrict.file_exists", return_value=True) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_overwrites_unmanaged_files(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect): + def test_overwrites_unmanaged_files( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect + ): """Should overwrite existing unmanaged .htaccess files.""" from nssec.modules.waf.restrict import init_restrictions @@ -299,8 +302,9 @@ def test_no_targets_returns_skip(self): """Should return skip result when no targets apply.""" from nssec.modules.waf.restrict import init_restrictions - with patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=[]), \ - patch("nssec.modules.waf.restrict.is_directory", return_value=False): + with patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=[]), patch( + "nssec.modules.waf.restrict.is_directory", return_value=False + ): results = init_restrictions("core", ["192.168.1.100"]) assert len(results) == 1 @@ -313,7 +317,9 @@ def test_no_targets_returns_skip(self): @patch("nssec.modules.waf.restrict.file_exists", return_value=False) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_always_includes_localhost(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect): + def test_always_includes_localhost( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect + ): """Should always include 127.0.0.1 in rendered IPs.""" from nssec.modules.waf.restrict import init_restrictions @@ -331,7 +337,9 @@ def test_always_includes_localhost(self, mock_render, mock_isdir, mock_exists, m @patch("nssec.modules.waf.restrict.file_exists", return_value=True) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_merges_existing_ips_from_all_targets(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect): + def test_merges_existing_ips_from_all_targets( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect + ): """Should merge existing IPs from all targets into every file.""" from nssec.modules.waf.restrict import init_restrictions @@ -355,7 +363,9 @@ def test_merges_existing_ips_from_all_targets(self, mock_render, mock_isdir, moc @patch("nssec.modules.waf.restrict.file_exists", return_value=False) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_merges_ips_from_cache(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect): + def test_merges_ips_from_cache( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save, mock_collect + ): """Should merge IPs from cache file into new .htaccess files.""" from nssec.modules.waf.restrict import init_restrictions @@ -376,7 +386,9 @@ def test_merges_ips_from_cache(self, mock_render, mock_isdir, mock_exists, mock_ @patch("nssec.modules.waf.restrict.file_exists", return_value=False) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_saves_ips_to_cache_after_init(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_collect): + def test_saves_ips_to_cache_after_init( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_collect + ): """Should save IPs to cache file after successful init.""" from nssec.modules.waf.restrict import init_restrictions @@ -399,8 +411,9 @@ def test_collects_ips_from_existing_htaccess(self, mock_isdir, mock_cache): from nssec.modules.waf.restrict import collect_existing_ips content = "Require ip 127.0.0.1\nRequire ip 10.0.0.5\n" - with patch("nssec.modules.waf.restrict.file_exists", return_value=True), \ - patch("nssec.modules.waf.restrict.read_file", return_value=content): + with patch("nssec.modules.waf.restrict.file_exists", return_value=True), patch( + "nssec.modules.waf.restrict.read_file", return_value=content + ): ips = collect_existing_ips("core") assert "10.0.0.5" in ips @@ -411,8 +424,9 @@ def test_collects_ips_from_cache(self, mock_isdir): """Should include IPs from the cache file.""" from nssec.modules.waf.restrict import collect_existing_ips - with patch("nssec.modules.waf.restrict.file_exists", return_value=False), \ - patch("nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1", "172.16.0.1"]): + with patch("nssec.modules.waf.restrict.file_exists", return_value=False), patch( + "nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1", "172.16.0.1"] + ): ips = collect_existing_ips("core") assert "172.16.0.1" in ips @@ -435,8 +449,9 @@ def test_collects_legacy_allow_from_ips(self, mock_isdir, mock_cache): from nssec.modules.waf.restrict import collect_existing_ips content = "Order deny,allow\nDeny from all\nAllow from 10.0.0.5\n" - with patch("nssec.modules.waf.restrict.file_exists", return_value=True), \ - patch("nssec.modules.waf.restrict.read_file", return_value=content): + with patch("nssec.modules.waf.restrict.file_exists", return_value=True), patch( + "nssec.modules.waf.restrict.read_file", return_value=content + ): ips = collect_existing_ips("core") assert "10.0.0.5" in ips @@ -451,12 +466,13 @@ class TestInitRestrictionsNoMerge: @patch("nssec.modules.waf.restrict.file_exists", return_value=True) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_does_not_merge_existing_when_disabled(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save): + def test_does_not_merge_existing_when_disabled( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save + ): """Should not merge existing IPs when merge_existing=False.""" from nssec.modules.waf.restrict import init_restrictions - results = init_restrictions("core", ["192.168.1.100"], - merge_existing=False) + results = init_restrictions("core", ["192.168.1.100"], merge_existing=False) for name, result in results: assert result.success @@ -474,12 +490,13 @@ def test_does_not_merge_existing_when_disabled(self, mock_render, mock_isdir, mo @patch("nssec.modules.waf.restrict.file_exists", return_value=False) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_does_not_merge_cache_when_disabled(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save): + def test_does_not_merge_cache_when_disabled( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write, mock_save + ): """Should not merge cache IPs when merge_existing=False.""" from nssec.modules.waf.restrict import init_restrictions - results = init_restrictions("core", ["192.168.1.100"], - merge_existing=False) + init_restrictions("core", ["192.168.1.100"], merge_existing=False) # Render should only include provided IPs + localhost for call_args in mock_render.call_args_list: @@ -500,7 +517,18 @@ class TestAddRestrictedIp: @patch("nssec.modules.waf.restrict.parse_htaccess_ips", return_value=["127.0.0.1"]) @patch("nssec.modules.waf.restrict.file_exists", return_value=True) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) - def test_adds_ip_to_managed_files(self, mock_isdir, mock_exists, mock_parse, mock_managed, mock_render, mock_backup, mock_write, mock_load, mock_save): + def test_adds_ip_to_managed_files( + self, + mock_isdir, + mock_exists, + mock_parse, + mock_managed, + mock_render, + mock_backup, + mock_write, + mock_load, + mock_save, + ): """Should add IP to all managed .htaccess files and update cache.""" from nssec.modules.waf.restrict import add_restricted_ip @@ -516,7 +544,9 @@ def test_adds_ip_to_managed_files(self, mock_isdir, mock_exists, mock_parse, moc assert "192.168.1.100" in saved_ips @patch("nssec.modules.waf.restrict.is_nssec_managed", return_value=True) - @patch("nssec.modules.waf.restrict.parse_htaccess_ips", return_value=["127.0.0.1", "192.168.1.100"]) + @patch( + "nssec.modules.waf.restrict.parse_htaccess_ips", return_value=["127.0.0.1", "192.168.1.100"] + ) @patch("nssec.modules.waf.restrict.file_exists", return_value=True) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) def test_skips_duplicate_ip(self, mock_isdir, mock_exists, mock_parse, mock_managed): @@ -569,15 +599,30 @@ def test_refuses_to_remove_localhost(self): assert "Cannot remove 127.0.0.1" in results[0][1].error @patch("nssec.modules.waf.restrict.save_cached_ips") - @patch("nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1", "192.168.1.100"]) + @patch( + "nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1", "192.168.1.100"] + ) @patch("nssec.modules.waf.restrict.write_file", return_value=True) @patch("nssec.modules.waf.restrict.backup_file") @patch("nssec.modules.waf.restrict.render", return_value="rendered") @patch("nssec.modules.waf.restrict.is_nssec_managed", return_value=True) - @patch("nssec.modules.waf.restrict.parse_htaccess_ips", return_value=["127.0.0.1", "192.168.1.100"]) + @patch( + "nssec.modules.waf.restrict.parse_htaccess_ips", return_value=["127.0.0.1", "192.168.1.100"] + ) @patch("nssec.modules.waf.restrict.file_exists", return_value=True) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) - def test_removes_user_added_ip(self, mock_isdir, mock_exists, mock_parse, mock_managed, mock_render, mock_backup, mock_write, mock_load, mock_save): + def test_removes_user_added_ip( + self, + mock_isdir, + mock_exists, + mock_parse, + mock_managed, + mock_render, + mock_backup, + mock_write, + mock_load, + mock_save, + ): """Should remove a user-added IP from managed files and update cache.""" from nssec.modules.waf.restrict import remove_restricted_ip @@ -695,7 +740,9 @@ def test_returns_skip_when_no_cache(self): @patch("nssec.modules.waf.restrict.file_exists", return_value=False) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_restores_from_cache(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write): + def test_restores_from_cache( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write + ): """Should re-create .htaccess files from cached IPs.""" from nssec.modules.waf.restrict import reapply_restrictions @@ -719,9 +766,9 @@ def test_dry_run_does_not_write(self, mock_isdir): from nssec.modules.waf.restrict import reapply_restrictions cached = json.dumps({"ips": ["127.0.0.1", "10.0.0.1"]}) - with patch("nssec.modules.waf.restrict.read_file", return_value=cached), \ - patch("nssec.modules.waf.restrict.file_exists", return_value=False), \ - patch("nssec.modules.waf.restrict.write_file") as mock_write: + with patch("nssec.modules.waf.restrict.read_file", return_value=cached), patch( + "nssec.modules.waf.restrict.file_exists", return_value=False + ), patch("nssec.modules.waf.restrict.write_file") as mock_write: results = reapply_restrictions("core", dry_run=True) mock_write.assert_not_called() @@ -734,7 +781,9 @@ def test_dry_run_does_not_write(self, mock_isdir): @patch("nssec.modules.waf.restrict.file_exists", return_value=True) @patch("nssec.modules.waf.restrict.is_directory", return_value=True) @patch("nssec.modules.waf.restrict.render", return_value="rendered") - def test_backs_up_existing_before_overwrite(self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write): + def test_backs_up_existing_before_overwrite( + self, mock_render, mock_isdir, mock_exists, mock_backup, mock_write + ): """Should backup existing files before restoring from cache.""" from nssec.modules.waf.restrict import reapply_restrictions diff --git a/tests/unit/test_waf_restrict_commands.py b/tests/unit/test_waf_restrict_commands.py index facde43..1a4d6cb 100644 --- a/tests/unit/test_waf_restrict_commands.py +++ b/tests/unit/test_waf_restrict_commands.py @@ -1,7 +1,8 @@ """Tests for WAF restrict CLI commands.""" +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import patch, MagicMock from click.testing import CliRunner from nssec.cli.waf_commands import waf @@ -34,8 +35,9 @@ def test_shows_status_table(self, runner): "ips": [], }, ] - with patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.get_restrict_status", return_value=statuses): + with patch("nssec.core.server_types.detect_server_type") as mock_detect, patch( + "nssec.modules.waf.restrict.get_restrict_status", return_value=statuses + ): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "show"]) @@ -45,8 +47,9 @@ def test_shows_status_table(self, runner): def test_shows_empty_message(self, runner): """Should show message when no targets apply.""" - with patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.get_restrict_status", return_value=[]): + with patch("nssec.core.server_types.detect_server_type") as mock_detect, patch( + "nssec.modules.waf.restrict.get_restrict_status", return_value=[] + ): mock_detect.return_value = MagicMock(value="unknown") result = runner.invoke(waf, ["restrict", "show"]) @@ -64,8 +67,9 @@ def test_default_subcommand_shows_status(self, runner): "ips": ["127.0.0.1"], }, ] - with patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.get_restrict_status", return_value=statuses): + with patch("nssec.core.server_types.detect_server_type") as mock_detect, patch( + "nssec.modules.waf.restrict.get_restrict_status", return_value=statuses + ): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict"]) @@ -83,8 +87,9 @@ def test_lists_ips_from_first_managed_file(self, runner): "ips": ["127.0.0.1", "10.0.0.1"], }, ] - with patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.get_restrict_status", return_value=statuses): + with patch("nssec.core.server_types.detect_server_type") as mock_detect, patch( + "nssec.modules.waf.restrict.get_restrict_status", return_value=statuses + ): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "show"]) @@ -113,11 +118,13 @@ def test_creates_htaccess_files(self, runner): ("ns-api", StepResult(message="Created file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=[]), \ - patch("nssec.modules.waf.restrict.init_restrictions", return_value=mock_results) as mock_init, \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.collect_existing_ips", return_value=[] + ), patch( + "nssec.modules.waf.restrict.init_restrictions", return_value=mock_results + ) as mock_init, patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "init", "--ip", "192.168.1.100", "-y"]) @@ -128,9 +135,9 @@ def test_creates_htaccess_files(self, runner): def test_validates_ip_address(self, runner): """Should reject invalid IP addresses.""" - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=[]): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=[]): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "init", "--ip", "not-an-ip", "-y"]) @@ -145,10 +152,11 @@ def test_dry_run(self, runner): ("SiPbx Admin UI", StepResult(message="Would create file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=[]), \ - patch("nssec.modules.waf.restrict.init_restrictions", return_value=mock_results): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.collect_existing_ips", return_value=[] + ), patch("nssec.modules.waf.restrict.init_restrictions", return_value=mock_results): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "init", "--ip", "10.0.0.1", "--dry-run"]) @@ -163,11 +171,13 @@ def test_accepts_cidr_notation(self, runner): ("SiPbx Admin UI", StepResult(message="Created file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=[]), \ - patch("nssec.modules.waf.restrict.init_restrictions", return_value=mock_results), \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.collect_existing_ips", return_value=[] + ), patch("nssec.modules.waf.restrict.init_restrictions", return_value=mock_results), patch( + "nssec.modules.waf.utils.run_cmd", return_value=("", "", 0) + ): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "init", "--ip", "10.0.0.0/8", "-y"]) @@ -181,15 +191,19 @@ def test_shows_existing_ips_and_keeps_by_default(self, runner): ("SiPbx Admin UI", StepResult(message="Created file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=["10.0.0.5", "172.16.0.1"]), \ - patch("nssec.modules.waf.restrict.init_restrictions", return_value=mock_results) as mock_init, \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.collect_existing_ips", + return_value=["10.0.0.5", "172.16.0.1"], + ), patch( + "nssec.modules.waf.restrict.init_restrictions", return_value=mock_results + ) as mock_init, patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): mock_detect.return_value = MagicMock(value="core") # Confirm keep=Yes, create=Yes, reload=Yes - result = runner.invoke(waf, ["restrict", "init", "--ip", "192.168.1.100"], - input="y\ny\ny\n") + result = runner.invoke( + waf, ["restrict", "init", "--ip", "192.168.1.100"], input="y\ny\ny\n" + ) assert result.exit_code == 0 assert "10.0.0.5" in result.output @@ -206,15 +220,18 @@ def test_shows_existing_ips_and_overwrites_on_no(self, runner): ("SiPbx Admin UI", StepResult(message="Created file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=["10.0.0.5"]), \ - patch("nssec.modules.waf.restrict.init_restrictions", return_value=mock_results) as mock_init, \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.collect_existing_ips", return_value=["10.0.0.5"] + ), patch( + "nssec.modules.waf.restrict.init_restrictions", return_value=mock_results + ) as mock_init, patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): mock_detect.return_value = MagicMock(value="core") # Confirm keep=No, create=Yes, reload=Yes - result = runner.invoke(waf, ["restrict", "init", "--ip", "192.168.1.100"], - input="n\ny\ny\n") + result = runner.invoke( + waf, ["restrict", "init", "--ip", "192.168.1.100"], input="n\ny\ny\n" + ) assert result.exit_code == 0 assert "Overwriting" in result.output @@ -230,11 +247,13 @@ def test_yes_flag_keeps_existing_ips_by_default(self, runner): ("SiPbx Admin UI", StepResult(message="Created file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.collect_existing_ips", return_value=["10.0.0.5"]), \ - patch("nssec.modules.waf.restrict.init_restrictions", return_value=mock_results) as mock_init, \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.collect_existing_ips", return_value=["10.0.0.5"] + ), patch( + "nssec.modules.waf.restrict.init_restrictions", return_value=mock_results + ) as mock_init, patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "init", "--ip", "192.168.1.100", "-y"]) @@ -263,10 +282,11 @@ def test_adds_ip_to_managed_files(self, runner): ("SiPbx Admin UI", StepResult(message="Added 192.168.1.100")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.add_restricted_ip", return_value=mock_results) as mock_add, \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.add_restricted_ip", return_value=mock_results + ) as mock_add, patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "add", "192.168.1.100", "-y"]) @@ -275,8 +295,9 @@ def test_adds_ip_to_managed_files(self, runner): def test_validates_ip_address(self, runner): """Should reject invalid IP addresses.""" - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect: + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect: mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "add", "not-valid", "-y"]) @@ -303,10 +324,11 @@ def test_removes_ip_from_managed_files(self, runner): ("SiPbx Admin UI", StepResult(message="Removed 192.168.1.100")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.remove_restricted_ip", return_value=mock_results) as mock_remove, \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.remove_restricted_ip", return_value=mock_results + ) as mock_remove, patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "remove", "192.168.1.100", "-y"]) @@ -318,12 +340,20 @@ def test_blocks_localhost_removal(self, runner): from nssec.modules.waf.types import StepResult mock_results = [ - ("", StepResult(success=False, error="Cannot remove 127.0.0.1 (localhost must always be allowed)")), + ( + "", + StepResult( + success=False, + error="Cannot remove 127.0.0.1 (localhost must always be allowed)", + ), + ), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.remove_restricted_ip", return_value=mock_results): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.remove_restricted_ip", return_value=mock_results + ): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "remove", "127.0.0.1", "-y"]) @@ -350,11 +380,13 @@ def test_restores_from_cache(self, runner): ("SiPbx Admin UI", StepResult(message="Restored file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1", "10.0.0.1"]), \ - patch("nssec.modules.waf.restrict.reapply_restrictions", return_value=mock_results) as mock_reapply, \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1", "10.0.0.1"] + ), patch( + "nssec.modules.waf.restrict.reapply_restrictions", return_value=mock_results + ) as mock_reapply, patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "reapply", "-y"]) @@ -369,11 +401,13 @@ def test_shows_cached_ips(self, runner): ("SiPbx Admin UI", StepResult(message="Restored file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1", "10.0.0.1"]), \ - patch("nssec.modules.waf.restrict.reapply_restrictions", return_value=mock_results), \ - patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1", "10.0.0.1"] + ), patch( + "nssec.modules.waf.restrict.reapply_restrictions", return_value=mock_results + ), patch("nssec.modules.waf.utils.run_cmd", return_value=("", "", 0)): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "reapply", "-y"]) @@ -387,10 +421,11 @@ def test_dry_run(self, runner): ("SiPbx Admin UI", StepResult(message="Would write file")), ] - with patch("nssec.core.ssh.is_root", return_value=True), \ - patch("nssec.core.server_types.detect_server_type") as mock_detect, \ - patch("nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1"]), \ - patch("nssec.modules.waf.restrict.reapply_restrictions", return_value=mock_results): + with patch("nssec.core.ssh.is_root", return_value=True), patch( + "nssec.core.server_types.detect_server_type" + ) as mock_detect, patch( + "nssec.modules.waf.restrict.load_cached_ips", return_value=["127.0.0.1"] + ), patch("nssec.modules.waf.restrict.reapply_restrictions", return_value=mock_results): mock_detect.return_value = MagicMock(value="core") result = runner.invoke(waf, ["restrict", "reapply", "--dry-run"]) diff --git a/tests/unit/test_waf_status.py b/tests/unit/test_waf_status.py index e04bb15..d880e59 100644 --- a/tests/unit/test_waf_status.py +++ b/tests/unit/test_waf_status.py @@ -1,7 +1,5 @@ """Tests for WAF status reporting.""" -import pytest -from unittest.mock import patch, MagicMock from jinja2 import Template diff --git a/tests/unit/test_waf_utils.py b/tests/unit/test_waf_utils.py index 2f80b19..efdb9fc 100644 --- a/tests/unit/test_waf_utils.py +++ b/tests/unit/test_waf_utils.py @@ -2,8 +2,6 @@ from unittest.mock import patch -import pytest - class TestVersionGte: """Tests for version_gte helper.""" @@ -158,9 +156,9 @@ def capture_write(path, content): written["content"] = content return True - with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), \ - patch("nssec.modules.waf.utils.backup_file"), \ - patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), patch( + "nssec.modules.waf.utils.backup_file" + ), patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): result = append_crs_to_security2("/etc/modsecurity/crs") assert result is True @@ -179,9 +177,9 @@ def capture_write(path, content): written["content"] = content return True - with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), \ - patch("nssec.modules.waf.utils.backup_file"), \ - patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), patch( + "nssec.modules.waf.utils.backup_file" + ), patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): append_crs_to_security2("/etc/modsecurity/crs") content = written["content"] @@ -205,18 +203,19 @@ def capture_write(path, content): written["content"] = content return True - with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), \ - patch("nssec.modules.waf.utils.backup_file"), \ - patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), patch( + "nssec.modules.waf.utils.backup_file" + ), patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): append_crs_to_security2("/etc/modsecurity/crs") content = written["content"] # The modsecurity/*.conf line should remain active (not commented) active_lines = [ - l.strip() for l in content.splitlines() - if not l.strip().startswith("#") and l.strip() + line.strip() + for line in content.splitlines() + if not line.strip().startswith("#") and line.strip() ] - assert any("/etc/modsecurity/*.conf" in l for l in active_lines) + assert any("/etc/modsecurity/*.conf" in line for line in active_lines) def test_does_not_comment_out_already_commented_lines(self): """Should not double-comment already commented CRS lines.""" @@ -235,9 +234,9 @@ def capture_write(path, content): written["content"] = content return True - with patch("nssec.modules.waf.utils.read_file", return_value=content_with_comment), \ - patch("nssec.modules.waf.utils.backup_file"), \ - patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): + with patch("nssec.modules.waf.utils.read_file", return_value=content_with_comment), patch( + "nssec.modules.waf.utils.backup_file" + ), patch("nssec.modules.waf.utils.write_file", side_effect=capture_write): append_crs_to_security2("/etc/modsecurity/crs") content = written["content"] @@ -247,9 +246,9 @@ def capture_write(path, content): def test_returns_false_on_write_failure(self): from nssec.modules.waf.utils import append_crs_to_security2 - with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), \ - patch("nssec.modules.waf.utils.backup_file"), \ - patch("nssec.modules.waf.utils.write_file", return_value=False): + with patch("nssec.modules.waf.utils.read_file", return_value=DEBIAN_DEFAULT_SEC2), patch( + "nssec.modules.waf.utils.backup_file" + ), patch("nssec.modules.waf.utils.write_file", return_value=False): result = append_crs_to_security2("/etc/modsecurity/crs") assert result is False