Files
rengine/tools/dnsx/client.py
T
Yogesh Ojha 621d1b207c feat(scans): discovery phase for all target types
Every scan now runs a discovery phase, gated per seed type:

- seed_resolution: expand ip / cidr / asn seeds into IpAddress assets
  (stdlib ipaddress, even-stride sampling + rfc1918 skip on big ranges)
- reverse_dns: dnsx -ptr over discovered IPs
- ip_enrichment: asn / org / country from cached bgp + whois
- target_enrichment: consolidate cached whois/dns/bgp, refresh on miss/stale

The engine framework grows a phase axis (discovery/expansion/depth) and
per-stage applies_to gating so a stage skips cleanly when it doesn't apply
to the seed; subdomain discovery is retagged as expansion. Adds the
IpAddress model + migration.
2026-06-25 13:56:07 +05:30

138 lines
3.6 KiB
Python

"""dnsx CLI client - wraps CLIToolRunner with dnsx-specific arguments."""
from __future__ import annotations
from shared.logging import get_logger
from tools.runner import CLIToolRunner, OutputFormat, ToolNotFoundError, ToolResult
from tools.runner.models import CommandRecorder
logger = get_logger(__name__)
DNSX_BINARY = "dnsx"
DEFAULT_TIMEOUT = 120
DEFAULT_RETRY = 3
DEFAULT_THREADS = 10
class DnsxError(Exception):
"""Raised when dnsx execution fails."""
class DnsxClient:
def __init__(
self,
timeout: int = DEFAULT_TIMEOUT,
retry: int = DEFAULT_RETRY,
threads: int = DEFAULT_THREADS,
resolvers: list[str] | None = None,
recorder: CommandRecorder | None = None,
extra_args: list[str] | None = None,
) -> None:
self.timeout = timeout
self.retry = retry
self.threads = threads
self.resolvers = resolvers
self.recorder = recorder
self.extra_args = extra_args or []
try:
self._runner = CLIToolRunner(DNSX_BINARY, default_timeout=timeout)
except ToolNotFoundError as e:
raise DnsxError(str(e)) from e
def _build_base_args(self) -> list[str]:
args = [
"-retry",
str(self.retry),
"-t",
str(self.threads),
]
if self.resolvers:
args.extend(["-r", ",".join(self.resolvers)])
return args
def recon(
self,
targets: str | list[str],
cdn: bool = True,
) -> ToolResult:
"""Run full DNS recon (-recon) querying all record types on one or more targets."""
args = self._build_base_args()
args.extend(["-recon", "-resp"])
if cdn:
args.append("-cdn")
return self._run(targets, args)
def query(
self,
targets: str | list[str],
record_types: list[str] | None = None,
resp_only: bool = False,
cdn: bool = False,
) -> ToolResult:
"""Run targeted DNS queries for specific record types (defaults to A)."""
args = self._build_base_args()
if record_types:
for rt in record_types:
flag = f"-{rt.lower()}"
args.append(flag)
if resp_only:
args.append("-resp-only")
else:
args.append("-resp")
if cdn:
args.append("-cdn")
return self._run(targets, args)
def resolve(
self,
targets: str | list[str],
) -> ToolResult:
"""Simple DNS resolution check (filters alive hosts)."""
args = self._build_base_args()
return self._run(
targets,
args,
output_format=OutputFormat.PLAIN,
)
def ptr(
self,
ips: str | list[str],
) -> ToolResult:
"""Reverse-DNS (PTR) lookup for one or more IPs (-ptr)."""
args = self._build_base_args()
args.extend(["-ptr", "-resp"])
return self._run(ips, args)
def _run(
self,
targets: str | list[str],
args: list[str],
output_format: OutputFormat = OutputFormat.JSONL,
) -> ToolResult:
"""Execute dnsx with the given arguments."""
if isinstance(targets, str):
targets = [targets]
return self._runner.run(
args=args,
input_data=targets,
input_flag="-l",
output_format=output_format,
json_flag="-json",
timeout=self.timeout,
silent=True,
silent_flag="-silent",
recorder=self.recorder,
tool=DNSX_BINARY,
extra_args=self.extra_args,
)