Severity Scoring
Severity assessment (stage 6) scores every finding with a weighted composite model and clusters findings into campaigns. It is the decision engine behind the dashboard and reports.
The scoring model
Section titled “The scoring model”The scoring/ package implements a weighted model of 29 signals across eight categories:
| Category | Signals cover |
|---|---|
| Registration | registrar, registration age, registrant details, RDAP/WHOIS anomalies |
| DNS | record layout, resolver data, DNS anomalies |
| Brand overlap | typosquat distance, homoglyph content, keyword variants |
| TLS | certificate issuer, SANs, validity |
| Content | HTTP fingerprint, title, HTML similarity |
| Visual | screenshot pHash similarity to the brand |
| Infrastructure | bulletproof ASN, sanctioned countries, co-hosting |
| Campaign | membership in clusters |
Each signal contributes strength × weight; the composite produces a score (0–100), a severity band, an is_phishing verdict, a list of flags, and a debug trace of every signal’s contribution.
Key modules:
signals.py— signal extraction (661 lines).weights.py— weight lookup and overrides.score.py— the composite scoring function.brand_profile.py— brand context for overlap signals.keywords.py— keyword-derived signals.
Threat feeds
Section titled “Threat feeds”threats.py cross-references findings against public threat-intel feeds, adding feed hits to the score and to the finding’s threats list:
- Google Safe Browsing (
SAFE_BROWSING_API_KEY) - URLhaus (
URLHAUS_API_KEY) - AbuseIPDB (
ABUSEIPDB_API_KEY) - VirusTotal (
VIRUSTOTAL_API_KEY)
Feed lookups degrade gracefully when keys are missing (and in tests — the test_urlhaus_* tests hit the live API and are therefore network-dependent).
False-positive suppression
Section titled “False-positive suppression”The score incorporates suppression signals — e.g. content that clearly belongs to the registrant, or registration data that matches the brand — reducing the composite for findings that look benign.
Campaign clustering
Section titled “Campaign clustering”Findings that share infrastructure are grouped into campaigns:
- Intra-scan — the Rails dashboard clusters the latest scan’s findings by shared infra IP or DNS A-record (min cluster size 2), then requests LLM narratives.
- Cross-scan —
CampaignClusterer(in the Rails internal API) runs a union-find over the last 30 days of findings:- Strong keys (shared A-IP, identical SSL SAN + issuer, same registrant org) union on their own.
- Weak keys (shared NS, registration within a 48-hour window) only extend existing clusters.
Campaigns feed the report’s campaign analysis and the learner’s context.
Exposure score
Section titled “Exposure score”The report computes an overall exposure score from the severity distribution:
exposure_score = min(100, round(avg_weight/4 * 70 + min(flagged, 10) * 3))and derives the headline label from the same bands as the gauge:
| Condition | Label |
|---|---|
| any critical finding, or score ≥ 66 | Critical |
| score ≥ 41 | Elevated |
| score ≥ 21 | Moderate |
| otherwise | Low |
Weights and learning
Section titled “Weights and learning”Weights are stored versioned in scoring_weights (source manual or learned). The adaptive learner updates them from analyst feedback; the latest version is served to the scorer via the internal API.