This commit is contained in:
overcuriousity 2025-09-11 00:00:00 +02:00
parent db2101d814
commit 2d485c5703
7 changed files with 373 additions and 365 deletions

View File

@ -21,6 +21,7 @@ class Config:
self.default_recursion_depth = 2 self.default_recursion_depth = 2
self.default_timeout = 30 self.default_timeout = 30
self.max_concurrent_requests = 5 self.max_concurrent_requests = 5
self.large_entity_threshold = 100
# Rate limiting settings (requests per minute) # Rate limiting settings (requests per minute)
self.rate_limits = { self.rate_limits = {

View File

@ -9,6 +9,7 @@ from datetime import datetime
from typing import Dict, List, Any, Optional, Tuple, Set from typing import Dict, List, Any, Optional, Tuple, Set
from enum import Enum from enum import Enum
from datetime import timezone from datetime import timezone
from collections import defaultdict
import networkx as nx import networkx as nx
@ -24,14 +25,28 @@ class NodeType(Enum):
class RelationshipType(Enum): class RelationshipType(Enum):
"""Enumeration of supported relationship types with confidence scores.""" """Enumeration of supported relationship types with confidence scores."""
SAN_CERTIFICATE = ("san", 0.9) # Certificate SAN relationships SAN_CERTIFICATE = ("san", 0.9)
A_RECORD = ("a_record", 0.8) # A/AAAA record relationships A_RECORD = ("a_record", 0.8)
CNAME_RECORD = ("cname", 0.8) # CNAME relationships AAAA_RECORD = ("aaaa_record", 0.8)
PASSIVE_DNS = ("passive_dns", 0.6) # Passive DNS relationships CNAME_RECORD = ("cname", 0.8)
ASN_MEMBERSHIP = ("asn", 0.7) # ASN relationships MX_RECORD = ("mx_record", 0.7)
MX_RECORD = ("mx_record", 0.7) # MX record relationships NS_RECORD = ("ns_record", 0.7)
NS_RECORD = ("ns_record", 0.7) # NS record relationships PTR_RECORD = ("ptr_record", 0.8)
SOA_RECORD = ("soa_record", 0.7)
TXT_RECORD = ("txt_record", 0.7)
SRV_RECORD = ("srv_record", 0.7)
CAA_RECORD = ("caa_record", 0.7)
DNSKEY_RECORD = ("dnskey_record", 0.7)
DS_RECORD = ("ds_record", 0.7)
RRSIG_RECORD = ("rrsig_record", 0.7)
SSHFP_RECORD = ("sshfp_record", 0.7)
TLSA_RECORD = ("tlsa_record", 0.7)
NAPTR_RECORD = ("naptr_record", 0.7)
SPF_RECORD = ("spf_record", 0.7)
PASSIVE_DNS = ("passive_dns", 0.6)
ASN_MEMBERSHIP = ("asn", 0.7)
def __init__(self, relationship_name: str, default_confidence: float): def __init__(self, relationship_name: str, default_confidence: float):
self.relationship_name = relationship_name self.relationship_name = relationship_name
self.default_confidence = default_confidence self.default_confidence = default_confidence
@ -42,24 +57,24 @@ class GraphManager:
Thread-safe graph manager for DNSRecon infrastructure mapping. Thread-safe graph manager for DNSRecon infrastructure mapping.
Uses NetworkX for in-memory graph storage with confidence scoring. Uses NetworkX for in-memory graph storage with confidence scoring.
""" """
def __init__(self): def __init__(self):
"""Initialize empty directed graph.""" """Initialize empty directed graph."""
self.graph = nx.DiGraph() self.graph = nx.DiGraph()
# self.lock = threading.Lock() # self.lock = threading.Lock()
self.creation_time = datetime.now(timezone.utc).isoformat() self.creation_time = datetime.now(timezone.utc).isoformat()
self.last_modified = self.creation_time self.last_modified = self.creation_time
def add_node(self, node_id: str, node_type: NodeType, def add_node(self, node_id: str, node_type: NodeType,
metadata: Optional[Dict[str, Any]] = None) -> bool: metadata: Optional[Dict[str, Any]] = None) -> bool:
""" """
Add a node to the graph. Add a node to the graph.
Args: Args:
node_id: Unique identifier for the node node_id: Unique identifier for the node
node_type: Type of the node (Domain, IP, Certificate, ASN) node_type: Type of the node (Domain, IP, Certificate, ASN)
metadata: Additional metadata for the node metadata: Additional metadata for the node
Returns: Returns:
bool: True if node was added, False if it already exists bool: True if node was added, False if it already exists
""" """
@ -70,33 +85,33 @@ class GraphManager:
existing_metadata.update(metadata) existing_metadata.update(metadata)
self.graph.nodes[node_id]['metadata'] = existing_metadata self.graph.nodes[node_id]['metadata'] = existing_metadata
return False return False
node_attributes = { node_attributes = {
'type': node_type.value, 'type': node_type.value,
'added_timestamp': datetime.now(timezone.utc).isoformat(), 'added_timestamp': datetime.now(timezone.utc).isoformat(),
'metadata': metadata or {} 'metadata': metadata or {}
} }
self.graph.add_node(node_id, **node_attributes) self.graph.add_node(node_id, **node_attributes)
self.last_modified = datetime.now(timezone.utc).isoformat() self.last_modified = datetime.now(timezone.utc).isoformat()
return True return True
def add_edge(self, source_id: str, target_id: str, def add_edge(self, source_id: str, target_id: str,
relationship_type: RelationshipType, relationship_type: RelationshipType,
confidence_score: Optional[float] = None, confidence_score: Optional[float] = None,
source_provider: str = "unknown", source_provider: str = "unknown",
raw_data: Optional[Dict[str, Any]] = None) -> bool: raw_data: Optional[Dict[str, Any]] = None) -> bool:
""" """
Add an edge between two nodes. Add an edge between two nodes.
Args: Args:
source_id: Source node identifier source_id: Source node identifier
target_id: Target node identifier target_id: Target node identifier
relationship_type: Type of relationship relationship_type: Type of relationship
confidence_score: Custom confidence score (overrides default) confidence_score: Custom confidence score (overrides default)
source_provider: Provider that discovered this relationship source_provider: Provider that discovered this relationship
raw_data: Raw data from provider response raw_data: Raw data from provider response
Returns: Returns:
bool: True if edge was added, False if it already exists bool: True if edge was added, False if it already exists
""" """
@ -112,14 +127,14 @@ class GraphManager:
# Update confidence score if new score is higher # Update confidence score if new score is higher
existing_confidence = self.graph.edges[source_id, target_id]['confidence_score'] existing_confidence = self.graph.edges[source_id, target_id]['confidence_score']
new_confidence = confidence_score or relationship_type.default_confidence new_confidence = confidence_score or relationship_type.default_confidence
if new_confidence > existing_confidence: if new_confidence > existing_confidence:
self.graph.edges[source_id, target_id]['confidence_score'] = new_confidence self.graph.edges[source_id, target_id]['confidence_score'] = new_confidence
self.graph.edges[source_id, target_id]['updated_timestamp'] = datetime.now(timezone.utc).isoformat() self.graph.edges[source_id, target_id]['updated_timestamp'] = datetime.now(timezone.utc).isoformat()
self.graph.edges[source_id, target_id]['updated_by'] = source_provider self.graph.edges[source_id, target_id]['updated_by'] = source_provider
return False return False
edge_attributes = { edge_attributes = {
'relationship_type': relationship_type.relationship_name, 'relationship_type': relationship_type.relationship_name,
'confidence_score': confidence_score or relationship_type.default_confidence, 'confidence_score': confidence_score or relationship_type.default_confidence,
@ -127,7 +142,7 @@ class GraphManager:
'discovery_timestamp': datetime.now(timezone.utc).isoformat(), 'discovery_timestamp': datetime.now(timezone.utc).isoformat(),
'raw_data': raw_data or {} 'raw_data': raw_data or {}
} }
self.graph.add_edge(source_id, target_id, **edge_attributes) self.graph.add_edge(source_id, target_id, **edge_attributes)
self.last_modified = datetime.now(timezone.utc).isoformat() self.last_modified = datetime.now(timezone.utc).isoformat()
return True return True
@ -136,19 +151,19 @@ class GraphManager:
"""Get total number of nodes in the graph.""" """Get total number of nodes in the graph."""
#with self.lock: #with self.lock:
return self.graph.number_of_nodes() return self.graph.number_of_nodes()
def get_edge_count(self) -> int: def get_edge_count(self) -> int:
"""Get total number of edges in the graph.""" """Get total number of edges in the graph."""
#with self.lock: #with self.lock:
return self.graph.number_of_edges() return self.graph.number_of_edges()
def get_nodes_by_type(self, node_type: NodeType) -> List[str]: def get_nodes_by_type(self, node_type: NodeType) -> List[str]:
""" """
Get all nodes of a specific type. Get all nodes of a specific type.
Args: Args:
node_type: Type of nodes to retrieve node_type: Type of nodes to retrieve
Returns: Returns:
List of node identifiers List of node identifiers
""" """
@ -157,32 +172,32 @@ class GraphManager:
node_id for node_id, attributes in self.graph.nodes(data=True) node_id for node_id, attributes in self.graph.nodes(data=True)
if attributes.get('type') == node_type.value if attributes.get('type') == node_type.value
] ]
def get_neighbors(self, node_id: str) -> List[str]: def get_neighbors(self, node_id: str) -> List[str]:
""" """
Get all neighboring nodes (both incoming and outgoing). Get all neighboring nodes (both incoming and outgoing).
Args: Args:
node_id: Node identifier node_id: Node identifier
Returns: Returns:
List of neighboring node identifiers List of neighboring node identifiers
""" """
#with self.lock: #with self.lock:
if not self.graph.has_node(node_id): if not self.graph.has_node(node_id):
return [] return []
predecessors = list(self.graph.predecessors(node_id)) predecessors = list(self.graph.predecessors(node_id))
successors = list(self.graph.successors(node_id)) successors = list(self.graph.successors(node_id))
return list(set(predecessors + successors)) return list(set(predecessors + successors))
def get_high_confidence_edges(self, min_confidence: float = 0.8) -> List[Tuple[str, str, Dict]]: def get_high_confidence_edges(self, min_confidence: float = 0.8) -> List[Tuple[str, str, Dict]]:
""" """
Get edges with confidence score above threshold. Get edges with confidence score above threshold.
Args: Args:
min_confidence: Minimum confidence threshold min_confidence: Minimum confidence threshold
Returns: Returns:
List of tuples (source, target, attributes) List of tuples (source, target, attributes)
""" """
@ -192,18 +207,49 @@ class GraphManager:
for source, target, attributes in self.graph.edges(data=True) for source, target, attributes in self.graph.edges(data=True)
if attributes.get('confidence_score', 0) >= min_confidence if attributes.get('confidence_score', 0) >= min_confidence
] ]
def get_graph_data(self) -> Dict[str, Any]: def get_graph_data(self) -> Dict[str, Any]:
""" """
Export graph data for visualization. Export graph data for visualization.
Returns: Returns:
Dictionary containing nodes and edges for frontend visualization Dictionary containing nodes and edges for frontend visualization
""" """
#with self.lock: #with self.lock:
nodes = [] nodes = []
edges = [] edges = []
# Create a dictionary to hold aggregated data for each node
node_details = defaultdict(lambda: defaultdict(list))
for source, target, attributes in self.graph.edges(data=True):
provider = attributes.get('source_provider', 'unknown')
raw_data = attributes.get('raw_data', {})
if provider == 'dns':
record_type = raw_data.get('query_type', 'UNKNOWN')
value = raw_data.get('value', target)
# DNS data is always about the source node of the query
node_details[source]['dns_records'].append(f"{record_type}: {value}")
elif provider == 'crtsh':
# Data from crt.sh are domain names found in certificates (SANs)
node_details[source]['related_domains_san'].append(target)
elif provider == 'shodan':
# Shodan data is about the IP, which can be either the source or target
source_node_type = self.graph.nodes[source].get('type')
target_node_type = self.graph.nodes[target].get('type')
if source_node_type == 'ip':
node_details[source]['shodan'] = raw_data
elif target_node_type == 'ip':
node_details[target]['shodan'] = raw_data
elif provider == 'virustotal':
# VirusTotal data is about the source node of the query
node_details[source]['virustotal'] = raw_data
# Format nodes for visualization # Format nodes for visualization
for node_id, attributes in self.graph.nodes(data=True): for node_id, attributes in self.graph.nodes(data=True):
node_data = { node_data = {
@ -213,7 +259,18 @@ class GraphManager:
'metadata': attributes.get('metadata', {}), 'metadata': attributes.get('metadata', {}),
'added_timestamp': attributes.get('added_timestamp') 'added_timestamp': attributes.get('added_timestamp')
} }
# Add the aggregated details to the metadata
if node_id in node_details:
for key, value in node_details[node_id].items():
# Use a set to avoid adding duplicate entries to lists
if key in node_data['metadata'] and isinstance(node_data['metadata'][key], list):
existing_values = set(node_data['metadata'][key])
new_values = [v for v in value if v not in existing_values]
node_data['metadata'][key].extend(new_values)
else:
node_data['metadata'][key] = value
# Color coding by type - now returns color objects for enhanced visualization # Color coding by type - now returns color objects for enhanced visualization
type_colors = { type_colors = {
'domain': { 'domain': {
@ -239,18 +296,24 @@ class GraphManager:
'border': '#0088cc', 'border': '#0088cc',
'highlight': {'background': '#44ccff', 'border': '#00aaff'}, 'highlight': {'background': '#44ccff', 'border': '#00aaff'},
'hover': {'background': '#22bbff', 'border': '#0099dd'} 'hover': {'background': '#22bbff', 'border': '#0099dd'}
},
'large_entity': {
'background': '#ff6b6b',
'border': '#cc3a3a',
'highlight': {'background': '#ff8c8c', 'border': '#ff6b6b'},
'hover': {'background': '#ff7a7a', 'border': '#dd4a4a'}
} }
} }
node_color_config = type_colors.get(attributes.get('type', 'unknown'), type_colors['domain']) node_color_config = type_colors.get(attributes.get('type', 'unknown'), type_colors['domain'])
node_data['color'] = node_color_config node_data['color'] = node_color_config
# Pass the has_valid_cert metadata for styling # Pass the has_valid_cert metadata for styling
if 'metadata' in attributes and 'has_valid_cert' in attributes['metadata']: if 'metadata' in attributes and 'has_valid_cert' in attributes['metadata']:
node_data['has_valid_cert'] = attributes['metadata']['has_valid_cert'] node_data['has_valid_cert'] = attributes['metadata']['has_valid_cert']
nodes.append(node_data) nodes.append(node_data)
# Format edges for visualization # Format edges for visualization
for source, target, attributes in self.graph.edges(data=True): for source, target, attributes in self.graph.edges(data=True):
edge_data = { edge_data = {
@ -261,7 +324,7 @@ class GraphManager:
'source_provider': attributes.get('source_provider', ''), 'source_provider': attributes.get('source_provider', ''),
'discovery_timestamp': attributes.get('discovery_timestamp') 'discovery_timestamp': attributes.get('discovery_timestamp')
} }
# Enhanced edge styling based on confidence # Enhanced edge styling based on confidence
confidence = attributes.get('confidence_score', 0) confidence = attributes.get('confidence_score', 0)
if confidence >= 0.8: if confidence >= 0.8:
@ -275,7 +338,7 @@ class GraphManager:
elif confidence >= 0.6: elif confidence >= 0.6:
edge_data['color'] = { edge_data['color'] = {
'color': '#ff9900', 'color': '#ff9900',
'highlight': '#ffbb44', 'highlight': '#ffbb44',
'hover': '#ffaa22', 'hover': '#ffaa22',
'inherit': False 'inherit': False
} }
@ -288,13 +351,13 @@ class GraphManager:
'inherit': False 'inherit': False
} }
edge_data['width'] = 2 edge_data['width'] = 2
# Add dashed line for low confidence # Add dashed line for low confidence
if confidence < 0.6: if confidence < 0.6:
edge_data['dashes'] = [5, 5] edge_data['dashes'] = [5, 5]
edges.append(edge_data) edges.append(edge_data)
return { return {
'nodes': nodes, 'nodes': nodes,
'edges': edges, 'edges': edges,
@ -305,18 +368,18 @@ class GraphManager:
'last_modified': self.last_modified 'last_modified': self.last_modified
} }
} }
def export_json(self) -> Dict[str, Any]: def export_json(self) -> Dict[str, Any]:
""" """
Export complete graph data as JSON for download. Export complete graph data as JSON for download.
Returns: Returns:
Dictionary containing complete graph data with metadata Dictionary containing complete graph data with metadata
""" """
#with self.lock: #with self.lock:
# Get basic graph data # Get basic graph data
graph_data = self.get_graph_data() graph_data = self.get_graph_data()
# Add comprehensive metadata # Add comprehensive metadata
export_data = { export_data = {
'export_metadata': { 'export_metadata': {
@ -339,13 +402,13 @@ class GraphManager:
], ],
'confidence_distribution': self._get_confidence_distribution() 'confidence_distribution': self._get_confidence_distribution()
} }
return export_data return export_data
def _get_confidence_distribution(self) -> Dict[str, int]: def _get_confidence_distribution(self) -> Dict[str, int]:
"""Get distribution of confidence scores.""" """Get distribution of confidence scores."""
distribution = {'high': 0, 'medium': 0, 'low': 0} distribution = {'high': 0, 'medium': 0, 'low': 0}
for _, _, attributes in self.graph.edges(data=True): for _, _, attributes in self.graph.edges(data=True):
confidence = attributes.get('confidence_score', 0) confidence = attributes.get('confidence_score', 0)
if confidence >= 0.8: if confidence >= 0.8:
@ -354,13 +417,13 @@ class GraphManager:
distribution['medium'] += 1 distribution['medium'] += 1
else: else:
distribution['low'] += 1 distribution['low'] += 1
return distribution return distribution
def get_statistics(self) -> Dict[str, Any]: def get_statistics(self) -> Dict[str, Any]:
""" """
Get comprehensive graph statistics. Get comprehensive graph statistics.
Returns: Returns:
Dictionary containing various graph metrics Dictionary containing various graph metrics
""" """
@ -377,26 +440,26 @@ class GraphManager:
'confidence_distribution': self._get_confidence_distribution(), 'confidence_distribution': self._get_confidence_distribution(),
'provider_distribution': {} 'provider_distribution': {}
} }
# Node type distribution # Node type distribution
for node_type in NodeType: for node_type in NodeType:
count = len(self.get_nodes_by_type(node_type)) count = len(self.get_nodes_by_type(node_type))
stats['node_type_distribution'][node_type.value] = count stats['node_type_distribution'][node_type.value] = count
# Relationship type distribution # Relationship type distribution
for _, _, attributes in self.graph.edges(data=True): for _, _, attributes in self.graph.edges(data=True):
rel_type = attributes.get('relationship_type', 'unknown') rel_type = attributes.get('relationship_type', 'unknown')
stats['relationship_type_distribution'][rel_type] = \ stats['relationship_type_distribution'][rel_type] = \
stats['relationship_type_distribution'].get(rel_type, 0) + 1 stats['relationship_type_distribution'].get(rel_type, 0) + 1
# Provider distribution # Provider distribution
for _, _, attributes in self.graph.edges(data=True): for _, _, attributes in self.graph.edges(data=True):
provider = attributes.get('source_provider', 'unknown') provider = attributes.get('source_provider', 'unknown')
stats['provider_distribution'][provider] = \ stats['provider_distribution'][provider] = \
stats['provider_distribution'].get(provider, 0) + 1 stats['provider_distribution'].get(provider, 0) + 1
return stats return stats
def clear(self) -> None: def clear(self) -> None:
"""Clear all nodes and edges from the graph.""" """Clear all nodes and edges from the graph."""
#with self.lock: #with self.lock:

View File

@ -8,6 +8,7 @@ import time
import traceback import traceback
from typing import List, Set, Dict, Any, Optional, Tuple from typing import List, Set, Dict, Any, Optional, Tuple
from concurrent.futures import ThreadPoolExecutor, as_completed, CancelledError from concurrent.futures import ThreadPoolExecutor, as_completed, CancelledError
from collections import defaultdict
from core.graph_manager import GraphManager, NodeType, RelationshipType from core.graph_manager import GraphManager, NodeType, RelationshipType
from core.logger import get_forensic_logger, new_session from core.logger import get_forensic_logger, new_session
@ -334,9 +335,7 @@ class Scanner:
print(f"Querying {len(self.providers)} providers for domain: {domain}") print(f"Querying {len(self.providers)} providers for domain: {domain}")
discovered_domains = set() discovered_domains = set()
discovered_ips = set() discovered_ips = set()
relationships_by_type = defaultdict(list)
# Define a threshold for creating a "large entity" node
LARGE_ENTITY_THRESHOLD = 50
if not self.providers or self.stop_event.is_set(): if not self.providers or self.stop_event.is_set():
return discovered_domains, discovered_ips return discovered_domains, discovered_ips
@ -355,35 +354,72 @@ class Scanner:
relationships = future.result() relationships = future.result()
print(f"Provider {provider.get_name()} returned {len(relationships)} relationships") print(f"Provider {provider.get_name()} returned {len(relationships)} relationships")
# Check if the number of relationships exceeds the threshold for rel in relationships:
if len(relationships) > LARGE_ENTITY_THRESHOLD: relationships_by_type[rel[2]].append(rel)
# Create a single "large entity" node
large_entity_id = f"large_entity_{provider.get_name()}_{domain}"
self.graph.add_node(large_entity_id, NodeType.LARGE_ENTITY, metadata={'count': len(relationships), 'provider': provider.get_name()})
self.graph.add_edge(domain, large_entity_id, RelationshipType.PASSIVE_DNS, 1.0, provider.get_name(), {})
print(f"Created large entity node for {domain} from {provider.get_name()} with {len(relationships)} relationships")
continue # Skip adding individual nodes
for source, target, rel_type, confidence, raw_data in relationships:
if self._is_valid_ip(target):
target_node_type = NodeType.IP
discovered_ips.add(target)
elif self._is_valid_domain(target):
target_node_type = NodeType.DOMAIN
discovered_domains.add(target)
else:
target_node_type = NodeType.ASN if target.startswith('AS') else NodeType.CERTIFICATE
self.graph.add_node(source, NodeType.DOMAIN)
self.graph.add_node(target, target_node_type)
if self.graph.add_edge(source, target, rel_type, confidence, provider.get_name(), raw_data):
print(f"Added relationship: {source} -> {target} ({rel_type.relationship_name})")
except (Exception, CancelledError) as e: except (Exception, CancelledError) as e:
print(f"Provider {provider.get_name()} failed for {domain}: {e}") print(f"Provider {provider.get_name()} failed for {domain}: {e}")
for rel_type, relationships in relationships_by_type.items():
if len(relationships) > config.large_entity_threshold and rel_type == RelationshipType.SAN_CERTIFICATE:
self._handle_large_entity(domain, relationships, rel_type, provider.get_name())
else:
for source, target, rel_type, confidence, raw_data in relationships:
# Determine if the target should create a new node
create_node = rel_type in [
RelationshipType.A_RECORD,
RelationshipType.AAAA_RECORD,
RelationshipType.CNAME_RECORD,
RelationshipType.MX_RECORD,
RelationshipType.NS_RECORD,
RelationshipType.PTR_RECORD,
RelationshipType.SAN_CERTIFICATE
]
# Determine if the target should be subject to recursion
recurse = rel_type in [
RelationshipType.A_RECORD,
RelationshipType.AAAA_RECORD,
RelationshipType.CNAME_RECORD,
RelationshipType.MX_RECORD,
RelationshipType.SAN_CERTIFICATE
]
if create_node:
target_node_type = NodeType.IP if self._is_valid_ip(target) else NodeType.DOMAIN
self.graph.add_node(target, target_node_type)
if self.graph.add_edge(source, target, rel_type, confidence, provider.get_name(), raw_data):
print(f"Added relationship: {source} -> {target} ({rel_type.relationship_name})")
else:
# For records that don't create nodes, we still want to log the relationship
self.logger.log_relationship_discovery(
source_node=source,
target_node=target,
relationship_type=rel_type.relationship_name,
confidence_score=confidence,
provider=provider.name,
raw_data=raw_data,
discovery_method=f"dns_{rel_type.name.lower()}_record"
)
if recurse:
if self._is_valid_ip(target):
discovered_ips.add(target)
elif self._is_valid_domain(target):
discovered_domains.add(target)
print(f"Domain {domain}: discovered {len(discovered_domains)} domains, {len(discovered_ips)} IPs") print(f"Domain {domain}: discovered {len(discovered_domains)} domains, {len(discovered_ips)} IPs")
return discovered_domains, discovered_ips return discovered_domains, discovered_ips
def _handle_large_entity(self, source_domain: str, relationships: list, rel_type: RelationshipType, provider_name: str):
"""
Handles the creation of a large entity node when a threshold is exceeded.
"""
print(f"Large number of {rel_type.name} relationships for {source_domain}. Creating a large entity node.")
entity_name = f"Large collection of {rel_type.name} for {source_domain}"
self.graph.add_node(entity_name, NodeType.LARGE_ENTITY, metadata={"count": len(relationships)})
self.graph.add_edge(source_domain, entity_name, rel_type, 0.9, provider_name, {"info": "Aggregated node"})
def _query_providers_for_ip(self, ip: str) -> None: def _query_providers_for_ip(self, ip: str) -> None:
""" """
Query all enabled providers for information about an IP address. Query all enabled providers for information about an IP address.

