CI / test (pull_request) Failing after 2m2s
Add text and JSON formatters with real-time progress via traverser hooks, summary statistics, and CLI integration for --show-* and --json flags. Co-authored-by: multica-agent <github@multica.ai>
192 lines
4.8 KiB
Go
192 lines
4.8 KiB
Go
package output
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
|
|
"github.com/hits/ExploreDNS/internal/dns"
|
|
"github.com/hits/ExploreDNS/internal/traverse"
|
|
)
|
|
|
|
type jsonFormatter struct {
|
|
cfg *Config
|
|
w io.Writer
|
|
payload jsonDocument
|
|
}
|
|
|
|
type jsonDocument struct {
|
|
Domain string `json:"domain"`
|
|
QueryType string `json:"query_type"`
|
|
Progress []jsonProgressEvent `json:"progress,omitempty"`
|
|
Resolves []jsonProgressEvent `json:"resolves,omitempty"`
|
|
Results []jsonResult `json:"results,omitempty"`
|
|
Servers []jsonServer `json:"servers,omitempty"`
|
|
Summary jsonSummary `json:"summary,omitempty"`
|
|
}
|
|
|
|
type jsonProgressEvent struct {
|
|
Stage string `json:"stage"`
|
|
Depth int `json:"depth"`
|
|
Name string `json:"name"`
|
|
QType string `json:"qtype"`
|
|
Server string `json:"server,omitempty"`
|
|
Bailiwick string `json:"bailiwick,omitempty"`
|
|
Resolving bool `json:"resolving,omitempty"`
|
|
}
|
|
|
|
type jsonResult struct {
|
|
Depth int `json:"depth"`
|
|
Probability float64 `json:"probability"`
|
|
ResponseType string `json:"response_type"`
|
|
Server string `json:"server,omitempty"`
|
|
Answers []string `json:"answers,omitempty"`
|
|
CNAMEChain []string `json:"cname_chain,omitempty"`
|
|
}
|
|
|
|
type jsonServer struct {
|
|
Name string `json:"name"`
|
|
IPs []string `json:"ips"`
|
|
}
|
|
|
|
type jsonSummary struct {
|
|
ByType map[string]float64 `json:"by_type,omitempty"`
|
|
Answers []jsonAnswerStat `json:"answers,omitempty"`
|
|
}
|
|
|
|
type jsonAnswerStat struct {
|
|
RData string `json:"rdata"`
|
|
Probability float64 `json:"probability"`
|
|
Records []string `json:"records,omitempty"`
|
|
}
|
|
|
|
func newJSONFormatter(cfg *Config, w io.Writer) *jsonFormatter {
|
|
return &jsonFormatter{
|
|
cfg: cfg,
|
|
w: w,
|
|
payload: jsonDocument{
|
|
Domain: cfg.Domain,
|
|
QueryType: cfg.QueryType,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (f *jsonFormatter) WriteProgress(event traverse.TraversalEvent) error {
|
|
if !f.cfg.ShowProgress {
|
|
return nil
|
|
}
|
|
f.payload.Progress = append(f.payload.Progress, f.eventToJSON(event))
|
|
return nil
|
|
}
|
|
|
|
func (f *jsonFormatter) WriteResolve(event traverse.TraversalEvent) error {
|
|
if !f.cfg.ShowResolves {
|
|
return nil
|
|
}
|
|
f.payload.Resolves = append(f.payload.Resolves, f.eventToJSON(event))
|
|
return nil
|
|
}
|
|
|
|
func (f *jsonFormatter) WriteResult(result traverse.TraversalResult) error {
|
|
if !f.cfg.ShowAllStats {
|
|
return nil
|
|
}
|
|
f.payload.Results = append(f.payload.Results, f.resultToJSON(result))
|
|
return nil
|
|
}
|
|
|
|
func (f *jsonFormatter) WriteSummary(results []traverse.TraversalResult) error {
|
|
if f.cfg.ShowResults {
|
|
for _, result := range terminalResults(results) {
|
|
f.payload.Results = append(f.payload.Results, f.resultToJSON(result))
|
|
}
|
|
}
|
|
|
|
if f.cfg.ShowServers {
|
|
servers := collectServers(results)
|
|
for name, ips := range servers {
|
|
f.payload.Servers = append(f.payload.Servers, jsonServer{
|
|
Name: name,
|
|
IPs: ips,
|
|
})
|
|
}
|
|
}
|
|
|
|
if f.cfg.ShowSummaryResults {
|
|
stats := ComputeSummary(results)
|
|
if stats != nil {
|
|
f.payload.Summary = jsonSummary{
|
|
ByType: stats.ByType,
|
|
}
|
|
for _, answer := range stats.Answers {
|
|
f.payload.Summary.Answers = append(f.payload.Summary.Answers, jsonAnswerStat{
|
|
RData: answer.RData,
|
|
Probability: answer.Prob,
|
|
Records: answer.RRs,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *jsonFormatter) Flush() error {
|
|
enc := json.NewEncoder(f.w)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(f.payload)
|
|
}
|
|
|
|
func (f *jsonFormatter) eventToJSON(event traverse.TraversalEvent) jsonProgressEvent {
|
|
ref := event.Result.Referral
|
|
if ref == nil {
|
|
return jsonProgressEvent{}
|
|
}
|
|
|
|
item := jsonProgressEvent{
|
|
Stage: stageName(event.Stage),
|
|
Depth: ref.Depth,
|
|
Name: trimDomain(ref.Name),
|
|
QType: dns.QNameType(ref.Qtype),
|
|
Bailiwick: trimDomain(ref.Bailiwick),
|
|
Resolving: !ref.HasAddresses(),
|
|
}
|
|
if event.Result.Response != nil && event.Result.Response.Server != nil {
|
|
item.Server = event.Result.Response.Server.String()
|
|
} else {
|
|
item.Server = referralServerLabel(ref, event.Result.Response)
|
|
}
|
|
return item
|
|
}
|
|
|
|
func (f *jsonFormatter) resultToJSON(result traverse.TraversalResult) jsonResult {
|
|
item := jsonResult{}
|
|
if result.Referral != nil {
|
|
item.Depth = result.Referral.Depth
|
|
item.Probability = result.Referral.Prob
|
|
}
|
|
if result.Response != nil {
|
|
item.ResponseType = result.Response.Type.String()
|
|
if result.Response.Server != nil {
|
|
item.Server = result.Response.Server.String()
|
|
}
|
|
if result.Response.Decoded != nil {
|
|
for _, rr := range result.Response.Decoded.Answers {
|
|
item.Answers = append(item.Answers, dns.FormatRecord(rr))
|
|
}
|
|
item.CNAMEChain = append(item.CNAMEChain, result.Response.Decoded.CNAMEChain...)
|
|
}
|
|
}
|
|
return item
|
|
}
|
|
|
|
func stageName(stage traverse.EventStage) string {
|
|
switch stage {
|
|
case traverse.EventStart:
|
|
return "start"
|
|
case traverse.EventComplete:
|
|
return "complete"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|