DELIGHT Cybersecurity Workbook Series

Phishing Detection System

Enter the access code provided by your instructor to open this workbook.

Incorrect access code. Please try again.
DELIGHT Cybersecurity Workbook Series

Phishing Website Detection System

A complete, step-by-step guide to building a machine learning model that flags malicious URLs — from raw data to a live scanner you can test yourself.

Python Google Colab scikit-learn Random Forest Feature Engineering Live URL Analysis

Table of Contents

  1. Problem explanation — what is phishing detection?
  2. Importing libraries — your toolbox
  3. Loading & exploring the dataset
  4. Data cleaning & correlation analysis
  5. Preparing features & splitting the data
  6. Training the Random Forest classifier
  7. Model evaluation — accuracy, precision, recall & F1
  8. Feature importance — what makes a URL suspicious?
  9. ★ Real-world URL tester — scan a live URL
  10. ★ Batch URL scanner — scan multiple URLs at once
1
Problem explanation — what is phishing detection?
Intro

Phishing is one of the most common and damaging cyberattacks in the world. Criminals build fake websites that look almost identical to real ones — your bank, PayPal, Amazon, a government portal — and trick people into entering their username, password, or credit card details. The victim has no idea they were on a fake site until the damage is done.

Traditional anti-phishing methods relied on blocklists (lists of known bad URLs). The problem? New phishing sites are created constantly, often lasting only a few hours before being taken down and reappearing under a different domain. Blocklists can't keep up.

Machine learning changes this. Instead of remembering bad URLs, we teach a model to recognise the patterns that phishing URLs share — suspicious characters, unusual domain structures, lack of HTTPS, misleading keywords — and flag new, unseen URLs it has never encountered before.

The goal of this workbook

By the end of this workbook, you will have:

  • Loaded and explored a real phishing dataset with 11,000+ URLs and 48 features
  • Trained a Random Forest classifier that achieves >97% accuracy
  • Evaluated the model using professional security metrics (precision, recall, F1)
  • Understood which URL features are the strongest signals of a phishing attempt
  • Built a live URL scanner you can run on any link in seconds

Why Random Forest?

A Random Forest is an ensemble model — it trains hundreds of individual decision trees and combines their votes for a final answer. Think of it as asking 100 cybersecurity experts to each independently inspect a URL, then taking the majority vote. This approach has several advantages for phishing detection:

🔨 Handles mixed data

Our features mix binary flags (0/1), integer counts, and lengths. Random Forest handles all of these natively without any scaling step.

🛡 Resistant to noise

Averaging across 100 trees smooths out the noise from any single tree that might overfit. It generalises well to URLs it has never seen.

🔎 Built-in explainability

The model calculates how much each feature contributed to its decisions — perfect for understanding why a URL was flagged.

⚡ Fast to train

With n_jobs=-1, training uses all your CPU cores in parallel. The 11,000-row dataset trains in under 30 seconds on Colab.

The dataset

We use the Phishing Dataset for Machine Learning published on Kaggle by Shashwat Work. It contains 11,055 URLs labelled as phishing or legitimate, with 48 features extracted from each URL's structure and HTML content.

Download it from: kaggle.com → shashwatwork/phishing-dataset-for-machine-learning

CLASS_LABEL key:  1 = Phishing (malicious)  |  0 = Legitimate (safe). This is what the model learns to predict.
Environment: All code in this workbook runs inside Google Colab — a free, browser-based Python environment. You do not need to install anything on your computer. Open colab.research.google.com, create a new notebook, and paste each section's code into its own cell.
2
Importing libraries — your toolbox
Setup

Before writing any analysis or model code, we import every Python library we'll need. Think of this as laying out all your tools before starting a job. In Google Colab, most of these come pre-installed — no extra setup required.

What each library does

