PhishLens

Backend CI Extension CI Security CI codecov

Project site · Architecture · Privacy policy · Threat model · ML methodology · Demo script

PhishLens is a defensive Chrome extension and FastAPI backend for explainable phishing risk analysis in real time.

It combines local URL heuristics, privacy-preserving DOM signals, optional PhishTank threat intelligence, backend-side TLS certificate inspection with Certificate Transparency lookups, RDAP domain-age checks, and an optional machine learning model. The project is built as a practical cybersecurity portfolio project with clear safety boundaries.

PhishLens on a cloned PayPal login at paypal-verify-account.net: the toolbar icon shows a red badge, the popup rates the page Dangerous 95/100 with backend enrichment, and the in-page warning lists the reasons and tells the user not to enter a password

PhishLens flagging a cloned PayPal login on a look-alike domain (paypal-verify-account.net) and explaining why, both in the popup and in the in-page warning. Both sites are local demo pages and the domain is not registered. The walkthrough below goes from the phishing email to the in-page warning:

PhishLens walkthrough in six slides: a fake "PayPal Security" email whose button really links to paypal-verify-account.net, the PhishLens popup rating the cloned login Dangerous 95 with the in-page warning behind it, the per-category signal breakdown (URL 32/35, page structure 30/30, then threat intel, TLS, domain age, and ML 0), and the in-page warning telling the user not to enter a password

Quick Start

# 1. Clone and set up the backend
git clone https://github.com/JuanCardesa/PhishLens.git && cd PhishLens
cp .env.example .env
python -m venv .venv && source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install -r backend/requirements-dev.txt

# 2. Start the backend and leave it running
uvicorn app.main:app --app-dir backend --reload

# 3. In a second terminal, from the repository root, build the extension
cd extension && npm install && npm run build

# 4. Load it in Chrome: chrome://extensions → Developer mode → Load unpacked → extension/dist

The extension also works without the backend; see Development Setup for Windows commands and Local Demo for pages that show each risk label. To rebuild the ML dataset and retrain the model: python ml/datasets/build_dataset.py && python ml/train_model.py.

Features

Screenshots

The popup on the local demo pages (demo/pages/), captured from the built extension with node extension/scripts/record-demo.mjs --docs.

Safe page Suspicious page Dangerous page Backend unavailable
Safe demo page: Safe 8, backend enriched, the only URL signal is the lack of HTTPS Suspicious demo page: Suspicious 48, backend enriched, URL 5/35 for plain HTTP and page structure 26/30 for a sign-in form with a password field that posts to another domain Dangerous demo page: Dangerous 100, backend enriched, URL 24/35 and page structure 30/30, plus the demo threat source Safe demo page with the backend unreachable: Safe 8, the same local score, confidence shown as "Heuristic" instead of a percentage, and a banner listing the checks that did not run

With the backend unreachable, the safe page keeps the same score (8). The local score is the base the backend adds to, so the two only differ by what the backend finds.

The warning overlay on the dangerous demo page. That page also matches the localhost-only demo threat source (see Local Demo):

Danger overlay: "High-risk phishing signals detected", risk score 100/100, the top reasons, and a Continue button

Architecture

Chrome page
  -> content script extracts non-sensitive DOM signals
  -> service worker scores the page locally and sets the toolbar badge
  -> popup computes the local score (URL + DOM points scaled to 0-100) and shows it
  -> popup optionally calls FastAPI /analyze
  -> backend starts from that same score and adds threat intel, TLS + Certificate
     Transparency, domain age, and an ML adjustment (-5 to +12)
  -> popup shows score, label, confidence, risk breakdown, and feedback controls
     (confidence is a percentage only when the ML model contributed, "Heuristic" otherwise)
  -> dangerous results can display a dismissible page overlay
  -> development diagnostics expose counters only

The extension never sends full HTML, form values, passwords, or typed emails. The backend receives only the current URL and technical DOM features.

