Files
labs/Notebooks-CSAI/Lab_M2_tung_khoi.ipynb

38 KiB

Lab Module 2 — chạy từng khối code

Cách làm: bấm vào một ô code rồi nhấn Shift + Enter để chạy ô đó và sang ô kế tiếp. Kết quả hiện ngay dưới ô. Chạy lần lượt từ trên xuống (ô sau dùng kết quả của ô trước). Kernel phải là Python (lớp CSAI) (góc trên bên phải).

Ô có dòng ✏️ SỬA là chỗ mời bạn đổi chữ / số rồi Shift + Enter lại để xem kết quả thay đổi thế nào. Sửa hỏng? Menu Kernel → Restart Kernel and Run All Cells để chạy lại từ đầu.

3 bài: M2 (máy học đầu tiên trên tin nhắn) · M2.2 (log honeypot thật → bảng đặc trưng) · M2.3 (máy học trên log honeypot thật).


Bài M2 — Máy học đầu tiên: Naive Bayes đối đầu cú lách luật

Ở bài M1, bộ lọc từ khóa chỉ bắt được 2/10 tin lách luật. Bài này cho máy tự học từ các tin nhắn đã có nhãn.

M2-1. Dữ liệu: máy học từ đâu, thi trên gì?

In [ ]:
import pandas as pd
# Bảng hiện đủ nội dung tin (mặc định pandas cắt ở 50 ký tự)
pd.set_option("display.max_colwidth", 150)
# Bộ HỌC: 50 tin có nhãn · Bộ THI: 10 tin lách luật của bài M1
hoc = pd.read_csv("~/lab/day1_labs/Manual_SOC_Dataset.csv")
thi = pd.read_csv("~/lab/day1_labs/Evasion_Dataset.csv")
# Đặt tên ngắn cho 5 cột: mã tin, kênh, người gửi, nội dung, nhãn đúng
hoc.columns = thi.columns = ["ma", "kenh", "nguoi_gui", "noi_dung", "nhan"]
print("BỘ HỌC:", len(hoc), "tin\n" + hoc.nhan.value_counts().to_string())
print("\nBỘ THI:", len(thi), "tin\n" + thi.nhan.value_counts().to_string())

Kết quả: BỘ HỌC: 50 tin (Lừa đảo 30, An toàn 20) · BỘ THI: 10 tin (Lừa đảo 10)

👉 Hiểu: máy học từ 50 tin (30 lừa đảo, 20 an toàn). Bộ thi 10 tin — toàn bộ là tin lừa đảo, không có tin an toàn nào. Nhớ điều này ở M2-5.

M2-2. Máy "đọc" chữ thế nào? Thử TF-IDF trên 3 câu

Máy tính chỉ hiểu con số. TF-IDF chấm điểm từng từ trong câu: từ càng nổi bật (lặp nhiều trong câu, ít gặp ở câu khác) thì điểm càng cao.

In [ ]:
from sklearn.feature_extraction.text import TfidfVectorizer
# ✏️ SỬA: 3 câu ví dụ, đổi thành câu của bạn
cau = ["chuyển khoản gấp vào tài khoản này",
       "chiều nay họp tại phòng họp lớn",
       "bấm vào link này để nhận quà"]
# Máy tự lập từ điển từ 3 câu, rồi biến mỗi câu thành một hàng số (điểm của từng từ)
tfidf_thu = TfidfVectorizer()
bang_so = tfidf_thu.fit_transform(cau)
tu_dien = tfidf_thu.get_feature_names_out()
print("Từ điển máy tự tạo (" + str(len(tu_dien)) + " từ):", list(tu_dien))
# Với mỗi câu: in các từ có điểm > 0, điểm cao xếp trước
for i in range(len(cau)):
    print("\nCâu", i + 1, ":", cau[i])
    diem = bang_so[i].toarray()[0]
    for tu, d in sorted(zip(tu_dien, diem), key=lambda x: -x[1]):
        if d > 0:
            print(f"   {tu:8s} {d:.2f}")

Kết quả: Từ điển máy tự tạo (17 từ): ['bấm', 'chiều', 'chuyển', ...], rồi câu 1: khoản 0.70, chuyển 0.35, gấp 0.35, tài 0.35, này 0.27, vào 0.27 …

👉 Hiểu:

  • khoản lặp 2 lần trong câu 1 → điểm cao nhất (0.70).
  • vào, này có ở cả câu 1 và câu 3 → kém đặc trưng hơn → điểm thấp hơn (0.27 so với 0.35).
  • Máy tách theo từng tiếng: "tài khoản" thành tài + khoản.

