iterating on fixes

This commit is contained in:
overcuriousity 2025-09-17 11:08:50 +02:00
parent b984189e08
commit f775c61731
5 changed files with 220 additions and 78 deletions

86
app.py
View File

@ -35,6 +35,29 @@ def get_user_scanner():
existing_scanner = session_manager.get_session(current_flask_session_id) existing_scanner = session_manager.get_session(current_flask_session_id)
if existing_scanner: if existing_scanner:
return current_flask_session_id, existing_scanner return current_flask_session_id, existing_scanner
else:
print(f"Session {current_flask_session_id} not found in Redis, checking for active sessions...")
# This prevents creating duplicate sessions when Flask session is lost but Redis session exists
stats = session_manager.get_statistics()
if stats['running_scans'] > 0:
# Get all session keys and find running ones
try:
import redis
redis_client = redis.StrictRedis(db=0, decode_responses=False)
session_keys = redis_client.keys("dnsrecon:session:*")
for session_key in session_keys:
session_id = session_key.decode('utf-8').split(':')[-1]
scanner = session_manager.get_session(session_id)
if scanner and scanner.status in ['running', 'completed']:
print(f"Reusing active session: {session_id}")
# Update Flask session to match
session['dnsrecon_session_id'] = session_id
session.permanent = True
return session_id, scanner
except Exception as e:
print(f"Error finding active session: {e}")
# Create new session if none exists # Create new session if none exists
print("Creating new session as none was found...") print("Creating new session as none was found...")
@ -44,10 +67,11 @@ def get_user_scanner():
if not new_scanner: if not new_scanner:
raise Exception("Failed to create new scanner session") raise Exception("Failed to create new scanner session")
# Store in Flask session # Store in Flask session with explicit settings
session['dnsrecon_session_id'] = new_session_id session['dnsrecon_session_id'] = new_session_id
session.permanent = True session.permanent = True
print(f"Created new session: {new_session_id}")
return new_session_id, new_scanner return new_session_id, new_scanner
@app.route('/') @app.route('/')
@ -240,29 +264,56 @@ def get_scan_status():
@app.route('/api/graph', methods=['GET']) @app.route('/api/graph', methods=['GET'])
def get_graph_data(): def get_graph_data():
"""Get current graph data with error handling.""" """Get current graph data with error handling and proper empty graph structure."""
try: try:
# Get user-specific scanner # Get user-specific scanner
user_session_id, scanner = get_user_scanner() user_session_id, scanner = get_user_scanner()
if not scanner: if not scanner:
# Return empty graph if no scanner # FIXED: Return proper empty graph structure instead of None
empty_graph = {
'nodes': [],
'edges': [],
'statistics': {
'node_count': 0,
'edge_count': 0,
'creation_time': datetime.now(timezone.utc).isoformat(),
'last_modified': datetime.now(timezone.utc).isoformat()
}
}
return jsonify({ return jsonify({
'success': True, 'success': True,
'graph': { 'graph': empty_graph,
'nodes': [],
'edges': [],
'statistics': {
'node_count': 0,
'edge_count': 0,
'creation_time': datetime.now(timezone.utc).isoformat(),
'last_modified': datetime.now(timezone.utc).isoformat()
}
},
'user_session_id': user_session_id 'user_session_id': user_session_id
}) })
graph_data = scanner.get_graph_data() graph_data = scanner.get_graph_data()
if not graph_data:
graph_data = {
'nodes': [],
'edges': [],
'statistics': {
'node_count': 0,
'edge_count': 0,
'creation_time': datetime.now(timezone.utc).isoformat(),
'last_modified': datetime.now(timezone.utc).isoformat()
}
}
# FIXED: Ensure required fields exist
if 'nodes' not in graph_data:
graph_data['nodes'] = []
if 'edges' not in graph_data:
graph_data['edges'] = []
if 'statistics' not in graph_data:
graph_data['statistics'] = {
'node_count': len(graph_data['nodes']),
'edge_count': len(graph_data['edges']),
'creation_time': datetime.now(timezone.utc).isoformat(),
'last_modified': datetime.now(timezone.utc).isoformat()
}
return jsonify({ return jsonify({
'success': True, 'success': True,
'graph': graph_data, 'graph': graph_data,
@ -272,13 +323,20 @@ def get_graph_data():
except Exception as e: except Exception as e:
print(f"ERROR: Exception in get_graph_data endpoint: {e}") print(f"ERROR: Exception in get_graph_data endpoint: {e}")
traceback.print_exc() traceback.print_exc()
# FIXED: Return proper error structure with empty graph fallback
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Internal server error: {str(e)}', 'error': f'Internal server error: {str(e)}',
'fallback_graph': { 'fallback_graph': {
'nodes': [], 'nodes': [],
'edges': [], 'edges': [],
'statistics': {'node_count': 0, 'edge_count': 0} 'statistics': {
'node_count': 0,
'edge_count': 0,
'creation_time': datetime.now(timezone.utc).isoformat(),
'last_modified': datetime.now(timezone.utc).isoformat()
}
} }
}), 500 }), 500

