Table of Contents
- Problem explanation — what is phishing detection?
- Importing libraries — your toolbox
- Loading & exploring the dataset
- Data cleaning & correlation analysis
- Preparing features & splitting the data
- Training the Random Forest classifier
- Model evaluation — accuracy, precision, recall & F1
- Feature importance — what makes a URL suspicious?
- ★ Real-world URL tester — scan a live URL
- ★ Batch URL scanner — scan multiple URLs at once
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:
Our features mix binary flags (0/1), integer counts, and lengths. Random Forest handles all of these natively without any scaling step.
Averaging across 100 trees smooths out the noise from any single tree that might overfit. It generalises well to URLs it has never seen.
The model calculates how much each feature contributed to its decisions — perfect for understanding why a URL was flagged.
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
1 = Phishing (malicious) | 0 = Legitimate (safe). This is what the model learns to predict.
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
| Library | Purpose in this project |
|---|---|
| numpy | Efficient numerical arrays. Used when we convert URL features into a vector the model can read. |
| pandas | Loads and manipulates the CSV dataset as a table (called a DataFrame). Think of it as Excel, but in Python. |
| matplotlib / seaborn | Plotting libraries. Used to visualise class distribution, feature correlations, and the confusion matrix. |
| sklearn.model_selection | Provides train_test_split to divide the data into training and test sets. |
| sklearn.ensemble | Contains the RandomForestClassifier — the core ML model we train. |
| sklearn.metrics | Provides evaluation tools: accuracy, precision, recall, F1 score, and the confusion matrix. |
| joblib | Saves and reloads the trained model to disk so you don't need to retrain it every time. |
| re / urllib.parse | Python built-ins for parsing URL strings and applying regular expressions — used in the live scanner. |
# ─── 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')
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.
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.
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
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.
# 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.
# ── 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()
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.
# ── 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.
# 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())
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.
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)
CLASS_LABEL. These are the inputs the model reads: URL length, number of dashes, whether HTTPS is used, etc.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.
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.
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.
# ── 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()}')
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
| Parameter | Value | What it means |
|---|---|---|
| n_estimators | 100 | Number of decision trees to build. More trees = more stable, but slower. 100 is a good starting point. |
| random_state | 42 | Fixes the randomness so your results are reproducible. Every student gets the same model. |
| n_jobs | -1 | Use all available CPU cores to train trees in parallel. Dramatically speeds up training on multi-core machines. |
What happens during .fit()?
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.
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')
phishing_rf_model.pkl, right-click it, and choose Download. Keep this file — it lets you deploy the scanner anywhere without retraining.
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
| Metric | Formula (simplified) | What it means in practice | Target |
|---|---|---|---|
| Accuracy | Correct predictions / Total URLs | Overall fraction of URLs classified correctly | > 97% |
| Precision | True Phishing / (True Phishing + False Alarms) | Of all URLs flagged as phishing, how many actually were? Low precision = too many false alarms. | > 95% |
| Recall | True Phishing / (True Phishing + Missed Phishing) | Of all actual phishing URLs, how many did we catch? Low recall = dangerous — sites slipping through. | Critical > 97% |
| F1-Score | Harmonic mean of Precision & Recall | Balanced 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.
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']))
# ── 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}')
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.
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
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))
• 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.
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.
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.
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.
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')
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.
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.
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
.txt file, one URL per line).scan_url(verbose=False) to get the prediction and confidence score silently (no long printout for each URL).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.
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')
urls_to_scan = open('urls.txt').read().splitlines()
This lets you scan hundreds of URLs with no code changes — just update the file.
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.
Going further — ideas for extending this project
- Fetch HTML features: Use the
requestslibrary to download the page's HTML, then extractPctExtHyperlinksand other HTML-based features for a more accurate scan. - Try other models: Compare your Random Forest against a
GradientBoostingClassifierorXGBClassifier. 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-whoislibrary can retrieve this.