M2-3. Cho máy học 50 tin, thi trên 10 tin lách luật

In [ ]:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
# Bước 1: lập từ điển từ 50 tin HỌC, biến mỗi tin thành một hàng số
tfidf = TfidfVectorizer()
X_hoc = tfidf.fit_transform(hoc.noi_dung)
# Nhãn -> số: 1 = lừa đảo, 0 = an toàn
y_hoc = hoc.nhan.str.contains("Lừa đảo").astype(int)
print("Từ điển:", len(tfidf.vocabulary_), "từ -> mỗi tin là một dãy", len(tfidf.vocabulary_), "con số")
# Bước 2: Naive Bayes HỌC xem từ nào hay đi với lừa đảo, từ nào với an toàn
mo_hinh = MultinomialNB().fit(X_hoc, y_hoc)
# Bước 3: biến tin THI thành số bằng ĐÚNG từ điển đã học (transform, không fit lại), rồi cho máy đoán
doan = mo_hinh.predict(tfidf.transform(thi.noi_dung))
for ma, nd, d in zip(thi.ma, thi.noi_dung, doan):
    print(ma, "->", "Lừa đảo" if d == 1 else "An toàn", "|", nd[:60])
print("Đúng:", (doan == 1).sum(), "/", len(thi), " (bộ lọc từ khóa ở M1: 2/10)")

Kết quả: Từ điển: 350 từ -> mỗi tin là một dãy 350 con số, 10 dòng EVA-… -> Lừa đảo, rồi Đúng: 10 / 10 (bộ lọc từ khóa ở M1: 2/10)

👉 Hiểu: máy học bắt được cả 10 tin lách luật mà không cần ai viết thêm từ khóa. Nhưng bộ thi không có tin an toàn nào — 100% chưa nói gì về việc máy có chặn nhầm tin thật hay không.

M2-4. Máy chắc chắn đến đâu?

In [ ]:
# predict_proba = xác suất của từng tin; [:, 1] = cột xác suất "lừa đảo"
xac_suat = mo_hinh.predict_proba(tfidf.transform(thi.noi_dung))[:, 1]
# ✏️ SỬA: ngưỡng kết luận lừa đảo (0.5 = 50%). Thử 0.6 hoặc 0.7
NGUONG = 0.5
for ma, nd, p in zip(thi.ma, thi.noi_dung, xac_suat):
    ket_luan = "LỪA ĐẢO" if p >= NGUONG else "an toàn"
    print(f"{ma} | khả năng lừa đảo {p:4.0%} -> {ket_luan:7s} | {nd[:55]}")

Kết quả: 10 dòng, cao nhất EVA-001 ... 81%, thấp nhất EVA-005 ... 55% và EVA-008 ... 56% — tất cả LỪA ĐẢO.

👉 Hiểu: đúng cả 10 nhưng máy không chắc lắm: EVA-005 và EVA-008 chỉ nhỉnh hơn 50% một chút. Đổi NGUONG = 0.6 → 3 tin bị cho qua thành "an toàn". Ngưỡng là chỗ người vận hành phải chọn: chặt thì lọt ít nhưng chặn nhầm nhiều, lỏng thì ngược lại.

M2-5. Đảo vai: học từ 10 tin lách luật, thi trên 50 tin cũ

In [ ]:
# Đảo vai: HỌC từ 10 tin lách luật (toàn lừa đảo), THI trên 50 tin cũ (30 lừa đảo + 20 an toàn)
tfidf_dao = TfidfVectorizer()
mo_hinh_dao = MultinomialNB().fit(tfidf_dao.fit_transform(thi.noi_dung), thi.nhan.str.contains("Lừa đảo").astype(int))
doan_dao = mo_hinh_dao.predict(tfidf_dao.transform(hoc.noi_dung))
dung = (doan_dao == hoc.nhan.str.contains("Lừa đảo").astype(int))
print("Từ điển:", len(tfidf_dao.vocabulary_), "từ")
print("Máy đoán: ", pd.Series(doan_dao).map({1: "Lừa đảo", 0: "An toàn"}).value_counts().to_dict())
print("Đúng:", dung.sum(), "/", len(hoc))

Kết quả: Từ điển: 174 từ · Máy đoán: {'Lừa đảo': 50} · Đúng: 30 / 50

👉 Hiểu: 10 tin dùng để học toàn là lừa đảo → máy chưa từng thấy tin an toàn → nó gắn mọi tin là lừa đảo, kể cả tin hẹn đá bóng. 30/50 = đúng bằng 30 tin lừa đảo. Dữ liệu học thiếu một loại thì máy không thể nhận ra loại đó.