View File

@ -477,8 +477,13 @@ class GraphManager:
} }
def _get_confidence_distribution(self) -> Dict[str, int]: def _get_confidence_distribution(self) -> Dict[str, int]:
"""Get distribution of edge confidence scores.""" """Get distribution of edge confidence scores with empty graph handling."""
distribution = {'high': 0, 'medium': 0, 'low': 0} distribution = {'high': 0, 'medium': 0, 'low': 0}
# FIXED: Handle empty graph case
if self.get_edge_count() == 0:
return distribution
for _, _, data in self.graph.edges(data=True): for _, _, data in self.graph.edges(data=True):
confidence = data.get('confidence_score', 0) confidence = data.get('confidence_score', 0)
if confidence >= 0.8: if confidence >= 0.8:
@ -490,22 +495,42 @@ class GraphManager:
return distribution return distribution
def get_statistics(self) -> Dict[str, Any]: def get_statistics(self) -> Dict[str, Any]:
"""Get comprehensive statistics about the graph.""" """Get comprehensive statistics about the graph with proper empty graph handling."""
stats = {'basic_metrics': {'total_nodes': self.get_node_count(),
'total_edges': self.get_edge_count(), # FIXED: Handle empty graph case properly
'creation_time': self.creation_time, node_count = self.get_node_count()
'last_modified': self.last_modified}, edge_count = self.get_edge_count()
'node_type_distribution': {}, 'relationship_type_distribution': {},
'confidence_distribution': self._get_confidence_distribution(), stats = {
'provider_distribution': {}} 'basic_metrics': {
# Calculate distributions 'total_nodes': node_count,
for node_type in NodeType: 'total_edges': edge_count,
stats['node_type_distribution'][node_type.value] = self.get_nodes_by_type(node_type).__len__() 'creation_time': self.creation_time,
for _, _, data in self.graph.edges(data=True): 'last_modified': self.last_modified
rel_type = data.get('relationship_type', 'unknown') },
stats['relationship_type_distribution'][rel_type] = stats['relationship_type_distribution'].get(rel_type, 0) + 1 'node_type_distribution': {},
provider = data.get('source_provider', 'unknown') 'relationship_type_distribution': {},
stats['provider_distribution'][provider] = stats['provider_distribution'].get(provider, 0) + 1 'confidence_distribution': self._get_confidence_distribution(),
'provider_distribution': {}
}
# FIXED: Only calculate distributions if we have data
if node_count > 0:
# Calculate node type distributions
for node_type in NodeType:
count = len(self.get_nodes_by_type(node_type))
if count > 0: # Only include types that exist
stats['node_type_distribution'][node_type.value] = count
if edge_count > 0:
# Calculate edge distributions
for _, _, data in self.graph.edges(data=True):
rel_type = data.get('relationship_type', 'unknown')
stats['relationship_type_distribution'][rel_type] = stats['relationship_type_distribution'].get(rel_type, 0) + 1
provider = data.get('source_provider', 'unknown')
stats['provider_distribution'][provider] = stats['provider_distribution'].get(provider, 0) + 1
return stats return stats
def clear(self) -> None: def clear(self) -> None:

View File

