data model refinement
This commit is contained in:
parent
930fdca500
commit
2974312278
@ -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)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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);}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user