M2-6. Tự ra đề thi có cả tin an toàn

In [ ]:
# ✏️ SỬA: viết thêm / đổi tin (nội dung, nhãn đúng: 1 = lừa đảo, 0 = an toàn)
DE_THI = [
    ("Chiều nay 3h họp toàn công ty tại phòng họp lớn. Mọi người nhớ mang laptop.", 0),
    ("Con ơi tối nay về ăn cơm nhé mẹ nấu canh chua.", 0),
    ("Hệ thống email sẽ bảo trì từ 22h đến 23h tối nay. Không cần làm gì thêm.", 0),
    ("Tài khoản của bạn bị khóa. Bấm vào link xac-minh-ngay.com và nhập mã OTP để mở khóa.", 1),
]
# Dùng mô hình học từ 50 tin ở M2-3
dung = 0
for nd, that in DE_THI:
    p = mo_hinh.predict_proba(tfidf.transform([nd]))[0, 1]
    doan_1 = 1 if p >= 0.5 else 0
    dung += (doan_1 == that)
    print(f"{'ĐÚNG' if doan_1 == that else 'SAI '} | lừa đảo {p:4.0%} | thật là {'lừa đảo' if that else 'an toàn'} | {nd[:60]}")
print("Đúng:", dung, "/", len(DE_THI))

Kết quả: ĐÚNG | lừa đảo 46% tin họp công ty · SAI | lừa đảo 51% tin mẹ nhắn con · SAI | lừa đảo 60% tin IT báo bảo trì · ĐÚNG | lừa đảo 81% tin khóa tài khoản · Đúng: 2 / 4

👉 Hiểu: mô hình "10/10" ở M2-3 gắn nhầm tin mẹ nhắn con và thông báo bảo trì của IT là lừa đảo. Đưa vào hệ thống thật, người dùng sẽ bị chặn tin bình thường. Thử thêm vài tin an toàn của chính bạn vào DE_THI.


Bài M2.2 — Log honeypot thật: làm sạch → biểu đồ → đặc trưng

Log ở /opt/csai/data/honeypot_m1: dữ liệu tấn công thật thu từ honeypot SSH của mình, 30 ngày 24/08–22/09/2026 (cùng bộ với bài M1.2).

M2.2-1. Thông tin của một lần tấn công nằm trên nhiều dòng

In [ ]:
%%bash
# Tìm mọi dòng log có mã phiên (session) 54613b5cec67 trong ngày 22/09
grep '"54613b5cec67"' /opt/csai/data/honeypot_m1/cowrie.json.2026-09-22

Kết quả: 3 dòng cùng "session": "54613b5cec67": cowrie.session.connect (có IP 109.160.32.67, giờ), cowrie.client.version (SSH-2.0-Go), cowrie.login.failed (ftpuser1 / 123456).

👉 Hiểu: cùng một lần tấn công nhưng nằm trên 3 dòng. Muốn mỗi kết nối là một hàng đủ IP + giờ + phần mềm thì phải ghép các dòng theo session.

M2.2-2. Làm sạch và ghép dữ liệu — 4 khối nhỏ

a — Đọc 30 file log

In [ ]:
import glob
# Đọc cả 30 file (mỗi dòng 1 sự kiện JSON) thành một bảng
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)
print("Đã tải:", len(log), "dòng log từ", len(cac_file), "file")
print(log.eventid.value_counts().to_string())

Kết quả: Đã tải: 395124 dòng log từ 30 file, rồi 4 loại sự kiện: session.connect 136063, client.version 129951, login.success 65772, login.failed 63338.

b — Lập "danh bạ": mã session → tên phần mềm

In [ ]:
# Từ các dòng client.version: mỗi session 1 tên phần mềm
danh_ba = log[log.eventid == "cowrie.client.version"].drop_duplicates("session").set_index("session").version
print("Danh bạ có", len(danh_ba), "session")
danh_ba.head(3)

Kết quả: Danh bạ có 129951 session + 3 dòng mã session → SSH-2.0-Go.

c — Giữ dòng kết nối, tra danh bạ để ghép tên phần mềm, tách ngày / giờ

