export manager modularized

This commit is contained in:
overcuriousity 2025-09-18 17:42:39 +02:00
parent 15227b392d
commit d4081e1a32
6 changed files with 406 additions and 243 deletions

126
app.py
View File

@ -16,6 +16,7 @@ from core.session_manager import session_manager
from config import config
from core.graph_manager import NodeType
from utils.helpers import is_valid_target
from utils.export_manager import export_manager
from decimal import Decimal
@ -45,28 +46,7 @@ def get_user_scanner():
return new_session_id, new_scanner
class CustomJSONEncoder(json.JSONEncoder):
"""Custom JSON encoder to handle non-serializable objects."""
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, set):
return list(obj)
elif isinstance(obj, Decimal):
return float(obj)
elif hasattr(obj, '__dict__'):
# For custom objects, try to serialize their dict representation
try:
return obj.__dict__
except:
return str(obj)
elif hasattr(obj, 'value') and hasattr(obj, 'name'):
# For enum objects
return obj.value
else:
# For any other non-serializable object, convert to string
return str(obj)
@app.route('/')
def index():
"""Serve the main web interface."""
@ -105,7 +85,7 @@ def start_scan():
if success:
return jsonify({
'success': True,
'message': 'Scan started successfully',
'message': 'Reconnaissance scan started successfully',
'scan_id': scanner.logger.session_id,
'user_session_id': user_session_id
})
@ -309,40 +289,29 @@ def export_results():
if not scanner:
return jsonify({'success': False, 'error': 'No active scanner session found'}), 404
# Get export data with error handling
# Get export data using the new export manager
try:
results = scanner.export_results()
results = export_manager.export_scan_results(scanner)
except Exception as e:
return jsonify({'success': False, 'error': f'Failed to gather export data: {str(e)}'}), 500
# Add export metadata
results['export_metadata'] = {
'user_session_id': user_session_id,
'export_timestamp': datetime.now(timezone.utc).isoformat(),
'export_version': '1.0.0',
'forensic_integrity': 'maintained'
}
# Add user session metadata
results['export_metadata']['user_session_id'] = user_session_id
results['export_metadata']['forensic_integrity'] = 'maintained'
# Generate filename with forensic naming convention
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
target = scanner.current_target or 'unknown'
# Sanitize target for filename
safe_target = "".join(c for c in target if c.isalnum() or c in ('-', '_', '.')).rstrip()
filename = f"dnsrecon_{safe_target}_{timestamp}.json"
# Generate filename
filename = export_manager.generate_filename(
target=scanner.current_target or 'unknown',
export_type='json'
)
# Serialize with custom encoder and error handling
# Serialize with export manager
try:
json_data = json.dumps(results, indent=2, cls=CustomJSONEncoder, ensure_ascii=False)
json_data = export_manager.serialize_to_json(results)
except Exception as e:
# If custom encoder fails, try a more aggressive approach
try:
# Convert problematic objects to strings recursively
cleaned_results = _clean_for_json(results)
json_data = json.dumps(cleaned_results, indent=2, ensure_ascii=False)
except Exception as e2:
return jsonify({
'success': False,
'error': f'JSON serialization failed: {str(e2)}'
'error': f'JSON serialization failed: {str(e)}'
}), 500
# Create file object
@ -371,11 +340,14 @@ def export_targets():
if not scanner:
return jsonify({'success': False, 'error': 'No active scanner session found'}), 404
targets_txt = scanner.export_targets_txt()
# Use export manager for targets export
targets_txt = export_manager.export_targets_list(scanner)
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
safe_target = "".join(c for c in (scanner.current_target or 'unknown') if c.isalnum() or c in ('-', '_', '.')).rstrip()
filename = f"dnsrecon_targets_{safe_target}_{timestamp}.txt"
# Generate filename using export manager
filename = export_manager.generate_filename(
target=scanner.current_target or 'unknown',
export_type='targets'
)
file_obj = io.BytesIO(targets_txt.encode('utf-8'))
@ -398,11 +370,14 @@ def export_summary():
if not scanner:
return jsonify({'success': False, 'error': 'No active scanner session found'}), 404
summary_txt = scanner.generate_executive_summary()
# Use export manager for summary generation
summary_txt = export_manager.generate_executive_summary(scanner)
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
safe_target = "".join(c for c in (scanner.current_target or 'unknown') if c.isalnum() or c in ('-', '_', '.')).rstrip()
filename = f"dnsrecon_summary_{safe_target}_{timestamp}.txt"
# Generate filename using export manager
filename = export_manager.generate_filename(
target=scanner.current_target or 'unknown',
export_type='summary'
)
file_obj = io.BytesIO(summary_txt.encode('utf-8'))
@ -416,49 +391,6 @@ def export_summary():
traceback.print_exc()
return jsonify({'success': False, 'error': f'Export failed: {str(e)}'}), 500
def _clean_for_json(obj, max_depth=10, current_depth=0):
"""
Recursively clean an object to make it JSON serializable.
Handles circular references and problematic object types.
"""
if current_depth > max_depth:
return f"<max_depth_exceeded_{type(obj).__name__}>"
if obj is None or isinstance(obj, (bool, int, float, str)):
return obj
elif isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, (set, frozenset)):
return list(obj)
elif isinstance(obj, dict):
cleaned = {}
for key, value in obj.items():
try:
# Ensure key is string
clean_key = str(key) if not isinstance(key, str) else key
cleaned[clean_key] = _clean_for_json(value, max_depth, current_depth + 1)
except Exception:
cleaned[str(key)] = f"<serialization_error_{type(value).__name__}>"
return cleaned
elif isinstance(obj, (list, tuple)):
cleaned = []
for item in obj:
try:
cleaned.append(_clean_for_json(item, max_depth, current_depth + 1))
except Exception:
cleaned.append(f"<serialization_error_{type(item).__name__}>")
return cleaned
elif hasattr(obj, '__dict__'):
try:
return _clean_for_json(obj.__dict__, max_depth, current_depth + 1)
except Exception:
return str(obj)
elif hasattr(obj, 'value'):
# For enum-like objects
return obj.value
else:
return str(obj)
@app.route('/api/config/api-keys', methods=['POST'])
def set_api_keys():
"""Set API keys for the current session."""

