diff --git a/README.md b/README.md index 6befa7b..9bccac5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# phisherslapper [v1.01] +# phisherslapper [v1.02] + +> **Note:** `isthislegit` has been renamed to `phisherslapper`. We do not +> condone physical violence against phishers — but you are warmly encouraged +> to metaphorically slap them with this tool's output in your reply the next +> time they try to scam you. Domain Impersonation & OSINT Triage Tool. @@ -128,7 +133,7 @@ resort for devices without stored CA certificates. ## Example output ```text -phisherslapper [v1.01] +phisherslapper [v1.02] Domain Impersonation & OSINT Triage Tool [*] Comparing: example.com (Target) <---> example.org (Suspect) diff --git a/STYLE.md b/STYLE.md new file mode 100644 index 0000000..76bd805 --- /dev/null +++ b/STYLE.md @@ -0,0 +1,84 @@ +# phisherslapper Code Conventions + +This file is the style contract for this repo. Follow it for every change; +reviewers should cite it when requesting revisions. + +## 1. No `else` — map lookups for values + +Boolean or small-domain forks become table lookups, not branches. Fork +targets stay visible as data, and diffs add rows instead of nesting. + +```go +// Good: the two outcomes sit side by side as data. +hostsLine := map[bool]string{ + true: fmt.Sprintf(" hosts override: STATIC MAPPING %s -> %s\n\n", ip, domain), + false: " hosts override: none\n\n", +}[hit] + +// Bad: an else branch hiding the same two outcomes. +if hit { + fmt.Fprintf(&b, " hosts override: STATIC MAPPING %s -> %s\n\n", ip, domain) +} else { + b.WriteString(" hosts override: none\n\n") +} +``` + +Ordered or priority mappings use `[]struct{…}` slices scanned in order — +see `verdictScale` (`internal/score/score.go`), `oidClassPriority` +(`internal/cert/cert.go`), and `platformAliases` / `archAliases` +(`compile.go`). Flat `map[K]V` lookups cover the rest — see +`classLongNames`. + +## 2. Dispatch tables for goroutines + +When a condition selects *behavior* rather than a value — e.g. which +probes to launch — dispatch through `map[bool]func()` instead of an +if/else launch block: + +```go +// Good: one dispatch, same concurrency shape either way. +probePaths := map[bool]func(){ + true: func() { g.Go(probeShared) }, + false: func() { g.Go(probeSys); g.Go(probePub) }, +}[sameEndpoint] +probePaths() +``` + +The table lives next to the errgroup it feeds (`cmd/phisherslapper/main.go`). + +## 3. Guard clauses over nesting + +Handle the degenerate case first with an early `return` or `continue`; +the happy path stays at the top indent level. Never nest to avoid an +else — flatten instead. + +```go +// Good: incomplete input exits before the real work. +if sys == nil || pub == nil || !sys.HasCert || !pub.HasCert { + return false, false, "" +} +``` + +## 4. Pure functions at the core, I/O at the edges + +Parsing, comparison, and scoring take values and return values: +`ParseHostsFile`, `ParseCymruTXT`, `cert.ComparePaths`, `detectSplit`, +`score.Score`. Network access, the clock, and the filesystem live at the +edges (`main.go` orchestration, `LookupASN`, `CheckHostsOverride`). + +## 5. Fail-open probes, fail-closed verdicts + +Lookup helpers return `(T, bool)` and never surface errors for expected +absence — an unanswered Team Cymru query or a missing hosts file simply +yields `false`. Only `score.VerifyDataQuality` may abort a run, and it +must name the underlying cause(s). + +## 6. Tests and hygiene + +- Every pure function gets a table-driven unit test (`cases := map[string]…`, + see `internal/dns/dns_test.go`). Live-network behavior is asserted + through fail-open semantics, not flaky integration tests. +- `gofmt -l .` must print nothing; `go vet ./...` must pass. +- Stdlib only, plus `golang.org/x/sync` for the fetch pipeline. +- Every exported symbol carries a doc comment. +- New scoring signals update `--scale`, the README scale table, and `docs/` together — a signal without docs is unfinished. diff --git a/cmd/phisherslapper/main.go b/cmd/phisherslapper/main.go index ae21e7e..b202ba6 100644 --- a/cmd/phisherslapper/main.go +++ b/cmd/phisherslapper/main.go @@ -28,7 +28,7 @@ import ( "phisherslapper/internal/score" ) -const version = "1.01" +const version = "1.02" const defaultPerCall = 5 * time.Second const globalTimeout = 15 * time.Second @@ -100,25 +100,30 @@ func fetchDomain(ctx context.Context, client *http.Client, domain string, now ti pubIP = dc.PublicAddrs[0] } if sysIP != nil && pubIP != nil { - if sysIP.Equal(pubIP) { - // Same endpoint both paths: one dial attests for both. - g.Go(func() error { - c, _ := cert.InspectAddr(ctx, sysIP, domain, perCall) - res.sysCert, res.pubCert = c, c - return nil - }) - } else { - g.Go(func() error { - c, _ := cert.InspectAddr(ctx, sysIP, domain, perCall) - res.sysCert = c - return nil - }) - g.Go(func() error { - c, _ := cert.InspectAddr(ctx, pubIP, domain, perCall) - res.pubCert = c - return nil - }) - } + // Shared vs split endpoints dispatch which per-path probes run: + // one dial attests for both when the paths converge. + probePaths := map[bool]func(){ + true: func() { + g.Go(func() error { + c, _ := cert.InspectAddr(ctx, sysIP, domain, perCall) + res.sysCert, res.pubCert = c, c + return nil + }) + }, + false: func() { + g.Go(func() error { + c, _ := cert.InspectAddr(ctx, sysIP, domain, perCall) + res.sysCert = c + return nil + }) + g.Go(func() error { + c, _ := cert.InspectAddr(ctx, pubIP, domain, perCall) + res.pubCert = c + return nil + }) + }, + }[sysIP.Equal(pubIP)] + probePaths() g.Go(func() error { a, ok := dns.LookupASN(ctx, sysIP, perCall) res.sysASN, res.sysASNOK = a, ok diff --git a/docs/main.html b/docs/main.html index 8cc9852..ec19450 100644 --- a/docs/main.html +++ b/docs/main.html @@ -54,7 +54,7 @@ plus report.Verbose when verbose.

