This commit is contained in:
overcuriousity 2025-09-10 16:26:44 +02:00
parent ce0e11cf0b
commit a0caedcb1f
4 changed files with 122 additions and 44 deletions

View File

@ -102,8 +102,10 @@ class GraphManager:
#with self.lock: #with self.lock:
# Ensure both nodes exist # Ensure both nodes exist
if not self.graph.has_node(source_id) or not self.graph.has_node(target_id): if not self.graph.has_node(source_id) or not self.graph.has_node(target_id):
return False # If the target node is a subdomain, it should be added.
# The scanner will handle this logic.
pass
# Check if edge already exists # Check if edge already exists
if self.graph.has_edge(source_id, target_id): if self.graph.has_edge(source_id, target_id):
# Update confidence score if new score is higher # Update confidence score if new score is higher
@ -241,6 +243,11 @@ class GraphManager:
node_color_config = type_colors.get(attributes.get('type', 'unknown'), type_colors['domain']) node_color_config = type_colors.get(attributes.get('type', 'unknown'), type_colors['domain'])
node_data['color'] = node_color_config node_data['color'] = node_color_config
# Pass the has_valid_cert metadata for styling
if 'metadata' in attributes and 'has_valid_cert' in attributes['metadata']:
node_data['has_valid_cert'] = attributes['metadata']['has_valid_cert']
nodes.append(node_data) nodes.append(node_data)
# Format edges for visualization # Format edges for visualization

View File

@ -626,6 +626,57 @@ class Scanner:
for provider in self.providers: for provider in self.providers:
stats[provider.get_name()] = provider.get_statistics() stats[provider.get_name()] = provider.get_statistics()
return stats return stats
def _is_valid_domain(self, domain: str) -> bool:
"""
Basic domain validation.
Args:
domain: Domain string to validate
Returns:
True if domain appears valid
"""
if not domain or len(domain) > 253:
return False
# Check for valid characters and structure
parts = domain.split('.')
if len(parts) < 2:
return False
for part in parts:
if not part or len(part) > 63:
return False
if not part.replace('-', '').replace('_', '').isalnum():
return False
return True
def _is_valid_ip(self, ip: str) -> bool:
"""
Basic IP address validation.
Args:
ip: IP address string to validate
Returns:
True if IP appears valid
"""
try:
parts = ip.split('.')
if len(parts) != 4:
return False
for part in parts:
num = int(part)
if not 0 <= num <= 255:
return False
return True
except (ValueError, AttributeError):
return False
class ScannerProxy: class ScannerProxy:

View File

@ -7,6 +7,7 @@ import json
import re import re
from typing import List, Dict, Any, Tuple, Set from typing import List, Dict, Any, Tuple, Set
from urllib.parse import quote from urllib.parse import quote
from datetime import datetime, timezone
from .base_provider import BaseProvider from .base_provider import BaseProvider
from core.graph_manager import RelationshipType from core.graph_manager import RelationshipType
@ -39,6 +40,20 @@ class CrtShProvider(BaseProvider):
""" """
return True return True
def _is_cert_valid(self, cert_data: Dict[str, Any]) -> bool:
"""Check if a certificate is currently valid."""
try:
not_after_str = cert_data.get('not_after')
if not_after_str:
# Append 'Z' to indicate UTC if it's not present
if not not_after_str.endswith('Z'):
not_after_str += 'Z'
not_after_date = datetime.fromisoformat(not_after_str.replace('Z', '+00:00'))
return not_after_date > datetime.now(timezone.utc)
except Exception:
return False
return False
def query_domain(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]: def query_domain(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
""" """
Query crt.sh for certificates containing the domain. Query crt.sh for certificates containing the domain.
@ -68,50 +83,47 @@ class CrtShProvider(BaseProvider):
return [] return []
# Process certificates to extract relationships # Process certificates to extract relationships
seen_certificates = set() discovered_subdomains = {}
for cert_data in certificates: for cert_data in certificates:
cert_id = cert_data.get('id')
if not cert_id or cert_id in seen_certificates:
continue
seen_certificates.add(cert_id)
# Extract domains from certificate
cert_domains = self._extract_domains_from_certificate(cert_data) cert_domains = self._extract_domains_from_certificate(cert_data)
is_valid = self._is_cert_valid(cert_data)
if domain in cert_domains and len(cert_domains) > 1:
# Create relationships between domains found in the same certificate for subdomain in cert_domains:
for related_domain in cert_domains: if self._is_valid_domain(subdomain) and subdomain != domain:
if related_domain != domain and self._is_valid_domain(related_domain): if subdomain not in discovered_subdomains:
# Create SAN relationship discovered_subdomains[subdomain] = {'has_valid_cert': False, 'issuers': set()}
raw_data = {
'certificate_id': cert_id, if is_valid:
'issuer': cert_data.get('issuer_name', ''), discovered_subdomains[subdomain]['has_valid_cert'] = True
'not_before': cert_data.get('not_before', ''),
'not_after': cert_data.get('not_after', ''), issuer = cert_data.get('issuer_name')
'serial_number': cert_data.get('serial_number', ''), if issuer:
'all_domains': list(cert_domains) discovered_subdomains[subdomain]['issuers'].add(issuer)
}
# Create relationships from the discovered subdomains
relationships.append(( for subdomain, data in discovered_subdomains.items():
domain, raw_data = {
related_domain, 'has_valid_cert': data['has_valid_cert'],
RelationshipType.SAN_CERTIFICATE, 'issuers': list(data['issuers']),
RelationshipType.SAN_CERTIFICATE.default_confidence, 'source': 'crt.sh'
raw_data }
)) relationships.append((
domain,
# Log the discovery subdomain,
self.log_relationship_discovery( RelationshipType.SAN_CERTIFICATE,
source_node=domain, RelationshipType.SAN_CERTIFICATE.default_confidence,
target_node=related_domain, raw_data
relationship_type=RelationshipType.SAN_CERTIFICATE, ))
confidence_score=RelationshipType.SAN_CERTIFICATE.default_confidence, self.log_relationship_discovery(
raw_data=raw_data, source_node=domain,
discovery_method="certificate_san_analysis" target_node=subdomain,
) relationship_type=RelationshipType.SAN_CERTIFICATE,
confidence_score=RelationshipType.SAN_CERTIFICATE.default_confidence,
raw_data=raw_data,
discovery_method="certificate_san_analysis"
)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
self.logger.logger.error(f"Failed to parse JSON response from crt.sh: {e}") self.logger.logger.error(f"Failed to parse JSON response from crt.sh: {e}")
except Exception as e: except Exception as e:

View File

@ -366,6 +366,14 @@ class GraphManager {
}; };
} }
// Style based on certificate validity
if (node.has_valid_cert === true) {
processedNode.borderColor = '#00ff41'; // Green for valid cert
} else if (node.has_valid_cert === false) {
processedNode.borderColor = '#ff9900'; // Amber for expired/no cert
processedNode.borderDashes = [5, 5];
}
return processedNode; return processedNode;
} }