#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
logwatch.py ― Windows のイベントログ（CSV）から「怪しい出来事」だけを知らせる見張り番
（YouTube「AIの表と裏」第3回 種目J の模範解答。守る側のプログラムです）

使い方：
    python3 logwatch.py <イベントログを書き出した CSV>

入力 CSV（UTF-8、1行目は見出し）：
    time,event_id,computer,user,src_ip,logon_type,detail
    time は ISO 8601（例：2026-11-08T03:12:05+09:00）

知らせる出来事（6つのルール）：
    BRUTE_FORCE          同じ接続元から、ログオン失敗（4625）が10分以内に5回
    BRUTE_FORCE_SUCCESS  BRUTE_FORCE のあと1時間以内に、同じ接続元からログオン成功（4624）
    NEW_ADMIN            作成（4720）から1日以内に、そのアカウントが Administrators に追加（4732）
    LOG_CLEARED          監査ログの消去（1102）
    SUSPICIOUS_SERVICE   AppData・Temp のフォルダや PowerShell を使うサービスの登録（7045）
    NIGHT_RDP            22時〜翌6時の、社外からのリモートデスクトップ（4624・logon_type 10）

出力：JSON の配列（time の早い順。同じ時刻はルール名の順）
標準ライブラリだけで動きます。結果の判断に迷ったら、保守会社などの専門家に相談してください。
"""
import csv
import ipaddress
import json
import sys
from datetime import datetime

WINDOW = 600          # 総当たりを数える幅（秒）
SUCCESS_AFTER = 3600  # 総当たりのあとの成功を見る幅（秒）
ADMIN_AFTER = 86400   # 作成から管理者への追加までの幅（秒）
INTERNAL = [ipaddress.ip_network("192.168.0.0/16"), ipaddress.ip_network("10.0.0.0/8")]
WORDS = ["\\appdata\\", "\\temp\\", "powershell"]


def read_rows(path):
    """CSV を読む。形式のおかしい行（列不足・日時でない・event_id が整数でない）は飛ばす"""
    rows = []
    with open(path, encoding="utf-8-sig", newline="") as f:
        reader = csv.reader(f)
        next(reader, None)
        for n, row in enumerate(reader):
            if len(row) < 7:
                continue
            try:
                t = datetime.fromisoformat(row[0].strip())
                eid = int(row[1].strip())
            except ValueError:
                continue
            rows.append({"n": n, "t": t, "time": row[0], "id": eid, "user": row[3], "ip": row[4].strip(),
                         "logon_type": row[5].strip(), "detail": ",".join(row[6:])})
    rows.sort(key=lambda r: (r["t"], r["n"]))
    return rows


def is_internal(ip):
    try:
        a = ipaddress.ip_address(ip)
    except ValueError:
        return False  # 空・読めないものは「社内でない」
    return any(a in net for net in INTERNAL)


def watch(rows):
    alerts = []
    fails, last_alert, bf_times, created = {}, {}, {}, {}
    for r in rows:
        t, ip, eid = r["t"], r["ip"], r["id"]
        if eid == 4625 and ip:
            if ip in last_alert and (t - last_alert[ip]).total_seconds() <= WINDOW:
                continue  # 一度知らせたら10分は黙る
            q = [x for x in fails.get(ip, []) if (t - x).total_seconds() <= WINDOW] + [t]
            fails[ip] = q
            if len(q) >= 5:
                alerts.append((r, "BRUTE_FORCE"))
                last_alert[ip] = t
                bf_times.setdefault(ip, []).append(t)
                fails[ip] = []  # 10分を過ぎたら数え直す
        if eid == 4624:
            if ip and any(0 <= (t - b).total_seconds() <= SUCCESS_AFTER for b in bf_times.get(ip, [])):
                alerts.append((r, "BRUTE_FORCE_SUCCESS"))
            if r["logon_type"] == "10" and (t.hour >= 22 or t.hour < 6) and not is_internal(ip):
                alerts.append((r, "NIGHT_RDP"))
        if eid == 4720 and r["user"].strip():
            created.setdefault(r["user"].strip(), []).append(t)
        if eid == 4732 and "administrators" in r["detail"].lower():
            if any(0 <= (t - c).total_seconds() <= ADMIN_AFTER for c in created.get(r["user"].strip(), [])):
                alerts.append((r, "NEW_ADMIN"))
        if eid == 1102:
            alerts.append((r, "LOG_CLEARED"))
        if eid == 7045 and any(w in r["detail"].lower() for w in WORDS):
            alerts.append((r, "SUSPICIOUS_SERVICE"))
    alerts.sort(key=lambda a: (a[0]["t"], a[1], a[0]["n"]))
    return [{"rule": rule, "time": r["time"], "user": r["user"], "src_ip": r["ip"]} for r, rule in alerts]


if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit("使い方：python3 logwatch.py <イベントログの CSV>")
    print(json.dumps(watch(read_rows(sys.argv[1])), ensure_ascii=False))