Version

-

Current version string is 1.01, printed in the banner, JSON +

Current version string is 1.02, printed in the banner, JSON output, and --scale header. Build target: CGO_ENABLED=0 go build -ldflags="-s -w" -o phisherslapper ./cmd/phisherslapper.

diff --git a/docs/overview.html b/docs/overview.html index 26ac43e..e8867f5 100644 --- a/docs/overview.html +++ b/docs/overview.html @@ -4,7 +4,7 @@ phisherslapper Developer Docs - Overview -

phisherslapper [v1.01]

+

phisherslapper [v1.02]

Domain Impersonation and OSINT Triage Tool. Compares a known-legitimate domain (target) against a suspect domain using DNS resolution, X.509 TLS certificates, and RDAP registration data, then produces a deterministic risk diff --git a/internal/report/report.go b/internal/report/report.go index e1362bf..9292f53 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -287,20 +287,17 @@ func writePaths(b *strings.Builder, label, domain string, d score.DomainData) { case d.PathChecked: b.WriteString(" per-path TLS: inconclusive (one endpoint refused port 443)\n") } - if d.ASNChecked { - status := "agree" - if !d.ASNMatch { - status = "SPLIT" - } - fmt.Fprintf(b, " origin AS: local %s vs public %s [%s]\n", asnOrUnknown(d.SysASN), asnOrUnknown(d.PubASN), status) - } else { - b.WriteString(" origin AS: unattributed (Team Cymru query unanswered)\n") - } - if d.HostsHit { - fmt.Fprintf(b, " hosts override: STATIC MAPPING %s -> %s\n\n", d.HostsIP, domain) - } else { - b.WriteString(" hosts override: none\n\n") - } + asnVerdict := map[bool]string{true: "agree", false: "SPLIT"}[d.ASNMatch] + asnLine := map[bool]string{ + true: fmt.Sprintf(" origin AS: local %s vs public %s [%s]\n", asnOrUnknown(d.SysASN), asnOrUnknown(d.PubASN), asnVerdict), + false: " origin AS: unattributed (Team Cymru query unanswered)\n", + }[d.ASNChecked] + b.WriteString(asnLine) + hostsLine := map[bool]string{ + true: fmt.Sprintf(" hosts override: STATIC MAPPING %s -> %s\n\n", d.HostsIP, domain), + false: " hosts override: none\n\n", + }[d.HostsHit] + b.WriteString(hostsLine) } func asnOrUnknown(s string) string {