new data model refinement
This commit is contained in:
parent
97aa18f788
commit
733e1da640
@ -40,6 +40,7 @@ class GraphManager:
|
||||
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}')
|
||||
self.EXCLUDED_KEYS = ['confidence', 'provider', 'timestamp', 'type']
|
||||
|
||||
def __getstate__(self):
|
||||
"""Prepare GraphManager for pickling, excluding compiled regex."""
|
||||
@ -54,145 +55,44 @@ class GraphManager:
|
||||
self.__dict__.update(state)
|
||||
self.date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}')
|
||||
|
||||
def _update_correlation_index(self, node_id: str, data: Any, path: List[str] = [], parent_attr: str = ""):
|
||||
"""Recursively traverse metadata and add hashable values to the index with better path tracking."""
|
||||
if path is None:
|
||||
path = []
|
||||
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
self._update_correlation_index(node_id, value, path + [key], key)
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
# Instead of just using [i], include the parent attribute context
|
||||
list_path_component = f"[{i}]" if not parent_attr else f"{parent_attr}[{i}]"
|
||||
self._update_correlation_index(node_id, item, path + [list_path_component], parent_attr)
|
||||
else:
|
||||
self._add_to_correlation_index(node_id, data, ".".join(path), parent_attr)
|
||||
|
||||
def _add_to_correlation_index(self, node_id: str, value: Any, path_str: str, parent_attr: str = ""):
|
||||
"""Add a hashable value to the correlation index, filtering out noise."""
|
||||
if not isinstance(value, (str, int, float, bool)) or value is None:
|
||||
def process_correlations_for_node(self, node_id: str):
|
||||
"""Process correlations for a given node based on its attributes."""
|
||||
if not self.graph.has_node(node_id):
|
||||
return
|
||||
|
||||
# Ignore certain paths that contain noisy, non-unique identifiers
|
||||
if any(keyword in path_str.lower() for keyword in ['count', 'total', 'timestamp', 'date']):
|
||||
return
|
||||
node_attributes = self.graph.nodes[node_id].get('attributes', [])
|
||||
for attr in node_attributes:
|
||||
attr_name = attr.get('name')
|
||||
attr_value = attr.get('value')
|
||||
|
||||
# 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) < 1024 or abs(value) > 65535):
|
||||
return # Ignore small integers and common port numbers
|
||||
elif isinstance(value, bool):
|
||||
return # Ignore boolean values
|
||||
if attr_name in self.EXCLUDED_KEYS or not isinstance(attr_value, (str, int, float, bool)) or attr_value is None:
|
||||
continue
|
||||
|
||||
# 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 isinstance(attr_value, bool):
|
||||
continue
|
||||
|
||||
# Store both the full path and the parent attribute for better edge labeling
|
||||
correlation_entry = {
|
||||
'path': path_str,
|
||||
'parent_attr': parent_attr,
|
||||
'meaningful_attr': self._extract_meaningful_attribute(path_str, parent_attr)
|
||||
}
|
||||
if isinstance(attr_value, str) and (len(attr_value) < 4 or self.date_pattern.match(attr_value)):
|
||||
continue
|
||||
|
||||
if correlation_entry not in self.correlation_index[value][node_id]:
|
||||
self.correlation_index[value][node_id].append(correlation_entry)
|
||||
if attr_value not in self.correlation_index:
|
||||
self.correlation_index[attr_value] = set()
|
||||
|
||||
def _extract_meaningful_attribute(self, path_str: str, parent_attr: str = "") -> str:
|
||||
"""Extract the most meaningful attribute name from a path string."""
|
||||
if not path_str:
|
||||
return "unknown"
|
||||
self.correlation_index[attr_value].add(node_id)
|
||||
|
||||
path_parts = path_str.split('.')
|
||||
if len(self.correlation_index[attr_value]) > 1:
|
||||
self._create_correlation_node_and_edges(attr_value, self.correlation_index[attr_value])
|
||||
|
||||
# Look for the last non-array-index part
|
||||
for part in reversed(path_parts):
|
||||
# Skip array indices like [0], [1], etc.
|
||||
if not (part.startswith('[') and part.endswith(']') and part[1:-1].isdigit()):
|
||||
# Clean up compound names like "hostnames[0]" to just "hostnames"
|
||||
clean_part = re.sub(r'\[\d+\]$', '', part)
|
||||
if clean_part:
|
||||
return clean_part
|
||||
def _create_correlation_node_and_edges(self, value, nodes):
|
||||
"""Create a correlation node and edges to the correlated nodes."""
|
||||
correlation_node_id = f"corr_{value}"
|
||||
if not self.graph.has_node(correlation_node_id):
|
||||
self.add_node(correlation_node_id, NodeType.CORRELATION_OBJECT,
|
||||
metadata={'value': value, 'correlated_nodes': list(nodes)})
|
||||
|
||||
# Fallback to parent attribute if available
|
||||
if parent_attr:
|
||||
return parent_attr
|
||||
for node_id in nodes:
|
||||
if self.graph.has_node(node_id) and not self.graph.has_edge(node_id, correlation_node_id):
|
||||
self.add_edge(node_id, correlation_node_id, "correlation", confidence_score=0.9)
|
||||
|
||||
# Last resort - use the first meaningful part
|
||||
for part in path_parts:
|
||||
if not (part.startswith('[') and part.endswith(']') and part[1:-1].isdigit()):
|
||||
clean_part = re.sub(r'\[\d+\]$', '', part)
|
||||
if clean_part:
|
||||
return clean_part
|
||||
|
||||
return "correlation"
|
||||
|
||||
def _check_for_correlations(self, new_node_id: str, data: Any, path: List[str] = [], parent_attr: str = "") -> 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], key))
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
list_path_component = f"[{i}]" if not parent_attr else f"{parent_attr}[{i}]"
|
||||
all_correlations.extend(self._check_for_correlations(new_node_id, item, path + [list_path_component], parent_attr))
|
||||
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),
|
||||
'parent_attr': parent_attr,
|
||||
'meaningful_attr': self._extract_meaningful_attribute(".".join(path), parent_attr)
|
||||
}
|
||||
all_sources = [new_source]
|
||||
|
||||
for node_id, path_entries in existing_nodes_with_paths.items():
|
||||
for entry in path_entries:
|
||||
if isinstance(entry, dict):
|
||||
all_sources.append({
|
||||
'node_id': node_id,
|
||||
'path': entry['path'],
|
||||
'parent_attr': entry.get('parent_attr', ''),
|
||||
'meaningful_attr': entry.get('meaningful_attr', self._extract_meaningful_attribute(entry['path'], entry.get('parent_attr', '')))
|
||||
})
|
||||
else:
|
||||
# Handle legacy string-only entries
|
||||
all_sources.append({
|
||||
'node_id': node_id,
|
||||
'path': str(entry),
|
||||
'parent_attr': '',
|
||||
'meaningful_attr': self._extract_meaningful_attribute(str(entry))
|
||||
})
|
||||
|
||||
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, attributes: Optional[List[Dict[str, Any]]] = None,
|
||||
description: str = "", metadata: Optional[Dict[str, Any]] = None) -> bool:
|
||||
@ -232,78 +132,9 @@ class GraphManager:
|
||||
existing_metadata.update(metadata)
|
||||
self.graph.nodes[node_id]['metadata'] = existing_metadata
|
||||
|
||||
if attributes and node_type != NodeType.CORRELATION_OBJECT:
|
||||
correlations = self._check_for_correlations(node_id, attributes)
|
||||
for corr in correlations:
|
||||
value = corr['value']
|
||||
|
||||
# STEP 1: Substring check against all existing nodes
|
||||
if self._correlation_value_matches_existing_node(value):
|
||||
# Skip creating correlation node - would be redundant
|
||||
continue
|
||||
|
||||
eligible_nodes = set(corr['nodes'])
|
||||
|
||||
if len(eligible_nodes) < 2:
|
||||
# Need at least 2 nodes to create a correlation
|
||||
continue
|
||||
|
||||
# STEP 3: Check for existing correlation node with same connection pattern
|
||||
correlation_nodes_with_pattern = self._find_correlation_nodes_with_same_pattern(eligible_nodes)
|
||||
|
||||
if correlation_nodes_with_pattern:
|
||||
# STEP 4: Merge with existing correlation node
|
||||
target_correlation_node = correlation_nodes_with_pattern[0]
|
||||
self._merge_correlation_values(target_correlation_node, value, corr)
|
||||
else:
|
||||
# STEP 5: Create new correlation node for eligible nodes only
|
||||
correlation_node_id = f"corr_{abs(hash(str(sorted(eligible_nodes))))}"
|
||||
self.add_node(correlation_node_id, NodeType.CORRELATION_OBJECT,
|
||||
metadata={'values': [value], 'sources': corr['sources'],
|
||||
'correlated_nodes': list(eligible_nodes)})
|
||||
|
||||
# Create edges from eligible nodes to this correlation node with better labeling
|
||||
for c_node_id in eligible_nodes:
|
||||
if self.graph.has_node(c_node_id):
|
||||
# Find the best attribute name for this node
|
||||
meaningful_attr = self._find_best_attribute_name_for_node(c_node_id, corr['sources'])
|
||||
relationship_type = f"c_{meaningful_attr}"
|
||||
self.add_edge(c_node_id, correlation_node_id, relationship_type, confidence_score=0.9)
|
||||
|
||||
self._update_correlation_index(node_id, attributes)
|
||||
|
||||
self.last_modified = datetime.now(timezone.utc).isoformat()
|
||||
return is_new_node
|
||||
|
||||
def _find_best_attribute_name_for_node(self, node_id: str, sources: List[Dict]) -> str:
|
||||
"""Find the best attribute name for a correlation edge by looking at the sources."""
|
||||
node_sources = [s for s in sources if s['node_id'] == node_id]
|
||||
|
||||
if not node_sources:
|
||||
return "correlation"
|
||||
|
||||
# Use the meaningful_attr if available
|
||||
for source in node_sources:
|
||||
meaningful_attr = source.get('meaningful_attr')
|
||||
if meaningful_attr and meaningful_attr != "unknown":
|
||||
return meaningful_attr
|
||||
|
||||
# Fallback to parent_attr
|
||||
for source in node_sources:
|
||||
parent_attr = source.get('parent_attr')
|
||||
if parent_attr:
|
||||
return parent_attr
|
||||
|
||||
# Last resort - extract from path
|
||||
for source in node_sources:
|
||||
path = source.get('path', '')
|
||||
if path:
|
||||
extracted = self._extract_meaningful_attribute(path)
|
||||
if extracted != "unknown":
|
||||
return extracted
|
||||
|
||||
return "correlation"
|
||||
|
||||
def _has_direct_edge_bidirectional(self, node_a: str, node_b: str) -> bool:
|
||||
"""
|
||||
Check if there's a direct edge between two nodes in either direction.
|
||||
|
||||
@ -506,6 +506,7 @@ class Scanner:
|
||||
large_entity_members.update(discovered)
|
||||
else:
|
||||
new_targets.update(discovered)
|
||||
self.graph.process_correlations_for_node(target)
|
||||
else:
|
||||
print(f"Stop requested after processing results from {provider.get_name()}")
|
||||
except Exception as e:
|
||||
|
||||
@ -134,7 +134,7 @@ class CrtShProvider(BaseProvider):
|
||||
self.logger.logger.info(f"Refreshed and merged cache for {domain}")
|
||||
else: # "not_found"
|
||||
# Create new result from processed certs
|
||||
result = self._process_certificates_to_result(domain, current_processed_certs)
|
||||
result = self._process_certificates_to_result(domain, raw_certificates)
|
||||
self.logger.logger.info(f"Created fresh result for {domain} ({result.get_relationship_count()} relationships)")
|
||||
|
||||
# Save the result to cache
|
||||
@ -277,37 +277,38 @@ class CrtShProvider(BaseProvider):
|
||||
print(f"CrtSh processing cancelled before processing for domain: {domain}")
|
||||
return result
|
||||
|
||||
# Aggregate certificate data by domain
|
||||
domain_certificates = {}
|
||||
all_discovered_domains = set()
|
||||
|
||||
# Process certificates with cancellation checking
|
||||
for i, cert_data in enumerate(certificates):
|
||||
if i % 5 == 0 and self._stop_event and self._stop_event.is_set():
|
||||
print(f"CrtSh processing cancelled at certificate {i} for domain: {domain}")
|
||||
break
|
||||
|
||||
cert_metadata = self._extract_certificate_metadata(cert_data)
|
||||
cert_domains = self._extract_domains_from_certificate(cert_data)
|
||||
|
||||
all_discovered_domains.update(cert_domains)
|
||||
|
||||
for cert_domain in cert_domains:
|
||||
if not _is_valid_domain(cert_domain):
|
||||
continue
|
||||
|
||||
if cert_domain not in domain_certificates:
|
||||
domain_certificates[cert_domain] = []
|
||||
|
||||
domain_certificates[cert_domain].append(cert_metadata)
|
||||
for key, value in self._extract_certificate_metadata(cert_data).items():
|
||||
if value is not None:
|
||||
result.add_attribute(
|
||||
target_node=cert_domain,
|
||||
name=f"cert_{key}",
|
||||
value=value,
|
||||
attr_type='certificate_data',
|
||||
provider=self.name,
|
||||
confidence=0.9
|
||||
)
|
||||
|
||||
if self._stop_event and self._stop_event.is_set():
|
||||
print(f"CrtSh query cancelled before relationship creation for domain: {domain}")
|
||||
return result
|
||||
|
||||
# Create relationships from query domain to ALL discovered domains
|
||||
for i, discovered_domain in enumerate(all_discovered_domains):
|
||||
if discovered_domain == domain:
|
||||
continue # Skip self-relationships
|
||||
continue
|
||||
|
||||
if i % 10 == 0 and self._stop_event and self._stop_event.is_set():
|
||||
print(f"CrtSh relationship creation cancelled for domain: {domain}")
|
||||
@ -316,65 +317,28 @@ class CrtShProvider(BaseProvider):
|
||||
if not _is_valid_domain(discovered_domain):
|
||||
continue
|
||||
|
||||
# Get certificates for both domains
|
||||
query_domain_certs = domain_certificates.get(domain, [])
|
||||
discovered_domain_certs = domain_certificates.get(discovered_domain, [])
|
||||
|
||||
# Find shared certificates
|
||||
shared_certificates = self._find_shared_certificates(query_domain_certs, discovered_domain_certs)
|
||||
|
||||
# Calculate confidence
|
||||
confidence = self._calculate_domain_relationship_confidence(
|
||||
domain, discovered_domain, shared_certificates, all_discovered_domains
|
||||
domain, discovered_domain, [], all_discovered_domains
|
||||
)
|
||||
|
||||
# Create comprehensive raw data for the relationship
|
||||
relationship_raw_data = {
|
||||
'relationship_type': 'certificate_discovery',
|
||||
'shared_certificates': shared_certificates,
|
||||
'total_shared_certs': len(shared_certificates),
|
||||
'discovery_context': self._determine_relationship_context(discovered_domain, domain),
|
||||
'domain_certificates': {
|
||||
domain: self._summarize_certificates(query_domain_certs),
|
||||
discovered_domain: self._summarize_certificates(discovered_domain_certs)
|
||||
}
|
||||
}
|
||||
|
||||
# Add relationship
|
||||
result.add_relationship(
|
||||
source_node=domain,
|
||||
target_node=discovered_domain,
|
||||
relationship_type='san_certificate',
|
||||
provider=self.name,
|
||||
confidence=confidence,
|
||||
raw_data=relationship_raw_data
|
||||
raw_data={'relationship_type': 'certificate_discovery'}
|
||||
)
|
||||
|
||||
# Log the relationship discovery
|
||||
self.log_relationship_discovery(
|
||||
source_node=domain,
|
||||
target_node=discovered_domain,
|
||||
relationship_type='san_certificate',
|
||||
confidence_score=confidence,
|
||||
raw_data=relationship_raw_data,
|
||||
raw_data={'relationship_type': 'certificate_discovery'},
|
||||
discovery_method="certificate_transparency_analysis"
|
||||
)
|
||||
|
||||
# Add certificate summary as attributes for all domains that have certificates
|
||||
for cert_domain, cert_list in domain_certificates.items():
|
||||
if cert_list:
|
||||
cert_summary = self._summarize_certificates(cert_list)
|
||||
|
||||
result.add_attribute(
|
||||
target_node=cert_domain,
|
||||
name='certificates',
|
||||
value=cert_summary,
|
||||
attr_type='certificate_data',
|
||||
provider=self.name,
|
||||
confidence=0.9,
|
||||
metadata={'total_certificates': len(cert_list)}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _extract_certificate_metadata(self, cert_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
@ -222,110 +222,62 @@ class ShodanProvider(BaseProvider):
|
||||
"""
|
||||
result = ProviderResult()
|
||||
|
||||
# Extract hostname relationships
|
||||
hostnames = data.get('hostnames', [])
|
||||
for hostname in hostnames:
|
||||
if _is_valid_domain(hostname):
|
||||
for key, value in data.items():
|
||||
if key == 'hostnames':
|
||||
for hostname in value:
|
||||
if _is_valid_domain(hostname):
|
||||
result.add_relationship(
|
||||
source_node=ip,
|
||||
target_node=hostname,
|
||||
relationship_type='a_record',
|
||||
provider=self.name,
|
||||
confidence=0.8,
|
||||
raw_data=data
|
||||
)
|
||||
self.log_relationship_discovery(
|
||||
source_node=ip,
|
||||
target_node=hostname,
|
||||
relationship_type='a_record',
|
||||
confidence_score=0.8,
|
||||
raw_data=data,
|
||||
discovery_method="shodan_host_lookup"
|
||||
)
|
||||
elif key == 'asn':
|
||||
asn_name = f"AS{value[2:]}" if isinstance(value, str) and value.startswith('AS') else f"AS{value}"
|
||||
result.add_relationship(
|
||||
source_node=ip,
|
||||
target_node=hostname,
|
||||
relationship_type='a_record',
|
||||
target_node=asn_name,
|
||||
relationship_type='asn_membership',
|
||||
provider=self.name,
|
||||
confidence=0.8,
|
||||
confidence=0.7,
|
||||
raw_data=data
|
||||
)
|
||||
|
||||
self.log_relationship_discovery(
|
||||
source_node=ip,
|
||||
target_node=hostname,
|
||||
relationship_type='a_record',
|
||||
confidence_score=0.8,
|
||||
target_node=asn_name,
|
||||
relationship_type='asn_membership',
|
||||
confidence_score=0.7,
|
||||
raw_data=data,
|
||||
discovery_method="shodan_host_lookup"
|
||||
discovery_method="shodan_asn_lookup"
|
||||
)
|
||||
elif key == 'ports':
|
||||
for port in value:
|
||||
result.add_attribute(
|
||||
target_node=ip,
|
||||
name='open_port',
|
||||
value=port,
|
||||
attr_type='network_info',
|
||||
provider=self.name,
|
||||
confidence=0.9
|
||||
)
|
||||
elif isinstance(value, (str, int, float, bool)) and value is not None:
|
||||
result.add_attribute(
|
||||
target_node=ip,
|
||||
name=f"shodan_{key}",
|
||||
value=value,
|
||||
attr_type='shodan_info',
|
||||
provider=self.name,
|
||||
confidence=0.9
|
||||
)
|
||||
|
||||
# Extract ASN relationship
|
||||
asn = data.get('asn')
|
||||
if asn:
|
||||
asn_name = f"AS{asn[2:]}" if isinstance(asn, str) and asn.startswith('AS') else f"AS{asn}"
|
||||
result.add_relationship(
|
||||
source_node=ip,
|
||||
target_node=asn_name,
|
||||
relationship_type='asn_membership',
|
||||
provider=self.name,
|
||||
confidence=0.7,
|
||||
raw_data=data
|
||||
)
|
||||
|
||||
self.log_relationship_discovery(
|
||||
source_node=ip,
|
||||
target_node=asn_name,
|
||||
relationship_type='asn_membership',
|
||||
confidence_score=0.7,
|
||||
raw_data=data,
|
||||
discovery_method="shodan_asn_lookup"
|
||||
)
|
||||
|
||||
# Add comprehensive Shodan host information as attributes
|
||||
if 'ports' in data:
|
||||
result.add_attribute(
|
||||
target_node=ip,
|
||||
name='ports',
|
||||
value=data['ports'],
|
||||
attr_type='network_info',
|
||||
provider=self.name,
|
||||
confidence=0.9
|
||||
)
|
||||
|
||||
if 'os' in data and data['os']:
|
||||
result.add_attribute(
|
||||
target_node=ip,
|
||||
name='operating_system',
|
||||
value=data['os'],
|
||||
attr_type='system_info',
|
||||
provider=self.name,
|
||||
confidence=0.8
|
||||
)
|
||||
|
||||
if 'org' in data:
|
||||
result.add_attribute(
|
||||
target_node=ip,
|
||||
name='organization',
|
||||
value=data['org'],
|
||||
attr_type='network_info',
|
||||
provider=self.name,
|
||||
confidence=0.8
|
||||
)
|
||||
|
||||
if 'country_name' in data:
|
||||
result.add_attribute(
|
||||
target_node=ip,
|
||||
name='country',
|
||||
value=data['country_name'],
|
||||
attr_type='location_info',
|
||||
provider=self.name,
|
||||
confidence=0.9
|
||||
)
|
||||
|
||||
if 'city' in data:
|
||||
result.add_attribute(
|
||||
target_node=ip,
|
||||
name='city',
|
||||
value=data['city'],
|
||||
attr_type='location_info',
|
||||
provider=self.name,
|
||||
confidence=0.8
|
||||
)
|
||||
|
||||
# Store complete Shodan data as a comprehensive attribute
|
||||
result.add_attribute(
|
||||
target_node=ip,
|
||||
name='shodan_host_info',
|
||||
value=data, # Complete Shodan response for full forensic detail
|
||||
attr_type='comprehensive_data',
|
||||
provider=self.name,
|
||||
confidence=0.9,
|
||||
metadata={'data_source': 'shodan_api', 'query_type': 'host_lookup'}
|
||||
)
|
||||
|
||||
return result
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Graph visualization module for DNSRecon
|
||||
* Handles network graph rendering using vis.js with proper large entity node hiding
|
||||
* UPDATED: Now compatible with unified data model (StandardAttribute objects)
|
||||
* UPDATED: Now compatible with a strictly flat, unified data model for attributes.
|
||||
*/
|
||||
const contextMenuCSS = `
|
||||
.graph-context-menu {
|
||||
@ -484,7 +484,7 @@ class GraphManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATED: Process node data with styling and metadata for unified data model
|
||||
* UPDATED: Process node data with styling and metadata for the flat data model
|
||||
* @param {Object} node - Raw node data with standardized attributes
|
||||
* @returns {Object} Processed node data
|
||||
*/
|
||||
@ -509,14 +509,6 @@ class GraphManager {
|
||||
processedNode.borderWidth = Math.max(2, Math.floor(node.confidence * 5));
|
||||
}
|
||||
|
||||
// UPDATED: Style based on certificate validity using unified data model
|
||||
if (node.type === 'domain') {
|
||||
const certificatesAttr = this.findAttributeByName(node.attributes, 'certificates');
|
||||
if (certificatesAttr && certificatesAttr.value && certificatesAttr.value.has_valid_cert === false) {
|
||||
processedNode.color = { background: '#888888', border: '#666666' };
|
||||
}
|
||||
}
|
||||
|
||||
// Handle merged correlation objects (similar to large entities)
|
||||
if (node.type === 'correlation_object') {
|
||||
const metadata = node.metadata || {};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Main application logic for DNSRecon web interface
|
||||
* Handles UI interactions, API communication, and data flow
|
||||
* UPDATED: Now compatible with unified data model (StandardAttribute objects)
|
||||
* UPDATED: Now compatible with a strictly flat, unified data model for attributes.
|
||||
*/
|
||||
|
||||
class DNSReconApp {
|
||||
@ -879,21 +879,9 @@ class DNSReconApp {
|
||||
// Relationships sections
|
||||
html += this.generateRelationshipsSection(node);
|
||||
|
||||
// UPDATED: Enhanced attributes section with special certificate handling for unified model
|
||||
// UPDATED: Simplified attributes section for the flat model
|
||||
if (node.attributes && Array.isArray(node.attributes) && node.attributes.length > 0) {
|
||||
// Find certificate attribute separately
|
||||
const certificatesAttr = this.findAttributeByName(node.attributes, 'certificates');
|
||||
|
||||
// Handle certificates separately with enhanced display
|
||||
if (certificatesAttr) {
|
||||
html += this.generateCertificateSection(certificatesAttr);
|
||||
}
|
||||
|
||||
// Handle other attributes normally (excluding certificates to avoid duplication)
|
||||
const otherAttributes = node.attributes.filter(attr => attr.name !== 'certificates');
|
||||
if (otherAttributes.length > 0) {
|
||||
html += this.generateAttributesSection(otherAttributes);
|
||||
}
|
||||
html += this.generateAttributesSection(node.attributes);
|
||||
}
|
||||
|
||||
// Description section
|
||||
@ -905,213 +893,6 @@ class DNSReconApp {
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATED: Enhanced certificate section generation for unified data model
|
||||
*/
|
||||
generateCertificateSection(certificatesAttr) {
|
||||
const certificates = certificatesAttr.value;
|
||||
if (!certificates || typeof certificates !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="modal-section">
|
||||
<details>
|
||||
<summary>🔒 SSL/TLS Certificates</summary>
|
||||
<div class="modal-section-content">
|
||||
`;
|
||||
|
||||
// Certificate summary using existing grid pattern
|
||||
html += this.generateCertificateSummary(certificates);
|
||||
|
||||
// Latest certificate info using existing attribute display
|
||||
if (certificates.latest_certificate) {
|
||||
html += this.generateLatestCertificateInfo(certificates.latest_certificate);
|
||||
}
|
||||
|
||||
// Detailed certificate list if available
|
||||
if (certificates.certificate_details && Array.isArray(certificates.certificate_details)) {
|
||||
html += this.generateCertificateList(certificates.certificate_details);
|
||||
}
|
||||
|
||||
html += '</div></details></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate latest certificate info using existing attribute list
|
||||
*/
|
||||
generateLatestCertificateInfo(latest) {
|
||||
const isValid = latest.is_currently_valid;
|
||||
const statusText = isValid ? 'Valid' : 'Invalid/Expired';
|
||||
const statusColor = isValid ? '#00ff41' : '#ff6b6b';
|
||||
|
||||
let html = `
|
||||
<div style="margin-bottom: 1rem; padding: 0.75rem; background: rgba(255, 255, 255, 0.02); border-radius: 4px; border: 1px solid #333;">
|
||||
<h5 style="margin: 0 0 0.5rem 0; color: #00ff41; font-size: 0.9rem;">Most Recent Certificate</h5>
|
||||
<div class="attribute-list">
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Status:</span>
|
||||
<span class="attribute-value-compact" style="color: ${statusColor}; font-weight: 600;">${statusText}</span>
|
||||
</div>
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Issued:</span>
|
||||
<span class="attribute-value-compact">${latest.not_before || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Expires:</span>
|
||||
<span class="attribute-value-compact">${latest.not_after || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Issuer:</span>
|
||||
<span class="attribute-value-compact">${this.escapeHtml(latest.issuer_name || 'Unknown')}</span>
|
||||
</div>
|
||||
${latest.certificate_id ? `
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Certificate:</span>
|
||||
<span class="attribute-value-compact">
|
||||
<a href="https://crt.sh/?id=${latest.certificate_id}" target="_blank" class="cert-link">
|
||||
View on crt.sh ↗
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate certificate list using existing collapsible structure
|
||||
*/
|
||||
generateCertificateList(certificateDetails) {
|
||||
if (!certificateDetails || certificateDetails.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Limit display to prevent overwhelming the UI
|
||||
const maxDisplay = 8;
|
||||
const certificates = certificateDetails.slice(0, maxDisplay);
|
||||
const remaining = certificateDetails.length - maxDisplay;
|
||||
|
||||
let html = `
|
||||
<details style="margin-top: 1rem;">
|
||||
<summary>📋 Certificate Details (${certificates.length}${remaining > 0 ? ` of ${certificateDetails.length}` : ''})</summary>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
`;
|
||||
|
||||
certificates.forEach((cert, index) => {
|
||||
const isValid = cert.is_currently_valid;
|
||||
let statusText = isValid ? '✅ Valid' : '❌ Invalid/Expired';
|
||||
let statusColor = isValid ? '#00ff41' : '#ff6b6b';
|
||||
|
||||
if (cert.expires_soon && isValid) {
|
||||
statusText = '⚠️ Valid (Expiring Soon)';
|
||||
statusColor = '#ff9900';
|
||||
}
|
||||
|
||||
html += `
|
||||
<div style="margin-bottom: 0.75rem; padding: 0.75rem; background: rgba(255, 255, 255, 0.02); border: 1px solid #333; border-radius: 4px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; border-bottom: 1px solid #333; padding-bottom: 0.5rem;">
|
||||
<span style="font-weight: 600; color: #999;">#${index + 1}</span>
|
||||
<span style="color: ${statusColor}; font-size: 0.85rem; font-weight: 500;">${statusText}</span>
|
||||
${cert.certificate_id ? `
|
||||
<a href="https://crt.sh/?id=${cert.certificate_id}" target="_blank" class="cert-link">crt.sh ↗</a>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="attribute-list">
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Common Name:</span>
|
||||
<span class="attribute-value-compact">${this.escapeHtml(cert.common_name || 'N/A')}</span>
|
||||
</div>
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Issuer:</span>
|
||||
<span class="attribute-value-compact">${this.escapeHtml(cert.issuer_name || 'Unknown')}</span>
|
||||
</div>
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Valid From:</span>
|
||||
<span class="attribute-value-compact">${cert.not_before || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Valid Until:</span>
|
||||
<span class="attribute-value-compact">${cert.not_after || 'Unknown'}</span>
|
||||
</div>
|
||||
${cert.validity_period_days ? `
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">Period:</span>
|
||||
<span class="attribute-value-compact">${cert.validity_period_days} days</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
if (remaining > 0) {
|
||||
html += `
|
||||
<div style="text-align: center; padding: 1rem; color: #ff9900; background: rgba(255, 153, 0, 0.1); border: 1px solid #ff9900; border-radius: 4px;">
|
||||
📋 ${remaining} additional certificate${remaining > 1 ? 's' : ''} not shown.<br>
|
||||
<small style="color: #999;">Use the export function to see all certificates.</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div></details>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate certificate summary using minimal new CSS
|
||||
*/
|
||||
generateCertificateSummary(certificates) {
|
||||
const total = certificates.total_certificates || 0;
|
||||
const valid = certificates.valid_certificates || 0;
|
||||
const expired = certificates.expired_certificates || 0;
|
||||
const expiringSoon = certificates.expires_soon_count || 0;
|
||||
const issuers = certificates.unique_issuers || [];
|
||||
|
||||
let html = `
|
||||
<div class="cert-summary-grid">
|
||||
<div class="cert-stat-item">
|
||||
<div class="cert-stat-value">${total}</div>
|
||||
<div class="cert-stat-label">Total</div>
|
||||
</div>
|
||||
<div class="cert-stat-item">
|
||||
<div class="cert-stat-value" style="color: #00ff41">${valid}</div>
|
||||
<div class="cert-stat-label">Valid</div>
|
||||
</div>
|
||||
<div class="cert-stat-item">
|
||||
<div class="cert-stat-value" style="color: #ff6b6b">${expired}</div>
|
||||
<div class="cert-stat-label">Expired</div>
|
||||
</div>
|
||||
<div class="cert-stat-item">
|
||||
<div class="cert-stat-value" style="color: #ff9900">${expiringSoon}</div>
|
||||
<div class="cert-stat-label">Expiring Soon</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Certificate authorities using existing array display
|
||||
if (issuers.length > 0) {
|
||||
html += `
|
||||
<div class="attribute-item-compact" style="margin-bottom: 1rem;">
|
||||
<span class="attribute-key-compact">Certificate Authorities:</span>
|
||||
<span class="attribute-value-compact">
|
||||
<div class="array-display">
|
||||
`;
|
||||
|
||||
issuers.forEach(issuer => {
|
||||
html += `<div class="array-display-item">${this.escapeHtml(issuer)}</div>`;
|
||||
});
|
||||
|
||||
html += '</div></span></div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATED: Generate large entity details using unified data model
|
||||
*/
|
||||
@ -1356,71 +1137,32 @@ class DNSReconApp {
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATED: Generate attributes section for unified data model
|
||||
* Now processes StandardAttribute objects instead of key-value pairs
|
||||
* UPDATED: Generate attributes section for the new flat data model
|
||||
*/
|
||||
generateAttributesSection(attributes) {
|
||||
if (!Array.isArray(attributes) || attributes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const categorized = this.categorizeStandardAttributes(attributes);
|
||||
let html = '';
|
||||
|
||||
Object.entries(categorized).forEach(([category, attrs]) => {
|
||||
if (attrs.length === 0) return;
|
||||
|
||||
html += `
|
||||
<div class="modal-section">
|
||||
<details>
|
||||
<summary>📊 ${category}</summary>
|
||||
<div class="modal-section-content">
|
||||
`;
|
||||
|
||||
html += '<div class="attribute-list">';
|
||||
attrs.forEach(attr => {
|
||||
html += `
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">${this.formatLabel(attr.name)}</span>
|
||||
<span class="attribute-value-compact">${this.formatStandardAttributeValue(attr)}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
html += '</div></details></div>';
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATED: Categorize StandardAttribute objects by type and content
|
||||
*/
|
||||
categorizeStandardAttributes(attributes) {
|
||||
const categories = {
|
||||
'DNS Records': [],
|
||||
'Network Info': [],
|
||||
'Provider Data': [],
|
||||
'Other': []
|
||||
};
|
||||
let html = `
|
||||
<div class="modal-section">
|
||||
<details open>
|
||||
<summary>📊 Attributes (${attributes.length})</summary>
|
||||
<div class="modal-section-content">
|
||||
<div class="attribute-list">
|
||||
`;
|
||||
|
||||
attributes.forEach(attr => {
|
||||
const lowerName = attr.name.toLowerCase();
|
||||
const attrType = attr.type ? attr.type.toLowerCase() : '';
|
||||
|
||||
if (lowerName.includes('dns') || lowerName.includes('record') || attrType.includes('dns')) {
|
||||
categories['DNS Records'].push(attr);
|
||||
} else if (lowerName.includes('ip') || lowerName.includes('asn') || lowerName.includes('network') || attrType.includes('network')) {
|
||||
categories['Network Info'].push(attr);
|
||||
} else if (lowerName.includes('shodan') || lowerName.includes('crtsh') || lowerName.includes('provider') || attrType.includes('provider')) {
|
||||
categories['Provider Data'].push(attr);
|
||||
} else {
|
||||
categories['Other'].push(attr);
|
||||
}
|
||||
html += `
|
||||
<div class="attribute-item-compact">
|
||||
<span class="attribute-key-compact">${this.formatLabel(attr.name)}</span>
|
||||
<span class="attribute-value-compact">${this.formatStandardAttributeValue(attr)}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
return categories;
|
||||
html += '</div></div></details></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user