Skip to content

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/ 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.

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).

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.

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-scanCampaignClusterer (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.

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 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.