In [ ]:
# Mỗi dòng connect = 1 kết nối
d = log[log.eventid == "cowrie.session.connect"][["timestamp", "src_ip", "session"]].copy()
# Tra danh bạ theo session; kết nối ngắt trước khi khai phần mềm -> "unknown"
d["client_version"] = d.session.map(danh_ba).fillna("unknown")
# Tách thời gian (giữ giờ UTC như log gốc): ngày, giờ 0-23, thứ trong tuần
t = pd.to_datetime(d.timestamp, utc=True)
d["date"] = t.dt.date.astype(str)
d["hour"] = t.dt.hour
d["weekday"] = t.dt.day_name()
print("Còn lại:", len(d), "kết nối | từ", d.src_ip.nunique(), "IP")
print(f"Có tên phần mềm: {(d.client_version != 'unknown').mean():.1%} kết nối")
d.head(5)

Kết quả: Còn lại: 136063 kết nối | từ 2906 IP · Có tên phần mềm: 95.5% kết nối, bảng 5 dòng (hàng đầu 217.154.13.82, giờ 0, SSH-2.0-Go).

👉 Hiểu: 395.124 dòng còn 136.063 hàng vì chỉ giữ dòng kết nối; các dòng client.version không mất mà đã được ghép vào kết nối tương ứng.

d — Lưu bảng sạch

In [ ]:
d.to_csv("~/lab/day1_labs/cleaned_honeypot_data.csv", index=False)
print("Đã lưu cleaned_honeypot_data.csv:", len(d), "dòng")

Kết quả: Đã lưu cleaned_honeypot_data.csv: 136063 dòng

M2.2-3. 5 biểu đồ, mỗi biểu đồ gợi ra một đặc trưng

Biểu đồ 1 — số kết nối mỗi ngày

In [ ]:
import matplotlib.pyplot as plt
# Vẽ biểu đồ ngay dưới ô (máy lớp mặc định chỉ lưu ảnh ra file khi chạy trong terminal)
%matplotlib inline
theo_ngay = d.groupby("date").size()
theo_ngay.plot(kind="bar", figsize=(14, 4), color="steelblue", title="Số kết nối mỗi ngày")
plt.show()
print(f"ngày đông nhất: {theo_ngay.idxmax()} ({theo_ngay.max():,}) | vắng nhất: {theo_ngay.idxmin()} ({theo_ngay.min():,}) | trung bình {theo_ngay.mean():,.0f}/ngày")

Kết quả: biểu đồ cột 30 ngày (hiện ngay dưới ô) + ngày đông nhất: 2026-09-20 (12,620) | vắng nhất: 2026-09-05 (1,314) | trung bình 4,535/ngày

👉 Hiểu: có ngày bùng nổ gấp gần 3 lần trung bình → đặc trưng: số kết nối mỗi ngày.

Biểu đồ 2 — top 20 IP

In [ ]:
# ✏️ SỬA: số IP muốn xem (20). Thử 50
TOP = 20
so_lan = d.src_ip.value_counts()
so_lan.head(TOP).plot(kind="bar", figsize=(12, 4), color="firebrick", title=f"Top {TOP} IP kết nối nhiều nhất")
plt.show()
mot_phan_tram = len(so_lan) // 100
print(f"top {TOP} IP = {so_lan.head(TOP).sum() / len(d):.0%} số kết nối; top 1% IP ({mot_phan_tram} IP) = {so_lan.head(mot_phan_tram).sum() / len(d):.0%}")

Kết quả: top 20 IP = 29% số kết nối; top 1% IP (29 IP) = 38%

👉 Hiểu: chỉ 1% số IP tạo ra gần 40% lưu lượng → đặc trưng: tổng số kết nối của mỗi IP.

Biểu đồ 3 — theo giờ trong ngày (UTC)

In [ ]:
theo_gio = d.groupby("hour").size()
theo_gio.plot(kind="bar", figsize=(12, 4), color="goldenrod", title="Số kết nối theo giờ (UTC; giờ VN = UTC + 7)")
plt.show()
print(f"giờ đông nhất {theo_gio.idxmax():02d}h UTC ({theo_gio.max():,}) | vắng nhất {theo_gio.idxmin():02d}h UTC ({theo_gio.min():,}) | gấp {theo_gio.max() / theo_gio.min():.1f} lần")

Kết quả: giờ đông nhất 17h UTC (7,459) | vắng nhất 11h UTC (4,049) | gấp 1.8 lần

👉 Hiểu: tấn công gần như suốt 24h (17h UTC = 0h Việt Nam) → đặc trưng: hoạt động bao nhiêu khung giờ.

Biểu đồ 4 — phần mềm kẻ tấn công dùng