Stack

Development Setup

Backend

Linux / macOS

python -m venv .venv
source .venv/bin/activate
pip install -r backend/requirements-dev.txt
uvicorn app.main:app --app-dir backend --reload

Windows (PowerShell)

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r backend/requirements-dev.txt
.\.venv\Scripts\python.exe -m uvicorn app.main:app --app-dir backend --reload

Health check:

curl http://localhost:8000/health

Interactive API documentation is available at http://localhost:8000/docs while the backend is running.

Extension

cd extension
npm install
npm run build

Load extension/dist in Chrome:

  1. Open chrome://extensions.
  2. Enable Developer mode.
  3. Select “Load unpacked”.
  4. Choose the extension/dist folder.

The extension works locally without the backend. When the backend is available at http://localhost:8000, the popup enriches the local result with backend analysis.

Extension settings are available from the popup settings button or Chrome extension details page. The default backend is http://localhost:8000.

Tests

Backend (the same checks as Backend CI):

cd backend
pytest tests
ruff check app tests ../demo
mypy app

Extension (the same checks as Extension CI):

cd extension
npm run lint                        # tsc --noEmit
npm run test
npm run build
npx playwright install chromium     # first run only
npm run test:e2e                    # loads the built extension in real Chromium

Dependency audits (run by Security CI, not Extension CI):

cd extension && npm audit --audit-level=high
pip install pip-audit && pip-audit -r backend/requirements.txt

ML:

python ml/train_model.py
python ml/evaluate_model.py
python ml/evaluate_cv_metrics.py                 # the metrics in "ML Model Performance" below
python ml/evaluate_ml_adjustment.py --temporal   # evidence behind the ML score adjustment (downloads data)

Docker:

docker compose build
docker compose up backend

Review automation:

python scripts/ci/pr_guardian.py --all

Demo readiness:

python scripts/dev/check_demo.py

Configuration

Copy .env.example to .env for local overrides. No real keys are committed.

Variable Default Description
PHISHTANK_API_KEY (empty) Optional PhishTank application key. Omit to skip threat intel.
PHISHTANK_USER_AGENT phishtank/phishlens-demo User-Agent sent with PhishTank requests (required by their API).
PHISHLENS_ALLOWED_ORIGINS http://localhost:5173 Comma-separated CORS origins. Add chrome-extension://* only for local dev.
PHISHLENS_CHROME_EXTENSION_IDS (empty) Comma-separated Chrome extension IDs for production CORS.
PHISHLENS_ENABLE_THREAT_INTEL true Enable/disable PhishTank lookups.
PHISHLENS_ENABLE_TLS_ANALYSIS true Enable/disable backend TLS certificate inspection.
PHISHLENS_ENABLE_CT_LOG_LOOKUP true Enable/disable Certificate Transparency log lookups (crt.sh) as an additional TLS risk signal.
PHISHLENS_ENABLE_DOMAIN_AGE_LOOKUP true Enable/disable RDAP domain-registration-age lookups.
PHISHLENS_EXTERNAL_TIMEOUT_SECONDS 4.0 Timeout for external backend enrichment calls such as PhishTank, RDAP, and crt.sh.
PHISHLENS_MODEL_PATH app/models/phishlens_model.joblib Path to a trained joblib model artifact.
PHISHLENS_BRAND_DOMAINS_PATH app/data/brand_domains.json Path to the curated brand-domain list used for typosquat/brand-impersonation detection.
PHISHLENS_ENABLE_DIAGNOSTICS true Expose aggregate counters at GET /diagnostics.
PHISHLENS_DIAGNOSTICS_TOKEN (empty) When set, GET /diagnostics requires X-Diagnostics-Token: <value>.
PHISHLENS_ENABLE_RATE_LIMITING true Enable in-memory sliding-window rate limits.
PHISHLENS_ANALYZE_RATE_LIMIT 60 Max /analyze requests per window per IP.
PHISHLENS_REPORT_RATE_LIMIT 20 Max /report requests per window per IP.
PHISHLENS_RATE_LIMIT_WINDOW_SECONDS 60 Rate-limit window in seconds.
PHISHLENS_BEHIND_PROXY false Trust X-Forwarded-For when behind nginx / Caddy / ALB.
PHISHLENS_FEEDBACK_DB_PATH feedback.db SQLite path for feedback metadata. Set to "" to disable.
PHISHLENS_ENABLE_DEMO_THREAT_SOURCE false Enable localhost-only dangerous demo signal.

