#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
================================================================================
 MiSoft - Black October 2026 - Cong cu do HIEN DIEN lo hong (AN TOAN)
================================================================================
 Trung tam Dich vu An ninh mang - MiSoft
 Kinh doanh : +84 24 3933 1613 - info@misoft.com.vn
 Ky thuat   : Nguyen Anh Phuong - 0902 190 898 - pentest@misoft.com.vn

 MUC DICH
 --------
 Cong cu nay CHI phat hien kha nang anh huong, dua tren:
   - Phien ban phan mem cai dat (offline) hoac cong bo qua banner/header (online)
   - Cau hinh lien quan (vi du session.upload_progress cua PHP)
   - Dau hieu thoa hiep (IOC) co san tren may: tien trinh, tep, ket noi, cong mo
 Cong cu KHONG gui ma khai thac, KHONG lam sap dich vu, KHONG ghi/sua gi tren
 he thong muc tieu. Ket qua la CANH BAO de con nguoi xac minh, KHONG phai bang
 chung khai thac duoc.

 RANH GIOI PHAP LY
 -----------------
 Chi chay tren he thong do ban so huu hoac da duoc uy quyen bang van ban.
 Che do --online chi doc thong tin phien ban cong khai (nhu mot trinh duyet).

 CACH DUNG
 ---------
   python3 misoft_blackoctober_scanner.py --offline            # do may cuc bo
   python3 misoft_blackoctober_scanner.py --offline --no-ioc   # bo qua quet IOC
   python3 misoft_blackoctober_scanner.py --online https://host # doc banner/header
   python3 misoft_blackoctober_scanner.py --offline --json kq.json