LibraryPurpose in this project
numpyEfficient numerical arrays. Used when we convert URL features into a vector the model can read.
pandasLoads and manipulates the CSV dataset as a table (called a DataFrame). Think of it as Excel, but in Python.
matplotlib / seabornPlotting libraries. Used to visualise class distribution, feature correlations, and the confusion matrix.
sklearn.model_selectionProvides train_test_split to divide the data into training and test sets.
sklearn.ensembleContains the RandomForestClassifier — the core ML model we train.
sklearn.metricsProvides evaluation tools: accuracy, precision, recall, F1 score, and the confusion matrix.
joblibSaves and reloads the trained model to disk so you don't need to retrain it every time.
re / urllib.parsePython built-ins for parsing URL strings and applying regular expressions — used in the live scanner.
Python — Cell 1
# ─── Core data science libraries ───────────────────────────────────────────
import numpy as np          # numerical arrays & math
import pandas as pd         # DataFrame (table) operations
import matplotlib.pyplot as plt  # plotting
import seaborn as sns        # enhanced statistical plots

# ─── Machine learning ──────────────────────────────────────────────────────
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    accuracy_score, precision_score,
    recall_score, f1_score,
    confusion_matrix, classification_report
)

# ─── Model persistence ─────────────────────────────────────────────────────
import joblib   # save and reload trained models

# ─── URL parsing (used in the live scanner, Sections 9 & 10) ───────────────
import re, urllib.parse

print('✓ All libraries imported successfully')
Missing a package? If you see an ImportError, run !pip install seaborn scikit-learn in a cell above this one. The ! prefix tells Colab to run it as a terminal command. In most Colab sessions everything is already available.
Student tip: Run this cell first every time you open the notebook. Python imports are not persistent between sessions — if Colab restarts, you must re-run all cells from the top.
3
Loading & exploring the dataset
EDA

Before training any model, a data scientist always does Exploratory Data Analysis (EDA) — getting familiar with the data, understanding its structure, and spotting any obvious problems early. Skipping this step is one of the most common beginner mistakes. Garbage in, garbage out.

Step 1 — Upload the CSV to Colab

Google Colab runs in the cloud, not on your computer, so we need to upload the dataset file manually. The files.upload() function opens a file picker dialog in your browser.

Python — Cell 2
from google.colab import files

# This opens a file picker — select Phishing_Legitimate_full.csv from your computer
uploaded = files.upload()

# Load the CSV into a pandas DataFrame
data = pd.read_csv('Phishing_Legitimate_full.csv')

# ── Quick overview ──────────────────────────────────────────
print(f'Dataset shape:      {data.shape}')       # (rows, columns)
print(f'Phishing URLs  (1): {(data["CLASS_LABEL"]==1).sum()}')
print(f'Legitimate URLs (0): {(data["CLASS_LABEL"]==0).sum()}')
data.head()                                        # preview first 5 rows
What to look for: The dataset should have around 11,055 rows and 49 columns (48 features + 1 label). The class split should be roughly 50/50 — an important fact, because imbalanced datasets can mislead models.

Step 2 — Inspect column types and statistics

data.info() shows column data types and whether any columns have missing values. data.describe() gives min, max, mean, and standard deviation for every numeric column — useful for spotting outliers or columns that are always 0.

Python — Cell 3
# Column data types and non-null counts
data.info()

# Statistical summary of every column
data.describe()

Step 3 — Visualise the class balance

A bar chart and pie chart tell us whether the dataset is balanced (roughly equal phishing vs legitimate). If one class is much larger than the other, the model could cheat by always predicting the majority class and still score high accuracy — without actually learning anything useful.

Python — Cell 4
# ── Class distribution visualisation ───────────────────────
fig, axes = plt.subplots(1, 2, figsize=(11, 4))

counts = data['CLASS_LABEL'].value_counts()

# Bar chart
axes[0].bar(['Legitimate (0)', 'Phishing (1)'],
            counts.values(), color=['#00e676', '#ff4d6d'])
axes[0].set_title('Class Distribution', fontweight='bold')
axes[0].set_ylabel('Number of URLs')

# Pie chart
axes[1].pie(counts.values(),
            labels=['Legitimate', 'Phishing'],
            colors=['#00e676', '#ff4d6d'],
            autopct='%1.1f%%', startangle=90)
axes[1].set_title('Class Balance', fontweight='bold')

plt.tight_layout()
plt.show()
Expected output: You should see close to a 50/50 split between phishing and legitimate URLs. This balanced dataset means our accuracy score will be a reliable measure of performance.
4
Data cleaning & correlation analysis
Preprocessing

Raw data is never perfectly clean. Before training, we need to remove columns that cannot help the model, check for missing values (which can break training), and understand which features are most likely to be useful.

Why do we drop the id column?