In [ ]:
# Rút gọn: "SSH-2.0-Go" -> "Go", "SSH-2.0-PuTTY_Release_0.84" -> "PuTTY"; không khai báo -> "unknown"
d["phan_mem"] = d.client_version.str.extract(r"^SSH-[\d.]+-([A-Za-z]+)")[0].fillna("unknown")
top_pm = d.phan_mem.value_counts().head(8)
top_pm.plot(kind="barh", figsize=(8, 4), color="seagreen", title="Phần mềm (số kết nối)")
plt.gca().invert_yaxis()
plt.show()
print(" · ".join(f"{k} {v / len(d):.1%}" for k, v in top_pm.head(5).items()))

Kết quả: Go 88.5% · unknown 5.0% · libssh 3.1% · PuTTY 1.0% · phpseclib 0.7%

👉 Hiểu: gần 9/10 kết nối là chương trình tự động viết bằng Go → đặc trưng: phần mềm.

Biểu đồ 5 — mỗi IP kết nối bao nhiêu lần một ngày

In [ ]:
moi_ip = d.groupby("src_ip").agg(tong=("src_ip", "size"), so_ngay=("date", "nunique"))
ti_le = moi_ip.tong / moi_ip.so_ngay
ti_le.plot(kind="hist", bins=50, log=True, figsize=(10, 4), color="purple", title="Số kết nối / ngày hoạt động của mỗi IP (trục đứng log)")
plt.show()
q = ti_le.quantile([0.5, 0.9, 0.99])
print(f"trung vị {q[0.5]:.1f} · 90% {q[0.9]:.1f} · 99% {q[0.99]:.1f} · cao nhất {ti_le.max():,.0f}")

Kết quả: trung vị 2.0 · 90% 19.2 · 99% 785.0 · cao nhất 1,437

👉 Hiểu: một nửa số IP chỉ ~2 kết nối/ngày; 1% hung hãn nhất trên 785/ngày → đặc trưng: mức độ hung hăng. ("Trung vị 2.0" = xếp mọi IP theo số kết nối/ngày, IP đứng giữa có 2.)

M2.2-4. Xếp đặc trưng thành bảng: mỗi IP một hàng

In [ ]:
# Gom mọi kết nối của cùng một IP thành MỘT hàng; mỗi cột là một đặc trưng
bang = d.groupby("src_ip").agg(
    so_ket_noi=("src_ip", "size"),                  # tổng số kết nối
    so_ngay=("date", "nunique"),                    # xuất hiện bao nhiêu ngày khác nhau
    so_khung_gio=("hour", "nunique"),               # hoạt động trong bao nhiêu giờ khác nhau của ngày (tối đa 24)
    phan_mem=("phan_mem", lambda s: s.mode()[0]),   # phần mềm dùng nhiều nhất
)
bang["ket_noi_moi_ngay"] = (bang.so_ket_noi / bang.so_ngay).round(1)
print("Bảng đặc trưng:", len(bang), "IP x", len(bang.columns), "đặc trưng")
bang.sort_values("so_ket_noi", ascending=False).head(5)

Kết quả: Bảng đặc trưng: 2906 IP x 5 đặc trưng, bảng 5 IP nhiều nhất: 154.18.197.29 5569 12 24 Go 464.1, 130.12.182.196 3757 6 24 Go 626.2, …

In [ ]:
# ✏️ SỬA: đổi số 1 thành số khác để xem 5 IP ngẫu nhiên khác
bang.sample(5, random_state=1)

Kết quả: 5 IP ngẫu nhiên, đa số chỉ 1–2 kết nối, 1 ngày, phần mềm unknown hoặc Go.

👉 Hiểu: 395.124 dòng log → 2.906 hàng × 5 cột. IP top kết nối hàng nghìn lần, suốt 24 giờ; IP ngẫu nhiên chỉ 1–2 lần rồi biến mất. Bảng này là "nhiên liệu" cho máy học ở M2.3.

M2.2-5. Tra cứu sâu trên bảng sạch

a — Top 5 IP chiếm bao nhiêu? Ai gây ra ngày bùng nổ?

In [ ]:
print(f"Top 5 IP chiếm {d.src_ip.value_counts().head(5).sum() / len(d):.1%} tổng số kết nối")
print("3 IP nhiều nhất ngày 2026-09-20:")
print(d[d.date == "2026-09-20"].src_ip.value_counts().head(3).to_string())

Kết quả: Top 5 IP chiếm 12.3% tổng số kết nối · 109.160.32.15 1099 · 109.160.32.19 1099 · 109.160.32.18 1099

👉 Hiểu: 3 IP liền nhau (cùng dải 109.160.32.x) với đúng cùng 1.099 kết nối trong một ngày → nhiều khả năng là một hệ thống điều khiển nhiều máy.

b — IP nào đổi nhiều phần mềm nhất?

