66 KiB
66 KiB
In [ ]:
import glob
import warnings
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
pd.set_option("display.width", 200)
RANDOM_STATE = 42
# parse_dates: đọc cột gio_vn thành kiểu thời gian (để vẽ và lọc theo ngày / tháng)
theo_gio = pd.read_csv("/opt/csai/data/m6_honeypot/hourly.csv", parse_dates=["gio_vn"])
# 📦 theo_gio = bảng, mỗi dòng = 1 giờ (giờ Việt Nam), các cột là số liệu của giờ đó
print("theo_gio.shape =", theo_gio.shape, "-> (số giờ, số cột)")
print("Từ", theo_gio.gio_vn.min(), "đến", theo_gio.gio_vn.max())
theo_gio.head(3)In [ ]:
du_gio = pd.date_range(theo_gio.gio_vn.min(), theo_gio.gio_vn.max(), freq="h") # mọi giờ lẽ ra phải có
thieu = du_gio.difference(theo_gio.gio_vn) # giờ không có dòng nào
print(f"Lẽ ra có {len(du_gio)} giờ, bảng có {len(theo_gio)} -> thiếu {len(thieu)} giờ")
print("Thiếu theo ngày:", pd.Series(thieu.date).value_counts().sort_index().to_dict())In [ ]:
cac_file = sorted(glob.glob("/opt/csai/data/honeypot_m1/cowrie.json.*"))
log = pd.concat(pd.read_json(f, lines=True, dtype=False, convert_dates=False) for f in cac_file)
ket_noi = log[log.eventid == "cowrie.session.connect"].copy()
ket_noi["t"] = pd.to_datetime(ket_noi.timestamp, utc=True)
# 📦 log = log gốc 30 ngày · ket_noi = các dòng kết nối, thêm cột thời gian t
# ✏️ SỬA: ngày chia đôi (trước = học danh sách chặn, sau = thử)
MOC = pd.Timestamp("2026-09-08", tz="UTC")
truoc, sau = ket_noi[ket_noi.t < MOC], ket_noi[ket_noi.t >= MOC]
danh_sach_chan = set(truoc.src_ip) # mọi IP đã thấy trước mốc
la_ip_moi = ~sau.src_ip.isin(danh_sach_chan) # True = IP chưa từng thấy
print(f"Danh sách chặn (IP thấy trước {MOC:%d/%m}): {len(danh_sach_chan)} IP")
print(f"Sau {MOC:%d/%m}: {sau.src_ip.nunique()} IP tấn công, trong đó {sau.src_ip[la_ip_moi].nunique()} IP MỚI (không có trong danh sách)")
print(f"-> {la_ip_moi.mean():.1%} số kết nối sau mốc đến từ IP mới: danh sách chặn KHÔNG chặn được")In [ ]:
ket_noi = ket_noi.sort_values("t")
# Khoảng cách (giây) từ mỗi kết nối tới kết nối TRƯỚC của cùng IP
ket_noi["cach"] = ket_noi.groupby("src_ip").t.diff().dt.total_seconds()
def do_lech_tuong_doi(x):
"""Độ lệch tuyệt đối trung vị so với nhịp, chia cho nhịp (0 = đều tuyệt đối)."""
x = x.dropna()
nhip_ = x.median()
return np.median(np.abs(x - nhip_)) / max(nhip_, 1)
# ✏️ SỬA: chỉ xét IP có ít nhất bao nhiêu kết nối (ít quá thì "nhịp" không có nghĩa)
IT_NHAT = 50
so_lan = ket_noi.src_ip.value_counts()
du_nhieu = ket_noi[ket_noi.src_ip.isin(so_lan[so_lan >= IT_NHAT].index)] # chỉ giữ kết nối của các IP đủ nhiều lần
nhom = du_nhieu.groupby("src_ip")
beacon = pd.DataFrame({"so_ket_noi": nhom.size(), "nhip": nhom.cach.median(), "do_lech": nhom.cach.apply(do_lech_tuong_doi),
"so_ngay": nhom.t.apply(lambda t: t.dt.date.nunique())})
# 📦 beacon = bảng mỗi IP (đủ nhiều kết nối) 1 hàng: số kết nối, nhịp (giây), độ lệch, số ngày
print(f"{len(beacon)} IP có >= {IT_NHAT} kết nối. 8 IP ĐỀU NHỊP nhất (nhịp > 10 giây):")
beacon[beacon.nhip > 10].sort_values("do_lech").head(8).round(3)In [ ]:
# ✏️ SỬA: 2 IP muốn so (lấy từ bảng 2.1)
IP_DEU, IP_LON_XON = "130.49.213.228", "185.246.128.133"
hinh, o = plt.subplots(2, 1, figsize=(14, 4))
for truc, ip in zip(o, [IP_DEU, IP_LON_XON]):
t_ip = ket_noi[ket_noi.src_ip == ip].t.head(40) # 40 kết nối đầu của IP
truc.vlines(t_ip, 0, 1, color="#e76f51"); truc.set_yticks([])
truc.set_title(f"{ip}: nhịp {beacon.loc[ip, 'nhip']:,.1f} giây, độ lệch {beacon.loc[ip, 'do_lech']:.3f} (40 kết nối đầu)")
plt.tight_layout(); plt.show()In [ ]:
chuoi = theo_gio.set_index("gio_vn").ket_noi # chuỗi số kết nối, chỉ mục là thời gian
nguong_co_dinh = chuoi.mean() + 3 * chuoi.std()
# ✏️ SỬA: K càng lớn ngưỡng động càng "khó tính" (ít báo hơn). Thử 3, 6, 10
K = 6
trung_vi_7n = chuoi.rolling("7D").median() # rolling("7D") = cửa sổ trượt 7 ngày tính ngược về trước
mad_7n = (chuoi - trung_vi_7n).abs().rolling("7D").median().clip(lower=1) # clip: tối thiểu 1 để ngưỡng không bằng 0
nguong_dong = trung_vi_7n + K * mad_7n
vuot_co_dinh, vuot_dong = chuoi > nguong_co_dinh, chuoi > nguong_dong
# 📦 chuoi = số kết nối theo giờ · nguong_dong = ngưỡng động từng giờ · vuot_* = True ở giờ vượt ngưỡng
print(f"Ngưỡng cố định = {nguong_co_dinh:,.0f} kết nối/giờ -> báo {vuot_co_dinh.sum()} giờ")
print(f"Ngưỡng động (K={K}) -> báo {vuot_dong.sum()} giờ (trong đó {(vuot_dong & vuot_co_dinh).sum()} giờ ngưỡng cố định cũng báo)")
plt.figure(figsize=(15, 4))
plt.plot(chuoi.index, chuoi, lw=0.5, label="kết nối / giờ")
plt.plot(nguong_dong.index, nguong_dong, lw=0.8, color="orange", label=f"ngưỡng động (K={K})")
plt.axhline(nguong_co_dinh, color="red", label="ngưỡng cố định")
plt.scatter(chuoi.index[vuot_dong], chuoi[vuot_dong], s=8, color="red", zorder=3, label="vượt ngưỡng động")
plt.yscale("log"); plt.ylabel("kết nối / giờ (thang log)"); plt.legend(loc="upper left"); plt.show()In [ ]:
cot_xem = ["ket_noi", "so_ip", "top_ip", "top_ip_ket_noi", "top_dai", "top_dai_ket_noi"]
bung_no = theo_gio.set_index("gio_vn")[vuot_dong.to_numpy()].sort_values("ket_noi", ascending=False)
bung_no = bung_no.assign(ti_le_dai=(bung_no.top_dai_ket_noi / bung_no.ket_noi).round(2))
print("8 giờ bùng nổ lớn nhất (vượt ngưỡng động):")
bung_no[cot_xem + ["ti_le_dai"]].head(8)In [ ]:
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
COT = ["ket_noi", "dang_nhap_sai", "dang_nhap_dung", "lenh", "tai_file", "duong_ham", "so_ip"]
# Nén số lớn (log1p) rồi chuẩn hóa, như Module 3
X_gio = pd.DataFrame(StandardScaler().fit_transform(np.log1p(theo_gio[COT])), columns=COT)
# ✏️ SỬA: tỉ lệ giờ coi là bất thường (0.01 = 1%)
TI_LE = 0.01
rung = IsolationForest(n_estimators=300, contamination=TI_LE, random_state=RANDOM_STATE).fit(X_gio)
theo_gio["diem"] = rung.decision_function(X_gio) # càng âm càng bất thường
# 📦 X_gio = 7 cột đã chuẩn bị · rung = Isolation Forest đã học · theo_gio.diem = điểm của từng giờ
print(f"Số giờ bị gắn cờ: {(rung.predict(X_gio) == -1).sum()} / {len(theo_gio)}")
theo_gio.sort_values("diem").head(8)[["gio_vn"] + COT + ["top_ip", "diem"]].round(3)In [ ]:
import warnings
warnings.filterwarnings("ignore", message="IProgress not found")
import shap
giai_thich = shap.TreeExplainer(rung)
def vi_sao(vi_tri):
"""In giá trị 7 cột và đóng góp vào cảnh báo (−SHAP) của 1 giờ."""
sv = giai_thich(X_gio.iloc[[vi_tri]])
return pd.DataFrame({"giá trị giờ này": theo_gio.loc[vi_tri, COT], "trung vị 5 tháng": theo_gio[COT].median(),
"đóng góp vào cảnh báo": -sv.values[0]}, index=COT).sort_values("đóng góp vào cảnh báo", ascending=False).round(2)
hai_gio = theo_gio.sort_values("diem").index[:2] # 2 giờ bất thường nhất
for vt in hai_gio:
print(f"=== {theo_gio.loc[vt, 'gio_vn']:%d/%m %H:%M} (điểm {theo_gio.loc[vt, 'diem']:+.3f}) ===")
print(vi_sao(vt).to_string(), "\n")In [ ]:
theo_gio["thang"] = theo_gio.gio_vn.dt.month
theo_thang = theo_gio.groupby("thang")[COT[:-1]].sum()
# 📦 theo_thang = tổng mỗi cột theo tháng
theo_thang.plot(kind="bar", subplots=True, layout=(2, 3), figsize=(14, 5), legend=False, rot=0, color="#457b9d")
plt.tight_layout(); plt.show()
theo_thangIn [ ]:
duong_ham = pd.read_csv("/opt/csai/data/m6_honeypot/tunnels.csv")
# 📦 duong_ham = mỗi dòng: ngày, IP kẻ tấn công, đích muốn tới, cổng đích, số lần
print("duong_ham.shape =", duong_ham.shape, f"| {duong_ham.so_lan.sum():,} yêu cầu từ {duong_ham.src_ip.nunique()} IP tới {duong_ham.dst_ip.nunique()} đích")
print("\nTheo CỔNG đích (80 = web, 25 = gửi thư, 443 = web mã hóa, 53 = DNS):")
print(duong_ham.groupby("dst_port").so_lan.sum().sort_values(ascending=False).head(6).to_string())
print("\n6 ĐÍCH được xin nhiều nhất:")
print(duong_ham.groupby("dst_ip").so_lan.sum().sort_values(ascending=False).head(6).to_string())
print("\n5 IP xin nhiều nhất:")
print(duong_ham.groupby("src_ip").so_lan.sum().sort_values(ascending=False).head(5).to_string())In [ ]:
from sklearn.cluster import KMeans
nhom_ip = duong_ham.groupby("src_ip")
hanh_vi = pd.DataFrame({"so_lan": nhom_ip.so_lan.sum(), "so_dich": nhom_ip.dst_ip.nunique(), "so_cong": nhom_ip.dst_port.nunique(),
"so_ngay": nhom_ip.ngay_vn.nunique(),
"ti_le_cong_25": nhom_ip.dst_port.apply(lambda s: (s == 25).mean()),
"ti_le_cong_443": nhom_ip.dst_port.apply(lambda s: (s == 443).mean())})
X_ip = StandardScaler().fit_transform(np.log1p(hanh_vi[["so_lan", "so_dich", "so_cong", "so_ngay"]]).join(hanh_vi[["ti_le_cong_25", "ti_le_cong_443"]]))
# ✏️ SỬA: số cụm. Thử 3, 5
SO_CUM = 4
hanh_vi["cum"] = KMeans(n_clusters=SO_CUM, n_init=10, random_state=RANDOM_STATE).fit_predict(X_ip)
# 📦 hanh_vi = mỗi IP 1 hàng + cột 'cum' (số cụm K-Means gán)
chan_dung = hanh_vi.groupby("cum").agg(so_ip=("so_lan", "size"), tong_yeu_cau=("so_lan", "sum"), lan_trung_vi=("so_lan", "median"),
dich_trung_vi=("so_dich", "median"), ngay_trung_vi=("so_ngay", "median"),
ti_le_cong_25=("ti_le_cong_25", "mean"), ti_le_cong_443=("ti_le_cong_443", "mean")).round(2)
print("Chân dung từng cụm:")
print(chan_dung.to_string())
print("\nVí dụ 3 IP mỗi cụm (nhiều yêu cầu nhất):")
for c in sorted(hanh_vi.cum.unique()):
print(f" cụm {c}:", list(hanh_vi[hanh_vi.cum == c].so_lan.sort_values(ascending=False).index[:3]))In [ ]:
from sklearn.metrics import precision_score, recall_score, f1_score
# ✏️ SỬA: định nghĩa nhãn
NGUONG_KN, TI_LE_DAI = 500, 0.8
nhan = ((theo_gio.ket_noi >= NGUONG_KN) & (theo_gio.top_dai_ket_noi / theo_gio.ket_noi >= TI_LE_DAI)).astype(int).to_numpy()
print(f"Theo nhãn này có {nhan.sum()} 'giờ bị áp đảo' trên {len(nhan)} giờ")
bo_phat_hien = {
"Ngưỡng cố định (Phần 3)": vuot_co_dinh.to_numpy().astype(int),
f"Ngưỡng động K={K} (Phần 3)": vuot_dong.to_numpy().astype(int),
"Isolation Forest 7 cột (Phần 4)": (rung.predict(X_gio) == -1).astype(int),
}
dong = []
for ten, du_doan in bo_phat_hien.items():
dong.append({"bộ phát hiện": ten, "số giờ báo": du_doan.sum(), "precision": precision_score(nhan, du_doan, zero_division=0),
"recall": recall_score(nhan, du_doan), "F1": f1_score(nhan, du_doan)})
pd.DataFrame(dong).set_index("bộ phát hiện").round(3)In [ ]:
cach_that = ket_noi[ket_noi.src_ip == IP_DEU].cach.dropna().to_numpy() # các khoảng cách thật của IP đều nhịp
ngau_nhien = np.random.RandomState(0)
# ✏️ SỬA: các mức jitter thử (giây). Ngưỡng nghi ngờ beaconing ở đây: độ lệch < NGUONG_DEU
NGUONG_DEU = 0.05
for J in [0, 5, 30, 60, 150]:
cach_moi = cach_that + ngau_nhien.uniform(-J, J, len(cach_that))
dl = np.median(np.abs(cach_moi - np.median(cach_moi))) / np.median(cach_moi)
print(f"jitter ±{J:3d} giây: độ lệch {dl:.3f} -> {'BỊ PHÁT HIỆN (đều nhịp)' if dl < NGUONG_DEU else 'né được'}")
print(f"\nNhưng: IP này vẫn kết nối {beacon.loc[IP_DEU, 'so_ket_noi']} lần, nhịp trung vị {np.median(cach_that):.0f} giây — số lượng và nhịp TRUNG BÌNH không đổi")In [ ]:
from pathlib import Path
THU_MUC_RA = Path.home() / "lab" / "out_lab6"
THU_MUC_RA.mkdir(parents=True, exist_ok=True)
gio_dau = theo_gio.sort_values("diem").iloc[0]
bao_cao = f'''BÁO CÁO MODULE 6: BẤT THƯỜNG MẠNG TRÊN HONEYPOT ({theo_gio.gio_vn.min():%d/%m} - {theo_gio.gio_vn.max():%d/%m/%Y}, {len(theo_gio):,} giờ)
==================================================================================
Danh sách đen : {la_ip_moi.mean():.0%} kết nối sau {MOC:%d/%m} đến từ IP mới -> không thể dựa vào danh sách chặn
Beaconing : {(beacon[beacon.nhip > 10].do_lech < NGUONG_DEU).sum()} IP gọi đều nhịp (độ lệch < {NGUONG_DEU}), ví dụ {IP_DEU} mỗi {beacon.loc[IP_DEU, 'nhip']:,.0f} giây
Ngưỡng cố định : báo {int(vuot_co_dinh.sum())} giờ | ngưỡng động K={K}: báo {int(vuot_dong.sum())} giờ
Isolation Forest : giờ bất thường nhất {gio_dau.gio_vn:%d/%m %H:%M} ({int(gio_dau.ket_noi)} kết nối, {int(gio_dau.tai_file)} lần tải file)
Đường hầm : {duong_ham.so_lan.sum():,} yêu cầu từ {duong_ham.src_ip.nunique()} IP, gom thành {SO_CUM} cụm hành vi
Dữ liệu trôi : tải file tháng 7 = {int(theo_thang.loc[7, 'tai_file']):,} -> tháng 8 = {int(theo_thang.loc[8, 'tai_file']):,}
Khuyến nghị:
- Giám sát theo HÀNH VI (nhịp, giờ bùng nổ, kiểu dùng bàn đạp), không chỉ theo IP / chữ ký.
- Dùng ngưỡng động và học lại "bình thường" định kỳ vì dữ liệu trôi theo tháng.
- Mọi điểm bất thường là gợi ý điều tra: có thể là tấn công, cũng có thể là sự cố của chính cảm biến.
'''
(THU_MUC_RA / "bao_cao_lab6.txt").write_text(bao_cao, encoding="utf-8")
print(bao_cao)