{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Lab Module 2 — chạy từng khối code\n", "\n", "> **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 ô.\n", "> 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).\n", ">\n", "> Ô 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.\n", "> Sửa hỏng? Menu **Kernel → Restart Kernel and Run All Cells** để chạy lại từ đầu.\n", "\n", "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).\n", "\n", "---\n", "\n", "## Bài M2 — Máy học đầu tiên: Naive Bayes đối đầu cú lách luật\n", "\n", "Ở 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.\n", "\n", "### M2-1. Dữ liệu: máy học từ đâu, thi trên gì?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# Bảng hiện đủ nội dung tin (mặc định pandas cắt ở 50 ký tự)\n", "pd.set_option(\"display.max_colwidth\", 150)\n", "# Bộ HỌC: 50 tin có nhãn · Bộ THI: 10 tin lách luật của bài M1\n", "hoc = pd.read_csv(\"~/lab/day1_labs/Manual_SOC_Dataset.csv\")\n", "thi = pd.read_csv(\"~/lab/day1_labs/Evasion_Dataset.csv\")\n", "# Đặt tên ngắn cho 5 cột: mã tin, kênh, người gửi, nội dung, nhãn đúng\n", "hoc.columns = thi.columns = [\"ma\", \"kenh\", \"nguoi_gui\", \"noi_dung\", \"nhan\"]\n", "print(\"BỘ HỌC:\", len(hoc), \"tin\\n\" + hoc.nhan.value_counts().to_string())\n", "print(\"\\nBỘ THI:\", len(thi), \"tin\\n\" + thi.nhan.value_counts().to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `BỘ HỌC: 50 tin` (`Lừa đảo 30`, `An toàn 20`) · `BỘ THI: 10 tin` (`Lừa đảo 10`)\n", "\n", "👉 **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.\n", "\n", "### M2-2. Máy \"đọc\" chữ thế nào? Thử TF-IDF trên 3 câu\n", "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.feature_extraction.text import TfidfVectorizer\n", "# ✏️ SỬA: 3 câu ví dụ, đổi thành câu của bạn\n", "cau = [\"chuyển khoản gấp vào tài khoản này\",\n", " \"chiều nay họp tại phòng họp lớn\",\n", " \"bấm vào link này để nhận quà\"]\n", "# 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ừ)\n", "tfidf_thu = TfidfVectorizer()\n", "bang_so = tfidf_thu.fit_transform(cau)\n", "tu_dien = tfidf_thu.get_feature_names_out()\n", "print(\"Từ điển máy tự tạo (\" + str(len(tu_dien)) + \" từ):\", list(tu_dien))\n", "# Với mỗi câu: in các từ có điểm > 0, điểm cao xếp trước\n", "for i in range(len(cau)):\n", " print(\"\\nCâu\", i + 1, \":\", cau[i])\n", " diem = bang_so[i].toarray()[0]\n", " for tu, d in sorted(zip(tu_dien, diem), key=lambda x: -x[1]):\n", " if d > 0:\n", " print(f\" {tu:8s} {d:.2f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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` …\n", "\n", "👉 **Hiểu:**\n", "- `khoản` lặp **2 lần** trong câu 1 → điểm cao nhất (0.70).\n", "- `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).\n", "- Máy tách theo từng tiếng: \"tài khoản\" thành `tài` + `khoản`.\n", "\n", "### M2-3. Cho máy học 50 tin, thi trên 10 tin lách luật" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.feature_extraction.text import TfidfVectorizer\n", "from sklearn.naive_bayes import MultinomialNB\n", "# 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ố\n", "tfidf = TfidfVectorizer()\n", "X_hoc = tfidf.fit_transform(hoc.noi_dung)\n", "# Nhãn -> số: 1 = lừa đảo, 0 = an toàn\n", "y_hoc = hoc.nhan.str.contains(\"Lừa đảo\").astype(int)\n", "print(\"Từ điển:\", len(tfidf.vocabulary_), \"từ -> mỗi tin là một dãy\", len(tfidf.vocabulary_), \"con số\")\n", "# 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\n", "mo_hinh = MultinomialNB().fit(X_hoc, y_hoc)\n", "# 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\n", "doan = mo_hinh.predict(tfidf.transform(thi.noi_dung))\n", "for ma, nd, d in zip(thi.ma, thi.noi_dung, doan):\n", " print(ma, \"->\", \"Lừa đảo\" if d == 1 else \"An toàn\", \"|\", nd[:60])\n", "print(\"Đúng:\", (doan == 1).sum(), \"/\", len(thi), \" (bộ lọc từ khóa ở M1: 2/10)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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)`\n", "\n", "👉 **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.\n", "\n", "### M2-4. Máy chắc chắn đến đâu?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# predict_proba = xác suất của từng tin; [:, 1] = cột xác suất \"lừa đảo\"\n", "xac_suat = mo_hinh.predict_proba(tfidf.transform(thi.noi_dung))[:, 1]\n", "# ✏️ SỬA: ngưỡng kết luận lừa đảo (0.5 = 50%). Thử 0.6 hoặc 0.7\n", "NGUONG = 0.5\n", "for ma, nd, p in zip(thi.ma, thi.noi_dung, xac_suat):\n", " ket_luan = \"LỪA ĐẢO\" if p >= NGUONG else \"an toàn\"\n", " print(f\"{ma} | khả năng lừa đảo {p:4.0%} -> {ket_luan:7s} | {nd[:55]}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`.\n", "\n", "👉 **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.\n", "\n", "### M2-5. Đảo vai: học từ 10 tin lách luật, thi trên 50 tin cũ" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Đả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)\n", "tfidf_dao = TfidfVectorizer()\n", "mo_hinh_dao = MultinomialNB().fit(tfidf_dao.fit_transform(thi.noi_dung), thi.nhan.str.contains(\"Lừa đảo\").astype(int))\n", "doan_dao = mo_hinh_dao.predict(tfidf_dao.transform(hoc.noi_dung))\n", "dung = (doan_dao == hoc.nhan.str.contains(\"Lừa đảo\").astype(int))\n", "print(\"Từ điển:\", len(tfidf_dao.vocabulary_), \"từ\")\n", "print(\"Máy đoán: \", pd.Series(doan_dao).map({1: \"Lừa đảo\", 0: \"An toàn\"}).value_counts().to_dict())\n", "print(\"Đúng:\", dung.sum(), \"/\", len(hoc))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Từ điển: 174 từ` · `Máy đoán: {'Lừa đảo': 50}` · `Đúng: 30 / 50`\n", "\n", "👉 **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 đó.**\n", "\n", "### M2-6. Tự ra đề thi có cả tin an toàn" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ SỬA: viết thêm / đổi tin (nội dung, nhãn đúng: 1 = lừa đảo, 0 = an toàn)\n", "DE_THI = [\n", " (\"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),\n", " (\"Con ơi tối nay về ăn cơm nhé mẹ nấu canh chua.\", 0),\n", " (\"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),\n", " (\"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),\n", "]\n", "# Dùng mô hình học từ 50 tin ở M2-3\n", "dung = 0\n", "for nd, that in DE_THI:\n", " p = mo_hinh.predict_proba(tfidf.transform([nd]))[0, 1]\n", " doan_1 = 1 if p >= 0.5 else 0\n", " dung += (doan_1 == that)\n", " 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]}\")\n", "print(\"Đúng:\", dung, \"/\", len(DE_THI))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`\n", "\n", "👉 **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`.\n", "\n", "---\n", "\n", "## Bài M2.2 — Log honeypot thật: làm sạch → biểu đồ → đặc trưng\n", "\n", "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).\n", "\n", "### M2.2-1. Thông tin của một lần tấn công nằm trên nhiều dòng" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%bash\n", "# Tìm mọi dòng log có mã phiên (session) 54613b5cec67 trong ngày 22/09\n", "grep '\"54613b5cec67\"' /opt/csai/data/honeypot_m1/cowrie.json.2026-09-22" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`).\n", "\n", "👉 **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`**.\n", "\n", "### M2.2-2. Làm sạch và ghép dữ liệu — 4 khối nhỏ\n", "\n", "**a — Đọc 30 file log**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import glob\n", "# Đọc cả 30 file (mỗi dòng 1 sự kiện JSON) thành một bảng\n", "cac_file = sorted(glob.glob(\"/opt/csai/data/honeypot_m1/cowrie.json.*\"))\n", "log = pd.concat(pd.read_json(f, lines=True, dtype=False, convert_dates=False) for f in cac_file)\n", "print(\"Đã tải:\", len(log), \"dòng log từ\", len(cac_file), \"file\")\n", "print(log.eventid.value_counts().to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`.\n", "\n", "**b — Lập \"danh bạ\": mã session → tên phần mềm**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Từ các dòng client.version: mỗi session 1 tên phần mềm\n", "danh_ba = log[log.eventid == \"cowrie.client.version\"].drop_duplicates(\"session\").set_index(\"session\").version\n", "print(\"Danh bạ có\", len(danh_ba), \"session\")\n", "danh_ba.head(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Danh bạ có 129951 session` + 3 dòng `mã session → SSH-2.0-Go`.\n", "\n", "**c — Giữ dòng kết nối, tra danh bạ để ghép tên phần mềm, tách ngày / giờ**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Mỗi dòng connect = 1 kết nối\n", "d = log[log.eventid == \"cowrie.session.connect\"][[\"timestamp\", \"src_ip\", \"session\"]].copy()\n", "# Tra danh bạ theo session; kết nối ngắt trước khi khai phần mềm -> \"unknown\"\n", "d[\"client_version\"] = d.session.map(danh_ba).fillna(\"unknown\")\n", "# Tách thời gian (giữ giờ UTC như log gốc): ngày, giờ 0-23, thứ trong tuần\n", "t = pd.to_datetime(d.timestamp, utc=True)\n", "d[\"date\"] = t.dt.date.astype(str)\n", "d[\"hour\"] = t.dt.hour\n", "d[\"weekday\"] = t.dt.day_name()\n", "print(\"Còn lại:\", len(d), \"kết nối | từ\", d.src_ip.nunique(), \"IP\")\n", "print(f\"Có tên phần mềm: {(d.client_version != 'unknown').mean():.1%} kết nối\")\n", "d.head(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`).\n", "\n", "👉 **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.\n", "\n", "**d — Lưu bảng sạch**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d.to_csv(\"~/lab/day1_labs/cleaned_honeypot_data.csv\", index=False)\n", "print(\"Đã lưu cleaned_honeypot_data.csv:\", len(d), \"dòng\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Đã lưu cleaned_honeypot_data.csv: 136063 dòng`\n", "\n", "### M2.2-3. 5 biểu đồ, mỗi biểu đồ gợi ra một đặc trưng\n", "\n", "**Biểu đồ 1 — số kết nối mỗi ngày**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "# Vẽ biểu đồ ngay dưới ô (máy lớp mặc định chỉ lưu ảnh ra file khi chạy trong terminal)\n", "%matplotlib inline\n", "theo_ngay = d.groupby(\"date\").size()\n", "theo_ngay.plot(kind=\"bar\", figsize=(14, 4), color=\"steelblue\", title=\"Số kết nối mỗi ngày\")\n", "plt.show()\n", "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\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`\n", "\n", "👉 **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**.\n", "\n", "**Biểu đồ 2 — top 20 IP**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ SỬA: số IP muốn xem (20). Thử 50\n", "TOP = 20\n", "so_lan = d.src_ip.value_counts()\n", "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\")\n", "plt.show()\n", "mot_phan_tram = len(so_lan) // 100\n", "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%}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `top 20 IP = 29% số kết nối; top 1% IP (29 IP) = 38%`\n", "\n", "👉 **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**.\n", "\n", "**Biểu đồ 3 — theo giờ trong ngày (UTC)**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "theo_gio = d.groupby(\"hour\").size()\n", "theo_gio.plot(kind=\"bar\", figsize=(12, 4), color=\"goldenrod\", title=\"Số kết nối theo giờ (UTC; giờ VN = UTC + 7)\")\n", "plt.show()\n", "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\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`\n", "\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ờ**.\n", "\n", "**Biểu đồ 4 — phần mềm kẻ tấn công dùng**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Rút gọn: \"SSH-2.0-Go\" -> \"Go\", \"SSH-2.0-PuTTY_Release_0.84\" -> \"PuTTY\"; không khai báo -> \"unknown\"\n", "d[\"phan_mem\"] = d.client_version.str.extract(r\"^SSH-[\\d.]+-([A-Za-z]+)\")[0].fillna(\"unknown\")\n", "top_pm = d.phan_mem.value_counts().head(8)\n", "top_pm.plot(kind=\"barh\", figsize=(8, 4), color=\"seagreen\", title=\"Phần mềm (số kết nối)\")\n", "plt.gca().invert_yaxis()\n", "plt.show()\n", "print(\" · \".join(f\"{k} {v / len(d):.1%}\" for k, v in top_pm.head(5).items()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Go 88.5% · unknown 5.0% · libssh 3.1% · PuTTY 1.0% · phpseclib 0.7%`\n", "\n", "👉 **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**.\n", "\n", "**Biểu đồ 5 — mỗi IP kết nối bao nhiêu lần một ngày**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "moi_ip = d.groupby(\"src_ip\").agg(tong=(\"src_ip\", \"size\"), so_ngay=(\"date\", \"nunique\"))\n", "ti_le = moi_ip.tong / moi_ip.so_ngay\n", "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)\")\n", "plt.show()\n", "q = ti_le.quantile([0.5, 0.9, 0.99])\n", "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}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `trung vị 2.0 · 90% 19.2 · 99% 785.0 · cao nhất 1,437`\n", "\n", "👉 **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.)\n", "\n", "### M2.2-4. Xếp đặc trưng thành bảng: mỗi IP một hàng" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 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\n", "bang = d.groupby(\"src_ip\").agg(\n", " so_ket_noi=(\"src_ip\", \"size\"), # tổng số kết nối\n", " so_ngay=(\"date\", \"nunique\"), # xuất hiện bao nhiêu ngày khác nhau\n", " so_khung_gio=(\"hour\", \"nunique\"), # hoạt động trong bao nhiêu giờ khác nhau của ngày (tối đa 24)\n", " phan_mem=(\"phan_mem\", lambda s: s.mode()[0]), # phần mềm dùng nhiều nhất\n", ")\n", "bang[\"ket_noi_moi_ngay\"] = (bang.so_ket_noi / bang.so_ngay).round(1)\n", "print(\"Bảng đặc trưng:\", len(bang), \"IP x\", len(bang.columns), \"đặc trưng\")\n", "bang.sort_values(\"so_ket_noi\", ascending=False).head(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`, …" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ SỬA: đổi số 1 thành số khác để xem 5 IP ngẫu nhiên khác\n", "bang.sample(5, random_state=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`.\n", "\n", "👉 **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.\n", "\n", "### M2.2-5. Tra cứu sâu trên bảng sạch\n", "\n", "**a — Top 5 IP chiếm bao nhiêu? Ai gây ra ngày bùng nổ?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "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\")\n", "print(\"3 IP nhiều nhất ngày 2026-09-20:\")\n", "print(d[d.date == \"2026-09-20\"].src_ip.value_counts().head(3).to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`\n", "\n", "👉 **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.\n", "\n", "**b — IP nào đổi nhiều phần mềm nhất?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Với mỗi IP: đếm số phần mềm KHÁC NHAU nó đã dùng (bỏ qua \"unknown\")\n", "so_phan_mem = d[d.phan_mem != \"unknown\"].groupby(\"src_ip\").phan_mem.nunique()\n", "print(\"Số IP chỉ dùng 1 phần mềm: \", (so_phan_mem == 1).sum())\n", "print(\"Số IP dùng từ 2 phần mềm trở lên:\", (so_phan_mem >= 2).sum())\n", "print(\"\\nTOP 5 IP đổi nhiều phần mềm nhất:\")\n", "for ip in so_phan_mem.sort_values(ascending=False).head(5).index:\n", " cac_pm = d[(d.src_ip == ip) & (d.phan_mem != \"unknown\")].phan_mem.unique()\n", " print(f\" {ip:16s} {len(cac_pm)} phần mềm: {', '.join(sorted(cac_pm))}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`\n", "\n", "👉 **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ủ.\n", "\n", "**c — IP \"không bao giờ ngủ\"**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "so_gio = d.groupby(\"src_ip\").hour.nunique()\n", "ip_24h = so_gio[so_gio == 24].index\n", "print(\"Số IP hoạt động ĐỦ 24 giờ trong ngày:\", len(ip_24h))\n", "print(\"Số IP chỉ hoạt động trong 1 giờ: \", (so_gio == 1).sum())\n", "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\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`\n", "\n", "👉 **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.\n", "\n", "**d — Mổ xẻ một ngày**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ SỬA: ngày muốn soi (ngày UTC), trong khoảng 2026-08-24 .. 2026-09-22\n", "NGAY_CAN_SOI = \"2026-09-20\"\n", "mot_ngay = d[d.date == NGAY_CAN_SOI]\n", "tb = len(d) / d.date.nunique()\n", "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\")\n", "dai = mot_ngay.src_ip.str.rsplit(\".\", n=1).str[0] + \".0/24\"\n", "print(\"TOP 3 dải mạng trong ngày:\\n\" + dai.rename(\"dai_mang\").value_counts().head(3).to_string())\n", "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\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`\n", "\n", "👉 **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).\n", "\n", "---\n", "\n", "## 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?\n", "\n", "Ở 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.\n", "\n", "### M2.3-1. Gắn nhãn: IP nào đã dò mật khẩu?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 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\n", "ip_dang_nhap = set(log[log.eventid.isin([\"cowrie.login.failed\", \"cowrie.login.success\"])].src_ip)\n", "# 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\n", "bang[\"do_mat_khau\"] = bang.index.isin(ip_dang_nhap).astype(int)\n", "print(bang.do_mat_khau.value_counts().rename({1: \"1 = dò mật khẩu\", 0: \"0 = chỉ quét\"}).to_string())\n", "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%}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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%`\n", "\n", "👉 **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.\n", "\n", "### M2.3-2. Thêm 3 đặc trưng về phần mềm" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "them = d.groupby(\"src_ip\").agg(\n", " so_phan_mem=(\"phan_mem\", \"nunique\"), # dùng bao nhiêu phần mềm khác nhau\n", " 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\n", ")\n", "bang = bang.drop(columns=[\"so_phan_mem\", \"ti_le_khong_khai\"], errors=\"ignore\").join(them)\n", "bang[\"dung_go\"] = (bang.phan_mem == \"Go\").astype(int) # phần mềm chính là Go? (1 = có)\n", "print(\"Các cột của bảng:\", list(bang.columns))\n", "# 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)\n", "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)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** danh sách cột, rồi bảng trung bình 2 nhóm:\n", "\n", "| nhóm | so_ket_noi | so_ngay | so_khung_gio | ket_noi_moi_ngay | ti_le_khong_khai | dung_go |\n", "|---|---|---|---|---|---|---|\n", "| 0 = chỉ quét | 2.16 | 1.19 | 1.21 | 1.80 | 0.79 | 0.10 |\n", "| 1 = dò mật khẩu | 153.61 | 2.45 | 5.35 | 77.36 | 0.10 | 0.55 |\n", "\n", "👉 **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.\n", "\n", "### M2.3-3. Cây quyết định: máy tự tìm luật" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split\n", "from sklearn.tree import DecisionTreeClassifier, export_text\n", "from sklearn.metrics import accuracy_score, confusion_matrix\n", "# ✏️ SỬA: chọn đặc trưng cho máy dùng. Thử thêm \"dung_go\", \"ti_le_khong_khai\", \"so_phan_mem\"\n", "DAC_TRUNG = [\"so_ket_noi\", \"so_ngay\", \"so_khung_gio\", \"ket_noi_moi_ngay\"]\n", "# ✏️ 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\n", "DO_SAU = 3\n", "\n", "# 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)\n", "X_hoc, X_thi, y_hoc, y_thi = train_test_split(bang[DAC_TRUNG], bang.do_mat_khau,\n", " test_size=0.3, random_state=42, stratify=bang.do_mat_khau)\n", "cay = DecisionTreeClassifier(max_depth=DO_SAU, random_state=0).fit(X_hoc, y_hoc)\n", "doan = cay.predict(X_thi)\n", "print(\"Học trên\", len(X_hoc), \"IP, thi trên\", len(X_thi), \"IP\")\n", "print(f\"Đoán bừa 'tất cả chỉ quét' : {(y_thi == 0).mean():.1%}\")\n", "print(f\"Cây tự chấm bài đã học : {accuracy_score(y_hoc, cay.predict(X_hoc)):.1%}\")\n", "print(f\"Cây làm bài THI : {accuracy_score(y_thi, doan):.1%}\")\n", "tn, fp, fn, tp = confusion_matrix(y_thi, doan).ravel()\n", "print(f\"IP dò mật khẩu bắt được : {tp} / {tp + fn}\")\n", "print(f\"IP chỉ quét bị nghi nhầm : {fp} / {tn + fp}\")\n", "if DO_SAU <= 4:\n", " print(\"\\nLuật máy TỰ tìm ra (class: 1 = dò mật khẩu, 0 = chỉ quét):\")\n", " print(export_text(cay, feature_names=DAC_TRUNG))\n", "else:\n", " 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)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`.\n", "\n", "👉 **Hiểu:**\n", "- 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.\n", "- 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**.\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.\n", "\n", "### M2.3-4. Thử thuật toán khác trên cùng dữ liệu" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.naive_bayes import GaussianNB\n", "from sklearn.ensemble import RandomForestClassifier\n", "# Dùng đúng DAC_TRUNG và cách chia ở ô trên\n", "cac_mo_hinh = {\n", " \"Naive Bayes (như bài M2)\": GaussianNB(),\n", " f\"Cây quyết định (sâu {DO_SAU})\": DecisionTreeClassifier(max_depth=DO_SAU, random_state=0),\n", " \"Rừng 200 cây (Random Forest)\": RandomForestClassifier(n_estimators=200, random_state=0, n_jobs=2),\n", "}\n", "for ten, m in cac_mo_hinh.items():\n", " p = m.fit(X_hoc, y_hoc).predict(X_thi)\n", " tn, fp, fn, tp = confusion_matrix(y_thi, p).ravel()\n", " 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}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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%**.\n", "\n", "👉 **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.\n", "\n", "### M2.3-5. Hỏi máy về một IP cụ thể" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ 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\")\n", "IP_CAN_HOI = \"154.18.197.29\"\n", "hang = bang.loc[[IP_CAN_HOI]]\n", "print(hang[DAC_TRUNG].to_string())\n", "p = cay.predict_proba(hang[DAC_TRUNG])[0, 1]\n", "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'}\")\n", "print(\"Thật ra :\", \"DÒ MẬT KHẨU\" if hang.do_mat_khau.iloc[0] == 1 else \"chỉ quét\",\n", " \"| IP này nằm ở phần\", \"THI\" if IP_CAN_HOI in X_thi.index else \"HỌC\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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`\n", "\n", "👉 **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." ] } ], "nbformat": 4, "nbformat_minor": 5, "metadata": { "kernelspec": { "name": "csai", "display_name": "Python (lớp CSAI)", "language": "python" }, "language_info": { "name": "python" } } }