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__
|
'error_type': type(e).__name__
|
||||||
}), 500
|
}), 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):
|
def _clean_for_json(obj, max_depth=10, current_depth=0):
|
||||||
"""
|
"""
|
||||||
Recursively clean an object to make it JSON serializable.
|
Recursively clean an object to make it JSON serializable.
|
||||||
|
|||||||
@ -112,7 +112,7 @@ class Scanner:
|
|||||||
# Fall back to local event
|
# Fall back to local event
|
||||||
return self.stop_event.is_set()
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
return False
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
def _set_stop_signal(self) -> None:
|
def _set_stop_signal(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -883,6 +883,96 @@ class Scanner:
|
|||||||
'scan_summary': self.logger.get_forensic_summary()
|
'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]]:
|
def get_provider_info(self) -> Dict[str, Dict[str, Any]]:
|
||||||
info = {}
|
info = {}
|
||||||
provider_dir = os.path.join(os.path.dirname(__file__), '..', 'providers')
|
provider_dir = os.path.join(os.path.dirname(__file__), '..', 'providers')
|
||||||
|
|||||||
@ -62,6 +62,8 @@ class DNSReconApp {
|
|||||||
exportModal: document.getElementById('export-modal'),
|
exportModal: document.getElementById('export-modal'),
|
||||||
exportModalClose: document.getElementById('export-modal-close'),
|
exportModalClose: document.getElementById('export-modal-close'),
|
||||||
exportGraphJson: document.getElementById('export-graph-json'),
|
exportGraphJson: document.getElementById('export-graph-json'),
|
||||||
|
exportTargetsTxt: document.getElementById('export-targets-txt'),
|
||||||
|
exportExecutiveSummary: document.getElementById('export-executive-summary'),
|
||||||
configureSettings: document.getElementById('configure-settings'),
|
configureSettings: document.getElementById('configure-settings'),
|
||||||
|
|
||||||
// Status elements
|
// Status elements
|
||||||
@ -165,6 +167,12 @@ class DNSReconApp {
|
|||||||
if (this.elements.exportGraphJson) {
|
if (this.elements.exportGraphJson) {
|
||||||
this.elements.exportGraphJson.addEventListener('click', () => this.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());
|
this.elements.configureSettings.addEventListener('click', () => this.showSettingsModal());
|
||||||
@ -486,6 +494,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
|
* Start polling for scan updates with configurable interval
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -194,7 +194,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
|
||||||
<div id="settings-modal" class="modal">
|
<div id="settings-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -203,7 +202,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="modal-details">
|
<div class="modal-details">
|
||||||
<!-- Scan Settings Section -->
|
|
||||||
<section class="modal-section">
|
<section class="modal-section">
|
||||||
<details open>
|
<details open>
|
||||||
<summary>
|
<summary>
|
||||||
@ -224,7 +222,6 @@
|
|||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Provider Configuration Section -->
|
|
||||||
<section class="modal-section">
|
<section class="modal-section">
|
||||||
<details open>
|
<details open>
|
||||||
<summary>
|
<summary>
|
||||||
@ -233,13 +230,11 @@
|
|||||||
</summary>
|
</summary>
|
||||||
<div class="modal-section-content">
|
<div class="modal-section-content">
|
||||||
<div id="provider-config-list">
|
<div id="provider-config-list">
|
||||||
<!-- Dynamically populated -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- API Keys Section -->
|
|
||||||
<section class="modal-section">
|
<section class="modal-section">
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
@ -252,13 +247,11 @@
|
|||||||
Only provide API keys you don't use for anything else.
|
Only provide API keys you don't use for anything else.
|
||||||
</p>
|
</p>
|
||||||
<div id="api-key-inputs">
|
<div id="api-key-inputs">
|
||||||
<!-- Dynamically populated -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="button-group" style="margin-top: 1.5rem;">
|
<div class="button-group" style="margin-top: 1.5rem;">
|
||||||
<button id="save-settings" class="btn btn-primary">
|
<button id="save-settings" class="btn btn-primary">
|
||||||
<span class="btn-icon">[SAVE]</span>
|
<span class="btn-icon">[SAVE]</span>
|
||||||
@ -274,7 +267,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Export Modal -->
|
|
||||||
<div id="export-modal" class="modal">
|
<div id="export-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -299,6 +291,20 @@
|
|||||||
provider statistics, and scan metadata in JSON format for analysis and
|
provider statistics, and scan metadata in JSON format for analysis and
|
||||||
archival.</span>
|
archival.</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user