| """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 = ["<script>alert(1)</script>", "<img src=x onerror=alert(document.cookie)>"] |
| NORMAL_PATHS = ["/", "/login", "/api/products", "/dashboard", "/static/app.js", |
| "/api/orders?page=2", "/health"] |
|
|
| |
| 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) |
| |
| 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() |
|
|