# dnsrecon/providers/dns_provider.py from dns import resolver, reversename from typing import Dict from .base_provider import BaseProvider from core.provider_result import ProviderResult from utils.helpers import _is_valid_ip, _is_valid_domain class DNSProvider(BaseProvider): """ Provider for standard DNS resolution and reverse DNS lookups. Now returns standardized ProviderResult objects. """ def __init__(self, name=None, session_config=None): """Initialize DNS provider with session-specific configuration.""" super().__init__( name="dns", rate_limit=100, timeout=10, session_config=session_config ) # Configure DNS resolver self.resolver = resolver.Resolver() self.resolver.timeout = 5 self.resolver.lifetime = 10 def get_name(self) -> str: """Return the provider name.""" return "dns" def get_display_name(self) -> str: """Return the provider display name for the UI.""" return "DNS" def requires_api_key(self) -> bool: """Return True if the provider requires an API key.""" return False def get_eligibility(self) -> Dict[str, bool]: """Return a dictionary indicating if the provider can query domains and/or IPs.""" return {'domains': True, 'ips': True} def is_available(self) -> bool: """DNS is always available - no API key required.""" return True def query_domain(self, domain: str) -> ProviderResult: """ Query DNS records for the domain to discover relationships and attributes. FIXED: Now creates separate attributes for each DNS record type. Args: domain: Domain to investigate Returns: ProviderResult containing discovered relationships and attributes """ if not _is_valid_domain(domain): return ProviderResult() result = ProviderResult() # Query all record types - each gets its own attribute for record_type in ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SOA', 'TXT', 'SRV', 'CAA']: try: self._query_record(domain, record_type, result) #except resolver.NoAnswer: # This is not an error, just a confirmation that the record doesn't exist. #self.logger.logger.debug(f"No {record_type} record found for {domain}") except Exception as e: self.failed_requests += 1 self.logger.logger.debug(f"{record_type} record query failed for {domain}: {e}") return result def query_ip(self, ip: str) -> ProviderResult: """ Query reverse DNS for the IP address. Args: ip: IP address to investigate Returns: ProviderResult containing discovered relationships and attributes """ if not _is_valid_ip(ip): return ProviderResult() result = ProviderResult() try: # Perform reverse DNS lookup self.total_requests += 1 reverse_name = reversename.from_address(ip) response = self.resolver.resolve(reverse_name, 'PTR') self.successful_requests += 1 ptr_records = [] for ptr_record in response: hostname = str(ptr_record).rstrip('.') if _is_valid_domain(hostname): # Add the relationship result.add_relationship( source_node=ip, target_node=hostname, relationship_type='dns_ptr_record', provider=self.name, confidence=0.8, raw_data={ 'query_type': 'PTR', 'ip_address': ip, 'hostname': hostname, 'ttl': response.ttl } ) # Add to PTR records list ptr_records.append(f"PTR: {hostname}") # Log the relationship discovery self.log_relationship_discovery( source_node=ip, target_node=hostname, relationship_type='dns_ptr_record', confidence_score=0.8, raw_data={ 'query_type': 'PTR', 'ip_address': ip, 'hostname': hostname, 'ttl': response.ttl }, discovery_method="reverse_dns_lookup" ) # Add PTR records as separate attribute if ptr_records: result.add_attribute( target_node=ip, name='ptr_records', # Specific name for PTR records value=ptr_records, attr_type='dns_record', provider=self.name, confidence=0.8, metadata={'ttl': response.ttl} ) except resolver.NXDOMAIN: self.failed_requests += 1 self.logger.logger.debug(f"Reverse DNS lookup failed for {ip}: NXDOMAIN") except Exception as e: self.failed_requests += 1 self.logger.logger.debug(f"Reverse DNS lookup failed for {ip}: {e}") # Re-raise the exception so the scanner can handle the failure raise e return result def _query_record(self, domain: str, record_type: str, result: ProviderResult) -> None: """ FIXED: Query DNS records with unique attribute names for each record type. """ try: self.total_requests += 1 response = self.resolver.resolve(domain, record_type) self.successful_requests += 1 dns_records = [] for record in response: target = "" if record_type in ['A', 'AAAA']: target = str(record) elif record_type in ['CNAME', 'NS', 'PTR']: target = str(record.target).rstrip('.') elif record_type == 'MX': target = str(record.exchange).rstrip('.') elif record_type == 'SOA': target = str(record.mname).rstrip('.') elif record_type in ['TXT']: # Keep raw TXT record value txt_value = str(record).strip('"') dns_records.append(txt_value) # Just the value for TXT continue elif record_type == 'SRV': target = str(record.target).rstrip('.') elif record_type == 'CAA': # Keep raw CAA record format caa_value = f"{record.flags} {record.tag.decode('utf-8')} \"{record.value.decode('utf-8')}\"" dns_records.append(caa_value) # Just the value for CAA continue else: target = str(record) if target: raw_data = { 'query_type': record_type, 'domain': domain, 'value': target, 'ttl': response.ttl } relationship_type = f"dns_{record_type.lower()}_record" confidence = 0.8 # Add relationship result.add_relationship( source_node=domain, target_node=target, relationship_type=relationship_type, provider=self.name, confidence=confidence, raw_data=raw_data ) # Add target to records list dns_records.append(target) # Log relationship discovery self.log_relationship_discovery( source_node=domain, target_node=target, relationship_type=relationship_type, confidence_score=confidence, raw_data=raw_data, discovery_method=f"dns_{record_type.lower()}_record" ) # FIXED: Create attribute with specific name for each record type if dns_records: # Use record type specific attribute name (e.g., 'a_records', 'mx_records', etc.) attribute_name = f"{record_type.lower()}_records" result.add_attribute( target_node=domain, name=attribute_name, # UNIQUE name for each record type! value=dns_records, attr_type='dns_record_list', provider=self.name, confidence=0.8, metadata={'record_type': record_type, 'ttl': response.ttl} ) except Exception as e: self.failed_requests += 1 self.logger.logger.debug(f"{record_type} record query failed for {domain}: {e}") raise e