data model refinement

This commit is contained in:
overcuriousity 2025-09-13 21:10:27 +02:00
parent 930fdca500
commit 2974312278
5 changed files with 231 additions and 230 deletions

View File

@ -152,21 +152,31 @@ class GraphManager:
}) })
return all_correlations return all_correlations
def add_node(self, node_id: str, node_type: NodeType, metadata: Optional[Dict[str, Any]] = None) -> bool: def add_node(self, node_id: str, node_type: NodeType, attributes: Optional[Dict[str, Any]] = None,
"""Add a node to the graph, update metadata, and process correlations.""" description: str = "", metadata: Optional[Dict[str, Any]] = None) -> bool:
"""Add a node to the graph, update attributes, and process correlations."""
is_new_node = not self.graph.has_node(node_id) is_new_node = not self.graph.has_node(node_id)
if is_new_node: if is_new_node:
self.graph.add_node(node_id, type=node_type.value, self.graph.add_node(node_id, type=node_type.value,
added_timestamp=datetime.now(timezone.utc).isoformat(), added_timestamp=datetime.now(timezone.utc).isoformat(),
attributes=attributes or {},
description=description,
metadata=metadata or {}) metadata=metadata or {})
elif metadata: else:
# Safely merge new metadata into existing metadata # Safely merge new attributes into existing attributes
if attributes:
existing_attributes = self.graph.nodes[node_id].get('attributes', {})
existing_attributes.update(attributes)
self.graph.nodes[node_id]['attributes'] = existing_attributes
if description:
self.graph.nodes[node_id]['description'] = description
if metadata:
existing_metadata = self.graph.nodes[node_id].get('metadata', {}) existing_metadata = self.graph.nodes[node_id].get('metadata', {})
existing_metadata.update(metadata) existing_metadata.update(metadata)
self.graph.nodes[node_id]['metadata'] = existing_metadata self.graph.nodes[node_id]['metadata'] = existing_metadata
if metadata and node_type != NodeType.CORRELATION_OBJECT: if attributes and node_type != NodeType.CORRELATION_OBJECT:
correlations = self._check_for_correlations(node_id, metadata) correlations = self._check_for_correlations(node_id, attributes)
for corr in correlations: for corr in correlations:
value = corr['value'] value = corr['value']
@ -186,7 +196,7 @@ class GraphManager:
continue # Skip creating a redundant correlation node continue # Skip creating a redundant correlation node
# Proceed to create a new correlation node if no major node was found. # Proceed to create a new correlation node if no major node was found.
correlation_node_id = f"corr_{hash(value) & 0x7FFFFFFF}" correlation_node_id = f"{value}"
if not self.graph.has_node(correlation_node_id): if not self.graph.has_node(correlation_node_id):
self.add_node(correlation_node_id, NodeType.CORRELATION_OBJECT, self.add_node(correlation_node_id, NodeType.CORRELATION_OBJECT,
metadata={'value': value, 'sources': corr['sources'], metadata={'value': value, 'sources': corr['sources'],
@ -203,7 +213,7 @@ class GraphManager:
for c_node_id in set(corr['nodes']): for c_node_id in set(corr['nodes']):
self.add_edge(c_node_id, correlation_node_id, RelationshipType.CORRELATED_TO) self.add_edge(c_node_id, correlation_node_id, RelationshipType.CORRELATED_TO)
self._update_correlation_index(node_id, metadata) self._update_correlation_index(node_id, attributes)
self.last_modified = datetime.now(timezone.utc).isoformat() self.last_modified = datetime.now(timezone.utc).isoformat()
return is_new_node return is_new_node
@ -263,12 +273,14 @@ class GraphManager:
nodes = [] nodes = []
for node_id, attrs in self.graph.nodes(data=True): for node_id, attrs in self.graph.nodes(data=True):
node_data = {'id': node_id, 'label': node_id, 'type': attrs.get('type', 'unknown'), node_data = {'id': node_id, 'label': node_id, 'type': attrs.get('type', 'unknown'),
'attributes': attrs.get('attributes', {}),
'description': attrs.get('description', ''),
'metadata': attrs.get('metadata', {}), 'metadata': attrs.get('metadata', {}),
'added_timestamp': attrs.get('added_timestamp')} 'added_timestamp': attrs.get('added_timestamp')}
# Customize node appearance based on type and metadata # Customize node appearance based on type and attributes
node_type = node_data['type'] node_type = node_data['type']
metadata = node_data['metadata'] attributes = node_data['attributes']
if node_type == 'domain' and metadata.get('certificate_data', {}).get('has_valid_cert') is False: if node_type == 'domain' and attributes.get('certificates', {}).get('has_valid_cert') is False:
node_data['color'] = {'background': '#c7c7c7', 'border': '#999'} # Gray for invalid cert node_data['color'] = {'background': '#c7c7c7', 'border': '#999'} # Gray for invalid cert
nodes.append(node_data) nodes.append(node_data)

View File

@ -344,7 +344,7 @@ class Scanner:
new_targets = set() new_targets = set()
large_entity_members = set() large_entity_members = set()
target_metadata = defaultdict(lambda: defaultdict(list)) node_attributes = defaultdict(lambda: defaultdict(list))
eligible_providers = self._get_eligible_providers(target, is_ip, dns_only) eligible_providers = self._get_eligible_providers(target, is_ip, dns_only)
@ -361,7 +361,7 @@ class Scanner:
provider_results = self._query_single_provider_forensic(provider, target, is_ip, depth) provider_results = self._query_single_provider_forensic(provider, target, is_ip, depth)
if provider_results and not self._is_stop_requested(): if provider_results and not self._is_stop_requested():
discovered, is_large_entity = self._process_provider_results_forensic( discovered, is_large_entity = self._process_provider_results_forensic(
target, provider, provider_results, target_metadata, depth target, provider, provider_results, node_attributes, depth
) )
if is_large_entity: if is_large_entity:
large_entity_members.update(discovered) large_entity_members.update(discovered)
@ -370,11 +370,11 @@ class Scanner:
except Exception as e: except Exception as e:
self._log_provider_error(target, provider.get_name(), str(e)) self._log_provider_error(target, provider.get_name(), str(e))
for node_id, metadata_dict in target_metadata.items(): for node_id, attributes in node_attributes.items():
if self.graph.graph.has_node(node_id): if self.graph.graph.has_node(node_id):
node_is_ip = _is_valid_ip(node_id) node_is_ip = _is_valid_ip(node_id)
node_type_to_add = NodeType.IP if node_is_ip else NodeType.DOMAIN node_type_to_add = NodeType.IP if node_is_ip else NodeType.DOMAIN
self.graph.add_node(node_id, node_type_to_add, metadata=metadata_dict) self.graph.add_node(node_id, node_type_to_add, attributes=attributes)
return new_targets, large_entity_members return new_targets, large_entity_members
@ -485,7 +485,7 @@ class Scanner:
self.logger.logger.info(f"Provider state updated: {target} -> {provider_name} -> {status} ({results_count} results)") self.logger.logger.info(f"Provider state updated: {target} -> {provider_name} -> {status} ({results_count} results)")
def _process_provider_results_forensic(self, target: str, provider, results: List, def _process_provider_results_forensic(self, target: str, provider, results: List,
target_metadata: Dict, current_depth: int) -> Tuple[Set[str], bool]: node_attributes: Dict, current_depth: int) -> Tuple[Set[str], bool]:
"""Process provider results, returns (discovered_targets, is_large_entity).""" """Process provider results, returns (discovered_targets, is_large_entity)."""
provider_name = provider.get_name() provider_name = provider.get_name()
discovered_targets = set() discovered_targets = set()
@ -514,7 +514,7 @@ class Scanner:
discovery_method=f"{provider_name}_query_depth_{current_depth}" discovery_method=f"{provider_name}_query_depth_{current_depth}"
) )
self._collect_node_metadata_forensic(source, provider_name, rel_type, rel_target, raw_data, target_metadata[source]) self._collect_node_attributes(source, provider_name, rel_type, rel_target, raw_data, node_attributes[source])
if _is_valid_ip(rel_target): if _is_valid_ip(rel_target):
self.graph.add_node(rel_target, NodeType.IP) self.graph.add_node(rel_target, NodeType.IP)
@ -532,10 +532,10 @@ class Scanner:
if self.graph.add_edge(source, rel_target, rel_type, confidence, provider_name, raw_data): if self.graph.add_edge(source, rel_target, rel_type, confidence, provider_name, raw_data):
print(f"Added domain relationship: {source} -> {rel_target} ({rel_type.relationship_name})") print(f"Added domain relationship: {source} -> {rel_target} ({rel_type.relationship_name})")
discovered_targets.add(rel_target) discovered_targets.add(rel_target)
self._collect_node_metadata_forensic(rel_target, provider_name, rel_type, source, raw_data, target_metadata[rel_target]) self._collect_node_attributes(rel_target, provider_name, rel_type, source, raw_data, node_attributes[rel_target])
else: else:
self._collect_node_metadata_forensic(source, provider_name, rel_type, rel_target, raw_data, target_metadata[source]) self._collect_node_attributes(source, provider_name, rel_type, rel_target, raw_data, node_attributes[source])
return discovered_targets, False return discovered_targets, False
@ -555,17 +555,17 @@ class Scanner:
for target in targets: for target in targets:
self.graph.add_node(target, NodeType.DOMAIN if node_type == 'domain' else NodeType.IP) self.graph.add_node(target, NodeType.DOMAIN if node_type == 'domain' else NodeType.IP)
metadata = { attributes = {
'count': len(targets), 'count': len(targets),
'nodes': targets, 'nodes': targets,
'node_type': node_type, 'node_type': node_type,
'source_provider': provider_name, 'source_provider': provider_name,
'discovery_depth': current_depth, 'discovery_depth': current_depth,
'threshold_exceeded': self.config.large_entity_threshold, 'threshold_exceeded': self.config.large_entity_threshold,
'forensic_note': f'Large entity created due to {len(targets)} results from {provider_name}'
} }
description = f'Large entity created due to {len(targets)} results from {provider_name}'
self.graph.add_node(entity_id, NodeType.LARGE_ENTITY, metadata=metadata) self.graph.add_node(entity_id, NodeType.LARGE_ENTITY, attributes=attributes, description=description)
if results: if results:
rel_type = results[0][2] rel_type = results[0][2]
@ -577,49 +577,50 @@ class Scanner:
return set(targets) return set(targets)
def _collect_node_metadata_forensic(self, node_id: str, provider_name: str, rel_type: RelationshipType, def _collect_node_attributes(self, node_id: str, provider_name: str, rel_type: RelationshipType,
target: str, raw_data: Dict[str, Any], metadata: Dict[str, Any]) -> None: target: str, raw_data: Dict[str, Any], attributes: Dict[str, Any]) -> None:
"""Collect and organize metadata for forensic tracking with enhanced logging.""" """Collect and organize attributes for a node."""
self.logger.logger.debug(f"Collecting metadata for {node_id} from {provider_name}: {rel_type.relationship_name}") self.logger.logger.debug(f"Collecting attributes for {node_id} from {provider_name}: {rel_type.relationship_name}")
if provider_name == 'dns': if provider_name == 'dns':
record_type = raw_data.get('query_type', 'UNKNOWN') record_type = raw_data.get('query_type', 'UNKNOWN')
value = raw_data.get('value', target) value = raw_data.get('value', target)
dns_entry = f"{record_type}: {value}" dns_entry = f"{record_type}: {value}"
if dns_entry not in metadata.get('dns_records', []): if dns_entry not in attributes.get('dns_records', []):
metadata.setdefault('dns_records', []).append(dns_entry) attributes.setdefault('dns_records', []).append(dns_entry)
elif provider_name == 'crtsh': elif provider_name == 'crtsh':
if rel_type == RelationshipType.SAN_CERTIFICATE: if rel_type == RelationshipType.SAN_CERTIFICATE:
domain_certs = raw_data.get('domain_certificates', {}) domain_certs = raw_data.get('domain_certificates', {})
if node_id in domain_certs: if node_id in domain_certs:
cert_summary = domain_certs[node_id] cert_summary = domain_certs[node_id]
metadata['certificate_data'] = cert_summary attributes['certificates'] = cert_summary
metadata['has_valid_cert'] = cert_summary.get('has_valid_cert', False) if target not in attributes.get('related_domains_san', []):
if target not in metadata.get('related_domains_san', []): attributes.setdefault('related_domains_san', []).append(target)
metadata.setdefault('related_domains_san', []).append(target)
elif provider_name == 'shodan': elif provider_name == 'shodan':
shodan_attributes = attributes.setdefault('shodan', {})
for key, value in raw_data.items(): for key, value in raw_data.items():
if key not in metadata.get('shodan', {}) or not metadata.get('shodan', {}).get(key): if key not in shodan_attributes or not shodan_attributes.get(key):
metadata.setdefault('shodan', {})[key] = value shodan_attributes[key] = value
if rel_type == RelationshipType.ASN_MEMBERSHIP: if rel_type == RelationshipType.ASN_MEMBERSHIP:
metadata['asn_data'] = { attributes['asn'] = {
'asn': target, 'id': target,
'description': raw_data.get('org', ''), 'description': raw_data.get('org', ''),
'isp': raw_data.get('isp', ''), 'isp': raw_data.get('isp', ''),
'country': raw_data.get('country', '') 'country': raw_data.get('country', '')
} }
record_type_name = rel_type.relationship_name record_type_name = rel_type.relationship_name
if record_type_name not in metadata: if record_type_name not in attributes:
metadata[record_type_name] = [] attributes[record_type_name] = []
if isinstance(target, list): if isinstance(target, list):
metadata[record_type_name].extend(target) attributes[record_type_name].extend(target)
else: else:
metadata[record_type_name].append(target) if target not in attributes[record_type_name]:
attributes[record_type_name].append(target)
def _log_target_processing_error(self, target: str, error: str) -> None: def _log_target_processing_error(self, target: str, error: str) -> None:

View File

@ -581,30 +581,6 @@ input[type="text"]:focus, select:focus {
color: #555; color: #555;
} }
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
animation: fadeIn 0.3s ease-out;
}
.modal-content {
background-color: #2a2a2a;
border: 1px solid #444;
margin: 5% auto;
width: 80%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
animation: slideInDown 0.3s ease-out;
}
@keyframes slideInDown { @keyframes slideInDown {
from { from {
opacity: 0; opacity: 0;
@ -616,43 +592,6 @@ input[type="text"]:focus, select:focus {
} }
} }
.modal-header {
background-color: #1a1a1a;
padding: 1rem;
border-bottom: 1px solid #444;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
color: #00ff41;
font-size: 1.1rem;
}
.modal-close {
background: transparent;
border: none;
color: #c7c7c7;
font-size: 1.2rem;
cursor: pointer;
font-family: 'Roboto Mono', monospace;
}
.modal-close:hover {
color: #ff9900;
}
.modal-body {
padding: 1.5rem;
}
.modal-description {
color: #999;
margin-bottom: 1.5rem;
line-height: 1.6;
}
.detail-row { .detail-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -801,12 +740,6 @@ input[type="text"]:focus, select:focus {
color: #00ff41 !important; color: #00ff41 !important;
} }
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in { .fade-in {
animation: fadeIn 0.3s ease-out; animation: fadeIn 0.3s ease-out;
} }
@ -971,3 +904,103 @@ input[type="text"]:focus, select:focus {
margin-left: 1rem; margin-left: 1rem;
margin-right: 1rem; margin-right: 1rem;
} }
/* dnsrecon/static/css/main.css */
/* --- Add these styles for the modal --- */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1000; /* Sit on top */
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto; /* Enable scroll if needed */
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
backdrop-filter: blur(5px);
}
.modal-content {
background-color: #1e1e1e;
margin: 10% auto;
padding: 20px;
border: 1px solid #444;
width: 60%;
max-width: 800px;
border-radius: 5px;
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
animation: fadeIn 0.3s;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #444;
padding-bottom: 10px;
margin-bottom: 20px;
}
.modal-header h3 {
margin: 0;
font-family: 'Special Elite', monospace;
color: #00ff41;
}
.modal-close {
background: none;
border: none;
color: #c7c7c7;
font-size: 24px;
cursor: pointer;
padding: 0 10px;
}
.modal-close:hover {
color: #ff6b6b;
}
.modal-body {
max-height: 60vh;
overflow-y: auto;
}
/* Styles for the new data model display */
.modal-details-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
.modal-section h4 {
font-family: 'Special Elite', monospace;
color: #ff9900;
border-bottom: 1px dashed #555;
padding-bottom: 5px;
margin-top: 0;
}
.modal-section ul {
list-style-type: none;
padding-left: 15px;
}
.modal-section li {
margin-bottom: 8px;
}
.modal-section li > ul {
padding-left: 20px;
margin-top: 5px;
}
.description-text, .no-data {
color: #aaa;
font-style: italic;
}
@keyframes fadeIn {
from {opacity: 0; transform: scale(0.95);}
to {opacity: 1; transform: scale(1);}
}