The id column is just a row number assigned when the dataset was created — it carries no information about whether a URL is malicious or safe. If we left it in, the model would try to learn from it and find nothing meaningful, adding noise. Always drop ID and timestamp columns before training.

Python — Cell 5
# ── Step 1: Drop the ID column — it is not a predictive feature ──────────
data.drop('id', axis=1, inplace=True)
print(f'Columns after dropping id: {data.shape[1]}')

# ── Step 2: Check for missing values ─────────────────────────────────────
# Missing values must be handled before training — scikit-learn will throw
# an error if NaNs are present in the feature matrix.
missing = data.isnull().sum()
print('\nMissing values:')
print(missing[missing > 0] if missing.any() else '✓ None found — dataset is clean')

# ── Step 3: Sanity check on max values ───────────────────────────────────
# Most features should be binary (0 or 1). Any unexpectedly large max
# value might indicate a data entry error or an outlier to investigate.
print('\nMax value per column (binary features should max at 1):')
print(data.max(axis=0))

Correlation analysis — which features matter most?

Correlation measures how much a feature moves in the same direction as the class label. A feature with high absolute correlation is a strong candidate for phishing detection. We visualise the top 20 to get an intuition for the data before we even train anything.

Note: Correlation alone does not decide what the model uses. The Random Forest will discover complex non-linear relationships that simple correlation can't capture. But this chart gives us a useful early signal.

Python — Cell 6
# Compute absolute correlation of every feature with the class label
# We use .abs() because a strong negative correlation is just as useful
# as a strong positive one — it still means the feature is informative.
corr = data.corr()['CLASS_LABEL'].abs().sort_values(ascending=False)

# Plot top 20 (skip index 0 which is CLASS_LABEL itself)
plt.figure(figsize=(10, 6))
corr[1:21].plot(kind='barh', color='#00d4ff')
plt.title('Top 20 Features by Absolute Correlation with CLASS_LABEL', fontweight='bold')
plt.xlabel('Absolute Correlation Coefficient')
plt.tight_layout()
plt.show()

# Print top 10 numerically
print('\nTop 10 correlated features:\n')
print(corr[1:11].to_string())
What to expect: Features like PctExtHyperlinks, PctExtResourceUrls, SubdomainLevel, and NoHttps usually top this chart. These represent the proportion of external links in the page, the subdomain depth, and whether the site uses HTTPS — all classic phishing red flags.
5
Preparing features & splitting the data
Train / Test Split

This is a critical step that is easy to get wrong. We need to separate two things: the features (what the model uses to learn) from the label (what it is trying to predict). Then we divide the data into a training set and a test set.

Features (X) vs Label (y)

X
Feature matrix — all 48 columns except CLASS_LABEL. These are the inputs the model reads: URL length, number of dashes, whether HTTPS is used, etc.
y
Target vector — just the CLASS_LABEL column (0 or 1). This is what the model tries to predict. You never let the model see y during training input — only during the learning phase.

Why split into training and test sets?

Imagine studying for an exam using the exact same questions that will appear on the test — you'd do perfectly, but you wouldn't actually have learned the subject. Machine learning works the same way. If we evaluate our model on the same data it trained on, the score will be unrealistically high. We hold back 20% of the data (the test set) and the model never sees it until evaluation time. This gives an honest measure of how well it generalises to new, unseen URLs.

Why stratify=y? Without stratification, the random split might accidentally put 60% of all phishing URLs into the training set and only 40% into the test set. stratify=y guarantees both halves mirror the original 50/50 class balance, making evaluation reliable.
Why random_state=42? Splitting is random, but using a fixed seed (42 is conventional) makes the split reproducible. Every student running this code gets the exact same split, so results can be compared directly.
Python — Cell 7
# ── Separate features from target ─────────────────────────────────────────
X = data.drop('CLASS_LABEL', axis=1)   # all columns except the label
y = data['CLASS_LABEL']                 # only the label column

# Save the column names — we need this later when scanning live URLs
# to ensure we build the feature vector in the exact same order the model expects
FEATURE_NAMES = X.columns.tolist()

print(f'Feature matrix X: {X.shape}')        # should be (11055, 48)
print(f'Target vector y:  {y.shape}')        # should be (11055,)
print(f'Number of features: {len(FEATURE_NAMES)}')

