Risk scores are integers from 0–100. Verdict thresholds and signal weights are defined in app/core/constants.py.
| Score Range | Verdict | Label |
|---|---|---|
0 – VERDICT_YELLOW_THRESHOLD - 1 |
green |
Safe |
VERDICT_YELLOW_THRESHOLD – VERDICT_RED_THRESHOLD - 1 |
yellow |
Suspicious |
VERDICT_RED_THRESHOLD – 100 |
red |
Dangerous |
Scores are capped at 100.
Uses only local CPU analysis. No external API data. Must complete in <1ms.
Inputs: heuristics dict, redirect hops, final URL, DNS status, metadata availability, SSL error flag.
Signals applied:
| Signal | Condition | Penalty |
|---|---|---|
| DNS failure | Domain does not resolve | +40 |
| Missing metadata | No OG/meta tags, not a trusted platform | +10 |
| Suspicious TLD | Domain ends in a high-risk TLD | +15 |
| Plain HTTP | Scheme is http:// |
+20 |
| SSL error | TLS handshake failed during redirect trace | +30 |
| Phishing keywords | Keywords found in URL path, title, or description | +10 |
| Keyword + suspicious domain synergy | Phishing keyword on suspicious TLD or typosquatted domain | +15 |
| Brand impersonation | Levenshtein distance = 1 from a high-value target | WEIGHT_HEURISTIC × BRAND_PENALTY_SCORE |
| Punycode/homograph | xn-- detected in any hop |
Hard floor at PUNYCODE_MIN_SCORE |
| TLD + keyword synergy | Suspicious TLD + high-risk keyword | WEIGHT_HEURISTIC × SYNERGY_PENALTY_SCORE |
| Excessive redirect depth | Hops > MAX_REDIRECT_HOPS_FREE |
Per-hop penalty (cross-domain > same-domain) |
| Typosquatting (no brand overlap) | Detected without brand penalty | WEIGHT_HEURISTIC × TYPOSQUATTING_PENALTY |
Starts from the heuristic base score produced by Pass 1, then applies external signals.
Additional signals applied:
| PhishTank / OpenPhish | Flagged as phishing by external feeds | Score floored at 80 (PhishTank) or 90 (OpenPhish) |
| Trusted domain abuse | Hosting domain + PhishTank/OpenPhish flag or phishing keyword | Score floored at VERDICT_YELLOW_THRESHOLD + 1 |
| Hosted phishing escalation | Suspicious path/param on trusted hosting platform | Score floored at VERDICT_YELLOW_THRESHOLD + 5 |
| SSL cert age | Certificate issued recently | WEIGHT_SSL_AGE × penalty (tiered) |
| Domain age (RDAP) | Domain registered recently | WEIGHT_RDAP_AGE × penalty (tiered) |
| New domain + Threat feed synergy | Domain < NEWLY_REGISTERED_DAYS and flagged by PhishTank or OpenPhish | Score floored at 90 |
| Google Safe Browsing | Match found | Score overridden to GSB_THREAT_MIN_SCORES[threat_type] |
| Uncertainty penalty | Timed-out sources × conditions | +2 to +5 per source |
| Trusted platform cap | Strong signals absent | Score capped at TRUSTED_PLATFORM_CAP, weak reasons removed |
| Age | Penalty Constant |
|---|---|
< SSL_CERT_VERY_NEW_DAYS |
SSL_CERT_VERY_NEW_PENALTY |
< SSL_CERT_NEW_DAYS |
SSL_CERT_NEW_PENALTY |
< SSL_CERT_RECENT_DAYS |
SSL_CERT_RECENT_PENALTY |
< SSL_CERT_YOUNG_DAYS |
SSL_CERT_YOUNG_PENALTY |
If the domain shows no other risk signals (typosquatting, punycode, synergy, PhishTank/OpenPhish flags, GSB match), the penalty is capped at 5 regardless of age. This prevents young-but-legitimate sites from false-positiving.
| Age | Penalty Constant |
|---|---|
< NEWLY_REGISTERED_DAYS |
NEWLY_REGISTERED_PENALTY |
< RECENTLY_REGISTERED_DAYS |
RECENTLY_REGISTERED_PENALTY |
Weights are applied as multipliers before adding to the base score:
| Weight | Used for |
|---|---|
WEIGHT_HEURISTIC |
Brand penalty, synergy, typosquatting |
WEIGHT_SSL_AGE |
SSL certificate age penalty |
WEIGHT_REDIRECT_DEPTH |
Per-hop redirect penalty |
WEIGHT_RDAP_AGE |
Domain age penalty |
PhishTank and OpenPhish act as high-confidence malicious indicators:
- PhishTank Match: Floor risk score at 80 (Dangerous).
- OpenPhish Match: Floor risk score at 90 (Dangerous).
These feeds do not require threshold corroboration and override standard heuristic calculations.
When Google Safe Browsing returns a match, compute_final_score overrides the accumulated score:
risk_score = max(risk_score, GSB_THREAT_MIN_SCORES[gsb_threat_type])GSB_THREAT_MIN_SCORES maps each threat type to a minimum score:
| Threat Type | Minimum Score |
|---|---|
MALWARE |
95 |
SOCIAL_ENGINEERING (phishing) |
90 |
UNWANTED_SOFTWARE |
80 |
POTENTIALLY_HARMFUL_APPLICATION |
75 |
This ensures GSB matches always produce at least a red verdict regardless of heuristic score.
Domains matching TRUSTED_PLATFORMS (e.g., github.com, google.com) have their score capped at TRUSTED_PLATFORM_CAP unless any of the following strong signals are present:
- PhishTank or OpenPhish match
- GSB match
- Redirect chain depth >
MAX_REDIRECT_HOPS_FREE - Punycode detected
- Brand penalty applied
- Hosted phishing signals active
When capping applies, reasons matching WEAK_SIGNAL_PATTERNS are also removed from the output to reduce noise.
detect_hosted_phishing fires specifically for URLs on TRUSTED_HOSTING_DOMAINS (e.g., pages.github.io, web.app). It checks for:
| Signal | Description |
|---|---|
suspicious_path |
URL path starts with a known phishing-associated path |
deceptive_param |
Query parameter redirects to an untrusted domain |
redirect_chain_suspicious |
Hop chain passes through an untrusted domain |
Corroboration count aggregates all signals + PhishTank/OpenPhish flags + phishing keywords. active=True triggers a score floor at VERDICT_YELLOW_THRESHOLD + 5.
Applied when external sources time out. Penalties are conditional — they do not blindly penalize every timeout:
SSL timeout: +2, only if base_score >= VERDICT_YELLOW_THRESHOLD or heuristics are suspicious.
PhishTank / OpenPhish / GSB / RDAP timeout: +5 per timed-out source, only if:
- Two or more of these sources timed out, or
- Heuristics are suspicious (
typosquatting,punycode,synergy,suspicious_keywords), or base_score >= VERDICT_YELLOW_THRESHOLD - 5
The uncertainty reason is not appended to reasons[] for green verdicts.
If punycode is detected in the final URL or any redirect hop, the risk score is raised to at least PUNYCODE_MIN_SCORE regardless of other signals. This reflects the high confidence that punycode in URLs represents a homograph attack.