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,7 +102,9 @@ 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):
@ -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

@ -627,6 +627,57 @@ class Scanner:
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:
def __init__(self): def __init__(self):

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,49 +83,46 @@ 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: for subdomain in cert_domains:
# Create relationships between domains found in the same certificate if self._is_valid_domain(subdomain) and subdomain != domain:
for related_domain in cert_domains: if subdomain not in discovered_subdomains:
if related_domain != domain and self._is_valid_domain(related_domain): discovered_subdomains[subdomain] = {'has_valid_cert': False, 'issuers': set()}
# Create SAN relationship
raw_data = {
'certificate_id': cert_id,
'issuer': cert_data.get('issuer_name', ''),
'not_before': cert_data.get('not_before', ''),
'not_after': cert_data.get('not_after', ''),
'serial_number': cert_data.get('serial_number', ''),
'all_domains': list(cert_domains)
}
relationships.append(( if is_valid:
domain, discovered_subdomains[subdomain]['has_valid_cert'] = True
related_domain,
RelationshipType.SAN_CERTIFICATE,
RelationshipType.SAN_CERTIFICATE.default_confidence,
raw_data
))
# Log the discovery issuer = cert_data.get('issuer_name')
self.log_relationship_discovery( if issuer:
source_node=domain, discovered_subdomains[subdomain]['issuers'].add(issuer)
target_node=related_domain,
relationship_type=RelationshipType.SAN_CERTIFICATE, # Create relationships from the discovered subdomains
confidence_score=RelationshipType.SAN_CERTIFICATE.default_confidence, for subdomain, data in discovered_subdomains.items():
raw_data=raw_data, raw_data = {
discovery_method="certificate_san_analysis" 'has_valid_cert': data['has_valid_cert'],
) 'issuers': list(data['issuers']),
'source': 'crt.sh'
}
relationships.append((
domain,
subdomain,
RelationshipType.SAN_CERTIFICATE,
RelationshipType.SAN_CERTIFICATE.default_confidence,
raw_data
))
self.log_relationship_discovery(
source_node=domain,
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}")

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;
} }