CSAI2026 labs: Lab-2..Lab-6 (Practical + Solution, data), Module-2..Module-6 notebooks and datasets

This commit is contained in:
mikrotik
2026-10-12 19:16:14 +07:00
commit 2fefa615cc
184 changed files with 1016761 additions and 0 deletions
@@ -0,0 +1,233 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a9ae0aa5",
"metadata": {},
"source": [
"# Module 2 — Lab 1: Python Fundamentals for Security Analysis\n",
"\n",
"**Topics:** variables, data types, strings, type casting, operators\n",
"\n",
"\n",
"> **Solution version:** This notebook contains the complete solution. Run the cells in order and compare the results with your Lab notebook.\n"
]
},
{
"cell_type": "markdown",
"id": "6b669205",
"metadata": {},
"source": [
"## Learning objectives\n",
"\n",
"After completing this lab, you will be able to:\n",
"\n",
"- store security information in variables and select suitable data types;\n",
"- normalize log strings using string methods;\n",
"- cast values read from logs to the required data types;\n",
"- combine comparison and logical operators to flag suspicious activity.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5338112e",
"metadata": {},
"outputs": [],
"source": [
"print(\"Hello, Cyber World!\")\n",
"\n",
"ip_address = \"198.51.100.14\"\n",
"port_number = 443\n",
"failed_logins = 7\n",
"response_time = 0.125\n",
"is_admin = False\n",
"\n",
"print(f\"IP: {ip_address} | Port: {port_number}\")\n",
"print(type(ip_address), type(port_number), type(response_time), type(is_admin))\n"
]
},
{
"cell_type": "markdown",
"id": "0094841a",
"metadata": {},
"source": [
"## 1. Strings in security logs\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b5ed12a4",
"metadata": {},
"outputs": [],
"source": [
"log_message = \" FAILED login for Admin from 198.51.100.14 \"\n",
"url = \"https://example.com/login\"\n",
"\n",
"print(log_message.strip())\n",
"print(log_message.lower())\n",
"print(log_message.replace(\"FAILED\", \"DENIED\"))\n",
"print(url[0:5])\n"
]
},
{
"cell_type": "markdown",
"id": "c484946c",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 2. Normalize a username and extract the protocol\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c62e36d5",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"raw_username = \" AdMin \"\n",
"normalized_username = raw_username.strip().lower()\n",
"\n",
"url = \"https://example.com/login\"\n",
"protocol = url[0:5]\n",
"\n",
"print(normalized_username)\n",
"print(protocol)\n"
]
},
{
"cell_type": "markdown",
"id": "9f02318b",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 3. Type casting and operators\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "61827b28",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"port_text = \"8080\"\n",
"port_number = int(port_text)\n",
"next_port = port_number + 1\n",
"\n",
"login_attempts = 7\n",
"user_ip = \"198.51.100.14\"\n",
"is_suspicious = login_attempts > 5 and user_ip != \"127.0.0.1\"\n",
"\n",
"print(f\"Next port: {next_port}\")\n",
"print(f\"Suspicious: {is_suspicious}\")\n"
]
},
{
"cell_type": "markdown",
"id": "0613c68c",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 4. Calculate a basic risk score\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fa8d9c4d",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"failed_logins = 7\n",
"is_admin = False\n",
"is_off_hours = True\n",
"\n",
"risk_score = failed_logins * 10 + int(is_admin) * 25 + int(is_off_hours) * 15\n",
"requires_review = risk_score >= 60\n",
"\n",
"print(f\"Risk score: {risk_score}\")\n",
"print(f\"Requires review: {requires_review}\")\n"
]
},
{
"cell_type": "markdown",
"id": "285b59c6",
"metadata": {},
"source": [
"## Self-check\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3a59fbe2",
"metadata": {},
"outputs": [],
"source": [
"assert normalized_username == \"admin\"\n",
"assert protocol == \"https\"\n",
"assert next_port == 8081\n",
"assert is_suspicious is True\n",
"assert risk_score == 85\n",
"assert requires_review is True\n",
"print(\"✓ Lab 1 completed\")\n"
]
},
{
"cell_type": "markdown",
"id": "92257020",
"metadata": {},
"source": [
"## Reflection questions\n",
"\n",
"1. Why does `port_text + 1` fail while `int(port_text) + 1` works?\n",
"2. What changes if you replace `and` with `or` in the `is_suspicious` condition?\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,242 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "db19a07f",
"metadata": {},
"source": [
"# Module 2 — Lab 2: Control Flow and Functions\n",
" \n",
"**Topics:** if/elif/else, for, while, break, continue, function\n",
"\n",
"\n",
"> **Solution version:** This notebook contains the complete solution. Run the cells in order and compare the results with your Lab notebook.\n"
]
},
{
"cell_type": "markdown",
"id": "0d178b19",
"metadata": {},
"source": [
"## Scenario\n",
"\n",
"You need to build small rules that classify login alerts and automatically review a list of IP addresses. All activity is simulated with local data; this notebook does not connect to or scan any real system.\n"
]
},
{
"cell_type": "markdown",
"id": "56bc45d4",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 1. Classify failed login attempts\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dac7be01",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"failed_attempts = 7\n",
"\n",
"if failed_attempts >= 10:\n",
" severity = \"CRITICAL\"\n",
"elif failed_attempts >= 5:\n",
" severity = \"HIGH\"\n",
"elif failed_attempts >= 1:\n",
" severity = \"MEDIUM\"\n",
"else:\n",
" severity = \"LOW\"\n",
"\n",
"print(severity)\n"
]
},
{
"cell_type": "markdown",
"id": "63b6d1a8",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 2. Review IP addresses with `for`, `continue`, and `break`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3820130f",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"blocked_ips = {\"10.0.0.5\", \"10.0.0.12\"}\n",
"targets = [f\"10.0.0.{i}\" for i in range(1, 16)]\n",
"reviewed = []\n",
"\n",
"for ip in targets:\n",
" last_octet = int(ip.split(\".\")[-1])\n",
"\n",
" if last_octet % 2 != 0:\n",
" continue\n",
"\n",
" reviewed.append(ip)\n",
" print(f\"Reviewing: {ip}\")\n",
"\n",
" if ip in blocked_ips:\n",
" print(f\"Alert: blocked IP found: {ip}\")\n",
" break\n",
"\n",
"print(\"Reviewed:\", reviewed)\n"
]
},
{
"cell_type": "markdown",
"id": "61f09e15",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 3. Limit retry attempts with `while`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0ec1dc95",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"attempt = 0\n",
"max_attempts = 3\n",
"service_responded = False\n",
"\n",
"while attempt < max_attempts and not service_responded:\n",
" attempt += 1\n",
" print(f\"Health check attempt {attempt}\")\n",
" service_responded = attempt == 2\n",
"\n",
"print(f\"Service responded: {service_responded}\")\n"
]
},
{
"cell_type": "markdown",
"id": "b849e734",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 4. Package a rule in a function\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a34324a1",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"def classify_login_risk(failed_attempts, is_admin=False, is_off_hours=False):\n",
" # Return LOW, MEDIUM, HIGH, or CRITICAL for a login event.\n",
" score = failed_attempts * 10\n",
" if is_admin:\n",
" score += 25\n",
" if is_off_hours:\n",
" score += 15\n",
"\n",
" if score >= 100:\n",
" return \"CRITICAL\"\n",
" if score >= 60:\n",
" return \"HIGH\"\n",
" if score >= 30:\n",
" return \"MEDIUM\"\n",
" return \"LOW\"\n",
"\n",
"\n",
"print(classify_login_risk(2))\n",
"print(classify_login_risk(7, is_off_hours=True))\n",
"print(classify_login_risk(8, is_admin=True))\n"
]
},
{
"cell_type": "markdown",
"id": "aba3b1a8",
"metadata": {},
"source": [
"## Self-check\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "de52830d",
"metadata": {},
"outputs": [],
"source": [
"assert severity == \"HIGH\"\n",
"assert reviewed == [\"10.0.0.2\", \"10.0.0.4\", \"10.0.0.6\", \"10.0.0.8\", \"10.0.0.10\", \"10.0.0.12\"]\n",
"assert attempt == 2 and service_responded is True\n",
"assert classify_login_risk(2) == \"LOW\"\n",
"assert classify_login_risk(7, is_off_hours=True) == \"HIGH\"\n",
"assert classify_login_risk(8, is_admin=True) == \"CRITICAL\"\n",
"print(\"✓ Lab 2 completed\")\n"
]
},
{
"cell_type": "markdown",
"id": "c97e0f71",
"metadata": {},
"source": [
"## Extension challenge\n",
"\n",
"Modify the function to add 20 points when `source_ip` belongs to a set of blocked IP addresses. Write at least two `assert` statements for the new rule.\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,242 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "06f7188c",
"metadata": {},
"source": [
"# Module 2 — Lab 3: Data Structures and Raw Log Analysis\n",
" \n",
"**Topics:** list, tuple, dictionary, set, parsing\n",
"\n",
"\n",
"> **Solution version:** This notebook contains the complete solution. Run the cells in order and compare the results with your Lab notebook.\n"
]
},
{
"cell_type": "markdown",
"id": "67ad00e0",
"metadata": {},
"source": [
"## Input data\n",
"\n",
"Each log line contains four comma-separated fields: `timestamp,source_ip,username,status`.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4b49fc55",
"metadata": {},
"outputs": [],
"source": [
"raw_logs = [\n",
" \"2026-09-01T08:00:00Z,203.0.113.55,alice,SUCCESS\",\n",
" \"2026-09-01T08:01:00Z,198.51.100.14,admin,FAILED\",\n",
" \"2026-09-01T08:01:20Z,198.51.100.14,root,FAILED\",\n",
" \"2026-09-01T08:02:00Z,192.0.2.88,bob,SUCCESS\",\n",
" \"2026-09-01T08:02:30Z,198.51.100.14,guest,FAILED\",\n",
" \"2026-09-01T08:03:00Z,192.0.2.99,carol,FAILED\",\n",
"]\n",
"\n",
"critical_fields = (\"timestamp\", \"source_ip\", \"username\", \"status\")\n",
"blocked_ips = {\"198.51.100.14\", \"203.0.113.200\"}\n",
"\n",
"print(f\"Rows: {len(raw_logs)}\")\n",
"print(\"Critical fields:\", critical_fields)\n"
]
},
{
"cell_type": "markdown",
"id": "3f8fcb17",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 1. Convert one log line into a dictionary\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "47d1eb8b",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"def parse_log_line(line):\n",
" timestamp, source_ip, username, status = line.split(\",\")\n",
" return {\n",
" \"timestamp\": timestamp,\n",
" \"source_ip\": source_ip,\n",
" \"username\": username.lower(),\n",
" \"status\": status.upper(),\n",
" }\n",
"\n",
"\n",
"sample_event = parse_log_line(raw_logs[0])\n",
"print(sample_event)\n"
]
},
{
"cell_type": "markdown",
"id": "56f0258d",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 2. Create an event list and a set of unique IP addresses\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a8c8af5e",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"events = [parse_log_line(line) for line in raw_logs]\n",
"unique_ips = {event[\"source_ip\"] for event in events}\n",
"\n",
"print(\"Unique IPs:\", sorted(unique_ips))\n",
"print(\"Number of unique IPs:\", len(unique_ips))\n"
]
},
{
"cell_type": "markdown",
"id": "7651a31c",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 3. Count failed logins with a dictionary\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0c0a3675",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"failed_by_ip = {}\n",
"\n",
"for event in events:\n",
" if event[\"status\"] != \"FAILED\":\n",
" continue\n",
" ip = event[\"source_ip\"]\n",
" failed_by_ip[ip] = failed_by_ip.get(ip, 0) + 1\n",
"\n",
"print(failed_by_ip)\n"
]
},
{
"cell_type": "markdown",
"id": "21436c12",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 4. Combine dictionaries and sets to create alerts\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11954df9",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"alerts = []\n",
"\n",
"for ip, count in failed_by_ip.items():\n",
" if count >= 3 or ip in blocked_ips:\n",
" alerts.append({\n",
" \"source_ip\": ip,\n",
" \"failed_attempts\": count,\n",
" \"blocked\": ip in blocked_ips,\n",
" })\n",
"\n",
"print(alerts)\n"
]
},
{
"cell_type": "markdown",
"id": "b18d5731",
"metadata": {},
"source": [
"## Self-check\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d30adfba",
"metadata": {},
"outputs": [],
"source": [
"assert sample_event[\"username\"] == \"alice\"\n",
"assert len(events) == 6\n",
"assert unique_ips == {\"203.0.113.55\", \"198.51.100.14\", \"192.0.2.88\", \"192.0.2.99\"}\n",
"assert failed_by_ip[\"198.51.100.14\"] == 3\n",
"assert alerts == [{\"source_ip\": \"198.51.100.14\", \"failed_attempts\": 3, \"blocked\": True}]\n",
"print(\"✓ Lab 3 completed\")\n"
]
},
{
"cell_type": "markdown",
"id": "55d97609",
"metadata": {},
"source": [
"## Reflection questions\n",
"\n",
"- Why is a `set` suitable for storing unique IP addresses or a blocklist?\n",
"- Why is a `tuple` suitable for field names that should not change?\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,200 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "03d77301",
"metadata": {},
"source": [
"# Module 2 — Lab 4: NumPy for Security Metrics\n",
"\n",
"\n",
"**Topics:** array, vectorization, statistics, boolean mask\n",
"\n",
"\n",
"> **Solution version:** This notebook contains the complete solution. Run the cells in order and compare the results with your Lab notebook.\n"
]
},
{
"cell_type": "markdown",
"id": "33cb0ba1",
"metadata": {},
"source": [
"## Scenario\n",
"\n",
"A network sensor reports packet sizes, latency, and failed authentication attempts for each time window. We use NumPy to calculate efficiently across the entire array.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d7d3de72",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"packet_sizes = np.array([64, 128, 1500, 512, 1514, 256, 1400, 80])\n",
"latency_ms = np.array([12.5, 18.0, 250.0, 33.5, 180.0, 25.0, 95.0, 10.0])\n",
"failed_attempts = np.array([0, 1, 8, 2, 10, 1, 6, 0])\n",
"\n",
"print(packet_sizes.shape, packet_sizes.dtype)\n"
]
},
{
"cell_type": "markdown",
"id": "5d225bd1",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 1. Unit conversion and descriptive statistics\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3da57f45",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"packet_kb = packet_sizes / 1024\n",
"mean_latency = latency_ms.mean()\n",
"p95_latency = np.percentile(latency_ms, 95)\n",
"\n",
"print(\"Packet sizes (KB):\", np.round(packet_kb, 3))\n",
"print(f\"Mean latency: {mean_latency:.2f} ms\")\n",
"print(f\"P95 latency: {p95_latency:.2f} ms\")\n"
]
},
{
"cell_type": "markdown",
"id": "dddbd812",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 2. Filter with a Boolean mask\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d24785c0",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"suspicious_mask = (packet_sizes > 1400) | (latency_ms > 150) | (failed_attempts >= 8)\n",
"suspicious_indices = np.where(suspicious_mask)[0]\n",
"\n",
"print(\"Mask:\", suspicious_mask)\n",
"print(\"Suspicious indices:\", suspicious_indices)\n",
"print(\"Suspicious packet sizes:\", packet_sizes[suspicious_mask])\n"
]
},
{
"cell_type": "markdown",
"id": "ce903e91",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 3. Normalize values and create a risk score\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dcece1eb",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"def min_max(values):\n",
" values = np.asarray(values, dtype=float)\n",
" value_range = values.max() - values.min()\n",
" if value_range == 0:\n",
" return np.zeros_like(values)\n",
" return (values - values.min()) / value_range\n",
"\n",
"\n",
"latency_score = min_max(latency_ms)\n",
"failure_score = min_max(failed_attempts)\n",
"risk_score = 0.4 * latency_score + 0.6 * failure_score\n",
"\n",
"print(\"Risk scores:\", np.round(risk_score, 3))\n",
"print(\"Highest-risk index:\", int(np.argmax(risk_score)))\n"
]
},
{
"cell_type": "markdown",
"id": "65a088f0",
"metadata": {},
"source": [
"## Self-check\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a730cc89",
"metadata": {},
"outputs": [],
"source": [
"assert packet_sizes.shape == (8,)\n",
"assert np.isclose(mean_latency, 78.0)\n",
"assert suspicious_indices.tolist() == [2, 4]\n",
"assert int(np.argmax(risk_score)) == 4\n",
"assert np.all((risk_score >= 0) & (risk_score <= 1))\n",
"print(\"✓ Lab 4 completed\")\n"
]
},
{
"cell_type": "markdown",
"id": "2c85c6e5",
"metadata": {},
"source": [
"## Extension challenge\n",
"\n",
"Change the weights assigned to latency and failed attempts. Observe whether the highest-risk element changes and explain why.\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,256 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "97d2f264",
"metadata": {},
"source": [
"# Module 2 — Lab 5: Pandas — Loading, Inspecting, and Cleaning Logs\n",
"\n",
"\n",
"**Topics:** CSV, DataFrame, missing values, duplicates, datetime, filtering\n",
"\n",
"\n",
"> **Solution version:** This notebook contains the complete solution. Run the cells in order and compare the results with your Lab notebook.\n"
]
},
{
"cell_type": "markdown",
"id": "7b1ce82e",
"metadata": {},
"source": [
"## Scenario\n",
"\n",
"The `data/auth_log.csv` file contains duplicate rows, invalid timestamps, missing critical fields, and inconsistent capitalization. The goal is to produce a clean DataFrame ready for analysis.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9dea9739",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import pandas as pd\n",
"\n",
"DATA_PATH = Path(\"data/auth_log.csv\")\n",
"OUTPUT_DIR = Path(\"output\")\n",
"OUTPUT_DIR.mkdir(exist_ok=True)\n",
"\n",
"log_df = pd.read_csv(DATA_PATH)\n",
"log_df.head()\n"
]
},
{
"cell_type": "markdown",
"id": "6e6da8ed",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 1. Inspect data quality\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c9ef821c",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"print(\"Shape:\", log_df.shape)\n",
"print(\"Duplicate rows:\", log_df.duplicated().sum())\n",
"print(\"Missing values by column:\")\n",
"print(log_df.isna().sum())\n",
"print(\"\\nData types:\")\n",
"print(log_df.dtypes)\n"
]
},
{
"cell_type": "markdown",
"id": "6b759b1c",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 2. Remove duplicates and normalize timestamps and critical fields\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d4342ee",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"clean_df = log_df.copy()\n",
"clean_df = clean_df.drop_duplicates()\n",
"\n",
"clean_df[\"timestamp\"] = pd.to_datetime(\n",
" clean_df[\"timestamp\"], errors=\"coerce\", utc=True\n",
")\n",
"\n",
"clean_df = clean_df.dropna(\n",
" subset=[\"timestamp\", \"source_ip\", \"status\"]\n",
")\n"
]
},
{
"cell_type": "markdown",
"id": "c5c59040",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 3. Normalize text and create time-based fields\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "012ba902",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"clean_df[\"source_ip\"] = clean_df[\"source_ip\"].str.strip()\n",
"clean_df[\"username\"] = clean_df[\"username\"].fillna(\"unknown\").str.strip().str.lower()\n",
"clean_df[\"status\"] = clean_df[\"status\"].str.strip().str.upper()\n",
"\n",
"clean_df = clean_df[clean_df[\"status\"].isin([\"SUCCESS\", \"FAILED\"])]\n",
"clean_df[\"hour\"] = clean_df[\"timestamp\"].dt.hour\n",
"clean_df = clean_df.sort_values(\"timestamp\").reset_index(drop=True)\n",
"\n",
"clean_df.head()\n"
]
},
{
"cell_type": "markdown",
"id": "354fc8c0",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 4. Filter and aggregate\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "38f69103",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"failed_df = clean_df[clean_df[\"status\"] == \"FAILED\"].copy()\n",
"failed_by_ip = failed_df[\"source_ip\"].value_counts().rename(\"failed_attempts\")\n",
"status_counts = clean_df[\"status\"].value_counts()\n",
"\n",
"print(\"Status counts:\")\n",
"print(status_counts)\n",
"print(\"\\nFailed attempts by IP:\")\n",
"print(failed_by_ip)\n"
]
},
{
"cell_type": "markdown",
"id": "748f22a9",
"metadata": {},
"source": [
"## 5. Export clean data\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fdcb2bf7",
"metadata": {},
"outputs": [],
"source": [
"clean_df.to_csv(OUTPUT_DIR / \"clean_auth_log.csv\", index=False)\n",
"clean_df.to_json(\n",
" OUTPUT_DIR / \"clean_auth_log.json\",\n",
" orient=\"records\",\n",
" indent=2,\n",
" date_format=\"iso\",\n",
")\n",
"failed_by_ip.to_csv(\n",
" OUTPUT_DIR / \"failed_login_by_ip.csv\",\n",
" header=True,\n",
")\n",
"\n",
"print(\"Exported files to:\", OUTPUT_DIR.resolve())\n"
]
},
{
"cell_type": "markdown",
"id": "4b07b67e",
"metadata": {},
"source": [
"## Self-check\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b16f153c",
"metadata": {},
"outputs": [],
"source": [
"assert log_df.duplicated().sum() >= 1\n",
"assert clean_df.duplicated().sum() == 0\n",
"assert clean_df[\"timestamp\"].isna().sum() == 0\n",
"assert set(clean_df[\"status\"]) <= {\"SUCCESS\", \"FAILED\"}\n",
"assert clean_df[\"username\"].str.strip().eq(clean_df[\"username\"]).all()\n",
"assert failed_by_ip.index[0] == \"198.51.100.14\"\n",
"assert (OUTPUT_DIR / \"clean_auth_log.csv\").exists()\n",
"assert (OUTPUT_DIR / \"clean_auth_log.json\").exists()\n",
"print(\"✓ Lab 5 completed\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,325 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "1727969b",
"metadata": {},
"source": [
"# Module 2 — Lab 6: Capstone — Investigating Suspicious Logins\n",
"\n",
" \n",
"**Topics:** Pandas, NumPy, Matplotlib, export, analyst summary\n",
"\n",
"\n",
"> **Solution version:** This notebook contains the complete solution. Run the cells in order and compare the results with your Lab notebook.\n"
]
},
{
"cell_type": "markdown",
"id": "467cdb20",
"metadata": {},
"source": [
"## Mission\n",
"\n",
"As a junior security analyst, you must build a complete workflow:\n",
"\n",
"1. load and clean authentication logs;\n",
"2. identify IP addresses with repeated failed logins;\n",
"3. identify off-hours activity;\n",
"4. visualize the results;\n",
"5. export the data and write a short conclusion.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "525cb519",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"DATA_PATH = Path(\"data/auth_log.csv\")\n",
"OUTPUT_DIR = Path(\"output\")\n",
"OUTPUT_DIR.mkdir(exist_ok=True)\n"
]
},
{
"cell_type": "markdown",
"id": "d94da21b",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 1. Write a reusable cleaning function\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c33bbbcb",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"def clean_auth_log(df):\n",
" result = df.copy().drop_duplicates()\n",
" result[\"timestamp\"] = pd.to_datetime(\n",
" result[\"timestamp\"], errors=\"coerce\", utc=True\n",
" )\n",
" result = result.dropna(subset=[\"timestamp\", \"source_ip\", \"status\"])\n",
" result[\"source_ip\"] = result[\"source_ip\"].str.strip()\n",
" result[\"username\"] = result[\"username\"].fillna(\"unknown\").str.strip().str.lower()\n",
" result[\"status\"] = result[\"status\"].str.strip().str.upper()\n",
" result = result[result[\"status\"].isin([\"SUCCESS\", \"FAILED\"])]\n",
" result[\"hour\"] = result[\"timestamp\"].dt.hour\n",
" result[\"is_off_hours\"] = (result[\"hour\"] < 6) | (result[\"hour\"] >= 22)\n",
" return result.sort_values(\"timestamp\").reset_index(drop=True)\n",
"\n",
"\n",
"raw_df = pd.read_csv(DATA_PATH)\n",
"clean_df = clean_auth_log(raw_df)\n",
"print(f\"Raw rows: {len(raw_df)} | Clean rows: {len(clean_df)}\")\n",
"clean_df.head()\n"
]
},
{
"cell_type": "markdown",
"id": "5205cafa",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 2. Analyze failures and rank IP addresses\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "085dd483",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"failed_df = clean_df[clean_df[\"status\"] == \"FAILED\"].copy()\n",
"failed_by_ip = (\n",
" failed_df.groupby(\"source_ip\")\n",
" .size()\n",
" .sort_values(ascending=False)\n",
" .rename(\"failed_attempts\")\n",
")\n",
"\n",
"THRESHOLD = 3\n",
"suspicious_ips = failed_by_ip[failed_by_ip >= THRESHOLD]\n",
"\n",
"print(\"Failed attempts by IP:\")\n",
"print(failed_by_ip)\n",
"print(\"\\nSuspicious IPs:\")\n",
"print(suspicious_ips)\n"
]
},
{
"cell_type": "markdown",
"id": "5967176b",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 3. Calculate additional investigation metrics\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7cab677c",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"status_counts = clean_df[\"status\"].value_counts().reindex([\"SUCCESS\", \"FAILED\"], fill_value=0)\n",
"off_hours_failed = failed_df[failed_df[\"is_off_hours\"]]\n",
"top_ip = failed_by_ip.index[0]\n",
"top_ip_count = int(failed_by_ip.iloc[0])\n",
"failure_rate = np.round((status_counts[\"FAILED\"] / status_counts.sum()) * 100, 2)\n",
"\n",
"print(f\"Top suspicious IP: {top_ip} ({top_ip_count} failures)\")\n",
"print(f\"Failure rate: {failure_rate}%\")\n",
"print(f\"Off-hours failed attempts: {len(off_hours_failed)}\")\n"
]
},
{
"cell_type": "markdown",
"id": "c472f43a",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 4. Visualize the findings\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4816f960",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
"\n",
"status_counts.plot(\n",
" kind=\"bar\",\n",
" ax=axes[0],\n",
" color=[\"#2a9d8f\", \"#e76f51\"],\n",
" title=\"Authentication status\",\n",
")\n",
"axes[0].set_xlabel(\"Status\")\n",
"axes[0].set_ylabel(\"Number of attempts\")\n",
"axes[0].tick_params(axis=\"x\", rotation=0)\n",
"\n",
"failed_by_ip.head(5).sort_values().plot(\n",
" kind=\"barh\",\n",
" ax=axes[1],\n",
" color=\"#e76f51\",\n",
" title=\"Top IPs by failed logins\",\n",
")\n",
"axes[1].set_xlabel(\"Failed attempts\")\n",
"axes[1].set_ylabel(\"Source IP\")\n",
"\n",
"fig.suptitle(\"Authentication Log Investigation\")\n",
"fig.tight_layout()\n",
"fig.savefig(OUTPUT_DIR / \"auth_log_analysis.png\", dpi=150, bbox_inches=\"tight\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"id": "933a5898",
"metadata": {
"tags": [
"exercise"
]
},
"source": [
"## 5. Export results and write a short report\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fc68ed13",
"metadata": {
"tags": [
"solution"
]
},
"outputs": [],
"source": [
"clean_df.to_csv(OUTPUT_DIR / \"capstone_clean_auth_log.csv\", index=False)\n",
"suspicious_ips.to_csv(\n",
" OUTPUT_DIR / \"capstone_suspicious_ips.csv\",\n",
" header=True,\n",
")\n",
"\n",
"summary = f'''SECURITY ANALYSIS SUMMARY\n",
"=========================\n",
"Clean events: {len(clean_df)}\n",
"Failed events: {int(status_counts['FAILED'])}\n",
"Failure rate: {failure_rate}%\n",
"Top suspicious IP: {top_ip} ({top_ip_count} failed attempts)\n",
"Off-hours failed attempts: {len(off_hours_failed)}\n",
"Decision threshold: {THRESHOLD} failed attempts\n",
"\n",
"Recommended action:\n",
"- Review events from {top_ip}.\n",
"- Correlate off-hours failures with endpoint and identity logs.\n",
"- Validate whether the account owners recognize these attempts.\n",
"'''\n",
"\n",
"(OUTPUT_DIR / \"capstone_summary.txt\").write_text(summary, encoding=\"utf-8\")\n",
"print(summary)\n"
]
},
{
"cell_type": "markdown",
"id": "023ea53a",
"metadata": {},
"source": [
"## Self-check\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "560fa29e",
"metadata": {},
"outputs": [],
"source": [
"assert len(raw_df) > len(clean_df)\n",
"assert clean_df.duplicated().sum() == 0\n",
"assert failed_by_ip.index[0] == \"198.51.100.14\"\n",
"assert top_ip_count >= THRESHOLD\n",
"assert len(off_hours_failed) >= 1\n",
"assert (OUTPUT_DIR / \"auth_log_analysis.png\").exists()\n",
"assert (OUTPUT_DIR / \"capstone_summary.txt\").exists()\n",
"print(\"✓ Capstone lab completed\")\n"
]
},
{
"cell_type": "markdown",
"id": "dedf0889",
"metadata": {},
"source": [
"## Report questions\n",
"\n",
"1. Which IP address should be investigated first, and what evidence supports that decision?\n",
"2. In what situations could a threshold of three failed attempts produce a false positive?\n",
"3. Which additional log sources should be reviewed before deciding to block an IP address?\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+23
View File
@@ -0,0 +1,23 @@
timestamp,source_ip,username,status
2026-09-01T10:00:12Z,203.0.113.55,admin,SUCCESS
2026-09-01T10:01:23Z,198.51.100.14,guest,FAILED
2026-09-01T10:01:28Z,198.51.100.14,root,FAILED
2026-09-01T10:01:33Z,198.51.100.14,admin,FAILED
2026-09-01T10:02:15Z,203.0.113.55,jdoe,SUCCESS
2026-09-01T10:03:01Z,192.0.2.88,admin,SUCCESS
2026-09-01T10:03:45Z,198.51.100.14,jdoe,FAILED
2026-09-01T10:03:51Z,198.51.100.14,admin,FAILED
2026-09-01T10:03:51Z,198.51.100.14,admin,FAILED
2026-09-01T10:04:10Z,198.51.100.14,Admin, failed
bad-timestamp,198.51.100.14,root,FAILED
,203.0.113.55,backup,SUCCESS
2026-09-01T22:15:00Z,192.0.2.99,nightops,FAILED
2026-09-01T23:40:00Z,192.0.2.99,admin,FAILED
2026-09-02T01:05:00Z,198.51.100.14,ROOT,FAILED
2026-09-02T01:10:00Z,203.0.113.77,,FAILED
2026-09-02T06:10:00Z,203.0.113.55,alice,SUCCESS
2026-09-02T07:20:00Z,192.0.2.88,bob,SUCCESS
2026-09-02T07:25:00Z,192.0.2.88,bob,success
2026-09-02T09:00:00Z,,unknown,FAILED
2026-09-02T09:15:00Z,203.0.113.77,carol,LOCKED
2026-09-02T10:30:00Z,198.51.100.14,admin,FAILED
1 timestamp source_ip username status
2 2026-09-01T10:00:12Z 203.0.113.55 admin SUCCESS
3 2026-09-01T10:01:23Z 198.51.100.14 guest FAILED
4 2026-09-01T10:01:28Z 198.51.100.14 root FAILED
5 2026-09-01T10:01:33Z 198.51.100.14 admin FAILED
6 2026-09-01T10:02:15Z 203.0.113.55 jdoe SUCCESS
7 2026-09-01T10:03:01Z 192.0.2.88 admin SUCCESS
8 2026-09-01T10:03:45Z 198.51.100.14 jdoe FAILED
9 2026-09-01T10:03:51Z 198.51.100.14 admin FAILED
10 2026-09-01T10:03:51Z 198.51.100.14 admin FAILED
11 2026-09-01T10:04:10Z 198.51.100.14 Admin failed
12 bad-timestamp 198.51.100.14 root FAILED
13 203.0.113.55 backup SUCCESS
14 2026-09-01T22:15:00Z 192.0.2.99 nightops FAILED
15 2026-09-01T23:40:00Z 192.0.2.99 admin FAILED
16 2026-09-02T01:05:00Z 198.51.100.14 ROOT FAILED
17 2026-09-02T01:10:00Z 203.0.113.77 FAILED
18 2026-09-02T06:10:00Z 203.0.113.55 alice SUCCESS
19 2026-09-02T07:20:00Z 192.0.2.88 bob SUCCESS
20 2026-09-02T07:25:00Z 192.0.2.88 bob success
21 2026-09-02T09:00:00Z unknown FAILED
22 2026-09-02T09:15:00Z 203.0.113.77 carol LOCKED
23 2026-09-02T10:30:00Z 198.51.100.14 admin FAILED
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

@@ -0,0 +1,18 @@
timestamp,source_ip,username,status,hour,is_off_hours
2026-09-01 10:00:12+00:00,203.0.113.55,admin,SUCCESS,10,False
2026-09-01 10:01:23+00:00,198.51.100.14,guest,FAILED,10,False
2026-09-01 10:01:28+00:00,198.51.100.14,root,FAILED,10,False
2026-09-01 10:01:33+00:00,198.51.100.14,admin,FAILED,10,False
2026-09-01 10:02:15+00:00,203.0.113.55,jdoe,SUCCESS,10,False
2026-09-01 10:03:01+00:00,192.0.2.88,admin,SUCCESS,10,False
2026-09-01 10:03:45+00:00,198.51.100.14,jdoe,FAILED,10,False
2026-09-01 10:03:51+00:00,198.51.100.14,admin,FAILED,10,False
2026-09-01 10:04:10+00:00,198.51.100.14,admin,FAILED,10,False
2026-09-01 22:15:00+00:00,192.0.2.99,nightops,FAILED,22,True
2026-09-01 23:40:00+00:00,192.0.2.99,admin,FAILED,23,True
2026-09-02 01:05:00+00:00,198.51.100.14,root,FAILED,1,True
2026-09-02 01:10:00+00:00,203.0.113.77,unknown,FAILED,1,True
2026-09-02 06:10:00+00:00,203.0.113.55,alice,SUCCESS,6,False
2026-09-02 07:20:00+00:00,192.0.2.88,bob,SUCCESS,7,False
2026-09-02 07:25:00+00:00,192.0.2.88,bob,SUCCESS,7,False
2026-09-02 10:30:00+00:00,198.51.100.14,admin,FAILED,10,False
1 timestamp source_ip username status hour is_off_hours
2 2026-09-01 10:00:12+00:00 203.0.113.55 admin SUCCESS 10 False
3 2026-09-01 10:01:23+00:00 198.51.100.14 guest FAILED 10 False
4 2026-09-01 10:01:28+00:00 198.51.100.14 root FAILED 10 False
5 2026-09-01 10:01:33+00:00 198.51.100.14 admin FAILED 10 False
6 2026-09-01 10:02:15+00:00 203.0.113.55 jdoe SUCCESS 10 False
7 2026-09-01 10:03:01+00:00 192.0.2.88 admin SUCCESS 10 False
8 2026-09-01 10:03:45+00:00 198.51.100.14 jdoe FAILED 10 False
9 2026-09-01 10:03:51+00:00 198.51.100.14 admin FAILED 10 False
10 2026-09-01 10:04:10+00:00 198.51.100.14 admin FAILED 10 False
11 2026-09-01 22:15:00+00:00 192.0.2.99 nightops FAILED 22 True
12 2026-09-01 23:40:00+00:00 192.0.2.99 admin FAILED 23 True
13 2026-09-02 01:05:00+00:00 198.51.100.14 root FAILED 1 True
14 2026-09-02 01:10:00+00:00 203.0.113.77 unknown FAILED 1 True
15 2026-09-02 06:10:00+00:00 203.0.113.55 alice SUCCESS 6 False
16 2026-09-02 07:20:00+00:00 192.0.2.88 bob SUCCESS 7 False
17 2026-09-02 07:25:00+00:00 192.0.2.88 bob SUCCESS 7 False
18 2026-09-02 10:30:00+00:00 198.51.100.14 admin FAILED 10 False
@@ -0,0 +1,13 @@
SECURITY ANALYSIS SUMMARY
=========================
Clean events: 17
Failed events: 11
Failure rate: 64.71%
Top suspicious IP: 198.51.100.14 (8 failed attempts)
Off-hours failed attempts: 4
Decision threshold: 3 failed attempts
Recommended action:
- Review events from 198.51.100.14.
- Correlate off-hours failures with endpoint and identity logs.
- Validate whether the account owners recognize these attempts.
@@ -0,0 +1,2 @@
source_ip,failed_attempts
198.51.100.14,8
1 source_ip failed_attempts
2 198.51.100.14 8