407 lines
11 KiB
Python
407 lines
11 KiB
Python
"""
|
|
ⒸAngelaMos | 2026
|
|
output.py
|
|
|
|
Rich terminal output formatting for DNS results
|
|
|
|
Handles all visual presentation for query, reverse, trace, batch, and
|
|
WHOIS results. Uses Rich tables, panels, and tree structures with color
|
|
coding per record type. Also provides JSON serialization used when the
|
|
--json flag is passed from the CLI.
|
|
|
|
Key exports:
|
|
console - Shared Rich Console instance imported by cli.py
|
|
print_results_table - Renders DNS records as a color-coded rounded table
|
|
print_reverse_result - Renders PTR lookup output with hostname table
|
|
print_trace_result - Renders the DNS resolution path as a Rich tree
|
|
print_batch_results - Renders a summary table for multiple domain results
|
|
results_to_json - Serializes one or more DNSResult objects to a JSON string
|
|
trace_to_json - Serializes a TraceResult to a JSON string
|
|
|
|
Connects to:
|
|
resolver.py - imports DNSResult, TraceResult, RecordType for type annotations
|
|
cli.py - all print_* functions and JSON serializers called in command handlers
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from rich import box
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
from rich.tree import Tree
|
|
|
|
from dnslookup.resolver import (
|
|
DNSResult,
|
|
RecordType,
|
|
TraceResult,
|
|
)
|
|
|
|
console = Console()
|
|
|
|
RECORD_COLORS: dict[RecordType,
|
|
str] = {
|
|
RecordType.A: "green",
|
|
RecordType.AAAA: "blue",
|
|
RecordType.MX: "magenta",
|
|
RecordType.NS: "cyan",
|
|
RecordType.TXT: "yellow",
|
|
RecordType.CNAME: "red",
|
|
RecordType.SOA: "white",
|
|
RecordType.PTR: "bright_cyan",
|
|
}
|
|
|
|
|
|
def get_record_color(record_type: RecordType) -> str:
|
|
"""
|
|
Get the color for a record type
|
|
"""
|
|
return RECORD_COLORS.get(record_type, "white")
|
|
|
|
|
|
def format_ttl(ttl: int) -> str:
|
|
"""
|
|
Format TTL in human-readable format
|
|
"""
|
|
if ttl >= 86400:
|
|
days = ttl // 86400
|
|
return f"{days}d"
|
|
elif ttl >= 3600:
|
|
hours = ttl // 3600
|
|
return f"{hours}h"
|
|
elif ttl >= 60:
|
|
minutes = ttl // 60
|
|
return f"{minutes}m"
|
|
return f"{ttl}s"
|
|
|
|
|
|
def print_header(
|
|
domain: str,
|
|
icon: str = ":globe_showing_americas:"
|
|
) -> None:
|
|
"""
|
|
Print a styled header for DNS lookup
|
|
"""
|
|
console.print()
|
|
console.print(
|
|
f"{icon} [bold cyan]DNS Lookup:[/bold cyan] [bold white]{domain}[/bold white]"
|
|
)
|
|
console.rule(style = "blue")
|
|
|
|
|
|
def print_results_table(result: DNSResult) -> None:
|
|
"""
|
|
Display DNS results in a nice table
|
|
"""
|
|
if not result.records:
|
|
console.print(
|
|
Panel(
|
|
f"[yellow]No records found for {result.domain}[/yellow]",
|
|
title = "[yellow]Warning[/yellow]",
|
|
border_style = "yellow",
|
|
expand = False,
|
|
)
|
|
)
|
|
return
|
|
|
|
table = Table(
|
|
title = "[bold]DNS Records[/bold]",
|
|
box = box.ROUNDED,
|
|
border_style = "blue",
|
|
row_styles = ["",
|
|
"dim"],
|
|
show_header = True,
|
|
header_style = "bold cyan",
|
|
)
|
|
|
|
table.add_column("Type", width = 8, no_wrap = True)
|
|
table.add_column("Value", style = "green", min_width = 30)
|
|
table.add_column(
|
|
"TTL",
|
|
justify = "right",
|
|
style = "dim",
|
|
width = 8
|
|
)
|
|
|
|
for record in result.records:
|
|
color = get_record_color(record.record_type)
|
|
value = record.value
|
|
|
|
if record.priority is not None:
|
|
value = f"{value} [dim](priority: {record.priority})[/dim]"
|
|
|
|
table.add_row(
|
|
f"[{color}]{record.record_type}[/{color}]",
|
|
value,
|
|
format_ttl(record.ttl),
|
|
)
|
|
|
|
console.print(table)
|
|
|
|
|
|
def print_summary(result: DNSResult) -> None:
|
|
"""
|
|
Print summary line after results
|
|
"""
|
|
record_count = len(result.records)
|
|
time_str = f"{result.query_time_ms:.0f}ms"
|
|
|
|
if record_count > 0:
|
|
console.print(
|
|
f"\n[green]:heavy_check_mark:[/green] Found [bold]{record_count}[/bold] record(s) in [cyan]{time_str}[/cyan]"
|
|
)
|
|
else:
|
|
console.print(
|
|
f"\n[yellow]:warning:[/yellow] No records found ({time_str})"
|
|
)
|
|
|
|
if result.nameserver:
|
|
console.print(f"[dim]Nameserver: {result.nameserver}[/dim]")
|
|
|
|
console.print()
|
|
|
|
|
|
def print_errors(result: DNSResult) -> None:
|
|
"""
|
|
Print any errors that occurred
|
|
"""
|
|
if result.errors:
|
|
for error in result.errors:
|
|
console.print(f"[red]:x:[/red] {error}")
|
|
|
|
|
|
def print_reverse_result(result: DNSResult) -> None:
|
|
"""
|
|
Display reverse DNS lookup results
|
|
"""
|
|
console.print()
|
|
console.print(
|
|
f":mag: [bold cyan]Reverse Lookup:[/bold cyan] [bold white]{result.domain}[/bold white]"
|
|
)
|
|
console.rule(style = "blue")
|
|
|
|
if result.records:
|
|
table = Table(
|
|
title = "[bold]PTR Records[/bold]",
|
|
box = box.ROUNDED,
|
|
border_style = "blue",
|
|
)
|
|
table.add_column("IP Address", style = "cyan")
|
|
table.add_column("Hostname", style = "green")
|
|
table.add_column(
|
|
"TTL",
|
|
justify = "right",
|
|
style = "dim",
|
|
width = 8
|
|
)
|
|
|
|
for record in result.records:
|
|
table.add_row(
|
|
result.domain,
|
|
record.value,
|
|
format_ttl(record.ttl),
|
|
)
|
|
|
|
console.print(table)
|
|
print_summary(result)
|
|
else:
|
|
print_errors(result)
|
|
console.print(
|
|
Panel(
|
|
f"[yellow]No PTR record found for {result.domain}[/yellow]",
|
|
border_style = "yellow",
|
|
expand = False,
|
|
)
|
|
)
|
|
console.print()
|
|
|
|
|
|
def print_trace_result(result: TraceResult) -> None:
|
|
"""
|
|
Display DNS trace as a tree visualization
|
|
"""
|
|
console.print()
|
|
console.print(
|
|
f":mag_right: [bold cyan]DNS Trace:[/bold cyan] [bold white]{result.domain}[/bold white]"
|
|
)
|
|
console.rule(style = "blue")
|
|
|
|
if result.error:
|
|
console.print(f"[red]:x:[/red] {result.error}")
|
|
console.print()
|
|
return
|
|
|
|
tree = Tree(
|
|
"[bold blue]:globe_showing_americas: DNS Resolution Path[/bold blue]",
|
|
guide_style = "blue",
|
|
)
|
|
|
|
zone_nodes: dict[str, Any] = {}
|
|
|
|
for hop in result.hops:
|
|
if hop.zone not in zone_nodes:
|
|
if hop.zone == ".":
|
|
zone_display = "[bold yellow][.] Root[/bold yellow]"
|
|
elif hop.zone.endswith("."):
|
|
zone_display = f"[bold yellow][{hop.zone}] TLD[/bold yellow]"
|
|
else:
|
|
zone_display = f"[bold yellow][{hop.zone}.] Authoritative[/bold yellow]"
|
|
|
|
zone_node = tree.add(zone_display)
|
|
zone_nodes[hop.zone] = zone_node
|
|
else:
|
|
zone_node = zone_nodes[hop.zone]
|
|
|
|
server_style = "green" if hop.is_authoritative else "cyan"
|
|
server_branch = zone_node.add(
|
|
f"[{server_style}]:arrow_right: {hop.server}[/{server_style}] [dim]({hop.server_ip})[/dim]"
|
|
)
|
|
server_branch.add(f"[dim]{hop.response}[/dim]")
|
|
|
|
console.print(tree)
|
|
|
|
if result.final_answer:
|
|
console.print(
|
|
f"\n[green]:heavy_check_mark:[/green] Resolution complete: [bold green]{result.final_answer}[/bold green]"
|
|
)
|
|
|
|
hop_count = len(result.hops)
|
|
console.print(f"[dim]Total hops: {hop_count}[/dim]")
|
|
console.print()
|
|
|
|
|
|
def print_batch_progress_header(total: int) -> None:
|
|
"""
|
|
Print header for batch operations
|
|
"""
|
|
console.print()
|
|
console.print(
|
|
f":package: [bold cyan]Batch Lookup:[/bold cyan] [bold white]{total} domains[/bold white]"
|
|
)
|
|
console.rule(style = "blue")
|
|
|
|
|
|
def print_batch_results(results: list[DNSResult]) -> None:
|
|
"""
|
|
Display batch lookup results in a summary table
|
|
"""
|
|
table = Table(
|
|
title = "[bold]Batch Results[/bold]",
|
|
box = box.ROUNDED,
|
|
border_style = "blue",
|
|
row_styles = ["",
|
|
"dim"],
|
|
)
|
|
|
|
table.add_column("Domain", style = "cyan", min_width = 25)
|
|
table.add_column("A", justify = "center", width = 15)
|
|
table.add_column("MX", justify = "center", width = 5)
|
|
table.add_column("NS", justify = "center", width = 5)
|
|
table.add_column(
|
|
"Time",
|
|
justify = "right",
|
|
style = "dim",
|
|
width = 8
|
|
)
|
|
|
|
for result in results:
|
|
a_records = [
|
|
r for r in result.records if r.record_type == RecordType.A
|
|
]
|
|
mx_count = len(
|
|
[
|
|
r for r in result.records
|
|
if r.record_type == RecordType.MX
|
|
]
|
|
)
|
|
ns_count = len(
|
|
[
|
|
r for r in result.records
|
|
if r.record_type == RecordType.NS
|
|
]
|
|
)
|
|
|
|
a_value = a_records[0].value if a_records else "[dim]-[/dim]"
|
|
mx_value = str(mx_count) if mx_count else "[dim]-[/dim]"
|
|
ns_value = str(ns_count) if ns_count else "[dim]-[/dim]"
|
|
|
|
table.add_row(
|
|
result.domain,
|
|
a_value,
|
|
mx_value,
|
|
ns_value,
|
|
f"{result.query_time_ms:.0f}ms",
|
|
)
|
|
|
|
console.print(table)
|
|
|
|
total_records = sum(len(r.records) for r in results)
|
|
total_time = sum(r.query_time_ms for r in results)
|
|
console.print(
|
|
f"\n[green]:heavy_check_mark:[/green] {len(results)} domains, {total_records} total records in {total_time:.0f}ms"
|
|
)
|
|
console.print()
|
|
|
|
|
|
def results_to_json(results: list[DNSResult] | DNSResult) -> str:
|
|
"""
|
|
Convert results to JSON string
|
|
"""
|
|
if isinstance(results, DNSResult):
|
|
results = [results]
|
|
|
|
data = []
|
|
for result in results:
|
|
record_data = [
|
|
{
|
|
"type": r.record_type.value,
|
|
"value": r.value,
|
|
"ttl": r.ttl,
|
|
"priority": r.priority,
|
|
} for r in result.records
|
|
]
|
|
|
|
data.append(
|
|
{
|
|
"domain": result.domain,
|
|
"records": record_data,
|
|
"errors": result.errors,
|
|
"query_time_ms": round(result.query_time_ms,
|
|
2),
|
|
"nameserver": result.nameserver,
|
|
}
|
|
)
|
|
|
|
if len(data) == 1:
|
|
return json.dumps(data[0], indent = 2)
|
|
|
|
return json.dumps(data, indent = 2)
|
|
|
|
|
|
def trace_to_json(result: TraceResult) -> str:
|
|
"""
|
|
Convert trace result to JSON string
|
|
"""
|
|
data = {
|
|
"domain":
|
|
result.domain,
|
|
"hops": [
|
|
{
|
|
"zone": hop.zone,
|
|
"server": hop.server,
|
|
"server_ip": hop.server_ip,
|
|
"response": hop.response,
|
|
"is_authoritative": hop.is_authoritative,
|
|
} for hop in result.hops
|
|
],
|
|
"final_answer":
|
|
result.final_answer,
|
|
"error":
|
|
result.error,
|
|
}
|
|
|
|
return json.dumps(data, indent = 2)
|