File size: 6,887 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 | """Rule-based security log classifier.
Ported from the original demo so detection works fully offline (no API key,
no model download). Given a log row it assigns a category, severity hint,
MITRE technique, risk score and human-readable reasons.
"""
from __future__ import annotations
import re
from typing import Any
# Single source of truth for category metadata (label / MITRE / risk / runbook).
CATEGORY_META: dict[str, dict[str, Any]] = {
"benign": {
"label": "ปกติ",
"mitre": "-",
"risk": 10,
"runbook": "บันทึกไว้เป็น baseline และใช้เทียบพฤติกรรมผิดปกติในช่วงเวลาเดียวกัน",
},
"brute_force": {
"label": "Brute force",
"mitre": "T1110",
"risk": 78,
"runbook": "ตรวจ IP ต้นทาง, lock account ชั่วคราว, เพิ่ม MFA, block IP ถ้ามี failed login ถี่ผิดปกติ",
},
"port_scan": {
"label": "Port scan",
"mitre": "T1046",
"risk": 62,
"runbook": "ตรวจ firewall drop burst, จำกัด exposure ของ port สำคัญ, เพิ่ม deny rule สำหรับ source ที่ scan ซ้ำ",
},
"web_attack": {
"label": "Web attack",
"mitre": "T1190",
"risk": 82,
"runbook": "ตรวจ request payload, WAF rule, access log รอบเวลาเดียวกัน และ patch endpoint ที่รับ parameter เสี่ยง",
},
"priv_esc": {
"label": "Privilege escalation",
"mitre": "T1068/T1548",
"risk": 94,
"runbook": "ตรวจ sudo command, revoke session, review sudoers, rotate credential และดู command history",
},
"data_exfil": {
"label": "Data exfiltration",
"mitre": "T1041",
"risk": 96,
"runbook": "ตัด connection ปลายทาง, ตรวจปริมาณ bytes, หา process เจ้าของ traffic และ preserve evidence",
},
"c2_beacon": {
"label": "C2 beacon",
"mitre": "T1071",
"risk": 95,
"runbook": "isolate host, ตรวจ interval คงที่, block C2 destination และเก็บ memory/network artifact",
},
}
# Severity derived from category when the source row does not provide one.
CATEGORY_DEFAULT_SEVERITY = {
"benign": "info",
"brute_force": "high",
"port_scan": "medium",
"web_attack": "high",
"priv_esc": "critical",
"data_exfil": "critical",
"c2_beacon": "critical",
}
TOKEN_RE = re.compile(r"[a-zA-Z0-9_.:/-]+|[\u0E00-\u0E7F]+")
SQLI_RE = re.compile(r"(union\s+select|drop\s+table|or\s+'?1'?='?1|sleep\()", re.I)
XSS_RE = re.compile(r"(<script|onerror=|document\.cookie)", re.I)
QUERY_EXPANSIONS = {
"brute_force": ["brute_force", "failed", "password", "login_failed", "ssh"],
"port_scan": ["port_scan", "drop", "syn", "dpt", "scan"],
"web_attack": ["web_attack", "sqli", "xss", "union", "select", "script", "payload"],
"priv_esc": ["priv_esc", "sudo", "root", "shadow", "passwd"],
"data_exfil": ["data_exfil", "outbound", "bytes", "exfil", "dst"],
"c2_beacon": ["c2_beacon", "c2", "beacon", "conn", "interval"],
}
QUERY_HINTS = {
"brute": "brute_force", "bruteforce": "brute_force", "failed login": "brute_force",
"เดารหัส": "brute_force", "ล็อกอิน": "brute_force",
"scan": "port_scan", "port": "port_scan", "สแกน": "port_scan",
"sqli": "web_attack", "sql": "web_attack", "xss": "web_attack",
"payload": "web_attack", "เว็บ": "web_attack",
"sudo": "priv_esc", "root": "priv_esc", "privilege": "priv_esc", "ยกระดับ": "priv_esc",
"exfil": "data_exfil", "outbound": "data_exfil", "ข้อมูลรั่ว": "data_exfil", "ส่งข้อมูล": "data_exfil",
"c2": "c2_beacon", "beacon": "c2_beacon", "interval": "c2_beacon", "คงที่": "c2_beacon",
}
def tokenize(text: str) -> list[str]:
return [t.lower() for t in TOKEN_RE.findall(text or "") if len(t) > 1]
def expand_query(query: str) -> tuple[list[str], set[str]]:
lowered = query.lower()
categories = {cat for cat in CATEGORY_META if cat in lowered}
for hint, category in QUERY_HINTS.items():
if hint in lowered:
categories.add(category)
terms = tokenize(query.replace("_", " "))
for category in categories:
terms.extend(QUERY_EXPANSIONS.get(category, []))
return terms, categories
def detect_rule(row: dict[str, Any]) -> dict[str, Any]:
"""Classify a single log row purely from its message + fields.
Returns the detected category and explanation. Does NOT trust an incoming
`category` label — that is what we are detecting — but compares against it
when present so callers can measure accuracy.
"""
message = str(row.get("message", "") or "")
category = "benign"
reasons: list[str] = []
if "Failed password" in message:
category = "brute_force"
reasons.append("พบ failed SSH login")
if "DROP" in message and "FLAGS=SYN" in message:
category = "port_scan"
reasons.append("พบ SYN หลายปลายทาง/หลาย port")
if SQLI_RE.search(message) or XSS_RE.search(message):
category = "web_attack"
reasons.append("พบ SQLi/XSS payload ใน request")
if "sudo:" in message and "USER=root" in message:
category = "priv_esc"
reasons.append("พบ sudo ไปยัง root")
if "OUTBOUND" in message and "bytes=" in message:
category = "data_exfil"
reasons.append("พบ outbound transfer ขนาดใหญ่")
if "interval=" in message and "CONN" in message:
category = "c2_beacon"
reasons.append("พบ connection interval คงที่แบบ beacon")
meta = CATEGORY_META[category]
severity = row.get("severity") or CATEGORY_DEFAULT_SEVERITY[category]
expected = row.get("category")
return {
"category": category,
"label": meta["label"],
"risk_score": meta["risk"],
"severity": severity,
"mitre": meta["mitre"],
"reasons": reasons or ["ไม่พบ pattern เสี่ยงตาม rule พื้นฐาน"],
"matches_label": (expected == category) if expected else None,
}
def row_text(row: dict[str, Any]) -> str:
parts = [
row.get("message", ""), row.get("category", ""), row.get("severity", ""),
row.get("source", ""), row.get("src_ip", ""), row.get("dst_ip", ""),
row.get("path", ""), row.get("user", ""),
]
return " ".join(str(p) for p in parts if p is not None)
|