it
This commit is contained in:
parent
84810cdbb0
commit
53baf2e291
@ -2,11 +2,10 @@
|
|||||||
Graph data model for DNSRecon using NetworkX.
|
Graph data model for DNSRecon using NetworkX.
|
||||||
Manages in-memory graph storage with confidence scoring and forensic metadata.
|
Manages in-memory graph storage with confidence scoring and forensic metadata.
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from typing import Dict, List, Any, Optional, Tuple
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from datetime import timezone
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
|
||||||
@ -16,8 +15,11 @@ class NodeType(Enum):
|
|||||||
DOMAIN = "domain"
|
DOMAIN = "domain"
|
||||||
IP = "ip"
|
IP = "ip"
|
||||||
ASN = "asn"
|
ASN = "asn"
|
||||||
DNS_RECORD = "dns_record"
|
|
||||||
LARGE_ENTITY = "large_entity"
|
LARGE_ENTITY = "large_entity"
|
||||||
|
CORRELATION_OBJECT = "correlation_object"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class RelationshipType(Enum):
|
class RelationshipType(Enum):
|
||||||
@ -30,25 +32,17 @@ class RelationshipType(Enum):
|
|||||||
NS_RECORD = ("ns_record", 0.7)
|
NS_RECORD = ("ns_record", 0.7)
|
||||||
PTR_RECORD = ("ptr_record", 0.8)
|
PTR_RECORD = ("ptr_record", 0.8)
|
||||||
SOA_RECORD = ("soa_record", 0.7)
|
SOA_RECORD = ("soa_record", 0.7)
|
||||||
TXT_RECORD = ("txt_record", 0.7)
|
|
||||||
SRV_RECORD = ("srv_record", 0.7)
|
|
||||||
CAA_RECORD = ("caa_record", 0.7)
|
|
||||||
DNSKEY_RECORD = ("dnskey_record", 0.7)
|
|
||||||
DS_RECORD = ("ds_record", 0.7)
|
|
||||||
RRSIG_RECORD = ("rrsig_record", 0.7)
|
|
||||||
SSHFP_RECORD = ("sshfp_record", 0.7)
|
|
||||||
TLSA_RECORD = ("tlsa_record", 0.7)
|
|
||||||
NAPTR_RECORD = ("naptr_record", 0.7)
|
|
||||||
SPF_RECORD = ("spf_record", 0.7)
|
|
||||||
DNS_RECORD = ("dns_record", 0.8)
|
|
||||||
PASSIVE_DNS = ("passive_dns", 0.6)
|
PASSIVE_DNS = ("passive_dns", 0.6)
|
||||||
ASN_MEMBERSHIP = ("asn", 0.7)
|
ASN_MEMBERSHIP = ("asn", 0.7)
|
||||||
|
CORRELATED_TO = ("correlated_to", 0.9)
|
||||||
|
|
||||||
def __init__(self, relationship_name: str, default_confidence: float):
|
def __init__(self, relationship_name: str, default_confidence: float):
|
||||||
self.relationship_name = relationship_name
|
self.relationship_name = relationship_name
|
||||||
self.default_confidence = default_confidence
|
self.default_confidence = default_confidence
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.relationship_name
|
||||||
|
|
||||||
|
|
||||||
class GraphManager:
|
class GraphManager:
|
||||||
"""
|
"""
|
||||||
@ -59,96 +53,185 @@ class GraphManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize empty directed graph."""
|
"""Initialize empty directed graph."""
|
||||||
self.graph = nx.DiGraph()
|
self.graph = nx.DiGraph()
|
||||||
# self.lock = threading.Lock()
|
|
||||||
self.creation_time = datetime.now(timezone.utc).isoformat()
|
self.creation_time = datetime.now(timezone.utc).isoformat()
|
||||||
self.last_modified = self.creation_time
|
self.last_modified = self.creation_time
|
||||||
|
self.correlation_index = {}
|
||||||
|
# Compile regex for date filtering for efficiency
|
||||||
|
self.date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}')
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
"""GraphManager is fully picklable, return full state."""
|
"""Prepare GraphManager for pickling, excluding compiled regex."""
|
||||||
return self.__dict__.copy()
|
state = self.__dict__.copy()
|
||||||
|
# Compiled regex patterns are not always picklable
|
||||||
|
if 'date_pattern' in state:
|
||||||
|
del state['date_pattern']
|
||||||
|
return state
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
"""Restore GraphManager state."""
|
"""Restore GraphManager state and recompile regex."""
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
|
self.date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}')
|
||||||
|
|
||||||
def add_node(self, node_id: str, node_type: NodeType,
|
def _update_correlation_index(self, node_id: str, data: Any, path: List[str] = None):
|
||||||
metadata: Optional[Dict[str, Any]] = None) -> bool:
|
"""Recursively traverse metadata and add hashable values to the index."""
|
||||||
"""
|
if path is None:
|
||||||
Add a node to the graph.
|
path = []
|
||||||
|
|
||||||
Args:
|
if isinstance(data, dict):
|
||||||
node_id: Unique identifier for the node
|
for key, value in data.items():
|
||||||
node_type: Type of the node (Domain, IP, Certificate, ASN)
|
self._update_correlation_index(node_id, value, path + [key])
|
||||||
metadata: Additional metadata for the node
|
elif isinstance(data, list):
|
||||||
|
for i, item in enumerate(data):
|
||||||
|
self._update_correlation_index(node_id, item, path + [f"[{i}]"])
|
||||||
|
else:
|
||||||
|
self._add_to_correlation_index(node_id, data, ".".join(path))
|
||||||
|
|
||||||
Returns:
|
def _add_to_correlation_index(self, node_id: str, value: Any, path_str: str):
|
||||||
bool: True if node was added, False if it already exists
|
"""Add a hashable value to the correlation index, filtering out noise."""
|
||||||
"""
|
if not isinstance(value, (str, int, float, bool)) or value is None:
|
||||||
if self.graph.has_node(node_id):
|
return
|
||||||
# Update metadata if node exists
|
|
||||||
|
# Ignore certain paths that contain noisy, non-unique identifiers
|
||||||
|
if any(keyword in path_str.lower() for keyword in ['count', 'total', 'timestamp', 'date']):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Filter out common low-entropy values and date-like strings
|
||||||
|
if isinstance(value, str):
|
||||||
|
# FIXED: Prevent correlation on date/time strings.
|
||||||
|
if self.date_pattern.match(value):
|
||||||
|
return
|
||||||
|
if len(value) < 4 or value.lower() in ['true', 'false', 'unknown', 'none', 'crt.sh']:
|
||||||
|
return
|
||||||
|
elif isinstance(value, int) and abs(value) < 9999:
|
||||||
|
return # Ignore small integers
|
||||||
|
elif isinstance(value, bool):
|
||||||
|
return # Ignore boolean values
|
||||||
|
|
||||||
|
# Add the valuable correlation data to the index
|
||||||
|
if value not in self.correlation_index:
|
||||||
|
self.correlation_index[value] = {}
|
||||||
|
if node_id not in self.correlation_index[value]:
|
||||||
|
self.correlation_index[value][node_id] = []
|
||||||
|
if path_str not in self.correlation_index[value][node_id]:
|
||||||
|
self.correlation_index[value][node_id].append(path_str)
|
||||||
|
|
||||||
|
def _check_for_correlations(self, new_node_id: str, data: Any, path: List[str] = None) -> List[Dict]:
|
||||||
|
"""Recursively traverse metadata to find correlations with existing data."""
|
||||||
|
if path is None:
|
||||||
|
path = []
|
||||||
|
|
||||||
|
all_correlations = []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for key, value in data.items():
|
||||||
|
if key == 'source': # Avoid correlating on the provider name
|
||||||
|
continue
|
||||||
|
all_correlations.extend(self._check_for_correlations(new_node_id, value, path + [key]))
|
||||||
|
elif isinstance(data, list):
|
||||||
|
for i, item in enumerate(data):
|
||||||
|
all_correlations.extend(self._check_for_correlations(new_node_id, item, path + [f"[{i}]"]))
|
||||||
|
else:
|
||||||
|
value = data
|
||||||
|
if value in self.correlation_index:
|
||||||
|
existing_nodes_with_paths = self.correlation_index[value]
|
||||||
|
unique_nodes = set(existing_nodes_with_paths.keys())
|
||||||
|
unique_nodes.add(new_node_id)
|
||||||
|
|
||||||
|
if len(unique_nodes) < 2:
|
||||||
|
return all_correlations # Correlation must involve at least two distinct nodes
|
||||||
|
|
||||||
|
new_source = {'node_id': new_node_id, 'path': ".".join(path)}
|
||||||
|
all_sources = [new_source]
|
||||||
|
for node_id, paths in existing_nodes_with_paths.items():
|
||||||
|
for p_str in paths:
|
||||||
|
all_sources.append({'node_id': node_id, 'path': p_str})
|
||||||
|
|
||||||
|
all_correlations.append({
|
||||||
|
'value': value,
|
||||||
|
'sources': all_sources,
|
||||||
|
'nodes': list(unique_nodes)
|
||||||
|
})
|
||||||
|
return all_correlations
|
||||||
|
|
||||||
|
def add_node(self, node_id: str, node_type: NodeType, metadata: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""Add a node to the graph, update metadata, and process correlations."""
|
||||||
|
is_new_node = not self.graph.has_node(node_id)
|
||||||
|
if is_new_node:
|
||||||
|
self.graph.add_node(node_id, type=node_type.value,
|
||||||
|
added_timestamp=datetime.now(timezone.utc).isoformat(),
|
||||||
|
metadata=metadata or {})
|
||||||
|
elif metadata:
|
||||||
|
# Safely merge new metadata into existing metadata
|
||||||
existing_metadata = self.graph.nodes[node_id].get('metadata', {})
|
existing_metadata = self.graph.nodes[node_id].get('metadata', {})
|
||||||
if 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:
|
||||||
|
correlations = self._check_for_correlations(node_id, metadata)
|
||||||
|
for corr in correlations:
|
||||||
|
value = corr['value']
|
||||||
|
|
||||||
|
# FIXED: Check if the correlation value contains an existing node ID.
|
||||||
|
found_major_node_id = None
|
||||||
|
if isinstance(value, str):
|
||||||
|
for existing_node in self.graph.nodes():
|
||||||
|
if existing_node in value:
|
||||||
|
found_major_node_id = existing_node
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_major_node_id:
|
||||||
|
# An existing major node is part of the value; link to it directly.
|
||||||
|
for c_node_id in set(corr['nodes']):
|
||||||
|
if self.graph.has_node(c_node_id) and c_node_id != found_major_node_id:
|
||||||
|
self.add_edge(c_node_id, found_major_node_id, RelationshipType.CORRELATED_TO)
|
||||||
|
continue # Skip creating a redundant correlation node
|
||||||
|
|
||||||
|
# Proceed to create a new correlation node if no major node was found.
|
||||||
|
correlation_node_id = f"corr_{hash(value) & 0x7FFFFFFF}"
|
||||||
|
if not self.graph.has_node(correlation_node_id):
|
||||||
|
self.add_node(correlation_node_id, NodeType.CORRELATION_OBJECT,
|
||||||
|
metadata={'value': value, 'sources': corr['sources'],
|
||||||
|
'correlated_nodes': list(set(corr['nodes']))})
|
||||||
|
else: # Update existing correlation node
|
||||||
|
existing_meta = self.graph.nodes[correlation_node_id]['metadata']
|
||||||
|
existing_nodes = set(existing_meta.get('correlated_nodes', []))
|
||||||
|
existing_meta['correlated_nodes'] = list(existing_nodes.union(set(corr['nodes'])))
|
||||||
|
existing_sources = {(s['node_id'], s['path']) for s in existing_meta.get('sources', [])}
|
||||||
|
for s in corr['sources']:
|
||||||
|
existing_sources.add((s['node_id'], s['path']))
|
||||||
|
existing_meta['sources'] = [{'node_id': nid, 'path': p} for nid, p in existing_sources]
|
||||||
|
|
||||||
|
for c_node_id in set(corr['nodes']):
|
||||||
|
self.add_edge(c_node_id, correlation_node_id, RelationshipType.CORRELATED_TO)
|
||||||
|
|
||||||
|
self._update_correlation_index(node_id, metadata)
|
||||||
|
|
||||||
|
self.last_modified = datetime.now(timezone.utc).isoformat()
|
||||||
|
return is_new_node
|
||||||
|
|
||||||
|
def add_edge(self, source_id: str, target_id: str, relationship_type: RelationshipType,
|
||||||
|
confidence_score: Optional[float] = None, source_provider: str = "unknown",
|
||||||
|
raw_data: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""Add or update an edge between two nodes, ensuring nodes exist."""
|
||||||
|
# LOGIC FIX: Ensure both source and target nodes exist before adding an edge.
|
||||||
|
if not self.graph.has_node(source_id) or not self.graph.has_node(target_id):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
node_attributes = {
|
|
||||||
'type': node_type.value,
|
|
||||||
'added_timestamp': datetime.now(timezone.utc).isoformat(),
|
|
||||||
'metadata': metadata or {}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.graph.add_node(node_id, **node_attributes)
|
|
||||||
self.last_modified = datetime.now(timezone.utc).isoformat()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def add_edge(self, source_id: str, target_id: str,
|
|
||||||
relationship_type: RelationshipType,
|
|
||||||
confidence_score: Optional[float] = None,
|
|
||||||
source_provider: str = "unknown",
|
|
||||||
raw_data: Optional[Dict[str, Any]] = None) -> bool:
|
|
||||||
"""
|
|
||||||
Add an edge between two nodes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source_id: Source node identifier
|
|
||||||
target_id: Target node identifier
|
|
||||||
relationship_type: Type of relationship
|
|
||||||
confidence_score: Custom confidence score (overrides default)
|
|
||||||
source_provider: Provider that discovered this relationship
|
|
||||||
raw_data: Raw data from provider response
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if edge was added, False if it already exists
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self.graph.has_node(source_id) or not self.graph.has_node(target_id):
|
|
||||||
# If the target node is a subdomain, it should be added.
|
|
||||||
# The scanner will handle this logic.
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Check if edge already exists
|
|
||||||
if self.graph.has_edge(source_id, target_id):
|
|
||||||
# Update confidence score if new score is higher
|
|
||||||
existing_confidence = self.graph.edges[source_id, target_id]['confidence_score']
|
|
||||||
new_confidence = confidence_score or relationship_type.default_confidence
|
new_confidence = confidence_score or relationship_type.default_confidence
|
||||||
|
if self.graph.has_edge(source_id, target_id):
|
||||||
if new_confidence > existing_confidence:
|
# If edge exists, update confidence if the new score is higher.
|
||||||
|
if new_confidence > self.graph.edges[source_id, target_id].get('confidence_score', 0):
|
||||||
self.graph.edges[source_id, target_id]['confidence_score'] = new_confidence
|
self.graph.edges[source_id, target_id]['confidence_score'] = new_confidence
|
||||||
self.graph.edges[source_id, target_id]['updated_timestamp'] = datetime.now(timezone.utc).isoformat()
|
self.graph.edges[source_id, target_id]['updated_timestamp'] = datetime.now(timezone.utc).isoformat()
|
||||||
self.graph.edges[source_id, target_id]['updated_by'] = source_provider
|
self.graph.edges[source_id, target_id]['updated_by'] = source_provider
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
edge_attributes = {
|
# Add a new edge with all attributes.
|
||||||
'relationship_type': relationship_type.relationship_name,
|
self.graph.add_edge(source_id, target_id,
|
||||||
'confidence_score': confidence_score or relationship_type.default_confidence,
|
relationship_type=relationship_type.relationship_name,
|
||||||
'source_provider': source_provider,
|
confidence_score=new_confidence,
|
||||||
'discovery_timestamp': datetime.now(timezone.utc).isoformat(),
|
source_provider=source_provider,
|
||||||
'raw_data': raw_data or {}
|
discovery_timestamp=datetime.now(timezone.utc).isoformat(),
|
||||||
}
|
raw_data=raw_data or {})
|
||||||
|
|
||||||
self.graph.add_edge(source_id, target_id, **edge_attributes)
|
|
||||||
self.last_modified = datetime.now(timezone.utc).isoformat()
|
self.last_modified = datetime.now(timezone.utc).isoformat()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -161,270 +244,92 @@ class GraphManager:
|
|||||||
return self.graph.number_of_edges()
|
return self.graph.number_of_edges()
|
||||||
|
|
||||||
def get_nodes_by_type(self, node_type: NodeType) -> List[str]:
|
def get_nodes_by_type(self, node_type: NodeType) -> List[str]:
|
||||||
"""
|
"""Get all nodes of a specific type."""
|
||||||
Get all nodes of a specific type.
|
return [n for n, d in self.graph.nodes(data=True) if d.get('type') == node_type.value]
|
||||||
|
|
||||||
Args:
|
|
||||||
node_type: Type of nodes to retrieve
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of node identifiers
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
node_id for node_id, attributes in self.graph.nodes(data=True)
|
|
||||||
if attributes.get('type') == node_type.value
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_neighbors(self, node_id: str) -> List[str]:
|
def get_neighbors(self, node_id: str) -> List[str]:
|
||||||
"""
|
"""Get all unique neighbors (predecessors and successors) for a node."""
|
||||||
Get all neighboring nodes (both incoming and outgoing).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
node_id: Node identifier
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of neighboring node identifiers
|
|
||||||
"""
|
|
||||||
if not self.graph.has_node(node_id):
|
if not self.graph.has_node(node_id):
|
||||||
return []
|
return []
|
||||||
|
return list(set(self.graph.predecessors(node_id)) | set(self.graph.successors(node_id)))
|
||||||
predecessors = list(self.graph.predecessors(node_id))
|
|
||||||
successors = list(self.graph.successors(node_id))
|
|
||||||
return list(set(predecessors + successors))
|
|
||||||
|
|
||||||
def get_high_confidence_edges(self, min_confidence: float = 0.8) -> List[Tuple[str, str, Dict]]:
|
def get_high_confidence_edges(self, min_confidence: float = 0.8) -> List[Tuple[str, str, Dict]]:
|
||||||
"""
|
"""Get edges with confidence score above a given threshold."""
|
||||||
Get edges with confidence score above threshold.
|
return [(u, v, d) for u, v, d in self.graph.edges(data=True)
|
||||||
|
if d.get('confidence_score', 0) >= min_confidence]
|
||||||
Args:
|
|
||||||
min_confidence: Minimum confidence threshold
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of tuples (source, target, attributes)
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
(source, target, attributes)
|
|
||||||
for source, target, attributes in self.graph.edges(data=True)
|
|
||||||
if attributes.get('confidence_score', 0) >= min_confidence
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_graph_data(self) -> Dict[str, Any]:
|
def get_graph_data(self) -> Dict[str, Any]:
|
||||||
"""
|
"""Export graph data formatted for frontend visualization."""
|
||||||
Export graph data for visualization.
|
|
||||||
Uses comprehensive metadata collected during scanning.
|
|
||||||
"""
|
|
||||||
nodes = []
|
nodes = []
|
||||||
edges = []
|
for node_id, attrs in self.graph.nodes(data=True):
|
||||||
|
node_data = {'id': node_id, 'label': node_id, 'type': attrs.get('type', 'unknown'),
|
||||||
# Create nodes with the comprehensive metadata already collected
|
'metadata': attrs.get('metadata', {}),
|
||||||
for node_id, attributes in self.graph.nodes(data=True):
|
'added_timestamp': attrs.get('added_timestamp')}
|
||||||
node_data = {
|
# Customize node appearance based on type and metadata
|
||||||
'id': node_id,
|
node_type = node_data['type']
|
||||||
'label': node_id,
|
|
||||||
'type': attributes.get('type', 'unknown'),
|
|
||||||
'metadata': attributes.get('metadata', {}),
|
|
||||||
'added_timestamp': attributes.get('added_timestamp')
|
|
||||||
}
|
|
||||||
|
|
||||||
# Handle certificate node labeling
|
|
||||||
if node_id.startswith('cert_'):
|
|
||||||
# For certificate nodes, create a more informative label
|
|
||||||
cert_metadata = node_data['metadata']
|
|
||||||
issuer = cert_metadata.get('issuer_name', 'Unknown')
|
|
||||||
valid_status = "✓" if cert_metadata.get('is_currently_valid') else "✗"
|
|
||||||
node_data['label'] = f"Certificate {valid_status}\n{issuer[:30]}..."
|
|
||||||
|
|
||||||
# Color coding by type
|
|
||||||
type_colors = {
|
|
||||||
'domain': {
|
|
||||||
'background': '#00ff41',
|
|
||||||
'border': '#00aa2e',
|
|
||||||
'highlight': {'background': '#44ff75', 'border': '#00ff41'},
|
|
||||||
'hover': {'background': '#22ff63', 'border': '#00cc35'}
|
|
||||||
},
|
|
||||||
'ip': {
|
|
||||||
'background': '#ff9900',
|
|
||||||
'border': '#cc7700',
|
|
||||||
'highlight': {'background': '#ffbb44', 'border': '#ff9900'},
|
|
||||||
'hover': {'background': '#ffaa22', 'border': '#dd8800'}
|
|
||||||
},
|
|
||||||
'asn': {
|
|
||||||
'background': '#00aaff',
|
|
||||||
'border': '#0088cc',
|
|
||||||
'highlight': {'background': '#44ccff', 'border': '#00aaff'},
|
|
||||||
'hover': {'background': '#22bbff', 'border': '#0099dd'}
|
|
||||||
},
|
|
||||||
'dns_record': {
|
|
||||||
'background': '#9d4edd',
|
|
||||||
'border': '#7b2cbf',
|
|
||||||
'highlight': {'background': '#c77dff', 'border': '#9d4edd'},
|
|
||||||
'hover': {'background': '#b392f0', 'border': '#8b5cf6'}
|
|
||||||
},
|
|
||||||
'large_entity': {
|
|
||||||
'background': '#ff6b6b',
|
|
||||||
'border': '#cc3a3a',
|
|
||||||
'highlight': {'background': '#ff8c8c', 'border': '#ff6b6b'},
|
|
||||||
'hover': {'background': '#ff7a7a', 'border': '#dd4a4a'}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node_color_config = type_colors.get(attributes.get('type', 'unknown'), type_colors['domain'])
|
|
||||||
|
|
||||||
node_data['color'] = node_color_config
|
|
||||||
|
|
||||||
# Add certificate validity indicator if available
|
|
||||||
metadata = node_data['metadata']
|
metadata = node_data['metadata']
|
||||||
if 'certificate_data' in metadata and 'has_valid_cert' in metadata['certificate_data']:
|
if node_type == 'domain' and metadata.get('certificate_data', {}).get('has_valid_cert') is False:
|
||||||
node_data['has_valid_cert'] = metadata['certificate_data']['has_valid_cert']
|
node_data['color'] = {'background': '#c7c7c7', 'border': '#999'} # Gray for invalid cert
|
||||||
|
|
||||||
nodes.append(node_data)
|
nodes.append(node_data)
|
||||||
|
|
||||||
# Create edges (unchanged from original)
|
edges = []
|
||||||
for source, target, attributes in self.graph.edges(data=True):
|
for source, target, attrs in self.graph.edges(data=True):
|
||||||
edge_data = {
|
edges.append({'from': source, 'to': target,
|
||||||
'from': source,
|
'label': attrs.get('relationship_type', ''),
|
||||||
'to': target,
|
'confidence_score': attrs.get('confidence_score', 0),
|
||||||
'label': attributes.get('relationship_type', ''),
|
'source_provider': attrs.get('source_provider', ''),
|
||||||
'confidence_score': attributes.get('confidence_score', 0),
|
'discovery_timestamp': attrs.get('discovery_timestamp')})
|
||||||
'source_provider': attributes.get('source_provider', ''),
|
|
||||||
'discovery_timestamp': attributes.get('discovery_timestamp')
|
|
||||||
}
|
|
||||||
|
|
||||||
# Enhanced edge styling based on confidence
|
|
||||||
confidence = attributes.get('confidence_score', 0)
|
|
||||||
if confidence >= 0.8:
|
|
||||||
edge_data['color'] = {
|
|
||||||
'color': '#00ff41',
|
|
||||||
'highlight': '#44ff75',
|
|
||||||
'hover': '#22ff63',
|
|
||||||
'inherit': False
|
|
||||||
}
|
|
||||||
edge_data['width'] = 4
|
|
||||||
elif confidence >= 0.6:
|
|
||||||
edge_data['color'] = {
|
|
||||||
'color': '#ff9900',
|
|
||||||
'highlight': '#ffbb44',
|
|
||||||
'hover': '#ffaa22',
|
|
||||||
'inherit': False
|
|
||||||
}
|
|
||||||
edge_data['width'] = 3
|
|
||||||
else:
|
|
||||||
edge_data['color'] = {
|
|
||||||
'color': '#666666',
|
|
||||||
'highlight': '#888888',
|
|
||||||
'hover': '#777777',
|
|
||||||
'inherit': False
|
|
||||||
}
|
|
||||||
edge_data['width'] = 2
|
|
||||||
|
|
||||||
# Add dashed line for low confidence
|
|
||||||
if confidence < 0.6:
|
|
||||||
edge_data['dashes'] = [5, 5]
|
|
||||||
|
|
||||||
edges.append(edge_data)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'nodes': nodes,
|
'nodes': nodes, 'edges': edges,
|
||||||
'edges': edges,
|
'statistics': self.get_statistics()['basic_metrics']
|
||||||
'statistics': {
|
|
||||||
'node_count': len(nodes),
|
|
||||||
'edge_count': len(edges),
|
|
||||||
'creation_time': self.creation_time,
|
|
||||||
'last_modified': self.last_modified
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def export_json(self) -> Dict[str, Any]:
|
def export_json(self) -> Dict[str, Any]:
|
||||||
"""
|
"""Export complete graph data as a JSON-serializable dictionary."""
|
||||||
Export complete graph data as JSON for download.
|
graph_data = nx.node_link_data(self.graph) # Use NetworkX's built-in robust serializer
|
||||||
|
return {
|
||||||
Returns:
|
|
||||||
Dictionary containing complete graph data with metadata
|
|
||||||
"""
|
|
||||||
# Get basic graph data
|
|
||||||
graph_data = self.get_graph_data()
|
|
||||||
|
|
||||||
# Add comprehensive metadata
|
|
||||||
export_data = {
|
|
||||||
'export_metadata': {
|
'export_metadata': {
|
||||||
'export_timestamp': datetime.now(timezone.utc).isoformat(),
|
'export_timestamp': datetime.now(timezone.utc).isoformat(),
|
||||||
'graph_creation_time': self.creation_time,
|
'graph_creation_time': self.creation_time,
|
||||||
'last_modified': self.last_modified,
|
'last_modified': self.last_modified,
|
||||||
'total_nodes': self.graph.number_of_nodes(),
|
'total_nodes': self.get_node_count(),
|
||||||
'total_edges': self.graph.number_of_edges(),
|
'total_edges': self.get_edge_count(),
|
||||||
'graph_format': 'dnsrecon_v1'
|
'graph_format': 'dnsrecon_v1_nodeling'
|
||||||
},
|
},
|
||||||
'nodes': graph_data['nodes'],
|
'graph': graph_data,
|
||||||
'edges': graph_data['edges'],
|
'statistics': self.get_statistics()
|
||||||
'node_types': [node_type.value for node_type in NodeType],
|
|
||||||
'relationship_types': [
|
|
||||||
{
|
|
||||||
'name': rel_type.relationship_name,
|
|
||||||
'default_confidence': rel_type.default_confidence
|
|
||||||
}
|
}
|
||||||
for rel_type in RelationshipType
|
|
||||||
],
|
|
||||||
'confidence_distribution': self._get_confidence_distribution()
|
|
||||||
}
|
|
||||||
|
|
||||||
return export_data
|
|
||||||
|
|
||||||
def _get_confidence_distribution(self) -> Dict[str, int]:
|
def _get_confidence_distribution(self) -> Dict[str, int]:
|
||||||
"""Get distribution of confidence scores."""
|
"""Get distribution of edge confidence scores."""
|
||||||
distribution = {'high': 0, 'medium': 0, 'low': 0}
|
distribution = {'high': 0, 'medium': 0, 'low': 0}
|
||||||
|
for _, _, confidence in self.graph.edges(data='confidence_score', default=0):
|
||||||
for _, _, attributes in self.graph.edges(data=True):
|
if confidence >= 0.8: distribution['high'] += 1
|
||||||
confidence = attributes.get('confidence_score', 0)
|
elif confidence >= 0.6: distribution['medium'] += 1
|
||||||
if confidence >= 0.8:
|
else: distribution['low'] += 1
|
||||||
distribution['high'] += 1
|
|
||||||
elif confidence >= 0.6:
|
|
||||||
distribution['medium'] += 1
|
|
||||||
else:
|
|
||||||
distribution['low'] += 1
|
|
||||||
|
|
||||||
return distribution
|
return distribution
|
||||||
|
|
||||||
def get_statistics(self) -> Dict[str, Any]:
|
def get_statistics(self) -> Dict[str, Any]:
|
||||||
"""
|
"""Get comprehensive statistics about the graph."""
|
||||||
Get comprehensive graph statistics.
|
stats = {'basic_metrics': {'total_nodes': self.get_node_count(),
|
||||||
|
'total_edges': self.get_edge_count(),
|
||||||
Returns:
|
|
||||||
Dictionary containing various graph metrics
|
|
||||||
"""
|
|
||||||
stats = {
|
|
||||||
'basic_metrics': {
|
|
||||||
'total_nodes': self.graph.number_of_nodes(),
|
|
||||||
'total_edges': self.graph.number_of_edges(),
|
|
||||||
'creation_time': self.creation_time,
|
'creation_time': self.creation_time,
|
||||||
'last_modified': self.last_modified
|
'last_modified': self.last_modified},
|
||||||
},
|
'node_type_distribution': {}, 'relationship_type_distribution': {},
|
||||||
'node_type_distribution': {},
|
|
||||||
'relationship_type_distribution': {},
|
|
||||||
'confidence_distribution': self._get_confidence_distribution(),
|
'confidence_distribution': self._get_confidence_distribution(),
|
||||||
'provider_distribution': {}
|
'provider_distribution': {}}
|
||||||
}
|
# Calculate distributions
|
||||||
|
|
||||||
# Node type distribution
|
|
||||||
for node_type in NodeType:
|
for node_type in NodeType:
|
||||||
count = len(self.get_nodes_by_type(node_type))
|
stats['node_type_distribution'][node_type.value] = self.get_nodes_by_type(node_type).__len__()
|
||||||
stats['node_type_distribution'][node_type.value] = count
|
for _, _, rel_type in self.graph.edges(data='relationship_type', default='unknown'):
|
||||||
|
stats['relationship_type_distribution'][rel_type] = stats['relationship_type_distribution'].get(rel_type, 0) + 1
|
||||||
# Relationship type distribution
|
for _, _, provider in self.graph.edges(data='source_provider', default='unknown'):
|
||||||
for _, _, attributes in self.graph.edges(data=True):
|
stats['provider_distribution'][provider] = stats['provider_distribution'].get(provider, 0) + 1
|
||||||
rel_type = attributes.get('relationship_type', 'unknown')
|
|
||||||
stats['relationship_type_distribution'][rel_type] = \
|
|
||||||
stats['relationship_type_distribution'].get(rel_type, 0) + 1
|
|
||||||
|
|
||||||
# Provider distribution
|
|
||||||
for _, _, attributes in self.graph.edges(data=True):
|
|
||||||
provider = attributes.get('source_provider', 'unknown')
|
|
||||||
stats['provider_distribution'][provider] = \
|
|
||||||
stats['provider_distribution'].get(provider, 0) + 1
|
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"""Clear all nodes and edges from the graph."""
|
"""Clear all nodes, edges, and indices from the graph."""
|
||||||
self.graph.clear()
|
self.graph.clear()
|
||||||
|
self.correlation_index.clear()
|
||||||
self.creation_time = datetime.now(timezone.utc).isoformat()
|
self.creation_time = datetime.now(timezone.utc).isoformat()
|
||||||
self.last_modified = self.creation_time
|
self.last_modified = self.creation_time
|
@ -707,8 +707,6 @@ class Scanner:
|
|||||||
return discovered_targets
|
return discovered_targets
|
||||||
|
|
||||||
# Process each relationship
|
# Process each relationship
|
||||||
dns_records_to_create = {}
|
|
||||||
|
|
||||||
for i, (source, rel_target, rel_type, confidence, raw_data) in enumerate(results):
|
for i, (source, rel_target, rel_type, confidence, raw_data) in enumerate(results):
|
||||||
# Check stop signal periodically during result processing
|
# Check stop signal periodically during result processing
|
||||||
if i % 10 == 0 and self._is_stop_requested():
|
if i % 10 == 0 and self._is_stop_requested():
|
||||||
@ -751,11 +749,9 @@ class Scanner:
|
|||||||
self._collect_node_metadata_forensic(rel_target, provider_name, rel_type, source, raw_data, target_metadata[rel_target])
|
self._collect_node_metadata_forensic(rel_target, provider_name, rel_type, source, raw_data, target_metadata[rel_target])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Handle DNS record content
|
# Store the record content in the domain's metadata
|
||||||
self._handle_dns_record_content(source, rel_target, rel_type, confidence, raw_data, provider_name, dns_records_to_create)
|
self._collect_node_metadata_forensic(source, provider_name, rel_type, rel_target, raw_data, target_metadata[source])
|
||||||
|
|
||||||
# Create DNS record nodes
|
|
||||||
self._create_dns_record_nodes(dns_records_to_create)
|
|
||||||
|
|
||||||
return discovered_targets
|
return discovered_targets
|
||||||
|
|
||||||
@ -834,56 +830,15 @@ class Scanner:
|
|||||||
'country': raw_data.get('country', '')
|
'country': raw_data.get('country', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_dns_record_content(self, source: str, rel_target: str, rel_type: RelationshipType,
|
record_type_name = rel_type.relationship_name
|
||||||
confidence: float, raw_data: Dict[str, Any], provider_name: str,
|
if record_type_name not in metadata:
|
||||||
dns_records: Dict) -> None:
|
metadata[record_type_name] = []
|
||||||
"""Handle DNS record content with forensic tracking."""
|
|
||||||
dns_record_types = [
|
|
||||||
RelationshipType.TXT_RECORD, RelationshipType.SPF_RECORD,
|
|
||||||
RelationshipType.CAA_RECORD, RelationshipType.SRV_RECORD,
|
|
||||||
RelationshipType.DNSKEY_RECORD, RelationshipType.DS_RECORD,
|
|
||||||
RelationshipType.RRSIG_RECORD, RelationshipType.SSHFP_RECORD,
|
|
||||||
RelationshipType.TLSA_RECORD, RelationshipType.NAPTR_RECORD
|
|
||||||
]
|
|
||||||
|
|
||||||
if rel_type in dns_record_types:
|
if isinstance(target, list):
|
||||||
record_type = rel_type.relationship_name.upper().replace('_RECORD', '')
|
metadata[record_type_name].extend(target)
|
||||||
record_content = rel_target.strip()
|
else:
|
||||||
content_hash = hash(record_content) & 0x7FFFFFFF
|
metadata[record_type_name].append(target)
|
||||||
dns_record_id = f"{record_type}:{content_hash}"
|
|
||||||
|
|
||||||
if dns_record_id not in dns_records:
|
|
||||||
dns_records[dns_record_id] = {
|
|
||||||
'content': record_content,
|
|
||||||
'type': record_type,
|
|
||||||
'domains': set(),
|
|
||||||
'raw_data': raw_data,
|
|
||||||
'provider_name': provider_name,
|
|
||||||
'confidence': confidence
|
|
||||||
}
|
|
||||||
dns_records[dns_record_id]['domains'].add(source)
|
|
||||||
|
|
||||||
def _create_dns_record_nodes(self, dns_records: Dict) -> None:
|
|
||||||
"""Create DNS record nodes with forensic metadata."""
|
|
||||||
for dns_record_id, record_info in dns_records.items():
|
|
||||||
record_metadata = {
|
|
||||||
'record_type': record_info['type'],
|
|
||||||
'content': record_info['content'],
|
|
||||||
'content_hash': dns_record_id.split(':')[1],
|
|
||||||
'associated_domains': list(record_info['domains']),
|
|
||||||
'source_data': record_info['raw_data'],
|
|
||||||
'forensic_note': f"DNS record created from {record_info['provider_name']} query"
|
|
||||||
}
|
|
||||||
|
|
||||||
self.graph.add_node(dns_record_id, NodeType.DNS_RECORD, metadata=record_metadata)
|
|
||||||
|
|
||||||
for domain_name in record_info['domains']:
|
|
||||||
self.graph.add_edge(domain_name, dns_record_id, RelationshipType.DNS_RECORD,
|
|
||||||
record_info['confidence'], record_info['provider_name'],
|
|
||||||
record_info['raw_data'])
|
|
||||||
|
|
||||||
# Forensic logging for DNS record creation
|
|
||||||
self.logger.logger.info(f"DNS record node created: {dns_record_id} for {len(record_info['domains'])} domains")
|
|
||||||
|
|
||||||
def _log_target_processing_error(self, target: str, error: str) -> None:
|
def _log_target_processing_error(self, target: str, error: str) -> None:
|
||||||
"""Log target processing errors for forensic trail."""
|
"""Log target processing errors for forensic trail."""
|
||||||
|
@ -144,8 +144,8 @@ class CrtShProvider(BaseProvider):
|
|||||||
metadata['expires_soon'] = (not_after - datetime.now(timezone.utc)).days <= 30
|
metadata['expires_soon'] = (not_after - datetime.now(timezone.utc)).days <= 30
|
||||||
|
|
||||||
# Add human-readable dates
|
# Add human-readable dates
|
||||||
metadata['not_before_formatted'] = not_before.strftime('%Y-%m-%d %H:%M:%S UTC')
|
metadata['not_before'] = not_before.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||||
metadata['not_after_formatted'] = not_after.strftime('%Y-%m-%d %H:%M:%S UTC')
|
metadata['not_after'] = not_after.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.logger.debug(f"Error computing certificate metadata: {e}")
|
self.logger.logger.debug(f"Error computing certificate metadata: {e}")
|
||||||
|
@ -27,6 +27,7 @@ class DNSProvider(BaseProvider):
|
|||||||
self.resolver = dns.resolver.Resolver()
|
self.resolver = dns.resolver.Resolver()
|
||||||
self.resolver.timeout = 5
|
self.resolver.timeout = 5
|
||||||
self.resolver.lifetime = 10
|
self.resolver.lifetime = 10
|
||||||
|
#self.resolver.nameservers = ['127.0.0.1']
|
||||||
|
|
||||||
def get_name(self) -> str:
|
def get_name(self) -> str:
|
||||||
"""Return the provider name."""
|
"""Return the provider name."""
|
||||||
@ -52,7 +53,7 @@ class DNSProvider(BaseProvider):
|
|||||||
relationships = []
|
relationships = []
|
||||||
|
|
||||||
# Query all record types
|
# Query all record types
|
||||||
for record_type in ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SOA', 'TXT', 'SRV', 'CAA', 'DNSKEY', 'DS', 'RRSIG', 'SSHFP', 'TLSA', 'NAPTR', 'SPF']:
|
for record_type in ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SOA', 'TXT', 'SRV', 'CAA']:
|
||||||
relationships.extend(self._query_record(domain, record_type))
|
relationships.extend(self._query_record(domain, record_type))
|
||||||
|
|
||||||
return relationships
|
return relationships
|
||||||
@ -133,7 +134,7 @@ class DNSProvider(BaseProvider):
|
|||||||
target = str(record.exchange).rstrip('.')
|
target = str(record.exchange).rstrip('.')
|
||||||
elif record_type == 'SOA':
|
elif record_type == 'SOA':
|
||||||
target = str(record.mname).rstrip('.')
|
target = str(record.mname).rstrip('.')
|
||||||
elif record_type in ['TXT', 'SPF']:
|
elif record_type in ['TXT']:
|
||||||
target = b' '.join(record.strings).decode('utf-8', 'ignore')
|
target = b' '.join(record.strings).decode('utf-8', 'ignore')
|
||||||
elif record_type == 'SRV':
|
elif record_type == 'SRV':
|
||||||
target = str(record.target).rstrip('.')
|
target = str(record.target).rstrip('.')
|
||||||
@ -151,7 +152,13 @@ class DNSProvider(BaseProvider):
|
|||||||
'ttl': response.ttl
|
'ttl': response.ttl
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
relationship_type_enum = getattr(RelationshipType, f"{record_type}_RECORD")
|
relationship_type_enum_name = f"{record_type}_RECORD"
|
||||||
|
# Handle TXT records as metadata, not relationships
|
||||||
|
if record_type == 'TXT':
|
||||||
|
relationship_type_enum = RelationshipType.A_RECORD # Dummy value, won't be used
|
||||||
|
else:
|
||||||
|
relationship_type_enum = getattr(RelationshipType, relationship_type_enum_name)
|
||||||
|
|
||||||
relationships.append((
|
relationships.append((
|
||||||
domain,
|
domain,
|
||||||
target,
|
target,
|
||||||
|
@ -336,6 +336,10 @@ class GraphManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.type === 'correlation_object') {
|
||||||
|
processedNode.label = this.formatNodeLabel(node.metadata.value, node.type);
|
||||||
|
}
|
||||||
|
|
||||||
return processedNode;
|
return processedNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,7 +410,7 @@ class GraphManager {
|
|||||||
'ip': '#ff9900', // Amber
|
'ip': '#ff9900', // Amber
|
||||||
'asn': '#00aaff', // Blue
|
'asn': '#00aaff', // Blue
|
||||||
'large_entity': '#ff6b6b', // Red for large entities
|
'large_entity': '#ff6b6b', // Red for large entities
|
||||||
'dns_record': '#9620c0ff'
|
'correlation_object': '#9620c0ff'
|
||||||
};
|
};
|
||||||
return colors[nodeType] || '#ffffff';
|
return colors[nodeType] || '#ffffff';
|
||||||
}
|
}
|
||||||
@ -422,7 +426,7 @@ class GraphManager {
|
|||||||
'domain': '#00aa2e',
|
'domain': '#00aa2e',
|
||||||
'ip': '#cc7700',
|
'ip': '#cc7700',
|
||||||
'asn': '#0088cc',
|
'asn': '#0088cc',
|
||||||
'dns_record': '#c235c9ff'
|
'correlation_object': '#c235c9ff'
|
||||||
};
|
};
|
||||||
return borderColors[nodeType] || '#666666';
|
return borderColors[nodeType] || '#666666';
|
||||||
}
|
}
|
||||||
@ -437,7 +441,7 @@ class GraphManager {
|
|||||||
'domain': 12,
|
'domain': 12,
|
||||||
'ip': 14,
|
'ip': 14,
|
||||||
'asn': 16,
|
'asn': 16,
|
||||||
'dns_record': 8
|
'correlation_object': 8
|
||||||
};
|
};
|
||||||
return sizes[nodeType] || 12;
|
return sizes[nodeType] || 12;
|
||||||
}
|
}
|
||||||
@ -452,7 +456,7 @@ class GraphManager {
|
|||||||
'domain': 'dot',
|
'domain': 'dot',
|
||||||
'ip': 'square',
|
'ip': 'square',
|
||||||
'asn': 'triangle',
|
'asn': 'triangle',
|
||||||
'dns_record': 'hexagon'
|
'correlation_object': 'hexagon'
|
||||||
};
|
};
|
||||||
return shapes[nodeType] || 'dot';
|
return shapes[nodeType] || 'dot';
|
||||||
}
|
}
|
||||||
@ -850,20 +854,6 @@ class GraphManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Export graph as image (if needed for future implementation)
|
|
||||||
* @param {string} format - Image format ('png', 'jpeg')
|
|
||||||
* @returns {string} Data URL of the image
|
|
||||||
*/
|
|
||||||
exportAsImage(format = 'png') {
|
|
||||||
if (!this.network) return null;
|
|
||||||
|
|
||||||
// This would require additional vis.js functionality
|
|
||||||
// Placeholder for future implementation
|
|
||||||
console.log('Image export not yet implemented');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply filters to the graph
|
* Apply filters to the graph
|
||||||
* @param {string} nodeType - The type of node to show ('all' for no filter)
|
* @param {string} nodeType - The type of node to show ('all' for no filter)
|
||||||
|
@ -859,6 +859,18 @@ class DNSReconApp {
|
|||||||
detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
|
detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
|
||||||
detailsHtml += createDetailRow('VirusTotal Data', metadata.virustotal);
|
detailsHtml += createDetailRow('VirusTotal Data', metadata.virustotal);
|
||||||
break;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata.certificate_data && Object.keys(metadata.certificate_data).length > 0) {
|
if (metadata.certificate_data && Object.keys(metadata.certificate_data).length > 0) {
|
||||||
|
@ -120,7 +120,7 @@
|
|||||||
<option value="domain">Domain</option>
|
<option value="domain">Domain</option>
|
||||||
<option value="ip">IP</option>
|
<option value="ip">IP</option>
|
||||||
<option value="asn">ASN</option>
|
<option value="asn">ASN</option>
|
||||||
<option value="dns_record">DNS Record</option>
|
<option value="correlation_object">Correlation Object</option>
|
||||||
<option value="large_entity">Large Entity</option>
|
<option value="large_entity">Large Entity</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -157,7 +157,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<div class="legend-color" style="background-color: #9d4edd;"></div>
|
<div class="legend-color" style="background-color: #9d4edd;"></div>
|
||||||
<span>DNS Records</span>
|
<span>Correlation Objects</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<div class="legend-edge high-confidence"></div>
|
<div class="legend-edge high-confidence"></div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user