HomeGuidesHybrid Phish Detection Engine

Hybrid Phish Detection Engine

Published Aug 1, 2025
Updated Sep 15, 2025
1 minutes read

The Engineering Challenge

The project targeted real-world phishing vectors observed on live domains. Key goals were near-real-time detection of malicious HTML signatures, minimizing false positives for high-traffic endpoints, and persisting detection metadata in a scalable cloud Postgres instance while maintaining sub-second query latencies.

The Architecture & Tech Stack

Core Implementation Logic

# app/api/detect.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, AnyUrl
import httpx
from bs4 import BeautifulSoup
import joblib
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.db.models import DetectionRecord  # SQLAlchemy model
from app.features import extract_html_features  # deterministic feature extractor
from sklearn.exceptions import NotFittedError
 
app = FastAPI()
model = joblib.load("models/phish_decision_tree.joblib")  # trained DecisionTree pipeline
engine = create_async_engine("postgresql+asyncpg://user:pass@neon-db-host/dbname", pool_size=20)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
 
class URLCheck(BaseModel):
    url: AnyUrl
    source: str | None = "api"
 
@app.post("/v1/detect")
async def detect(payload: URLCheck):
    async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
        try:
            resp = await client.get(payload.url)
        except httpx.HTTPError as e:
            raise HTTPException(status_code=502, detail="upstream fetch failed")
 
    soup = BeautifulSoup(resp.text, "lxml")
    features = extract_html_features(soup, resp.url)  # returns deterministic feature vector
    try:
        proba = model.predict_proba([features])[0, 1]
        label = int(model.predict([features])[0])
    except NotFittedError:
        raise HTTPException(status_code=500, detail="model not available")
 
    record = DetectionRecord(
        url=str(payload.url),
        source=payload.source,
        score=float(proba),
        label=label
    )
 
    async with AsyncSessionLocal() as session:
        async with session.begin():
            session.add(record)
        await session.commit()
 
    return {"url": payload.url, "phish_score": proba, "is_phish": bool(label)}

Notes: extract_html_features implements deterministic heuristics: domain age checks, form action mismatches, meta redirection, obfuscated JS detection, and Tranco-ranked domain comparisons (Tranco list integrated in the enrichment step).

System Impact & Results