In [ ]:
# Với mỗi IP: đếm số phần mềm KHÁC NHAU nó đã dùng (bỏ qua "unknown")
so_phan_mem = d[d.phan_mem != "unknown"].groupby("src_ip").phan_mem.nunique()
print("Số IP chỉ dùng 1 phần mềm:      ", (so_phan_mem == 1).sum())
print("Số IP dùng từ 2 phần mềm trở lên:", (so_phan_mem >= 2).sum())
print("\nTOP 5 IP đổi nhiều phần mềm nhất:")
for ip in so_phan_mem.sort_values(ascending=False).head(5).index:
    cac_pm = d[(d.src_ip == ip) & (d.phan_mem != "unknown")].phan_mem.unique()
    print(f"  {ip:16s} {len(cac_pm)} phần mềm: {', '.join(sorted(cac_pm))}")

Kết quả: chỉ dùng 1 phần mềm: 1279 · từ 2 phần mềm trở lên: 67 · đứng đầu 185.246.128.133 8 phần mềm: Granados, JSCH, Nmap, OpenSSH, PuTTY, WinSCP, libssh, paramiko

👉 Hiểu: đa số IP chỉ dùng một công cụ (một con bot làm một việc). Vài IP dùng hàng loạt công cụ, có cả Nmap (công cụ quét) → đang dò máy chủ.

c — IP "không bao giờ ngủ"

In [ ]:
so_gio = d.groupby("src_ip").hour.nunique()
ip_24h = so_gio[so_gio == 24].index
print("Số IP hoạt động ĐỦ 24 giờ trong ngày:", len(ip_24h))
print("Số IP chỉ hoạt động trong 1 giờ:     ", (so_gio == 1).sum())
print(f"-> {len(ip_24h)} IP 'không bao giờ ngủ' tạo ra {d.src_ip.isin(ip_24h).mean():.0%} tổng số kết nối")

Kết quả: ĐỦ 24 giờ: 80 · chỉ 1 giờ: 2312 · -> 80 IP 'không bao giờ ngủ' tạo ra 35% tổng số kết nối

👉 Hiểu: chỉ 80 IP (chưa tới 3% số IP) nhưng tạo ra hơn 1/3 lưu lượng, có mặt ở mọi giờ → máy chạy tự động.

d — Mổ xẻ một ngày

In [ ]:
# ✏️ SỬA: ngày muốn soi (ngày UTC), trong khoảng 2026-08-24 .. 2026-09-22
NGAY_CAN_SOI = "2026-09-20"
mot_ngay = d[d.date == NGAY_CAN_SOI]
tb = len(d) / d.date.nunique()
print(f"Ngày {NGAY_CAN_SOI}: {len(mot_ngay)} kết nối = gấp {len(mot_ngay) / tb:.1f} lần trung bình ({tb:.0f}/ngày), từ {mot_ngay.src_ip.nunique()} IP")
dai = mot_ngay.src_ip.str.rsplit(".", n=1).str[0] + ".0/24"
print("TOP 3 dải mạng trong ngày:\n" + dai.rename("dai_mang").value_counts().head(3).to_string())
print(f"-> dải đứng đầu chiếm {dai.value_counts().iloc[0] / len(mot_ngay):.0%} số kết nối của ngày")

Kết quả: Ngày 2026-09-20: 12620 kết nối = gấp 2.8 lần trung bình (4535/ngày), từ 180 IP · 109.160.32.0/24 9889 · -> dải đứng đầu chiếm 78% số kết nối của ngày

👉 Hiểu: ngày bùng nổ không phải "cả Internet cùng tấn công" — một dải mạng tạo ra gần 4/5 số kết nối. Thử đổi ngày thành "2026-09-05" (ngày vắng nhất).


Bài M2.3 — Máy học trên log honeypot thật: IP này dò mật khẩu hay chỉ quét?

Ở M1.2 ta đã đếm: 857 IP có thử đăng nhập (dò mật khẩu), 2.049 IP chỉ kết nối rồi đi (quét). Câu hỏi: chỉ nhìn cách một IP kết nối (bao nhiêu lần, mấy ngày, mấy giờ, phần mềm gì) — không nhìn dòng đăng nhập — máy có đoán ra IP đó sẽ dò mật khẩu không? Bộ lọc ở M1 do người viết luật; ở đây máy tự tìm luật từ dữ liệu.

M2.3-1. Gắn nhãn: IP nào đã dò mật khẩu?

