{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Lab Module 1 — 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: **M1** (bộ lọc từ khóa) · **M1.2** (log honeypot thật) · **M1.3** (máy học phân loại email).\n", "\n", "---\n", "\n", "## Bài M1 — Bộ lọc từ khóa (luật cứng IF/ELSE) và cú \"lách luật\"\n", "\n", "### M1-1. Đọc 50 tin nhắn có nhãn" ] }, { "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", "# Đọc file CSV: mỗi dòng 1 tin nhắn, 5 cột\n", "tin = pd.read_csv(\"~/lab/day1_labs/Manual_SOC_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", "tin.columns = [\"ma\", \"kenh\", \"nguoi_gui\", \"noi_dung\", \"nhan\"]\n", "print(\"Số tin:\", len(tin))\n", "# Đếm mỗi loại nhãn có bao nhiêu tin\n", "print(tin.nhan.value_counts().to_string())\n", "# Dòng cuối của ô = bảng hiện ra bên dưới: 5 tin đầu\n", "tin.head(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Số tin: 50` · `Lừa đảo (Phishing) 30` · `An toàn (Benign) 20`, rồi bảng 5 tin đầu (MSG-001 … MSG-005).\n", "\n", "👉 **Hiểu:** mỗi tin có **nhãn đúng** do người soạn bài gắn sẵn → dùng để chấm xem bộ lọc đoán đúng bao nhiêu.\n", "\n", "### M1-2. Viết luật: 2 danh sách \"đáng ngờ\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ SỬA: danh sách TỪ KHÓA đáng ngờ (viết chữ thường). Tin nào chứa 1 từ trong đây -> coi là lừa đảo\n", "TU_KHOA = [\"thanh toán gấp\", \"chuyển gấp\", \"bảo trì hệ thống\", \"số tài khoản mới\",\n", " \"tai nạn giao thông\", \"viện phí\", \"quên mang ví\", \"trúng thưởng\",\n", " \"chiết khấu 50%\", \"quay trộm\", \"mờ ám\", \"bitcoin\", \"vneid\", \".apk\"]\n", "# ✏️ SỬA: danh sách TÊN MIỀN giả mạo. Người gửi hoặc nội dung có tên miền này -> lừa đảo\n", "TEN_MIEN = [\"doitac-uytln\", \"gmali\", \"vnpt-billing-pay\", \"viettel-nhanthuong\",\n", " \"shopee-sale-khung\", \"vieclam-tainha\", \"cong-an-dieu-tra\", \"webmail-verify\", \"micros0ft\"]\n", "print(len(TU_KHOA), \"từ khóa,\", len(TEN_MIEN), \"tên miền\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `14 từ khóa, 9 tên miền`\n", "\n", "👉 **Hiểu:** đây là toàn bộ \"trí khôn\" của bộ lọc: 23 chuỗi chữ do người viết tay.\n", "\n", "### M1-3. Hàm kiểm tra một tin: IF/ELSE" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def la_lua_dao(noi_dung, nguoi_gui, tu_khoa, ten_mien):\n", " \"\"\"Trả về (True/False, lý do). True = dính luật = lừa đảo.\"\"\"\n", " # Đổi hết sang chữ thường để \"TRÚNG THƯỞNG\" cũng khớp \"trúng thưởng\"\n", " noi_dung = noi_dung.lower()\n", " nguoi_gui = nguoi_gui.lower()\n", " # Luật 1: nội dung có chứa từ khóa nào không?\n", " for tu in tu_khoa:\n", " if tu in noi_dung:\n", " return True, \"từ khóa '\" + tu + \"'\"\n", " # Luật 2: người gửi hoặc nội dung có tên miền giả mạo nào không?\n", " for mien in ten_mien:\n", " if mien in nguoi_gui or mien in noi_dung:\n", " return True, \"tên miền '\" + mien + \"'\"\n", " # Không dính luật nào -> an toàn\n", " return False, \"\"\n", "\n", "# ✏️ SỬA: viết thử tin nhắn của bạn trong ngoặc kép (nội dung, người gửi) rồi chạy lại\n", "print(la_lua_dao(\"Chúc mừng bạn đã TRÚNG THƯỞNG 1 chiếc iPhone!\", \"Shopee\", TU_KHOA, TEN_MIEN))\n", "print(la_lua_dao(\"Tối nay mấy giờ đá bóng nhỉ?\", \"Hưng IT\", TU_KHOA, TEN_MIEN))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:**\n", "```\n", "(True, \"từ khóa 'trúng thưởng'\")\n", "(False, '')\n", "```\n", "👉 **Hiểu:** máy chỉ **so chữ**: thấy đúng chuỗi trong danh sách thì báo lừa đảo, không thấy thì cho qua. Thử viết \"trúng giải\" thay cho \"TRÚNG THƯỞNG\" xem máy còn bắt không.\n", "\n", "### M1-4. Chạy bộ lọc trên cả 50 tin" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def cham_bo_tin(bo_tin, tu_khoa, ten_mien):\n", " \"\"\"Chạy la_lua_dao trên từng tin của bảng, thêm 3 cột: may_doan, ly_do, dung\"\"\"\n", " kq = bo_tin.copy()\n", " doan = [la_lua_dao(nd, ng, tu_khoa, ten_mien) for nd, ng in zip(kq.noi_dung, kq.nguoi_gui)]\n", " kq[\"may_doan\"] = [\"Lừa đảo\" if bat else \"An toàn\" for bat, ly_do in doan]\n", " kq[\"ly_do\"] = [ly_do for bat, ly_do in doan]\n", " # Đúng khi máy đoán \"Lừa đảo\" và nhãn đúng cũng là lừa đảo (hoặc cả hai đều an toàn)\n", " kq[\"dung\"] = (kq.may_doan == \"Lừa đảo\") == kq.nhan.str.contains(\"Lừa đảo\")\n", " return kq\n", "\n", "kq = cham_bo_tin(tin, TU_KHOA, TEN_MIEN)\n", "print(\"Đoán đúng:\", kq.dung.sum(), \"/\", len(kq), f\"({kq.dung.mean():.0%})\")\n", "# Bảng các tin máy đoán SAI (~ nghĩa là \"không\")\n", "kq[~kq.dung][[\"ma\", \"nguoi_gui\", \"noi_dung\", \"nhan\", \"may_doan\"]]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Đoán đúng: 48 / 50 (96%)`, rồi bảng 2 tin máy đoán sai: MSG-005 và MSG-018 (cùng một tin tống tiền \"Mật khẩu 'honghanh123' của bạn đã bị lộ...\", nhãn Lừa đảo, máy đoán An toàn).\n", "\n", "👉 **Hiểu:** trên tin **quen thuộc** (luật được viết ra từ chính những tin này) bộ lọc đúng 96%. Tin tống tiền lọt vì không chứa chữ nào trong danh sách — thử thêm `\"bị lộ\"` vào `TU_KHOA` ở ô M1-2, chạy lại M1-2 rồi ô này.\n", "\n", "### M1-5. Máy nhanh cỡ nào?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "# ✏️ SỬA: số tin muốn thử (10_000 = mười nghìn). Thử 100_000 rồi 1_000_000\n", "SO_TIN = 10_000\n", "# Nhân bản 50 tin cho đủ SO_TIN tin\n", "nhieu_tin = pd.concat([tin] * (SO_TIN // len(tin)), ignore_index=True)\n", "bat_dau = time.perf_counter()\n", "kq_nhieu = cham_bo_tin(nhieu_tin, TU_KHOA, TEN_MIEN)\n", "giay = time.perf_counter() - bat_dau\n", "print(f\"Máy quét {len(nhieu_tin):,} tin trong {giay:.2f} giây, đúng {kq_nhieu.dung.mean():.0%}\")\n", "print(f\"Người đọc 10 giây/tin cần {len(nhieu_tin) * 10 / 3600:,.1f} giờ\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả** (số giây mỗi lần một khác là bình thường): `Máy quét 10,000 tin trong 0.0x giây, đúng 96%` · `Người đọc 10 giây/tin cần 27.8 giờ`\n", "\n", "👉 **Hiểu:** máy không biết mệt: hàng chục nghìn tin trong chưa tới 1 giây.\n", "\n", "### M1-6. Kẻ tấn công viết lại tin để \"lách luật\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 10 tin lừa đảo được viết lại, tránh đúng các chữ có trong luật\n", "le = pd.read_csv(\"~/lab/day1_labs/Evasion_Dataset.csv\")\n", "le.columns = [\"ma\", \"kenh\", \"nguoi_gui\", \"noi_dung\", \"nhan\"]\n", "kq_le = cham_bo_tin(le, TU_KHOA, TEN_MIEN)\n", "print(\"Bắt được:\", (kq_le.may_doan == \"Lừa đảo\").sum(), \"/\", len(le), \"tin lừa đảo\")\n", "kq_le[[\"ma\", \"nguoi_gui\", \"noi_dung\", \"may_doan\", \"ly_do\"]]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Bắt được: 2 / 10 tin lừa đảo`. Bảng: EVA-004 bị bắt vì `tên miền 'webmail-verify'`, EVA-007 vì `từ khóa '.apk'`, 8 tin còn lại `An toàn`.\n", "\n", "👉 **Hiểu:** so tin lọt với luật: luật có `doitac-uytln` nhưng kẻ tấn công viết `doitac-uytin`; luật có `tai nạn giao thông` nhưng tin viết \"bị tông xe\". Chỉ cần **đổi cách viết** là qua mặt luật cứng.\n", "\n", "### M1-7. Thử làm người viết luật: vá thêm từ khóa" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ SỬA: thêm từ khóa mới (nhìn các tin BỎ LỌT ở bảng trên để chọn), rồi chạy lại ô này\n", "THEM_TU_KHOA = [\"tông xe\", \"chuyển khoản\", \"tài khoản\"]\n", "TU_KHOA_MOI = TU_KHOA + THEM_TU_KHOA\n", "\n", "moi_le = cham_bo_tin(le, TU_KHOA_MOI, TEN_MIEN)\n", "moi_50 = cham_bo_tin(tin, TU_KHOA_MOI, TEN_MIEN)\n", "print(\"10 tin lách luật: bắt được\", (moi_le.may_doan == \"Lừa đảo\").sum(), \"/ 10 (luật cũ: 2)\")\n", "print(\"50 tin cũ : đoán đúng\", moi_50.dung.sum(), \"/ 50 (luật cũ: 48)\")\n", "# Tin AN TOÀN mà luật mới bắt nhầm thành lừa đảo\n", "nham = moi_50[(moi_50.may_doan == \"Lừa đảo\") & moi_50.nhan.str.contains(\"An toàn\")]\n", "print(\"Tin an toàn bị báo nhầm:\", len(nham))\n", "nham[[\"ma\", \"nguoi_gui\", \"noi_dung\", \"ly_do\"]]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `bắt được 5 / 10 (luật cũ: 2)` (thêm EVA-001 \"chuyển khoản\", EVA-003 \"tông xe\", EVA-010 \"tài khoản\") · `đoán đúng 42 / 50 (luật cũ: 48)` · `Tin an toàn bị báo nhầm: 6`, kèm bảng 6 tin thật (thông báo VNeID, hóa đơn kế toán) bị bắt vì `từ khóa 'tài khoản'`.\n", "\n", "👉 **Hiểu:** vá luật bắt thêm được tin lách luật, nhưng từ khóa chung chung (\"tài khoản\") làm **báo nhầm** tin thật. Thử bỏ `\"tài khoản\"` khỏi danh sách rồi chạy lại: hết báo nhầm nhưng cũng bắt ít hơn. Luật cứng luôn phải **chạy theo** kẻ tấn công → Bài M2 sẽ cho máy **tự học** thay vì viết tay.\n", "\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bài M1.2 — 30 ngày log honeypot THẬT: xem → thống kê → làm sạch → đặc trưng\n", "\n", "Honeypot = máy chủ SSH \"mồi\" cố tình để kẻ tấn công vào. Log ở `/opt/csai/data/honeypot_m1` là dữ liệu tấn công **thật** thu từ máy chủ của mình, 30 ngày 24/08–22/09/2026.\n", "\n", "### M1.2-① Xem log: mỗi dòng log là gì?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%bash\n", "# Xem tên 3 file log đầu tiên (mỗi ngày 1 file, tên có ngày)\n", "ls /opt/csai/data/honeypot_m1 | head -3\n", "# Lấy 3 dòng đầu của file ngày 22/09 và trình bày lại cho dễ đọc (python -m json.tool)\n", "head -3 /opt/csai/data/honeypot_m1/cowrie.json.2026-09-22 | python -m json.tool --json-lines" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:**\n", "```\n", "cowrie.json.2026-08-24\n", "cowrie.json.2026-08-25\n", "cowrie.json.2026-08-26\n", "{\n", " \"eventid\": \"cowrie.session.connect\",\n", " \"timestamp\": \"2026-09-22T00:00:02.779416Z\",\n", " \"src_ip\": \"109.160.32.67\",\n", " \"src_port\": 36330,\n", " \"session\": \"54613b5cec67\"\n", "}\n", "{\n", " \"eventid\": \"cowrie.client.version\",\n", " \"timestamp\": \"2026-09-22T00:00:02.793738Z\",\n", " \"src_ip\": \"109.160.32.67\",\n", " \"session\": \"54613b5cec67\",\n", " \"version\": \"SSH-2.0-Go\"\n", "}\n", "{\n", " \"eventid\": \"cowrie.login.failed\",\n", " \"timestamp\": \"2026-09-22T00:00:03.650371Z\",\n", " \"src_ip\": \"109.160.32.67\",\n", " \"session\": \"54613b5cec67\",\n", " \"username\": \"ftpuser1\",\n", " \"password\": \"123456\"\n", "}\n", "```\n", "👉 **Hiểu:** mỗi dòng là **một sự kiện**; trường `eventid` cho biết loại:\n", "\n", "| `eventid` | Nghĩa | Trường quan trọng |\n", "|---|---|---|\n", "| `cowrie.session.connect` | một máy lạ **kết nối** vào | `src_ip` = IP kẻ tấn công · `timestamp` = thời điểm, giờ **UTC** (chữ `Z`) |\n", "| `cowrie.client.version` | kẻ tấn công **khai tên phần mềm** SSH | `version` (vd `SSH-2.0-Go` = chương trình viết bằng ngôn ngữ Go) |\n", "| `cowrie.login.failed` / `.success` | kẻ tấn công **thử đăng nhập** (sai / đúng) | `username`, `password` đã thử |\n", "\n", "3 dòng mẫu là **một** lần tấn công (cùng `session`): kết nối → khai `SSH-2.0-Go` → thử `ftpuser1` / `123456`, tất cả trong 1 giây.\n", "\n", "### M1.2-② Log lớn cỡ nào?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%bash\n", "# Có bao nhiêu file log? (ls liệt kê, wc -l đếm số dòng = số file)\n", "ls /opt/csai/data/honeypot_m1 | wc -l\n", "# Cả thư mục nặng bao nhiêu? (du -sh = dung lượng, dạng dễ đọc)\n", "du -sh /opt/csai/data/honeypot_m1\n", "# Tổng cộng bao nhiêu dòng log? (cat ghép 30 file, wc -l đếm dòng)\n", "cat /opt/csai/data/honeypot_m1/cowrie.json.* | wc -l" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:**\n", "```\n", "30\n", "63M\t/opt/csai/data/honeypot_m1\n", "395124\n", "```\n", "👉 **Hiểu:** 30 file · 63 MB · **395.124 dòng**. Đọc tay mỗi dòng 1 giây = hơn 4 ngày không ngủ.\n", "\n", "### M1.2-③ Thống kê sơ bộ bằng lệnh Linux" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%bash\n", "# (1) Mỗi LOẠI sự kiện có bao nhiêu dòng?\n", "# grep -oh = chỉ lấy phần khớp mẫu \"eventid\": \"...\" ; sort | uniq -c = gom giống nhau và đếm ; sort -rn = nhiều nhất lên đầu\n", "grep -oh '\"eventid\": \"[^\"]*\"' /opt/csai/data/honeypot_m1/cowrie.json.* | sort | uniq -c | sort -rn\n", "# (2) Có bao nhiêu IP KHÁC NHAU đã kết nối?\n", "# lấy dòng kết nối -> cắt riêng phần IP -> sort -u = bỏ trùng -> đếm dòng\n", "grep -h '\"cowrie.session.connect\"' /opt/csai/data/honeypot_m1/cowrie.json.* | grep -o '\"src_ip\": \"[^\"]*\"' | sort -u | wc -l\n", "# (3) Top 5 IP kết nối nhiều nhất\n", "grep -h '\"cowrie.session.connect\"' /opt/csai/data/honeypot_m1/cowrie.json.* | grep -o '\"src_ip\": \"[^\"]*\"' | sort | uniq -c | sort -rn | head -5" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:**\n", "```\n", " 136063 \"eventid\": \"cowrie.session.connect\"\n", " 129951 \"eventid\": \"cowrie.client.version\"\n", " 65772 \"eventid\": \"cowrie.login.success\"\n", " 63338 \"eventid\": \"cowrie.login.failed\"\n", "2906\n", " 5569 \"src_ip\": \"154.18.197.29\"\n", " 3757 \"src_ip\": \"130.12.182.196\"\n", " 2774 \"src_ip\": \"109.160.32.31\"\n", " 2350 \"src_ip\": \"103.245.237.17\"\n", " 2291 \"src_ip\": \"85.11.167.127\"\n", "```\n", "👉 **Hiểu:** 136.063 lần kết nối từ **2.906 IP**, 4 loại sự kiện trộn lẫn. `grep` đếm được vài thứ đơn giản, nhưng đổi giờ UTC → giờ Việt Nam hay đếm cặp username/password thì rất khó → cần **làm sạch** log thành bảng." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### M1.2-④ Làm sạch: biến log thành bảng gọn — 5 khối nhỏ\n", "\n", "**④a — Đọc cả 30 file log vào một bảng**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import glob, os\n", "import pandas as pd\n", "# Lấy danh sách 30 file log, đọc từng file (mỗi dòng 1 sự kiện JSON) rồi ghép lại\n", "cac_file = sorted(glob.glob(\"/opt/csai/data/honeypot_m1/cowrie.json.*\"))\n", "df = pd.concat(pd.read_json(f, lines=True, dtype=False, convert_dates=False) for f in cac_file)\n", "print(\"Đọc xong:\", len(df), \"dòng, trộn\", df.eventid.nunique(), \"loại sự kiện,\", len(df.columns), \"cột:\", list(df.columns))\n", "# Mỗi loại sự kiện chỉ dùng vài cột -> các ô còn lại trống (NaN)\n", "df.head(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Đọc xong: 395124 dòng, trộn 4 loại sự kiện, 8 cột: [...]`, rồi bảng 5 dòng đầu có nhiều ô `NaN` (trống).\n", "\n", "👉 **Hiểu:** 4 loại sự kiện nằm chung một bảng → mỗi dòng chỉ điền vài cột, còn lại bỏ trống. Cần **tách** ra.\n", "\n", "**④b — Tách thành 3 bảng, mỗi bảng chỉ giữ cột cần dùng**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Kết nối: giờ + IP ; Đăng nhập (thất bại + thành công): IP + username + password ; Phần mềm: IP + tên phần mềm\n", "ket_noi = df[df.eventid == \"cowrie.session.connect\"][[\"timestamp\", \"src_ip\"]]\n", "dang_nhap = df[df.eventid.isin([\"cowrie.login.failed\", \"cowrie.login.success\"])][[\"src_ip\", \"username\", \"password\"]]\n", "phan_mem = df[df.eventid == \"cowrie.client.version\"][[\"src_ip\", \"version\"]]\n", "print(\"ket_noi:\", len(ket_noi), \"dòng | dang_nhap:\", len(dang_nhap), \"dòng | phan_mem:\", len(phan_mem), \"dòng\")\n", "dang_nhap.head(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `ket_noi: 136063 dòng | dang_nhap: 129110 dòng | phan_mem: 129951 dòng`, rồi 3 dòng đầu của bảng đăng nhập.\n", "\n", "**④c — Xử lý ô trống**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Đếm ô trống trong từng cột của bảng đăng nhập\n", "print(\"Ô trống mỗi cột:\\n\" + dang_nhap.isna().sum().to_string())\n", "# Lần thử đăng nhập KHÔNG gửi mật khẩu -> ghi rõ \"(không có)\", không xóa dòng\n", "dang_nhap = dang_nhap.fillna({\"password\": \"(không có)\"})\n", "print(\"Sau khi điền, còn ô trống:\", dang_nhap.password.isna().sum())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** cột `password` có **244** ô trống, sau khi điền còn `0`.\n", "\n", "👉 **Hiểu:** không xóa dòng: \"không gửi mật khẩu\" cũng là một kiểu hành vi của kẻ tấn công.\n", "\n", "**④d — Đổi giờ quốc tế (UTC) sang giờ Việt Nam**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Log ghi giờ UTC (chữ Z ở cuối). Việt Nam = UTC + 7 giờ\n", "ket_noi[\"gio_vn\"] = pd.to_datetime(ket_noi.timestamp, utc=True).dt.tz_convert(\"Asia/Ho_Chi_Minh\")\n", "# Đặt 2 cột cạnh nhau để so, rồi chỉ giữ giờ VN + IP\n", "print(ket_noi[[\"timestamp\", \"gio_vn\"]].head(3).to_string(index=False))\n", "ket_noi = ket_noi[[\"gio_vn\", \"src_ip\"]]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** 3 dòng, ví dụ `2026-08-24T00:01:03.700755Z` → `2026-08-24 07:01:03.700755+07:00`\n", "\n", "👉 **Hiểu:** 0h01 UTC = 7h01 sáng Việt Nam. Không đổi giờ thì mọi thống kê \"giờ nào bị tấn công nhiều\" đều lệch 7 tiếng.\n", "\n", "**④e — Lưu 3 bảng sạch**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Lưu vào thư mục ~/m12_sach (các bước sau mở lại từ đây, kể cả khi bạn khởi động lại kernel)\n", "thu_muc = os.path.expanduser(\"~/m12_sach\")\n", "os.makedirs(thu_muc, exist_ok=True)\n", "ket_noi.to_csv(thu_muc + \"/ket_noi.csv\", index=False)\n", "dang_nhap.to_csv(thu_muc + \"/dang_nhap.csv\", index=False)\n", "phan_mem.to_csv(thu_muc + \"/phan_mem.csv\", index=False)\n", "print(\"Đã lưu:\", sorted(os.listdir(thu_muc)))\n", "print(\"395124 dòng trộn lẫn ->\", len(ket_noi), \"+\", len(dang_nhap), \"+\", len(phan_mem), \"dòng trong 3 bảng gọn\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Đã lưu: ['dang_nhap.csv', 'ket_noi.csv', 'phan_mem.csv']` · `395124 dòng trộn lẫn -> 136063 + 129110 + 129951 dòng trong 3 bảng gọn`\n", "\n", "### M1.2-⑤ Đặc trưng: trả lời 4 câu hỏi, mỗi câu một khối\n", "\n", "**⑤a — Có bao nhiêu IP tấn công? IP nào nhiều nhất?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# Mở bảng kết nối đã làm sạch ở bước ④\n", "ket_noi = pd.read_csv(\"~/m12_sach/ket_noi.csv\")\n", "# Đếm mỗi IP xuất hiện bao nhiêu lần, xếp nhiều nhất lên đầu\n", "so_lan = ket_noi.src_ip.value_counts()\n", "# len(ket_noi) = số kết nối; len(so_lan) = số IP khác nhau\n", "print(\"Số lần kết nối:\", len(ket_noi), \"| Số IP khác nhau:\", len(so_lan))\n", "# .head(3) = 3 IP đầu bảng\n", "print(\"Top 3 IP kết nối nhiều nhất:\\n\" + so_lan.head(3).to_string())\n", "# (so_lan == 1).sum() = đếm IP chỉ xuất hiện đúng 1 lần\n", "print(\"Số IP chỉ ghé 1 lần:\", (so_lan == 1).sum())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Số lần kết nối: 136063 | Số IP khác nhau: 2906` · top 3: `154.18.197.29 5569`, `130.12.182.196 3757`, `109.160.32.31 2774` · `Số IP chỉ ghé 1 lần: 1103`\n", "\n", "👉 **Hiểu:** giống hệt kết quả `grep` ở bước ③ — nhưng giờ chỉ cần **công thức TOP**: `bảng.cột.value_counts().head(N)`.\n", "\n", "**⑤b — Giờ nào bị tấn công nhiều nhất (giờ Việt Nam)?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "ket_noi = pd.read_csv(\"~/m12_sach/ket_noi.csv\")\n", "# Đọc lại cột giờ Việt Nam thành thời gian thật, rồi lấy riêng GIỜ (0-23)\n", "gio = pd.to_datetime(ket_noi.gio_vn, utc=True).dt.tz_convert(\"Asia/Ho_Chi_Minh\").dt.hour\n", "# Đếm số kết nối mỗi giờ, xếp theo giờ từ 0h đến 23h\n", "dem = gio.value_counts().sort_index()\n", "# Mỗi giờ in 1 dòng: giờ | số kết nối | thanh # (giờ đông nhất = 40 dấu #)\n", "for h, n in dem.items():\n", " print(f\"{h:02d}h | {n:5d} | \" + \"#\" * int(n * 40 / dem.max()))\n", "# idxmax() = giờ đông nhất; dem.max() / dem.min() = giờ đông gấp mấy lần giờ vắng\n", "print(\"Giờ đông nhất:\", dem.idxmax(), \"h (giờ VN) | gấp\", round(dem.max() / dem.min(), 1), \"lần giờ vắng nhất\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** 24 dòng `00h` … `23h` kèm thanh `#`, rồi `Giờ đông nhất: 0 h (giờ VN) | gấp 1.8 lần giờ vắng nhất`\n", "\n", "👉 **Hiểu:** giờ đông nhất chỉ gấp **1,8 lần** giờ vắng nhất → tấn công gần như **đều suốt 24 giờ**: đây là **máy chạy tự động**, không phải người ngồi gõ theo giờ làm việc.\n", "\n", "**⑤c — Kẻ tấn công thử username / password nào nhất?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# dtype=str: đọc mọi thứ là chữ (mật khẩu \"123456\" không bị biến thành số); keep_default_na=False: giữ mật khẩu rỗng\n", "dang_nhap = pd.read_csv(\"~/m12_sach/dang_nhap.csv\", dtype=str, keep_default_na=False)\n", "print(\"Tổng số lần thử đăng nhập:\", len(dang_nhap))\n", "# 5 username và 5 password bị thử nhiều nhất\n", "print(\"Top 5 username:\\n\" + dang_nhap.username.value_counts().head(5).to_string())\n", "print(\"Top 5 password:\\n\" + dang_nhap.password.value_counts().head(5).to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Tổng số lần thử đăng nhập: 129110` · username: `root 74861`, `ubuntu 5265`, `admin 4027`, `user 2125`, `test 1376` · password: `123456 6247`, `Aa112211 2476`, `123 2418`, `1234 2050`, `root 1593`\n", "\n", "👉 **Hiểu:** kẻ tấn công thử theo **danh sách mật khẩu yếu** có sẵn — máy chủ nào dùng các mật khẩu này sẽ bị đoán trúng ngay.\n", "\n", "**⑤d — Kẻ tấn công dùng phần mềm gì?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "phan_mem = pd.read_csv(\"~/m12_sach/phan_mem.csv\")\n", "# Đếm từng tên phần mềm SSH mà kẻ tấn công khai báo\n", "dem = phan_mem.version.value_counts()\n", "print(\"Top 5 phần mềm:\\n\" + dem.head(5).to_string())\n", "# dem.index[0] = tên đứng đầu; dem.iloc[0] / len(phan_mem) = tỉ lệ phần trăm của nó\n", "print(f\"Riêng {dem.index[0]} chiếm {dem.iloc[0] / len(phan_mem):.0%} số lần khai báo\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `SSH-2.0-Go 120382`, `SSH-2.0-libssh_0.9.6 2722`, `SSH-2.0-phpseclib_1.0 (openssl) 738`, `SSH-2.0-PuTTY_Release_0.84 719`, `SSH-2.0-libssh2_1.8.1 716` · `Riêng SSH-2.0-Go chiếm 93% số lần khai báo`\n", "\n", "👉 **Hiểu:** 93% là **chương trình tự động** viết bằng Go — không phải phần mềm người dùng hằng ngày.\n", "\n", "### M1.2-⑥ Tra cứu sâu: mỗi câu hỏi một khối\n", "Khối nào có dòng `SỬA DÒNG NÀY` thì được đổi giá trị trong ngoặc kép (IP, username, ngày) rồi chạy lại ô đó để hỏi câu khác.\n", "\n", "**⑥a — Ngày nào, thứ mấy bị tấn công nhiều nhất?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "ket_noi = pd.read_csv(\"~/m12_sach/ket_noi.csv\")\n", "# File CSV lưu thời gian dưới dạng chữ -> đổi lại thành thời gian thật (giờ Việt Nam)\n", "thoi_gian = pd.to_datetime(ket_noi.gio_vn, utc=True).dt.tz_convert(\"Asia/Ho_Chi_Minh\")\n", "# .dt.date = lấy NGÀY -> đếm -> lấy 5 ngày đông nhất\n", "print(\"TOP 5 NGÀY bị tấn công nhiều nhất:\")\n", "print(thoi_gian.dt.date.rename(\"ngay\").value_counts().head(5).to_string())\n", "# .dt.day_name() = lấy THỨ trong tuần (tiếng Anh: Monday, Tuesday...) -> đếm\n", "print(\"\\nTấn công theo THỨ trong tuần:\")\n", "print(thoi_gian.dt.day_name().rename(\"thu\").value_counts().to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** top ngày `2026-09-20 10275` · `2026-09-13 9183` · `2026-09-15 8014` · `2026-09-18 7720` · `2026-09-16 7326` · theo thứ: `Sunday 26130` đứng đầu … `Monday 16841` cuối\n", "\n", "👉 **Hiểu:** 30 ngày này chỉ có 4 Chủ nhật (thứ Hai, thứ Ba có 5) mà Chủ nhật vẫn đứng đầu — vì **2 ngày đông nhất 20/09 và 13/09 đều là Chủ nhật**. Vài ngày bùng nổ có thể kéo lệch cả bảng thống kê.\n", "\n", "**⑥b — Top cặp username / password**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# Mở bảng THỬ ĐĂNG NHẬP. dtype=str: đọc mọi thứ là chữ (mật khẩu \"123456\" không bị biến thành số)\n", "# keep_default_na=False: giữ nguyên mật khẩu rỗng, không coi là ô trống\n", "dang_nhap = pd.read_csv(\"~/m12_sach/dang_nhap.csv\", dtype=str, keep_default_na=False)\n", "# Ghép 2 cột thành 1 chuỗi \"username / password\" rồi đếm -> top theo CẶP\n", "print(\"TOP 10 CẶP username / password:\")\n", "print((dang_nhap.username + \" / \" + dang_nhap.password).value_counts().head(10).to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `root / Aa112211 2474` · `root / root 837` · `root / abcd1234 790` · `root / 3245gs5662d34 604` · `345gs5662d34 / 345gs5662d34 601` · `root / 123456 600` · `ubuntu / 123456 436` · `ubuntu / ubuntu 398` · …\n", "\n", "👉 **Hiểu:** kẻ tấn công thử theo **cặp** có sẵn, vd tên đăng nhập trùng mật khẩu (`ubuntu / ubuntu`).\n", "\n", "**⑥c — Một username bị thử những mật khẩu gì?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# ===== SỬA DÒNG NÀY: username muốn soi (thử \"admin\", \"ubuntu\", \"postgres\"...) =====\n", "USER_CAN_SOI = \"root\"\n", "dang_nhap = pd.read_csv(\"~/m12_sach/dang_nhap.csv\", dtype=str, keep_default_na=False)\n", "# Lọc: chỉ giữ các lần thử có username đúng bằng USER_CAN_SOI (== nghĩa là \"bằng\")\n", "loc = dang_nhap[dang_nhap.username == USER_CAN_SOI]\n", "print(\"Username\", USER_CAN_SOI, \"bị thử\", len(loc), \"lần, từ\", loc.src_ip.nunique(), \"IP khác nhau\")\n", "print(\"TOP 10 mật khẩu kẻ tấn công thử với\", USER_CAN_SOI + \":\")\n", "print(loc.password.value_counts().head(10).to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Username root bị thử 74861 lần, từ 706 IP khác nhau` · top: `Aa112211 2474`, `root 837`, `abcd1234 790`, `3245gs5662d34 604`, `123456 600`, … `P@ssw0rd 255`\n", "\n", "👉 **Hiểu:** thử đổi `\"root\"` thành `\"postgres\"` hoặc `\"oracle\"` (tài khoản dịch vụ hay có trên máy chủ) — danh sách mật khẩu thử khác hẳn.\n", "\n", "**⑥d — Kiểu tấn công: chỉ quét hay dò mật khẩu? Dùng công cụ gì?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# Mở cả 3 bảng sạch\n", "ket_noi = pd.read_csv(\"~/m12_sach/ket_noi.csv\")\n", "dang_nhap = pd.read_csv(\"~/m12_sach/dang_nhap.csv\", dtype=str, keep_default_na=False)\n", "phan_mem = pd.read_csv(\"~/m12_sach/phan_mem.csv\")\n", "# set(...) = tập hợp các IP KHÁC NHAU của mỗi bảng\n", "ip_ket_noi = set(ket_noi.src_ip)\n", "ip_dang_nhap = set(dang_nhap.src_ip)\n", "# Phép trừ tập hợp: IP có kết nối nhưng KHÔNG hề thử mật khẩu -> chỉ \"gõ cửa\" rồi đi (quét, dò dịch vụ)\n", "print(\"KIỂU 1 - Chỉ kết nối rồi đi (quét / dò dịch vụ):\", len(ip_ket_noi - ip_dang_nhap), \"IP\")\n", "# Phép giao tập hợp: IP vừa kết nối VỪA thử mật khẩu -> dò mật khẩu (brute-force)\n", "print(\"KIỂU 2 - Có thử mật khẩu (brute-force): \", len(ip_ket_noi & ip_dang_nhap), \"IP\")\n", "# Rút gọn tên phần mềm: \"SSH-2.0-Go\" -> \"Go\", \"SSH-2.0-PuTTY_Release_0.84\" -> \"PuTTY\"\n", "ho_phan_mem = phan_mem.version.str.extract(r\"^SSH-[\\d.]+-([A-Za-z]+)\")[0].fillna(\"khác\").rename(\"cong_cu\")\n", "print(\"\\nTOP 5 CÔNG CỤ tấn công (theo số lần khai báo):\")\n", "print(ho_phan_mem.value_counts().head(5).to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:**\n", "```\n", "KIỂU 1 - Chỉ kết nối rồi đi (quét / dò dịch vụ): 2049 IP\n", "KIỂU 2 - Có thử mật khẩu (brute-force): 857 IP\n", "```\n", "công cụ: `Go 120382` · `libssh 4269` · `PuTTY 1396` · `phpseclib 947` · `khác 739`\n", "\n", "👉 **Hiểu:** hơn 2/3 số IP chỉ \"gõ cửa\" rồi đi (**máy quét** dò khắp Internet); gần 1/3 mới thật sự **dò mật khẩu**.\n", "\n", "**⑥e — Hồ sơ một IP: nó đã làm gì?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# ===== SỬA DÒNG NÀY: IP muốn soi (lấy từ kết quả TOP 10 IP ở trên) =====\n", "IP_CAN_SOI = \"154.18.197.29\"\n", "ket_noi = pd.read_csv(\"~/m12_sach/ket_noi.csv\")\n", "dang_nhap = pd.read_csv(\"~/m12_sach/dang_nhap.csv\", dtype=str, keep_default_na=False)\n", "phan_mem = pd.read_csv(\"~/m12_sach/phan_mem.csv\")\n", "# Lọc từng bảng: chỉ giữ dòng của đúng IP này\n", "kn = ket_noi[ket_noi.src_ip == IP_CAN_SOI]\n", "dn = dang_nhap[dang_nhap.src_ip == IP_CAN_SOI].replace(\"\", \"(trống)\") # ô rỗng -> ghi \"(trống)\" cho dễ đọc\n", "pm = phan_mem[phan_mem.src_ip == IP_CAN_SOI]\n", "thoi_gian = pd.to_datetime(kn.gio_vn, utc=True).dt.tz_convert(\"Asia/Ho_Chi_Minh\")\n", "print(\"HỒ SƠ IP\", IP_CAN_SOI)\n", "# .min() = sớm nhất, .max() = muộn nhất, .dt.date.nunique() = số ngày khác nhau, .dt.hour.nunique() = số giờ khác nhau\n", "print(\" Lần đầu:\", thoi_gian.min(), \"| lần cuối:\", thoi_gian.max())\n", "print(\" Xuất hiện\", thoi_gian.dt.date.nunique(), \"ngày, trong\", thoi_gian.dt.hour.nunique(), \"khung giờ khác nhau của ngày\")\n", "print(\" Số lần kết nối:\", len(kn), \"| số lần thử đăng nhập:\", len(dn))\n", "print(\" Phần mềm đã khai:\", \", \".join(pm.version.unique()))\n", "print(\" TOP 5 username:\", \", \".join(dn.username.value_counts().head(5).index))\n", "print(\" TOP 5 password:\", \", \".join(dn.password.value_counts().head(5).index))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:**\n", "```\n", "HỒ SƠ IP 154.18.197.29\n", " Lần đầu: 2026-08-29 13:00:09.623421+07:00 | lần cuối: 2026-09-21 00:59:17.779344+07:00\n", " Xuất hiện 12 ngày, trong 24 khung giờ khác nhau của ngày\n", " Số lần kết nối: 5569 | số lần thử đăng nhập: 4590\n", " Phần mềm đã khai: SSH-2.0-Go\n", " TOP 5 username: root, ubuntu, toor, (trống)\n", " TOP 5 password: root@1234, root@123, root12345, root1, root123\n", "```\n", "👉 **Hiểu:** hoạt động ở **đủ 24 giờ**, chỉ một chương trình Go → **máy tự động**. Thử đổi IP thành `109.160.32.31`.\n", "\n", "**⑥f — Top dải mạng: nhiều IP nhưng thật ra là một \"chủ\"?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "ket_noi = pd.read_csv(\"~/m12_sach/ket_noi.csv\")\n", "# Cắt số cuối của IP: \"109.160.32.18\" -> tách ở dấu chấm CUỐI -> \"109.160.32\" -> thêm \".0/24\"\n", "ket_noi[\"dai_mang\"] = ket_noi.src_ip.str.rsplit(\".\", n=1).str[0] + \".0/24\"\n", "# Gom theo dải: đếm số kết nối và số IP khác nhau trong mỗi dải\n", "bang = ket_noi.groupby(\"dai_mang\").agg(so_ket_noi=(\"src_ip\", \"size\"), so_ip=(\"src_ip\", \"nunique\"))\n", "print(\"TOP 10 DẢI MẠNG /24 (cùng 3 số đầu của IP):\")\n", "print(bang.sort_values(\"so_ket_noi\", ascending=False).head(10).to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** đứng đầu `109.160.32.0/24 41899 42`, rồi `91.92.42.0/24 6805 11`, `160.187.229.0/24 6429 14`, `154.18.197.0/24 5569 1`, …\n", "\n", "👉 **Hiểu:** **42 IP** cùng dải `109.160.32.x` tạo ra **41.899** kết nối — gấp hơn 7 lần IP đứng đầu ⑤a. Nhìn từng IP không thấy; nhìn **cả dải** mới thấy nguồn tấn công lớn nhất. Chặn từng IP là không đủ.\n", "\n", "**⑥g — IP \"kiên trì\": quay lại nhiều ngày nhất**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "ket_noi = pd.read_csv(\"~/m12_sach/ket_noi.csv\")\n", "ket_noi[\"ngay\"] = pd.to_datetime(ket_noi.gio_vn, utc=True).dt.tz_convert(\"Asia/Ho_Chi_Minh\").dt.date\n", "# Với mỗi IP: đếm số NGÀY khác nhau nó xuất hiện -> xếp giảm dần -> lấy 10\n", "so_ngay = ket_noi.groupby(\"src_ip\").ngay.nunique().sort_values(ascending=False)\n", "print(\"TOP 10 IP 'kiên trì' nhất (xuất hiện nhiều ngày nhất trong 30 ngày):\")\n", "print(so_ngay.head(10).to_string())\n", "# (so_ngay == 30).sum() = có bao nhiêu IP xuất hiện đủ 30/30 ngày\n", "print(\"Số IP xuất hiện đủ 30/30 ngày:\", (so_ngay == 30).sum())\n", "print(\"Số IP chỉ xuất hiện 1 ngày: \", (so_ngay == 1).sum())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `94.154.43.56 30` · `185.246.128.133 30` · `176.53.159.196 29` · `94.154.43.234 29` · `185.246.130.20 27` · … · `Số IP xuất hiện đủ 30/30 ngày: 2` · `Số IP chỉ xuất hiện 1 ngày: 2487`\n", "\n", "👉 **Hiểu:** đa số IP (2.487 / 2.906) chỉ ghé **1 ngày** rồi biến mất — chặn sau khi phát hiện thì đã muộn; vài IP thì ngày nào cũng có mặt.\n", "\n", "**⑥h — Soi một ngày cụ thể**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# ===== SỬA DÒNG NÀY: ngày muốn soi (giờ Việt Nam), dạng năm-tháng-ngày, trong khoảng 2026-08-24 .. 2026-09-22 =====\n", "NGAY_CAN_SOI = \"2026-09-20\"\n", "ket_noi = pd.read_csv(\"~/m12_sach/ket_noi.csv\")\n", "thoi_gian = pd.to_datetime(ket_noi.gio_vn, utc=True).dt.tz_convert(\"Asia/Ho_Chi_Minh\")\n", "# Lọc các kết nối có NGÀY đúng bằng ngày cần soi (.astype(str) đổi ngày thành chữ để so sánh)\n", "mot_ngay = ket_noi[thoi_gian.dt.date.astype(str) == NGAY_CAN_SOI]\n", "gio_mot_ngay = thoi_gian[thoi_gian.dt.date.astype(str) == NGAY_CAN_SOI].dt.hour.rename(\"gio\")\n", "print(\"Ngày\", NGAY_CAN_SOI, \":\", len(mot_ngay), \"kết nối từ\", mot_ngay.src_ip.nunique(), \"IP\")\n", "print(\"TOP 5 IP trong ngày:\")\n", "print(mot_ngay.src_ip.value_counts().head(5).to_string())\n", "print(\"TOP 3 giờ trong ngày:\")\n", "print(gio_mot_ngay.value_counts().head(3).to_string())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Ngày 2026-09-20 : 10275 kết nối từ 178 IP` · top IP: 5 IP `109.160.32.x`, **mỗi IP đúng 1099** · top giờ: `12 1498`, `23 1425`, `16 937`\n", "\n", "👉 **Hiểu:** nhiều IP cùng dải, cùng **đúng** một số kết nối trong ngày → một hệ thống điều khiển chung. Thử đổi ngày thành `\"2026-09-05\"` (ngày vắng nhất).\n", "\n", "### M1.2-⑦ Tổng kết\n", "\n", "| Bước | Làm gì | Thu được |\n", "|---|---|---|\n", "| ① Xem log | `head`, `json.tool` | mỗi dòng = một sự kiện, 4 loại |\n", "| ② Đếm dữ liệu | `ls`, `du`, `wc -l` | 30 file, 63 MB, 395.124 dòng |\n", "| ③ Thống kê sơ bộ | `grep \\| sort \\| uniq -c` | số kết nối, số IP, top IP |\n", "| ④ Làm sạch | Python + pandas | 3 bảng gọn, giờ Việt Nam |\n", "| ⑤ Đặc trưng | `value_counts()` | top IP, giờ, username/password, phần mềm |\n", "| ⑥ Tra cứu sâu | lọc + đếm | ngày/thứ, cặp user/pass, kiểu tấn công, hồ sơ IP, dải mạng, IP kiên trì |" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Bài M1.3 — Máy học phân loại email (và cái bẫy \"100%\")\n", "\n", "### M1.3-1. Dữ liệu: 10 email, mỗi email lặp 10 lần" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# 10 email mẫu (tiếng Anh): spam = lừa đảo, ham = email thật\n", "EMAIL = [\n", " (\"URGENT: Your account will be closed. Please verify your password here.\", \"spam\"),\n", " (\"Hey team, just checking in on the project status.\", \"ham\"),\n", " (\"Claim your FREE lottery winnings now! Click this link.\", \"spam\"),\n", " (\"Meeting notes from yesterday's sync are attached.\", \"ham\"),\n", " (\"Verify your banking details immediately to prevent suspension.\", \"spam\"),\n", " (\"Let's grab lunch tomorrow around noon.\", \"ham\"),\n", " (\"Security Alert: We detected unusual login activity. Reset password.\", \"spam\"),\n", " (\"Please review the attached invoice for Q3.\", \"ham\"),\n", " (\"You have 1 new voice message. Listen here.\", \"spam\"),\n", " (\"Don't forget to submit your timesheets by Friday.\", \"ham\"),\n", "]\n", "# Nhân mỗi email lên 10 bản -> bảng 100 dòng, 2 cột: text, label\n", "email = pd.DataFrame(EMAIL * 10, columns=[\"text\", \"label\"])\n", "print(\"Tổng số dòng:\", len(email), \"| số email KHÁC NHAU:\", email.text.nunique())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Tổng số dòng: 100 | số email KHÁC NHAU: 10`\n", "\n", "👉 **Hiểu:** 100 dòng nhưng thật ra chỉ có **10 email** — nhớ điều này, nó là cái bẫy ở M1.3-6.\n", "\n", "### M1.3-2. Cách cũ: chấm điểm theo từ khóa" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ SỬA: từ khóa đáng ngờ (chữ thường)\n", "TU_KHOA_EMAIL = [\"urgent\", \"verify\", \"free\", \"reset\", \"password\"]\n", "for text, label in EMAIL:\n", " # Điểm = số từ khóa xuất hiện trong email\n", " diem = sum(1 for tu in TU_KHOA_EMAIL if tu in text.lower())\n", " print(f\"điểm {diem} | thật là: {label:4s} | {text}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** 10 dòng; email đầu (`URGENT ... verify ... password`) được **3 điểm**, nhưng `You have 1 new voice message` (spam) chỉ **0 điểm** — y như bài M1.\n", "\n", "### M1.3-3. Chia học / thi theo từng email" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import GroupShuffleSplit\n", "# ✏️ SỬA: đổi số 42 thành số khác -> máy chia 7/3 theo cách khác -> kết quả các ô sau đổi theo\n", "HAT_GIONG = 42\n", "# groups=email.text: 10 bản sao của một email luôn đi CÙNG nhau (hoặc cùng vào học, hoặc cùng vào thi)\n", "chia = GroupShuffleSplit(n_splits=1, test_size=0.3, random_state=HAT_GIONG)\n", "vi_tri_hoc, vi_tri_thi = next(chia.split(email, email.label, groups=email.text))\n", "hoc, thi = email.iloc[vi_tri_hoc], email.iloc[vi_tri_thi]\n", "print(\"Phần HỌC:\", hoc.text.nunique(), \"email (\", len(hoc), \"dòng) | Phần THI:\", thi.text.nunique(), \"email (\", len(thi), \"dòng)\")\n", "print(\"Email vừa có trong phần học vừa có trong phần thi:\", len(set(hoc.text) & set(thi.text)))\n", "print(\"Đề thi:\")\n", "for t in thi.text.unique():\n", " print(\" -\", t)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Phần HỌC: 7 email ( 70 dòng) | Phần THI: 3 email ( 30 dòng)` · `... cả hai: 0` · đề thi 3 email (trong đó có `Don't forget to submit your timesheets by Friday.`).\n", "\n", "👉 **Hiểu:** đề thi chỉ gồm email máy **chưa từng thấy** — giống thực tế: email lừa đảo ngày mai là email mới.\n", "\n", "### M1.3-4. Chữ → số (TF-IDF), rồi cho máy học (Naive Bayes)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.feature_extraction.text import TfidfVectorizer\n", "from sklearn.naive_bayes import MultinomialNB\n", "# Lập từ điển từ phần HỌC (bỏ các từ quá phổ biến tiếng Anh như \"the\", \"your\"), biến mỗi email thành 1 hàng số\n", "tfidf = TfidfVectorizer(stop_words=\"english\")\n", "X_hoc = tfidf.fit_transform(hoc.text)\n", "X_thi = tfidf.transform(thi.text)\n", "print(\"Từ điển máy học được:\", len(tfidf.vocabulary_), \"từ\")\n", "print(sorted(tfidf.vocabulary_))\n", "# Naive Bayes học: từ nào hay đi với spam, từ nào hay đi với ham\n", "mo_hinh = MultinomialNB().fit(X_hoc, hoc.label)\n", "print(\"Đã học xong.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Từ điển máy học được: 38 từ` + danh sách `['account', 'activity', 'alert', 'attached', ...]` + `Đã học xong.`\n", "\n", "👉 **Hiểu:** máy không \"đọc hiểu\": nó chỉ đếm từ nào hay xuất hiện ở email lừa đảo, từ nào ở email thật.\n", "\n", "### M1.3-5. Chấm bài thi" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix\n", "doan = mo_hinh.predict(X_thi)\n", "print(f\"Accuracy (đúng bao nhiêu phần) : {accuracy_score(thi.label, doan):.2f}\")\n", "print(f\"Precision (báo spam thì đúng bao nhiêu) : {precision_score(thi.label, doan, pos_label='spam', zero_division=0):.2f}\")\n", "print(f\"Recall (bắt được bao nhiêu spam thật) : {recall_score(thi.label, doan, pos_label='spam', zero_division=0):.2f}\")\n", "print(\"Bảng nhầm lẫn (hàng = thật ham/spam, cột = máy đoán ham/spam):\")\n", "print(confusion_matrix(thi.label, doan, labels=[\"ham\", \"spam\"]))\n", "print()\n", "for t in thi.text.unique():\n", " du_doan = mo_hinh.predict(tfidf.transform([t]))[0]\n", " that = email[email.text == t].label.iloc[0]\n", " print(\"ĐÚNG\" if du_doan == that else \"SAI \", f\"| máy đoán {du_doan:4s} | thật là {that:4s} |\", t)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Accuracy 0.67` · `Precision 0.50` · `Recall 1.00` · bảng nhầm lẫn `[[10 10] [ 0 10]]` (đếm theo **dòng**: mỗi email đề thi có 10 bản sao → 10 dòng ham đoán đúng, 10 dòng ham bị đoán nhầm thành spam, 10 dòng spam đoán đúng), rồi 3 dòng: 2 dòng `ĐÚNG`, 1 dòng `SAI | máy đoán spam | thật là ham | Don't forget to submit your timesheets by Friday.`\n", "\n", "👉 **Hiểu:** với email **mới**, máy đúng 2/3 và báo nhầm một email công việc bình thường là lừa đảo — vì chỉ có 7 email để học.\n", "\n", "### M1.3-6. Tự viết email để thử máy" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ✏️ SỬA: viết email của bạn (tiếng Anh) trong ngoặc kép rồi chạy lại\n", "EMAIL_CUA_TOI = \"Please verify your password to claim a free gift\"\n", "xac_suat = mo_hinh.predict_proba(tfidf.transform([EMAIL_CUA_TOI]))[0]\n", "for nhan, p in zip(mo_hinh.classes_, xac_suat):\n", " print(f\"{nhan}: {p:.0%}\")\n", "print(\"=> máy đoán:\", mo_hinh.classes_[xac_suat.argmax()])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `ham: 4%` · `spam: 96%` · `=> máy đoán: spam`\n", "\n", "👉 **Hiểu:** thử viết email không có chữ nào trong từ điển ở M1.3-4 (ví dụ `\"See you at the gym\"`): máy chỉ còn đoán theo tỉ lệ spam/ham đã học.\n", "\n", "### M1.3-7. Cái bẫy \"100% ảo\": chia ngẫu nhiên từng dòng" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split\n", "# Cách chia SAI: chia ngẫu nhiên 100 DÒNG 70/30, không để ý các dòng là bản sao của nhau\n", "hoc2, thi2 = train_test_split(email, test_size=0.3, random_state=42)\n", "trung = set(hoc2.text) & set(thi2.text)\n", "print(\"Đề thi có\", thi2.text.nunique(), \"email khác nhau,\", len(trung), \"email trong số đó ĐÃ CÓ trong phần học\")\n", "tfidf2 = TfidfVectorizer(stop_words=\"english\")\n", "mo_hinh2 = MultinomialNB().fit(tfidf2.fit_transform(hoc2.text), hoc2.label)\n", "print(f\"Accuracy: {accuracy_score(thi2.label, mo_hinh2.predict(tfidf2.transform(thi2.text))):.2f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Kết quả:** `Đề thi có 10 email khác nhau, 10 email trong số đó ĐÃ CÓ trong phần học` · `Accuracy: 1.00`\n", "\n", "👉 **Hiểu:** chia ngẫu nhiên thì bản sao của **mọi** email đề thi đã nằm sẵn trong phần học → máy chỉ cần **nhớ** là được 100%. Con số thật về khả năng bắt email **mới** là 0.67 ở M1.3-5. Gặp kết quả 100%, luôn hỏi: đề thi có lẫn bài đã học không?" ] } ], "nbformat": 4, "nbformat_minor": 5, "metadata": { "kernelspec": { "name": "csai", "display_name": "Python (lớp CSAI)", "language": "python" }, "language_info": { "name": "python" } } }