================================================================================
"""

import argparse
import json
import os
import platform
import re
import shutil
import ssl
import subprocess
import sys
from datetime import datetime, timezone

try:
    import urllib.request
    import urllib.error
except Exception:
    urllib = None

VERSION = "2.0 (2026-10-26)"

# ----------------------------------------------------------------------------
# IOC - chi so thoa hiep (doi chieu voi bang IOC trong ban tin, 26/10/2026)
# ----------------------------------------------------------------------------
IOC_IP = {
    "96.62.218.59":  "C2 - Siemens Software Center",
    "2.27.248.127":  "Phat tan MSI - Siemens Software Center",
    "91.92.40.202":  "IP quet - Plex2Shell",
    "176.65.139.228": "IP quet - Plex2Shell",
    "45.194.92.222": "IP quet - Plex2Shell",
    "217.60.194.116": "Phan phoi payload - Plex2Shell",
    "144.172.64.99": "ARTEX / ConnectWise (chien dich)",
}
IOC_DOMAIN = {
    "ota.proxystream.st": "Plex2Shell",
    "global.relay.proxystream.st": "Plex2Shell",
    "wizardexpensive.com": "Siemens Software Center",
}
IOC_PROC = {
    "xmrig": "Coin miner - Plex2Shell",
    "mylovelyminer": "Coin miner - Plex2Shell",
    "myawesomeproxy": "Proxy la - Plex2Shell",
    "x12345": "Tien trinh la - Plex2Shell",
}
IOC_FILE = {
    "AllMyBasic.msi": "Siemens Software Center",
    "archive.dll": "Siemens Software Center",
}
IOC_PORT = {27918: "gRPC FortiSIEM (CVE-2026-106173) - KHONG de lo ra Internet"}

# ----------------------------------------------------------------------------
# Trang thai & tien ich
# ----------------------------------------------------------------------------
# VULNERABLE: phien ban/ cau hinh cho thay nguy co cao
# LIKELY    : dau hieu online (co the bi an header) -> xac minh them
# IOC       : phat hien dau hieu thoa hiep -> dieu tra ngay
# CHECK     : can kiem tra thu cong (thiet bi mang, ERP, ...)
# OK        : phien ban >= ban va
# NOT_FOUND : khong tim thay thanh phan tren may nay

def now_iso():
    return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")

def cmp_ver(a, b):
    """So sanh phien ban dang so.cham. Tra -1/0/1."""
    def norm(v):
        return [int(x) for x in re.findall(r"\d+", v)]
    pa, pb = norm(a), norm(b)
    n = max(len(pa), len(pb))
    pa += [0] * (n - len(pa)); pb += [0] * (n - len(pb))
    return (pa > pb) - (pa < pb)

def run_cmd(cmd, timeout=10):
    """Chay lenh doc-only, tra ve stdout+stderr. An toan: khong shell=True."""
    try:
        out = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return ((out.stdout or "") + (out.stderr or "")).strip()
    except Exception:
        return ""

def which_version(cmd, args, pattern):
    exe = shutil.which(cmd)
    if not exe:
        return None
    txt = run_cmd([exe] + args)
    m = re.search(pattern, txt)
    return m.group(1) if m else None

def pkg_version(name):
    """Lay phien ban goi Python qua metadata - KHONG import/chay code cua goi."""
    try:
        from importlib.metadata import version, PackageNotFoundError
        try:
            return version(name)
        except PackageNotFoundError:
            return None
    except Exception:
        return None

def add(findings, comp, status, detail, installed=None, fixed=None, cve=None):
    findings.append({"thanh_phan": comp, "trang_thai": status,
                     "phien_ban_phat_hien": installed, "phien_ban_an_toan": fixed,
                     "cve": cve, "chi_tiet": detail})

def ver_finding(findings, name, installed, fixed_map_or_val, cve, branch_key=None, eol=None, eol_fix=None):
    """So phien ban cai dat voi ban va; sinh finding VULNERABLE/OK/CHECK."""
    if isinstance(fixed_map_or_val, dict):
        key = branch_key(installed) if branch_key else None
        fixed = fixed_map_or_val.get(key)
        if fixed:
            st = "VULNERABLE" if cmp_ver(installed, fixed) < 0 else "OK"
            add(findings, name, st,
                f"{installed} {'thap hon' if st=='VULNERABLE' else '>='} ban va {fixed}.",
                installed, fixed, cve)
            return
        if eol and key in eol:
            add(findings, name, "VULNERABLE",
                f"{installed} thuoc nhanh EOL ({key}) - khong co ban va chinh thuc. "
                f"Nang len nhanh duoc ho tro{(' (' + eol_fix + ')') if eol_fix else ''}.",
                installed, eol_fix or "nhanh duoc ho tro", cve)
            return
        add(findings, name, "CHECK", f"{installed} - doi chieu thu cong voi ban va.", installed, None, cve)
    else:
        fixed = fixed_map_or_val
        st = "VULNERABLE" if cmp_ver(installed, fixed) < 0 else "OK"
        add(findings, name, st,
            f"{installed} {'thap hon' if st=='VULNERABLE' else '>='} ban va {fixed}.",
            installed, fixed, cve)

# ----------------------------------------------------------------------------
# OFFLINE - kiem tra phan mem cai tren may dang chay
# ----------------------------------------------------------------------------
def scan_offline(findings, do_ioc=True):
    # 1) PHP2Shell
    ver = which_version("php", ["-v"], r"PHP\s+([\d.]+)")
    if ver:
        ver_finding(findings, "PHP2Shell (PHP core RCE)", ver,
                    {"8.5": "8.5.12", "8.4": "8.4.27", "8.3": "8.3.36", "8.2": "8.2.35"},
                    "CVE-2026-109618, CVE-2026-109619",
                    branch_key=lambda v: ".".join(v.split(".")[:2]),
                    eol=["8.1","8.0","7.4","7.3","7.2","7.1","7.0","5.6","5.5","5.4"],
                    eol_fix="8.5.12 / 8.4.27 / 8.3.36 / 8.2.35")
        _php_upload_progress(findings)
    else:
        add(findings, "PHP2Shell (PHP core RCE)", "NOT_FOUND", "Khong tim thay PHP CLI tren may nay.")

    # 2) Node.js (Node2Shell) + he sinh thai Brotli trong Python
    ver = which_version("node", ["-v"], r"v?([\d.]+)")
    if ver:
        ver_finding(findings, "Node.js (Node2Shell, Brotli RCE)", ver,
                    {"26": "26.12.1", "24": "24.22.1", "22": "22.23.4"},
                    "CVE-2026-111497",
                    branch_key=lambda v: v.split(".")[0],
                    eol=["20","18","16"], eol_fix="22.23.4 / 24.22.1 / 26.12.1")
    else:
        add(findings, "Node.js (Node2Shell, Brotli RCE)", "NOT_FOUND", "Khong tim thay Node.js tren may nay.")
    # Brotli / brotlicffi trong Python (cung dinh loi Brotli)
    for pkg, fixed in (("Brotli", "1.2.1"), ("brotlicffi", "1.2.1.0")):
        pv = pkg_version(pkg)
        if pv:
            st = "VULNERABLE" if cmp_ver(pv, fixed) < 0 else "OK"
            add(findings, f"Python {pkg} (loi Brotli, CVE-2026-111497)", st,
                f"{pkg} {pv} {'thap hon' if st=='VULNERABLE' else '>='} ban va {fixed}. "
                f"Keo theo urllib3/requests, httpx[brotli], aiohttp, Scrapy.",
                pv, fixed, "CVE-2026-111497")

    # 3) GitLab (self-managed) - doc version-manifest (read-only)
    gl = _gitlab_version()
    if gl:
        ver_finding(findings, "GitLab self-managed (Onigmo RCE)", gl,
                    {"19.5": "19.5.1", "19.4": "19.4.3", "19.3": "19.3.6", "19.2": "19.2.9"},
                    "CVE-2026-110264",
                    branch_key=lambda v: ".".join(v.split(".")[:2]))
        add(findings, "GitLab - ban va 12/10 CHUA du", "CHECK",
            "Neu da nang len 19.4.2 (12/10) VAN chua du cho CVE-2026-110264 - phai nang them mot bac. "
            "Giam sat puma_stderr.log tim crash [BUG] frame org-ruby/Onigmo.",
            None, "19.5.1 / 19.4.3 / 19.3.6 / 19.2.9", "CVE-2026-110264")
    else:
        add(findings, "GitLab self-managed (Onigmo RCE)", "NOT_FOUND",
            "Khong thay /opt/gitlab/version-manifest.txt. Neu co GitLab: cat file do | head -1.")

    # 4) LiteLLM (goi doc 1.107.0)
    lv = pkg_version("litellm")
    if lv:
        if lv == "1.107.0":
            add(findings, "LiteLLM (chuoi cung ung, goi doc 1.107.0)", "IOC",
                "DA CAI litellm 1.107.0 - ban goi doc. Co lap may, KHONG chay pip o day, "
                "xoay MOI secret, dung lai sach tu ban khong nhiem.",
                lv, "khac 1.107.0", "supply-chain")
        else:
            add(findings, "LiteLLM (chuoi cung ung)", "OK",
                f"litellm {lv} (khong phai 1.107.0). Van nen ra lich su cai dat trong cua so 21/10.",
                lv, "khac 1.107.0", "supply-chain")

    # 5) Plex Media Server (Plex2Shell)
    ver = which_version("Plex Media Server", ["--version"], r"([\d.]+)") or \
          which_version("plexmediaserver", ["--version"], r"([\d.]+)")
    if ver:
        ver_finding(findings, "Plex Media Server (Plex2Shell)", ver, "1.43.3.10861",
                    "CVE-2026-96652/54/55/56")
    else:
        add(findings, "Plex Media Server (Plex2Shell)", "NOT_FOUND",
            "Khong thay Plex qua CLI. Kiem tra trong Settings > General cua Plex (van >= 1.43.3.10861).")

    # 6) OpenCode (MCP exec)
    ver = which_version("opencode", ["--version"], r"([\d.]+)")
    if ver:
        st = "VULNERABLE" if cmp_ver(ver, "1.18.33") < 0 else "OK"
        add(findings, "OpenCode (MCP exec khi sign-in)", st,
            f"OpenCode {ver} {'thap hon' if st=='VULNERABLE' else '>='} ban va 1.18.33 "
            f"(va am tham 28/9, vung dinh loi 1.0.137-1.18.32). "
            f"Go moi MCP server tu xa chua kiem chung; xoay API key neu dung Console.",
            ver, "1.18.33", "chua cap CVE")
    else:
        add(findings, "OpenCode (MCP exec khi sign-in)", "NOT_FOUND", "Khong thay OpenCode CLI tren may nay.")

    # 7) OVHcloud CLI (GlassWorm - binary bi thay)
    if shutil.which("ovhcloud") or shutil.which("ovh"):
        add(findings, "OVHcloud CLI (GlassWorm)", "CHECK",
            "Co OVHcloud CLI tren may. Binary ban v0.11.0-v0.15.0 tai tu GitHub ngay 19/10 (12:47-14:21 UTC) "
            "co the da bi thay bang infostealer. Doi chieu hash voi ban phat hanh lai; neu nghi nhiem: "
            "xoay OVH credential va moi secret may da cham toi.",
            None, "cai lai ban sach + xoay credential", "chuoi cung ung")

    # 8) certifi (eMudhra - thu hoi CA trung gian)
    cv = pkg_version("certifi")
    if cv:
        st = "VULNERABLE" if cmp_ver(cv, "2026.10.19") < 0 else "OK"
        add(findings, "certifi / CA bundle (eMudhra)", st,
            f"certifi {cv} {'cu hon' if st=='VULNERABLE' else '>='} 2026.10.19 "
            f"(ban go CA trung gian eMudhra bi lo khoa). Cap nhat ca-certificates (OS), Node ban va 21/10, "
            f"curl CA bundle tu 2026-10-20.",
            cv, "2026.10.19", "CVE-2026-111943/111944")

    # 9) Siemens Software Center (Windows, best-effort)
    if platform.system() == "Windows":
        add(findings, "Siemens Software Center (DLL side-loading)", "CHECK",
            "Kiem tra phien ban trong Siemens Software Center > About (van >= 4.0.9.8). Soi HANH VI: "
            "SiemensSoftwareCenter.exe chay tu AppData\\Local voi tien trinh cha msiexec; DLL la canh exe.",
            None, "4.0.9.8", "CVE-2026-110936")

    # 10) Thiet bi mang / ERP - khong do tu dong duoc, nhac kiem tra thu cong
    _manual_items(findings)

    # 11) Quet IOC tren may (doc-only)
    if do_ioc:
        scan_ioc(findings)

def _php_upload_progress(findings):
    exe = shutil.which("php")
    if not exe:
        return
    txt = run_cmd([exe, "-i"])
    m = re.search(r"session\.upload_progress\.enabled\s*=>\s*(\w+)", txt, re.I)
    if m:
        val = m.group(1).lower()
        if val in ("on", "1", "true"):
            add(findings, "PHP cau hinh session.upload_progress", "VULNERABLE",
                "session.upload_progress.enabled = On - duong tan cong cua CVE-2026-109619. "
                "TAT (Off) nhu giam thieu tam thoi. LUU Y: CVE-2026-109618 co co che khac, tat "
                "upload_progress KHONG du - phai nang PHP.",
                "On", "Off", "CVE-2026-109619")
        else:
            add(findings, "PHP cau hinh session.upload_progress", "OK",
                "session.upload_progress.enabled da Off.", "Off", None, "CVE-2026-109619")

def _gitlab_version():
    for p in ("/opt/gitlab/version-manifest.txt",):
        if os.path.isfile(p):
            try:
                with open(p, "r", errors="ignore") as fh:
                    head = fh.readline()
                m = re.search(r"(\d+\.\d+\.\d+)", head)
                if m:
                    return m.group(1)
            except Exception:
                pass
    v = which_version("gitlab-rake", ["gitlab:env:info"], r"GitLab information[\s\S]*?Version:\s*([\d.]+)")
    return v

def _manual_items(findings):
    manual = [
        ("Oracle E-Business Suite (SQLi -> RCE)", "CVE-2026-101767",
         "Ap Critical Patch Update 10/2026 (Doc ID KA923). Anh huong EBS 12.2.3-12.2.15. "
         "Dat EBS sau WAF/VPN.", "CPU 10/2026"),
        ("Citrix NetScaler ADC/Gateway (memory overflow)", "CVE-2026-107406",
         "Chi anh huong khi cau hinh SAML (SP/IdP). Kiem tra firmware va cau hinh SAML.",
         "14.1-73.46 / 13.1-64.29"),
        ("Fortinet FortiSIEM (gRPC file write -> RCE)", "CVE-2026-106173",
         "Chi anh huong 7.5.0 (7.6 va Cloud khong dinh). Han che TCP 27918, KHONG de ra Internet.",
         "7.5.1"),
        ("Arista VeloCloud Orchestrator (SQLi leo quyen)", "CVE-2026-114625",
         "Dang bi khai thac (CISA KEV). Ra soat tai khoan/quyen bat thuong tren VCO.",
         "5.2.3.18 / 6.1.3.10 / 6.4.2.12"),
        ("SiteGuard Server Edition (OS command injection)", "CVE-2026-106931",
         "Dang bi khai thac. Tat chuc nang statistics hoac ap ban va EG Secure. "
         "LUU bang chung TRUOC khi va.", "theo ban va EG Secure"),
        ("Railway (ro ri bien moi truong)", "su co nen tang",
         "Neu trien khai tren Railway: xoay MOI bien moi truong, ra log tu 21/10.",
         "xoay toan bo env var"),
        ("SBI / ro ri du lieu PII", "su co du lieu",
         "Khong phai lo hong can va. Bai hoc: giam sat dark web, ra soat ben thu ba xu ly du lieu.",
         "giam sat & ung pho"),
    ]
    for name, cve, note, fixed in manual:
        add(findings, name, "CHECK", note, None, fixed, cve)

# ----------------------------------------------------------------------------
# QUET IOC - doc-only: tien trinh, tep, ket noi mang, cong mo
# ----------------------------------------------------------------------------
def scan_ioc(findings):
    win = platform.system() == "Windows"
    hit = False

    # Tien trinh
    procs = run_cmd(["tasklist"]) if win else run_cmd(["ps", "-eo", "comm,args"])
    low = procs.lower()
    for name, desc in IOC_PROC.items():
        if name in low:
            add(findings, f"IOC tien trinh: {name}", "IOC",
                f"Phat hien tien trinh khop IOC ({desc}). Dieu tra ngay, co lap may.",
                None, None, None); hit = True

    # Ket noi mang & cong mo
    net = run_cmd(["netstat", "-ano"]) if win else (run_cmd(["ss", "-tunap"]) or run_cmd(["netstat", "-tunap"]))
    for ip, desc in IOC_IP.items():
        if ip in net:
            add(findings, f"IOC ket noi: {ip}", "IOC",
                f"Co ket noi toi IP trong danh sach IOC ({desc}).", None, None, None); hit = True
    for port, desc in IOC_PORT.items():
        if re.search(r"[:.]%d\b" % port, net):
            add(findings, f"Cong mo: {port}", "CHECK",
                f"Phat hien cong {port} ({desc}).", None, None, None)

    # Tep nghi ngo (cac thu muc tam/pho bien)
    cand_dirs = []
    if win:
        for e in ("LOCALAPPDATA", "APPDATA", "TEMP"):
            if os.environ.get(e):
                cand_dirs.append(os.environ[e])
    else:
        cand_dirs += ["/tmp", "/var/tmp", os.path.expanduser("~")]
    for d in cand_dirs:
        for fn in IOC_FILE:
            try:
                if d and os.path.isfile(os.path.join(d, fn)):
                    add(findings, f"IOC tep: {fn}", "IOC",
                        f"Tim thay tep khop IOC tai {d} ({IOC_FILE[fn]}).", None, None, None); hit = True
            except Exception:
                pass

    # Phan giai domain IOC (khong ket noi, chi tra DNS cache qua getaddrinfo? -> bo qua de an toan)
    if not hit:
        add(findings, "Quet IOC tren may", "OK",
            "Khong thay tien trinh/tep/ket noi khop IOC trong danh sach. "
            "Day la quet nhanh, khong thay the giam sat EDR/SIEM.", None, None, None)

# ----------------------------------------------------------------------------
# ONLINE - chi doc banner/header cong khai (HEAD vo hai, nhu trinh duyet)
# ----------------------------------------------------------------------------
def http_head(url, timeout=12):
    if urllib is None:
        return None, None, "urllib khong kha dung"
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    req = urllib.request.Request(url, method="HEAD",
                                 headers={"User-Agent": "MiSoft-SafeScanner/2.0 (presence-only)"})
    try:
        with urllib.request.urlopen(req, timeout=timeout, context=ctx) as r:
            return r.status, dict(r.headers), None
    except urllib.error.HTTPError as e:
        return e.code, dict(e.headers), None
    except Exception as e:
        return None, None, str(e)

def scan_online(url, findings):
    if not re.match(r"^https?://", url):
        url = "https://" + url
    status, headers, err = http_head(url)
    if headers is None:
        add(findings, "Ket noi", "NOT_FOUND", f"Khong doc duoc header tu {url}: {err}")
        return
    server = headers.get("Server", "")
    powered = headers.get("X-Powered-By", "")

    m = re.search(r"PHP/([\d.]+)", powered)
    if m:
        ver = m.group(1); branch = ".".join(ver.split(".")[:2])
        fixed = {"8.5":"8.5.12","8.4":"8.4.27","8.3":"8.3.36","8.2":"8.2.35"}.get(branch)
        if fixed and cmp_ver(ver, fixed) < 0:
            add(findings, "PHP2Shell (qua header)", "LIKELY",
                f"Header cong bo PHP/{ver} (< {fixed}). Nhieu server an header nay nen khong "
                f"cong bo KHONG co nghia la an toan - xac minh tai may chu.", ver, fixed,
                "CVE-2026-109618/109619")
        elif fixed:
            add(findings, "PHP2Shell (qua header)", "OK", f"Header cong bo PHP/{ver} (>= {fixed}).",
                ver, fixed, None)
    else:
        add(findings, "PHP (qua header)", "CHECK",
            "May chu khong cong bo phien ban PHP (tot ve an thong tin). Xac minh bang php -v tai may chu.")

    if "siteguard" in server.lower() or any("siteguard" in str(v).lower() for v in headers.values()):
        add(findings, "SiteGuard (qua header)", "CHECK",
            "Co dau hieu SiteGuard. Kiem tra phien ban, tat/va chuc nang statistics (CVE-2026-106931).",
            None, "theo ban va EG Secure", "CVE-2026-106931")
    if re.search(r"netscaler|citrix", server, re.I):
        add(findings, "Citrix NetScaler (qua header)", "CHECK",
            "Co dau hieu NetScaler. Neu dung SAML: kiem tra firmware (CVE-2026-107406).",
            None, "14.1-73.46 / 13.1-64.29", "CVE-2026-107406")

    add(findings, "Ghi chu online", "CHECK",
        "Che do online chi doc thong tin cong bo. GitLab, Plex, Node.js, LiteLLM, OpenCode, "
        "cac thiet bi mang can kiem tra tai chinh he thong (offline) de chac chan.")

# ----------------------------------------------------------------------------
# In bao cao
# ----------------------------------------------------------------------------
BANNER = r"""
  __  __ _ ____         __ _
 |  \/  (_) ___|  ___  / _| |_
 | |\/| | \___ \ / _ \| |_| __|   Black October 2026
 | |  | | |___) | (_) |  _| |_    Trinh do hien dien lo hong (AN TOAN) v%s
 |_|  |_|_|____/ \___/|_|  \__|   Trung tam Dich vu An ninh mang - MiSoft