In [ ]:
# Lấy danh sách IP có ít nhất 1 dòng đăng nhập (thất bại hoặc thành công) trong log gốc
ip_dang_nhap = set(log[log.eventid.isin(["cowrie.login.failed", "cowrie.login.success"])].src_ip)
# Nhãn: 1 = dò mật khẩu, 0 = chỉ quét. Đây là "đáp án" — KHÔNG được đưa vào làm đặc trưng
bang["do_mat_khau"] = bang.index.isin(ip_dang_nhap).astype(int)
print(bang.do_mat_khau.value_counts().rename({1: "1 = dò mật khẩu", 0: "0 = chỉ quét"}).to_string())
print(f"Nếu đoán bừa 'IP nào cũng chỉ quét' thì đã đúng {(bang.do_mat_khau == 0).mean():.1%}")

Kết quả: 0 = chỉ quét 2049 · 1 = dò mật khẩu 857 · Nếu đoán bừa 'IP nào cũng chỉ quét' thì đã đúng 70.5%

👉 Hiểu: 70,5% là mốc phải vượt. Máy học mà chỉ được 70% thì chẳng hơn gì đoán bừa.

M2.3-2. Thêm 3 đặc trưng về phần mềm

In [ ]:
them = d.groupby("src_ip").agg(
    so_phan_mem=("phan_mem", "nunique"),                            # dùng bao nhiêu phần mềm khác nhau
    ti_le_khong_khai=("phan_mem", lambda s: (s == "unknown").mean()),  # bao nhiêu phần kết nối ngắt trước khi khai phần mềm
)
bang = bang.drop(columns=["so_phan_mem", "ti_le_khong_khai"], errors="ignore").join(them)
bang["dung_go"] = (bang.phan_mem == "Go").astype(int)                 # phần mềm chính là Go? (1 = có)
print("Các cột của bảng:", list(bang.columns))
# So 2 nhóm: trung bình mỗi đặc trưng của IP chỉ quét (0) và IP dò mật khẩu (1)
bang.groupby("do_mat_khau")[["so_ket_noi", "so_ngay", "so_khung_gio", "ket_noi_moi_ngay", "ti_le_khong_khai", "dung_go"]].mean().round(2)

Kết quả: danh sách cột, rồi bảng trung bình 2 nhóm:

nhóm so_ket_noi so_ngay so_khung_gio ket_noi_moi_ngay ti_le_khong_khai dung_go
0 = chỉ quét 2.16 1.19 1.21 1.80 0.79 0.10
1 = dò mật khẩu 153.61 2.45 5.35 77.36 0.10 0.55

👉 Hiểu: IP chỉ quét thường ghé ~2 lần rồi đi, 79% số lần ngắt trước cả khi khai phần mềm; IP dò mật khẩu kết nối trung bình 150 lần. 2 nhóm khác nhau rõ → máy có cơ sở để phân biệt.

M2.3-3. Cây quyết định: máy tự tìm luật

In [ ]:
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text
from sklearn.metrics import accuracy_score, confusion_matrix
# ✏️ SỬA: chọn đặc trưng cho máy dùng. Thử thêm "dung_go", "ti_le_khong_khai", "so_phan_mem"
DAC_TRUNG = ["so_ket_noi", "so_ngay", "so_khung_gio", "ket_noi_moi_ngay"]
# ✏️ SỬA: độ sâu của cây = số câu hỏi liên tiếp máy được hỏi. Thử 1, 2, 5, 20
DO_SAU = 3

# Chia 70% IP để học, 30% IP để thi (stratify: 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(bang[DAC_TRUNG], bang.do_mat_khau,
                                              test_size=0.3, random_state=42, stratify=bang.do_mat_khau)
cay = DecisionTreeClassifier(max_depth=DO_SAU, random_state=0).fit(X_hoc, y_hoc)
doan = cay.predict(X_thi)
print("Học trên", len(X_hoc), "IP, thi trên", len(X_thi), "IP")
print(f"Đoán bừa 'tất cả chỉ quét' : {(y_thi == 0).mean():.1%}")
print(f"Cây tự chấm bài đã học     : {accuracy_score(y_hoc, cay.predict(X_hoc)):.1%}")
print(f"Cây làm bài THI            : {accuracy_score(y_thi, doan):.1%}")
tn, fp, fn, tp = confusion_matrix(y_thi, doan).ravel()
print(f"IP dò mật khẩu bắt được    : {tp} / {tp + fn}")
print(f"IP chỉ quét bị nghi nhầm   : {fp} / {tn + fp}")
if DO_SAU <= 4:
    print("\nLuật máy TỰ tìm ra (class: 1 = dò mật khẩu, 0 = chỉ quét):")
    print(export_text(cay, feature_names=DAC_TRUNG))