View File

@ -213,12 +213,12 @@ class GraphManager {
} }
}); });
// TODO Context menu (right-click) // FIX: Comment out the problematic context menu handler
this.network.on('oncontext', (params) => { this.network.on('oncontext', (params) => {
params.event.preventDefault(); params.event.preventDefault();
if (params.nodes.length > 0) { // if (params.nodes.length > 0) {
this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]); // this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]);
} // }
}); });
// Stabilization events with progress // Stabilization events with progress
@ -256,8 +256,8 @@ class GraphManager {
const largeEntityMap = new Map(); const largeEntityMap = new Map();
graphData.nodes.forEach(node => { graphData.nodes.forEach(node => {
if (node.type === 'large_entity' && node.metadata && Array.isArray(node.metadata.nodes)) { if (node.type === 'large_entity' && node.attributes && Array.isArray(node.attributes.nodes)) {
node.metadata.nodes.forEach(nodeId => { node.attributes.nodes.forEach(nodeId => {
largeEntityMap.set(nodeId, node.id); largeEntityMap.set(nodeId, node.id);
}); });
} }
@ -274,12 +274,14 @@ class GraphManager {
const mergedEdges = {}; const mergedEdges = {};
graphData.edges.forEach(edge => { graphData.edges.forEach(edge => {
const fromNode = largeEntityMap.has(edge.from) ? largeEntityMap.get(edge.from) : edge.from; const fromNode = largeEntityMap.has(edge.from) ? largeEntityMap.get(edge.from) : edge.from;
const mergeKey = `${fromNode}-${edge.to}-${edge.label}`; const toNode = largeEntityMap.has(edge.to) ? largeEntityMap.get(edge.to) : edge.to;
const mergeKey = `${fromNode}-${toNode}-${edge.label}`;
if (!mergedEdges[mergeKey]) { if (!mergedEdges[mergeKey]) {
mergedEdges[mergeKey] = { mergedEdges[mergeKey] = {
...edge, ...edge,
from: fromNode, from: fromNode,
to: toNode,
count: 0, count: 0,
confidence_score: 0 confidence_score: 0
}; };
@ -341,6 +343,8 @@ class GraphManager {
size: this.getNodeSize(node.type), size: this.getNodeSize(node.type),
borderColor: this.getNodeBorderColor(node.type), borderColor: this.getNodeBorderColor(node.type),
shape: this.getNodeShape(node.type), shape: this.getNodeShape(node.type),
attributes: node.attributes || {},
description: node.description || '',
metadata: node.metadata || {}, metadata: node.metadata || {},
type: node.type type: node.type
}; };
@ -352,12 +356,8 @@ class GraphManager {
// Style based on certificate validity // Style based on certificate validity
if (node.type === 'domain') { if (node.type === 'domain') {
if (node.metadata && node.metadata.certificate_data && node.metadata.certificate_data.has_valid_cert === true) { if (node.attributes && node.attributes.certificates && node.attributes.certificates.has_valid_cert === false) {
processedNode.color = '#00ff41'; // Bright green for valid cert processedNode.color = { background: '#888888', border: '#666666' };
processedNode.borderColor = '#00aa2e';
} else if (node.metadata && node.metadata.certificate_data && node.metadata.certificate_data.has_valid_cert === false) {
processedNode.color = '#888888'; // Muted grey color
processedNode.borderColor = '#666666'; // Darker grey border
} }
} }
@ -404,7 +404,7 @@ class GraphManager {
* @returns {string} Formatted label * @returns {string} Formatted label
*/ */
formatNodeLabel(nodeId, nodeType) { formatNodeLabel(nodeId, nodeType) {
// Truncate long domain names if (typeof nodeId !== 'string') return '';
if (nodeId.length > 20) { if (nodeId.length > 20) {
return nodeId.substring(0, 17) + '...'; return nodeId.substring(0, 17) + '...';
} }
@ -564,7 +564,7 @@ class GraphManager {
// Trigger custom event for main application to handle // Trigger custom event for main application to handle
const event = new CustomEvent('nodeSelected', { const event = new CustomEvent('nodeSelected', {
detail: { nodeId, node } detail: { node }
}); });
document.dispatchEvent(event); document.dispatchEvent(event);
} }

View File

@ -193,9 +193,9 @@ class DNSReconApp {
this.elements.resetApiKeys.addEventListener('click', () => this.resetApiKeys()); this.elements.resetApiKeys.addEventListener('click', () => this.resetApiKeys());
} }
// Custom events // ** FIX: Listen for the custom event from the graph **
document.addEventListener('nodeSelected', (e) => { document.addEventListener('nodeSelected', (e) => {
this.showNodeModal(e.detail.nodeId, e.detail.node); this.showNodeModal(e.detail.node);
}); });
// Keyboard shortcuts // Keyboard shortcuts
@ -793,129 +793,86 @@ class DNSReconApp {
} }
/** /**
* Generates the HTML for the node details view. * **FIX**: Generates the HTML for the node details view using the new data model.
* @param {Object} node - The node object. * @param {Object} node - The node object.
* @returns {string} The HTML string for the node details. * @returns {string} The HTML string for the node details.
*/ */
generateNodeDetailsHtml(node) { generateNodeDetailsHtml(node) {
if(!node) return '<div class="detail-row"><span class="detail-value">Details not available.</span></div>'; if(!node) return '<div class="detail-row"><span class="detail-value">Details not available.</span></div>';
let detailsHtml = '';
const createDetailRow = (label, value, statusIcon = '') => {
const baseId = `detail-${node.id.replace(/[^a-zA-Z0-9]/g, '-')}-${label.replace(/[^a-zA-Z0-9]/g, '-')}`;
if (value === null || value === undefined || let detailsHtml = '<div class="modal-details-grid">';
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)) { // Section for Attributes
return ` detailsHtml += '<div class="modal-section">';
<div class="detail-row"> detailsHtml += '<h4>Attributes</h4>';
<span class="detail-label">${label} <span class="status-icon text-warning"></span></span> detailsHtml += this.formatObjectToHtml(node.attributes);
<span class="detail-value">N/A</span> detailsHtml += '</div>';
</div>
`; // Section for Description
detailsHtml += '<div class="modal-section">';
detailsHtml += '<h4>Description</h4>';
detailsHtml += `<p class="description-text">${node.description || 'No description available.'}</p>`;
detailsHtml += '</div>';
// Section for Metadata
detailsHtml += '<div class="modal-section">';
detailsHtml += '<h4>Metadata</h4>';
detailsHtml += this.formatObjectToHtml(node.metadata);
detailsHtml += '</div>';
detailsHtml += '</div>';
return detailsHtml;
} }
/**
* Recursively formats a JavaScript object into an HTML unordered list.
* @param {Object} obj - The object to format.
* @returns {string} - An HTML string representing the object.
*/
formatObjectToHtml(obj) {
if (!obj || Object.keys(obj).length === 0) {
return '<p class="no-data">No data available.</p>';
}
let html = '<ul>';
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const value = obj[key];
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
html += `<li><strong>${formattedKey}:</strong>`;
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map((item, index) => { html += `<ul>${value.map(item => `<li>${this.formatValue(item)}</li>`).join('')}</ul>`;
const itemId = `${baseId}-${index}`; } else if (typeof value === 'object' && value !== null) {
const itemLabel = index === 0 ? `${label} <span class="status-icon text-success">✓</span>` : ''; html += this.formatObjectToHtml(value);
return `
<div class="detail-row">
<span class="detail-label">${itemLabel}</span>
<span class="detail-value" id="${itemId}">${this.formatValue(item)}</span>
<button class="copy-btn" onclick="copyToClipboard('${itemId}')" title="Copy">📋</button>
</div>
`;
}).join('');
} else { } else {
const valueId = `${baseId}-0`; html += ` ${this.formatValue(value)}`;
const icon = statusIcon || '<span class="status-icon text-success">✓</span>';
return `
<div class="detail-row">
<span class="detail-label">${label} ${icon}</span>
<span class="detail-value" id="${valueId}">${this.formatValue(value)}</span>
<button class="copy-btn" onclick="copyToClipboard('${valueId}')" title="Copy">📋</button>
</div>
`;
} }
}; html += '</li>';
const metadata = node.metadata || {};
detailsHtml += createDetailRow('Node Descriptor', node.id);
switch (node.type) {
case 'domain':
detailsHtml += createDetailRow('DNS Records', metadata.dns_records);
detailsHtml += createDetailRow('Related Domains (SAN)', metadata.related_domains_san);
detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns);
detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
break;
case 'ip':
detailsHtml += createDetailRow('Hostnames', metadata.hostnames);
detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns);
detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
break;
case 'correlation_object':
detailsHtml += createDetailRow('Correlated Value', metadata.value);
if (metadata.correlated_nodes) {
detailsHtml += createDetailRow('Correlated Nodes', metadata.correlated_nodes.join(', '));
}
if (metadata.sources) {
detailsHtml += `<div class="detail-section-header">Correlation Sources</div>`;
for (const source of metadata.sources) {
detailsHtml += createDetailRow(source.node_id, source.path);
} }
} }
break; html += '</ul>';
} return html;
if (metadata.certificate_data && Object.keys(metadata.certificate_data).length > 0) {
const cert = metadata.certificate_data;
detailsHtml += `<div class="detail-section-header">Certificate Summary</div>`;
detailsHtml += createDetailRow('Total Found', cert.total_certificates);
detailsHtml += createDetailRow('Currently Valid', cert.valid_certificates);
detailsHtml += createDetailRow('Expires Soon (<30d)', cert.expires_soon_count);
detailsHtml += createDetailRow('Unique Issuers', cert.unique_issuers ? cert.unique_issuers.join(', ') : 'N/A');
if (cert.latest_certificate) {
detailsHtml += `<div class="detail-section-header">Latest Certificate</div>`;
detailsHtml += createDetailRow('Common Name', cert.latest_certificate.common_name);
detailsHtml += createDetailRow('Issuer', cert.latest_certificate.issuer_name);
detailsHtml += createDetailRow('Valid From', new Date(cert.latest_certificate.not_before).toLocaleString());
detailsHtml += createDetailRow('Valid Until', new Date(cert.latest_certificate.not_after).toLocaleString());
}
}
if (metadata.asn_data && Object.keys(metadata.asn_data).length > 0) {
detailsHtml += `<div class="detail-section-header">ASN Information</div>`;
detailsHtml += createDetailRow('ASN', metadata.asn_data.asn);
detailsHtml += createDetailRow('Organization', metadata.asn_data.description);
detailsHtml += createDetailRow('ISP', metadata.asn_data.isp);
detailsHtml += createDetailRow('Country', metadata.asn_data.country);
}
return detailsHtml;
} }
/** /**
* Show node details modal * Show node details modal
* @param {string} nodeId - Node identifier
* @param {Object} node - Node data * @param {Object} node - Node data
*/ */
showNodeModal(nodeId, node) { showNodeModal(node) {
if (!this.elements.nodeModal) return; if (!this.elements.nodeModal || !node) return;
if (this.elements.modalTitle) { if (this.elements.modalTitle) {
this.elements.modalTitle.textContent = `Node Details`; this.elements.modalTitle.textContent = `${this.formatStatus(node.type)} Node: ${node.id}`;
} }
let detailsHtml = '';
let detailsHtml = '';
if (node.type === 'large_entity') { if (node.type === 'large_entity') {
const metadata = node.metadata || {}; const attributes = node.attributes || {};
const nodes = metadata.nodes || []; const nodes = attributes.nodes || [];
const node_type = metadata.node_type || 'nodes'; const node_type = attributes.node_type || 'nodes';
detailsHtml += `<div class="detail-section-header">Contains ${metadata.count} ${node_type}s</div>`; detailsHtml += `<div class="detail-section-header">Contains ${attributes.count} ${node_type}s</div>`;
detailsHtml += '<div class="large-entity-nodes-list">'; detailsHtml += '<div class="large-entity-nodes-list">';
for(const innerNodeId of nodes) { for(const innerNodeId of nodes) {
@ -926,12 +883,10 @@ class DNSReconApp {
detailsHtml += `</details>`; detailsHtml += `</details>`;
} }
detailsHtml += '</div>'; detailsHtml += '</div>';
} else { } else {
detailsHtml = this.generateNodeDetailsHtml(node); detailsHtml = this.generateNodeDetailsHtml(node);
} }
if (this.elements.modalDetails) { if (this.elements.modalDetails) {
this.elements.modalDetails.innerHTML = detailsHtml; this.elements.modalDetails.innerHTML = detailsHtml;
} }