""" % VERSION

ICON = {"VULNERABLE": "[!] NGUY CO CAO ", "IOC": "[X] DAU HIEU IOC",
        "LIKELY": "[~] CO KHA NANG ", "CHECK": "[?] CAN KIEM TRA",
        "OK": "[+] AN TOAN    ", "NOT_FOUND": "[ ] KHONG THAY  "}
ORDER = {"IOC": 0, "VULNERABLE": 1, "LIKELY": 2, "CHECK": 3, "OK": 4, "NOT_FOUND": 5}

def print_report(findings, meta):
    print(BANNER)
    print(f"Thoi diem quet : {meta['thoi_diem']}")
    print(f"Che do         : {meta['che_do']}")
    if meta.get("muc_tieu"):
        print(f"Muc tieu       : {meta['muc_tieu']}")
    print("-" * 78)
    for f in sorted(findings, key=lambda x: ORDER.get(x["trang_thai"], 9)):
        print(f"{ICON.get(f['trang_thai'], '[?]')} | {f['thanh_phan']}")
        if f.get("cve"):
            print(f"        CVE   : {f['cve']}")
        if f.get("phien_ban_phat_hien") or f.get("phien_ban_an_toan"):
            print(f"        Hien  : {f.get('phien_ban_phat_hien') or '-'}  ->  An toan: {f.get('phien_ban_an_toan') or '-'}")
        print(f"        {f['chi_tiet']}")
        print()
    print("-" * 78)
    c = lambda *s: sum(1 for f in findings if f["trang_thai"] in s)
    print(f"TOM TAT: {c('IOC')} dau hieu IOC, {c('VULNERABLE','LIKELY')} nguy co phien ban, "
          f"{c('CHECK')} muc can kiem tra thu cong, {c('OK')} dat.")
    print("Day la CANH BAO dua tren phien ban/cau hinh/IOC, KHONG phai bang chung khai thac duoc.")
    print("Ho tro xac minh & xu ly (Ky thuat): Nguyen Anh Phuong - 0902 190 898 - pentest@misoft.com.vn")
    print("Kinh doanh: +84 24 3933 1613 - info@misoft.com.vn")

def main():
    ap = argparse.ArgumentParser(description="MiSoft Black October 2026 - trinh do hien dien an toan")
    g = ap.add_mutually_exclusive_group(required=True)
    g.add_argument("--offline", action="store_true", help="Do phan mem cai tren may dang chay + quet IOC")
    g.add_argument("--online", metavar="URL", help="Doc banner/header cong khai cua mot host (vo hai)")
    ap.add_argument("--no-ioc", action="store_true", help="Bo qua quet IOC khi chay --offline")
    ap.add_argument("--json", metavar="FILE", help="Xuat ket qua ra file JSON")
    args = ap.parse_args()

    findings = []
    meta = {"thoi_diem": now_iso(), "phien_ban_cong_cu": VERSION}
    # Lop chong treo cap cao nhat: du mot buoc loi, cong cu van in ket qua va thoat sach,
    # KHONG bao gio lam thay doi/gay loi cho he thong muc tieu.
    try:
        if args.offline:
            meta["che_do"] = "OFFLINE (may cuc bo" + ("" if args.no_ioc else " + quet IOC") + ")"
            scan_offline(findings, do_ioc=not args.no_ioc)
        else:
            meta["che_do"] = "ONLINE (chi doc banner/header)"
            meta["muc_tieu"] = args.online
            scan_online(args.online, findings)
    except KeyboardInterrupt:
        print("\nDa dung theo yeu cau nguoi dung.")
    except Exception as e:
        add(findings, "Trang thai cong cu", "CHECK",
            f"Mot buoc gap loi khong mong muon ({type(e).__name__}: {e}). Cac ket qua da thu van hien o tren. "
            f"Cong cu chi doc, KHONG thay doi gi tren he thong.")

    print_report(findings, meta)
    if args.json:
        with open(args.json, "w", encoding="utf-8") as fh:
            json.dump({"meta": meta, "ket_qua": findings}, fh, ensure_ascii=False, indent=2)
        print(f"\nDa luu JSON: {args.json}")

if __name__ == "__main__":
    main()
