2024-12-21 09:08:22 +01:00
|
|
|
import os
|
|
|
|
|
import subprocess
|
|
|
|
|
import logging
|
2025-01-03 13:15:27 +01:00
|
|
|
from pathlib import Path
|
2025-02-28 11:21:33 +01:00
|
|
|
import shutil
|
|
|
|
|
import filecmp
|
|
|
|
|
import time
|
2024-12-21 09:08:22 +01:00
|
|
|
|
2025-02-28 11:21:33 +01:00
|
|
|
# --- Configuration ---
|
|
|
|
|
LOG_LEVEL = logging.INFO # DEBUG, INFO, WARNING, ERROR
|
|
|
|
|
WAF_DIR = Path(os.getenv("WAF_DIR", "waf_patterns/haproxy")).resolve()
|
|
|
|
|
HAPROXY_WAF_DIR = Path(os.getenv("HAPROXY_WAF_DIR", "/etc/haproxy/waf/")).resolve()
|
|
|
|
|
HAPROXY_CONF = Path(os.getenv("HAPROXY_CONF", "/etc/haproxy/haproxy.cfg")).resolve()
|
|
|
|
|
BACKUP_DIR = Path(os.getenv("BACKUP_DIR", "/etc/haproxy/waf_backup/")).resolve()
|
2025-01-03 13:15:27 +01:00
|
|
|
|
|
|
|
|
# HAProxy WAF configuration snippet
|
|
|
|
|
WAF_CONFIG_SNIPPET = """
|
2025-02-28 11:21:33 +01:00
|
|
|
# WAF and Bot Protection (Generated by import_haproxy_waf.py)
|
2025-01-03 13:15:27 +01:00
|
|
|
frontend http-in
|
|
|
|
|
bind *:80
|
2025-02-28 11:21:33 +01:00
|
|
|
mode http
|
|
|
|
|
option httplog
|
|
|
|
|
# WAF and Bot Protection ACLs and Rules
|
|
|
|
|
# Include generated ACL files
|
|
|
|
|
include /etc/haproxy/waf/*.acl
|
2025-01-03 13:15:27 +01:00
|
|
|
"""
|
2025-02-28 11:21:33 +01:00
|
|
|
# --- Logging Setup ---
|
|
|
|
|
logging.basicConfig(level=LOG_LEVEL, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2024-12-21 09:08:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def copy_waf_files():
|
2025-02-28 11:21:33 +01:00
|
|
|
"""Copies WAF files, handling existing files, creating backups."""
|
|
|
|
|
logger.info("Copying HAProxy WAF patterns...")
|
2025-01-16 14:02:19 +01:00
|
|
|
|
2025-02-28 11:21:33 +01:00
|
|
|
HAPROXY_WAF_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
BACKUP_DIR.mkdir(parents=True, exist_ok=True) # Ensure backup dir exists
|
2024-12-21 09:08:22 +01:00
|
|
|
|
2025-02-28 11:21:33 +01:00
|
|
|
for acl_file in WAF_DIR.glob("*.acl"): # Find all .acl files
|
|
|
|
|
dst_path = HAPROXY_WAF_DIR / acl_file.name
|
2025-01-03 13:15:27 +01:00
|
|
|
|
2025-02-28 11:21:33 +01:00
|
|
|
try:
|
|
|
|
|
if dst_path.exists():
|
|
|
|
|
# Compare and backup if different
|
|
|
|
|
if filecmp.cmp(acl_file, dst_path, shallow=False):
|
|
|
|
|
logger.info(f"Skipping {acl_file.name} (identical file exists).")
|
|
|
|
|
continue
|
|
|
|
|
# Different file exists: backup
|
|
|
|
|
backup_path = BACKUP_DIR / f"{dst_path.name}.{int(time.time())}"
|
|
|
|
|
logger.warning(f"Existing {dst_path.name} differs. Backing up to {backup_path}")
|
|
|
|
|
shutil.copy2(dst_path, backup_path) # Backup old file
|
2024-12-21 09:08:22 +01:00
|
|
|
|
2025-02-28 11:21:33 +01:00
|
|
|
# Copy the (new or updated) file
|
|
|
|
|
shutil.copy2(acl_file, dst_path)
|
|
|
|
|
logger.info(f"Copied {acl_file.name} to {dst_path}")
|
|
|
|
|
|
|
|
|
|
except OSError as e:
|
|
|
|
|
logger.error(f"Error copying {acl_file.name}: {e}")
|
|
|
|
|
raise
|
2025-01-16 14:02:19 +01:00
|
|
|
|
2025-02-28 11:21:33 +01:00
|
|
|
|
|
|
|
|
def update_haproxy_conf():
|
|
|
|
|
"""Ensures the include statement is in haproxy.cfg, avoiding duplicates."""
|
|
|
|
|
logger.info("Checking HAProxy configuration for WAF include...")
|
2024-12-21 09:08:22 +01:00
|
|
|
|
2025-01-03 13:15:27 +01:00
|
|
|
try:
|
|
|
|
|
with open(HAPROXY_CONF, "r") as f:
|
2025-02-28 11:21:33 +01:00
|
|
|
config_lines = f.readlines()
|
|
|
|
|
|
|
|
|
|
# Check if the *exact* snippet is already present. We'll check for the
|
|
|
|
|
# key parts of the snippet to be more robust.
|
|
|
|
|
snippet_present = False
|
|
|
|
|
for line in config_lines:
|
|
|
|
|
if "include /etc/haproxy/waf/*.acl" in line:
|
|
|
|
|
snippet_present = True
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not snippet_present:
|
|
|
|
|
# Find the 'frontend http-in' section
|
|
|
|
|
frontend_start = -1
|
|
|
|
|
for i, line in enumerate(config_lines):
|
|
|
|
|
if line.strip().startswith("frontend http-in"):
|
|
|
|
|
frontend_start = i
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if frontend_start == -1:
|
|
|
|
|
logger.warning("No 'frontend http-in' section found. Appending to end of file.")
|
|
|
|
|
with open(HAPROXY_CONF, "a") as f:
|
|
|
|
|
f.write(f"\n{WAF_CONFIG_SNIPPET}\n")
|
|
|
|
|
logger.info(f"Added WAF configuration snippet to {HAPROXY_CONF}")
|
|
|
|
|
else:
|
|
|
|
|
# Find the end of the 'frontend http-in' section
|
|
|
|
|
frontend_end = -1
|
|
|
|
|
for i in range(frontend_start + 1, len(config_lines)):
|
|
|
|
|
if line.strip() == "" or not line.startswith(" "): # Check it is part of the config
|
|
|
|
|
frontend_end = i
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if frontend_end == -1:
|
|
|
|
|
frontend_end = len(config_lines) # End of file
|
|
|
|
|
|
|
|
|
|
# Insert the include statement *within* the frontend section.
|
|
|
|
|
config_lines.insert(frontend_end, " include /etc/haproxy/waf/*.acl\n")
|
|
|
|
|
|
|
|
|
|
# Write the modified configuration back to the file
|
|
|
|
|
with open(HAPROXY_CONF, "w") as f:
|
|
|
|
|
f.writelines(config_lines)
|
|
|
|
|
logger.info(f"Added WAF include to 'frontend http-in' section in {HAPROXY_CONF}")
|
2025-01-03 13:15:27 +01:00
|
|
|
else:
|
2025-02-28 11:21:33 +01:00
|
|
|
logger.info("WAF include statement already present.")
|
|
|
|
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
logger.error(f"HAProxy configuration file not found: {HAPROXY_CONF}")
|
|
|
|
|
raise
|
|
|
|
|
except OSError as e:
|
|
|
|
|
logger.error(f"Error updating HAProxy configuration: {e}")
|
2025-01-03 13:15:27 +01:00
|
|
|
raise
|
|
|
|
|
|
2024-12-21 09:08:22 +01:00
|
|
|
|
|
|
|
|
def reload_haproxy():
|
2025-02-28 11:21:33 +01:00
|
|
|
"""Tests the HAProxy configuration and reloads if valid."""
|
|
|
|
|
logger.info("Reloading HAProxy...")
|
2025-01-03 13:15:27 +01:00
|
|
|
|
|
|
|
|
try:
|
2025-02-28 11:21:33 +01:00
|
|
|
# Test configuration
|
|
|
|
|
result = subprocess.run(["haproxy", "-c", "-f", str(HAPROXY_CONF)],
|
|
|
|
|
capture_output=True, text=True, check=True)
|
|
|
|
|
logger.info(f"HAProxy configuration test successful:\n{result.stdout}")
|
|
|
|
|
|
2025-01-03 13:15:27 +01:00
|
|
|
|
|
|
|
|
# Reload HAProxy
|
2025-02-28 11:21:33 +01:00
|
|
|
result = subprocess.run(["systemctl", "reload", "haproxy"],
|
|
|
|
|
capture_output=True, text=True, check=True)
|
|
|
|
|
logger.info("HAProxy reloaded.")
|
|
|
|
|
|
|
|
|
|
|
2025-01-03 13:15:27 +01:00
|
|
|
except subprocess.CalledProcessError as e:
|
2025-02-28 11:21:33 +01:00
|
|
|
logger.error(f"HAProxy command failed: {e.cmd} - Return code: {e.returncode}")
|
|
|
|
|
logger.error(f"Stdout: {e.stdout}")
|
|
|
|
|
logger.error(f"Stderr: {e.stderr}")
|
|
|
|
|
raise # Re-raise to signal failure
|
2025-01-03 13:15:27 +01:00
|
|
|
except FileNotFoundError:
|
2025-02-28 11:21:33 +01:00
|
|
|
logger.error("'haproxy' or 'systemctl' command not found. Is HAProxy/systemd installed?")
|
2025-01-03 13:15:27 +01:00
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2025-02-28 11:21:33 +01:00
|
|
|
"""Main function."""
|
2025-01-03 13:15:27 +01:00
|
|
|
try:
|
|
|
|
|
copy_waf_files()
|
|
|
|
|
update_haproxy_conf()
|
|
|
|
|
reload_haproxy()
|
2025-02-28 11:21:33 +01:00
|
|
|
logger.info("HAProxy WAF configuration updated successfully.")
|
2025-01-03 13:15:27 +01:00
|
|
|
except Exception as e:
|
2025-02-28 11:21:33 +01:00
|
|
|
logger.critical(f"Script failed: {e}")
|
2025-01-03 13:15:27 +01:00
|
|
|
exit(1)
|
|
|
|
|
|
2024-12-21 09:08:22 +01:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2025-02-28 11:21:33 +01:00
|
|
|
main()
|