URL risk scorer for phishing-style links.
I wanted something I could run offline, tweak the weights on, and still explain every point on the score. Python does the feature extraction and scoring. TypeScript is just a thin API/CLI that calls into that engine — I already use JS for small services, so this split felt natural.
Most “is this link bad?” tools either:
- call a third-party reputation API, or
- dump you into a black box classifier
I went the other way: pull cheap signals out of the URL string itself (IP hosts, brand words in the path, shady TLDs, missing HTTPS, punycode, etc.), weight them, and return a label plus a short reason list.
Not perfect. Intentionally readable.
Pipeline for one URL:
- Normalize (add
http://if someone pasted bare host). - Parse host / path / query.
- Flag features.
- Add weighted points (cap at 100).
- Map to:
benign·low_risk·suspicious·likely_phish.
Weights live in engine/scorer.py as a plain dict. If you disagree with “IP host = 25”, change it. That was the whole point.
Example of a brand trick I care about: path says paypal but the host is a random IP or junk domain.
flowchart LR
subgraph edge [TypeScript]
A[POST /score or CLI]
B[scorer_bridge.ts]
A --> B
end
subgraph core [Python]
C[engine/cli.py]
D[features.py]
E[scorer.py]
C --> D --> E
end
B -->|spawn + stdin URLs| C
E -->|JSON score + reasons| B
B --> F[HTTP response / terminal]
Contract between the two sides is boring JSON over stdin/stdout. No shared runtime, no gRPC.
phish-snare/
engine/ # scoring
features.py
scorer.py
cli.py
api/ # HTTP + TS CLI
server.ts
cli.ts
scorer_bridge.ts
sample_data/urls.txt
tests/
git clone https://github.com/Nabil201-ctrl/phish-snare.git
cd phish-snare
python3 engine/cli.py "http://203.0.113.50/paypal/login/verify"
python3 engine/cli.py -f sample_data/urls.txt --jsoncd api
npm install
npx ts-node cli.ts "https://github.com" "http://secure-paypal-update.tk/login"
# API on :3847
npx ts-node server.tscurl -s http://127.0.0.1:3847/health
curl -s -X POST http://127.0.0.1:3847/score \
-H 'content-type: application/json' \
-d '{"url":"http://203.0.113.50/paypal/login"}'python3 -c "
import sys
from pathlib import Path
sys.path.insert(0, str(Path('engine').resolve()))
sys.path.insert(0, str(Path('tests').resolve()))
from test_scorer import test_clean_site_low, test_ip_paypal_path_high
test_clean_site_low(); test_ip_paypal_path_high()
print('ok')
"- First version treated words like
loginas brand tokens. That flaggedgithub.com/login. Fixed by keeping brand list to real brands only. - I almost put Express in. Didn't need it. Node
httpis enough for a demo API. - Keeping Python pure (no Flask) made unit tests stupidly easy.
- Parse full email HTML and extract URLs first
- Cache results
- Optional local model later as one more feature, not a replacement for the rules