diff --git a/app.py b/app.py index ae2206b..101e720 100644 --- a/app.py +++ b/app.py @@ -363,6 +363,59 @@ def export_results(): 'error_type': type(e).__name__ }), 500 +@app.route('/api/export/targets', methods=['GET']) +def export_targets(): + """Export all discovered targets as a TXT file.""" + try: + user_session_id, scanner = get_user_scanner() + if not scanner: + return jsonify({'success': False, 'error': 'No active scanner session found'}), 404 + + targets_txt = scanner.export_targets_txt() + + 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" + + file_obj = io.BytesIO(targets_txt.encode('utf-8')) + + return send_file( + file_obj, + as_attachment=True, + download_name=filename, + mimetype='text/plain' + ) + except Exception as e: + traceback.print_exc() + return jsonify({'success': False, 'error': f'Export failed: {str(e)}'}), 500 + + +@app.route('/api/export/summary', methods=['GET']) +def export_summary(): + """Export an executive summary as a TXT file.""" + try: + user_session_id, scanner = get_user_scanner() + if not scanner: + return jsonify({'success': False, 'error': 'No active scanner session found'}), 404 + + summary_txt = scanner.generate_executive_summary() + + 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" + + file_obj = io.BytesIO(summary_txt.encode('utf-8')) + + return send_file( + file_obj, + as_attachment=True, + download_name=filename, + mimetype='text/plain' + ) + except Exception as e: + 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. diff --git a/core/scanner.py b/core/scanner.py index 67de6d0..3e04d7e 100644 --- a/core/scanner.py +++ b/core/scanner.py @@ -112,7 +112,7 @@ class Scanner: # Fall back to local event return self.stop_event.is_set() - return False + return self.stop_event.is_set() def _set_stop_signal(self) -> None: """ @@ -883,6 +883,96 @@ class Scanner: '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 = {} provider_dir = os.path.join(os.path.dirname(__file__), '..', 'providers') diff --git a/static/js/main.js b/static/js/main.js index ee56fe7..63474ea 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -62,6 +62,8 @@ class DNSReconApp { exportModal: document.getElementById('export-modal'), exportModalClose: document.getElementById('export-modal-close'), exportGraphJson: document.getElementById('export-graph-json'), + exportTargetsTxt: document.getElementById('export-targets-txt'), + exportExecutiveSummary: document.getElementById('export-executive-summary'), configureSettings: document.getElementById('configure-settings'), // Status elements @@ -165,6 +167,12 @@ class DNSReconApp { if (this.elements.exportGraphJson) { this.elements.exportGraphJson.addEventListener('click', () => this.exportGraphJson()); } + if (this.elements.exportTargetsTxt) { + this.elements.exportTargetsTxt.addEventListener('click', () => this.exportTargetsTxt()); + } + if (this.elements.exportExecutiveSummary) { + this.elements.exportExecutiveSummary.addEventListener('click', () => this.exportExecutiveSummary()); + } this.elements.configureSettings.addEventListener('click', () => this.showSettingsModal()); @@ -485,6 +493,60 @@ class DNSReconApp { } } } + + async exportTargetsTxt() { + await this.exportFile('/api/export/targets', this.elements.exportTargetsTxt, 'Exporting Targets...'); + } + + async exportExecutiveSummary() { + await this.exportFile('/api/export/summary', this.elements.exportExecutiveSummary, 'Generating Summary...'); + } + + async exportFile(endpoint, buttonElement, loadingMessage) { + try { + console.log(`Exporting from ${endpoint}...`); + + const originalContent = buttonElement.innerHTML; + buttonElement.innerHTML = `${loadingMessage}`; + buttonElement.disabled = true; + + const response = await fetch(endpoint, { method: 'GET' }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`); + } + + const contentDisposition = response.headers.get('content-disposition'); + let filename = 'export.txt'; + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); + if (filenameMatch) { + filename = filenameMatch[1].replace(/['"]/g, ''); + } + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + this.showSuccess('File exported successfully'); + this.hideExportModal(); + } catch (error) { + console.error(`Failed to export from ${endpoint}:`, error); + this.showError(`Export failed: ${error.message}`); + } finally { + const originalContent = buttonElement._originalContent || buttonElement.innerHTML; + buttonElement.innerHTML = originalContent; + buttonElement.disabled = false; + } + } /** * Start polling for scan updates with configurable interval diff --git a/templates/index.html b/templates/index.html index f32d02c..776dc6a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -194,7 +194,6 @@ -