1734 lines
62 KiB
Plaintext
1734 lines
62 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"id": "ea5511c9-c672-4545-ba1f-84f9f3e2d775",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import json\n",
|
||
"import pickle\n",
|
||
"import random\n",
|
||
"import re\n",
|
||
"import urllib.request\n",
|
||
"import zipfile\n",
|
||
"from pathlib import Path\n",
|
||
"import numpy as np\n",
|
||
"\n",
|
||
"# Reproducibility\n",
|
||
"random.seed(1337)\n",
|
||
"np.random.seed(1337)\n",
|
||
"\n",
|
||
"import pandas as pd\n",
|
||
"import matplotlib.pyplot as plt\n",
|
||
"from sklearn.feature_extraction.text import CountVectorizer\n",
|
||
"from sklearn.model_selection import train_test_split\n",
|
||
"from sklearn.naive_bayes import MultinomialNB\n",
|
||
"from sklearn.metrics import classification_report, accuracy_score\n",
|
||
"\n",
|
||
"from htb_ai_library import (\n",
|
||
" AZURE,\n",
|
||
" HACKER_GREY,\n",
|
||
" HTB_GREEN,\n",
|
||
" MALWARE_RED,\n",
|
||
" NODE_BLACK,\n",
|
||
" NUGGET_YELLOW,\n",
|
||
" WHITE,\n",
|
||
" AQUAMARINE,\n",
|
||
" load_model,\n",
|
||
" save_model,\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"id": "e66ce529-60c0-42ac-8536-02f0ffb88cbc",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Loading SMS Spam Dataset...\n",
|
||
"[+] Using cached dataset: data/sms_spam.csv\n",
|
||
"[+] Loaded 5574 messages\n",
|
||
" Spam: 747\n",
|
||
" Ham: 4827\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Loading SMS Spam Dataset...\")\n",
|
||
"\n",
|
||
"data_dir = Path(\"data\")\n",
|
||
"data_dir.mkdir(exist_ok=True)\n",
|
||
"dataset_path = data_dir / \"sms_spam.csv\"\n",
|
||
"\n",
|
||
"if dataset_path.exists():\n",
|
||
" print(f\"[+] Using cached dataset: {dataset_path}\")\n",
|
||
" df = pd.read_csv(dataset_path)\n",
|
||
"else:\n",
|
||
" print(\"[*] Downloading from UCI repository...\")\n",
|
||
" url = \"https://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip\"\n",
|
||
" zip_path = data_dir / \"sms_spam.zip\"\n",
|
||
"\n",
|
||
" urllib.request.urlretrieve(url, zip_path)\n",
|
||
"\n",
|
||
" with zipfile.ZipFile(zip_path, \"r\") as zf:\n",
|
||
" with zf.open(\"SMSSpamCollection\") as f:\n",
|
||
" lines = [line.decode(\"utf-8\").strip() for line in f]\n",
|
||
"\n",
|
||
" # Parse tab-separated format\n",
|
||
" data = []\n",
|
||
" for line in lines:\n",
|
||
" parts = line.split('\\t')\n",
|
||
" if len(parts) == 2:\n",
|
||
" data.append({\"label\": parts[0].lower(), \"message\": parts[1]})\n",
|
||
"\n",
|
||
" df = pd.DataFrame(data)\n",
|
||
" df.to_csv(dataset_path, index=False)\n",
|
||
" zip_path.unlink()\n",
|
||
" print(f\"[+] Dataset saved to {dataset_path}\")\n",
|
||
"\n",
|
||
"print(f\"[+] Loaded {len(df)} messages\")\n",
|
||
"print(f\" Spam: {sum(df['label'] == 'spam')}\")\n",
|
||
"print(f\" Ham: {sum(df['label'] == 'ham')}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"id": "4ff06a17-1640-4b1b-9a65-c9ec5686a982",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Sample messages:\n",
|
||
"\n",
|
||
"SPAM samples:\n",
|
||
" - Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 8...\n",
|
||
" - FreeMsg Hey there darling it's been 3 week's now and no word back! I'd like some...\n",
|
||
" - WINNER!! As a valued network customer you have been selected to receivea £900 pr...\n",
|
||
"\n",
|
||
"HAM samples:\n",
|
||
" - Go until jurong point, crazy.. Available only in bugis n great world la e buffet...\n",
|
||
" - Ok lar... Joking wif u oni......\n",
|
||
" - U dun say so early hor... U c already then say......\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Sample messages:\")\n",
|
||
"print(\"\\nSPAM samples:\")\n",
|
||
"for msg in df[df['label'] == 'spam']['message'].head(3):\n",
|
||
" print(f\" - {msg[:80]}...\")\n",
|
||
"print(\"\\nHAM samples:\")\n",
|
||
"for msg in df[df['label'] == 'ham']['message'].head(3):\n",
|
||
" print(f\" - {msg[:80]}...\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "3446771b-b673-4e4b-8951-4efe5b3eff7c",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Cleaning"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"id": "4e826b27-ba03-4d56-aa24-92906e8c98b8",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import html as html_module\n",
|
||
"import unicodedata\n",
|
||
"\n",
|
||
"def minimal_clean(text):\n",
|
||
" \"\"\"\n",
|
||
" Minimal cleaning that preserves spam indicators.\n",
|
||
"\n",
|
||
" Parameters: text (str) raw SMS message\n",
|
||
" Returns: str cleaned text with entities decoded, unicode normalized, and\n",
|
||
" whitespace collapsed while keeping informative symbols.\n",
|
||
" \"\"\"\n",
|
||
" # Decode HTML entities (e.g., & -> &)\n",
|
||
" text = html_module.unescape(text)\n",
|
||
"\n",
|
||
" # Normalize unicode characters\n",
|
||
" text = unicodedata.normalize('NFKC', text)\n",
|
||
"\n",
|
||
" # Clean up excessive whitespace\n",
|
||
" text = re.sub(r'\\s+', ' ', text)\n",
|
||
" text = re.sub(r'\\n+', ' ', text)\n",
|
||
" text = re.sub(r'\\r+', ' ', text)\n",
|
||
"\n",
|
||
" return text.strip()\n",
|
||
"\n",
|
||
"def clean_text(text):\n",
|
||
" \"\"\"\n",
|
||
" Final cleaning for vectorization.\n",
|
||
"\n",
|
||
" Converts to lowercase and removes only problematic characters so that\n",
|
||
" informative symbols remain available to the vectorizer.\n",
|
||
"\n",
|
||
" Parameters:\n",
|
||
" text (str): Preprocessed message from `minimal_clean`.\n",
|
||
"\n",
|
||
" Returns:\n",
|
||
" str: Normalized, whitespace‑collapsed text ready for tokenization.\n",
|
||
" \"\"\"\n",
|
||
" text = text.lower()\n",
|
||
" # Keep numbers, currency symbols, punctuation - they're spam features!\n",
|
||
" # Only remove truly problematic characters\n",
|
||
" text = re.sub(r'[^\\w\\s£$€¥!?.,;:\\'\\\"-]', ' ', text)\n",
|
||
" text = re.sub(r'\\s+', ' ', text)\n",
|
||
" return text.strip()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "39bdf320-c877-468f-833f-ba2fbf4025ec",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Preprocessing"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"id": "56d40a5c-77e3-4edf-a111-d157f283ffec",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[*] Applying minimal text cleaning (preserving spam indicators)...\n",
|
||
"\n",
|
||
"[*] Sample spam messages with preserved features:\n",
|
||
" - Summers finally here! Fancy a chat or flirt with sexy singles in yr area? To get MATCHED up just rep...\n",
|
||
" - This is the 2nd time we have tried 2 contact u. U have won the 750 Pound prize. 2 claim is easy, cal...\n",
|
||
" - Get ur 1st RINGTONE FREE NOW! Reply to this msg with TONE. Gr8 TOP 20 tones to your phone every week...\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"[*] Applying minimal text cleaning (preserving spam indicators)...\")\n",
|
||
"df['preprocessed'] = df['message'].apply(minimal_clean)\n",
|
||
"\n",
|
||
"# Apply final cleaning for vectorization\n",
|
||
"df['clean_message'] = df['preprocessed'].apply(clean_text)\n",
|
||
"\n",
|
||
"print(\"\\n[*] Sample spam messages with preserved features:\")\n",
|
||
"spam_samples = df[df['label'] == 'spam'].sample(3, random_state=42)\n",
|
||
"for idx, row in spam_samples.iterrows():\n",
|
||
" msg = row['preprocessed'][:100] + \"...\" if len(row['preprocessed']) > 100 else row['preprocessed']\n",
|
||
" print(f\" - {msg}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"id": "423ce49f-60c9-4131-aea9-d190560e19cd",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[+] Removed 419 duplicates\n",
|
||
"[+] Removed 0 empty messages\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Remove only exact duplicates\n",
|
||
"original_size = len(df)\n",
|
||
"df = df.drop_duplicates(subset=['label', 'clean_message'])\n",
|
||
"print(f\"\\n[+] Removed {original_size - len(df)} duplicates\")\n",
|
||
"\n",
|
||
"# Remove empty messages\n",
|
||
"before_empty = len(df)\n",
|
||
"df = df[df['clean_message'].str.len() > 0]\n",
|
||
"print(f\"[+] Removed {before_empty - len(df)} empty messages\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "9a7e2e62-bb23-4dde-8736-f03fcb25d558",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Split"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"id": "babced9b-8363-41b5-8c60-b96131433a95",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[+] Data split:\n",
|
||
" Training: 4124 messages\n",
|
||
" Testing: 1031 messages\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"X = df['clean_message'].values\n",
|
||
"y = df['label'].values\n",
|
||
"\n",
|
||
"X_train, X_test, y_train, y_test = train_test_split(\n",
|
||
" X, y, test_size=0.2, random_state=42, stratify=y\n",
|
||
")\n",
|
||
"\n",
|
||
"print(f\"\\n[+] Data split:\")\n",
|
||
"print(f\" Training: {len(X_train)} messages\")\n",
|
||
"print(f\" Testing: {len(X_test)} messages\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "60a9d5ae-a10a-491f-a05e-c2c8dc59dec1",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Train"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"id": "5de0e33d-9f0c-4c67-b73b-aec1d9832bb0",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Training Naive Bayes classifier...\n",
|
||
"[+] Loading saved model from models/spam_classifier.pkl\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Training Naive Bayes classifier...\")\n",
|
||
"\n",
|
||
"model_dir = Path(\"models\")\n",
|
||
"model_dir.mkdir(exist_ok=True)\n",
|
||
"model_path = model_dir / \"spam_classifier.pkl\"\n",
|
||
"\n",
|
||
"if model_path.exists():\n",
|
||
" print(f\"[+] Loading saved model from {model_path}\")\n",
|
||
" with open(model_path, 'rb') as f:\n",
|
||
" saved_data = pickle.load(f)\n",
|
||
" vectorizer = saved_data['vectorizer']\n",
|
||
" classifier = saved_data['classifier']\n",
|
||
"\n",
|
||
" # Transform data using existing vocabulary\n",
|
||
" X_train_vec = vectorizer.transform(X_train)\n",
|
||
" X_test_vec = vectorizer.transform(X_test)\n",
|
||
"\n",
|
||
"else:\n",
|
||
" # Configure vectorizer to capture spam patterns\n",
|
||
" vectorizer = CountVectorizer(\n",
|
||
" max_features=3000,\n",
|
||
" token_pattern=r'\\b\\w+\\b|[£$€¥]+|\\d+|!!+|\\?\\?+|\\.\\.+',\n",
|
||
" lowercase=True,\n",
|
||
" stop_words='english'\n",
|
||
" )\n",
|
||
" X_train_vec = vectorizer.fit_transform(X_train)\n",
|
||
" X_test_vec = vectorizer.transform(X_test)\n",
|
||
" \n",
|
||
" classifier = MultinomialNB()\n",
|
||
" classifier.fit(X_train_vec, y_train)\n",
|
||
"\n",
|
||
" # Save model for reproducibility\n",
|
||
" with open(model_path, 'wb') as f:\n",
|
||
" pickle.dump({'vectorizer': vectorizer, 'classifier': classifier}, f)\n",
|
||
" print(f\"[+] Model saved to {model_path}\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "487125a8-5d4f-45cd-bc29-42aad90df8b5",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Eval"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"id": "d3d09aad-c687-49ab-9e42-7f1f21d40ea8",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[+] Training accuracy: 0.9918\n",
|
||
"[+] Testing accuracy: 0.9855\n",
|
||
"\n",
|
||
"[*] Classification Report:\n",
|
||
" precision recall f1-score support\n",
|
||
"\n",
|
||
" ham 0.99 0.99 0.99 903\n",
|
||
" spam 0.95 0.93 0.94 128\n",
|
||
"\n",
|
||
" accuracy 0.99 1031\n",
|
||
" macro avg 0.97 0.96 0.97 1031\n",
|
||
"weighted avg 0.99 0.99 0.99 1031\n",
|
||
"\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Calculate accuracy scores\n",
|
||
"train_acc = classifier.score(X_train_vec, y_train)\n",
|
||
"test_acc = classifier.score(X_test_vec, y_test)\n",
|
||
"print(f\"[+] Training accuracy: {train_acc:.4f}\")\n",
|
||
"print(f\"[+] Testing accuracy: {test_acc:.4f}\")\n",
|
||
"\n",
|
||
"# Get detailed predictions\n",
|
||
"y_pred = classifier.predict(X_test_vec)\n",
|
||
"print(\"\\n[*] Classification Report:\")\n",
|
||
"print(classification_report(y_test, y_pred))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 10,
|
||
"id": "659be282-cdfe-4d38-9f28-4a293731e81d",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Extracting GoodWords from model...\n",
|
||
"[+] Top 10 GoodWords (most 'ham-like'):\n",
|
||
" lor | goodness: 50.94 | ham_p: 0.0047 | spam_p: 0.0001\n",
|
||
" ü | goodness: 47.57 | ham_p: 0.0044 | spam_p: 0.0001\n",
|
||
" ... | goodness: 45.81 | ham_p: 0.0299 | spam_p: 0.0007\n",
|
||
" da | goodness: 45.70 | ham_p: 0.0043 | spam_p: 0.0001\n",
|
||
" later | goodness: 29.97 | ham_p: 0.0028 | spam_p: 0.0001\n",
|
||
" doing | goodness: 26.22 | ham_p: 0.0024 | spam_p: 0.0001\n",
|
||
" really | goodness: 25.47 | ham_p: 0.0024 | spam_p: 0.0001\n",
|
||
" ask | goodness: 24.72 | ham_p: 0.0023 | spam_p: 0.0001\n",
|
||
" cos | goodness: 23.60 | ham_p: 0.0022 | spam_p: 0.0001\n",
|
||
" lol | goodness: 23.60 | ham_p: 0.0022 | spam_p: 0.0001\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Extracting GoodWords from model...\")\n",
|
||
"\n",
|
||
"# Get feature names and probabilities\n",
|
||
"feature_names = vectorizer.get_feature_names_out()\n",
|
||
"ham_log_probs = classifier.feature_log_prob_[0] # Ham class\n",
|
||
"spam_log_probs = classifier.feature_log_prob_[1] # Spam class\n",
|
||
"\n",
|
||
"# Calculate goodness scores\n",
|
||
"goodness_scores = []\n",
|
||
"for i, word in enumerate(feature_names):\n",
|
||
" ham_prob = np.exp(ham_log_probs[i])\n",
|
||
" spam_prob = np.exp(spam_log_probs[i])\n",
|
||
" goodness = ham_prob / (spam_prob + 1e-10)\n",
|
||
" goodness_scores.append((word, goodness, ham_prob, spam_prob))\n",
|
||
"\n",
|
||
"# Sort by goodness\n",
|
||
"goodness_scores.sort(key=lambda x: x[1], reverse=True)\n",
|
||
"top_good_words = goodness_scores[:100]\n",
|
||
"\n",
|
||
"print(f\"[+] Top 10 GoodWords (most 'ham-like'):\")\n",
|
||
"for word, score, hp, sp in top_good_words[:10]:\n",
|
||
" print(f\" {word:15} | goodness: {score:8.2f} | ham_p: {hp:.4f} | spam_p: {sp:.4f}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "7e816857-59b4-472d-8bf7-c024f5156bd7",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Attack"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"id": "020d5ebc-9d9f-456f-89aa-a58199fb193d",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Testing GoodWords attack...\n",
|
||
"[+] Testing on 128 spam messages\n",
|
||
"[*] Testing word counts: [0, 5, 10, 15, 20, 25, 30, 35, 40]\n",
|
||
" Using words: lor, ü, ..., da, later\n",
|
||
"\n",
|
||
"Original: as a registered subscriber yr draw 4 a £100 gift voucher will b entered on receipt of a correct ans. when are the next olympics. txt ans to 80062...\n",
|
||
"Augmented: as a registered subscriber yr draw 4 a £100 gift voucher will b entered on receipt of a correct ans. when are the next olympics. txt ans to 80062 lor ü ... da later...\n",
|
||
" Words: 0 | Evasion: 7.03% (9/128)\n",
|
||
" Words: 5 | Evasion: 41.41% (53/128)\n",
|
||
" Words: 10 | Evasion: 74.22% (95/128)\n",
|
||
" Words: 15 | Evasion: 96.09% (123/128)\n",
|
||
" Words: 20 | Evasion: 100.00% (128/128)\n",
|
||
" Words: 25 | Evasion: 100.00% (128/128)\n",
|
||
" Words: 30 | Evasion: 100.00% (128/128)\n",
|
||
" Words: 35 | Evasion: 100.00% (128/128)\n",
|
||
" Words: 40 | Evasion: 100.00% (128/128)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Testing GoodWords attack...\")\n",
|
||
"\n",
|
||
"# Extract only spam messages for testing\n",
|
||
"spam_test_messages = X_test[y_test == 'spam']\n",
|
||
"print(f\"[+] Testing on {len(spam_test_messages)} spam messages\")\n",
|
||
"\n",
|
||
"# Define test points from baseline (0) to saturation (40)\n",
|
||
"word_counts = [0, 5, 10, 15, 20, 25, 30, 35, 40]\n",
|
||
"attack_results = []\n",
|
||
"\n",
|
||
"print(f\"[*] Testing word counts: {word_counts}\")\n",
|
||
"\n",
|
||
"for num_words in word_counts:\n",
|
||
" # Select the top N good words for this iteration\n",
|
||
" selected_words = [w for w, _, _, _ in top_good_words[:num_words]]\n",
|
||
"\n",
|
||
" # Show which words we're using (first iteration only for clarity)\n",
|
||
" if num_words == 5:\n",
|
||
" print(f\" Using words: {', '.join(selected_words)}\")\n",
|
||
"\n",
|
||
"def augment_message(message, words_to_add):\n",
|
||
" \"\"\"Append good words to a message\"\"\"\n",
|
||
" if len(words_to_add) > 0:\n",
|
||
" return message + \" \" + \" \".join(words_to_add)\n",
|
||
" return message\n",
|
||
"\n",
|
||
"# Test augmentation on one example using the top 5 words\n",
|
||
"sample_spam = spam_test_messages[0]\n",
|
||
"sample_augmented = augment_message(\n",
|
||
" sample_spam,\n",
|
||
" [w for w, _, _, _ in top_good_words[:5]]\n",
|
||
")\n",
|
||
"print(f\"\\nOriginal: {sample_spam}...\")\n",
|
||
"print(f\"Augmented: {sample_augmented}...\")\n",
|
||
"\n",
|
||
"for num_words in word_counts:\n",
|
||
" # Select the top N good words for this iteration\n",
|
||
" selected_words = [w for w, _, _, _ in top_good_words[:num_words]]\n",
|
||
"\n",
|
||
" # Count how many spam messages evade after augmentation\n",
|
||
" evaded = 0\n",
|
||
" for message in spam_test_messages:\n",
|
||
" # Augment the message\n",
|
||
" augmented = augment_message(message, selected_words)\n",
|
||
"\n",
|
||
" # Transform and predict\n",
|
||
" vec = vectorizer.transform([augmented])\n",
|
||
" prob = classifier.predict_proba(vec)[0]\n",
|
||
"\n",
|
||
" # Check evasion: ham probability > spam probability\n",
|
||
" if prob[0] > prob[1]:\n",
|
||
" evaded += 1\n",
|
||
"\n",
|
||
" # Record results for this configuration\n",
|
||
" evasion_rate = (evaded / len(spam_test_messages)) * 100\n",
|
||
" attack_results.append({\n",
|
||
" 'num_words': num_words,\n",
|
||
" 'evasion_rate': evasion_rate,\n",
|
||
" 'evaded': evaded,\n",
|
||
" 'total': len(spam_test_messages)\n",
|
||
" })\n",
|
||
"\n",
|
||
" print(f\" Words: {num_words:2d} | Evasion: {evasion_rate:6.2f}% ({evaded}/{len(spam_test_messages)})\")\n",
|
||
"\n",
|
||
"results_df = pd.DataFrame(attack_results)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "27ca7953-f8b5-4997-a3ce-8db22fc2a544",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Plot"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 12,
|
||
"id": "1a8ffb38-bd5f-4e94-8861-8f7826ee4fe2",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[+] Plot saved to attachments/attack_effectiveness.png\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"plt.style.use('dark_background')\n",
|
||
"fig, ax = plt.subplots(figsize=(12, 6), facecolor=NODE_BLACK)\n",
|
||
"\n",
|
||
"ax.plot(results_df['num_words'], results_df['evasion_rate'],\n",
|
||
" marker='o', markersize=8, linewidth=2.5,\n",
|
||
" color=HTB_GREEN, markeredgecolor='white', markeredgewidth=1)\n",
|
||
"\n",
|
||
"ax.fill_between(results_df['num_words'], 0, results_df['evasion_rate'],\n",
|
||
" alpha=0.3, color=HTB_GREEN)\n",
|
||
"\n",
|
||
"# Add threshold lines\n",
|
||
"ax.axhline(y=50, color=NUGGET_YELLOW, linestyle='--', alpha=0.7, label='50% threshold')\n",
|
||
"ax.axhline(y=90, color=AZURE, linestyle='--', alpha=0.7, label='90% threshold')\n",
|
||
"\n",
|
||
"# Highlight maximum\n",
|
||
"max_idx = results_df['evasion_rate'].idxmax()\n",
|
||
"max_rate = results_df.loc[max_idx, 'evasion_rate']\n",
|
||
"max_words = results_df.loc[max_idx, 'num_words']\n",
|
||
"ax.scatter(max_words, max_rate, s=200, color=MALWARE_RED, zorder=5)\n",
|
||
"ax.annotate(f'Peak: {max_rate:.1f}%\\n@ {max_words} words',\n",
|
||
" xy=(max_words, max_rate), xytext=(max_words+5, max_rate-10),\n",
|
||
" color='white', fontsize=10,\n",
|
||
" arrowprops=dict(arrowstyle='->', color=MALWARE_RED, lw=1.5))\n",
|
||
"\n",
|
||
"ax.set_xlabel('Number of Good Words Added', fontsize=12, color=HTB_GREEN)\n",
|
||
"ax.set_ylabel('Evasion Rate (%)', fontsize=12, color=HTB_GREEN)\n",
|
||
"ax.set_title('GoodWords Attack Effectiveness', fontsize=14, color=HTB_GREEN, pad=20)\n",
|
||
"ax.grid(True, alpha=0.2)\n",
|
||
"ax.set_facecolor(NODE_BLACK)\n",
|
||
"ax.legend()\n",
|
||
"\n",
|
||
"for spine in ax.spines.values():\n",
|
||
" spine.set_color(HACKER_GREY)\n",
|
||
"ax.tick_params(colors=HACKER_GREY)\n",
|
||
"\n",
|
||
"plt.tight_layout()\n",
|
||
"output_dir = Path(\"attachments\")\n",
|
||
"output_dir.mkdir(exist_ok=True)\n",
|
||
"plt.savefig(output_dir / \"attack_effectiveness.png\", dpi=150, facecolor=NODE_BLACK)\n",
|
||
"plt.close()\n",
|
||
"print(f\"\\n[+] Plot saved to {output_dir / 'attack_effectiveness.png'}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "raw",
|
||
"id": "d6613d7e-8bac-4be9-a7c8-9ed9cf44f870",
|
||
"metadata": {},
|
||
"source": [
|
||
"logP(ham∣message)=logP(ham)+∑w∈wordslogP(w∣ham)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "21dcba53-c378-44d0-923b-cff8c5579503",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Optimization (Best Word :0)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"id": "f0aaa9b2-3a81-48a6-bd30-60c10934186f",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Analyzing individual word impact...\n",
|
||
"[+] Testing 20 words on 50 spam messages\n",
|
||
" Word 'lor': 7.13% reduction\n",
|
||
" Word 'ü': 7.04% reduction\n",
|
||
" Word '...': 6.98% reduction\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Analyzing individual word impact...\")\n",
|
||
"\n",
|
||
"# Use 50 spam messages as a representative sample\n",
|
||
"sample_spam = spam_test_messages[:50]\n",
|
||
"word_impacts = []\n",
|
||
"\n",
|
||
"print(f\"[+] Testing {len(top_good_words[:20])} words on {len(sample_spam)} spam messages\")\n",
|
||
"\n",
|
||
"for word, _, _, _ in top_good_words[:20]:\n",
|
||
" total_impact = 0\n",
|
||
"\n",
|
||
" for message in sample_spam:\n",
|
||
" # Calculate original spam probability\n",
|
||
" vec_orig = vectorizer.transform([message])\n",
|
||
" prob_orig = classifier.predict_proba(vec_orig)[0][1] # spam prob\n",
|
||
"\n",
|
||
" # Calculate probability after adding the word\n",
|
||
" vec_aug = vectorizer.transform([message + \" \" + word])\n",
|
||
" prob_aug = classifier.predict_proba(vec_aug)[0][1]\n",
|
||
"\n",
|
||
" # Measure the probability reduction\n",
|
||
" impact = prob_orig - prob_aug\n",
|
||
" total_impact += impact\n",
|
||
"\n",
|
||
" # Calculate average impact across all messages\n",
|
||
" avg_impact = (total_impact / len(sample_spam)) * 100\n",
|
||
" word_impacts.append((word, avg_impact))\n",
|
||
"\n",
|
||
" # Show progress for first few words\n",
|
||
" if len(word_impacts) <= 3:\n",
|
||
" print(f\" Word '{word}': {avg_impact:.2f}% reduction\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "248d4b21-e1dd-4b82-a0ae-d142e5623f37",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Plot"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 14,
|
||
"id": "74929764-4fcc-4b48-91f5-2e7635295190",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[+] Plot saved to attachments/word_impact.png\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Plot word impacts\n",
|
||
"fig, ax = plt.subplots(figsize=(10, 8), facecolor=NODE_BLACK)\n",
|
||
"\n",
|
||
"words = [w for w, _ in word_impacts]\n",
|
||
"impacts = [i for _, i in word_impacts]\n",
|
||
"colors = [HTB_GREEN if i > 15 else NUGGET_YELLOW if i > 10 else HACKER_GREY for i in impacts]\n",
|
||
"\n",
|
||
"bars = ax.barh(range(len(words)), impacts, color=colors, edgecolor='white', linewidth=0.5)\n",
|
||
"\n",
|
||
"ax.set_yticks(range(len(words)))\n",
|
||
"ax.set_yticklabels(words)\n",
|
||
"ax.set_xlabel('Average Spam Probability Reduction (%)', fontsize=12, color=HTB_GREEN)\n",
|
||
"ax.set_title('Individual Word Impact on Spam Detection', fontsize=14, color=HTB_GREEN, pad=20)\n",
|
||
"ax.grid(axis='x', alpha=0.2)\n",
|
||
"ax.set_facecolor(NODE_BLACK)\n",
|
||
"\n",
|
||
"for spine in ax.spines.values():\n",
|
||
" spine.set_color(HACKER_GREY)\n",
|
||
"ax.tick_params(colors=HACKER_GREY)\n",
|
||
"\n",
|
||
"plt.tight_layout()\n",
|
||
"plt.savefig(output_dir / \"word_impact.png\", dpi=150, facecolor=NODE_BLACK)\n",
|
||
"plt.close()\n",
|
||
"print(f\"[+] Plot saved to {output_dir / 'word_impact.png'}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "7a888c46-129d-4182-afbc-44d1b2cd3cc1",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Probability Shift Analysis"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 15,
|
||
"id": "a816c1fa-5eba-46b2-b553-ab21cb408bc8",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Visualizing probability shifts...\n",
|
||
"[+] Plot saved to attachments/probability_shift.png\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Visualizing probability shifts...\")\n",
|
||
"\n",
|
||
"# Sample messages for detailed analysis\n",
|
||
"sample_messages = spam_test_messages[:8]\n",
|
||
"test_word_counts = [0, 5, 10, 20, 30]\n",
|
||
"\n",
|
||
"fig, ax = plt.subplots(figsize=(14, 6), facecolor=NODE_BLACK)\n",
|
||
"\n",
|
||
"x = np.arange(len(sample_messages))\n",
|
||
"width = 0.15\n",
|
||
"colors_list = [MALWARE_RED, NUGGET_YELLOW, AZURE, HTB_GREEN, AQUAMARINE]\n",
|
||
"\n",
|
||
"for i, num_words in enumerate(test_word_counts):\n",
|
||
" # Select the appropriate number of good words\n",
|
||
" selected = [w for w, _, _, _ in top_good_words[:num_words]]\n",
|
||
" probs = []\n",
|
||
"\n",
|
||
" for msg in sample_messages:\n",
|
||
" # Augment message with selected words\n",
|
||
" if num_words > 0:\n",
|
||
" aug_msg = msg + \" \" + \" \".join(selected)\n",
|
||
" else:\n",
|
||
" aug_msg = msg\n",
|
||
"\n",
|
||
" # Calculate spam probability\n",
|
||
" vec = vectorizer.transform([aug_msg])\n",
|
||
" spam_prob = classifier.predict_proba(vec)[0][1]\n",
|
||
" probs.append(spam_prob)\n",
|
||
"\n",
|
||
" # Create grouped bars with distinct colors\n",
|
||
" bars = ax.bar(x + i*width, probs, width,\n",
|
||
" label=f'{num_words} words',\n",
|
||
" color=colors_list[i], alpha=0.8)\n",
|
||
"\n",
|
||
" # Mark successful evasions\n",
|
||
" for j, (bar, prob) in enumerate(zip(bars, probs)):\n",
|
||
" if prob < 0.5:\n",
|
||
" ax.text(bar.get_x() + bar.get_width()/2, prob + 0.02,\n",
|
||
" '✓', ha='center', va='bottom', color=HTB_GREEN, fontweight='bold')\n",
|
||
"\n",
|
||
"ax.axhline(y=0.5, color='white', linestyle='--', alpha=0.5, label='Decision boundary')\n",
|
||
"ax.set_xlabel('Message Index', fontsize=12, color=HTB_GREEN)\n",
|
||
"ax.set_ylabel('Spam Probability', fontsize=12, color=HTB_GREEN)\n",
|
||
"ax.set_title('Probability Shift with Increasing Good Words', fontsize=14, color=HTB_GREEN, pad=20)\n",
|
||
"ax.set_xticks(x + width * 2)\n",
|
||
"ax.set_xticklabels([f'M{i+1}' for i in range(len(sample_messages))])\n",
|
||
"ax.legend(loc='upper right')\n",
|
||
"ax.grid(axis='y', alpha=0.2)\n",
|
||
"ax.set_facecolor(NODE_BLACK)\n",
|
||
"\n",
|
||
"for spine in ax.spines.values():\n",
|
||
" spine.set_color(HACKER_GREY)\n",
|
||
"ax.tick_params(colors=HACKER_GREY)\n",
|
||
"\n",
|
||
"plt.tight_layout()\n",
|
||
"plt.savefig(output_dir / \"probability_shift.png\", dpi=150, facecolor=NODE_BLACK)\n",
|
||
"plt.close()\n",
|
||
"print(f\"[+] Plot saved to {output_dir / 'probability_shift.png'}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "61bbc496-93de-413b-bb99-52d82e01e82e",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Begin Blackbox attack"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 16,
|
||
"id": "62174fd9-69c8-444b-825a-0e9d4c2acf48",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Simulating black-box attack scenario...\n",
|
||
"[*] Budget: 1000 queries\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Simulating black-box attack scenario...\")\n",
|
||
"print(\"[*] Budget: 1000 queries\")\n",
|
||
"\n",
|
||
"# Simulate limited query access\n",
|
||
"query_budget = 1000\n",
|
||
"queries_used = 0\n",
|
||
"query_log = []"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 17,
|
||
"id": "985bf085-e771-497d-be38-91b95a6805ba",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[*] Example: extract_ham_word_freq\n",
|
||
" Ham messages sampled: 500\n",
|
||
" Unique tokens found: 2162\n",
|
||
" you: 207\n",
|
||
" the: 125\n",
|
||
" and: 107\n",
|
||
" that: 67\n",
|
||
" for: 62\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"def extract_ham_word_freq(X_train, y_train, sample_size=500):\n",
|
||
" \"\"\"\n",
|
||
" Compute token frequencies from a sample of ham messages.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X_train : array-like of str\n",
|
||
" Cleaned training messages.\n",
|
||
" y_train : array-like of str\n",
|
||
" Labels aligned with X_train ('ham' or 'spam').\n",
|
||
" sample_size : int, default 500\n",
|
||
" Number of ham messages to analyze.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" dict[str, int]\n",
|
||
" Mapping of word -> frequency within sampled ham messages.\n",
|
||
" \"\"\"\n",
|
||
" ham_msgs = X_train[y_train == 'ham']\n",
|
||
" limit = min(sample_size, len(ham_msgs))\n",
|
||
" freq = {}\n",
|
||
" for msg in ham_msgs[:limit]:\n",
|
||
" for w in str(msg).split():\n",
|
||
" if 2 < len(w) < 10: # keep typical conversational tokens\n",
|
||
" freq[w] = freq.get(w, 0) + 1\n",
|
||
" return freq\n",
|
||
"\n",
|
||
"wf_example = extract_ham_word_freq(X_train, y_train, sample_size=500)\n",
|
||
"print(\"[*] Example: extract_ham_word_freq\")\n",
|
||
"print(f\" Ham messages sampled: {min(500, sum(y_train == 'ham'))}\")\n",
|
||
"print(f\" Unique tokens found: {len(wf_example)}\")\n",
|
||
"top5 = sorted(wf_example.items(), key=lambda x: (-x[1], x[0]))[:5]\n",
|
||
"for w, c in top5:\n",
|
||
" print(f\" {w}: {c}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 18,
|
||
"id": "425ebd85-4ad2-4e22-b98f-163b497dadb8",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[*] Example: select_high_frequency_words\n",
|
||
" Selected top words: 100 (min_freq=5)\n",
|
||
" First 10: you, the, and, that, for, your, are, have, but, not\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"def select_high_frequency_words(word_freq, max_words=100, min_freq=5):\n",
|
||
" \"\"\"\n",
|
||
" Select the most frequent ham words above a minimum frequency.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" word_freq : dict[str, int]\n",
|
||
" Token frequency table for sampled ham messages.\n",
|
||
" max_words : int, default 100\n",
|
||
" Maximum number of words to return.\n",
|
||
" min_freq : int, default 5\n",
|
||
" Minimum frequency a word must meet to be considered.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" list[str]\n",
|
||
" Top words sorted by decreasing frequency then lexicographically.\n",
|
||
" \"\"\"\n",
|
||
" sorted_by_freq = sorted(word_freq.items(), key=lambda x: (-x[1], x[0]))\n",
|
||
" top = [w for w, c in sorted_by_freq if c > min_freq][:max_words]\n",
|
||
" return top\n",
|
||
"\n",
|
||
"top_words_example = select_high_frequency_words(wf_example, max_words=100, min_freq=5)\n",
|
||
"print(\"[*] Example: select_high_frequency_words\")\n",
|
||
"print(f\" Selected top words: {len(top_words_example)} (min_freq=5)\")\n",
|
||
"print(\" First 10:\", \", \".join(top_words_example[:10]))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 19,
|
||
"id": "3649a469-5d97-4205-bedc-3d9320bfade0",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[*] Example: merge_with_curated\n",
|
||
" Merged size: 111 | Added curated: 11\n",
|
||
" Sample added terms: ask, didnt, doing, ill, later\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"def merge_with_curated(top_words, additional_candidates=None):\n",
|
||
" \"\"\"\n",
|
||
" Merge data-driven top words with curated conversational candidates.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" top_words : list[str]\n",
|
||
" High-frequency ham words from the previous step.\n",
|
||
" additional_candidates : list[str] | None\n",
|
||
" Optional curated list to include regardless of frequency.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" list[str]\n",
|
||
" Deduplicated merged list (lexicographically ordered).\n",
|
||
" \"\"\"\n",
|
||
" if additional_candidates is None:\n",
|
||
" additional_candidates = [\n",
|
||
" \"ok\", \"cos\", \"ill\", \"thats\", \"later\", \"said\", \"ask\", \"didnt\",\n",
|
||
" \"dont\", \"doing\", \"going\", \"come\", \"home\", \"tomorrow\", \"today\", \"sorry\",\n",
|
||
" \"thanks\", \"yeah\", \"yes\", \"sure\", \"see\", \"tell\", \"know\", \"think\",\n",
|
||
" ]\n",
|
||
" merged = set(top_words) | set(additional_candidates)\n",
|
||
" return sorted(merged)\n",
|
||
"\n",
|
||
"merged_example = merge_with_curated(top_words_example)\n",
|
||
"added = sorted(set(merged_example) - set(top_words_example))\n",
|
||
"print(\"[*] Example: merge_with_curated\")\n",
|
||
"print(f\" Merged size: {len(merged_example)} | Added curated: {len(added)}\")\n",
|
||
"print(\" Sample added terms:\", \", \".join(added[:5]))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 20,
|
||
"id": "be05bc00-7ec4-4c5d-bb77-9779240cbcf7",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[*] Example: build_candidate_vocabulary\n",
|
||
" Candidates: 111\n",
|
||
" First 10: you, the, and, that, for, your, are, have, but, not\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"def build_candidate_vocabulary(\n",
|
||
" X_train,\n",
|
||
" y_train,\n",
|
||
" sample_size=500,\n",
|
||
" max_words=100,\n",
|
||
" min_freq=5,\n",
|
||
" additional_candidates=None,\n",
|
||
"):\n",
|
||
" \"\"\"\n",
|
||
" Build a candidate vocabulary for black-box discovery from ham messages.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" X_train : array-like of str\n",
|
||
" Cleaned training messages.\n",
|
||
" y_train : array-like of str\n",
|
||
" Labels aligned with X_train ('ham' or 'spam').\n",
|
||
" sample_size : int, default 500\n",
|
||
" Number of ham messages to analyze.\n",
|
||
" max_words : int, default 100\n",
|
||
" Maximum number of top frequent ham words to keep before merging extras.\n",
|
||
" min_freq : int, default 5\n",
|
||
" Minimum frequency threshold for inclusion from the ham corpus.\n",
|
||
" additional_candidates : list[str] | None\n",
|
||
" Optional curated conversational terms to include.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" list[str]\n",
|
||
" Deduplicated candidate words ordered by decreasing ham frequency,\n",
|
||
" then lexicographically for stable ties.\n",
|
||
" \"\"\"\n",
|
||
" word_freq = extract_ham_word_freq(X_train, y_train, sample_size=sample_size)\n",
|
||
" top_words = select_high_frequency_words(word_freq, max_words=max_words, min_freq=min_freq)\n",
|
||
" merged = merge_with_curated(top_words, additional_candidates=additional_candidates)\n",
|
||
"\n",
|
||
" # Stable final ordering driven by ham frequency, then lexical for ties\n",
|
||
" def sort_key(w):\n",
|
||
" return (-word_freq.get(w, 0), w)\n",
|
||
"\n",
|
||
" return sorted(merged, key=sort_key)\n",
|
||
"\n",
|
||
"cv_example = build_candidate_vocabulary(X_train, y_train)\n",
|
||
"print(\"[*] Example: build_candidate_vocabulary\")\n",
|
||
"print(f\" Candidates: {len(cv_example)}\")\n",
|
||
"print(\" First 10:\", \", \".join(cv_example[:10]))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 21,
|
||
"id": "109b6a4a-95ab-43b3-a400-5d9f5c99f194",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[+] Testing 111 candidate words extracted from ham messages\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Build candidate vocabulary for discovery\n",
|
||
"candidate_words = build_candidate_vocabulary(X_train, y_train)\n",
|
||
"print(f\"[+] Testing {len(candidate_words)} candidate words extracted from ham messages\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 22,
|
||
"id": "1ff0d821-d5e0-4b18-b07c-2fa5252da403",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Budget allocation:\n",
|
||
" exploration : 400 queries\n",
|
||
" exploitation: 400 queries\n",
|
||
" combination : 200 queries\n",
|
||
" Total: 1000 / 1000\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"def estimate_budget_allocation(total_budget):\n",
|
||
" \"\"\"\n",
|
||
" Estimate allocation across exploration, exploitation, and combination.\n",
|
||
"\n",
|
||
" Parameters\n",
|
||
" ----------\n",
|
||
" total_budget : int\n",
|
||
" Total query budget available for discovery.\n",
|
||
"\n",
|
||
" Returns\n",
|
||
" -------\n",
|
||
" dict\n",
|
||
" Mapping phase -> integer number of queries that sums to `total_budget`.\n",
|
||
" \"\"\"\n",
|
||
" explore = int(0.4 * total_budget)\n",
|
||
" exploit = int(0.4 * total_budget)\n",
|
||
" combine = total_budget - explore - exploit # absorb rounding\n",
|
||
" return {\n",
|
||
" 'exploration': explore,\n",
|
||
" 'exploitation': exploit,\n",
|
||
" 'combination': combine,\n",
|
||
" }\n",
|
||
"\n",
|
||
"# Quick demo for budget allocation\n",
|
||
"allocation = estimate_budget_allocation(query_budget)\n",
|
||
"print(\"\\n[*] Budget allocation:\")\n",
|
||
"for phase, budget in allocation.items():\n",
|
||
" print(f\" {phase:12}: {budget:4d} queries\")\n",
|
||
"print(f\" Total: {sum(allocation.values())} / {query_budget}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 23,
|
||
"id": "3e399027-9815-42ee-875e-c00798d3a534",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[*] Discovery phase: testing 111 candidates...\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Discovery phase - test word effectiveness\n",
|
||
"word_scores = {}\n",
|
||
"test_spam_samples = spam_test_messages[:50] # More test messages\n",
|
||
"\n",
|
||
"# Test in batches to be more efficient\n",
|
||
"print(f\"[*] Discovery phase: testing {len(candidate_words)} candidates...\")\n",
|
||
"\n",
|
||
"# Randomly sample candidates and messages for better coverage\n",
|
||
"np.random.shuffle(candidate_words)\n",
|
||
"np.random.shuffle(test_spam_samples)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 24,
|
||
"id": "031afe59-0dcb-4a13-b8c2-7df1d34aaebe",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def initialize_adaptive_scorer():\n",
|
||
" \"\"\"Initialize adaptive scoring data structures\"\"\"\n",
|
||
" return {\n",
|
||
" 'word_scores': {}, # Maps word -> effectiveness score\n",
|
||
" 'word_counts': {}, # Maps word -> number of times tested\n",
|
||
" 'exploration_rate': 0.2 # 20% exploration for discovery phase\n",
|
||
" }"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 25,
|
||
"id": "c52c95ea-65e7-4a83-815e-3f0dfcab9ded",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def epsilon_greedy_select(scorer, available_words):\n",
|
||
" \"\"\"Select word using epsilon-greedy strategy\n",
|
||
"\n",
|
||
" Parameters:\n",
|
||
" scorer (dict): Adaptive scorer state\n",
|
||
" available_words (list): Candidate words to choose from\n",
|
||
"\n",
|
||
" Returns:\n",
|
||
" str: Selected word for testing\n",
|
||
" \"\"\"\n",
|
||
" import random\n",
|
||
"\n",
|
||
" if random.random() < scorer['exploration_rate']:\n",
|
||
" # Exploration: try untested or rarely tested words\n",
|
||
" untested = [w for w in available_words if w not in scorer['word_counts']]\n",
|
||
" if untested:\n",
|
||
" return random.choice(untested)\n",
|
||
" else:\n",
|
||
" # Choose least tested word\n",
|
||
" return min(available_words,\n",
|
||
" key=lambda w: scorer['word_counts'].get(w, 0))\n",
|
||
" python\n",
|
||
" else:\n",
|
||
" # Exploitation: choose best performing word\n",
|
||
" return max(available_words,\n",
|
||
" key=lambda w: scorer['word_scores'].get(w, 0))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 26,
|
||
"id": "91904977-0044-4bdc-94e8-64be9a0c889d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def update_word_score(scorer, word, impact, alpha=0.3):\n",
|
||
" \"\"\"Update word score using exponential moving average\n",
|
||
"\n",
|
||
" Parameters:\n",
|
||
" scorer (dict): Adaptive scorer state\n",
|
||
" word (str): Word being scored\n",
|
||
" impact (float): Observed reduction in spam probability\n",
|
||
" alpha (float): Learning rate\n",
|
||
" \"\"\"\n",
|
||
" if word not in scorer['word_scores']:\n",
|
||
" scorer['word_scores'][word] = impact\n",
|
||
" scorer['word_counts'][word] = 1\n",
|
||
" else:\n",
|
||
" # Exponential moving average\n",
|
||
" old_score = scorer['word_scores'][word]\n",
|
||
" scorer['word_scores'][word] = (1 - alpha) * old_score + alpha * impact\n",
|
||
" scorer['word_counts'][word] += 1"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 27,
|
||
"id": "883fa203-95b5-4965-9a80-e5f052b91c4b",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def discover_word_combinations(message, test_words, max_size=3):\n",
|
||
" \"\"\"Discover effective word combinations through systematic search\n",
|
||
"\n",
|
||
" Parameters:\n",
|
||
" message (str): Target spam message\n",
|
||
" test_words (list): Promising words to test\n",
|
||
" max_size (int): Maximum combination size\n",
|
||
"\n",
|
||
" Returns:\n",
|
||
" dict: Mapping of word combinations to effectiveness scores\n",
|
||
" \"\"\"\n",
|
||
" from itertools import combinations\n",
|
||
"\n",
|
||
" combination_scores = {}\n",
|
||
" message_vec = vectorizer.transform([message])\n",
|
||
" message_score = classifier.predict_proba(message_vec)[0][1]\n",
|
||
"\n",
|
||
" # Test individual words first\n",
|
||
" for word in test_words[:20]:\n",
|
||
" test_message = message + \" \" + word\n",
|
||
" test_vec = vectorizer.transform([test_message])\n",
|
||
" score = classifier.predict_proba(test_vec)[0][1]\n",
|
||
" impact = message_score - score\n",
|
||
" combination_scores[(word,)] = impact\n",
|
||
"\n",
|
||
" # Test pairs for synergy\n",
|
||
" if max_size >= 2:\n",
|
||
" for word1, word2 in combinations(test_words[:15], 2):\n",
|
||
" test_message = message + \" \" + word1 + \" \" + word2\n",
|
||
" test_vec = vectorizer.transform([test_message])\n",
|
||
" score = classifier.predict_proba(test_vec)[0][1]\n",
|
||
"\n",
|
||
" # Calculate synergy\n",
|
||
" individual_impact = combination_scores.get((word1,), 0) + combination_scores.get((word2,), 0)\n",
|
||
" actual_impact = message_score - score\n",
|
||
" synergy = actual_impact - individual_impact\n",
|
||
"\n",
|
||
" if synergy > 0: # Positive synergy detected\n",
|
||
" combination_scores[(word1, word2)] = actual_impact\n",
|
||
"\n",
|
||
" # Test triplets for top pairs\n",
|
||
" if max_size >= 3:\n",
|
||
" top_pairs = sorted(\n",
|
||
" [(k, v) for k, v in combination_scores.items() if len(k) == 2],\n",
|
||
" key=lambda x: x[1], reverse=True\n",
|
||
" )[:5]\n",
|
||
"\n",
|
||
" for pair, pair_score in top_pairs:\n",
|
||
" for word in test_words[:10]:\n",
|
||
" if word not in pair:\n",
|
||
" triplet = tuple(sorted(pair + (word,)))\n",
|
||
" test_message = message + \" \" + \" \".join(triplet)\n",
|
||
" test_vec = vectorizer.transform([test_message])\n",
|
||
" score = classifier.predict_proba(test_vec)[0][1]\n",
|
||
" combination_scores[triplet] = message_score - score\n",
|
||
"\n",
|
||
" return combination_scores"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 28,
|
||
"id": "e4444202-9df3-45dd-ae52-79bced2fe939",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def three_phase_discovery(spam_messages, candidate_words, budget=1000):\n",
|
||
" \"\"\"Three-phase discovery: exploration, exploitation, combination\n",
|
||
"\n",
|
||
" Parameters:\n",
|
||
" spam_messages (list): Target spam messages\n",
|
||
" candidate_words (list): Vocabulary to test\n",
|
||
" budget (int): Total query budget\n",
|
||
"\n",
|
||
" Returns:\n",
|
||
" tuple: (discovered_words, combination_scores, queries_used)\n",
|
||
" \"\"\"\n",
|
||
" scorer = initialize_adaptive_scorer()\n",
|
||
" queries_used = 0\n",
|
||
"\n",
|
||
" # Allocate budgets using 40-40-20 split strategy\n",
|
||
" allocation = estimate_budget_allocation(budget)\n",
|
||
" exploration_budget = allocation['exploration']\n",
|
||
" exploitation_budget = allocation['exploitation']\n",
|
||
" combination_budget = allocation['combination']\n",
|
||
"\n",
|
||
" # Phase 1: Broad exploration (allocated budget)\n",
|
||
" print(f\"[*] Phase 1: Exploration (budget: {exploration_budget} queries)\")\n",
|
||
"\n",
|
||
" p1_marks = {\n",
|
||
" max(1, int(0.25 * exploration_budget)),\n",
|
||
" max(1, int(0.50 * exploration_budget)),\n",
|
||
" max(1, int(0.75 * exploration_budget)),\n",
|
||
" }\n",
|
||
" p1_reported = set()\n",
|
||
"\n",
|
||
" # Select a message and a candidate word\n",
|
||
" test_message = random.choice(spam_messages)\n",
|
||
" word = epsilon_greedy_select(scorer, candidate_words)\n",
|
||
"\n",
|
||
" # Baseline and augmented spam probabilities\n",
|
||
" vec_orig = vectorizer.transform([test_message])\n",
|
||
" prob_orig = classifier.predict_proba(vec_orig)[0][1] # spam prob\n",
|
||
"\n",
|
||
" vec_aug = vectorizer.transform([test_message + \" \" + word])\n",
|
||
" prob_aug = classifier.predict_proba(vec_aug)[0][1]\n",
|
||
"\n",
|
||
" impact = prob_orig - prob_aug\n",
|
||
"\n",
|
||
" # Update running score and consume query budget\n",
|
||
" update_word_score(scorer, word, impact)\n",
|
||
" queries_used += 2\n",
|
||
"\n",
|
||
" # Optional milestone report\n",
|
||
" if queries_used in p1_marks and queries_used not in p1_reported:\n",
|
||
" top3 = sorted(scorer['word_scores'].items(), key=lambda x: x[1], reverse=True)[:3]\n",
|
||
" print(\n",
|
||
" f\" [P1 {queries_used}/{exploration_budget}] \"\n",
|
||
" f\"tested_words={len(scorer['word_scores'])} | \"\n",
|
||
" f\"top3=\" + \", \".join(f\"{w}:{s:.3f}\" for w, s in top3)\n",
|
||
" )\n",
|
||
" p1_reported.add(queries_used)\n",
|
||
"\n",
|
||
" while queries_used < exploration_budget and len(candidate_words) > 0:\n",
|
||
" # Select inputs\n",
|
||
" test_message = random.choice(spam_messages)\n",
|
||
" word = epsilon_greedy_select(scorer, candidate_words)\n",
|
||
"\n",
|
||
" # Measure impact with two queries\n",
|
||
" vec_orig = vectorizer.transform([test_message])\n",
|
||
" prob_orig = classifier.predict_proba(vec_orig)[0][1]\n",
|
||
" vec_aug = vectorizer.transform([test_message + \" \" + word])\n",
|
||
" prob_aug = classifier.predict_proba(vec_aug)[0][1]\n",
|
||
" impact = prob_orig - prob_aug\n",
|
||
"\n",
|
||
" # Update score and account for budget\n",
|
||
" update_word_score(scorer, word, impact)\n",
|
||
" queries_used += 2\n",
|
||
"\n",
|
||
" # Milestone report\n",
|
||
" if queries_used in p1_marks and queries_used not in p1_reported:\n",
|
||
" top3 = sorted(scorer['word_scores'].items(), key=lambda x: x[1], reverse=True)[:3]\n",
|
||
" print(\n",
|
||
" f\" [P1 {queries_used}/{exploration_budget}] \"\n",
|
||
" f\"tested_words={len(scorer['word_scores'])} | \"\n",
|
||
" f\"top3=\" + \", \".join(f\"{w}:{s:.3f}\" for w, s in top3)\n",
|
||
" )\n",
|
||
" p1_reported.add(queries_used)\n",
|
||
"\n",
|
||
" print(f\"[+] Exploration complete. Queries: {queries_used}, Words tested: {len(scorer['word_scores'])}\")\n",
|
||
" top5 = sorted(scorer['word_scores'].items(), key=lambda x: x[1], reverse=True)[:5]\n",
|
||
" if top5:\n",
|
||
" print(\" Top5 after exploration:\")\n",
|
||
" for w, s in top5:\n",
|
||
" print(f\" {w:12} | score: {s:.3f}\")\n",
|
||
"\n",
|
||
" # Phase 2: Focused exploitation\n",
|
||
" scorer['exploration_rate'] = 0.1 # Reduce exploration\n",
|
||
"\n",
|
||
" # Get top words for exploitation\n",
|
||
" top_words = sorted(scorer['word_scores'].items(), key=lambda x: x[1], reverse=True)[:30]\n",
|
||
" top_word_list = [w for w, _ in top_words]\n",
|
||
"\n",
|
||
" print(f\"\\n[*] Phase 2: Exploitation (budget: {exploitation_budget} queries)\")\n",
|
||
" initial_queries = queries_used\n",
|
||
" p2_mid = initial_queries + max(1, exploitation_budget // 2)\n",
|
||
"\n",
|
||
" while queries_used < initial_queries + exploitation_budget and len(top_word_list) > 0:\n",
|
||
" test_message = random.choice(spam_messages[:20]) # Focus on fewer messages\n",
|
||
" word = random.choice(top_word_list[:15]) # Focus on best words\n",
|
||
"\n",
|
||
" vec_orig = vectorizer.transform([test_message])\n",
|
||
" prob_orig = classifier.predict_proba(vec_orig)[0][1]\n",
|
||
"\n",
|
||
" vec_aug = vectorizer.transform([test_message + \" \" + word])\n",
|
||
" prob_aug = classifier.predict_proba(vec_aug)[0][1]\n",
|
||
"\n",
|
||
" impact = prob_orig - prob_aug\n",
|
||
" update_word_score(scorer, word, impact)\n",
|
||
" queries_used += 2\n",
|
||
"\n",
|
||
" print(f\"[+] Exploitation complete. Total queries: {queries_used}\")\n",
|
||
"\n",
|
||
" # Phase 3: Combination discovery (allocated budget)\n",
|
||
" remaining_combo = combination_budget\n",
|
||
" print(f\"\\n[*] Phase 3: Combination search (budget: {remaining_combo} queries)\")\n",
|
||
"\n",
|
||
" best_combinations = {}\n",
|
||
" combos_tested = 0\n",
|
||
"\n",
|
||
" if remaining_combo > 50: # Need minimum queries for combinations\n",
|
||
" for i in range(min(3, len(spam_messages))):\n",
|
||
" if queries_used >= budget or remaining_combo <= 0:\n",
|
||
" break\n",
|
||
"\n",
|
||
" test_msg = spam_messages[i]\n",
|
||
" combos = discover_word_combinations(test_msg, top_word_list[:20], max_size=3)\n",
|
||
"\n",
|
||
" # Track best combinations across messages\n",
|
||
" for combo, score in combos.items():\n",
|
||
" if combo not in best_combinations or score > best_combinations[combo]:\n",
|
||
" best_combinations[combo] = score\n",
|
||
"\n",
|
||
" # Account for queries (~2 per combination) while respecting the budget\n",
|
||
" to_add = min(remaining_combo, len(combos) * 2)\n",
|
||
" queries_used += to_add\n",
|
||
" remaining_combo -= to_add\n",
|
||
" combos_tested += len(combos)\n",
|
||
"\n",
|
||
" # Midpoint snapshot\n",
|
||
" if combination_budget > 0 and remaining_combo <= combination_budget // 2 and best_combinations:\n",
|
||
" best = max(best_combinations.items(), key=lambda x: x[1])\n",
|
||
" print(\n",
|
||
" f\" [P3 mid ~{combination_budget - remaining_combo}/{combination_budget}] \"\n",
|
||
" f\"combos_tested={combos_tested} | best={' + '.join(best[0])}:{best[1]:.3f}\"\n",
|
||
" )\n",
|
||
"\n",
|
||
" if remaining_combo <= 0:\n",
|
||
" break\n",
|
||
"\n",
|
||
" print(f\"[+] Combination search complete. Total queries: {queries_used}\")\n",
|
||
"\n",
|
||
" # Return final results\n",
|
||
" final_words = sorted(scorer['word_scores'].items(), key=lambda x: x[1], reverse=True)\n",
|
||
" return final_words, best_combinations, queries_used\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "f247ef8e-f284-4057-8626-70fa863ae43d",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Run Three-Phase Discovery"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 29,
|
||
"id": "d3579810-3723-4bef-b819-e1296de2c0d5",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[*] Using three-phase discovery algorithm...\n",
|
||
"[+] Built vocabulary of 111 candidate words\n",
|
||
"\n",
|
||
"[*] Budget allocation:\n",
|
||
" exploration : 400 queries\n",
|
||
" exploitation: 400 queries\n",
|
||
" combination : 200 queries\n",
|
||
"[*] Phase 1: Exploration (budget: 400 queries)\n",
|
||
" [P1 100/400] tested_words=11 | top3=remember:0.014, time:0.012, thats:0.007\n",
|
||
" [P1 200/400] tested_words=17 | top3=remember:0.004, time:0.004, ...:0.003\n",
|
||
" [P1 300/400] tested_words=30 | top3=...:0.003, thats:0.002, time:0.002\n",
|
||
"[+] Exploration complete. Queries: 400, Words tested: 40\n",
|
||
" Top5 after exploration:\n",
|
||
" come | score: 0.030\n",
|
||
" work | score: 0.027\n",
|
||
" yes | score: 0.026\n",
|
||
" remember | score: 0.002\n",
|
||
" ... | score: 0.001\n",
|
||
"\n",
|
||
"[*] Phase 2: Exploitation (budget: 400 queries)\n",
|
||
"[+] Exploitation complete. Total queries: 800\n",
|
||
"\n",
|
||
"[*] Phase 3: Combination search (budget: 200 queries)\n",
|
||
" [P3 mid ~200/200] combos_tested=170 | best=... + ask + doing:0.000\n",
|
||
"[+] Combination search complete. Total queries: 1000\n",
|
||
"\n",
|
||
"[+] Discovery complete. Total queries used: 1000/1000\n",
|
||
"[+] Top 10 discovered words:\n",
|
||
" remember | impact: 0.217\n",
|
||
" make | impact: 0.159\n",
|
||
" come | impact: 0.134\n",
|
||
" doing | impact: 0.099\n",
|
||
" time | impact: 0.095\n",
|
||
" ... | impact: 0.093\n",
|
||
" did | impact: 0.083\n",
|
||
" ask | impact: 0.047\n",
|
||
" cos | impact: 0.043\n",
|
||
" want | impact: 0.034\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(\"\\n[*] Using three-phase discovery algorithm...\")\n",
|
||
"\n",
|
||
"# Build candidate vocabulary\n",
|
||
"candidate_words = build_candidate_vocabulary(X_train, y_train)\n",
|
||
"print(f\"[+] Built vocabulary of {len(candidate_words)} candidate words\")\n",
|
||
"\n",
|
||
"# Show budget allocation\n",
|
||
"allocation = estimate_budget_allocation(query_budget)\n",
|
||
"print(f\"\\n[*] Budget allocation:\")\n",
|
||
"for phase, budget in allocation.items():\n",
|
||
" print(f\" {phase:12}: {budget:4d} queries\")\n",
|
||
"\n",
|
||
"# Run three-phase discovery\n",
|
||
"discovered_words, combination_scores, total_queries = three_phase_discovery(\n",
|
||
" spam_test_messages[:50],\n",
|
||
" candidate_words,\n",
|
||
" budget=query_budget\n",
|
||
")\n",
|
||
"\n",
|
||
"print(f\"\\n[+] Discovery complete. Total queries used: {total_queries}/{query_budget}\")\n",
|
||
"print(f\"[+] Top 10 discovered words:\")\n",
|
||
"for word, score in discovered_words[:10]:\n",
|
||
" print(f\" {word:10} | impact: {score:.3f}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 30,
|
||
"id": "10362c0c-a816-4c51-b62f-165963b5ee3a",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[+] Top 5 word combinations:\n",
|
||
" ..., ask, doing | synergy: 0.000\n",
|
||
" ..., come, doing | synergy: 0.000\n",
|
||
" ..., ask, come | synergy: 0.000\n",
|
||
" ..., come, cos | synergy: 0.000\n",
|
||
" ..., doing, work | synergy: 0.000\n",
|
||
"\n",
|
||
"[+] Black-box attack complete. Total queries: 1000/1000\n",
|
||
"\n",
|
||
"============================================================\n",
|
||
"ATTACK SUMMARY\n",
|
||
"============================================================\n",
|
||
"Model Accuracy: 98.55%\n",
|
||
"Best White-box Evasion: 100.0% @ 20 words\n",
|
||
"Most Effective Word: 'lor' (reduces spam prob by 7.1%)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"if combination_scores:\n",
|
||
" print(f\"\\n[+] Top 5 word combinations:\")\n",
|
||
" top_combos = sorted(combination_scores.items(), key=lambda x: x[1], reverse=True)[:5]\n",
|
||
" for combo, score in top_combos:\n",
|
||
" combo_str = ', '.join(combo)\n",
|
||
" print(f\" {combo_str:30} | synergy: {score:.3f}\")\n",
|
||
"\n",
|
||
"# Update queries_used for compatibility\n",
|
||
"queries_used = total_queries\n",
|
||
"\n",
|
||
"# Test discovered words\n",
|
||
"blackbox_results = []\n",
|
||
"test_counts = [0, 5, 10, 15, 20, 25, 30] # Test with more words\n",
|
||
"\n",
|
||
"for num_words in test_counts:\n",
|
||
" if queries_used >= query_budget:\n",
|
||
" break\n",
|
||
"\n",
|
||
" selected = [w for w, _ in discovered_words[:num_words]]\n",
|
||
" evaded = 0\n",
|
||
" tested = 0\n",
|
||
"\n",
|
||
" # Test on a different subset of spam messages\n",
|
||
" eval_messages = spam_test_messages[30:50] # Different messages from discovery\n",
|
||
"\n",
|
||
" for msg in eval_messages:\n",
|
||
" if queries_used >= query_budget:\n",
|
||
" break\n",
|
||
"\n",
|
||
" aug = msg if num_words == 0 else msg + \" \" + \" \".join(selected)\n",
|
||
" vec = vectorizer.transform([aug])\n",
|
||
" prob = classifier.predict_proba(vec)[0][1] # spam prob\n",
|
||
" queries_used += 1\n",
|
||
"\n",
|
||
" if prob < 0.5: # evasion threshold\n",
|
||
" evaded += 1\n",
|
||
" tested += 1\n",
|
||
"\n",
|
||
" if tested > 0:\n",
|
||
" rate = (evaded / tested) * 100\n",
|
||
" blackbox_results.append({'num_words': num_words, 'evasion_rate': rate})\n",
|
||
" print(f\" Words: {num_words:2d} | Evasion: {rate:6.2f}% | Queries total: {queries_used}\")\n",
|
||
"\n",
|
||
"print(f\"\\n[+] Black-box attack complete. Total queries: {queries_used}/{query_budget}\")\n",
|
||
"\n",
|
||
"print(\"\\n\" + \"=\"*60)\n",
|
||
"print(\"ATTACK SUMMARY\")\n",
|
||
"print(\"=\"*60)\n",
|
||
"print(f\"Model Accuracy: {test_acc:.2%}\")\n",
|
||
"print(f\"Best White-box Evasion: {results_df['evasion_rate'].max():.1f}% @ {results_df.loc[results_df['evasion_rate'].idxmax(), 'num_words']} words\")\n",
|
||
"if blackbox_results:\n",
|
||
" bb_max = max(r['evasion_rate'] for r in blackbox_results)\n",
|
||
" print(f\"Best Black-box Evasion: {bb_max:.1f}% (with {queries_used} queries)\")\n",
|
||
"print(f\"Most Effective Word: '{word_impacts[0][0]}' (reduces spam prob by {word_impacts[0][1]:.1f}%)\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 31,
|
||
"id": "f0141b1b-f456-4865-82ab-511de60e8f41",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"[+] Results saved to attachments/results.json\n",
|
||
"[+] Attack demonstration complete!\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Save results\n",
|
||
"results = {\n",
|
||
" 'white_box': attack_results,\n",
|
||
" 'black_box': blackbox_results,\n",
|
||
" 'top_good_words': [(w, float(s)) for w, s, _, _ in top_good_words[:20]],\n",
|
||
" 'word_impacts': word_impacts[:10]\n",
|
||
"}\n",
|
||
"\n",
|
||
"with open(output_dir / \"results.json\", 'w') as f:\n",
|
||
" json.dump(results, f, indent=2)\n",
|
||
"\n",
|
||
"print(f\"\\n[+] Results saved to {output_dir / 'results.json'}\")\n",
|
||
"print(\"[+] Attack demonstration complete!\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "02e40ece-f285-4e0e-9a6b-b679f802a376",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": []
|
||
}
|
||
],
|
||
"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
|
||
}
|