upgrades
This commit is contained in:
parent
ce0e11cf0b
commit
a0caedcb1f
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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}")
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user