package output import ( "fmt" "sort" "strings" "gitea.hansenits.com.au/hits/ExploreDNS/internal/dns" "gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse" miekgdns "github.com/miekg/dns" ) type summaryEntry struct { Type string Prob float64 } type answerEntry struct { RData string Prob float64 RRs []string } type SummaryStats struct { ByType map[string]float64 Answers []answerEntry } func ComputeSummary(results []traverse.TraversalResult) *SummaryStats { stats := &SummaryStats{ ByType: make(map[string]float64), } for _, result := range results { if result.Response == nil || !result.Response.IsTerminal() { continue } if result.Referral == nil { continue } prob := result.Referral.Prob respType := result.Response.Type.String() switch result.Response.Type { case traverse.RespAnswer: key, rrs := answerKey(result.Response) if key == "" { stats.ByType[respType] += prob continue } found := false for i := range stats.Answers { if stats.Answers[i].RData == key { stats.Answers[i].Prob += prob found = true break } } if !found { stats.Answers = append(stats.Answers, answerEntry{ RData: key, Prob: prob, RRs: rrs, }) } default: stats.ByType[respType] += prob } } sort.Slice(stats.Answers, func(i, j int) bool { return stats.Answers[i].RData < stats.Answers[j].RData }) if len(stats.Answers) == 0 && len(stats.ByType) == 0 { return nil } return stats } func answerKey(resp *traverse.Response) (string, []string) { if resp == nil || resp.Decoded == nil { return "", nil } var rdatas []string var formatted []string for _, rr := range resp.Decoded.Answers { if _, ok := rr.(*miekgdns.CNAME); ok { continue } rdata := rrDataString(rr) if rdata == "" { continue } rdatas = append(rdatas, rdata) formatted = append(formatted, dns.FormatRecord(rr)) } if len(rdatas) == 0 { return "", nil } sort.Strings(rdatas) return strings.Join(rdatas, " / "), formatted } func rrDataString(rr miekgdns.RR) string { switch v := rr.(type) { case *miekgdns.A: return v.A.String() case *miekgdns.AAAA: return v.AAAA.String() case *miekgdns.CNAME: return v.Target case *miekgdns.NS: return v.Ns case *miekgdns.MX: return fmt.Sprintf("%d %s", v.Preference, v.Mx) case *miekgdns.TXT: return strings.Join(v.Txt, " ") default: return rr.String() } } func formatProbability(prob float64) string { text := fmt.Sprintf("%.1f%%", prob*100) text = strings.Replace(text, ".0%", "%", 1) return fmt.Sprintf("%5s", text) } func summaryTypeLabel(respType string) string { switch respType { case "nodata": return "found no such record" case "nxdomain": return "name does not exist" case "servfail": return "resulted in SERVFAIL" case "refused": return "query refused by server" case "notimp": return "query type not implemented by server" case "cname_loop": return "resulted in a CNAME loop" case "ns_error": return "nameserver lookup failed" case "error": return "resulted in an error" case "referral": return "resulted in a referral" default: return respType } } func trimDomain(name string) string { return strings.TrimSuffix(name, ".") } func collectServers(results []traverse.TraversalResult) map[string][]string { servers := make(map[string][]string) for _, result := range results { if result.Response == nil || result.Response.Server == nil { continue } name := serverName(result) ip := result.Response.Server.String() if containsString(servers[name], ip) { continue } servers[name] = append(servers[name], ip) } return servers } func serverName(result traverse.TraversalResult) string { if result.Referral != nil && result.Referral.Bailiwick != "" && result.Referral.Bailiwick != "." { return trimDomain(result.Referral.Bailiwick) } if result.Referral != nil && result.Referral.NSName != "" { return result.Referral.NSName } if result.Response != nil && result.Response.Server != nil { return result.Response.Server.String() } return "unknown" } func containsString(items []string, target string) bool { for _, item := range items { if item == target { return true } } return false } // DeduplicateResults collapses terminal results that represent the same // outcome from the same server into a single entry with summed probability. // This prevents the same nameserver failure (or answer) from appearing once // per delegation path when several parent servers all refer to the same child. func DeduplicateResults(results []traverse.TraversalResult) []traverse.TraversalResult { type entry struct { result traverse.TraversalResult prob float64 } keys := make(map[string]*entry) var order []string for _, r := range results { if r.Response == nil || r.Referral == nil { continue } key := resultDeduplicationKey(r) if e, ok := keys[key]; ok { e.prob += r.Referral.Prob } else { keys[key] = &entry{result: r, prob: r.Referral.Prob} order = append(order, key) } } deduped := make([]traverse.TraversalResult, 0, len(order)) for _, key := range order { e := keys[key] refCopy := *e.result.Referral refCopy.Prob = e.prob deduped = append(deduped, traverse.TraversalResult{ Referral: &refCopy, Response: e.result.Response, }) } return deduped } func resultDeduplicationKey(r traverse.TraversalResult) string { bailiwick := strings.TrimSuffix(r.Referral.Bailiwick, ".") switch r.Response.Type { case traverse.RespAnswer: key, _ := answerKey(r.Response) return "answer:" + bailiwick + ":" + key case traverse.RespNSResolutionFailed: return "ns_error:" + r.Response.ErrorMessage default: return r.Response.Type.String() + ":" + bailiwick + ":" + r.Response.ErrorMessage } }