|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+#!/usr/bin/env python3
|
|
|
2
|
+"""
|
|
|
3
|
+test-firewall-syslog.py — Send FortiGate-style syslog test messages to Wazuh manager port 514/UDP.
|
|
|
4
|
+
|
|
|
5
|
+Usage:
|
|
|
6
|
+ python3 scripts/test-firewall-syslog.py [--host HOST] [--port PORT] [--src-ip IP] [--scenario SCENARIO]
|
|
|
7
|
+ python3 scripts/test-firewall-syslog.py --via-docker # send from inside container (avoids NAT)
|
|
|
8
|
+
|
|
|
9
|
+Examples:
|
|
|
10
|
+ python3 scripts/test-firewall-syslog.py
|
|
|
11
|
+ python3 scripts/test-firewall-syslog.py --via-docker
|
|
|
12
|
+ python3 scripts/test-firewall-syslog.py --host 192.168.1.10 --src-ip 172.16.22.253
|
|
|
13
|
+ python3 scripts/test-firewall-syslog.py --scenario rdp
|
|
|
14
|
+ python3 scripts/test-firewall-syslog.py --scenario all --delay 0.5
|
|
|
15
|
+"""
|
|
|
16
|
+
|
|
|
17
|
+import argparse
|
|
|
18
|
+import socket
|
|
|
19
|
+import subprocess
|
|
|
20
|
+import time
|
|
|
21
|
+import datetime
|
|
|
22
|
+
|
|
|
23
|
+# ── Default target ────────────────────────────────────────────────────────────
|
|
|
24
|
+DEFAULT_HOST = "127.0.0.1"
|
|
|
25
|
+DEFAULT_PORT = 514
|
|
|
26
|
+DEFAULT_SRC_IP = "172.16.22.253" # must be in allowed-ips list
|
|
|
27
|
+WAZUH_CONTAINER = "wazuh-single-wazuh.manager-1"
|
|
|
28
|
+
|
|
|
29
|
+# ── FortiGate syslog scenarios ────────────────────────────────────────────────
|
|
|
30
|
+def _ts():
|
|
|
31
|
+ now = datetime.datetime.now(datetime.timezone.utc)
|
|
|
32
|
+ return now.strftime("%Y-%m-%d"), now.strftime("%H:%M:%S")
|
|
|
33
|
+
|
|
|
34
|
+def make_fortigate_log(src_ip, **fields):
|
|
|
35
|
+ """Build a FortiGate v6 syslog line (key=value pairs)."""
|
|
|
36
|
+ date, time_ = _ts()
|
|
|
37
|
+ base = {
|
|
|
38
|
+ "date": date,
|
|
|
39
|
+ "time": time_,
|
|
|
40
|
+ "devname": "FG-TEST-FW",
|
|
|
41
|
+ "devid": "FGT60E0000000001",
|
|
|
42
|
+ "logid": "0000000013",
|
|
|
43
|
+ "type": "traffic",
|
|
|
44
|
+ "subtype": "forward",
|
|
|
45
|
+ "level": "notice",
|
|
|
46
|
+ "vd": "root",
|
|
|
47
|
+ "srcip": src_ip,
|
|
|
48
|
+ "srcport": "54321",
|
|
|
49
|
+ "srcintf": "port1",
|
|
|
50
|
+ "dstip": "10.0.0.1",
|
|
|
51
|
+ "dstport": "80",
|
|
|
52
|
+ "dstintf": "port2",
|
|
|
53
|
+ "policyid": "1",
|
|
|
54
|
+ "proto": "6",
|
|
|
55
|
+ "action": "accept",
|
|
|
56
|
+ "service": "HTTP",
|
|
|
57
|
+ "duration": "1",
|
|
|
58
|
+ "sentbyte": "1024",
|
|
|
59
|
+ "rcvdbyte": "2048",
|
|
|
60
|
+ }
|
|
|
61
|
+ base.update(fields)
|
|
|
62
|
+ parts = []
|
|
|
63
|
+ for k, v in base.items():
|
|
|
64
|
+ if " " in str(v) or "=" in str(v):
|
|
|
65
|
+ parts.append(f'{k}="{v}"')
|
|
|
66
|
+ else:
|
|
|
67
|
+ parts.append(f"{k}={v}")
|
|
|
68
|
+ return " ".join(parts)
|
|
|
69
|
+
|
|
|
70
|
+SCENARIOS = {
|
|
|
71
|
+ # A2-01: RDP traffic allowed
|
|
|
72
|
+ "rdp": {
|
|
|
73
|
+ "description": "A2-01 — RDP (3389) traffic allowed",
|
|
|
74
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
75
|
+ logid="0000000013", type="traffic", subtype="forward",
|
|
|
76
|
+ dstport="3389", action="accept", service="RDP",
|
|
|
77
|
+ ),
|
|
|
78
|
+ },
|
|
|
79
|
+ # A2-02: Admin password change
|
|
|
80
|
+ "password_change": {
|
|
|
81
|
+ "description": "A2-02 — Admin password changed",
|
|
|
82
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
83
|
+ logid="0100032001", type="event", subtype="system",
|
|
|
84
|
+ level="information", action="password-change",
|
|
|
85
|
+ user="admin", msg="Change admin password",
|
|
|
86
|
+ ),
|
|
|
87
|
+ },
|
|
|
88
|
+ # A2-03: New admin account created
|
|
|
89
|
+ "create_admin": {
|
|
|
90
|
+ "description": "A2-03 — New admin account created",
|
|
|
91
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
92
|
+ logid="0100032002", type="event", subtype="system",
|
|
|
93
|
+ level="information", action="create-admin",
|
|
|
94
|
+ user="newadmin", msg="Create admin account",
|
|
|
95
|
+ ),
|
|
|
96
|
+ },
|
|
|
97
|
+ # A2-04: Alerting disabled via config change
|
|
|
98
|
+ "disable_alert": {
|
|
|
99
|
+ "description": "A2-04 — Alerting disabled (config-change)",
|
|
|
100
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
101
|
+ logid="0100032003", type="event", subtype="system",
|
|
|
102
|
+ level="warning", action="config-change",
|
|
|
103
|
+ config_value="disable", msg="Email alerting disabled",
|
|
|
104
|
+ ),
|
|
|
105
|
+ },
|
|
|
106
|
+ # A2-05: Config file downloaded
|
|
|
107
|
+ "download_config": {
|
|
|
108
|
+ "description": "A2-05 — Firewall config downloaded",
|
|
|
109
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
110
|
+ logid="0100032004", type="event", subtype="system",
|
|
|
111
|
+ level="warning", action="download-config",
|
|
|
112
|
+ user="admin", msg="Configuration backup downloaded",
|
|
|
113
|
+ ),
|
|
|
114
|
+ },
|
|
|
115
|
+ # A2-06: Multiple critical IPS signatures
|
|
|
116
|
+ "ips_critical": {
|
|
|
117
|
+ "description": "A2-06 — Multiple critical IPS signatures",
|
|
|
118
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
119
|
+ logid="0419016384", type="utm", subtype="ips",
|
|
|
120
|
+ level="critical", action="dropped",
|
|
|
121
|
+ attack="Multiple.Critical.Signatures", severity="critical",
|
|
|
122
|
+ srcip="203.0.113.42", dstip="172.16.1.10",
|
|
|
123
|
+ ),
|
|
|
124
|
+ },
|
|
|
125
|
+ # A2-07: TCP port scan
|
|
|
126
|
+ "port_scan": {
|
|
|
127
|
+ "description": "A2-07 — TCP port scan detected",
|
|
|
128
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
129
|
+ logid="0419016385", type="utm", subtype="anomaly",
|
|
|
130
|
+ level="critical", action="dropped",
|
|
|
131
|
+ attack="TCP.Port.Scan", severity="critical",
|
|
|
132
|
+ srcip="203.0.113.99", dstip="172.16.0.0",
|
|
|
133
|
+ ),
|
|
|
134
|
+ },
|
|
|
135
|
+ # A2-08: IPS IOC-based IP indicator
|
|
|
136
|
+ "ioc_ip": {
|
|
|
137
|
+ "description": "A2-08 — IPS IOC-based IP indicator",
|
|
|
138
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
139
|
+ logid="0419016386", type="utm", subtype="ips",
|
|
|
140
|
+ level="critical", action="blocked",
|
|
|
141
|
+ ioc_type="ip", ioc_value="198.51.100.42",
|
|
|
142
|
+ srcip="198.51.100.42", dstip="172.16.1.5",
|
|
|
143
|
+ ),
|
|
|
144
|
+ },
|
|
|
145
|
+ # Generic traffic allow
|
|
|
146
|
+ "traffic_allow": {
|
|
|
147
|
+ "description": "Generic — Traffic allowed (HTTP)",
|
|
|
148
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
149
|
+ dstport="80", action="accept", service="HTTP",
|
|
|
150
|
+ ),
|
|
|
151
|
+ },
|
|
|
152
|
+ # Generic traffic deny
|
|
|
153
|
+ "traffic_deny": {
|
|
|
154
|
+ "description": "Generic — Traffic denied",
|
|
|
155
|
+ "log": lambda src: make_fortigate_log(src,
|
|
|
156
|
+ dstport="22", action="deny", service="SSH",
|
|
|
157
|
+ ),
|
|
|
158
|
+ },
|
|
|
159
|
+}
|
|
|
160
|
+
|
|
|
161
|
+
|
|
|
162
|
+def _build_syslog_packet(message, src_ip, facility=16, severity=6):
|
|
|
163
|
+ pri = (facility * 8) + severity
|
|
|
164
|
+ _, time_ = _ts()
|
|
|
165
|
+ month_day = datetime.datetime.now(datetime.timezone.utc).strftime("%b %d").replace(" 0", " ")
|
|
|
166
|
+ return f"<{pri}>{month_day} {time_} {src_ip} {message}"
|
|
|
167
|
+
|
|
|
168
|
+
|
|
|
169
|
+def send_syslog_udp(host, port, message, src_ip):
|
|
|
170
|
+ """Send directly via UDP socket (source IP will appear as Docker bridge on local test)."""
|
|
|
171
|
+ packet = _build_syslog_packet(message, src_ip)
|
|
|
172
|
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
|
173
|
+ try:
|
|
|
174
|
+ sock.sendto(packet.encode("utf-8"), (host, port))
|
|
|
175
|
+ return True
|
|
|
176
|
+ except OSError:
|
|
|
177
|
+ return False
|
|
|
178
|
+ finally:
|
|
|
179
|
+ sock.close()
|
|
|
180
|
+
|
|
|
181
|
+
|
|
|
182
|
+def send_syslog_via_docker(message, src_ip, port):
|
|
|
183
|
+ """Send UDP from inside the Wazuh container using /dev/udp — source IP is container's own IP."""
|
|
|
184
|
+ packet = _build_syslog_packet(message, src_ip)
|
|
|
185
|
+ py_cmd = (
|
|
|
186
|
+ f"import socket; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); "
|
|
|
187
|
+ f"s.sendto({repr(packet.encode())}, ('127.0.0.1',{port})); s.close()"
|
|
|
188
|
+ )
|
|
|
189
|
+ result = subprocess.run(
|
|
|
190
|
+ ["docker", "exec", WAZUH_CONTAINER, "python3", "-c", py_cmd],
|
|
|
191
|
+ capture_output=True, timeout=5,
|
|
|
192
|
+ )
|
|
|
193
|
+ return result.returncode == 0
|
|
|
194
|
+
|
|
|
195
|
+
|
|
|
196
|
+def run_scenario(name, info, host, port, src_ip, via_docker):
|
|
|
197
|
+ log = info["log"](src_ip)
|
|
|
198
|
+ if via_docker:
|
|
|
199
|
+ ok = send_syslog_via_docker(log, src_ip, port)
|
|
|
200
|
+ else:
|
|
|
201
|
+ ok = send_syslog_udp(host, port, log, src_ip)
|
|
|
202
|
+ status = "✓" if ok else "✗"
|
|
|
203
|
+ print(f" {status} {name:20s} {info['description']}")
|
|
|
204
|
+ return ok
|
|
|
205
|
+
|
|
|
206
|
+
|
|
|
207
|
+def main():
|
|
|
208
|
+ parser = argparse.ArgumentParser(description="Send FortiGate syslog test messages to Wazuh")
|
|
|
209
|
+ parser.add_argument("--host", default=DEFAULT_HOST, help=f"Wazuh manager host (default: {DEFAULT_HOST})")
|
|
|
210
|
+ parser.add_argument("--port", default=DEFAULT_PORT, type=int, help=f"Syslog UDP port (default: {DEFAULT_PORT})")
|
|
|
211
|
+ parser.add_argument("--src-ip", default=DEFAULT_SRC_IP, help=f"Simulated firewall source IP (default: {DEFAULT_SRC_IP})")
|
|
|
212
|
+ parser.add_argument("--scenario", default="all",
|
|
|
213
|
+ choices=list(SCENARIOS.keys()) + ["all"],
|
|
|
214
|
+ help="Scenario to send (default: all)")
|
|
|
215
|
+ parser.add_argument("--delay", default=0.2, type=float, help="Delay between messages in seconds (default: 0.2)")
|
|
|
216
|
+ parser.add_argument("--repeat", default=1, type=int, help="Number of times to repeat each scenario (default: 1)")
|
|
|
217
|
+ parser.add_argument("--via-docker", action="store_true",
|
|
|
218
|
+ help="Send from inside the Wazuh container (avoids Docker NAT source-IP rewrite)")
|
|
|
219
|
+ args = parser.parse_args()
|
|
|
220
|
+
|
|
|
221
|
+ mode = f"docker exec {WAZUH_CONTAINER}" if args.via_docker else f"{args.host}:{args.port}"
|
|
|
222
|
+ print(f"Wazuh syslog test — mode: {mode} src-ip: {args.src_ip}")
|
|
|
223
|
+ print(f"{'─' * 65}")
|
|
|
224
|
+
|
|
|
225
|
+ to_run = list(SCENARIOS.items()) if args.scenario == "all" else [(args.scenario, SCENARIOS[args.scenario])]
|
|
|
226
|
+ total = ok_count = 0
|
|
|
227
|
+
|
|
|
228
|
+ for _ in range(args.repeat):
|
|
|
229
|
+ for name, info in to_run:
|
|
|
230
|
+ success = run_scenario(name, info, args.host, args.port, args.src_ip, args.via_docker)
|
|
|
231
|
+ total += 1
|
|
|
232
|
+ ok_count += int(success)
|
|
|
233
|
+ if args.delay:
|
|
|
234
|
+ time.sleep(args.delay)
|
|
|
235
|
+
|
|
|
236
|
+ print(f"{'─' * 65}")
|
|
|
237
|
+ print(f"Sent {ok_count}/{total} messages")
|
|
|
238
|
+ print()
|
|
|
239
|
+ print("Verify with:")
|
|
|
240
|
+ print(f" docker exec {WAZUH_CONTAINER} tail -f /var/ossec/logs/archives/archives.log | grep {args.src_ip}")
|
|
|
241
|
+
|
|
|
242
|
+
|
|
|
243
|
+if __name__ == "__main__":
|
|
|
244
|
+ main()
|