Extension settings:

Ethical And Privacy Notice

PhishLens is defensive only. It must not collect credentials, typed emails, private form content, or full page HTML. It is a risk-assistance tool, not a phishing verdict authority. False positives and false negatives are expected, especially outside the limited dataset and signal coverage documented below.

Local Demo

Run the backend, demo pages, and extension locally, each in its own terminal:

# Terminal 1: backend with the localhost-only demo threat source
# (Linux / macOS: PHISHLENS_ENABLE_DEMO_THREAT_SOURCE=true uvicorn app.main:app --app-dir backend --reload)
$env:PHISHLENS_ENABLE_DEMO_THREAT_SOURCE="true"
.\.venv\Scripts\python.exe -m uvicorn app.main:app --app-dir backend --reload

# Terminal 2: demo pages on port 8080
python demo/serve_demo.py

# Terminal 3: build the extension
cd extension
npm run build

Load extension/dist in Chrome and visit:

Each page shows its label with or without the backend, and the E2E test checks all three. PHISHLENS_ENABLE_DEMO_THREAT_SOURCE=true adds a threat-intelligence match to the dangerous page so that category is visible without a PhishTank key; it only matches localhost URLs containing phishlens-demo-dangerous. Use localhost rather than 127.0.0.1: the backend rejects private IP literals as an SSRF safeguard.

Package the extension:

cd extension
npm run package

The zip is written to extension/release/.

ML Model Performance

78.5% precision / 81.7% recall (5-fold stratified CV, N=1,200) for the shipped RandomForestClassifier on the committed PhishTank + Tranco dataset, with a 22% false-positive rate. This is a modest, URL-shape-only model. That is why it only nudges the rule-based score (−5 to +12) and never decides a verdict on its own.

These numbers used to read 99.0% precision / 0.8% false positives. That was a dataset artifact: 599 of the 600 legitimate URLs had no subdomain, so the model learned “has a subdomain” as phishing. Once legitimate URLs got realistic subdomains, and www. stopped counting as one, precision fell to its honest level (see Lessons Learned).

Metric As-is (N=1,200) Deduplicated (N=1,052)
Precision 0.785 0.786
Recall 0.817 0.823
F1 0.801 0.804
False-positive rate 0.223 0.214
ROC-AUC 0.886 0.889
Accuracy 0.797 0.804
Confusion matrix [[TN, FP], [FN, TP]] [[466, 134], [110, 490]] [[423, 115], [91, 423]]

Reproduce with python ml/evaluate_cv_metrics.py. Phishing is the positive class, and the dataset is balanced 50/50. The deduplicated column keeps one row per distinct feature vector: it is the closest check for train/test leakage, because the CSV stores no URLs.

docs/ml-methodology.md has the full methodology: class balance, leakage, the URL-length and subdomain dataset biases that were found and fixed, the temporal validation, and how the adjustment was sized.

Limitations

Lessons Learned

A few things found during a deliberate self-audit of this project, kept here instead of quietly fixed and forgotten, because how a bug was found and corrected is often more informative than the fact that the code is now clean:

Roadmap

See docs/roadmap.md.

Review And Release Process

PhishLens uses deterministic review gates instead of relying on a single reviewer. See docs/review-methodology.md and docs/release-process.md.

Publication preparation lives in docs/chrome-web-store.md, with permission rationale in docs/permissions.md.