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 @@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 {