2026-09-08 19:23:49 -04:00
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
< html >
< head >
2026-09-09 12:26:51 -04:00
< title > phisherslapper Developer Docs - Package score< / title >
2026-09-08 19:23:49 -04:00
< / head >
< body bgcolor = "#002b36" text = "#a3b8b9" link = "#859900" vlink = "#b58900" >
< font color = "#b58900" > < h1 > Package score< / h1 > < / font >
2026-09-09 15:13:20 -04:00
< p > Deterministic risk scoring with a relationship-gated five-way verdict:
contextual domain-age tiers, certificate downgrade (+40), registrar
mismatch (+30), DNS resolution tampering (+50), rogue-redirection signals
(per-path TLS divergence +50, origin-AS split +30, hosts-file override
+50), and display-only sender-authority/chronology context. Sources:
< tt > internal/score/score.go< / tt > , < tt > internal/score/date.go< / tt > . Max
total is 330.< / p >
2026-09-08 19:23:49 -04:00
< hr >
< a name = "DomainData" > < / a >
< font color = "#859900" > < h2 > DomainData< / h2 > < / font >
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > type DomainData struct {
Domain string // queried domain
Date string // YYYY-MM-DD, or sentinel on failure
Registrar string // "Unknown" when unavailable
Issuer string // "Unknown" when unavailable
Class string // EV, OV, IV, DV, or Unknown
SANs string // comma-joined, or "Unknown"
AgeDays int
AgeKnown bool
2026-09-09 15:13:20 -04:00
DNSTampered bool // suppression/NS split (scores on suspect)
DNSSplit dns.SplitKind // split direction (horizon never scores)
DNSDetail string // human-readable split description
SysIP string // first system-resolved A record, "" when none
PubIP string // first public-resolved A record, "" when none
PathChecked bool // both paths resolved to an IP
PathComplete bool // both per-path TLS identities fetched
PathMatch bool // per-path TLS identities agree
PathDetail string // mismatch description
SysASN string // origin AS via local path, e.g. "AS15169"
PubASN string // origin AS via public path
SysASNNet string // prefix/country detail, e.g. "8.8.8.0/24, US"
PubASNNet string
ASNChecked bool // both paths attributed
ASNMatch bool // both paths share an origin AS
HostsHit bool // static hosts-file mapping exists
HostsIP string // the mapped IP
SPFChecked bool // target SPF was retrieved and evaluated
SPFAuthorized bool // suspect IP is covered by target SPF
SPFCover string // covering mechanism, e.g. "include:_spf.google.com"
DMARCPolicy string // target DMARC p= value, "" when unpublished
2026-09-08 19:23:49 -04:00
}< / pre > < / td > < / tr > < / table >
2026-09-09 15:13:20 -04:00
< p > Collected triage fields for one domain; the scoring input. Raw
addresses are retained for per-path interrogation only and are never
compared for points.< / p >
2026-09-08 19:23:49 -04:00
< hr >
< a name = "Result" > < / a >
< font color = "#859900" > < h2 > Result< / h2 > < / font >
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > type Result struct {
2026-09-09 15:13:20 -04:00
Score int
Findings []string
Verdict string
AgeFlag string // display suffix for young suspect domains
Relationship string // identical | san_endorsed | spf_endorsed | lookalike | unrelated
2026-09-08 19:23:49 -04:00
}< / pre > < / td > < / tr > < / table >
2026-09-09 15:13:20 -04:00
< p > Scoring outcome. < tt > Relationship< / tt > names the target/suspect
relationship (see < tt > Score< / tt > ); it gates the LEGITIMATE verdict.< / p >
2026-09-08 19:23:49 -04:00
< hr >
< a name = "ClassLong" > < / a >
< font color = "#859900" > < h2 > ClassLong< / h2 > < / font >
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > func ClassLong(c string) string< / pre > < / td > < / tr > < / table >
< p > Human-readable validation name via lookup table: EV to
< tt > "Extended Validation"< / tt > , OV to < tt > "Organization Validated"< / tt > , DV
to < tt > "Domain Validated"< / tt > , IV to < tt > "Individual Validated"< / tt > ,
anything else to < tt > "Unknown"< / tt > .< / p >
< hr >
< a name = "VerifyDataQuality" > < / a >
< font color = "#859900" > < h2 > VerifyDataQuality< / h2 > < / font >
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > func VerifyDataQuality(target, suspect DomainData) error< / pre > < / td > < / tr > < / table >
< p > Fails closed: returns an error when either domain lacks a valid RDAP
date, a computable age, or any TLS signal (issuer and class both unknown).
The CLI aborts instead of printing a verdict built on missing data.< / p >
< hr >
< a name = "Score" > < / a >
< font color = "#859900" > < h2 > Score< / h2 > < / font >
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > func Score(target, suspect DomainData) Result< / pre > < / td > < / tr > < / table >
2026-09-08 21:49:25 -04:00
< p > Applies the signals. Identical domains force score zero and LEGITIMATE
while still attaching DNS NOTEs. Suspect age
is contextual: the full tier (< 30d: 80, 30-89d: 40) needs company
(registrar mismatch or cert downgrade), otherwise the next tier down;
2026-09-09 15:13:20 -04:00
endorsed infrastructure (shared certificate SANs or target SPF coverage)
halves whatever remains. Target EV/OV versus
2026-09-08 19:23:49 -04:00
suspect DV adds 40 as DOWNGRADE; a same-class issuer difference is a
display-only NOTE (+0). Case-insensitive registrar mismatch adds 30,
2026-09-09 15:13:20 -04:00
skipped when either side is unknown; a shared known registrar is a
display-only NOTE. Target DMARC policy is a display-only NOTE. Suspect-side
DNS suppression and NS
2026-09-08 21:49:25 -04:00
splits add 50; split-horizon shapes and anything target-side are
2026-09-09 15:13:20 -04:00
display-only NOTEs (+0). Rogue redirection on the suspect side: divergent
per-path TLS identities add 50, split origin AS adds 30, a hosts-file
override adds 50 (target-side and inconclusive probes stay display-only).
A suspect far older than a very young target (under 90d vs over a year)
adds a display-only CHRONOLOGY NOTE — the pair was likely fed backwards.
Every pair is classified (identical, SAN/SPF-endorsed, lookalike via
confusable-aware label similarity, or unrelated): LEGITIMATE requires a
relationship, so unrelated pairs scoring 0-39 read UNRELATED with a
zero-trust NO AUTHORITY finding instead. SUSPICIOUS and PHISHING bands
keep their verdicts. The 15-39 band appends an EXPLANATION finding
(malicious-infrastructure vs business-incompetence readings).< / p >
2026-09-08 19:23:49 -04:00
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > r := score.Score(target, suspect)
2026-09-09 15:13:20 -04:00
// r.Score, r.Findings, r.Verdict, r.Relationship, r.AgeFlag< / pre > < / td > < / tr > < / table >
2026-09-08 19:23:49 -04:00
< hr >
< a name = "Verdict" > < / a >
< font color = "#859900" > < h2 > Verdict< / h2 > < / font >
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > func Verdict(score int) string< / pre > < / td > < / tr > < / table >
< p > Maps the total via threshold table: 51+ phishing, 40+ suspicious, 15+
2026-09-09 15:13:20 -04:00
malicious-or-negligent (related pairs), otherwise legitimate (related
pairs). Unrelated pairs scoring 0-39 map to the UNRELATED verdict
instead — see < tt > Score< / tt > . Verdict strings live in named constants
(< tt > VerdictPhishing< / tt > , < tt > VerdictSuspicious< / tt > ,
< tt > VerdictMaliciousOrNegligent< / tt > , < tt > VerdictLegitimate< / tt > ,
< tt > VerdictUnrelated< / tt > ); relationships in < tt > RelIdentical< / tt > ,
< tt > RelSANEndorsed< / tt > , < tt > RelSPFEndorsed< / tt > , < tt > RelLookalike< / tt > ,
< tt > RelUnrelated< / tt > . Max total is 330.< / p >
2026-09-08 19:23:49 -04:00
< hr >
< a name = "ValidISODate" > < / a >
< font color = "#859900" > < h2 > ValidISODate< / h2 > < / font >
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > func ValidISODate(s string) bool< / pre > < / td > < / tr > < / table >
< p > True only for clean, calendar-valid < tt > YYYY-MM-DD< / tt > strings. Any
sentinel or garbage value fails by design.< / p >
< hr >
< a name = "AgeDays" > < / a >
< font color = "#859900" > < h2 > AgeDays< / h2 > < / font >
< table border = "0" cellpadding = "8" width = "100%" > < tr > < td bgcolor = "#073642" > < pre > func AgeDays(ymd string, now time.Time) (days int, ok bool)< / pre > < / td > < / tr > < / table >
< p > Whole days between < tt > ymd< / tt > and < tt > now< / tt > ; < tt > ok< / tt > is false
when < tt > ymd< / tt > is unparseable.< / p >
2026-09-08 21:49:25 -04:00
< hr >
< font color = "#b58900" > Licensed under the GNU General Public License v3 - Mark Robillard Jr (MARKMENTAL)< / font >
2026-09-08 19:23:49 -04:00
< / body >
< / html >