File size: 8,333 Bytes
c4f5819
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
"""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"]

# 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()