executive summary
This commit is contained in:
		
							parent
							
								
									140ef54674
								
							
						
					
					
						commit
						fdc26dcf15
					
				
							
								
								
									
										53
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								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.
 | 
			
		||||
 | 
			
		||||
@ -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')
 | 
			
		||||
 | 
			
		||||
@ -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 = `<span class="btn-icon">[...]</span><span>${loadingMessage}</span>`;
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
@ -194,7 +194,6 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Settings Modal -->
 | 
			
		||||
        <div id="settings-modal" class="modal">
 | 
			
		||||
            <div class="modal-content">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
@ -203,7 +202,6 @@
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <div class="modal-details">
 | 
			
		||||
                        <!-- Scan Settings Section -->
 | 
			
		||||
                        <section class="modal-section">
 | 
			
		||||
                            <details open>
 | 
			
		||||
                                <summary>
 | 
			
		||||
@ -224,7 +222,6 @@
 | 
			
		||||
                            </details>
 | 
			
		||||
                        </section>
 | 
			
		||||
 | 
			
		||||
                        <!-- Provider Configuration Section -->
 | 
			
		||||
                        <section class="modal-section">
 | 
			
		||||
                            <details open>
 | 
			
		||||
                                <summary>
 | 
			
		||||
@ -233,13 +230,11 @@
 | 
			
		||||
                                </summary>
 | 
			
		||||
                                <div class="modal-section-content">
 | 
			
		||||
                                    <div id="provider-config-list">
 | 
			
		||||
                                        <!-- Dynamically populated -->
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </details>
 | 
			
		||||
                        </section>
 | 
			
		||||
 | 
			
		||||
                        <!-- API Keys Section -->
 | 
			
		||||
                        <section class="modal-section">
 | 
			
		||||
                            <details>
 | 
			
		||||
                                <summary>
 | 
			
		||||
@ -252,13 +247,11 @@
 | 
			
		||||
                                        Only provide API keys you don't use for anything else.
 | 
			
		||||
                                    </p>
 | 
			
		||||
                                    <div id="api-key-inputs">
 | 
			
		||||
                                        <!-- Dynamically populated -->
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </details>
 | 
			
		||||
                        </section>
 | 
			
		||||
 | 
			
		||||
                        <!-- Action Buttons -->
 | 
			
		||||
                        <div class="button-group" style="margin-top: 1.5rem;">
 | 
			
		||||
                            <button id="save-settings" class="btn btn-primary">
 | 
			
		||||
                                <span class="btn-icon">[SAVE]</span>
 | 
			
		||||
@ -274,7 +267,6 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Export Modal -->
 | 
			
		||||
        <div id="export-modal" class="modal">
 | 
			
		||||
            <div class="modal-content">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
@ -299,6 +291,20 @@
 | 
			
		||||
                                                provider statistics, and scan metadata in JSON format for analysis and
 | 
			
		||||
                                                archival.</span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        <button id="export-targets-txt" class="btn btn-primary" style="margin-top: 1rem;">
 | 
			
		||||
                                            <span class="btn-icon">[TXT]</span>
 | 
			
		||||
                                            <span>Export Targets</span>
 | 
			
		||||
                                        </button>
 | 
			
		||||
                                        <div class="status-row" style="margin-top: 0.5rem;">
 | 
			
		||||
                                            <span class="status-label">A simple text file containing all discovered domains and IP addresses.</span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        <button id="export-executive-summary" class="btn btn-primary" style="margin-top: 1rem;">
 | 
			
		||||
                                            <span class="btn-icon">[TXT]</span>
 | 
			
		||||
                                            <span>Export Executive Summary</span>
 | 
			
		||||
                                        </button>
 | 
			
		||||
                                        <div class="status-row" style="margin-top: 0.5rem;">
 | 
			
		||||
                                            <span class="status-label">A natural-language summary of the scan findings.</span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </details>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user