211 KiB
211 KiB
In [ ]:
%%bash
# grep -h = tìm mọi dòng chứa IP này trong cả 30 file log (không in tên file)
grep -h '"src_ip": "1.10.195.220"' /opt/csai/data/honeypot_m1/cowrie.json.*In [ ]:
%%bash
# grep -m 8 = dừng sau 8 dòng khớp đầu tiên
grep -h -m 8 '"src_ip": "130.12.182.196"' /opt/csai/data/honeypot_m1/cowrie.json.2026-08-30In [ ]:
import glob
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
pd.set_option("display.max_colwidth", 80)
pd.set_option("display.width", 200)
RANDOM_STATE = 42 # "hạt giống" ngẫu nhiên: cố định để lần nào chạy cũng ra cùng kết quả
# Đọc 30 file log (mỗi dòng 1 sự kiện JSON), ghép thành 1 bảng
log = pd.concat(pd.read_json(f, lines=True, dtype=False, convert_dates=False)
for f in sorted(glob.glob("/opt/csai/data/honeypot_m1/cowrie.json.*")))
log["thoi_gian"] = pd.to_datetime(log.timestamp, utc=True) # chữ -> thời gian thật (UTC)
# 📦 log = bảng MỌI sự kiện 30 ngày; cả notebook dùng bảng này
so_kn = (log.eventid == "cowrie.session.connect").sum()
so_thu = log.eventid.str.startswith("cowrie.login").sum()
print("log:", len(log), "dòng,", log.src_ip.nunique(), "IP")
print(log.eventid.value_counts().to_string())
print(f"Trung bình cứ {30 * 86400 / so_kn:.0f} giây có 1 kết nối mới, cứ {30 * 86400 / so_thu:.0f} giây có 1 lần thử mật khẩu — suốt 30 ngày")In [ ]:
# ✏️ SỬA: ngưỡng số lần sai và độ dài cửa sổ thời gian
NGUONG_LAN, CUA_SO = 5, "10min"
# Các lần đăng nhập THẤT BẠI, xếp theo thời gian, lấy cột thời gian làm chỉ mục (để đếm theo khung thời gian)
sai = log[log.eventid == "cowrie.login.failed"].sort_values("thoi_gian").set_index("thoi_gian")
# rolling(CUA_SO).count(): tại mỗi lần sai, đếm số lần sai của CÙNG IP trong 10 phút tính ngược về trước
# .max() theo IP = "đỉnh": khung 10 phút dồn dập nhất của IP đó
dinh_10_phut = sai.groupby("src_ip").eventid.rolling(CUA_SO).count().groupby(level=0).max()
# 📦 dinh_10_phut = mỗi IP (có thử sai) -> số lần sai nhiều nhất trong 1 khung 10 phút
bi_bat = set(dinh_10_phut[dinh_10_phut > NGUONG_LAN].index)
tong_sai = sai.groupby("src_ip").size()
print("IP có thử sai mật khẩu:", len(dinh_10_phut))
print(f"Luật ngưỡng bắt được : {len(bi_bat)} IP")
lot = tong_sai.drop(list(bi_bat))
print(f"Lọt qua luật : {len(lot)} IP, tổng cộng {lot.sum():,} lần thử sai mà không một cảnh báo nào")In [ ]:
cac_ip = ["130.12.182.196", "109.160.32.31", "2.57.121.112", "185.246.128.133"]
bang = pd.DataFrame({
"tổng lần sai 30 ngày": tong_sai.reindex(cac_ip, fill_value=0),
"đỉnh 10 phút": dinh_10_phut.reindex(cac_ip, fill_value=0).astype(int),
"luật bắt được?": [ip in bi_bat for ip in cac_ip],
})
print(bang.to_string())
# Soi kỹ 2.57.121.112: trong MỌI khung 10 phút, nó sai bao nhiêu lần?
cua_so_257 = sai[sai.src_ip == "2.57.121.112"].eventid.rolling(CUA_SO).count()
print("\n2.57.121.112 — số lần sai trong khung 10 phút (đếm tại mỗi lần thử):")
print(cua_so_257.value_counts().sort_index().rename("số lần gặp").to_string())In [ ]:
%%bash
cd /opt/csai/data/honeypot_m1
# grep IP -> chỉ lấy dòng login.failed -> head -15 = 15 dòng đầu (lưu ý: grep -m 8 với nhiều file là 8 dòng MỖI FILE)
# grep -o = chỉ in phần khớp (3 trường) ; paste - - - = ghép 3 trường về lại 1 dòng
grep -h '"src_ip": "2.57.121.112"' cowrie.json.* | grep login.failed | head -15 \
| grep -o '"timestamp": "[^"]*"\|"username": "[^"]*"\|"password": "[^"]*"' | paste - - -In [ ]:
%%bash
cd /opt/csai/data/honeypot_m1
IP='"src_ip": "185.246.128.133"'
echo "--- Mọi loại sự kiện của nó trong 30 ngày (uniq -c = đếm số dòng giống nhau):"
grep -h "$IP" cowrie.json.* | grep -o '"eventid": "[^"]*"' | sort | uniq -c
echo "--- 3 lần đăng nhập ĐÚNG đầu tiên:"
grep -h "$IP" cowrie.json.* | grep login.success | head -3 \
| grep -o '"timestamp": "[^"]*"\|"username": "[^"]*"\|"password": "[^"]*"' | paste - - -
echo "--- Tên phần mềm nó khai ở 5 phiên đầu:"
grep -h "$IP" cowrie.json.* | grep client.version | head -5 | grep -o '"version": "[^"]*"'
echo "--- Số tên phần mềm khác nhau trong 30 ngày:"
grep -h "$IP" cowrie.json.* | grep client.version | grep -o '"version": "[^"]*"' | sort -u | wc -lIn [ ]:
# ✏️ SỬA: ngưỡng của từng luật (đổi rồi chạy lại để xem đánh đổi)
SAI_TRUOC_KHI_VAO = 5 # R2: vào được sau >= bao nhiêu lần sai
SO_USERNAME_1H = 10 # R3: 1 IP thử >= bao nhiêu tên đăng nhập khác nhau trong 1 giờ
SO_IP_MOT_USERNAME = 20 # R4: 1 tên đăng nhập bị >= bao nhiêu IP thử trong 1 ngày
KET_NOI_KHONG_THU = 10 # R6: >= bao nhiêu kết nối / 10 phút mà không thử mật khẩu lần nào
SO_IP_CONG_CU_HIEM = 2 # R7: phần mềm SSH chỉ <= bao nhiêu IP dùng thì coi là "hiếm"
SO_NGAY_KIEN_TRI = 7 # R8: hoạt động >= bao nhiêu ngày khác nhau
thu = log[log.eventid.str.startswith("cowrie.login")] # mọi lần thử đăng nhập (sai + được)
vao = log[log.eventid == "cowrie.login.success"] # các lần vào được
ket_noi = log[log.eventid == "cowrie.session.connect"]
luat = {"R1 dò dồn dập (1.1)": bi_bat}
# R2: số lần sai TRƯỚC lần vào được đầu tiên của mỗi IP
lan_vao_dau = vao.groupby("src_ip").thoi_gian.min().rename("t_vao")
sai_co_vao = log[log.eventid == "cowrie.login.failed"].join(lan_vao_dau, on="src_ip", how="inner")
sai_truoc = sai_co_vao[sai_co_vao.thoi_gian < sai_co_vao.t_vao].groupby("src_ip").size()
luat["R2 vào được sau nhiều lần sai"] = set(sai_truoc[sai_truoc >= SAI_TRUOC_KHI_VAO].index)
# R3: trong khung 1 giờ "đông" nhất, IP thử bao nhiêu tên đăng nhập khác nhau
ten_1h = (thu.set_index("thoi_gian").groupby(["src_ip", pd.Grouper(freq="1h")])
.username.nunique().groupby(level=0).max())
luat["R3 rải tài khoản"] = set(ten_1h[ten_1h >= SO_USERNAME_1H].index)
# R4: cặp (tên đăng nhập, ngày) bị nhiều IP thử -> mọi IP tham gia
thu_ngay = thu.assign(ngay=thu.thoi_gian.dt.date)
ip_moi_ten = thu_ngay.groupby(["username", "ngay"]).src_ip.nunique()
ten_nong = ip_moi_ten[ip_moi_ten >= SO_IP_MOT_USERNAME].reset_index()[["username", "ngay"]]
luat["R4 1 tài khoản bị nhiều IP thử"] = set(thu_ngay.merge(ten_nong).src_ip)
# R5: honeypot không có "danh sách cho phép" -> mọi IP vào được đều là IP lạ
luat["R5 vào được từ IP lạ"] = set(vao.src_ip)
# R6: IP chưa thử mật khẩu lần nào, nhưng gõ cửa dồn dập
chi_go = set(ket_noi.src_ip) - set(thu.src_ip)
go_10p = (ket_noi[ket_noi.src_ip.isin(chi_go)].set_index("thoi_gian").sort_index()
.groupby("src_ip").eventid.rolling("10min").count().groupby(level=0).max())
luat["R6 quét dồn dập"] = set(go_10p[go_10p >= KET_NOI_KHONG_THU].index)
# R7: phần mềm SSH mà rất ít IP dùng
phan_mem = log[log.eventid == "cowrie.client.version"]
so_ip_dung = phan_mem.groupby("version").src_ip.nunique()
hiem = so_ip_dung[so_ip_dung <= SO_IP_CONG_CU_HIEM].index
luat["R7 phần mềm SSH hiếm"] = set(phan_mem[phan_mem.version.isin(hiem)].src_ip)
# R8: số ngày khác nhau có hoạt động
so_ngay_hd = log.assign(ngay=log.thoi_gian.dt.date).groupby("src_ip").ngay.nunique()
luat["R8 kiên trì nhiều ngày"] = set(so_ngay_hd[so_ngay_hd >= SO_NGAY_KIEN_TRI].index)
# 📦 luat = tên luật -> tập IP bị luật đó cảnh báo
bang_luat = pd.DataFrame({ten: {"IP bị bắt": len(tap), "mới (R1 không bắt)": len(tap - bi_bat),
"bắt 2.57.121.112?": "2.57.121.112" in tap,
"bắt 185.246.128.133?": "185.246.128.133" in tap}
for ten, tap in luat.items()}).T
print(bang_luat.to_string())
bi_canh_bao = set().union(*luat.values())
so_luat = pd.Series({ip: sum(ip in tap for tap in luat.values()) for ip in bi_canh_bao})
# 📦 so_luat = mỗi IP bị cảnh báo -> bị mấy luật cùng bắt
print(f"\nBật cả {len(luat)} luật: {len(bi_canh_bao)} / {log.src_ip.nunique()} IP bị ít nhất 1 luật cảnh báo")
print("Số IP theo số luật cùng bắt:", so_luat.value_counts().sort_index().to_dict())
print(f"IP bị >= 4 luật cùng bắt: {(so_luat >= 4).sum()}")In [ ]:
def tao_dac_trung(bang_log):
"""Nhận một bảng log (như 'log'), trả về bảng đặc trưng: mỗi IP 1 hàng, 7 cột."""
kn_ = bang_log[bang_log.eventid == "cowrie.session.connect"] # các lần kết nối
dn_ = bang_log[bang_log.eventid.str.startswith("cowrie.login")].sort_values("thoi_gian") # các lần thử đăng nhập
sai_ = dn_[dn_.eventid == "cowrie.login.failed"] # các lần thử SAI
F_ = pd.DataFrame(index=pd.Index(kn_.src_ip.unique(), name="ip")) # bảng rỗng, mỗi IP 1 hàng
# 1. failed_logins: đếm số lần sai của mỗi IP; IP không sai lần nào -> 0
F_["failed_logins"] = sai_.groupby("src_ip").size().reindex(F_.index, fill_value=0)
# 2. unique_usernames: số username KHÁC NHAU đã thử
F_["unique_usernames"] = dn_.groupby("src_ip").username.nunique().reindex(F_.index, fill_value=0)
# 3. attempt_interval_std: với mỗi IP, khoảng cách (giây) tới lần thử trước (diff), rồi lấy độ lệch chuẩn (std)
cach = dn_.groupby("src_ip").thoi_gian.diff().dt.total_seconds()
F_["attempt_interval_std"] = cach.groupby(dn_.src_ip).std().reindex(F_.index)
# 4. credential_reuse_score: mỗi mật khẩu được bao nhiêu IP khác nhau thử? >= 3 IP = "mật khẩu dùng chung"
so_ip_moi_mk = dn_.groupby("password").src_ip.nunique()
dung_chung = dn_.password.map(so_ip_moi_mk) >= 3 # True/False cho từng lần thử
F_["credential_reuse_score"] = dung_chung.groupby(dn_.src_ip).mean().reindex(F_.index, fill_value=0)
# 5-6. so_ket_noi, so_ngay
F_["so_ket_noi"] = kn_.groupby("src_ip").size()
F_["so_ngay"] = kn_.groupby("src_ip").thoi_gian.apply(lambda t: t.dt.date.nunique())
# 7. ip_cung_phan_mem: phần mềm chính của IP (mode = giá trị hay gặp nhất) được bao nhiêu IP dùng chung
pm_ = bang_log[bang_log.eventid == "cowrie.client.version"]
pm_chinh = pm_.groupby("src_ip").version.agg(lambda s: s.mode()[0])
so_ip_moi_pm = pm_.groupby("version").src_ip.nunique()
F_["ip_cung_phan_mem"] = pm_chinh.map(so_ip_moi_pm).reindex(F_.index, fill_value=0)
return F_
F = tao_dac_trung(log)
# .shape = (số hàng, số cột) của bảng: dùng để kiểm tra nhanh kích thước sau mỗi bước
print("F.shape =", F.shape, "-> (số IP, số đặc trưng)")
# 📦 F = bảng đặc trưng: mỗi HÀNG 1 IP (tên IP ở chỉ mục bên trái), mỗi CỘT 1 đặc trưng — "ngôn ngữ" của AI
print("F:", F.shape[0], "IP x", F.shape[1], "đặc trưng")
F.loc[["1.10.195.220", "130.12.182.196", "109.160.32.31", "2.57.121.112", "185.246.128.133"]].round(2)In [ ]:
# Bảng tóm tắt 7 cột: nhỏ nhất / trung vị (số đứng giữa) / lớn nhất / số ô trống
tom_tat = pd.DataFrame({"nhỏ nhất": F.min(), "trung vị": F.median(), "lớn nhất": F.max(), "số ô trống (NaN)": F.isna().sum()})
print(tom_tat.round(2).to_string())
# Ô trống ở attempt_interval_std: IP đó đã thử đăng nhập bao nhiêu lần?
so_lan_thu = log[log.eventid.str.startswith("cowrie.login")].groupby("src_ip").size().reindex(F.index, fill_value=0)
trong = F.attempt_interval_std.isna() # True ở các IP bị trống cột này
print("\nCác IP trống attempt_interval_std đã thử đăng nhập bao nhiêu lần?")
print(so_lan_thu[trong].value_counts().sort_index().rename("số IP").to_string())
print("\nVí dụ 3 IP bị trống:")
print(F[trong].head(3).to_string())In [ ]:
# ==============================================================================
# BƯỚC 4a: ĐIỀN Ô TRỐNG
# ==============================================================================
# Vì sao ô bị trống? attempt_interval_std đo "các lần thử mật khẩu cách nhau có đều không".
# IP thử 0, 1 hoặc 2 lần thì không đủ khoảng cách để đo -> NaN (xem 3.2).
# Vì sao KHÔNG điền 0? Độ lệch = 0 nghĩa là "đều tăm tắp như đồng hồ" = dấu hiệu của MÁY.
# Điền 0 sẽ biến một IP gõ cửa 1 lần rồi đi thành "siêu bot đều nhịp" -> sai hẳn nghĩa.
# Giải pháp: điền TRUNG VỊ của chính cột đó = coi IP "chưa đo được" là IP bình thường nhất
# về mặt này: không phạt, không thưởng.
cot = F.attempt_interval_std
print(f"attempt_interval_std: trung bình = {cot.mean():,.0f} giây | trung vị = {cot.median():,.0f} giây")
print("-> vài IP 'rải rác cả tháng' kéo trung bình lên hơn 7 tiếng; trung vị (2,4 phút) mới là mức thường gặp")
trung_vi_cot = F.median() # F.median() = trung vị của TỪNG cột (7 số, mỗi cột 1 số)
F_du = F.fillna(trung_vi_cot) # fillna = điền vào ô trống; cột nào dùng trung vị của cột đó
# 📦 trung_vi_cot = 7 trung vị (Phần 7, 8 dùng lại) · F_du = bảng F đã điền đủ số
print("\nTrung vị dùng để điền:")
print(trung_vi_cot.round(2).to_string())
print("\nSố ô trống còn lại:", int(F_du.isna().sum().sum()))
print("Ví dụ IP 1.10.195.220 (Phần 0, chưa thử mật khẩu lần nào):")
print(" TRƯỚC: attempt_interval_std =", F.loc["1.10.195.220", "attempt_interval_std"], "(trống)")
print(" SAU : attempt_interval_std =", round(F_du.loc["1.10.195.220", "attempt_interval_std"], 2), "(mức bình thường)")In [ ]:
# ==============================================================================
# BƯỚC 4b: NÉN SỐ LỚN BẰNG log1p
# ==============================================================================
# Bảng minh họa: cột phải "co" lại thế nào so với cột trái
vd = pd.Series([0, 1, 2, 10, 100, 1000, 3310], name="x")
print(pd.DataFrame({"x (số gốc)": vd, "log1p(x)": np.log1p(vd).round(2)}).to_string(index=False))
print(f"\n3310 lớn gấp {3310 / 2:,.0f} lần 2 | sau log1p chỉ còn gấp {np.log1p(3310) / np.log1p(2):.1f} lần")
F_log = np.log1p(F_du) # áp dụng cho MỌI ô của bảng (mọi cột đều >= 0 nên dùng được)
# 📦 F_log = bảng F_du sau khi nén
print("\nHai IP quen thuộc — số gốc -> sau khi nén:")
for ip, mo_ta in [("130.12.182.196", "kẻ xả lũ"), ("185.246.128.133", "kẻ trinh sát 29 ngày")]:
print(f" {ip:16s} ({mo_ta:20s}): so_ket_noi {F_du.loc[ip, 'so_ket_noi']:5.0f} -> {F_log.loc[ip, 'so_ket_noi']:.2f}"
f" | so_ngay {F_du.loc[ip, 'so_ngay']:3.0f} -> {F_log.loc[ip, 'so_ngay']:.2f}")In [ ]:
# ==============================================================================
# BƯỚC 4c: QUY VỀ CÙNG MỘT THƯỚC ĐO BẰNG StandardScaler
# ==============================================================================
from sklearn.preprocessing import StandardScaler
chuan_hoa = StandardScaler()
F_chuan = pd.DataFrame(chuan_hoa.fit_transform(F_log), index=F.index, columns=F.columns)
# 📦 chuan_hoa = bộ chuẩn hóa đã HỌC TB/ĐLC của 7 cột (Phần 7, 8 dùng lại) · F_chuan = bảng đưa vào Isolation Forest
# Tự tính lại bằng tay 1 ô để thấy công thức không có gì bí ẩn: cột so_ngay của 185.246.128.133
i = list(F.columns).index("so_ngay")
tb, dlc = chuan_hoa.mean_[i], chuan_hoa.scale_[i] # TB và ĐLC mà chuan_hoa học được cho cột so_ngay (trên F_log)
x = F_log.loc["185.246.128.133", "so_ngay"]
print(f"so_ngay của 185.246.128.133: gốc 29 ngày -> log1p = {x:.3f}")
print(f"TB cột = {tb:.3f}, ĐLC cột = {dlc:.3f} -> z = ({x:.3f} - {tb:.3f}) / {dlc:.3f} = {(x - tb) / dlc:.2f}")
print(f"StandardScaler cho ra: {F_chuan.loc['185.246.128.133', 'so_ngay']:.2f} <- khớp")
print("\nSO SÁNH TRÊN CÙNG MỘT THƯỚC ĐO (điểm z):")
for ip, mo_ta in [("130.12.182.196", "kẻ xả lũ"), ("185.246.128.133", "kẻ trinh sát 29 ngày")]:
print(f" {ip} ({mo_ta}): số lần sai z = {F_chuan.loc[ip, 'failed_logins']:+.2f} | "
f"số kết nối z = {F_chuan.loc[ip, 'so_ket_noi']:+.2f} | số ngày có mặt z = {F_chuan.loc[ip, 'so_ngay']:+.2f}")
print("\nKiểm tra: TB mỗi cột sau chuẩn hóa =", F_chuan.mean().round(2).tolist(), "| ĐLC =", F_chuan.std(ddof=0).round(2).tolist())In [ ]:
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import NearestNeighbors
def xep_hang(bang, mo_hinh):
"""Trả về thứ hạng bất thường của mỗi IP (1 = bất thường nhất) theo 1 mô hình."""
if mo_hinh == "Isolation Forest":
m = IsolationForest(n_estimators=300, random_state=RANDOM_STATE).fit(bang)
diem_ = m.decision_function(bang) # càng âm càng bất thường
else:
lang_gieng = NearestNeighbors(n_neighbors=6).fit(bang) # 6 = chính nó + 5 hàng xóm
khoang_cach = lang_gieng.kneighbors(bang)[0][:, 1:].mean(axis=1) # khoảng cách trung bình tới 5 hàng xóm
diem_ = -khoang_cach # càng xa -> càng âm -> càng bất thường
return pd.Series(diem_, index=bang.index).rank().astype(int)
def chuan(bang):
return pd.DataFrame(StandardScaler().fit_transform(bang), index=bang.index, columns=bang.columns)
cach_chuan_bi = {
"chỉ điền ô trống (F_du)": F_du,
"+ chuẩn hóa, KHÔNG nén": chuan(F_du),
"+ nén log1p, KHÔNG chuẩn hóa (F_log)": F_log,
"+ nén + chuẩn hóa (F_chuan, đầy đủ)": F_chuan,
}
dong = []
for ten, bang in cach_chuan_bi.items():
for mo_hinh in ["Isolation Forest", "kNN (đo khoảng cách)"]:
h = xep_hang(bang, mo_hinh)
dong.append({"cách chuẩn bị": ten, "mô hình": mo_hinh,
"185.246.128.133 hạng": h["185.246.128.133"], "2.57.121.112 hạng": h["2.57.121.112"]})
pd.DataFrame(dong).set_index(["cách chuẩn bị", "mô hình"])In [ ]:
from sklearn.ensemble import IsolationForest
# ✏️ SỬA: tỉ lệ IP coi là bất thường. Thử 0.01, 0.10, 0.20
CONTAMINATION = 0.05
rung_co_lap = IsolationForest(
n_estimators=300, # số cây trong rừng (càng nhiều, kết luận càng ổn định)
contamination=CONTAMINATION, # đoán trước khoảng bao nhiêu phần IP là bất thường -> dùng để đặt ranh giới ở 5.3
random_state=RANDOM_STATE, # cố định cách chọn ngẫu nhiên -> lần nào chạy cũng ra cùng kết quả
)
rung_co_lap.fit(F_chuan) # "học" = xây 300 cây trên F_chuan. Chỉ đưa F_chuan, KHÔNG đưa đáp án nào
# 📦 rung_co_lap = rừng 300 cây đã xây xong
print("Số cây:", len(rung_co_lap.estimators_), "| mỗi cây xây trên", rung_co_lap.max_samples_, "IP chọn ngẫu nhiên")In [ ]:
def so_nhat_chat(ip):
"""Đếm, trong từng cây của rừng, phải chặt bao nhiêu nhát thì IP này tới được ô lá cuối cùng."""
x = F_chuan.loc[[ip]].to_numpy()
ket_qua = []
for cay, cot_cua_cay in zip(rung_co_lap.estimators_, rung_co_lap.estimators_features_):
# decision_path = các nút cây mà IP đi qua từ gốc tới lá; số nút - 1 = số nhát chặt
ket_qua.append(cay.decision_path(x[:, cot_cua_cay]).sum() - 1)
return np.array(ket_qua)
for ip, mo_ta in [("1.10.195.220", "máy quét bình thường (Phần 0)"), ("185.246.128.133", "kẻ trinh sát 29 ngày"), ("2.57.121.112", "kẻ dò 5 lần rồi dừng")]:
n = so_nhat_chat(ip)
print(f"{ip:16s} ({mo_ta:30s}): cây số 1 chặt {n[0]} nhát | trung bình 300 cây: {n.mean():.1f} nhát (ít nhất {n.min()}, nhiều nhất {n.max()})")In [ ]:
diem = pd.Series(rung_co_lap.decision_function(F_chuan), index=F.index) # điểm liên tục: càng ÂM càng bất thường
ket_luan = pd.Series(rung_co_lap.predict(F_chuan), index=F.index) # -1 = bất thường, +1 = bình thường
hang = diem.rank().astype(int) # hạng 1 = điểm thấp nhất = bất thường nhất
# 📦 diem, ket_luan, hang = 3 cột song song, mỗi IP một giá trị
print(f"Số IP bị gắn cờ bất thường (-1): {(ket_luan == -1).sum()} / {len(F)}")
for ip in ["1.10.195.220", "185.246.128.133", "2.57.121.112"]:
print(f" {ip:16s} điểm {diem[ip]:+.3f} -> {'BẤT THƯỜNG' if ket_luan[ip] == -1 else 'bình thường'} (hạng {hang[ip]})")
print("\n15 IP bất thường nhất:")
F.assign(diem=diem.round(3), hang=hang).sort_values("diem").head(15)In [ ]:
plt.figure(figsize=(12, 4))
plt.hist(diem, bins=80, color="#457b9d")
plt.axvline(0, color="red", linestyle="--", label="ranh giới (điểm < 0 -> gắn cờ -1)")
for ip, mau in [("2.57.121.112", "orange"), ("185.246.128.133", "purple"), ("130.12.182.196", "green")]:
plt.axvline(diem[ip], color=mau, label=f"{ip} (hạng {hang[ip]})")
plt.xlabel("điểm bất thường (decision_function)"); plt.ylabel("số IP"); plt.legend(); plt.title("Phân bố điểm bất thường")
plt.show()In [ ]:
co_if = set(ket_luan[ket_luan == -1].index)
print(f"Luật ngưỡng gắn cờ : {len(bi_bat)} IP")
print(f"Isolation Forest gắn cờ: {len(co_if)} IP")
print(f"Cả hai cùng gắn cờ : {len(bi_bat & co_if)} IP")
chi_if = co_if - bi_bat
print(f"CHỈ Isolation Forest thấy (luật ngưỡng bỏ sót): {len(chi_if)} IP — 8 IP điểm thấp nhất:")
F.loc[list(chi_if)].assign(diem=diem.round(3), hang=hang, dinh_10_phut=dinh_10_phut.reindex(list(chi_if), fill_value=0).astype(int)).sort_values("diem").head(8)In [ ]:
co_dang_nhap = F.unique_usernames > 0 # IP có thử đăng nhập (chỉ để tham khảo, mô hình KHÔNG dùng)
dong = []
for c in ["auto", 0.01, 0.03, 0.05, 0.10, 0.20, 0.30]:
m = IsolationForest(n_estimators=300, contamination=c, random_state=RANDOM_STATE).fit(F_chuan)
co = pd.Series(m.predict(F_chuan), index=F.index) == -1
dong.append({"contamination": c, "số IP gắn cờ": co.sum(), "trong đó có thử đăng nhập": (co & co_dang_nhap).sum(),
"185.246.128.133?": co["185.246.128.133"], "2.57.121.112?": co["2.57.121.112"]})
pd.DataFrame(dong).set_index("contamination")In [ ]:
import warnings
warnings.simplefilter("ignore", DeprecationWarning) # ẩn một cảnh báo kỹ thuật vô hại của thư viện khi tạo mốc thời gian
# ✏️ SỬA: số máy health-checker nội bộ, mỗi máy đăng nhập bao nhiêu giây một lần
SO_MAY, CU_MOI_GIAY = 150, 1200
bat_dau = pd.Timestamp("2026-08-24", tz="UTC")
gia_lap = []
for j in range(SO_MAY):
ip = "127.0.0.1" if j == 0 else f"10.0.{j // 250}.{j % 250 + 1}" # địa chỉ NỘI BỘ
moc = bat_dau + pd.to_timedelta(np.arange(0, 30 * 86400, CU_MOI_GIAY) + j * 7, unit="s")
gia_lap.append(pd.DataFrame({"eventid": "cowrie.session.connect", "thoi_gian": moc, "src_ip": ip}))
gia_lap.append(pd.DataFrame({"eventid": "cowrie.login.success", "thoi_gian": moc + pd.Timedelta(seconds=1),
"src_ip": ip, "username": "healthcheck", "password": "hc"}))
log_nhiem = pd.concat([log] + gia_lap, ignore_index=True)
# 📦 log_nhiem = log thật + log giả lập của SO_MAY máy nội bộ
def cham_diem(F_):
"""Làm lại đúng Phần 4-5 cho một bảng đặc trưng bất kỳ: điền trung vị từng cột -> log1p -> chuẩn hóa -> Isolation Forest. Trả về điểm của từng IP."""
X_ = StandardScaler().fit_transform(np.log1p(F_.fillna(F_.median())))
m = IsolationForest(n_estimators=300, contamination=CONTAMINATION, random_state=RANDOM_STATE).fit(X_)
return pd.Series(m.decision_function(X_), index=F_.index)
# ✏️ SỬA: mỗi ngày đội SOC chỉ kịp điều tra bao nhiêu IP đứng đầu danh sách
TOP_SOC = 50
diem_nhiem = cham_diem(tao_dac_trung(log_nhiem))
for ten, d in [("Dữ liệu sạch (Phần 5)", diem), (f"Có {SO_MAY} máy health-checker", diem_nhiem)]:
h = int(d.rank()["185.246.128.133"])
print(f"{ten:32s}: 185.246.128.133 điểm {d['185.246.128.133']:+.3f}, hạng {h:4d} / {len(d)} -> "
f"{'NẰM TRONG' if h <= TOP_SOC else 'RƠI KHỎI'} top {TOP_SOC} đội SOC xem mỗi ngày")In [ ]:
import ipaddress
# is_private = True cho 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (và vài dải đặc biệt khác)
la_noi_bo = log_nhiem.src_ip.map(lambda ip: ipaddress.ip_address(ip).is_private)
log_sach = log_nhiem[~la_noi_bo] # LỌC TRƯỚC, rồi mới tạo đặc trưng
print("Đã loại", la_noi_bo.sum(), "dòng log từ IP nội bộ")
diem_sach = cham_diem(tao_dac_trung(log_sach))
print(f"Sau khi lọc: 185.246.128.133 điểm {diem_sach['185.246.128.133']:+.3f}, hạng {int(diem_sach.rank()['185.246.128.133'])} / {len(diem_sach)}")In [ ]:
import warnings
warnings.filterwarnings("ignore", message="IProgress not found") # ẩn cảnh báo về thanh tiến trình, không ảnh hưởng kết quả
import shap
# TreeExplainer: bộ giải thích SHAP cho mô hình dạng cây (Isolation Forest ở 5.1)
bo_giai_thich = shap.TreeExplainer(rung_co_lap)
def giai_thich(ip):
"""In và vẽ phần đóng góp SHAP của từng đặc trưng cho 1 IP."""
sv = bo_giai_thich(F_chuan.loc[[ip]]) # SHAP cho đúng 1 hàng (đã chuẩn hóa như lúc học)
nen = float(sv.base_values[0]) # số nhát chặt trung bình của MỌI IP
tong = nen + sv.values[0].sum() # số nhát chặt của IP này
print(f"{ip}: điểm {diem[ip]:+.3f} ({'BẤT THƯỜNG' if diem[ip] < 0 else 'bình thường'}) | "
f"IP trung bình cần {nen:.1f} nhát chặt, IP này chỉ cần {tong:.1f} nhát")
bang_gt = pd.DataFrame({"giá trị của IP": F.loc[ip], "SHAP (nhát chặt +/-)": sv.values[0],
"đóng góp vào cảnh báo": -sv.values[0]}, index=F.columns)
bang_gt = bang_gt.sort_values("đóng góp vào cảnh báo", ascending=False)
bang_gt["đóng góp vào cảnh báo"].sort_values().plot(kind="barh", figsize=(7, 3), color="#e76f51",
title=f"SHAP: vì sao {ip} bị gắn cờ? (dài sang phải = nguy hiểm hơn)")
plt.axvline(0, color="black"); plt.xlabel("số nhát chặt bớt đi nhờ đặc trưng này"); plt.show()
return bang_gt.round(3)
# Cách đọc hình: mỗi thanh = 1 đặc trưng; thanh dài sang PHẢI = đặc trưng đó làm IP bị cô lập NHANH hơn (nguy hiểm hơn);
# thanh sang TRÁI = đặc trưng đó làm IP trông bình thường hơn; thanh bằng 0 = không ảnh hưởng.
# ✏️ SỬA: IP muốn giải thích (thử "130.12.182.196", "193.46.255.86", hoặc IP trong bảng 5.1)
giai_thich("185.246.128.133")In [ ]:
giai_thich("2.57.121.112")In [ ]:
sv_tat_ca = bo_giai_thich(F_chuan) # SHAP cho mọi IP (mất vài giây)
# Mức ảnh hưởng trung bình = trung bình |SHAP| của mỗi đặc trưng trên mọi IP (không quan tâm dấu)
anh_huong = pd.Series(np.abs(sv_tat_ca.values).mean(axis=0), index=F.columns).sort_values()
# 📦 anh_huong = mỗi đặc trưng trung bình làm thay đổi bao nhiêu nhát chặt
anh_huong.plot(kind="barh", figsize=(7, 3), color="#2a9d8f", title="SHAP trung bình |giá trị| trên 2.906 IP")
plt.xlabel("số nhát chặt thay đổi trung bình"); plt.show()
print(anh_huong.sort_values(ascending=False).round(3).to_string())
# Riêng 146 IP bị gắn cờ: đặc trưng nào hay "tố cáo" chúng nhất?
co = (ket_luan == -1).to_numpy()
to_cao = pd.Series((-sv_tat_ca.values[co]).mean(axis=0), index=F.columns).sort_values(ascending=False)
print("\nVới các IP bị gắn cờ, đóng góp vào cảnh báo trung bình:")
print(to_cao.round(3).to_string())In [ ]:
def diem_cua(hang_df):
"""Chấm điểm 1 hàng đặc trưng bằng mô hình ở 5.1 (điền -> log1p -> chuẩn hóa -> Isolation Forest)."""
x = pd.DataFrame(chuan_hoa.transform(np.log1p(hang_df.fillna(trung_vi_cot))), columns=F.columns)
return rung_co_lap.decision_function(x)[0]
goc = F.loc[["130.12.182.196"]]
# ✏️ SỬA: các cách né tránh — mỗi cách là một bộ đặc trưng bị kẻ tấn công thay đổi
cach_ne = {
"Không đổi gì": {},
"Thêm độ trễ ngẫu nhiên (std 3.000 giây)": {"attempt_interval_std": 3000},
"Rải ra 20 IP (mỗi IP 1/20 khối lượng)": {"failed_logins": 3310 / 20, "so_ket_noi": 3757 / 20},
"Rải ra 100 IP + trễ ngẫu nhiên, mỗi IP 1 ngày": {"failed_logins": 33, "so_ket_noi": 38, "attempt_interval_std": 3000, "so_ngay": 1},
}
for ten, thay_doi in cach_ne.items():
thu = goc.copy()
for cot, gia_tri in thay_doi.items():
thu[cot] = gia_tri
d = diem_cua(thu)
print(f"{ten:48s} điểm {d:+.3f} -> {'BẤT THƯỜNG' if d < 0 else 'BÌNH THƯỜNG — né được!'}")In [ ]:
# Mỗi dòng session.connect = 1 lần kết nối; xếp theo thời gian để tính khoảng cách giữa 2 lần
kn = log[log.eventid == "cowrie.session.connect"].sort_values("thoi_gian").copy()
# Tên phần mềm SSH khai báo, nằm ở dòng client.version cùng mã phiên (session) -> tra theo session
phan_mem = log[log.eventid == "cowrie.client.version"].drop_duplicates("session").set_index("session").version
kn["phan_mem"] = kn.session.map(phan_mem).str.extract(r"^SSH-[\d.]+-([A-Za-z]+)")[0].fillna("không khai")
# Khoảng cách (giây) tới lần kết nối TRƯỚC của CÙNG IP: groupby theo IP rồi diff(); lần đầu tiên của mỗi IP -> trống (NaN)
kn["cach_lan_truoc"] = kn.groupby("src_ip").thoi_gian.diff().dt.total_seconds()
kn["dai_mang"] = kn.src_ip.str.rsplit(".", n=1).str[0] + ".0/24" # 3 số đầu của IP = dải mạng /24
# 📦 kn = bảng các lần kết nối, mỗi hàng 1 kết nối, thêm 3 cột ta vừa tạo
print("kn:", len(kn), "kết nối, các cột:", list(kn.columns))
kn[["thoi_gian", "src_ip", "src_port", "phan_mem", "cach_lan_truoc", "dai_mang"]].head(4)In [ ]:
g = kn.groupby("src_ip") # gom các kết nối theo IP: mỗi nhóm = mọi kết nối của 1 IP
X = pd.DataFrame({
"so_ket_noi": g.size(), # đếm số hàng của nhóm
"so_ngay": g.thoi_gian.apply(lambda t: t.dt.date.nunique()), # số ngày khác nhau
"so_khung_gio": g.thoi_gian.apply(lambda t: t.dt.hour.nunique()), # số giờ khác nhau
"nhip_trung_vi": g.cach_lan_truoc.median(), # trung vị khoảng cách (giây)
"so_phan_mem": g.phan_mem.nunique(),
"ti_le_khong_khai": g.phan_mem.apply(lambda s: (s == "không khai").mean()), # mean của True/False = tỉ lệ True
"ti_le_go": g.phan_mem.apply(lambda s: (s == "Go").mean()),
"cong_nguon_tb": g.src_port.mean(),
"gio_xuat_hien": g.thoi_gian.min().dt.hour,
})
X["ket_noi_moi_ngay"] = X.so_ket_noi / X.so_ngay
# map: tra mỗi dải /24 -> số IP khác nhau của dải đó
X["so_ip_cung_dai"] = g.dai_mang.first().map(kn.groupby("dai_mang").src_ip.nunique())
# 📦 X = bảng đặc trưng: mỗi HÀNG là 1 IP (tên IP nằm ở chỉ mục bên trái), mỗi CỘT là 1 đặc trưng
print("X:", X.shape[0], "IP (mẫu) x", X.shape[1], "đặc trưng")
X.loc[["1.10.195.220", "130.12.182.196"]].round(2)In [ ]:
# Đếm số dòng login (thất bại + thành công) của mỗi IP; IP không có dòng login nào -> 0
so_lan_dang_nhap = (log[log.eventid.str.startswith("cowrie.login")].groupby("src_ip").size()
.reindex(X.index, fill_value=0)) # reindex: xếp đúng thứ tự IP của X, thiếu thì 0
y = (so_lan_dang_nhap > 0).astype(int) # 1 = dò mật khẩu, 0 = chỉ quét
# 📦 y = 2.906 số 0/1, thứ tự trùng với các hàng của X
print("Nhãn của 2 IP ở Phần 0:", y.loc[["1.10.195.220", "130.12.182.196"]].to_dict())
print(y.value_counts().rename({0: "0 = chỉ quét", 1: "1 = dò mật khẩu"}).to_string())
print(f"Tỉ lệ dò mật khẩu: {y.mean():.1%}")
print(f"Đoán bừa 'IP nào cũng chỉ quét' -> đúng {(y == 0).mean():.1%} <- mốc phải vượt")In [ ]:
# ✏️ SỬA: IP muốn xem (lấy từ các bảng trên)
IP_XEM = "130.12.182.196"
print("Vector đặc trưng của", IP_XEM, "(đây là thứ máy 'nhìn thấy'):")
print(X.loc[IP_XEM].round(2).to_string())
print("Nhãn (đáp án):", y.loc[IP_XEM], "=", "dò mật khẩu" if y.loc[IP_XEM] == 1 else "chỉ quét")In [ ]:
print("Hàng trùng nhau:", X.duplicated().sum(), "(nhiều IP có hành vi giống hệt nhau — đó là thật, không xóa)")
print("Giá trị vô cùng (inf):", int(np.isinf(X.to_numpy()).sum()))
print("Ô trống mỗi cột:\n" + X.isna().sum()[X.isna().sum() > 0].to_string())
# Vì sao nhip_trung_vi trống? -> IP chỉ kết nối 1 lần thì không có "khoảng cách tới lần trước"
trong = X.nhip_trung_vi.isna()
print("IP có nhip_trung_vi trống: so_ket_noi nhỏ nhất =", X.so_ket_noi[trong].min(), ", lớn nhất =", X.so_ket_noi[trong].max())In [ ]:
from sklearn.model_selection import train_test_split
# test_size=0.2: giữ lại 20% số IP để thi; stratify=y: 2 phần giữ CÙNG tỉ lệ dò mật khẩu / chỉ quét
X_hoc, X_thi, y_hoc, y_thi = train_test_split(X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y)
print("Học:", X_hoc.shape, "| Thi:", X_thi.shape)
print(f"Tỉ lệ dò mật khẩu: học {y_hoc.mean():.3f} | thi {y_thi.mean():.3f}")
print("5 IP đầu của phần thi:", list(X_thi.index[:5]))Warning:
Output truncated. This notebook contains too many cells to display efficiently.