# ── 80 / 20 train-test split ──────────────────────────────────────────────
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.20,       # 20% held out for testing
    random_state=42,      # reproducible split
    stratify=y             # preserve class balance in both halves
)

print(f'\nTraining samples: {X_train.shape[0]}')
print(f'Test samples:     {X_test.shape[0]}')
print(f'\nTraining label split (should be ~50/50):\n{y_train.value_counts()}')
6
Training the Random Forest classifier
Model Training

This is where the actual machine learning happens. We initialise a RandomForestClassifier, call .fit() on the training data, and the algorithm builds 100 decision trees — each one trained on a random subset of the training rows and features. The whole process takes about 5–20 seconds on Colab.

Understanding the parameters

ParameterValueWhat it means
n_estimators100Number of decision trees to build. More trees = more stable, but slower. 100 is a good starting point.
random_state42Fixes the randomness so your results are reproducible. Every student gets the same model.
n_jobs-1Use all available CPU cores to train trees in parallel. Dramatically speeds up training on multi-core machines.

What happens during .fit()?

1
For each of the 100 trees, the algorithm randomly samples (with replacement) a subset of training rows. This is called bootstrap sampling.
2
At each node of the tree, it randomly selects a subset of features to consider for the best split. This forces trees to be diverse and prevents one dominant feature from controlling everything.
3
Each tree grows until it perfectly classifies its training rows (or hits a depth limit). These individual trees are somewhat overfit, but their average is not.
4
At prediction time, all 100 trees independently vote. The class with the most votes wins. This majority-vote mechanism is why Random Forests are so reliable.

Saving the model with joblib

Training takes time. Once done, we save the model to a .pkl file on Colab's disk. The live scanner in Section 9 loads this file and reuses the trained model instantly, without retraining. If Colab restarts, you only need to re-run the training cell once, then download the .pkl file to keep permanently.

Python — Cell 8
from sklearn.ensemble import RandomForestClassifier
import joblib

# ── Initialise the model ──────────────────────────────────────────────────
rfc = RandomForestClassifier(
    n_estimators=100,     # build 100 individual decision trees
    random_state=42,      # reproducible results
    n_jobs=-1             # use all CPU cores (speeds up training)
)

# ── Train the model ───────────────────────────────────────────────────────
# .fit() is where all the learning happens. This may take 5-20 seconds.
print('⏳ Training Random Forest on 80% of the data...')
rfc.fit(X_train, y_train)
print('✓ Training complete!')

# ── Generate predictions on the test set ──────────────────────────────────
# preds gives the hard label (0 or 1)
# pred_proba gives the probability of phishing (0.0 to 1.0) — useful for tuning
preds      = rfc.predict(X_test)
pred_proba = rfc.predict_proba(X_test)[:, 1]   # column 1 = probability of class 1 (phishing)

# ── Save the trained model to disk ────────────────────────────────────────
# Download this file from the Colab Files panel (left sidebar → folder icon)
# to reuse it without retraining.
joblib.dump(rfc, 'phishing_rf_model.pkl')
print('💾 Model saved as phishing_rf_model.pkl')
Tip: To download the saved model, click the folder icon in the left sidebar of Colab, find phishing_rf_model.pkl, right-click it, and choose Download. Keep this file — it lets you deploy the scanner anywhere without retraining.
7
Model evaluation — accuracy, precision, recall & F1
Metrics

Accuracy alone is not enough to evaluate a security model. A model that always guesses "legitimate" on a balanced dataset would score 50% accuracy while being completely useless. We need metrics that tell us how well it handles each class — especially how often it misses actual phishing sites.

The four key metrics

MetricFormula (simplified)What it means in practiceTarget
AccuracyCorrect predictions / Total URLsOverall fraction of URLs classified correctly> 97%
PrecisionTrue Phishing / (True Phishing + False Alarms)Of all URLs flagged as phishing, how many actually were? Low precision = too many false alarms.> 95%
RecallTrue Phishing / (True Phishing + Missed Phishing)Of all actual phishing URLs, how many did we catch? Low recall = dangerous — sites slipping through.Critical > 97%
F1-ScoreHarmonic mean of Precision & RecallBalanced single score. Useful when precision and recall are both important.> 96%

Understanding the confusion matrix

