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, and an optional machine learning model. The project is built as a practical cybersecurity portfolio project with clear safety boundaries.

PhishLens catching a look-alike PayPal login (paypa1-secure-login.com) and explaining exactly why it is dangerous. End-to-end walkthrough:

# 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
uvicorn app.main:app --app-dir backend --reload
# 3. Build the extension
cd extension && npm install && npm run build
# 4. Load the extension in Chrome: chrome://extensions → Developer mode → Load unpacked → select extension/dist
# 5. (Optional) Build a real ML dataset and retrain the model
python ml/datasets/build_dataset.py && python ml/train_model.py
/analyze, /report, /health; PhishTank threat intel, backend TLS + Certificate Transparency inspection, RDAP domain-age lookups — each with URL normalization, TTL caching, timeouts, and clean degradation when unavailable.dangerous results. Works fully offline; the backend only enriches.| Safe result | Suspicious result | Dangerous result |
|---|---|---|
![]() |
![]() |
![]() |
| Local-only mode (backend unavailable) | Danger overlay |
|---|---|
![]() |
![]() |
Chrome page
-> content script extracts non-sensitive DOM signals
-> popup computes local heuristic score
-> popup optionally calls FastAPI /analyze
-> backend adds URL, threat intel, TLS, and ML signals
-> popup shows score, label, confidence, risk breakdown, and feedback controls
-> 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.
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
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.
cd extension
npm install
npm run build
Load extension/dist in Chrome:
chrome://extensions.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.
Backend:
.\.venv\Scripts\python.exe -m pip install -r backend/requirements-dev.txt
pytest backend/tests
Extension:
cd extension
npm run lint
npm run test
npm run build
npm audit --audit-level=high
ML demo:
python ml/train_model.py
python ml/evaluate_model.py
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
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:
http://localhost:8000.dangerous results.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.
Run the backend, demo pages, and extension locally:
$env:PHISHLENS_ENABLE_DEMO_THREAT_SOURCE="true"
.\.venv\Scripts\python.exe -m uvicorn app.main:app --app-dir backend --reload
python demo/serve_demo.py
cd extension
npm run build
Load extension/dist in Chrome and visit:
http://localhost:8080/pages/safe.htmlhttp://localhost:8080/pages/suspicious.htmlhttp://localhost:8080/pages/phishlens-demo-dangerous-login-secure-update.htmlThe dangerous demo requires PHISHLENS_ENABLE_DEMO_THREAT_SOURCE=true and 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/.
[[197, 1], [29, 169]]; see docs/ml-methodology.md for a known URL-length dataset bias that was found and fixed and the full classification report). These numbers do not describe the model’s production behavior: DOM features (has_password_field, num_forms, etc.) are hardcoded to 0 for every training row because the dataset is built from URLs only, without a live browser session — but backend/app/services/ml_service.py feeds real DOM features from the extension’s content script at inference time. The reported accuracy/precision/recall reflect a URL-only model; the 6 DOM columns it sees in production were never exercised with real variation during training, so any influence they have on live predictions is unvalidated extrapolation, not something these metrics back up.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:
import statement into two files that execute as classic, non-module scripts (MV3 content scripts and chrome.scripting.executeScript-injected files can’t be modules). tsc, vitest, and vite build all passed, because none of them load the bundle in an actual browser — Vitest mocks the module graph, and Vite’s build doesn’t check runtime module-format compatibility. Found by recording this README’s demo GIF with a real Playwright + Chromium session instead of a screen recording tool, which surfaced “Could not establish connection” the moment the popup tried to collect DOM features. Fixed by splitting the build into two Rollup passes — ES modules for popup/options/the service worker, IIFE for the content script and overlay. The lesson: a green test suite proves the code you tested, not the environment you didn’t.https://example.com/) while phishing URLs from
PhishTank carry real paths, so url_length alone separated the two classes almost
perfectly — the model was learning “has a path” instead of phishing patterns. Fixed by
adding realistic paths to legitimate URLs; honest accuracy dropped to ~91% CV. Detailed
in docs/ml-methodology.md.sklearn import triggered by lazily unpickling the ML model on
the first request, which also blocked the event loop for concurrent requests. Fixed by
warming up the model from a FastAPI lifespan handler at startup; first-request latency
dropped from ~5.5s to ~1.4s.See docs/roadmap.md.
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.