View File

@ -5,6 +5,7 @@ Graph data model for DNSRecon using NetworkX.
Manages in-memory graph storage with confidence scoring and forensic metadata.
Now fully compatible with the unified ProviderResult data model.
UPDATED: Fixed correlation exclusion keys to match actual attribute names.
UPDATED: Removed export_json() method - now handled by ExportManager.
"""
import re
from datetime import datetime, timezone
@ -212,7 +213,7 @@ class GraphManager:
def _has_direct_edge_bidirectional(self, node_a: str, node_b: str) -> bool:
"""
Check if there's a direct edge between two nodes in either direction.
Returns True if node_aââ 'node_b OR node_bâ†'node_a exists.
Returns True if node_aââ⬠'node_b OR node_bââ⬠'node_a exists.
"""
return (self.graph.has_edge(node_a, node_b) or
self.graph.has_edge(node_b, node_a))
@ -503,22 +504,6 @@ class GraphManager:
'statistics': self.get_statistics()['basic_metrics']
}
def export_json(self) -> Dict[str, Any]:
"""Export complete graph data as a JSON-serializable dictionary."""
graph_data = nx.node_link_data(self.graph, edges="edges")
return {
'export_metadata': {
'export_timestamp': datetime.now(timezone.utc).isoformat(),
'graph_creation_time': self.creation_time,
'last_modified': self.last_modified,
'total_nodes': self.get_node_count(),
'total_edges': self.get_edge_count(),
'graph_format': 'dnsrecon_v1_unified_model'
},
'graph': graph_data,
'statistics': self.get_statistics()
}
def _get_confidence_distribution(self) -> Dict[str, int]:
"""Get distribution of edge confidence scores with empty graph handling."""
distribution = {'high': 0, 'medium': 0, 'low': 0}

View File

@ -17,6 +17,7 @@ from core.graph_manager import GraphManager, NodeType
from core.logger import get_forensic_logger, new_session
from core.provider_result import ProviderResult
from utils.helpers import _is_valid_ip, _is_valid_domain
from utils.export_manager import export_manager
from providers.base_provider import BaseProvider
from core.rate_limiter import GlobalRateLimiter
@ -868,114 +869,6 @@ class Scanner:
graph_data['initial_targets'] = list(self.initial_targets)
return graph_data
def export_results(self) -> Dict[str, Any]:
graph_data = self.graph.export_json()
audit_trail = self.logger.export_audit_trail()
provider_stats = {}
for provider in self.providers:
provider_stats[provider.get_name()] = provider.get_statistics()
return {
'scan_metadata': {
'target_domain': self.current_target, 'max_depth': self.max_depth,
'final_status': self.status, 'total_indicators_processed': self.indicators_processed,
'enabled_providers': list(provider_stats.keys()), 'session_id': self.session_id
},
'graph_data': graph_data,
'forensic_audit': audit_trail,
'provider_statistics': provider_stats,
'scan_summary': self.logger.get_forensic_summary()
}
def export_targets_txt(self) -> str:
"""Export all discovered domains and IPs as a text file."""
nodes = self.graph.get_graph_data().get('nodes', [])
targets = {node['id'] for node in nodes if _is_valid_domain(node['id']) or _is_valid_ip(node['id'])}
return "\n".join(sorted(list(targets)))
def generate_executive_summary(self) -> str:
"""Generate a natural-language executive summary of the scan results."""
summary = []
now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S %Z')
scan_metadata = self.get_scan_status()
graph_data = self.graph.get_graph_data()
nodes = graph_data.get('nodes', [])
edges = graph_data.get('edges', [])
summary.append(f"DNSRecon Executive Summary")
summary.append(f"Report Generated: {now}")
summary.append("="*40)
# Scan Overview
summary.append("\n## Scan Overview")
summary.append(f"- Initial Target: {self.current_target}")
summary.append(f"- Scan Status: {self.status.capitalize()}")
summary.append(f"- Analysis Depth: {self.max_depth}")
summary.append(f"- Total Indicators Found: {len(nodes)}")
summary.append(f"- Total Relationships Discovered: {len(edges)}")
# Key Findings
summary.append("\n## Key Findings")
domains = [n for n in nodes if n['type'] == 'domain']
ips = [n for n in nodes if n['type'] == 'ip']
isps = [n for n in nodes if n['type'] == 'isp']
cas = [n for n in nodes if n['type'] == 'ca']
summary.append(f"- Discovered {len(domains)} unique domain(s).")
summary.append(f"- Identified {len(ips)} unique IP address(es).")
if isps:
summary.append(f"- Infrastructure is hosted across {len(isps)} unique ISP(s).")
if cas:
summary.append(f"- Found certificates issued by {len(cas)} unique Certificate Authorit(y/ies).")
# Detailed Findings
summary.append("\n## Detailed Findings")
# Domain Analysis
if domains:
summary.append("\n### Domain Analysis")
for domain in domains[:5]: # report on first 5
summary.append(f"\n- Domain: {domain['id']}")
# Associated IPs
associated_ips = [edge['to'] for edge in edges if edge['from'] == domain['id'] and _is_valid_ip(edge['to'])]
if associated_ips:
summary.append(f" - Associated IPs: {', '.join(associated_ips)}")
# Certificate info
cert_attributes = [attr for attr in domain.get('attributes', []) if attr.get('name', '').startswith('cert_')]
if cert_attributes:
issuer = next((attr['value'] for attr in cert_attributes if attr['name'] == 'cert_issuer_name'), 'N/A')
valid_until = next((attr['value'] for attr in cert_attributes if attr['name'] == 'cert_not_after'), 'N/A')
summary.append(f" - Certificate Issuer: {issuer}")
summary.append(f" - Certificate Valid Until: {valid_until}")
# IP Address Analysis
if ips:
summary.append("\n### IP Address Analysis")
for ip in ips[:5]: # report on first 5
summary.append(f"\n- IP Address: {ip['id']}")
# Hostnames
hostnames = [edge['to'] for edge in edges if edge['from'] == ip['id'] and _is_valid_domain(edge['to'])]
if hostnames:
summary.append(f" - Associated Hostnames: {', '.join(hostnames)}")
# ISP
isp_edge = next((edge for edge in edges if edge['from'] == ip['id'] and self.graph.graph.nodes[edge['to']]['type'] == 'isp'), None)
if isp_edge:
summary.append(f" - ISP: {isp_edge['to']}")
# Data Sources
summary.append("\n## Data Sources")
provider_stats = self.logger.get_forensic_summary().get('provider_statistics', {})
for provider, stats in provider_stats.items():
summary.append(f"- {provider.capitalize()}: {stats.get('relationships_discovered', 0)} relationships from {stats.get('successful_requests', 0)} requests.")
summary.append("\n" + "="*40)
summary.append("End of Report")
return "\n".join(summary)
def get_provider_info(self) -> Dict[str, Dict[str, Any]]:
info = {}

View File

@ -146,35 +146,30 @@ class ShodanProvider(BaseProvider):
result = self._process_shodan_data(normalized_ip, data)
self._save_to_cache(cache_file, result, data) # Save both result and raw data
elif response and response.status_code == 404:
# Handle 404 "No information available" as successful empty result
try:
error_data = response.json()
if "No information available" in error_data.get('error', ''):
# This is a successful query - Shodan just has no data
# Handle all 404s as successful "no information available" responses
# Shodan returns 404 when no information is available for an IP
self.logger.logger.debug(f"Shodan has no information for {normalized_ip}")
result = ProviderResult() # Empty but successful result
# Cache the empty result to avoid repeated queries
self._save_to_cache(cache_file, result, {'error': 'No information available'})
else:
# Some other 404 error - treat as failure
raise requests.exceptions.RequestException(f"Shodan API returned 404: {error_data}")
except (ValueError, KeyError):
# Could not parse JSON response - treat as failure
raise requests.exceptions.RequestException(f"Shodan API returned 404 with unparseable response")
elif cache_status == "stale":
# If API fails on a stale cache, use the old data
result = self._load_from_cache(cache_file)
self.logger.logger.info(f"Using stale cache for {normalized_ip} due to API failure")
else:
# Other HTTP error codes should be treated as failures
status_code = response.status_code if response else "No response"
raise requests.exceptions.RequestException(f"Shodan API returned HTTP {status_code}")
except requests.exceptions.RequestException as e:
self.logger.logger.info(f"Shodan API query returned no info for {normalized_ip}: {e}")
self.logger.logger.debug(f"Shodan API error for {normalized_ip}: {e}")
if cache_status == "stale":
# Use stale cache if available
result = self._load_from_cache(cache_file)
self.logger.logger.info(f"Using stale cache for {normalized_ip} due to API error")
else:
# Re-raise for retry scheduling - but only for actual failures
# FIXED: Only re-raise for actual network/timeout errors, not 404s
# 404s are already handled above as successful empty results
raise e
return result

View File

@ -0,0 +1,22 @@
# dnsrecon-reduced/utils/__init__.py
"""
Utility modules for DNSRecon.
Contains helper functions, export management, and supporting utilities.
"""
from .helpers import is_valid_target, _is_valid_domain, _is_valid_ip, get_ip_version, normalize_ip
from .export_manager import export_manager, ExportManager, CustomJSONEncoder
__all__ = [
'is_valid_target',
'_is_valid_domain',
'_is_valid_ip',
'get_ip_version',
'normalize_ip',
'export_manager',
'ExportManager',
'CustomJSONEncoder'
]
__version__ = "1.0.0"

336
utils/export_manager.py Normal file
View File

@ -0,0 +1,336 @@
# dnsrecon-reduced/utils/export_manager.py
"""
Centralized export functionality for DNSRecon.
Handles all data export operations with forensic integrity and proper formatting.
"""
import json
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional
from decimal import Decimal
from utils.helpers import _is_valid_domain, _is_valid_ip
import networkx as nx
class ExportManager:
"""
Centralized manager for all DNSRecon export operations.
Maintains forensic integrity and provides consistent export formats.
"""
def __init__(self):
"""Initialize export manager."""
pass
def export_scan_results(self, scanner) -> Dict[str, Any]:
"""
Export complete scan results with forensic metadata.
Args:
scanner: Scanner instance with completed scan data
Returns:
Complete scan results dictionary
"""
graph_data = self.export_graph_json(scanner.graph)
audit_trail = scanner.logger.export_audit_trail()
provider_stats = {}
for provider in scanner.providers:
provider_stats[provider.get_name()] = provider.get_statistics()
results = {
'scan_metadata': {
'target_domain': scanner.current_target,
'max_depth': scanner.max_depth,
'final_status': scanner.status,
'total_indicators_processed': scanner.indicators_processed,
'enabled_providers': list(provider_stats.keys()),
'session_id': scanner.session_id
},
'graph_data': graph_data,
'forensic_audit': audit_trail,
'provider_statistics': provider_stats,
'scan_summary': scanner.logger.get_forensic_summary()
}
# Add export metadata
results['export_metadata'] = {
'export_timestamp': datetime.now(timezone.utc).isoformat(),
'export_version': '1.0.0',
'forensic_integrity': 'maintained'
}
return results
def export_targets_list(self, scanner) -> str:
"""
Export all discovered domains and IPs as a text file.
Args:
scanner: Scanner instance with graph data
Returns:
Newline-separated list of targets
"""
nodes = scanner.graph.get_graph_data().get('nodes', [])
targets = {
node['id'] for node in nodes
if _is_valid_domain(node['id']) or _is_valid_ip(node['id'])
}
return "\n".join(sorted(list(targets)))
def generate_executive_summary(self, scanner) -> str:
"""
Generate a natural-language executive summary of scan results.
Args:
scanner: Scanner instance with completed scan data
Returns:
Formatted executive summary text
"""
summary = []
now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S %Z')
scan_metadata = scanner.get_scan_status()
graph_data = scanner.graph.get_graph_data()
nodes = graph_data.get('nodes', [])
edges = graph_data.get('edges', [])
summary.append(f"DNSRecon Executive Summary")
summary.append(f"Report Generated: {now}")
summary.append("="*40)
# Scan Overview
summary.append("\n## Scan Overview")
summary.append(f"- Initial Target: {scanner.current_target}")
summary.append(f"- Scan Status: {scanner.status.capitalize()}")
summary.append(f"- Analysis Depth: {scanner.max_depth}")
summary.append(f"- Total Indicators Found: {len(nodes)}")
summary.append(f"- Total Relationships Discovered: {len(edges)}")
# Key Findings
summary.append("\n## Key Findings")
domains = [n for n in nodes if n['type'] == 'domain']
ips = [n for n in nodes if n['type'] == 'ip']
isps = [n for n in nodes if n['type'] == 'isp']
cas = [n for n in nodes if n['type'] == 'ca']
summary.append(f"- Discovered {len(domains)} unique domain(s).")
summary.append(f"- Identified {len(ips)} unique IP address(es).")
if isps:
summary.append(f"- Infrastructure is hosted across {len(isps)} unique ISP(s).")
if cas:
summary.append(f"- Found certificates issued by {len(cas)} unique Certificate Authorit(y/ies).")
# Detailed Findings
summary.append("\n## Detailed Findings")
# Domain Analysis
if domains:
summary.append("\n### Domain Analysis")
for domain in domains[:5]: # Report on first 5
summary.append(f"\n- Domain: {domain['id']}")
# Associated IPs
associated_ips = [edge['to'] for edge in edges
if edge['from'] == domain['id'] and _is_valid_ip(edge['to'])]
if associated_ips:
summary.append(f" - Associated IPs: {', '.join(associated_ips)}")
# Certificate info
cert_attributes = [attr for attr in domain.get('attributes', [])
if attr.get('name', '').startswith('cert_')]
if cert_attributes:
issuer = next((attr['value'] for attr in cert_attributes
if attr['name'] == 'cert_issuer_name'), 'N/A')
valid_until = next((attr['value'] for attr in cert_attributes
if attr['name'] == 'cert_not_after'), 'N/A')
summary.append(f" - Certificate Issuer: {issuer}")
summary.append(f" - Certificate Valid Until: {valid_until}")
# IP Address Analysis
if ips:
summary.append("\n### IP Address Analysis")
for ip in ips[:5]: # Report on first 5
summary.append(f"\n- IP Address: {ip['id']}")
# Hostnames
hostnames = [edge['to'] for edge in edges
if edge['from'] == ip['id'] and _is_valid_domain(edge['to'])]
if hostnames:
summary.append(f" - Associated Hostnames: {', '.join(hostnames)}")
# ISP
isp_edge = next((edge for edge in edges
if edge['from'] == ip['id'] and
any(node['id'] == edge['to'] and node['type'] == 'isp'
for node in nodes)), None)
if isp_edge:
summary.append(f" - ISP: {isp_edge['to']}")
# Data Sources
summary.append("\n## Data Sources")
provider_stats = scanner.logger.get_forensic_summary().get('provider_statistics', {})
for provider, stats in provider_stats.items():
relationships = stats.get('relationships_discovered', 0)
requests = stats.get('successful_requests', 0)
summary.append(f"- {provider.capitalize()}: {relationships} relationships from {requests} requests.")
summary.append("\n" + "="*40)
summary.append("End of Report")
return "\n".join(summary)
def export_graph_json(self, graph_manager) -> Dict[str, Any]:
"""
Export complete graph data as a JSON-serializable dictionary.
Moved from GraphManager to centralize export functionality.
Args:
graph_manager: GraphManager instance with graph data
Returns:
Complete graph data with export metadata
"""
graph_data = nx.node_link_data(graph_manager.graph, edges="edges")
return {
'export_metadata': {
'export_timestamp': datetime.now(timezone.utc).isoformat(),
'graph_creation_time': graph_manager.creation_time,
'last_modified': graph_manager.last_modified,
'total_nodes': graph_manager.get_node_count(),
'total_edges': graph_manager.get_edge_count(),
'graph_format': 'dnsrecon_v1_unified_model'
},
'graph': graph_data,
'statistics': graph_manager.get_statistics()
}
def serialize_to_json(self, data: Dict[str, Any], indent: int = 2) -> str:
"""
Serialize data to JSON with custom handling for non-serializable objects.
Args:
data: Data to serialize
indent: JSON indentation level
Returns:
JSON string representation
"""
try:
return json.dumps(data, indent=indent, cls=CustomJSONEncoder, ensure_ascii=False)
except Exception:
# Fallback to aggressive cleaning
cleaned_data = self._clean_for_json(data)
return json.dumps(cleaned_data, indent=indent, ensure_ascii=False)
def _clean_for_json(self, obj, max_depth: int = 10, current_depth: int = 0) -> Any:
"""
Recursively clean an object to make it JSON serializable.
Handles circular references and problematic object types.
Args:
obj: Object to clean
max_depth: Maximum recursion depth
current_depth: Current recursion depth
Returns:
JSON-serializable object
"""
if current_depth > max_depth:
return f"<max_depth_exceeded_{type(obj).__name__}>"
if obj is None or isinstance(obj, (bool, int, float, str)):
return obj
elif isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, (set, frozenset)):
return list(obj)
elif isinstance(obj, dict):
cleaned = {}
for key, value in obj.items():
try:
# Ensure key is string
clean_key = str(key) if not isinstance(key, str) else key
cleaned[clean_key] = self._clean_for_json(value, max_depth, current_depth + 1)
except Exception:
cleaned[str(key)] = f"<serialization_error_{type(value).__name__}>"
return cleaned
elif isinstance(obj, (list, tuple)):
cleaned = []
for item in obj:
try:
cleaned.append(self._clean_for_json(item, max_depth, current_depth + 1))
except Exception:
cleaned.append(f"<serialization_error_{type(item).__name__}>")
return cleaned
elif hasattr(obj, '__dict__'):
try:
return self._clean_for_json(obj.__dict__, max_depth, current_depth + 1)
except Exception:
return str(obj)
elif hasattr(obj, 'value'):
# For enum-like objects
return obj.value
else:
return str(obj)
def generate_filename(self, target: str, export_type: str, timestamp: Optional[datetime] = None) -> str:
"""
Generate standardized filename for exports.
Args:
target: Target domain/IP being scanned
export_type: Type of export (json, txt, summary)
timestamp: Optional timestamp (defaults to now)
Returns:
Formatted filename with forensic naming convention
"""
if timestamp is None:
timestamp = datetime.now(timezone.utc)
timestamp_str = timestamp.strftime('%Y%m%d_%H%M%S')
safe_target = "".join(c for c in target if c.isalnum() or c in ('-', '_', '.')).rstrip()
extension_map = {
'json': 'json',
'txt': 'txt',
'summary': 'txt',
'targets': 'txt'
}
extension = extension_map.get(export_type, 'txt')
return f"dnsrecon_{export_type}_{safe_target}_{timestamp_str}.{extension}"
class CustomJSONEncoder(json.JSONEncoder):
"""Custom JSON encoder to handle non-serializable objects."""
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, set):
return list(obj)
elif isinstance(obj, Decimal):
return float(obj)
elif hasattr(obj, '__dict__'):
# For custom objects, try to serialize their dict representation
try:
return obj.__dict__
except:
return str(obj)
elif hasattr(obj, 'value') and hasattr(obj, 'name'):
# For enum objects
return obj.value
else:
# For any other non-serializable object, convert to string
return str(obj)
# Global export manager instance
export_manager = ExportManager()