@ -50,6 +50,7 @@ class DNSProvider(BaseProvider):
def query_domain(self, domain: str) -> ProviderResult: def query_domain(self, domain: str) -> ProviderResult:
""" """
Query DNS records for the domain to discover relationships and attributes. Query DNS records for the domain to discover relationships and attributes.
FIXED: Now creates separate attributes for each DNS record type.
Args: Args:
domain: Domain to investigate domain: Domain to investigate
@ -62,7 +63,7 @@ class DNSProvider(BaseProvider):
result = ProviderResult() result = ProviderResult()
# Query all record types # Query all record types - each gets its own attribute
for record_type in ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SOA', 'TXT', 'SRV', 'CAA']: for record_type in ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SOA', 'TXT', 'SRV', 'CAA']:
try: try:
self._query_record(domain, record_type, result) self._query_record(domain, record_type, result)
@ -97,6 +98,7 @@ class DNSProvider(BaseProvider):
response = self.resolver.resolve(reverse_name, 'PTR') response = self.resolver.resolve(reverse_name, 'PTR')
self.successful_requests += 1 self.successful_requests += 1
ptr_records = []
for ptr_record in response: for ptr_record in response:
hostname = str(ptr_record).rstrip('.') hostname = str(ptr_record).rstrip('.')
@ -116,16 +118,8 @@ class DNSProvider(BaseProvider):
} }
) )
# Add PTR record as attribute to the IP # Add to PTR records list
result.add_attribute( ptr_records.append(f"PTR: {hostname}")
target_node=ip,
name='ptr_record',
value=hostname,
attr_type='dns_record',
provider=self.name,
confidence=0.8,
metadata={'ttl': response.ttl}
)
# Log the relationship discovery # Log the relationship discovery
self.log_relationship_discovery( self.log_relationship_discovery(
@ -142,6 +136,18 @@ class DNSProvider(BaseProvider):
discovery_method="reverse_dns_lookup" 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: except resolver.NXDOMAIN:
self.failed_requests += 1 self.failed_requests += 1
self.logger.logger.debug(f"Reverse DNS lookup failed for {ip}: NXDOMAIN") self.logger.logger.debug(f"Reverse DNS lookup failed for {ip}: NXDOMAIN")
@ -155,7 +161,7 @@ class DNSProvider(BaseProvider):
def _query_record(self, domain: str, record_type: str, result: ProviderResult) -> None: def _query_record(self, domain: str, record_type: str, result: ProviderResult) -> None:
""" """
UPDATED: Query DNS records with minimal formatting - keep raw values. FIXED: Query DNS records with unique attribute names for each record type.
""" """
try: try:
self.total_requests += 1 self.total_requests += 1
@ -175,16 +181,16 @@ class DNSProvider(BaseProvider):
elif record_type == 'SOA': elif record_type == 'SOA':
target = str(record.mname).rstrip('.') target = str(record.mname).rstrip('.')
elif record_type in ['TXT']: elif record_type in ['TXT']:
# UPDATED: Keep raw TXT record value # Keep raw TXT record value
txt_value = str(record).strip('"') txt_value = str(record).strip('"')
dns_records.append(f"TXT: {txt_value}") dns_records.append(txt_value) # Just the value for TXT
continue continue
elif record_type == 'SRV': elif record_type == 'SRV':
target = str(record.target).rstrip('.') target = str(record.target).rstrip('.')
elif record_type == 'CAA': elif record_type == 'CAA':
# UPDATED: Keep raw CAA record format # Keep raw CAA record format
caa_value = f"{record.flags} {record.tag.decode('utf-8')} \"{record.value.decode('utf-8')}\"" caa_value = f"{record.flags} {record.tag.decode('utf-8')} \"{record.value.decode('utf-8')}\""
dns_records.append(f"CAA: {caa_value}") dns_records.append(caa_value) # Just the value for CAA
continue continue
else: else:
target = str(record) target = str(record)
@ -196,7 +202,7 @@ class DNSProvider(BaseProvider):
'value': target, 'value': target,
'ttl': response.ttl 'ttl': response.ttl
} }
relationship_type = f"{record_type.lower()}_record" # Raw relationship type relationship_type = f"{record_type.lower()}_record"
confidence = 0.8 confidence = 0.8
# Add relationship # Add relationship
@ -209,8 +215,8 @@ class DNSProvider(BaseProvider):
raw_data=raw_data raw_data=raw_data
) )
# UPDATED: Keep raw DNS record format # Add target to records list
dns_records.append(f"{record_type}: {target}") dns_records.append(target)
# Log relationship discovery # Log relationship discovery
self.log_relationship_discovery( self.log_relationship_discovery(
@ -222,20 +228,22 @@ class DNSProvider(BaseProvider):
discovery_method=f"dns_{record_type.lower()}_record" discovery_method=f"dns_{record_type.lower()}_record"
) )
# Add DNS records as a consolidated attribute (raw format) # FIXED: Create attribute with specific name for each record type
if dns_records: 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( result.add_attribute(
target_node=domain, target_node=domain,
name='dns_records', name=attribute_name, # UNIQUE name for each record type!
value=dns_records, value=dns_records,
attr_type='dns_record_list', attr_type='dns_record_list',
provider=self.name, provider=self.name,
confidence=0.8, confidence=0.8,
metadata={'record_types': [record_type]} metadata={'record_type': record_type, 'ttl': response.ttl}
) )
except Exception as e: except Exception as e:
self.failed_requests += 1 self.failed_requests += 1
self.logger.logger.debug(f"{record_type} record query failed for {domain}: {e}") self.logger.logger.debug(f"{record_type} record query failed for {domain}: {e}")
raise e raise e

