106 KiB
106 KiB
In [ ]:
import warnings
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 30)
RANDOM_STATE = 42
goc = pd.read_csv("/opt/csai/data/attackers/features.csv").set_index("src_ip")
# Đổi tên cột sang tiếng Việt cho dễ đọc
TEN = {"connections": "so_ket_noi", "login_fail": "dang_nhap_sai", "login_success": "dang_nhap_dung", "fail_ratio": "ti_le_sai",
"avg_session_sec": "phien_tb_giay", "max_session_sec": "phien_dai_nhat_giay", "sessions_with_cmds": "so_phien_co_lenh",
"cmds_per_session": "lenh_moi_phien", "distinct_cmds": "so_lenh_khac_nhau", "uses_proxy": "dung_proxy",
"file_download": "so_lan_tai_file", "active_days": "so_ngay", "client_family": "cong_cu"}
ho_so = goc.drop(columns="period").rename(columns=TEN)
# 📦 ho_so = bảng hồ sơ: mỗi HÀNG 1 IP ("tài khoản"), mỗi CỘT 1 thói quen đo trong 30 ngày
print("ho_so.shape =", ho_so.shape, "-> (số tài khoản, số cột)")
ho_so.loc[["1.10.195.220", "130.12.182.196", "185.246.128.133"]]In [ ]:
SO = [c for c in ho_so.columns if c != "cong_cu"] # 12 cột số đưa vào máy học
# 📦 SO = danh sách 12 cột số
tom_tat = pd.DataFrame({"trung vị (tài khoản 'giữa')": ho_so[SO].median(), "phân vị 90%": ho_so[SO].quantile(0.9), "lớn nhất": ho_so[SO].max()})
print(tom_tat.round(2).to_string())
print("\nCông cụ hay gặp:", ho_so.cong_cu.value_counts().head(6).to_dict())In [ ]:
# ✏️ SỬA: luật của bạn
NGUONG_SAI = 50
bi_khoa = (ho_so.dang_nhap_sai > NGUONG_SAI) | (ho_so.so_lan_tai_file > 0)
print(f"Luật khóa {bi_khoa.sum()} / {len(ho_so)} tài khoản")
# Những tài khoản "đáng sợ" mà luật KHÔNG khóa: đăng nhập ĐÚNG rất nhiều lần, hoặc dùng máy làm bàn đạp
lot = ho_so[~bi_khoa & ((ho_so.dang_nhap_dung > 100) | (ho_so.dung_proxy == 1))]
print(f"Lọt luật mà đăng nhập đúng > 100 lần hoặc dùng proxy: {len(lot)} tài khoản. 6 tài khoản đăng nhập đúng nhiều nhất:")
lot.sort_values("dang_nhap_dung", ascending=False).head(6)[["so_ket_noi", "dang_nhap_sai", "dang_nhap_dung", "dung_proxy", "so_ngay", "cong_cu"]]In [ ]:
from sklearn.cluster import KMeans
hai_chieu = np.log1p(ho_so[["so_ket_noi", "phien_tb_giay"]]).to_numpy()
# ✏️ SỬA: số cụm cho phần minh họa
K_MINH_HOA = 3
MAU = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b"] # cụm 0, 1, 2... lần lượt mang các màu này
TEN_MAU = ["xanh dương", "cam", "xanh lá", "đỏ", "tím", "nâu"]
tam_ban_dau = hai_chieu[np.random.RandomState(1).choice(len(hai_chieu), K_MINH_HOA, replace=False)] # thả tâm ngẫu nhiên
hinh, o = plt.subplots(1, 4, figsize=(18, 4), sharey=True)
for truc, so_vong in zip(o, [0, 1, 2, 10]):
if so_vong == 0:
nhan_ = np.argmin(((hai_chieu[:, None, :] - tam_ban_dau[None]) ** 2).sum(-1), axis=1) # gom về tâm gần nhất
tam = tam_ban_dau
else:
km_ = KMeans(K_MINH_HOA, init=tam_ban_dau, n_init=1, max_iter=so_vong).fit(hai_chieu) # chạy đúng so_vong vòng
nhan_, tam = km_.labels_, km_.cluster_centers_
truc.scatter(hai_chieu[:, 0], hai_chieu[:, 1], c=np.array(MAU)[nhan_], s=6, alpha=0.5) # màu của cụm c = MAU[c]
truc.scatter(tam[:, 0], tam[:, 1], marker="X", s=250, c="black")
truc.set_title("bước 1: thả tâm ngẫu nhiên, gom" if so_vong == 0 else f"sau {so_vong} vòng dời tâm")
truc.set_xlabel("log(1 + số kết nối)")
o[0].set_ylabel("log(1 + phiên trung bình, giây)")
plt.tight_layout(); plt.show()
# Ý nghĩa từng màu ở hình cuối (sau 10 vòng): đọc từ giá trị điển hình (trung vị) của nhóm
for c in range(K_MINH_HOA):
nhom = ho_so[nhan_ == c] # các tài khoản mang màu c ở hình cuối
print(f"màu {TEN_MAU[c]:10s}: {len(nhom):5d} tài khoản | kết nối điển hình {nhom.so_ket_noi.median():6.0f} | phiên điển hình {nhom.phien_tb_giay.median():6.1f} giây")In [ ]:
from sklearn.preprocessing import StandardScaler
X = pd.DataFrame(StandardScaler().fit_transform(np.log1p(ho_so[SO])), index=ho_so.index, columns=SO)
# 📦 X = 12 cột đã nén + chuẩn hóa: đầu vào của K-Means
print("X.shape =", X.shape, "| trung bình mỗi cột ~0:", X.mean().abs().max().round(3), "| độ lệch chuẩn ~1:", X.std(ddof=0).round(2).unique())In [ ]:
# 3.2a Tính silhouette BẰNG TAY cho 1 tài khoản, với K = 4
from sklearn.metrics import silhouette_score, silhouette_samples
km4 = KMeans(n_clusters=4, n_init=10, random_state=RANDOM_STATE).fit(X)
IP_VI_DU = "1.10.195.220" # ✏️ SỬA: thử tài khoản khác, ví dụ "185.246.128.133"
i = X.index.get_loc(IP_VI_DU) # vị trí (số thứ tự hàng) của tài khoản trong X
nhan4 = km4.labels_
kc = np.linalg.norm(X.to_numpy() - X.to_numpy()[i], axis=1) # khoảng cách từ tài khoản này tới MỌI tài khoản
cung = (nhan4 == nhan4[i]); cung[i] = False # cùng cụm, trừ chính nó
a = kc[cung].mean()
b = min(kc[nhan4 == c].mean() for c in range(4) if c != nhan4[i]) # cụm khác GẦN NHẤT (trung bình nhỏ nhất)
print(f"{IP_VI_DU}: ở cụm {nhan4[i]} | a (tới bạn cùng cụm) = {a:.2f} | b (tới cụm gần nhất khác) = {b:.2f}")
print(f"silhouette = (b - a) / max(a, b) = ({b:.2f} - {a:.2f}) / {max(a, b):.2f} = {(b - a) / max(a, b):.3f}")
print(f"thư viện sklearn tính được: {silhouette_samples(X, nhan4)[i]:.3f} (phải giống nhau)")In [ ]:
from sklearn.metrics import silhouette_score
dong = []
for k in range(2, 9):
km_ = KMeans(n_clusters=k, n_init=10, random_state=RANDOM_STATE).fit(X)
dong.append({"K": k, "inertia": km_.inertia_, "silhouette": silhouette_score(X, km_.labels_, sample_size=2000, random_state=0)})
chon_k = pd.DataFrame(dong).set_index("K")
hinh, o = plt.subplots(1, 2, figsize=(12, 3.5))
chon_k.inertia.plot(ax=o[0], marker="o", title="Inertia (tìm chỗ gãy)")
chon_k.silhouette.plot(ax=o[1], marker="o", color="green", title="Silhouette (càng cao càng tốt)")
plt.tight_layout(); plt.show()
chon_k.round(3)In [ ]:
# ✏️ SỬA: số cụm (thử 3, 5)
SO_CUM = 4
kmeans = KMeans(n_clusters=SO_CUM, n_init=10, random_state=RANDOM_STATE).fit(X)
ho_so["cum"] = kmeans.labels_ # mỗi tài khoản thuộc cụm số mấy
# 📦 kmeans = mô hình đã học (kmeans.cluster_centers_ = tọa độ SO_CUM tâm) · ho_so.cum = cụm của từng tài khoản
print("Số tài khoản mỗi cụm:", ho_so.cum.value_counts().sort_index().to_dict())In [ ]:
chan_dung = ho_so.groupby("cum").agg(
so_tai_khoan=("so_ket_noi", "size"), ket_noi=("so_ket_noi", "median"), sai=("dang_nhap_sai", "median"),
dung=("dang_nhap_dung", "median"), phien_tb=("phien_tb_giay", "median"), lenh_moi_phien=("lenh_moi_phien", "median"),
lenh_khac_nhau=("so_lenh_khac_nhau", "median"), ti_le_proxy=("dung_proxy", "mean"), tai_file_tb=("so_lan_tai_file", "mean"),
so_ngay=("so_ngay", "median"))
chan_dung["cong_cu_hay_gap"] = ho_so.groupby("cum").cong_cu.agg(lambda s: ", ".join(f"{k} ({v})" for k, v in s.value_counts().head(2).items()))
# 📦 chan_dung = mỗi cụm 1 hàng: giá trị điển hình + công cụ hay gặp
chan_dung.round(2)In [ ]:
tam_cua_moi_tk = kmeans.cluster_centers_[ho_so.cum] # tọa độ tâm cụm của từng tài khoản
khoang_cach = np.linalg.norm(X.to_numpy() - tam_cua_moi_tk, axis=1) # khoảng cách thẳng (Euclid) tới tâm đó
moc_99 = np.percentile(khoang_cach, 99)
ho_so["diem_la"] = (100 * khoang_cach / moc_99).clip(0, 100)
# 📦 khoang_cach = độ lệch của từng tài khoản khỏi nhóm mình · ho_so.diem_la = điểm khác thường 0-100
# ✏️ SỬA: 2 ngưỡng ưu tiên
NGUONG_XEM, NGUONG_DIEU_TRA = 20, 60
plt.figure(figsize=(10, 3.5))
plt.hist(ho_so.diem_la, bins=50, color="#457b9d")
plt.axvline(NGUONG_XEM, color="orange", ls="--"); plt.axvline(NGUONG_DIEU_TRA, color="red", ls="--")
plt.xlabel("điểm khác thường (0 = điển hình của nhóm, 100 = khác nhất)"); plt.ylabel("số tài khoản"); plt.yscale("log"); plt.show()
print(f"Khoảng cách: trung vị {np.median(khoang_cach):.2f}, phân vị 99% = {moc_99:.2f}")In [ ]:
muc = pd.cut(ho_so.diem_la, [-1, NGUONG_XEM, NGUONG_DIEU_TRA, 101], right=False, # right=False: khoảng [a, b)
labels=["🟢 bot điển hình", "🟡 xem khi có thời gian", "🔴 điều tra ngay"])
ho_so["muc"] = muc
print(ho_so.muc.value_counts().reindex(["🟢 bot điển hình", "🟡 xem khi có thời gian", "🔴 điều tra ngay"]).to_string())
print("\n8 tài khoản KHÁC THƯỜNG nhất:")
ho_so.sort_values("diem_la", ascending=False).head(8)[["so_ket_noi", "dang_nhap_sai", "dang_nhap_dung", "phien_tb_giay", "dung_proxy", "so_lan_tai_file", "so_ngay", "cong_cu", "cum", "diem_la"]].round(1)In [ ]:
tam_df = pd.DataFrame(kmeans.cluster_centers_[ho_so.cum], index=X.index, columns=SO) # tâm cụm của từng tài khoản, dạng bảng
binh_phuong = (X - tam_df) ** 2 # độ lệch bình phương trên từng cột
dong_gop = binh_phuong.div(binh_phuong.sum(axis=1), axis=0) * 100 # % đóng góp của mỗi cột vào khoảng cách (mỗi hàng cộng = 100)
# 📦 dong_gop = mỗi tài khoản × 12 cột: cột đó chiếm bao nhiêu % "độ khác thường"
SO_TOP = 8 # ✏️ SỬA: xem bao nhiêu tài khoản đầu bảng
dong = []
for ip in ho_so.sort_values("diem_la", ascending=False).index[:SO_TOP]:
c = ho_so.at[ip, "cum"]
trung_vi_cum = ho_so.loc[ho_so.cum == c, SO].median() # tài khoản "điển hình" của cụm đó
cot1, cot2 = dong_gop.loc[ip].nlargest(2).index # 2 cột đóng góp nhiều nhất
dong.append({"IP": ip, "cụm": c, "điểm": round(ho_so.at[ip, "diem_la"]),
"cột lệch nhất": f"{cot1} = {ho_so.at[ip, cot1]} (cụm: {trung_vi_cum[cot1]:g}) → {dong_gop.at[ip, cot1]:.0f}%",
"cột thứ hai": f"{cot2} = {ho_so.at[ip, cot2]} (cụm: {trung_vi_cum[cot2]:g}) → {dong_gop.at[ip, cot2]:.0f}%"})
print(pd.DataFrame(dong).to_string(index=False))
print("\nTỉ lệ tài khoản dùng proxy trong từng cụm:", ho_so.groupby("cum").dung_proxy.mean().round(3).to_dict())
print("Số tài khoản bị cắt ở đúng 100 điểm:", int((ho_so.diem_la >= 100).sum()))In [ ]:
%%bash
IP=192.42.116.96 # ✏️ SỬA: thử 80.94.95.118
echo "--- log honeypot 30 ngày của $IP (giờ UTC) ---"
grep -ah "\"$IP\"" /opt/csai/data/honeypot_m1/* | grep -aoE '"eventid": "[^"]*"|"timestamp": "[^"]*"|"version": "[^"]*"|"username": "[^"]*"|"password": "[^"]*"' | paste -sd' ' | sed 's/"eventid"/\n"eventid"/g' | head -12
echo; echo "--- đường hầm $IP đã xin (ngày, đích, cổng, số lần) ---"
grep -a ",$IP," /opt/csai/data/m6_honeypot/tunnels.csvIn [ ]:
duong_ham = pd.read_csv("/opt/csai/data/m6_honeypot/tunnels.csv") # ngày, IP nguồn, đích, cổng, số lần
# 📦 duong_ham = mọi yêu cầu mở đường hầm mà honeypot ghi được (toàn bộ log, theo ngày)
for dai in ["192.42.116.", "80.94.95."]: # ✏️ SỬA: thêm dải khác
ho = ho_so[ho_so.index.str.startswith(dai)]
dh = duong_ham[duong_ham.src_ip.str.startswith(dai)]
print(f"Dải {dai}0/24")
print(f" hồ sơ 30 ngày : {len(ho)} IP, {int(ho.dung_proxy.sum())} IP dùng proxy, công cụ {ho.cong_cu.value_counts().to_dict()}, "
f"đăng nhập đúng tổng {int(ho.dang_nhap_dung.sum())}")
print(f" đường hầm : {len(dh)} dòng từ {dh.src_ip.nunique()} IP, {dh.ngay_vn.nunique()} ngày ({dh.ngay_vn.min()} → {dh.ngay_vn.max()}), "
f"đích hay gặp {dh.dst_ip.value_counts().head(3).to_dict()}, cổng {dh.dst_port.value_counts().to_dict()}")
print(f"\nCả honeypot: {duong_ham.src_ip.nunique()} IP từng xin đường hầm")In [ ]:
# ✏️ SỬA: các tài khoản muốn hỏi (gặp ở các lab trước)
for ip in ["185.246.128.133", "2.57.121.112", "130.12.182.196", "1.10.195.220"]:
h = ho_so.loc[ip]
print(f"{ip:16s} cụm {h.cum} | khác thường {h.diem_la:5.1f} -> {h.muc} | đăng nhập sai {h.dang_nhap_sai}, đúng {h.dang_nhap_dung}, proxy {h.dung_proxy}, {h.so_ngay} ngày | luật cứng Phần 1: {'KHÓA' if bi_khoa[ip] else 'cho qua'}")In [ ]:
cot = ho_so.so_ket_noi
ba_tk = {"tài khoản 'giữa' (trung vị)": cot.median(), "phân vị 90%": cot.quantile(0.9), "lớn nhất": cot.max()}
z_khong_nen = lambda v: (v - cot.mean()) / cot.std(ddof=0) # chỉ chuẩn hoá
cot_nen = np.log1p(cot)
z_co_nen = lambda v: (np.log1p(v) - cot_nen.mean()) / cot_nen.std(ddof=0) # nén log1p rồi mới chuẩn hoá
vi_du = pd.DataFrame({"số kết nối": ba_tk,
"KHÔNG nén: vị trí": {k: z_khong_nen(v) for k, v in ba_tk.items()},
"CÓ nén: vị trí": {k: z_co_nen(v) for k, v in ba_tk.items()}}).round(2)
# 📦 vi_du = 3 tài khoản × (số gốc, vị trí sau khi chuẩn hoá không nén, vị trí sau khi nén + chuẩn hoá)
print(vi_du.to_string())
k = vi_du.iloc[:, 1]; n = vi_du.iloc[:, 2]
print(f"\nKHÔNG nén: 'giữa' → 90% cách {k.iloc[1] - k.iloc[0]:.2f}, còn 90% → lớn nhất cách {k.iloc[2] - k.iloc[1]:.2f} (gấp {(k.iloc[2] - k.iloc[1]) / (k.iloc[1] - k.iloc[0]):.0f} lần)")
print(f"CÓ nén : 'giữa' → 90% cách {n.iloc[1] - n.iloc[0]:.2f}, còn 90% → lớn nhất cách {n.iloc[2] - n.iloc[1]:.2f}")In [ ]:
# Giá trị cực lớn: so 5 tài khoản lớn nhất với tài khoản "giữa" (trung vị) ở vài cột
for cot in ["so_ket_noi", "dang_nhap_sai", "dang_nhap_dung", "phien_dai_nhat_giay"]:
top = ho_so[cot].nlargest(5) # 5 giá trị lớn nhất của cột
print(f"{cot:20s} trung vị {ho_so[cot].median():6.1f} | 5 lớn nhất: {', '.join(f'{v:,.0f} ({ip})' for ip, v in top.items())}")
print()
X_khong_nen = StandardScaler().fit_transform(ho_so[SO]) # chỉ chuẩn hóa, KHÔNG log1p
km_khong_nen = KMeans(n_clusters=SO_CUM, n_init=10, random_state=RANDOM_STATE).fit(X_khong_nen)
print("KHÔNG nén — số tài khoản mỗi cụm:", sorted(np.bincount(km_khong_nen.labels_).tolist(), reverse=True))
print("CÓ nén log1p — số tài khoản mỗi cụm:", sorted(np.bincount(kmeans.labels_).tolist(), reverse=True))
# Không nén: mỗi cụm là ai? (cụm nhỏ do số cực lớn hoặc cột 0/1 hiếm tạo ra)
print("\nKhông nén — chân dung từng cụm:")
print(ho_so.assign(c=km_khong_nen.labels_).groupby("c").agg(so_tai_khoan=("so_ket_noi", "size"), ket_noi=("so_ket_noi", "median"),
sai=("dang_nhap_sai", "median"), dung=("dang_nhap_dung", "median"), phien_dai_nhat=("phien_dai_nhat_giay", "median"),
ti_le_proxy=("dung_proxy", "mean")).sort_values("so_tai_khoan", ascending=False).round(2).to_string())In [ ]:
# Với K = 5: cụm nào tách ra thêm?
km5 = KMeans(n_clusters=5, n_init=10, random_state=RANDOM_STATE).fit(X)
bang5 = ho_so.assign(cum5=km5.labels_).groupby("cum5").agg(so_tai_khoan=("so_ket_noi", "size"), ti_le_proxy=("dung_proxy", "mean"),
tai_file_tb=("so_lan_tai_file", "mean"), dung=("dang_nhap_dung", "median"))
print("Bảng chéo: cụm (K=4, hàng) chia thành cụm nào khi K=5 (cột):")
print(pd.crosstab(ho_so.cum, km5.labels_).to_string())
bang5.round(2)In [ ]:
from sklearn.metrics import adjusted_rand_score
# (1) Chấm lại 8 tài khoản đầu bảng 5.2b với K = 5
tam5 = km5.cluster_centers_[km5.labels_]
kc5 = np.linalg.norm(X.to_numpy() - tam5, axis=1)
diem5 = pd.Series((100 * kc5 / np.percentile(kc5, 99)).clip(0, 100), index=X.index) # cùng công thức điểm khác thường
top8 = ho_so.sort_values("diem_la", ascending=False).index[:8]
print(pd.DataFrame({"điểm K=4": ho_so.loc[top8, "diem_la"].round(0), "điểm K=5": diem5[top8].round(0),
"cụm K=5": km5.labels_[X.index.get_indexer(top8)]}).to_string())
print(f"Số tài khoản ≥ 60 điểm ('điều tra ngay'): K=4 {int((ho_so.diem_la >= 60).sum())} | K=5 {int((diem5 >= 60).sum())}")
# (2) Ổn định: chạy mỗi K với 5 hạt giống khác nhau, so từng cặp bằng ARI
print("\nĐộ ổn định (ARI trung bình giữa các lần chạy khác hạt giống):")
for k in [3, 4, 5, 6, 8]: # ✏️ SỬA: thêm K khác
nhan = [KMeans(n_clusters=k, n_init=1, random_state=h).fit(X).labels_ for h in range(5)]
ari = [adjusted_rand_score(nhan[i], nhan[j]) for i in range(5) for j in range(i + 1, 5)]
print(f" K={k}: {np.mean(ari):.2f} (thấp nhất {min(ari):.2f})")In [ ]:
from sklearn.ensemble import IsolationForest
rung = IsolationForest(n_estimators=300, random_state=RANDOM_STATE).fit(X)
diem_if = -rung.decision_function(X) # đảo dấu: càng LỚN càng bất thường (cho cùng chiều với khoảng cách)
# ✏️ SỬA: so bao nhiêu tài khoản đứng đầu
TOP = 50
top_km = set(ho_so.index[np.argsort(-khoang_cach)[:TOP]])
top_if = set(ho_so.index[np.argsort(-diem_if)[:TOP]])
print(f"Tương quan giữa khoảng cách K-Means và điểm Isolation Forest: {np.corrcoef(khoang_cach, diem_if)[0, 1]:.2f}")
print(f"Trong {TOP} tài khoản lạ nhất của mỗi cách: {len(top_km & top_if)} tài khoản TRÙNG nhau")
print("\nChỉ K-Means xếp vào top (ví dụ):", list(top_km - top_if)[:4])
print("Chỉ Isolation Forest xếp vào top (ví dụ):", list(top_if - top_km)[:4])
plt.figure(figsize=(6, 4.5))
plt.scatter(khoang_cach, diem_if, s=5, alpha=0.4)
plt.xlabel("K-Means: khoảng cách tới tâm cụm"); plt.ylabel("Isolation Forest: độ bất thường"); plt.title("Mỗi chấm = 1 tài khoản"); plt.show()In [ ]:
nhan_gv = pd.read_csv("/opt/csai/giangvien/attackers_labels.csv").set_index("src_ip")
# 📦 nhan_gv = mỗi IP 1 hàng, 4 cột nhãn (bảng trên)
print("nhan_gv.shape =", nhan_gv.shape, "-> (số IP, số cột)")
print("\nSố IP mỗi nhóm (hassh_group):")
print(nhan_gv.hassh_group.value_counts().to_string())
print("\nSố IP có ghi chú '(conflict)':", nhan_gv.tool_fingerprint_attributions.str.contains("conflict", na=False).sum())
print("Số IP đã xác nhận chắc chắn (ta_confirmed):", nhan_gv.ta_confirmed.notna().sum())
nhan_gv.loc[["1.10.195.220", "130.12.182.196", "185.246.128.133"]]In [ ]:
TEN_CUM = {0: "0 gõ cửa", 1: "1 vào và làm việc", 2: "2 thử 1 lần", 3: "3 cài đồ"} # tên đặt ở Phần 4 (với SO_CUM = 4)
gop = ho_so.join(nhan_gv) # join: nối 2 bảng theo chỉ mục (IP)
bang_cheo = pd.crosstab(gop.hassh_group, gop.cum.map(TEN_CUM))
so_ip_nhom = bang_cheo.sum(axis=1) # tổng IP mỗi nhóm (cộng theo hàng)
bang_cheo["% ở cụm chính"] = (bang_cheo.max(axis=1) / so_ip_nhom * 100).round(0)
bang_cheo["số IP"] = so_ip_nhom
# 📦 gop = hồ sơ + nhãn · bang_cheo = số IP theo (nhóm, cụm) + % ở cụm đông nhất + tổng IP của nhóm
# Chú ý: nhóm chỉ 1–2 IP thì "100%" không nói lên gì — đọc kèm cột "số IP"
print(bang_cheo.sort_values("% ở cụm chính", ascending=False).to_string())
ti_le_hang = pd.crosstab(gop.hassh_group, gop.cum.map(TEN_CUM), normalize="index") * 100 # mỗi hàng chia cho tổng hàng
plt.figure(figsize=(8, 4.5))
plt.imshow(ti_le_hang, cmap="Blues", aspect="auto")
for i in range(ti_le_hang.shape[0]):
for j in range(ti_le_hang.shape[1]):
plt.text(j, i, f"{ti_le_hang.iat[i, j]:.0f}%", ha="center", va="center", fontsize=8,
color="white" if ti_le_hang.iat[i, j] > 60 else "black")
plt.xticks(range(ti_le_hang.shape[1]), ti_le_hang.columns, rotation=20)
plt.yticks(range(ti_le_hang.shape[0]), [f"{nhom} ({so_ip_nhom[nhom]} IP)" for nhom in ti_le_hang.index])
plt.title("Mỗi nhóm theo nhãn: bao nhiêu % IP rơi vào từng cụm"); plt.colorbar(label="% IP của nhóm"); plt.show()In [ ]:
from sklearn.metrics import adjusted_rand_score, homogeneity_score, completeness_score
dong_so_khop = []
for ten_cach_chia, nhan_cum in [("K-Means K=4 (Phần 3.3)", ho_so.cum), ("K-Means K=5 (Phần 6.2)", km5.labels_)]:
dong_so_khop.append({"cách chia": ten_cach_chia, "ARI": adjusted_rand_score(gop.hassh_group, nhan_cum),
"độ thuần": homogeneity_score(gop.hassh_group, nhan_cum), "độ trọn": completeness_score(gop.hassh_group, nhan_cum)})
pd.DataFrame(dong_so_khop).set_index("cách chia").round(3)In [ ]:
cum_proxy = int(pd.Series(ho_so.dung_proxy.to_numpy()).groupby(km5.labels_).mean().idxmax()) # cụm K=5 có tỉ lệ proxy cao nhất
trong_cum_proxy = gop[km5.labels_ == cum_proxy]
# 📦 trong_cum_proxy = các tài khoản của cụm proxy (K=5), kèm nhãn
print(f"Cụm proxy (K=5) = cụm số {cum_proxy}: {len(trong_cum_proxy)} tài khoản")
print(trong_cum_proxy.hassh_group.value_counts().to_string())
print("\nCác tài khoản trong cụm này có nhãn khác 'other':")
print(trong_cum_proxy[trong_cum_proxy.hassh_group != "other"][["hassh_group", "ta_confirmed", "so_ket_noi", "dang_nhap_dung", "so_ngay", "cong_cu"]].to_string())
cum_cua_lithuania = sorted({int(c) for c in km5.labels_[(gop.hassh_group == "TA-07 Lithuania proxy").to_numpy()]}) # int(): bỏ kiểu số của numpy cho dễ đọc
print("\nNhóm 'TA-07 Lithuania proxy' nằm ở cụm K=5 nào:", cum_cua_lithuania)In [ ]:
from pathlib import Path
THU_MUC_RA = Path.home() / "lab" / "out_lab7"
THU_MUC_RA.mkdir(parents=True, exist_ok=True)
ho_so.sort_values("diem_la", ascending=False)[["cum", "diem_la", "muc"] + SO + ["cong_cu"]].to_csv(THU_MUC_RA / "uu_tien_dieu_tra.csv")
dem_muc = ho_so.muc.value_counts()
bao_cao = f'''BÁO CÁO MODULE 7: UBA & K-MEANS TRÊN {len(ho_so):,} "TÀI KHOẢN" (IP tấn công honeypot, 24/08 - 21/09/2026)
=========================================================================================
Luật cứng (sai > {NGUONG_SAI} hoặc tải file): khóa {int(bi_khoa.sum())} tài khoản; {len(lot)} tài khoản đăng nhập đúng nhiều / dùng proxy vẫn lọt
K-Means K={SO_CUM} : cụm {ho_so.cum.value_counts().sort_index().to_dict()}
Khớp nhãn GV (ARI) : K=4 {dong_so_khop[0]['ARI']:.2f} | K=5 {dong_so_khop[1]['ARI']:.2f}; nhóm (≥ 10 IP) gọn nhất trong 1 cụm: {bang_cheo[bang_cheo['số IP'] >= 10]['% ở cụm chính'].idxmax()} ({bang_cheo[bang_cheo['số IP'] >= 10]['% ở cụm chính'].max():.0f}%)
Ưu tiên điều tra : bot điển hình {int(dem_muc.get('🟢 bot điển hình', 0))} | xem khi có thời gian {int(dem_muc.get('🟡 xem khi có thời gian', 0))} | điều tra ngay {int(dem_muc.get('🔴 điều tra ngay', 0))}
Khác thường nhất : {', '.join(ho_so.sort_values('diem_la', ascending=False).index[:3])}
File ưu tiên : {THU_MUC_RA / 'uu_tien_dieu_tra.csv'}
Khuyến nghị:
- Chặn tự động mọi IP tấn công; dùng điểm khác thường để xếp THỨ TỰ ĐIỀU TRA — người chỉ đọc nhóm 🔴 trước.
- Áp cho tài khoản nhân viên thì cùng phép tính thành điểm uy tín (cho qua / xác minh / khoá), ngưỡng tùy tài khoản.
- Luôn nén + chuẩn hóa trước K-Means; chọn K bằng cả số liệu (inertia, silhouette) lẫn hiểu biết nghiệp vụ.
- Đọc chân dung cụm, đặt tên, rồi mới hành động — máy gom, người quyết định.
- Kiểm chứng cụm bằng một ít dữ liệu có nhãn từ điều tra (như Phần 8) trước khi tin vào tên cụm.
'''
(THU_MUC_RA / "bao_cao_lab7.txt").write_text(bao_cao, encoding="utf-8")
print(bao_cao)