"""Synthetic security-log generator (diverse, non-repeating). Ported from Data/files/gen_logs.py but defaults to an *unseeded* RNG so every call produces a different mix — used by the "Generate CSV" and "Send mock" buttons to exercise the realtime pipeline with fresh data each time. """ from __future__ import annotations import csv import io import random from datetime import datetime, timedelta, timezone from typing import Any, Callable USERS = ["root", "admin", "oppa", "somchai", "nattapong", "svc_backup", "www-data", "deploy", "guest", "postgres"] ADMIN_USERS = {"root", "admin", "oppa"} BAD_IPS = ["45.137.21.9", "193.27.228.114", "185.220.101.34", "91.219.236.18", "209.141.56.12"] PORTS = [22, 80, 443, 3306, 5432, 8080, 3389, 21, 25, 53, 6379, 27017] HOSTS = ["web-01", "app-02", "db-03", "bastion-01"] SQLI = ["' OR '1'='1", "UNION SELECT username,password FROM users--", "'; DROP TABLE sessions;--", "1' AND SLEEP(5)--"] XSS = ["", ""] NORMAL_PATHS = ["/", "/login", "/api/products", "/dashboard", "/static/app.js", "/api/orders?page=2", "/health"] # Columns used when exporting CSV. CSV_FIELDS = ["timestamp", "source", "category", "severity", "message", "src_ip", "dst_ip", "user", "dst_port", "path", "action"] def _internal_ip() -> str: return f"10.0.{random.randint(0, 5)}.{random.randint(2, 254)}" def _external_ip() -> str: return f"{random.randint(11, 223)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(1, 254)}" def _rec(t: datetime, source: str, category: str, severity: str, message: str, **extra: Any) -> dict[str, Any]: r = {"timestamp": t.isoformat(timespec="seconds"), "source": source, "category": category, "severity": severity, "message": message} r.update(extra) return r def _ev_benign(t): pick = random.choice(["auth", "web", "fw"]) if pick == "auth": u, ip = random.choice(USERS), _internal_ip() msg = f"Accepted password for {u} from {ip} port {random.randint(40000, 60000)} ssh2" return [_rec(t, "auth", "benign", "info", msg, src_ip=ip, user=u, action="login_success")] if pick == "web": ip, path = _external_ip(), random.choice(NORMAL_PATHS) msg = f'{ip} - - "GET {path} HTTP/1.1" 200 {random.randint(200, 8000)}' return [_rec(t, "nginx", "benign", "info", msg, src_ip=ip, status=200, path=path)] ip, dport = _external_ip(), random.choice([80, 443]) msg = f"ALLOW IN={ip} OUT= PROTO=TCP DPT={dport}" return [_rec(t, "firewall", "benign", "info", msg, src_ip=ip, dst_port=dport, action="allow")] def _ev_brute_force(t): u, ip = random.choice(USERS), random.choice(BAD_IPS + [_external_ip()]) out = [] n = random.randint(8, 25) for i in range(n): tt = t + timedelta(seconds=i * random.randint(1, 3)) msg = f"Failed password for {u} from {ip} port {random.randint(40000, 60000)} ssh2" out.append(_rec(tt, "auth", "brute_force", "high", msg, src_ip=ip, user=u, action="login_failed")) if random.random() < 0.3: tt = t + timedelta(seconds=n * 2) out.append(_rec(tt, "auth", "brute_force", "critical", f"Accepted password for {u} from {ip} port 51234 ssh2", src_ip=ip, user=u, action="login_success", note="success_after_bruteforce")) return out def _ev_port_scan(t): ip = random.choice(BAD_IPS + [_external_ip()]) out = [] for i, p in enumerate(random.sample(PORTS, k=random.randint(8, 12))): tt = t + timedelta(milliseconds=i * random.randint(50, 400)) msg = f"DROP IN={ip} OUT= PROTO=TCP DPT={p} FLAGS=SYN" out.append(_rec(tt, "firewall", "port_scan", "medium", msg, src_ip=ip, dst_port=p, action="drop")) return out def _ev_web_attack(t): ip = random.choice(BAD_IPS + [_external_ip()]) if random.random() < 0.5: payload, kind = random.choice(SQLI), "sqli" path = f"/api/products?id={payload}" status = random.choice([500, 403, 200]) else: payload, kind = random.choice(XSS), "xss" path = f"/search?q={payload}" status = random.choice([200, 403]) msg = f'{ip} - - "GET {path} HTTP/1.1" {status} {random.randint(0, 500)}' return [_rec(t, "nginx", "web_attack", "high", msg, src_ip=ip, status=status, path=path, attack_kind=kind)] def _ev_priv_esc(t): u = random.choice([x for x in USERS if x not in ADMIN_USERS]) host = random.choice(HOSTS) cmd = random.choice(["/bin/bash", "/usr/bin/cat /etc/shadow", "/usr/bin/passwd root", "/bin/su -"]) msg = f"sudo: {u} : TTY=pts/0 ; PWD=/home/{u} ; USER=root ; COMMAND={cmd}" return [_rec(t, "system", "priv_esc", "critical", msg, user=u, host=host, command=cmd, action="sudo")] def _ev_data_exfil(t): ip = random.choice(BAD_IPS) src = _internal_ip() mb = random.randint(500, 8000) msg = (f"OUTBOUND src={src} dst={ip} bytes={mb * 1024 * 1024} proto=TCP dport=443 " f"duration={random.randint(60, 600)}s") return [_rec(t, "firewall", "data_exfil", "critical", msg, src_ip=src, dst_ip=ip, bytes_mb=mb, off_hours=(t.hour < 6 or t.hour > 22))] def _ev_c2_beacon(t): ip = random.choice(BAD_IPS) src = _internal_ip() out = [] interval = random.choice([30, 60, 300]) for i in range(random.randint(5, 10)): tt = t + timedelta(seconds=i * interval) msg = (f"CONN src={src} dst={ip} dport=443 bytes={random.randint(200, 900)} interval={interval}s") out.append(_rec(tt, "firewall", "c2_beacon", "critical", msg, src_ip=src, dst_ip=ip, beacon_interval=interval)) return out GENERATORS: dict[str, Callable] = { "benign": _ev_benign, "brute_force": _ev_brute_force, "port_scan": _ev_port_scan, "web_attack": _ev_web_attack, "priv_esc": _ev_priv_esc, "data_exfil": _ev_data_exfil, "c2_beacon": _ev_c2_beacon, } PROFILES = { "mixed": {"benign": 0.70, "brute_force": 0.06, "port_scan": 0.06, "web_attack": 0.07, "priv_esc": 0.04, "data_exfil": 0.03, "c2_beacon": 0.04}, "realistic": {"benign": 0.92, "brute_force": 0.025, "port_scan": 0.02, "web_attack": 0.02, "priv_esc": 0.006, "data_exfil": 0.004, "c2_beacon": 0.005}, "attack": {"benign": 0.20, "brute_force": 0.16, "port_scan": 0.14, "web_attack": 0.16, "priv_esc": 0.12, "data_exfil": 0.10, "c2_beacon": 0.12}, } def generate(count: int, profile: str = "mixed", days: int = 7, seed: int | None = None, now: datetime | None = None) -> list[dict[str, Any]]: """Generate up to `count` log records. seed=None (default) -> different output every call (uses process RNG). Pass an int seed only when you need reproducible data. """ if seed is not None: random.seed(seed) profile_weights = PROFILES.get(profile, PROFILES["mixed"]) cats = list(profile_weights.keys()) weights = [profile_weights[c] for c in cats] base = (now or datetime.now(timezone.utc)) - timedelta(days=days) span = max(days, 1) * 24 * 3600 records: list[dict[str, Any]] = [] guard = 0 while len(records) < count and guard < count * 4 + 50: guard += 1 cat = random.choices(cats, weights=weights, k=1)[0] t = base + timedelta(seconds=random.randint(0, span)) records.extend(GENERATORS[cat](t)) records.sort(key=lambda r: r["timestamp"]) return records[:count] def generate_live(count: int, profile: str = "mixed") -> list[dict[str, Any]]: """Generate records stamped at 'now' for realtime emission (unique each call).""" now = datetime.now(timezone.utc) records = generate(count, profile=profile, days=0, seed=None, now=now) # Re-stamp close to now so the live feed shows current time. for offset, rec in enumerate(records): rec["timestamp"] = (now + timedelta(milliseconds=offset)).isoformat(timespec="seconds") return records def to_csv(records: list[dict[str, Any]]) -> str: buf = io.StringIO() writer = csv.DictWriter(buf, fieldnames=CSV_FIELDS, extrasaction="ignore") writer.writeheader() for rec in records: writer.writerow({k: rec.get(k, "") for k in CSV_FIELDS}) return buf.getvalue()