- Update go.mod module declaration - Update all internal import paths in .go files - Update go install lines in README.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai>
250 lines
5.7 KiB
Go
250 lines
5.7 KiB
Go
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
|
|
}
|
|
}
|