View File

@ -13,7 +13,7 @@ class DNSProvider(BaseProvider):
Provider for standard DNS resolution and reverse DNS lookups. Provider for standard DNS resolution and reverse DNS lookups.
Discovers domain-to-IP and IP-to-domain relationships through DNS records. Discovers domain-to-IP and IP-to-domain relationships through DNS records.
""" """
def __init__(self): def __init__(self):
"""Initialize DNS provider with appropriate rate limiting.""" """Initialize DNS provider with appropriate rate limiting."""
super().__init__( super().__init__(
@ -21,77 +21,66 @@ class DNSProvider(BaseProvider):
rate_limit=100, # DNS queries can be faster rate_limit=100, # DNS queries can be faster
timeout=10 timeout=10
) )
# Configure DNS resolver # Configure DNS resolver
self.resolver = dns.resolver.Resolver() self.resolver = dns.resolver.Resolver()
self.resolver.timeout = 5 self.resolver.timeout = 5
self.resolver.lifetime = 10 self.resolver.lifetime = 10
def get_name(self) -> str: def get_name(self) -> str:
"""Return the provider name.""" """Return the provider name."""
return "dns" return "dns"
def is_available(self) -> bool: def is_available(self) -> bool:
"""DNS is always available - no API key required.""" """DNS is always available - no API key required."""
return True return True
def query_domain(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]: def query_domain(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
""" """
Query DNS records for the domain to discover relationships. Query DNS records for the domain to discover relationships.
Args: Args:
domain: Domain to investigate domain: Domain to investigate
Returns: Returns:
List of relationships discovered from DNS analysis List of relationships discovered from DNS analysis
""" """
if not self._is_valid_domain(domain): if not self._is_valid_domain(domain):
return [] return []
relationships = [] relationships = []
# Query A records # Query all record types
relationships.extend(self._query_a_records(domain)) for record_type in ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SOA', 'TXT', 'SRV', 'CAA', 'DNSKEY', 'DS', 'RRSIG', 'SSHFP', 'TLSA', 'NAPTR', 'SPF']:
relationships.extend(self._query_record(domain, record_type))
# Query AAAA records (IPv6)
relationships.extend(self._query_aaaa_records(domain))
# Query CNAME records
relationships.extend(self._query_cname_records(domain))
# Query MX records
relationships.extend(self._query_mx_records(domain))
# Query NS records
relationships.extend(self._query_ns_records(domain))
return relationships return relationships
def query_ip(self, ip: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]: def query_ip(self, ip: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
""" """
Query reverse DNS for the IP address. Query reverse DNS for the IP address.
Args: Args:
ip: IP address to investigate ip: IP address to investigate
Returns: Returns:
List of relationships discovered from reverse DNS List of relationships discovered from reverse DNS
""" """
if not self._is_valid_ip(ip): if not self._is_valid_ip(ip):
return [] return []
relationships = [] relationships = []
try: try:
# Perform reverse DNS lookup # Perform reverse DNS lookup
self.total_requests += 1 self.total_requests += 1
reverse_name = dns.reversename.from_address(ip) reverse_name = dns.reversename.from_address(ip)
response = self.resolver.resolve(reverse_name, 'PTR') response = self.resolver.resolve(reverse_name, 'PTR')
self.successful_requests += 1 self.successful_requests += 1
for ptr_record in response: for ptr_record in response:
hostname = str(ptr_record).rstrip('.') hostname = str(ptr_record).rstrip('.')
if self._is_valid_domain(hostname): if self._is_valid_domain(hostname):
raw_data = { raw_data = {
'query_type': 'PTR', 'query_type': 'PTR',
@ -99,255 +88,90 @@ class DNSProvider(BaseProvider):
'hostname': hostname, 'hostname': hostname,
'ttl': response.ttl 'ttl': response.ttl
} }
relationships.append(( relationships.append((
ip, ip,
hostname, hostname,
RelationshipType.A_RECORD, # Reverse relationship RelationshipType.PTR_RECORD,
RelationshipType.A_RECORD.default_confidence, RelationshipType.PTR_RECORD.default_confidence,
raw_data raw_data
)) ))
self.log_relationship_discovery( self.log_relationship_discovery(
source_node=ip, source_node=ip,
target_node=hostname, target_node=hostname,
relationship_type=RelationshipType.A_RECORD, relationship_type=RelationshipType.PTR_RECORD,
confidence_score=RelationshipType.A_RECORD.default_confidence, confidence_score=RelationshipType.PTR_RECORD.default_confidence,
raw_data=raw_data, raw_data=raw_data,
discovery_method="reverse_dns_lookup" discovery_method="reverse_dns_lookup"
) )
except Exception as e: except Exception as e:
self.failed_requests += 1 self.failed_requests += 1
self.logger.logger.debug(f"Reverse DNS lookup failed for {ip}: {e}") self.logger.logger.debug(f"Reverse DNS lookup failed for {ip}: {e}")
return relationships return relationships
def _query_a_records(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]: def _query_record(self, domain: str, record_type: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query A records for the domain.""" """
Query a specific type of DNS record for the domain.
"""
relationships = [] relationships = []
#if not DNS_AVAILABLE:
# return relationships
try: try:
self.total_requests += 1 self.total_requests += 1
response = self.resolver.resolve(domain, 'A') response = self.resolver.resolve(domain, record_type)
self.successful_requests += 1 self.successful_requests += 1
for a_record in response: for record in response:
ip_address = str(a_record) target = ""
if record_type in ['A', 'AAAA']:
raw_data = { target = str(record)
'query_type': 'A', elif record_type in ['CNAME', 'NS', 'PTR']:
'domain': domain, target = str(record.target).rstrip('.')
'ip_address': ip_address, elif record_type == 'MX':
'ttl': response.ttl target = str(record.exchange).rstrip('.')
} elif record_type == 'SOA':
target = str(record.mname).rstrip('.')
relationships.append(( elif record_type in ['TXT', 'SPF']:
domain, target = b' '.join(record.strings).decode('utf-8', 'ignore')
ip_address, elif record_type == 'SRV':
RelationshipType.A_RECORD, target = str(record.target).rstrip('.')
RelationshipType.A_RECORD.default_confidence, elif record_type == 'CAA':
raw_data target = f"{record.flags} {record.tag.decode('utf-8')} \"{record.value.decode('utf-8')}\""
)) else:
target = str(record)
self.log_relationship_discovery(
source_node=domain,
target_node=ip_address, if target:
relationship_type=RelationshipType.A_RECORD,
confidence_score=RelationshipType.A_RECORD.default_confidence,
raw_data=raw_data,
discovery_method="dns_a_record"
)
except Exception as e:
self.failed_requests += 1
self.logger.logger.debug(f"A record query failed for {domain}: {e}")
return relationships
def _query_aaaa_records(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query AAAA records (IPv6) for the domain."""
relationships = []
#if not DNS_AVAILABLE:
# return relationships
try:
self.total_requests += 1
response = self.resolver.resolve(domain, 'AAAA')
self.successful_requests += 1
for aaaa_record in response:
ip_address = str(aaaa_record)
raw_data = {
'query_type': 'AAAA',
'domain': domain,
'ip_address': ip_address,
'ttl': response.ttl
}
relationships.append((
domain,
ip_address,
RelationshipType.A_RECORD, # Using same type for IPv6
RelationshipType.A_RECORD.default_confidence,
raw_data
))
self.log_relationship_discovery(
source_node=domain,
target_node=ip_address,
relationship_type=RelationshipType.A_RECORD,
confidence_score=RelationshipType.A_RECORD.default_confidence,
raw_data=raw_data,
discovery_method="dns_aaaa_record"
)
except Exception as e:
self.failed_requests += 1
self.logger.logger.debug(f"AAAA record query failed for {domain}: {e}")
return relationships
def _query_cname_records(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query CNAME records for the domain."""
relationships = []
#if not DNS_AVAILABLE:
# return relationships
try:
self.total_requests += 1
response = self.resolver.resolve(domain, 'CNAME')
self.successful_requests += 1
for cname_record in response:
target_domain = str(cname_record).rstrip('.')
if self._is_valid_domain(target_domain):
raw_data = { raw_data = {
'query_type': 'CNAME', 'query_type': record_type,
'source_domain': domain,
'target_domain': target_domain,
'ttl': response.ttl
}
relationships.append((
domain,
target_domain,
RelationshipType.CNAME_RECORD,
RelationshipType.CNAME_RECORD.default_confidence,
raw_data
))
self.log_relationship_discovery(
source_node=domain,
target_node=target_domain,
relationship_type=RelationshipType.CNAME_RECORD,
confidence_score=RelationshipType.CNAME_RECORD.default_confidence,
raw_data=raw_data,
discovery_method="dns_cname_record"
)
except Exception as e:
self.failed_requests += 1
self.logger.logger.debug(f"CNAME record query failed for {domain}: {e}")
return relationships
def _query_mx_records(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query MX records for the domain."""
relationships = []
#if not DNS_AVAILABLE:
# return relationships
try:
self.total_requests += 1
response = self.resolver.resolve(domain, 'MX')
self.successful_requests += 1
for mx_record in response:
mx_host = str(mx_record.exchange).rstrip('.')
if self._is_valid_domain(mx_host):
raw_data = {
'query_type': 'MX',
'domain': domain, 'domain': domain,
'mx_host': mx_host, 'value': target,
'priority': mx_record.preference,
'ttl': response.ttl 'ttl': response.ttl
} }
try:
relationships.append(( relationship_type_enum = getattr(RelationshipType, f"{record_type}_RECORD")
domain, relationships.append((
mx_host, domain,
RelationshipType.MX_RECORD, target,
RelationshipType.MX_RECORD.default_confidence, relationship_type_enum,
raw_data relationship_type_enum.default_confidence,
)) raw_data
))
self.log_relationship_discovery(
source_node=domain, self.log_relationship_discovery(
target_node=mx_host, source_node=domain,
relationship_type=RelationshipType.MX_RECORD, target_node=target,
confidence_score=RelationshipType.MX_RECORD.default_confidence, relationship_type=relationship_type_enum,
raw_data=raw_data, confidence_score=relationship_type_enum.default_confidence,
discovery_method="dns_mx_record" raw_data=raw_data,
) discovery_method=f"dns_{record_type.lower()}_record"
)
except AttributeError:
self.logger.logger.error(f"Unsupported record type '{record_type}' encountered for domain {domain}")
except Exception as e: except Exception as e:
self.failed_requests += 1 self.failed_requests += 1
self.logger.logger.debug(f"MX record query failed for {domain}: {e}") self.logger.logger.debug(f"{record_type} record query failed for {domain}: {e}")
return relationships
def _query_ns_records(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query NS records for the domain."""
relationships = []
#if not DNS_AVAILABLE:
# return relationships
try:
self.total_requests += 1
response = self.resolver.resolve(domain, 'NS')
self.successful_requests += 1
for ns_record in response:
ns_host = str(ns_record).rstrip('.')
if self._is_valid_domain(ns_host):
raw_data = {
'query_type': 'NS',
'domain': domain,
'ns_host': ns_host,
'ttl': response.ttl
}
relationships.append((
domain,
ns_host,
RelationshipType.NS_RECORD,
RelationshipType.NS_RECORD.default_confidence,
raw_data
))
self.log_relationship_discovery(
source_node=domain,
target_node=ns_host,
relationship_type=RelationshipType.NS_RECORD,
confidence_score=RelationshipType.NS_RECORD.default_confidence,
raw_data=raw_data,
discovery_method="dns_ns_record"
)
except Exception as e:
self.failed_requests += 1
self.logger.logger.debug(f"NS record query failed for {domain}: {e}")
return relationships return relationships

View File

@ -653,6 +653,7 @@ input[type="text"]:focus, select:focus {
.detail-row { .detail-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
@ -668,6 +669,23 @@ input[type="text"]:focus, select:focus {
word-break: break-word; word-break: break-word;
} }
.copy-btn {
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 1rem;
margin-left: 10px;
}
.copy-btn:hover {
color: #00ff41;
}
.status-icon {
margin-left: 5px;
}
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.main-content { .main-content {

View File

@ -724,29 +724,82 @@ class DNSReconApp {
*/ */
showNodeModal(nodeId, node) { showNodeModal(nodeId, node) {
if (!this.elements.nodeModal) return; if (!this.elements.nodeModal) return;
if (this.elements.modalTitle) { if (this.elements.modalTitle) {
this.elements.modalTitle.textContent = `Node Details: ${nodeId}`; this.elements.modalTitle.textContent = `Node Details: ${nodeId}`;
} }
let detailsHtml = ''; let detailsHtml = '';
detailsHtml += `<div class="detail-row"><span class="detail-label">Identifier:</span><span class="detail-value">${nodeId}</span></div>`; const createDetailRow = (label, value) => {
detailsHtml += `<div class="detail-row"><span class="detail-label">Type:</span><span class="detail-value">${node.metadata.type || node.type || 'Unknown'}</span></div>`; const baseId = `detail-${label.replace(/[^a-zA-Z0-9]/g, '-')}`;
if (node.metadata) { // Handle empty or undefined values by showing N/A
for (const [key, value] of Object.entries(node.metadata)) { if (value === null || value === undefined || (Array.isArray(value) && value.length === 0)) {
if (key !== 'type') { return `
detailsHtml += `<div class="detail-row"><span class="detail-label">${this.formatLabel(key)}:</span><span class="detail-value">${this.formatValue(value)}</span></div>`; <div class="detail-row">
} <span class="detail-label">${label} <span class="status-icon text-warning"></span></span>
<span class="detail-value">N/A</span>
</div>
`;
} }
// If value is an array, create a row for each item
if (Array.isArray(value)) {
return value.map((item, index) => {
const itemId = `${baseId}-${index}`;
// Only show the label for the first item in the list
const itemLabel = index === 0 ? label : '';
return `
<div class="detail-row">
<span class="detail-label">${itemLabel}</span>
<span class="detail-value" id="${itemId}">${this.formatValue(item)}</span>
<button class="copy-btn" onclick="copyToClipboard('${itemId}')" title="Copy">📋</button>
</div>
`;
}).join('');
}
// Handle objects and other primitive values in a single row
else {
const valueId = `${baseId}-0`;
return `
<div class="detail-row">
<span class="detail-label">${label} <span class="status-icon text-success"></span></span>
<span class="detail-value" id="${valueId}">${this.formatValue(value)}</span>
<button class="copy-btn" onclick="copyToClipboard('${valueId}')" title="Copy">📋</button>
</div>
`;
}
};
const metadata = node.metadata || {};
switch (node.type) {
case 'domain':
detailsHtml += createDetailRow('DNS Records', metadata.dns_records);
detailsHtml += createDetailRow('Related Domains (SAN)', metadata.related_domains_san);
detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
detailsHtml += createDetailRow('VirusTotal Data', metadata.virustotal);
break;
case 'ip':
detailsHtml += createDetailRow('DNS Records', metadata.dns_records);
detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
detailsHtml += createDetailRow('VirusTotal Data', metadata.virustotal);
break;
case 'certificate':
detailsHtml += createDetailRow('Certificate Hash', metadata.hash);
detailsHtml += createDetailRow('SANs', metadata.sans);
detailsHtml += createDetailRow('Issuer', metadata.issuer);
detailsHtml += createDetailRow('Validity', `From: ${metadata.not_before || 'N/A'} To: ${metadata.not_after || 'N/A'}`);
break;
case 'asn':
detailsHtml += createDetailRow('ASN', metadata.asn);
detailsHtml += createDetailRow('Description', metadata.description);
break;
case 'large_entity':
detailsHtml += createDetailRow('Discovered Domains', metadata.domains);
break;
} }
// Add timestamps if available
if (node.added_timestamp) {
const addedDate = new Date(node.added_timestamp);
detailsHtml += `<div class="detail-row"><span class="detail-label">Added:</span><span class="detail-value">${addedDate.toLocaleString()}</span></div>`;
}
if (this.elements.modalDetails) { if (this.elements.modalDetails) {
this.elements.modalDetails.innerHTML = detailsHtml; this.elements.modalDetails.innerHTML = detailsHtml;
} }
@ -982,12 +1035,13 @@ class DNSReconApp {
* @returns {string} Formatted value * @returns {string} Formatted value
*/ */
formatValue(value) { formatValue(value) {
if (Array.isArray(value)) { if (typeof value === 'object' && value !== null) {
return value.join(', '); // Use <pre> for nicely formatted JSON
} else if (typeof value === 'object') { return `<pre>${JSON.stringify(value, null, 2)}</pre>`;
return JSON.stringify(value, null, 2);
} else { } else {
return String(value); // Escape HTML to prevent XSS issues with string values
const strValue = String(value);
return strValue.replace(/</g, "&lt;").replace(/>/g, "&gt;");
} }
} }

View File

@ -218,6 +218,18 @@
</div> </div>
</div> </div>
<script>
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
const textToCopy = element.innerText;
navigator.clipboard.writeText(textToCopy).then(() => {
// Optional: Show a success message
console.log('Copied to clipboard');
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
</script>
<script src="{{ url_for('static', filename='js/graph.js') }}"></script> <script src="{{ url_for('static', filename='js/graph.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script> <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body> </body>