View File

@ -377,6 +377,21 @@ class GraphManager {
this.initialize(); this.initialize();
} }
// Check if we have actual data to display
const hasData = graphData.nodes.length > 0 || graphData.edges.length > 0;
// Handle placeholder visibility
const placeholder = this.container.querySelector('.graph-placeholder');
if (placeholder) {
if (hasData) {
placeholder.style.display = 'none';
} else {
placeholder.style.display = 'flex';
// Early return if no data to process
return;
}
}
this.largeEntityMembers.clear(); this.largeEntityMembers.clear();
const largeEntityMap = new Map(); const largeEntityMap = new Map();
@ -398,11 +413,11 @@ class GraphManager {
console.log(`Filtered ${graphData.nodes.length - filteredNodes.length} large entity member nodes from visualization`); console.log(`Filtered ${graphData.nodes.length - filteredNodes.length} large entity member nodes from visualization`);
// FIXED: Process nodes with proper certificate coloring // Process nodes with proper certificate coloring
const processedNodes = filteredNodes.map(node => { const processedNodes = filteredNodes.map(node => {
const processed = this.processNode(node); const processed = this.processNode(node);
// FIXED: Apply certificate-based coloring here in frontend // Apply certificate-based coloring here in frontend
if (node.type === 'domain' && Array.isArray(node.attributes)) { if (node.type === 'domain' && Array.isArray(node.attributes)) {
const certInfo = this.analyzeCertificateInfo(node.attributes); const certInfo = this.analyzeCertificateInfo(node.attributes);

View File

@ -35,6 +35,9 @@ class DNSReconApp {
this.loadProviders(); this.loadProviders();
this.initializeEnhancedModals(); this.initializeEnhancedModals();
// FIXED: Force initial graph update to handle empty sessions properly
this.updateGraph();
console.log('DNSRecon application initialized successfully'); console.log('DNSRecon application initialized successfully');
} catch (error) { } catch (error) {
console.error('Failed to initialize DNSRecon application:', error); console.error('Failed to initialize DNSRecon application:', error);
@ -484,9 +487,8 @@ class DNSReconApp {
console.log('- Nodes:', graphData.nodes ? graphData.nodes.length : 0); console.log('- Nodes:', graphData.nodes ? graphData.nodes.length : 0);
console.log('- Edges:', graphData.edges ? graphData.edges.length : 0); console.log('- Edges:', graphData.edges ? graphData.edges.length : 0);
// Only update if data has changed // FIXED: Always update graph, even if empty - let GraphManager handle placeholder
if (this.hasGraphChanged(graphData)) { if (this.graphManager) {
console.log('*** GRAPH DATA CHANGED - UPDATING VISUALIZATION ***');
this.graphManager.updateGraph(graphData); this.graphManager.updateGraph(graphData);
this.lastGraphUpdate = Date.now(); this.lastGraphUpdate = Date.now();
@ -495,19 +497,31 @@ class DNSReconApp {
if (this.elements.relationshipsDisplay) { if (this.elements.relationshipsDisplay) {
this.elements.relationshipsDisplay.textContent = edgeCount; this.elements.relationshipsDisplay.textContent = edgeCount;
} }
} else {
console.log('Graph data unchanged, skipping update');
} }
} else { } else {
console.error('Graph update failed:', response); console.error('Graph update failed:', response);
// FIXED: Show placeholder when graph update fails
if (this.graphManager && this.graphManager.container) {
const placeholder = this.graphManager.container.querySelector('.graph-placeholder');
if (placeholder) {
placeholder.style.display = 'flex';
}
}
} }
} catch (error) { } catch (error) {
console.error('Failed to update graph:', error); console.error('Failed to update graph:', error);
// Don't show error for graph updates to avoid spam // FIXED: Show placeholder on error
if (this.graphManager && this.graphManager.container) {
const placeholder = this.graphManager.container.querySelector('.graph-placeholder');
if (placeholder) {
placeholder.style.display = 'flex';
}
}
} }
} }
/** /**
* Update status display elements * Update status display elements
* @param {Object} status - Status object from server * @param {Object} status - Status object from server
@ -925,18 +939,32 @@ class DNSReconApp {
return 'Empty Array'; return 'Empty Array';
} }
// Special handling for DNS records and similar arrays // ENHANCED: Special handling for specific DNS record types
if (name === 'dns_records' || name.includes('record') || name.includes('hostname')) { if (name.endsWith('_records') || name.includes('record')) {
// Show DNS records as a readable list const recordType = name.replace('_records', '').toUpperCase();
// Format nicely for DNS records
if (value.length <= 5) { if (value.length <= 5) {
return this.escapeHtml(value.join(', ')); const formattedRecords = value.map(record => {
// Add record type prefix if not already present
if (recordType !== 'DNS' && !record.includes(':')) {
return `${recordType}: ${record}`;
}
return record;
});
return this.escapeHtml(formattedRecords.join('\n'));
} else { } else {
const preview = value.slice(0, 3).join(', '); const preview = value.slice(0, 3).map(record => {
return this.escapeHtml(`${preview} ... (+${value.length - 3} more)`); if (recordType !== 'DNS' && !record.includes(':')) {
return `${recordType}: ${record}`;
}
return record;
}).join('\n');
return this.escapeHtml(`${preview}\n... (+${value.length - 3} more ${recordType} records)`);
} }
} }
// For other arrays // For other arrays (existing logic)
if (value.length <= 3) { if (value.length <= 3) {
return this.escapeHtml(value.join(', ')); return this.escapeHtml(value.join(', '));
} else { } else {
@ -953,6 +981,10 @@ class DNSReconApp {
} }
groupAttributesByProviderAndType(attributes, nodeType) { groupAttributesByProviderAndType(attributes, nodeType) {
if (!Array.isArray(attributes) || attributes.length === 0) {
return {};
}
const groups = { const groups = {
'DNS Records': { icon: '📋', priority: 'high', attributes: [] }, 'DNS Records': { icon: '📋', priority: 'high', attributes: [] },
'Certificate Information': { icon: '🔒', priority: 'high', attributes: [] }, 'Certificate Information': { icon: '🔒', priority: 'high', attributes: [] },
@ -968,11 +1000,11 @@ class DNSReconApp {
let assigned = false; let assigned = false;
// DNS-related attributes (better detection) // ENHANCED: Better DNS record detection for specific record types
if (provider === 'dns' || if (provider === 'dns' ||
name === 'dns_records' || name.endsWith('_records') || // Catches a_records, mx_records, txt_records, etc.
name.includes('record') || name.includes('record') ||
['ptr', 'mx', 'cname', 'ns', 'txt', 'soa'].some(keyword => name.includes(keyword))) { ['ptr', 'mx', 'cname', 'ns', 'txt', 'soa', 'srv', 'caa', 'a_records', 'aaaa_records'].some(keyword => name.includes(keyword))) {
groups['DNS Records'].attributes.push(attr); groups['DNS Records'].attributes.push(attr);
assigned = true; assigned = true;
} }
@ -1013,7 +1045,6 @@ class DNSReconApp {
formatEdgeLabel(relationshipType, confidence) { formatEdgeLabel(relationshipType, confidence) {
if (!relationshipType) return ''; if (!relationshipType) return '';
// UPDATED: No formatting of relationship type - use raw values
const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○'; const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
return `${relationshipType} ${confidenceText}`; return `${relationshipType} ${confidenceText}`;
} }
@ -1695,13 +1726,18 @@ class DNSReconApp {
const newNodeCount = graphData.nodes ? graphData.nodes.length : 0; const newNodeCount = graphData.nodes ? graphData.nodes.length : 0;
const newEdgeCount = graphData.edges ? graphData.edges.length : 0; const newEdgeCount = graphData.edges ? graphData.edges.length : 0;
// FIXED: Always update if we currently have no data (ensures placeholder is handled correctly)
if (currentStats.nodeCount === 0 && currentStats.edgeCount === 0) {
return true;
}
// Check if counts changed // Check if counts changed
const countsChanged = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount; const countsChanged = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount;
// Also check if we have new timestamp data // Also check if we have new timestamp data
const hasNewTimestamp = graphData.statistics && const hasNewTimestamp = graphData.statistics &&
graphData.statistics.last_modified && graphData.statistics.last_modified &&
graphData.statistics.last_modified !== this.lastGraphTimestamp; graphData.statistics.last_modified !== this.lastGraphTimestamp;
if (hasNewTimestamp) { if (hasNewTimestamp) {
this.lastGraphTimestamp = graphData.statistics.last_modified; this.lastGraphTimestamp = graphData.statistics.last_modified;