The confusion matrix is a 2x2 table that breaks down predictions into four categories:

  • True Positive (TP): Phishing URL correctly flagged as phishing ✓
  • True Negative (TN): Legitimate URL correctly classified as safe ✓
  • False Positive (FP): Legitimate URL wrongly flagged as phishing — a false alarm
  • False Negative (FN): Phishing URL wrongly classified as safe — a missed attack ⚠️

In cybersecurity, False Negatives are more dangerous than False Positives. A false alarm is annoying; a missed phishing site can lead to credential theft or financial loss. This is why recall is the most critical metric here.

Python — Cell 9
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns

# ── Compute the four core metrics ──────────────────────────────────────────
acc  = accuracy_score(y_test, preds)
prec = precision_score(y_test, preds)
rec  = recall_score(y_test, preds)
f1   = f1_score(y_test, preds)

print('═' * 45)
print(f'  Accuracy:  {acc:.4f}  ({acc*100:.2f}%)')
print(f'  Precision: {prec:.4f}  — of flagged URLs, this many are real phishing')
print(f'  Recall:    {rec:.4f}  — of all phishing URLs, this many were caught')
print(f'  F1-Score:  {f1:.4f}  — balanced precision/recall score')
print('═' * 45)

# ── Full per-class breakdown ───────────────────────────────────────────────
print('\n📊 Classification Report:')
print(classification_report(y_test, preds,
                             target_names=['Legitimate', 'Phishing']))
Python — Cell 10
# ── Confusion matrix heatmap ───────────────────────────────────────────────
# Each cell shows the count: rows = actual class, columns = predicted class
cm = confusion_matrix(y_test, preds)

plt.figure(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['Predicted: Legit', 'Predicted: Phishing'],
            yticklabels=['Actual: Legit', 'Actual: Phishing'])
plt.title('Confusion Matrix — Phishing Detector', fontweight='bold')
plt.tight_layout()
plt.show()

# ── Explain the confusion matrix cells ────────────────────────────────────
tn, fp, fn, tp = cm.ravel()
print(f'\nTrue Negatives  (correctly marked safe):     {tn}')
print(f'False Positives (safe URLs wrongly flagged): {fp}  ← false alarms')
print(f'False Negatives (phishing URLs missed!):     {fn}  ← dangerous misses')
print(f'True Positives  (phishing correctly caught):  {tp}')
Security trade-off: You can adjust the classification threshold using pred_proba. Lowering it (e.g. flag as phishing if probability > 0.3 instead of the default 0.5) increases recall but reduces precision — you catch more phishing sites but produce more false alarms. Raising it does the opposite. The right threshold depends on the application.
8
Feature importance — what makes a URL suspicious?
Explainability

One of the biggest advantages of Random Forest over deep learning or neural networks is explainability. After training, the model can tell you exactly which features contributed most to its decisions across all 100 trees. This is called feature importance.

Each feature receives an importance score based on how much it reduced prediction errors across all the splits in all trees. Features with higher scores were more useful for distinguishing phishing from legitimate URLs.

Why does feature importance matter for security?

Understanding which signals the model relies on lets security teams:

  • Explain model decisions to non-technical stakeholders ("it flagged this URL because it uses 3 subdomains, no HTTPS, and contains the word 'login'")
  • Identify new phishing tactics if the model's top features shift over time
  • Design better URL filtering rules for firewall policies
  • Spot potential weaknesses — if phishers learn which features matter, they might try to disguise them
Python — Cell 11
import pandas as pd

# ── Build a ranked DataFrame of all feature importances ───────────────────
feat_df = pd.DataFrame({
    'feature':    FEATURE_NAMES,
    'importance': rfc.feature_importances_   # built-in Random Forest attribute
}).sort_values('importance', ascending=False).reset_index(drop=True)

# ── Visualise the top 20 ──────────────────────────────────────────────────
top20  = feat_df.head(20)
# Highlight the top 5 in bright cyan; the rest in a darker shade
colors = ['#00d4ff' if i < 5 else '#2a6080' for i in range(20)]

plt.figure(figsize=(11, 7))
plt.barh(top20['feature'][::-1],
         top20['importance'][::-1],
         color=colors[::-1])
plt.title('Top 20 Most Important Features for Phishing Detection',
          fontweight='bold', fontsize=13)