else:
    print("\nCây có", cay.get_n_leaves(), "nhánh lá -> quá dài để in (đặt DO_SAU từ 4 trở xuống để xem luật)")

Kết quả: Học trên 2034 IP, thi trên 872 IP · Đoán bừa: 70.5% · Cây tự chấm bài đã học: 88.8% · Cây làm bài THI: 87.6% · IP dò mật khẩu bắt được: 172 / 257 · IP chỉ quét bị nghi nhầm: 23 / 615, rồi các luật, câu hỏi đầu tiên là |--- ket_noi_moi_ngay <= 2.85.

👉 Hiểu:

  • Chỉ nhìn cách kết nối, máy đúng ~88% — hơn hẳn đoán bừa 70,5%. Đọc luật máy in ra: nó tự phát hiện "kết nối nhiều lần mỗi ngày" là dấu hiệu dò mật khẩu.
  • Thêm "dung_go", "ti_le_khong_khai", "so_phan_mem" vào DAC_TRUNG → thi đúng 90.7%, bắt được 211 / 257 IP dò mật khẩu (trước: 172): chọn đặc trưng tốt quan trọng ngang chọn thuật toán.
  • Đổi DO_SAU = 20: cây to hơn hẳn nhưng điểm THI không tăng (87.5%), bắt được còn 162 / 257. Nhiều IP có đặc trưng giống hệt nhau (1 kết nối, 1 ngày, 1 giờ) mà đáp án khác nhau — hỏi thêm bao nhiêu câu cũng không tách được. Muốn giỏi hơn phải có đặc trưng mới, không phải cây to hơn.

M2.3-4. Thử thuật toán khác trên cùng dữ liệu

In [ ]:
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier
# Dùng đúng DAC_TRUNG và cách chia ở ô trên
cac_mo_hinh = {
    "Naive Bayes (như bài M2)": GaussianNB(),
    f"Cây quyết định (sâu {DO_SAU})": DecisionTreeClassifier(max_depth=DO_SAU, random_state=0),
    "Rừng 200 cây (Random Forest)": RandomForestClassifier(n_estimators=200, random_state=0, n_jobs=2),
}
for ten, m in cac_mo_hinh.items():
    p = m.fit(X_hoc, y_hoc).predict(X_thi)
    tn, fp, fn, tp = confusion_matrix(y_thi, p).ravel()
    print(f"{ten:30s} thi đúng {accuracy_score(y_thi, p):.1%} | bắt được {tp}/{tp + fn} IP dò mật khẩu | nghi nhầm {fp}")

Kết quả (với 4 đặc trưng mặc định): Naive Bayes 81.2% | bắt được 106/257 · Cây quyết định 87.6% | 172/257 · Rừng 200 cây 87.7% | 165/257. Với 7 đặc trưng (thêm 3 đặc trưng phần mềm): Rừng lên 92.2%.

👉 Hiểu: Naive Bayes rất hợp với chữ (bài M2) nhưng kém hơn với bảng số này. Không có thuật toán "tốt nhất cho mọi việc" — phải thử và đo trên đề thi.

M2.3-5. Hỏi máy về một IP cụ thể

In [ ]:
# ✏️ SỬA: IP muốn hỏi (lấy từ các bảng ở trên, ví dụ "130.12.182.196", "185.246.128.133")
IP_CAN_HOI = "154.18.197.29"
hang = bang.loc[[IP_CAN_HOI]]
print(hang[DAC_TRUNG].to_string())
p = cay.predict_proba(hang[DAC_TRUNG])[0, 1]
print(f"\nMáy đoán: khả năng dò mật khẩu {p:.0%} -> {'DÒ MẬT KHẨU' if p >= 0.5 else 'chỉ quét'}")
print("Thật ra :", "DÒ MẬT KHẨU" if hang.do_mat_khau.iloc[0] == 1 else "chỉ quét",
      "| IP này nằm ở phần", "THI" if IP_CAN_HOI in X_thi.index else "HỌC")

Kết quả: dòng đặc trưng 154.18.197.29 5569 12 24 464.1, rồi Máy đoán: khả năng dò mật khẩu 96% -> DÒ MẬT KHẨU · Thật ra : DÒ MẬT KHẨU | IP này nằm ở phần THI

👉 Hiểu: với IP thuộc phần THI, máy chưa từng thấy đáp án của nó — đó mới là phép thử thật. Trong SOC, mô hình kiểu này giúp xếp hàng ưu tiên: IP vừa xuất hiện mà máy đoán "sẽ dò mật khẩu" thì chặn / theo dõi trước.