2
2
import subprocess
3
3
import logging
4
4
from pathlib import Path
5
+ import shutil
6
+ import filecmp
7
+ import time
5
8
6
- # Configure logging
7
- logging .basicConfig (
8
- level = logging .INFO ,
9
- format = "%(asctime)s - %(levelname)s - %(message)s" ,
10
- handlers = [logging .StreamHandler ()],
11
- )
12
-
13
- # Constants (configurable via environment variables)
14
- WAF_DIR = Path (os .getenv ("WAF_DIR" , "waf_patterns/haproxy" )).resolve () # Source directory for WAF files
15
- HAPROXY_WAF_DIR = Path (os .getenv ("HAPROXY_WAF_DIR" , "/etc/haproxy/waf/" )).resolve () # Target directory
16
- HAPROXY_CONF = Path (os .getenv ("HAPROXY_CONF" , "/etc/haproxy/haproxy.cfg" )).resolve () # HAProxy config file
9
+ # --- Configuration ---
10
+ LOG_LEVEL = logging .INFO # DEBUG, INFO, WARNING, ERROR
11
+ WAF_DIR = Path (os .getenv ("WAF_DIR" , "waf_patterns/haproxy" )).resolve ()
12
+ HAPROXY_WAF_DIR = Path (os .getenv ("HAPROXY_WAF_DIR" , "/etc/haproxy/waf/" )).resolve ()
13
+ HAPROXY_CONF = Path (os .getenv ("HAPROXY_CONF" , "/etc/haproxy/haproxy.cfg" )).resolve ()
14
+ BACKUP_DIR = Path (os .getenv ("BACKUP_DIR" , "/etc/haproxy/waf_backup/" )).resolve ()
17
15
18
16
# HAProxy WAF configuration snippet
19
17
WAF_CONFIG_SNIPPET = """
20
- # WAF and Bot Protection
18
+ # WAF and Bot Protection (Generated by import_haproxy_waf.py)
21
19
frontend http-in
22
20
bind *:80
23
- default_backend web_backend
24
- acl bad_bot hdr_sub(User-Agent) -i waf/bots.acl
25
- acl waf_attack path_reg waf/waf.acl
26
- http-request deny if bad_bot
27
- http-request deny if waf_attack
21
+ mode http
22
+ option httplog
23
+ # WAF and Bot Protection ACLs and Rules
24
+ # Include generated ACL files
25
+ include /etc/haproxy/waf/*.acl
28
26
"""
27
+ # --- Logging Setup ---
28
+ logging .basicConfig (level = LOG_LEVEL , format = "%(asctime)s - %(levelname)s - %(message)s" )
29
+ logger = logging .getLogger (__name__ )
30
+
29
31
30
32
31
33
def copy_waf_files ():
32
- """
33
- Copy HAProxy WAF ACL files to the target directory.
34
+ """Copies WAF files, handling existing files, creating backups."""
35
+ logger . info ( "Copying HAProxy WAF patterns..." )
34
36
35
- Raises:
36
- Exception: If there is an error copying files.
37
- """
38
- logging .info ("Copying HAProxy WAF patterns..." )
37
+ HAPROXY_WAF_DIR .mkdir (parents = True , exist_ok = True )
38
+ BACKUP_DIR .mkdir (parents = True , exist_ok = True ) # Ensure backup dir exists
39
39
40
- try :
41
- # Ensure the target directory exists
42
- HAPROXY_WAF_DIR .mkdir (parents = True , exist_ok = True )
43
- logging .info (f"[+] Created or verified directory: { HAPROXY_WAF_DIR } " )
44
-
45
- # Copy ACL files
46
- for file in ["bots.acl" , "waf.acl" ]:
47
- src_path = WAF_DIR / file
48
- dst_path = HAPROXY_WAF_DIR / file
49
-
50
- if not src_path .exists ():
51
- logging .warning (f"[!] { file } not found in { WAF_DIR } " )
52
- continue
53
-
54
- try :
55
- subprocess .run (["cp" , str (src_path ), str (dst_path )], check = True )
56
- logging .info (f"[+] { file } copied to { HAPROXY_WAF_DIR } " )
57
- except subprocess .CalledProcessError as e :
58
- logging .error (f"[!] Failed to copy { file } : { e } " )
59
- raise
60
- except Exception as e :
61
- logging .error (f"[!] Error copying WAF files: { e } " )
62
- raise
40
+ for acl_file in WAF_DIR .glob ("*.acl" ): # Find all .acl files
41
+ dst_path = HAPROXY_WAF_DIR / acl_file .name
63
42
43
+ try :
44
+ if dst_path .exists ():
45
+ # Compare and backup if different
46
+ if filecmp .cmp (acl_file , dst_path , shallow = False ):
47
+ logger .info (f"Skipping { acl_file .name } (identical file exists)." )
48
+ continue
49
+ # Different file exists: backup
50
+ backup_path = BACKUP_DIR / f"{ dst_path .name } .{ int (time .time ())} "
51
+ logger .warning (f"Existing { dst_path .name } differs. Backing up to { backup_path } " )
52
+ shutil .copy2 (dst_path , backup_path ) # Backup old file
64
53
65
- def update_haproxy_conf ():
66
- """
67
- Ensure the WAF configuration snippet is included in haproxy.cfg.
54
+ # Copy the (new or updated) file
55
+ shutil .copy2 (acl_file , dst_path )
56
+ logger .info (f"Copied { acl_file .name } to { dst_path } " )
57
+
58
+ except OSError as e :
59
+ logger .error (f"Error copying { acl_file .name } : { e } " )
60
+ raise
68
61
69
- Raises:
70
- Exception: If there is an error updating the HAProxy configuration.
71
- """
72
- logging .info ("Ensuring WAF patterns are included in haproxy.cfg ..." )
62
+
63
+ def update_haproxy_conf ():
64
+ """Ensures the include statement is in haproxy.cfg, avoiding duplicates."""
65
+ logger .info ("Checking HAProxy configuration for WAF include ..." )
73
66
74
67
try :
75
- # Read the current configuration
76
68
with open (HAPROXY_CONF , "r" ) as f :
77
- config = f .read ()
78
-
79
- # Append WAF configuration snippet if not present
80
- if WAF_CONFIG_SNIPPET .strip () not in config :
81
- logging .info ("Adding WAF rules to haproxy.cfg..." )
82
- with open (HAPROXY_CONF , "a" ) as f :
83
- f .write (WAF_CONFIG_SNIPPET )
84
- logging .info ("[+] WAF rules added to haproxy.cfg." )
69
+ config_lines = f .readlines ()
70
+
71
+ # Check if the *exact* snippet is already present. We'll check for the
72
+ # key parts of the snippet to be more robust.
73
+ snippet_present = False
74
+ for line in config_lines :
75
+ if "include /etc/haproxy/waf/*.acl" in line :
76
+ snippet_present = True
77
+ break
78
+
79
+ if not snippet_present :
80
+ # Find the 'frontend http-in' section
81
+ frontend_start = - 1
82
+ for i , line in enumerate (config_lines ):
83
+ if line .strip ().startswith ("frontend http-in" ):
84
+ frontend_start = i
85
+ break
86
+
87
+ if frontend_start == - 1 :
88
+ logger .warning ("No 'frontend http-in' section found. Appending to end of file." )
89
+ with open (HAPROXY_CONF , "a" ) as f :
90
+ f .write (f"\n { WAF_CONFIG_SNIPPET } \n " )
91
+ logger .info (f"Added WAF configuration snippet to { HAPROXY_CONF } " )
92
+ else :
93
+ # Find the end of the 'frontend http-in' section
94
+ frontend_end = - 1
95
+ for i in range (frontend_start + 1 , len (config_lines )):
96
+ if line .strip () == "" or not line .startswith (" " ): # Check it is part of the config
97
+ frontend_end = i
98
+ break
99
+
100
+
101
+ if frontend_end == - 1 :
102
+ frontend_end = len (config_lines ) # End of file
103
+
104
+ # Insert the include statement *within* the frontend section.
105
+ config_lines .insert (frontend_end , " include /etc/haproxy/waf/*.acl\n " )
106
+
107
+ # Write the modified configuration back to the file
108
+ with open (HAPROXY_CONF , "w" ) as f :
109
+ f .writelines (config_lines )
110
+ logger .info (f"Added WAF include to 'frontend http-in' section in { HAPROXY_CONF } " )
85
111
else :
86
- logging .info ("WAF patterns already included in haproxy.cfg." )
87
- except Exception as e :
88
- logging .error (f"[!] Error updating HAProxy configuration: { e } " )
112
+ logger .info ("WAF include statement already present." )
113
+
114
+ except FileNotFoundError :
115
+ logger .error (f"HAProxy configuration file not found: { HAPROXY_CONF } " )
116
+ raise
117
+ except OSError as e :
118
+ logger .error (f"Error updating HAProxy configuration: { e } " )
89
119
raise
90
120
91
121
92
122
def reload_haproxy ():
93
- """
94
- Reload HAProxy to apply the new WAF rules.
95
-
96
- Raises:
97
- Exception: If there is an error reloading HAProxy.
98
- """
99
- logging .info ("Testing HAProxy configuration..." )
123
+ """Tests the HAProxy configuration and reloads if valid."""
124
+ logger .info ("Reloading HAProxy..." )
100
125
101
126
try :
102
- # Test HAProxy configuration
103
- subprocess .run (["haproxy" , "-c" , "-f" , str (HAPROXY_CONF )], check = True )
104
- logging .info ("[+] HAProxy configuration test passed." )
127
+ # Test configuration
128
+ result = subprocess .run (["haproxy" , "-c" , "-f" , str (HAPROXY_CONF )],
129
+ capture_output = True , text = True , check = True )
130
+ logger .info (f"HAProxy configuration test successful:\n { result .stdout } " )
131
+
105
132
106
133
# Reload HAProxy
107
- subprocess .run (["systemctl" , "reload" , "haproxy" ], check = True )
108
- logging .info ("[+] HAProxy reloaded successfully." )
134
+ result = subprocess .run (["systemctl" , "reload" , "haproxy" ],
135
+ capture_output = True , text = True , check = True )
136
+ logger .info ("HAProxy reloaded." )
137
+
138
+
109
139
except subprocess .CalledProcessError as e :
110
- logging .error (f"[!] HAProxy configuration test failed: { e } " )
111
- raise
140
+ logger .error (f"HAProxy command failed: { e .cmd } - Return code: { e .returncode } " )
141
+ logger .error (f"Stdout: { e .stdout } " )
142
+ logger .error (f"Stderr: { e .stderr } " )
143
+ raise # Re-raise to signal failure
112
144
except FileNotFoundError :
113
- logging .error ("[!] 'haproxy' or 'systemctl' command not found. Are you on a supported system?" )
114
- raise
115
- except Exception as e :
116
- logging .error (f"[!] Error reloading HAProxy: { e } " )
145
+ logger .error ("'haproxy' or 'systemctl' command not found. Is HAProxy/systemd installed?" )
117
146
raise
118
147
119
148
120
149
def main ():
121
- """
122
- Main function to execute the script.
123
- """
150
+ """Main function."""
124
151
try :
125
152
copy_waf_files ()
126
153
update_haproxy_conf ()
127
154
reload_haproxy ()
128
- logging .info ("[✔] HAProxy configured with latest WAF rules ." )
155
+ logger .info ("HAProxy WAF configuration updated successfully ." )
129
156
except Exception as e :
130
- logging .critical (f"[!] Script failed: { e } " )
157
+ logger .critical (f"Script failed: { e } " )
131
158
exit (1 )
132
159
133
160
134
161
if __name__ == "__main__" :
135
- main ()
162
+ main ()
0 commit comments