plt.xlabel('Importance Score (contribution to predictions across all trees)')
plt.tight_layout()
plt.show()

# ── Print the top 10 as a table ───────────────────────────────────────────
print('\nTop 10 most important features:\n')
print(feat_df.head(10).to_string(index=False))
Top signals to understand:
PctExtHyperlinks — Phishing pages link heavily to external sites to make them look real.
SubdomainLevel — Attackers use deep subdomains like login.verify.secure.paypal.evil.com.
NoHttps — Legitimate sites use HTTPS; many phishing sites skip it.
NumDashInHostname — Hyphens like paypal-secure-verify.com are a classic phishing pattern.
NumSensitiveWords — Words like "login", "verify", "secure", "account" in a URL are suspicious.
Real-world URL tester — scan a live URL
Live Testing

This is where the project becomes truly practical. We write a Python function that takes any raw URL string, extracts the same 48 features the model was trained on, and feeds them to the saved Random Forest model for an instant verdict. No external API, no internet lookup — just the URL and the model you trained.

Important design note: The original dataset contained both URL-based and HTML-based features. In URL-only mode (when you only have the URL string, not the webpage's source code), we set HTML features to 0. This is a known limitation — the model works well, but HTML-based features like PctExtHyperlinks would make it even more accurate if you could also fetch and parse the page.

Step 1 — The URL feature extractor function

This function parses the URL using Python's built-in urllib.parse and computes every feature in the same order as the training dataset. The column order must match exactly — if you swap two columns, the model reads the wrong values and gives garbage predictions.

Python — Cell 12
import re, urllib.parse

def extract_url_features(url):
    """
    Extract phishing-detection features from a raw URL string.
    Returns a dict of feature name → value, aligned to the training
    dataset's column order so the model can read it correctly.
    HTML-based features are defaulted to 0 (URL-only mode).
    """

    try:
        parsed = urllib.parse.urlparse(url)
    except Exception:
        return {}

    # Split URL into its components for targeted analysis
    domain = parsed.netloc.replace('www.', '')   # hostname without www
    path   = parsed.path                            # everything after the domain
    query  = parsed.query                           # everything after the ?

    # Known brand names — phishing sites often embed these in subdomains/paths
    BRAND_WORDS = ['paypal','ebay','google','amazon','microsoft','apple','chase']
    # Sensitive keywords that phishing URLs commonly contain
    SENS_WORDS  = ['secure','account','update','login','verify','banking','password']

    f = {}

    # ── URL structure features ─────────────────────────────────────────────
    f['NumDots']           = url.count('.')              # total dots in full URL
    f['SubdomainLevel']    = max(0, domain.count('.') - 1)  # subdomain depth (0 = none)
    f['PathLevel']         = len([p for p in path.split('/') if p])
    f['UrlLength']         = len(url)                     # total URL character length
    f['NumDash']           = url.count('-')              # dashes anywhere in URL
    f['NumDashInHostname'] = domain.count('-')           # dashes only in domain
    f['AtSymbol']          = 1 if '@' in url else 0     # @ tricks browsers into ignoring the domain
    f['TildeSymbol']       = 1 if '~' in url else 0     # tilde often signals old CGI phishing pages
    f['NumUnderscore']     = url.count('_')
    f['NumPercent']        = url.count('%')              # URL-encoded chars — used to obfuscate
    f['NumQueryComponents'] = len(urllib.parse.parse_qs(query))
    f['NumAmpersand']      = url.count('&')
    f['NumHash']           = url.count('#')
    f['NumNumericChars']   = sum(c.isdigit() for c in url)
    f['NoHttps']           = 0 if parsed.scheme == 'https' else 1  # 1 = no HTTPS (suspicious)
    f['RandomString']      = 1 if re.search(r'[a-z]{10,}', domain) else 0
    f['IpAddress']         = 1 if re.match(r'\d+\.\d+\.\d+\.\d+', domain) else 0  # IP instead of domain name
    f['DomainInSubdomains'] = 1 if any(b in domain for b in BRAND_WORDS) else 0
    f['DomainInPaths']     = 1 if any(b in path for b in BRAND_WORDS) else 0
    f['HttpsInHostname']   = 1 if 'https' in domain else 0   # e.g. "https-paypal.com" — deceptive
    f['HostnameLength']    = len(domain)
    f['PathLength']        = len(path)
    f['QueryLength']       = len(query)
    f['DoubleSlashInPath'] = 1 if '//' in path else 0
    f['NumSensitiveWords'] = sum(1 for w in SENS_WORDS if w in url.lower())
    f['UrlLengthRT']      = 1 if len(url) > 75 else 0   # binary: is URL suspiciously long?
    f['SubdomainLevelRT']  = f['SubdomainLevel']          # same value, used as a separate feature

    # ── HTML-based features — default to 0 in URL-only mode ───────────────
    # These features require fetching and parsing the webpage's HTML source.
    # Since we only have the URL here, we set them to 0 (neutral).
    # The model still works well — it just can't use these signals.
    for col in ['PctExtHyperlinks','PctExtResourceUrls','ExtFavicon',
                 'InsecureForms','RelativeFormAction','ExtFormAction',
                 'AbnormalFormAction','PctNullSelfRedirectHyperlinks',
                 'FrequentDomainNameMismatch','FakeLinkInStatusBar',
                 'RightClickDisabled','PopUpWindow','SubmitInfoToEmail',
                 'IframeOrFrame','MissingTitle','ImagesOnlyInForm',
                 'PctExtNullSelfRedirectHyperlinksRT']:
        f[col] = 0

    return f

print('✓ extract_url_features() is ready')

Step 2 — The scan function

The scan_url() function ties everything together. It calls the extractor, assembles the feature vector in the correct column order using FEATURE_NAMES (which we saved in Section 5), runs it through the model, and prints a human-readable verdict with a confidence score.

Python — Cell 13
import joblib
import numpy as np

# Load the model we saved in Section 6
# If Colab restarted, retrain first (run Sections 2–6 again)
model = joblib.load('phishing_rf_model.pkl')

def scan_url(url, verbose=True):
    """
    Scan a single URL using the trained Random Forest model.
    
    Parameters:
        url     : the full URL string (include https:// or http://)
        verbose : if True, prints a human-readable verdict with feature details
    
    Returns:
        (prediction, confidence)
        prediction  : 0 = Legitimate, 1 = Phishing
        confidence  : probability of being phishing (0.0 to 1.0)
    """
    # Extract features from the URL string
    features = extract_url_features(url)

    # Build the feature vector in the EXACT column order the model expects
    vec = np.array([features.get(col, 0) for col in FEATURE_NAMES]).reshape(1, -1)

    # Get the hard prediction (0 or 1) and the phishing probability
    pred = model.predict(vec)[0]
    conf = model.predict_proba(vec)[0][1]   # probability of class 1 (phishing)

    if verbose:
        verdict = '🚨 PHISHING' if pred == 1 else '✓  LEGITIMATE'
        print(f'\n{"─"*55}')
        print(f'URL      : {url}')
        print(f'Verdict  : {verdict}')
        print(f'Confidence (phishing probability): {conf*100:.1f}%')
        print(f'\nKey features that influenced this result:')
        print(f'  HTTPS:            {"Yes ✓" if not features["NoHttps"] else "NO — suspicious ⚠️"}')
        print(f'  URL length:       {features["UrlLength"]} chars')
        print(f'  Domain dashes:    {features["NumDashInHostname"]}')
        print(f'  Subdomain depth:  {features["SubdomainLevel"]} (0 = no subdomains)')
        print(f'  Sensitive words:  {features["NumSensitiveWords"]}')
        print(f'  IP as domain:     {"YES — suspicious ⚠️" if features["IpAddress"] else "No ✓"}')
        print(f'  Brand in subdomain: {"YES — suspicious ⚠️" if features["DomainInSubdomains"] else "No ✓"}')
        print(f'{"─"*55}')

    return pred, conf

# ── Test the scanner on two example URLs ─────────────────────────────────
scan_url('https://www.google.com')
scan_url('http://secure-paypal-verify.suspicious-site.com/login')
Expected results: google.com should be classified as LEGITIMATE with high confidence. The fake PayPal URL — which uses HTTP (no HTTPS), hyphens in the domain, and sensitive words ("secure", "verify", "login") — should be flagged as PHISHING.
🔍 Interactive Feature Analyser — Try it in your browser

Enter any URL below to instantly see the features that would be sent to the model. This browser demo uses a heuristic scoring system — run the full Python model in Colab for the authoritative verdict.

Batch URL scanner — scan multiple URLs at once
Bulk Analysis

Real security operations don't scan one URL at a time. Security analysts work with threat feeds, browser logs, email link extracts, or user-submitted suspicious URLs — often hundreds at a time. This section builds a batch scanner that processes a list of URLs and exports the results to a CSV file for documentation and reporting.

How the batch scanner works

1
We define a list of URLs to scan (or read them from a .txt file, one URL per line).
2
We loop through each URL, calling scan_url(verbose=False) to get the prediction and confidence score silently (no long printout for each URL).
3
Results are collected in a list and printed in a compact table format.
4
The full results are exported to scan_results.csv — a file you can open in Excel or Google Sheets for further analysis and reporting.

We also wrap each scan in a try/except block. In real-world data, you'll encounter malformed URLs, empty lines, or network issues. Graceful error handling ensures one bad URL doesn't crash the entire batch.

Python — Cell 14
import pandas as pd

# ── Define your list of URLs to scan ──────────────────────────────────────
# Add your own URLs here, or read from a file (see tip below)
urls_to_scan = [
    'https://www.google.com',
    'https://www.amazon.com',
    'http://secure-paypa1-login.verify-account.com/login',  # note typo 'paypa1' — common trick
    'http://192.168.1.1/update-credentials',              # raw IP address — suspicious
    'https://github.com',
    'http://microsoft-account-verify.tk/signin',          # .tk domain, suspicious keywords
    'https://www.wikipedia.org',
    'http://banking-update.secure-login.verify.net/account', # stacked sensitive words
]

# ── Scan each URL and collect results ────────────────────────────────────
results = []
print(f"{'#':<4} {'VERDICT':<14} {'CONF':>6}  URL")
print('─' * 80)

for i, url in enumerate(urls_to_scan, 1):
    try:
        pred, conf = scan_url(url, verbose=False)   # silent mode for batch
        verdict = 'PHISHING' if pred == 1 else 'LEGITIMATE'
        icon    = '🚨' if pred == 1 else '✓'
        results.append({
            'url':        url,
            'verdict':    verdict,
            'confidence': round(conf, 4)
        })
        print(f"{i:<4} {icon} {verdict:<12} {conf*100:>5.1f}%  {url[:55]}")
    except Exception as e:
        # Don't crash on a single bad URL — log the error and move on
        print(f"{i:<4} ⚠️  ERROR        {'N/A':>6}  {str(e)[:40]}")

# ── Summary statistics ────────────────────────────────────────────────────
ph  = sum(1 for r in results if r['verdict'] == 'PHISHING')
leg = len(results) - ph
print(f'\n📊 Summary: {leg} Legitimate  |  {ph} Phishing  |  {len(results)} total scanned')

# ── Export results to CSV ─────────────────────────────────────────────────
# Download this from the Colab Files panel for your report
pd.DataFrame(results).to_csv('scan_results.csv', index=False)
print('💾 Results saved to scan_results.csv')
Loading URLs from a file: If you have a text file with one URL per line, replace the list definition above with:
urls_to_scan = open('urls.txt').read().splitlines()
This lets you scan hundreds of URLs with no code changes — just update the file.
📊 Interactive Batch URL Analyser — Try it here

Paste one URL per line below. The browser-based heuristic engine analyses each instantly. For authoritative results, always use the Python Random Forest model above.

Classroom note: The in-browser scanner uses a simplified heuristic model for instant visual feedback. It does NOT use the 100-tree Random Forest — that runs only in Python/Colab. The browser demo is for teaching and exploration. Never use it for real security decisions.

Going further — ideas for extending this project

  • Fetch HTML features: Use the requests library to download the page's HTML, then extract PctExtHyperlinks and other HTML-based features for a more accurate scan.
  • Try other models: Compare your Random Forest against a GradientBoostingClassifier or XGBClassifier. Which gets better recall?
  • Tune the threshold: Plot a precision-recall curve and find the confidence threshold that gives the best recall without flooding users with false alarms.
  • Deploy as a web app: Use Flask or Streamlit to wrap scan_url() in a simple web interface that anyone can use without Colab.
  • WHOIS integration: Domain age is a powerful feature — new domains (under 6 months old) are far more likely to be phishing sites. The python-whois library can retrieve this.