gradient
This commit is contained in:
		
							parent
							
								
									571912218e
								
							
						
					
					
						commit
						897bb80183
					
				@ -22,7 +22,6 @@ For power users who require more in-depth information, DNScope can be configured
 | 
				
			|||||||
  * **In-Memory Graph Analysis**: Uses NetworkX for efficient relationship mapping.
 | 
					  * **In-Memory Graph Analysis**: Uses NetworkX for efficient relationship mapping.
 | 
				
			||||||
  * **Real-Time Visualization**: The graph updates dynamically as the scan progresses.
 | 
					  * **Real-Time Visualization**: The graph updates dynamically as the scan progresses.
 | 
				
			||||||
  * **Forensic Logging**: A complete audit trail of all reconnaissance activities is maintained.
 | 
					  * **Forensic Logging**: A complete audit trail of all reconnaissance activities is maintained.
 | 
				
			||||||
  * **Confidence Scoring**: Relationships are weighted based on the reliability of the data source.
 | 
					 | 
				
			||||||
  * **Session Management**: Supports concurrent user sessions with isolated scanner instances.
 | 
					  * **Session Management**: Supports concurrent user sessions with isolated scanner instances.
 | 
				
			||||||
  * **Extensible Provider Architecture**: Easily add new data sources to expand the tool's capabilities.
 | 
					  * **Extensible Provider Architecture**: Easily add new data sources to expand the tool's capabilities.
 | 
				
			||||||
  * **Web-Based UI**: An intuitive and interactive web interface for managing scans and visualizing results.
 | 
					  * **Web-Based UI**: An intuitive and interactive web interface for managing scans and visualizing results.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								app.py
									
									
									
									
									
								
							@ -332,7 +332,6 @@ def revert_graph_action():
 | 
				
			|||||||
                    scanner.graph.add_edge(
 | 
					                    scanner.graph.add_edge(
 | 
				
			||||||
                        source_id=edge['from'], target_id=edge['to'],
 | 
					                        source_id=edge['from'], target_id=edge['to'],
 | 
				
			||||||
                        relationship_type=edge['metadata']['relationship_type'],
 | 
					                        relationship_type=edge['metadata']['relationship_type'],
 | 
				
			||||||
                        confidence_score=edge['metadata']['confidence_score'],
 | 
					 | 
				
			||||||
                        source_provider=edge['metadata']['source_provider'],
 | 
					                        source_provider=edge['metadata']['source_provider'],
 | 
				
			||||||
                        raw_data=edge.get('raw_data', {})
 | 
					                        raw_data=edge.get('raw_data', {})
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Graph data model for DNScope using NetworkX.
 | 
					Graph data model for DNScope using NetworkX.
 | 
				
			||||||
Manages in-memory graph storage with confidence scoring and forensic metadata.
 | 
					Manages in-memory graph storage with forensic metadata.
 | 
				
			||||||
Now fully compatible with the unified ProviderResult data model.
 | 
					Now fully compatible with the unified ProviderResult data model.
 | 
				
			||||||
UPDATED: Fixed correlation exclusion keys to match actual attribute names.
 | 
					UPDATED: Fixed correlation exclusion keys to match actual attribute names.
 | 
				
			||||||
UPDATED: Removed export_json() method - now handled by ExportManager.
 | 
					UPDATED: Removed export_json() method - now handled by ExportManager.
 | 
				
			||||||
@ -31,7 +31,7 @@ class NodeType(Enum):
 | 
				
			|||||||
class GraphManager:
 | 
					class GraphManager:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Thread-safe graph manager for DNScope infrastructure mapping.
 | 
					    Thread-safe graph manager for DNScope infrastructure mapping.
 | 
				
			||||||
    Uses NetworkX for in-memory graph storage with confidence scoring.
 | 
					    Uses NetworkX for in-memory graph storage.
 | 
				
			||||||
    Compatible with unified ProviderResult data model.
 | 
					    Compatible with unified ProviderResult data model.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -83,7 +83,7 @@ class GraphManager:
 | 
				
			|||||||
        return is_new_node
 | 
					        return is_new_node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_edge(self, source_id: str, target_id: str, relationship_type: str,
 | 
					    def add_edge(self, source_id: str, target_id: str, relationship_type: str,
 | 
				
			||||||
                confidence_score: float = 0.5, source_provider: str = "unknown",
 | 
					                source_provider: str = "unknown",
 | 
				
			||||||
                raw_data: Optional[Dict[str, Any]] = None) -> bool:
 | 
					                raw_data: Optional[Dict[str, Any]] = None) -> bool:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        UPDATED: Add or update an edge between two nodes with raw relationship labels.
 | 
					        UPDATED: Add or update an edge between two nodes with raw relationship labels.
 | 
				
			||||||
@ -91,23 +91,13 @@ class GraphManager:
 | 
				
			|||||||
        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
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new_confidence = confidence_score
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # UPDATED: Use raw relationship type - no formatting
 | 
					        # UPDATED: Use raw relationship type - no formatting
 | 
				
			||||||
        edge_label = relationship_type
 | 
					        edge_label = relationship_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.graph.has_edge(source_id, target_id):
 | 
					 | 
				
			||||||
            # 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]['updated_timestamp'] = datetime.now(timezone.utc).isoformat()
 | 
					 | 
				
			||||||
                self.graph.edges[source_id, target_id]['updated_by'] = source_provider
 | 
					 | 
				
			||||||
            return False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Add a new edge with raw attributes
 | 
					        # Add a new edge with raw attributes
 | 
				
			||||||
        self.graph.add_edge(source_id, target_id,
 | 
					        self.graph.add_edge(source_id, target_id,
 | 
				
			||||||
                            relationship_type=edge_label,
 | 
					                            relationship_type=edge_label,
 | 
				
			||||||
                            confidence_score=new_confidence,
 | 
					 | 
				
			||||||
                            source_provider=source_provider,
 | 
					                            source_provider=source_provider,
 | 
				
			||||||
                            discovery_timestamp=datetime.now(timezone.utc).isoformat(),
 | 
					                            discovery_timestamp=datetime.now(timezone.utc).isoformat(),
 | 
				
			||||||
                            raw_data=raw_data or {})
 | 
					                            raw_data=raw_data or {})
 | 
				
			||||||
@ -137,11 +127,6 @@ class GraphManager:
 | 
				
			|||||||
        """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]
 | 
					        return [n for n, d in self.graph.nodes(data=True) if d.get('type') == node_type.value]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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."""
 | 
					 | 
				
			||||||
        return [(u, v, d) for u, v, d in self.graph.edges(data=True)
 | 
					 | 
				
			||||||
                if d.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 formatted for frontend visualization.
 | 
				
			||||||
@ -177,7 +162,6 @@ class GraphManager:
 | 
				
			|||||||
                'from': source, 
 | 
					                'from': source, 
 | 
				
			||||||
                'to': target,
 | 
					                'to': target,
 | 
				
			||||||
                'label': attrs.get('relationship_type', ''),
 | 
					                'label': attrs.get('relationship_type', ''),
 | 
				
			||||||
                'confidence_score': attrs.get('confidence_score', 0),
 | 
					 | 
				
			||||||
                'source_provider': attrs.get('source_provider', ''),
 | 
					                'source_provider': attrs.get('source_provider', ''),
 | 
				
			||||||
                'discovery_timestamp': attrs.get('discovery_timestamp')
 | 
					                'discovery_timestamp': attrs.get('discovery_timestamp')
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
@ -188,24 +172,6 @@ class GraphManager:
 | 
				
			|||||||
            'statistics': self.get_statistics()['basic_metrics']
 | 
					            'statistics': self.get_statistics()['basic_metrics']
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _get_confidence_distribution(self) -> Dict[str, int]:
 | 
					 | 
				
			||||||
        """Get distribution of edge confidence scores with empty graph handling."""
 | 
					 | 
				
			||||||
        distribution = {'high': 0, 'medium': 0, 'low': 0}
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        # FIXED: Handle empty graph case
 | 
					 | 
				
			||||||
        if self.get_edge_count() == 0:
 | 
					 | 
				
			||||||
            return distribution
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
        for _, _, data in self.graph.edges(data=True):
 | 
					 | 
				
			||||||
            confidence = data.get('confidence_score', 0)
 | 
					 | 
				
			||||||
            if confidence >= 0.8:
 | 
					 | 
				
			||||||
                distribution['high'] += 1
 | 
					 | 
				
			||||||
            elif confidence >= 0.6:
 | 
					 | 
				
			||||||
                distribution['medium'] += 1
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                distribution['low'] += 1
 | 
					 | 
				
			||||||
        return distribution
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_statistics(self) -> Dict[str, Any]:
 | 
					    def get_statistics(self) -> Dict[str, Any]:
 | 
				
			||||||
        """Get comprehensive statistics about the graph with proper empty graph handling."""
 | 
					        """Get comprehensive statistics about the graph with proper empty graph handling."""
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@ -222,7 +188,6 @@ class GraphManager:
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            'node_type_distribution': {}, 
 | 
					            'node_type_distribution': {}, 
 | 
				
			||||||
            'relationship_type_distribution': {},
 | 
					            'relationship_type_distribution': {},
 | 
				
			||||||
            'confidence_distribution': self._get_confidence_distribution(),
 | 
					 | 
				
			||||||
            'provider_distribution': {}
 | 
					            'provider_distribution': {}
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,6 @@ class RelationshipDiscovery:
 | 
				
			|||||||
    source_node: str
 | 
					    source_node: str
 | 
				
			||||||
    target_node: str
 | 
					    target_node: str
 | 
				
			||||||
    relationship_type: str
 | 
					    relationship_type: str
 | 
				
			||||||
    confidence_score: float
 | 
					 | 
				
			||||||
    provider: str
 | 
					    provider: str
 | 
				
			||||||
    raw_data: Dict[str, Any]
 | 
					    raw_data: Dict[str, Any]
 | 
				
			||||||
    discovery_method: str
 | 
					    discovery_method: str
 | 
				
			||||||
@ -157,7 +156,7 @@ class ForensicLogger:
 | 
				
			|||||||
            self.logger.info(f"API Request - {provider}: {url} - Status: {status_code}")
 | 
					            self.logger.info(f"API Request - {provider}: {url} - Status: {status_code}")
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def log_relationship_discovery(self, source_node: str, target_node: str,
 | 
					    def log_relationship_discovery(self, source_node: str, target_node: str,
 | 
				
			||||||
                                 relationship_type: str, confidence_score: float,
 | 
					                                 relationship_type: str,
 | 
				
			||||||
                                 provider: str, raw_data: Dict[str, Any],
 | 
					                                 provider: str, raw_data: Dict[str, Any],
 | 
				
			||||||
                                 discovery_method: str) -> None:
 | 
					                                 discovery_method: str) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -167,7 +166,6 @@ class ForensicLogger:
 | 
				
			|||||||
            source_node: Source node identifier
 | 
					            source_node: Source node identifier
 | 
				
			||||||
            target_node: Target node identifier
 | 
					            target_node: Target node identifier
 | 
				
			||||||
            relationship_type: Type of relationship (e.g., 'SAN', 'A_Record')
 | 
					            relationship_type: Type of relationship (e.g., 'SAN', 'A_Record')
 | 
				
			||||||
            confidence_score: Confidence score (0.0 to 1.0)
 | 
					 | 
				
			||||||
            provider: Provider that discovered this relationship
 | 
					            provider: Provider that discovered this relationship
 | 
				
			||||||
            raw_data: Raw data from provider response
 | 
					            raw_data: Raw data from provider response
 | 
				
			||||||
            discovery_method: Method used to discover relationship
 | 
					            discovery_method: Method used to discover relationship
 | 
				
			||||||
@ -177,7 +175,6 @@ class ForensicLogger:
 | 
				
			|||||||
            source_node=source_node,
 | 
					            source_node=source_node,
 | 
				
			||||||
            target_node=target_node,
 | 
					            target_node=target_node,
 | 
				
			||||||
            relationship_type=relationship_type,
 | 
					            relationship_type=relationship_type,
 | 
				
			||||||
            confidence_score=confidence_score,
 | 
					 | 
				
			||||||
            provider=provider,
 | 
					            provider=provider,
 | 
				
			||||||
            raw_data=raw_data,
 | 
					            raw_data=raw_data,
 | 
				
			||||||
            discovery_method=discovery_method
 | 
					            discovery_method=discovery_method
 | 
				
			||||||
@ -188,7 +185,7 @@ class ForensicLogger:
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
        self.logger.info(
 | 
					        self.logger.info(
 | 
				
			||||||
            f"Relationship Discovered - {source_node} -> {target_node} "
 | 
					            f"Relationship Discovered - {source_node} -> {target_node} "
 | 
				
			||||||
            f"({relationship_type}) - Confidence: {confidence_score:.2f} - Provider: {provider}"
 | 
					            f"({relationship_type}) - Provider: {provider}"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def log_scan_start(self, target_domain: str, recursion_depth: int, 
 | 
					    def log_scan_start(self, target_domain: str, recursion_depth: int, 
 | 
				
			||||||
@ -238,7 +235,6 @@ class ForensicLogger:
 | 
				
			|||||||
                'successful_requests': len([req for req in provider_requests if req.error is None]),
 | 
					                'successful_requests': len([req for req in provider_requests if req.error is None]),
 | 
				
			||||||
                'failed_requests': len([req for req in provider_requests if req.error is not None]),
 | 
					                'failed_requests': len([req for req in provider_requests if req.error is not None]),
 | 
				
			||||||
                'relationships_discovered': len(provider_relationships),
 | 
					                'relationships_discovered': len(provider_relationships),
 | 
				
			||||||
                'avg_confidence': sum(rel.confidence_score for rel in provider_relationships) / len(provider_relationships) if provider_relationships else 0
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
 | 
				
			|||||||
@ -18,33 +18,19 @@ class StandardAttribute:
 | 
				
			|||||||
    value: Any
 | 
					    value: Any
 | 
				
			||||||
    type: str
 | 
					    type: str
 | 
				
			||||||
    provider: str
 | 
					    provider: str
 | 
				
			||||||
    confidence: float
 | 
					 | 
				
			||||||
    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
 | 
					    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
 | 
				
			||||||
    metadata: Optional[Dict[str, Any]] = field(default_factory=dict)
 | 
					    metadata: Optional[Dict[str, Any]] = field(default_factory=dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __post_init__(self):
 | 
					 | 
				
			||||||
        """Validate the attribute after initialization."""
 | 
					 | 
				
			||||||
        if not isinstance(self.confidence, (int, float)) or not 0.0 <= self.confidence <= 1.0:
 | 
					 | 
				
			||||||
            raise ValueError(f"Confidence must be between 0.0 and 1.0, got {self.confidence}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
class Relationship:
 | 
					class Relationship:
 | 
				
			||||||
    """A unified data structure for a directional link between two nodes."""
 | 
					    """A unified data structure for a directional link between two nodes."""
 | 
				
			||||||
    source_node: str
 | 
					    source_node: str
 | 
				
			||||||
    target_node: str
 | 
					    target_node: str
 | 
				
			||||||
    relationship_type: str
 | 
					    relationship_type: str
 | 
				
			||||||
    confidence: float
 | 
					 | 
				
			||||||
    provider: str
 | 
					    provider: str
 | 
				
			||||||
    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
 | 
					    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
 | 
				
			||||||
    raw_data: Optional[Dict[str, Any]] = field(default_factory=dict)
 | 
					    raw_data: Optional[Dict[str, Any]] = field(default_factory=dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __post_init__(self):
 | 
					 | 
				
			||||||
        """Validate the relationship after initialization."""
 | 
					 | 
				
			||||||
        if not isinstance(self.confidence, (int, float)) or not 0.0 <= self.confidence <= 1.0:
 | 
					 | 
				
			||||||
            raise ValueError(f"Confidence must be between 0.0 and 1.0, got {self.confidence}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
class ProviderResult:
 | 
					class ProviderResult:
 | 
				
			||||||
    """A container for all data returned by a provider from a single query."""
 | 
					    """A container for all data returned by a provider from a single query."""
 | 
				
			||||||
@ -52,8 +38,7 @@ class ProviderResult:
 | 
				
			|||||||
    relationships: List[Relationship] = field(default_factory=list)
 | 
					    relationships: List[Relationship] = field(default_factory=list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_attribute(self, target_node: str, name: str, value: Any, attr_type: str, 
 | 
					    def add_attribute(self, target_node: str, name: str, value: Any, attr_type: str, 
 | 
				
			||||||
                     provider: str, confidence: float = 0.8, 
 | 
					                     provider: str, metadata: Optional[Dict[str, Any]] = None) -> None:
 | 
				
			||||||
                     metadata: Optional[Dict[str, Any]] = None) -> None:
 | 
					 | 
				
			||||||
        """Helper method to add an attribute to the result."""
 | 
					        """Helper method to add an attribute to the result."""
 | 
				
			||||||
        self.attributes.append(StandardAttribute(
 | 
					        self.attributes.append(StandardAttribute(
 | 
				
			||||||
            target_node=target_node,
 | 
					            target_node=target_node,
 | 
				
			||||||
@ -61,19 +46,16 @@ class ProviderResult:
 | 
				
			|||||||
            value=value,
 | 
					            value=value,
 | 
				
			||||||
            type=attr_type,
 | 
					            type=attr_type,
 | 
				
			||||||
            provider=provider,
 | 
					            provider=provider,
 | 
				
			||||||
            confidence=confidence,
 | 
					 | 
				
			||||||
            metadata=metadata or {}
 | 
					            metadata=metadata or {}
 | 
				
			||||||
        ))
 | 
					        ))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_relationship(self, source_node: str, target_node: str, relationship_type: str,
 | 
					    def add_relationship(self, source_node: str, target_node: str, relationship_type: str,
 | 
				
			||||||
                        provider: str, confidence: float = 0.8, 
 | 
					                        provider: str, raw_data: Optional[Dict[str, Any]] = None) -> None:
 | 
				
			||||||
                        raw_data: Optional[Dict[str, Any]] = None) -> None:
 | 
					 | 
				
			||||||
        """Helper method to add a relationship to the result."""
 | 
					        """Helper method to add a relationship to the result."""
 | 
				
			||||||
        self.relationships.append(Relationship(
 | 
					        self.relationships.append(Relationship(
 | 
				
			||||||
            source_node=source_node,
 | 
					            source_node=source_node,
 | 
				
			||||||
            target_node=target_node,
 | 
					            target_node=target_node,
 | 
				
			||||||
            relationship_type=relationship_type,
 | 
					            relationship_type=relationship_type,
 | 
				
			||||||
            confidence=confidence,
 | 
					 | 
				
			||||||
            provider=provider,
 | 
					            provider=provider,
 | 
				
			||||||
            raw_data=raw_data or {}
 | 
					            raw_data=raw_data or {}
 | 
				
			||||||
        ))
 | 
					        ))
 | 
				
			||||||
 | 
				
			|||||||
@ -847,7 +847,6 @@ class Scanner:
 | 
				
			|||||||
                    'source_node': rel.source_node,
 | 
					                    'source_node': rel.source_node,
 | 
				
			||||||
                    'target_node': rel.target_node,
 | 
					                    'target_node': rel.target_node,
 | 
				
			||||||
                    'relationship_type': rel.relationship_type,
 | 
					                    'relationship_type': rel.relationship_type,
 | 
				
			||||||
                    'confidence': rel.confidence,
 | 
					 | 
				
			||||||
                    'provider': rel.provider,
 | 
					                    'provider': rel.provider,
 | 
				
			||||||
                    'raw_data': rel.raw_data
 | 
					                    'raw_data': rel.raw_data
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
@ -905,7 +904,6 @@ class Scanner:
 | 
				
			|||||||
                        source_id=rel_data['source_node'],
 | 
					                        source_id=rel_data['source_node'],
 | 
				
			||||||
                        target_id=rel_data['target_node'],
 | 
					                        target_id=rel_data['target_node'],
 | 
				
			||||||
                        relationship_type=rel_data['relationship_type'],
 | 
					                        relationship_type=rel_data['relationship_type'],
 | 
				
			||||||
                        confidence_score=rel_data['confidence'],
 | 
					 | 
				
			||||||
                        source_provider=rel_data['provider'],
 | 
					                        source_provider=rel_data['provider'],
 | 
				
			||||||
                        raw_data=rel_data['raw_data']
 | 
					                        raw_data=rel_data['raw_data']
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
@ -1012,7 +1010,6 @@ class Scanner:
 | 
				
			|||||||
            self.graph.add_edge(
 | 
					            self.graph.add_edge(
 | 
				
			||||||
                visual_source, visual_target,
 | 
					                visual_source, visual_target,
 | 
				
			||||||
                relationship.relationship_type,
 | 
					                relationship.relationship_type,
 | 
				
			||||||
                relationship.confidence,
 | 
					 | 
				
			||||||
                provider_name,
 | 
					                provider_name,
 | 
				
			||||||
                relationship.raw_data
 | 
					                relationship.raw_data
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
@ -1035,7 +1032,7 @@ class Scanner:
 | 
				
			|||||||
        for attribute in provider_result.attributes:
 | 
					        for attribute in provider_result.attributes:
 | 
				
			||||||
            attr_dict = {
 | 
					            attr_dict = {
 | 
				
			||||||
                "name": attribute.name, "value": attribute.value, "type": attribute.type,
 | 
					                "name": attribute.name, "value": attribute.value, "type": attribute.type,
 | 
				
			||||||
                "provider": attribute.provider, "confidence": attribute.confidence, "metadata": attribute.metadata
 | 
					                "provider": attribute.provider, "metadata": attribute.metadata
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            attributes_by_node[attribute.target_node].append(attr_dict)
 | 
					            attributes_by_node[attribute.target_node].append(attr_dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -229,7 +229,6 @@ class BaseProvider(ABC):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def log_relationship_discovery(self, source_node: str, target_node: str,
 | 
					    def log_relationship_discovery(self, source_node: str, target_node: str,
 | 
				
			||||||
                                 relationship_type: str,
 | 
					                                 relationship_type: str,
 | 
				
			||||||
                                 confidence_score: float,
 | 
					 | 
				
			||||||
                                 raw_data: Dict[str, Any],
 | 
					                                 raw_data: Dict[str, Any],
 | 
				
			||||||
                                 discovery_method: str) -> None:
 | 
					                                 discovery_method: str) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -239,7 +238,6 @@ class BaseProvider(ABC):
 | 
				
			|||||||
            source_node: Source node identifier
 | 
					            source_node: Source node identifier
 | 
				
			||||||
            target_node: Target node identifier
 | 
					            target_node: Target node identifier
 | 
				
			||||||
            relationship_type: Type of relationship
 | 
					            relationship_type: Type of relationship
 | 
				
			||||||
            confidence_score: Confidence score
 | 
					 | 
				
			||||||
            raw_data: Raw data from provider
 | 
					            raw_data: Raw data from provider
 | 
				
			||||||
            discovery_method: Method used for discovery
 | 
					            discovery_method: Method used for discovery
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -249,7 +247,6 @@ class BaseProvider(ABC):
 | 
				
			|||||||
            source_node=source_node,
 | 
					            source_node=source_node,
 | 
				
			||||||
            target_node=target_node,
 | 
					            target_node=target_node,
 | 
				
			||||||
            relationship_type=relationship_type,
 | 
					            relationship_type=relationship_type,
 | 
				
			||||||
            confidence_score=confidence_score,
 | 
					 | 
				
			||||||
            provider=self.name,
 | 
					            provider=self.name,
 | 
				
			||||||
            raw_data=raw_data,
 | 
					            raw_data=raw_data,
 | 
				
			||||||
            discovery_method=discovery_method
 | 
					            discovery_method=discovery_method
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
from typing import Dict, Any, List
 | 
					from typing import Dict, Any, List
 | 
				
			||||||
 | 
					from datetime import datetime, timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .base_provider import BaseProvider
 | 
					from .base_provider import BaseProvider
 | 
				
			||||||
from core.provider_result import ProviderResult
 | 
					from core.provider_result import ProviderResult
 | 
				
			||||||
@ -10,6 +11,7 @@ from core.graph_manager import NodeType, GraphManager
 | 
				
			|||||||
class CorrelationProvider(BaseProvider):
 | 
					class CorrelationProvider(BaseProvider):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    A provider that finds correlations between nodes in the graph.
 | 
					    A provider that finds correlations between nodes in the graph.
 | 
				
			||||||
 | 
					    UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, name: str = "correlation", session_config=None):
 | 
					    def __init__(self, name: str = "correlation", session_config=None):
 | 
				
			||||||
@ -61,12 +63,14 @@ class CorrelationProvider(BaseProvider):
 | 
				
			|||||||
    def query_domain(self, domain: str) -> ProviderResult:
 | 
					    def query_domain(self, domain: str) -> ProviderResult:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Query the provider for information about a domain.
 | 
					        Query the provider for information about a domain.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._find_correlations(domain)
 | 
					        return self._find_correlations(domain)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def query_ip(self, ip: str) -> ProviderResult:
 | 
					    def query_ip(self, ip: str) -> ProviderResult:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Query the provider for information about an IP address.
 | 
					        Query the provider for information about an IP address.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._find_correlations(ip)
 | 
					        return self._find_correlations(ip)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -79,8 +83,10 @@ class CorrelationProvider(BaseProvider):
 | 
				
			|||||||
    def _find_correlations(self, node_id: str) -> ProviderResult:
 | 
					    def _find_correlations(self, node_id: str) -> ProviderResult:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Find correlations for a given node with enhanced filtering and error handling.
 | 
					        Find correlations for a given node with enhanced filtering and error handling.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        result = ProviderResult()
 | 
					        result = ProviderResult()
 | 
				
			||||||
 | 
					        discovery_time = datetime.now(timezone.utc)
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Enhanced safety checks
 | 
					        # Enhanced safety checks
 | 
				
			||||||
        if not self.graph or not self.graph.graph.has_node(node_id):
 | 
					        if not self.graph or not self.graph.graph.has_node(node_id):
 | 
				
			||||||
@ -133,7 +139,7 @@ class CorrelationProvider(BaseProvider):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                # Create correlation if we have multiple nodes with this value
 | 
					                # Create correlation if we have multiple nodes with this value
 | 
				
			||||||
                if len(self.correlation_index[attr_value]['nodes']) > 1:
 | 
					                if len(self.correlation_index[attr_value]['nodes']) > 1:
 | 
				
			||||||
                    self._create_correlation_relationships(attr_value, self.correlation_index[attr_value], result)
 | 
					                    self._create_correlation_relationships(attr_value, self.correlation_index[attr_value], result, discovery_time)
 | 
				
			||||||
                    correlations_found += 1
 | 
					                    correlations_found += 1
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
            # Log correlation results
 | 
					            # Log correlation results
 | 
				
			||||||
@ -187,9 +193,11 @@ class CorrelationProvider(BaseProvider):
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def _create_correlation_relationships(self, value: Any, correlation_data: Dict[str, Any], result: ProviderResult):
 | 
					    def _create_correlation_relationships(self, value: Any, correlation_data: Dict[str, Any], 
 | 
				
			||||||
 | 
					                                        result: ProviderResult, discovery_time: datetime):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Create correlation relationships with enhanced deduplication and validation.
 | 
					        Create correlation relationships with enhanced deduplication and validation.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        correlation_node_id = f"corr_{hash(str(value)) & 0x7FFFFFFF}"
 | 
					        correlation_node_id = f"corr_{hash(str(value)) & 0x7FFFFFFF}"
 | 
				
			||||||
        nodes = correlation_data['nodes']
 | 
					        nodes = correlation_data['nodes']
 | 
				
			||||||
@ -216,7 +224,6 @@ class CorrelationProvider(BaseProvider):
 | 
				
			|||||||
            value=value,
 | 
					            value=value,
 | 
				
			||||||
            attr_type=str(type(value).__name__),
 | 
					            attr_type=str(type(value).__name__),
 | 
				
			||||||
            provider=self.name,
 | 
					            provider=self.name,
 | 
				
			||||||
            confidence=0.9,
 | 
					 | 
				
			||||||
            metadata={
 | 
					            metadata={
 | 
				
			||||||
                'correlated_nodes': list(nodes),
 | 
					                'correlated_nodes': list(nodes),
 | 
				
			||||||
                'sources': sources,
 | 
					                'sources': sources,
 | 
				
			||||||
@ -225,7 +232,7 @@ class CorrelationProvider(BaseProvider):
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create relationships with source validation
 | 
					        # Create relationships with source validation and enhanced timestamps
 | 
				
			||||||
        created_relationships = set()
 | 
					        created_relationships = set()
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        for source in sources:
 | 
					        for source in sources:
 | 
				
			||||||
@ -240,19 +247,23 @@ class CorrelationProvider(BaseProvider):
 | 
				
			|||||||
                
 | 
					                
 | 
				
			||||||
            relationship_label = f"corr_{provider}_{attribute}"
 | 
					            relationship_label = f"corr_{provider}_{attribute}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Enhanced raw_data with discovery timestamp for time-based edge coloring
 | 
				
			||||||
 | 
					            raw_data = {
 | 
				
			||||||
 | 
					                'correlation_value': value,
 | 
				
			||||||
 | 
					                'original_attribute': attribute,
 | 
				
			||||||
 | 
					                'correlation_type': 'attribute_matching',
 | 
				
			||||||
 | 
					                'correlation_size': len(nodes),
 | 
				
			||||||
 | 
					                'discovery_timestamp': discovery_time.isoformat(),
 | 
				
			||||||
 | 
					                'relevance_timestamp': discovery_time.isoformat()  # Correlation data is "fresh" when discovered
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Add the relationship to the result
 | 
					            # Add the relationship to the result
 | 
				
			||||||
            result.add_relationship(
 | 
					            result.add_relationship(
 | 
				
			||||||
                source_node=node_id,
 | 
					                source_node=node_id,
 | 
				
			||||||
                target_node=correlation_node_id,
 | 
					                target_node=correlation_node_id,
 | 
				
			||||||
                relationship_type=relationship_label,
 | 
					                relationship_type=relationship_label,
 | 
				
			||||||
                provider=self.name,
 | 
					                provider=self.name,
 | 
				
			||||||
                confidence=0.9,
 | 
					                raw_data=raw_data
 | 
				
			||||||
                raw_data={
 | 
					 | 
				
			||||||
                    'correlation_value': value,
 | 
					 | 
				
			||||||
                    'original_attribute': attribute,
 | 
					 | 
				
			||||||
                    'correlation_type': 'attribute_matching',
 | 
					 | 
				
			||||||
                    'correlation_size': len(nodes)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            created_relationships.add(relationship_key)
 | 
					            created_relationships.add(relationship_key)
 | 
				
			||||||
@ -18,6 +18,7 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
    Provider for querying crt.sh certificate transparency database.
 | 
					    Provider for querying crt.sh certificate transparency database.
 | 
				
			||||||
    FIXED: Improved caching logic and error handling to prevent infinite retry loops.
 | 
					    FIXED: Improved caching logic and error handling to prevent infinite retry loops.
 | 
				
			||||||
    Returns standardized ProviderResult objects with caching support.
 | 
					    Returns standardized ProviderResult objects with caching support.
 | 
				
			||||||
 | 
					    UPDATED: Enhanced with certificate timestamps for time-based edge coloring.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def __init__(self, name=None, session_config=None):
 | 
					    def __init__(self, name=None, session_config=None):
 | 
				
			||||||
@ -131,6 +132,7 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
    def query_domain(self, domain: str) -> ProviderResult:
 | 
					    def query_domain(self, domain: str) -> ProviderResult:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        FIXED: Simplified and more robust domain querying with better error handling.
 | 
					        FIXED: Simplified and more robust domain querying with better error handling.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with certificate timestamps for time-based edge coloring.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not _is_valid_domain(domain):
 | 
					        if not _is_valid_domain(domain):
 | 
				
			||||||
            return ProviderResult()
 | 
					            return ProviderResult()
 | 
				
			||||||
@ -245,7 +247,6 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
                            target_node=rel_data.get("target_node", ""),
 | 
					                            target_node=rel_data.get("target_node", ""),
 | 
				
			||||||
                            relationship_type=rel_data.get("relationship_type", ""),
 | 
					                            relationship_type=rel_data.get("relationship_type", ""),
 | 
				
			||||||
                            provider=rel_data.get("provider", self.name),
 | 
					                            provider=rel_data.get("provider", self.name),
 | 
				
			||||||
                            confidence=float(rel_data.get("confidence", 0.8)),
 | 
					 | 
				
			||||||
                            raw_data=rel_data.get("raw_data", {})
 | 
					                            raw_data=rel_data.get("raw_data", {})
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                    except (ValueError, TypeError) as e:
 | 
					                    except (ValueError, TypeError) as e:
 | 
				
			||||||
@ -265,7 +266,6 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
                            value=attr_data.get("value"),
 | 
					                            value=attr_data.get("value"),
 | 
				
			||||||
                            attr_type=attr_data.get("type", "unknown"),
 | 
					                            attr_type=attr_data.get("type", "unknown"),
 | 
				
			||||||
                            provider=attr_data.get("provider", self.name),
 | 
					                            provider=attr_data.get("provider", self.name),
 | 
				
			||||||
                            confidence=float(attr_data.get("confidence", 0.9)),
 | 
					 | 
				
			||||||
                            metadata=attr_data.get("metadata", {})
 | 
					                            metadata=attr_data.get("metadata", {})
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                    except (ValueError, TypeError) as e:
 | 
					                    except (ValueError, TypeError) as e:
 | 
				
			||||||
@ -293,7 +293,6 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
                        "source_node": rel.source_node,
 | 
					                        "source_node": rel.source_node,
 | 
				
			||||||
                        "target_node": rel.target_node,
 | 
					                        "target_node": rel.target_node,
 | 
				
			||||||
                        "relationship_type": rel.relationship_type,
 | 
					                        "relationship_type": rel.relationship_type,
 | 
				
			||||||
                        "confidence": rel.confidence,
 | 
					 | 
				
			||||||
                        "provider": rel.provider,
 | 
					                        "provider": rel.provider,
 | 
				
			||||||
                        "raw_data": rel.raw_data
 | 
					                        "raw_data": rel.raw_data
 | 
				
			||||||
                    } for rel in result.relationships
 | 
					                    } for rel in result.relationships
 | 
				
			||||||
@ -305,7 +304,6 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
                        "value": attr.value,
 | 
					                        "value": attr.value,
 | 
				
			||||||
                        "type": attr.type,
 | 
					                        "type": attr.type,
 | 
				
			||||||
                        "provider": attr.provider,
 | 
					                        "provider": attr.provider,
 | 
				
			||||||
                        "confidence": attr.confidence,
 | 
					 | 
				
			||||||
                        "metadata": attr.metadata
 | 
					                        "metadata": attr.metadata
 | 
				
			||||||
                    } for attr in result.attributes
 | 
					                    } for attr in result.attributes
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
@ -372,6 +370,7 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Process certificates to create proper domain and CA nodes.
 | 
					        Process certificates to create proper domain and CA nodes.
 | 
				
			||||||
        FIXED: Better error handling and progress tracking.
 | 
					        FIXED: Better error handling and progress tracking.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with certificate timestamps for time-based edge coloring.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        result = ProviderResult()
 | 
					        result = ProviderResult()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -391,8 +390,7 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
                name="crtsh_data_warning",
 | 
					                name="crtsh_data_warning",
 | 
				
			||||||
                value=incompleteness_warning,
 | 
					                value=incompleteness_warning,
 | 
				
			||||||
                attr_type='metadata',
 | 
					                attr_type='metadata',
 | 
				
			||||||
                provider=self.name,
 | 
					                provider=self.name
 | 
				
			||||||
                confidence=1.0
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        all_discovered_domains = set()
 | 
					        all_discovered_domains = set()
 | 
				
			||||||
@ -415,16 +413,28 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
                if cert_domains:
 | 
					                if cert_domains:
 | 
				
			||||||
                    all_discovered_domains.update(cert_domains)
 | 
					                    all_discovered_domains.update(cert_domains)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # Create CA nodes for certificate issuers
 | 
					                # Create CA nodes for certificate issuers with timestamp
 | 
				
			||||||
                issuer_name = self._parse_issuer_organization(cert_data.get('issuer_name', ''))
 | 
					                issuer_name = self._parse_issuer_organization(cert_data.get('issuer_name', ''))
 | 
				
			||||||
                if issuer_name and issuer_name not in processed_issuers:
 | 
					                if issuer_name and issuer_name not in processed_issuers:
 | 
				
			||||||
 | 
					                    # Enhanced raw_data with certificate timestamp for time-based edge coloring
 | 
				
			||||||
 | 
					                    issuer_raw_data = {'issuer_dn': cert_data.get('issuer_name', '')}
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    # Add certificate issue date (not_before) as relevance timestamp
 | 
				
			||||||
 | 
					                    not_before = cert_data.get('not_before')
 | 
				
			||||||
 | 
					                    if not_before:
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            not_before_date = self._parse_certificate_date(not_before)
 | 
				
			||||||
 | 
					                            issuer_raw_data['cert_not_before'] = not_before_date.isoformat()
 | 
				
			||||||
 | 
					                            issuer_raw_data['relevance_timestamp'] = not_before_date.isoformat()  # Standardized field
 | 
				
			||||||
 | 
					                        except Exception as e:
 | 
				
			||||||
 | 
					                            self.logger.logger.debug(f"Failed to parse not_before date for issuer: {e}")
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
                    result.add_relationship(
 | 
					                    result.add_relationship(
 | 
				
			||||||
                        source_node=query_domain,
 | 
					                        source_node=query_domain,
 | 
				
			||||||
                        target_node=issuer_name,
 | 
					                        target_node=issuer_name,
 | 
				
			||||||
                        relationship_type='crtsh_cert_issuer',
 | 
					                        relationship_type='crtsh_cert_issuer',
 | 
				
			||||||
                        provider=self.name,
 | 
					                        provider=self.name,
 | 
				
			||||||
                        confidence=0.95,
 | 
					                        raw_data=issuer_raw_data
 | 
				
			||||||
                        raw_data={'issuer_dn': cert_data.get('issuer_name', '')}
 | 
					 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    processed_issuers.add(issuer_name)
 | 
					                    processed_issuers.add(issuer_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -442,7 +452,6 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
                                value=value,
 | 
					                                value=value,
 | 
				
			||||||
                                attr_type='certificate_data',
 | 
					                                attr_type='certificate_data',
 | 
				
			||||||
                                provider=self.name,
 | 
					                                provider=self.name,
 | 
				
			||||||
                                confidence=0.9,
 | 
					 | 
				
			||||||
                                metadata={'certificate_id': cert_data.get('id')}
 | 
					                                metadata={'certificate_id': cert_data.get('id')}
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
@ -457,7 +466,7 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
            self.logger.logger.info(f"CrtSh query cancelled before relationship creation for domain: {query_domain}")
 | 
					            self.logger.logger.info(f"CrtSh query cancelled before relationship creation for domain: {query_domain}")
 | 
				
			||||||
            return result
 | 
					            return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create selective relationships to avoid large entities
 | 
					        # Create selective relationships to avoid large entities with enhanced timestamps
 | 
				
			||||||
        relationships_created = 0
 | 
					        relationships_created = 0
 | 
				
			||||||
        for discovered_domain in all_discovered_domains:
 | 
					        for discovered_domain in all_discovered_domains:
 | 
				
			||||||
            if discovered_domain == query_domain:
 | 
					            if discovered_domain == query_domain:
 | 
				
			||||||
@ -467,25 +476,36 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if self._should_create_relationship(query_domain, discovered_domain):
 | 
					            if self._should_create_relationship(query_domain, discovered_domain):
 | 
				
			||||||
                confidence = self._calculate_domain_relationship_confidence(
 | 
					                # Enhanced raw_data with certificate timestamp for domain relationships
 | 
				
			||||||
                    query_domain, discovered_domain, [], all_discovered_domains
 | 
					                domain_raw_data = {'relationship_type': 'certificate_discovery'}
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Find the most recent certificate for this domain pair to use as timestamp
 | 
				
			||||||
 | 
					                most_recent_cert = self._find_most_recent_cert_for_domains(
 | 
				
			||||||
 | 
					                    certificates, query_domain, discovered_domain
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					                if most_recent_cert:
 | 
				
			||||||
 | 
					                    not_before = most_recent_cert.get('not_before')
 | 
				
			||||||
 | 
					                    if not_before:
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            not_before_date = self._parse_certificate_date(not_before)
 | 
				
			||||||
 | 
					                            domain_raw_data['cert_not_before'] = not_before_date.isoformat()
 | 
				
			||||||
 | 
					                            domain_raw_data['relevance_timestamp'] = not_before_date.isoformat()
 | 
				
			||||||
 | 
					                        except Exception as e:
 | 
				
			||||||
 | 
					                            self.logger.logger.debug(f"Failed to parse not_before date for domain relationship: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                result.add_relationship(
 | 
					                result.add_relationship(
 | 
				
			||||||
                    source_node=query_domain,
 | 
					                    source_node=query_domain,
 | 
				
			||||||
                    target_node=discovered_domain,
 | 
					                    target_node=discovered_domain,
 | 
				
			||||||
                    relationship_type='crtsh_san_certificate',
 | 
					                    relationship_type='crtsh_san_certificate',
 | 
				
			||||||
                    provider=self.name,
 | 
					                    provider=self.name,
 | 
				
			||||||
                    confidence=confidence,
 | 
					                    raw_data=domain_raw_data
 | 
				
			||||||
                    raw_data={'relationship_type': 'certificate_discovery'}
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                self.log_relationship_discovery(
 | 
					                self.log_relationship_discovery(
 | 
				
			||||||
                    source_node=query_domain,
 | 
					                    source_node=query_domain,
 | 
				
			||||||
                    target_node=discovered_domain,
 | 
					                    target_node=discovered_domain,
 | 
				
			||||||
                    relationship_type='crtsh_san_certificate',
 | 
					                    relationship_type='crtsh_san_certificate',
 | 
				
			||||||
                    confidence_score=confidence,
 | 
					                    raw_data=domain_raw_data,
 | 
				
			||||||
                    raw_data={'relationship_type': 'certificate_discovery'},
 | 
					 | 
				
			||||||
                    discovery_method="certificate_transparency_analysis"
 | 
					                    discovery_method="certificate_transparency_analysis"
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                relationships_created += 1
 | 
					                relationships_created += 1
 | 
				
			||||||
@ -493,6 +513,31 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
        self.logger.logger.info(f"CrtSh processing completed for {query_domain}: processed {processed_certs}/{len(certificates)} certificates, {len(all_discovered_domains)} domains, {relationships_created} relationships")
 | 
					        self.logger.logger.info(f"CrtSh processing completed for {query_domain}: processed {processed_certs}/{len(certificates)} certificates, {len(all_discovered_domains)} domains, {relationships_created} relationships")
 | 
				
			||||||
        return result
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _find_most_recent_cert_for_domains(self, certificates: List[Dict[str, Any]], 
 | 
				
			||||||
 | 
					                                         domain1: str, domain2: str) -> Optional[Dict[str, Any]]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Find the most recent certificate that contains both domains.
 | 
				
			||||||
 | 
					        Used for determining the relevance timestamp for domain relationships.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        most_recent_cert = None
 | 
				
			||||||
 | 
					        most_recent_date = None
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for cert in certificates:
 | 
				
			||||||
 | 
					            # Check if this certificate contains both domains
 | 
				
			||||||
 | 
					            cert_domains = self._extract_domains_from_certificate(cert)
 | 
				
			||||||
 | 
					            if domain1 in cert_domains and domain2 in cert_domains:
 | 
				
			||||||
 | 
					                not_before = cert.get('not_before')
 | 
				
			||||||
 | 
					                if not_before:
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        cert_date = self._parse_certificate_date(not_before)
 | 
				
			||||||
 | 
					                        if most_recent_date is None or cert_date > most_recent_date:
 | 
				
			||||||
 | 
					                            most_recent_date = cert_date
 | 
				
			||||||
 | 
					                            most_recent_cert = cert
 | 
				
			||||||
 | 
					                    except Exception:
 | 
				
			||||||
 | 
					                        continue
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return most_recent_cert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # [Rest of the methods remain the same as in the original file]
 | 
					    # [Rest of the methods remain the same as in the original file]
 | 
				
			||||||
    def _should_create_relationship(self, source_domain: str, target_domain: str) -> bool:
 | 
					    def _should_create_relationship(self, source_domain: str, target_domain: str) -> bool:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -664,25 +709,6 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return [d for d in final_domains if _is_valid_domain(d)]
 | 
					        return [d for d in final_domains if _is_valid_domain(d)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _calculate_domain_relationship_confidence(self, domain1: str, domain2: str, 
 | 
					 | 
				
			||||||
                                                shared_certificates: List[Dict[str, Any]],
 | 
					 | 
				
			||||||
                                                all_discovered_domains: Set[str]) -> float:
 | 
					 | 
				
			||||||
        """Calculate confidence score for domain relationship based on various factors."""
 | 
					 | 
				
			||||||
        base_confidence = 0.9
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        relationship_context = self._determine_relationship_context(domain2, domain1)
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        if relationship_context == 'exact_match':
 | 
					 | 
				
			||||||
            context_bonus = 0.0
 | 
					 | 
				
			||||||
        elif relationship_context == 'subdomain':
 | 
					 | 
				
			||||||
            context_bonus = 0.1
 | 
					 | 
				
			||||||
        elif relationship_context == 'parent_domain':
 | 
					 | 
				
			||||||
            context_bonus = 0.05
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            context_bonus = 0.0
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        final_confidence = base_confidence + context_bonus
 | 
					 | 
				
			||||||
        return max(0.1, min(1.0, final_confidence))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _determine_relationship_context(self, cert_domain: str, query_domain: str) -> str:
 | 
					    def _determine_relationship_context(self, cert_domain: str, query_domain: str) -> str:
 | 
				
			||||||
        """Determine the context of the relationship between certificate domain and query domain."""
 | 
					        """Determine the context of the relationship between certificate domain and query domain."""
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from dns import resolver, reversename
 | 
					from dns import resolver, reversename
 | 
				
			||||||
from typing import Dict
 | 
					from typing import Dict
 | 
				
			||||||
 | 
					from datetime import datetime, timezone
 | 
				
			||||||
from .base_provider import BaseProvider
 | 
					from .base_provider import BaseProvider
 | 
				
			||||||
from core.provider_result import ProviderResult
 | 
					from core.provider_result import ProviderResult
 | 
				
			||||||
from utils.helpers import _is_valid_ip, _is_valid_domain, get_ip_version
 | 
					from utils.helpers import _is_valid_ip, _is_valid_domain, get_ip_version
 | 
				
			||||||
@ -11,6 +12,7 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    Provider for standard DNS resolution and reverse DNS lookups.
 | 
					    Provider for standard DNS resolution and reverse DNS lookups.
 | 
				
			||||||
    Now returns standardized ProviderResult objects with IPv4 and IPv6 support.
 | 
					    Now returns standardized ProviderResult objects with IPv4 and IPv6 support.
 | 
				
			||||||
 | 
					    UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, name=None, session_config=None):
 | 
					    def __init__(self, name=None, session_config=None):
 | 
				
			||||||
@ -51,6 +53,7 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Query DNS records for the domain to discover relationships and attributes.
 | 
					        Query DNS records for the domain to discover relationships and attributes.
 | 
				
			||||||
        FIXED: Now creates separate attributes for each DNS record type.
 | 
					        FIXED: Now creates separate attributes for each DNS record type.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        Args:
 | 
					        Args:
 | 
				
			||||||
            domain: Domain to investigate
 | 
					            domain: Domain to investigate
 | 
				
			||||||
@ -62,11 +65,12 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
            return ProviderResult()
 | 
					            return ProviderResult()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        result = ProviderResult()
 | 
					        result = ProviderResult()
 | 
				
			||||||
 | 
					        discovery_time = datetime.now(timezone.utc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Query all record types - each gets its own attribute
 | 
					        # Query all record types - each gets its own attribute
 | 
				
			||||||
        for record_type in ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SOA', 'TXT', 'SRV', 'CAA']:
 | 
					        for record_type in ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'SOA', 'TXT', 'SRV', 'CAA']:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                self._query_record(domain, record_type, result)
 | 
					                self._query_record(domain, record_type, result, discovery_time)
 | 
				
			||||||
            #except resolver.NoAnswer:
 | 
					            #except resolver.NoAnswer:
 | 
				
			||||||
                # This is not an error, just a confirmation that the record doesn't exist.
 | 
					                # This is not an error, just a confirmation that the record doesn't exist.
 | 
				
			||||||
                #self.logger.logger.debug(f"No {record_type} record found for {domain}")
 | 
					                #self.logger.logger.debug(f"No {record_type} record found for {domain}")
 | 
				
			||||||
@ -79,6 +83,7 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
    def query_ip(self, ip: str) -> ProviderResult:
 | 
					    def query_ip(self, ip: str) -> ProviderResult:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Query reverse DNS for the IP address (supports both IPv4 and IPv6).
 | 
					        Query reverse DNS for the IP address (supports both IPv4 and IPv6).
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Args:
 | 
					        Args:
 | 
				
			||||||
            ip: IP address to investigate (IPv4 or IPv6)
 | 
					            ip: IP address to investigate (IPv4 or IPv6)
 | 
				
			||||||
@ -91,6 +96,7 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        result = ProviderResult()
 | 
					        result = ProviderResult()
 | 
				
			||||||
        ip_version = get_ip_version(ip)
 | 
					        ip_version = get_ip_version(ip)
 | 
				
			||||||
 | 
					        discovery_time = datetime.now(timezone.utc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            # Perform reverse DNS lookup (works for both IPv4 and IPv6)
 | 
					            # Perform reverse DNS lookup (works for both IPv4 and IPv6)
 | 
				
			||||||
@ -112,20 +118,24 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
                        relationship_type = 'dns_a_record'
 | 
					                        relationship_type = 'dns_a_record'
 | 
				
			||||||
                        record_prefix = 'A'
 | 
					                        record_prefix = 'A'
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
 | 
					                    # Enhanced raw_data with discovery timestamp for time-based edge coloring
 | 
				
			||||||
 | 
					                    raw_data = {
 | 
				
			||||||
 | 
					                        'query_type': 'PTR',
 | 
				
			||||||
 | 
					                        'ip_address': ip,
 | 
				
			||||||
 | 
					                        'ip_version': ip_version,
 | 
				
			||||||
 | 
					                        'hostname': hostname,
 | 
				
			||||||
 | 
					                        'ttl': response.ttl,
 | 
				
			||||||
 | 
					                        'discovery_timestamp': discovery_time.isoformat(),
 | 
				
			||||||
 | 
					                        'relevance_timestamp': discovery_time.isoformat()  # DNS data is "fresh" when discovered
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
                    # Add the relationship
 | 
					                    # Add the relationship
 | 
				
			||||||
                    result.add_relationship(
 | 
					                    result.add_relationship(
 | 
				
			||||||
                        source_node=ip,
 | 
					                        source_node=ip,
 | 
				
			||||||
                        target_node=hostname,
 | 
					                        target_node=hostname,
 | 
				
			||||||
                        relationship_type='dns_ptr_record',
 | 
					                        relationship_type='dns_ptr_record',
 | 
				
			||||||
                        provider=self.name,
 | 
					                        provider=self.name,
 | 
				
			||||||
                        confidence=0.8,
 | 
					                        raw_data=raw_data
 | 
				
			||||||
                        raw_data={
 | 
					 | 
				
			||||||
                            'query_type': 'PTR',
 | 
					 | 
				
			||||||
                            'ip_address': ip,
 | 
					 | 
				
			||||||
                            'ip_version': ip_version,
 | 
					 | 
				
			||||||
                            'hostname': hostname,
 | 
					 | 
				
			||||||
                            'ttl': response.ttl
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    # Add to PTR records list
 | 
					                    # Add to PTR records list
 | 
				
			||||||
@ -136,14 +146,7 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
                        source_node=ip,
 | 
					                        source_node=ip,
 | 
				
			||||||
                        target_node=hostname,
 | 
					                        target_node=hostname,
 | 
				
			||||||
                        relationship_type='dns_ptr_record',
 | 
					                        relationship_type='dns_ptr_record',
 | 
				
			||||||
                        confidence_score=0.8,
 | 
					                        raw_data=raw_data,
 | 
				
			||||||
                        raw_data={
 | 
					 | 
				
			||||||
                            'query_type': 'PTR',
 | 
					 | 
				
			||||||
                            'ip_address': ip,
 | 
					 | 
				
			||||||
                            'ip_version': ip_version,
 | 
					 | 
				
			||||||
                            'hostname': hostname,
 | 
					 | 
				
			||||||
                            'ttl': response.ttl
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        discovery_method=f"reverse_dns_lookup_ipv{ip_version}"
 | 
					                        discovery_method=f"reverse_dns_lookup_ipv{ip_version}"
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -155,7 +158,6 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
                    value=ptr_records,
 | 
					                    value=ptr_records,
 | 
				
			||||||
                    attr_type='dns_record',
 | 
					                    attr_type='dns_record',
 | 
				
			||||||
                    provider=self.name,
 | 
					                    provider=self.name,
 | 
				
			||||||
                    confidence=0.8,
 | 
					 | 
				
			||||||
                    metadata={'ttl': response.ttl, 'ip_version': ip_version}
 | 
					                    metadata={'ttl': response.ttl, 'ip_version': ip_version}
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -170,10 +172,11 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return result
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _query_record(self, domain: str, record_type: str, result: ProviderResult) -> None:
 | 
					    def _query_record(self, domain: str, record_type: str, result: ProviderResult, discovery_time: datetime) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        FIXED: Query DNS records with unique attribute names for each record type.
 | 
					        FIXED: Query DNS records with unique attribute names for each record type.
 | 
				
			||||||
        Enhanced to better handle IPv6 AAAA records.
 | 
					        Enhanced to better handle IPv6 AAAA records.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with discovery timestamps for time-based edge coloring.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.total_requests += 1
 | 
					            self.total_requests += 1
 | 
				
			||||||
@ -217,18 +220,20 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
                    if record_type in ['A', 'AAAA'] and _is_valid_ip(target):
 | 
					                    if record_type in ['A', 'AAAA'] and _is_valid_ip(target):
 | 
				
			||||||
                        ip_version = get_ip_version(target)
 | 
					                        ip_version = get_ip_version(target)
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
 | 
					                    # Enhanced raw_data with discovery timestamp for time-based edge coloring
 | 
				
			||||||
                    raw_data = {
 | 
					                    raw_data = {
 | 
				
			||||||
                        'query_type': record_type,
 | 
					                        'query_type': record_type,
 | 
				
			||||||
                        'domain': domain,
 | 
					                        'domain': domain,
 | 
				
			||||||
                        'value': target,
 | 
					                        'value': target,
 | 
				
			||||||
                        'ttl': response.ttl
 | 
					                        'ttl': response.ttl,
 | 
				
			||||||
 | 
					                        'discovery_timestamp': discovery_time.isoformat(),
 | 
				
			||||||
 | 
					                        'relevance_timestamp': discovery_time.isoformat()  # DNS data is "fresh" when discovered
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    if ip_version:
 | 
					                    if ip_version:
 | 
				
			||||||
                        raw_data['ip_version'] = ip_version
 | 
					                        raw_data['ip_version'] = ip_version
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    relationship_type = f"dns_{record_type.lower()}_record"
 | 
					                    relationship_type = f"dns_{record_type.lower()}_record"
 | 
				
			||||||
                    confidence = 0.8
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    # Add relationship
 | 
					                    # Add relationship
 | 
				
			||||||
                    result.add_relationship(
 | 
					                    result.add_relationship(
 | 
				
			||||||
@ -236,7 +241,6 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
                        target_node=target,
 | 
					                        target_node=target,
 | 
				
			||||||
                        relationship_type=relationship_type,
 | 
					                        relationship_type=relationship_type,
 | 
				
			||||||
                        provider=self.name,
 | 
					                        provider=self.name,
 | 
				
			||||||
                        confidence=confidence,
 | 
					 | 
				
			||||||
                        raw_data=raw_data
 | 
					                        raw_data=raw_data
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -252,7 +256,6 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
                        source_node=domain,
 | 
					                        source_node=domain,
 | 
				
			||||||
                        target_node=target,
 | 
					                        target_node=target,
 | 
				
			||||||
                        relationship_type=relationship_type,
 | 
					                        relationship_type=relationship_type,
 | 
				
			||||||
                        confidence_score=confidence,
 | 
					 | 
				
			||||||
                        raw_data=raw_data,
 | 
					                        raw_data=raw_data,
 | 
				
			||||||
                        discovery_method=discovery_method
 | 
					                        discovery_method=discovery_method
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
@ -276,7 +279,6 @@ class DNSProvider(BaseProvider):
 | 
				
			|||||||
                    value=dns_records,
 | 
					                    value=dns_records,
 | 
				
			||||||
                    attr_type='dns_record_list',
 | 
					                    attr_type='dns_record_list',
 | 
				
			||||||
                    provider=self.name,
 | 
					                    provider=self.name,
 | 
				
			||||||
                    confidence=0.8,
 | 
					 | 
				
			||||||
                    metadata=metadata
 | 
					                    metadata=metadata
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    Provider for querying Shodan API for IP address information.
 | 
					    Provider for querying Shodan API for IP address information.
 | 
				
			||||||
    Now returns standardized ProviderResult objects with caching support for IPv4 and IPv6.
 | 
					    Now returns standardized ProviderResult objects with caching support for IPv4 and IPv6.
 | 
				
			||||||
 | 
					    UPDATED: Enhanced with last_seen timestamp for time-based edge coloring.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, name=None, session_config=None):
 | 
					    def __init__(self, name=None, session_config=None):
 | 
				
			||||||
@ -145,6 +146,7 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Query Shodan for information about an IP address (IPv4 or IPv6), with caching of processed data.
 | 
					        Query Shodan for information about an IP address (IPv4 or IPv6), with caching of processed data.
 | 
				
			||||||
        FIXED: Proper 404 handling to prevent unnecessary retries.
 | 
					        FIXED: Proper 404 handling to prevent unnecessary retries.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with last_seen timestamp extraction for time-based edge coloring.
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        Args:
 | 
					        Args:
 | 
				
			||||||
            ip: IP address to investigate (IPv4 or IPv6)
 | 
					            ip: IP address to investigate (IPv4 or IPv6)
 | 
				
			||||||
@ -304,7 +306,6 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                    target_node=rel_data["target_node"],
 | 
					                    target_node=rel_data["target_node"],
 | 
				
			||||||
                    relationship_type=rel_data["relationship_type"],
 | 
					                    relationship_type=rel_data["relationship_type"],
 | 
				
			||||||
                    provider=rel_data["provider"],
 | 
					                    provider=rel_data["provider"],
 | 
				
			||||||
                    confidence=rel_data["confidence"],
 | 
					 | 
				
			||||||
                    raw_data=rel_data.get("raw_data", {})
 | 
					                    raw_data=rel_data.get("raw_data", {})
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
@ -316,7 +317,6 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                    value=attr_data["value"],
 | 
					                    value=attr_data["value"],
 | 
				
			||||||
                    attr_type=attr_data["type"],
 | 
					                    attr_type=attr_data["type"],
 | 
				
			||||||
                    provider=attr_data["provider"],
 | 
					                    provider=attr_data["provider"],
 | 
				
			||||||
                    confidence=attr_data["confidence"],
 | 
					 | 
				
			||||||
                    metadata=attr_data.get("metadata", {})
 | 
					                    metadata=attr_data.get("metadata", {})
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
@ -336,7 +336,6 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                        "source_node": rel.source_node,
 | 
					                        "source_node": rel.source_node,
 | 
				
			||||||
                        "target_node": rel.target_node,
 | 
					                        "target_node": rel.target_node,
 | 
				
			||||||
                        "relationship_type": rel.relationship_type,
 | 
					                        "relationship_type": rel.relationship_type,
 | 
				
			||||||
                        "confidence": rel.confidence,
 | 
					 | 
				
			||||||
                        "provider": rel.provider,
 | 
					                        "provider": rel.provider,
 | 
				
			||||||
                        "raw_data": rel.raw_data
 | 
					                        "raw_data": rel.raw_data
 | 
				
			||||||
                    } for rel in result.relationships
 | 
					                    } for rel in result.relationships
 | 
				
			||||||
@ -348,7 +347,6 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                        "value": attr.value,
 | 
					                        "value": attr.value,
 | 
				
			||||||
                        "type": attr.type,
 | 
					                        "type": attr.type,
 | 
				
			||||||
                        "provider": attr.provider,
 | 
					                        "provider": attr.provider,
 | 
				
			||||||
                        "confidence": attr.confidence,
 | 
					 | 
				
			||||||
                        "metadata": attr.metadata
 | 
					                        "metadata": attr.metadata
 | 
				
			||||||
                    } for attr in result.attributes
 | 
					                    } for attr in result.attributes
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
@ -362,25 +360,40 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        VERIFIED: Process Shodan data creating ISP nodes with ASN attributes and proper relationships.
 | 
					        VERIFIED: Process Shodan data creating ISP nodes with ASN attributes and proper relationships.
 | 
				
			||||||
        Enhanced to include IP version information for IPv6 addresses.
 | 
					        Enhanced to include IP version information for IPv6 addresses.
 | 
				
			||||||
 | 
					        UPDATED: Enhanced with last_seen timestamp for time-based edge coloring.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        result = ProviderResult()
 | 
					        result = ProviderResult()
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Determine IP version for metadata
 | 
					        # Determine IP version for metadata
 | 
				
			||||||
        ip_version = get_ip_version(ip)
 | 
					        ip_version = get_ip_version(ip)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Extract last_seen timestamp for time-based edge coloring
 | 
				
			||||||
 | 
					        last_seen = data.get('last_seen')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        # VERIFIED: Extract ISP information and create proper ISP node with ASN
 | 
					        # VERIFIED: Extract ISP information and create proper ISP node with ASN
 | 
				
			||||||
        isp_name = data.get('org')
 | 
					        isp_name = data.get('org')
 | 
				
			||||||
        asn_value = data.get('asn')
 | 
					        asn_value = data.get('asn')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if isp_name and asn_value:
 | 
					        if isp_name and asn_value:
 | 
				
			||||||
 | 
					            # Enhanced raw_data with last_seen timestamp
 | 
				
			||||||
 | 
					            raw_data = {
 | 
				
			||||||
 | 
					                'asn': asn_value, 
 | 
				
			||||||
 | 
					                'shodan_org': isp_name, 
 | 
				
			||||||
 | 
					                'ip_version': ip_version
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Add last_seen timestamp if available
 | 
				
			||||||
 | 
					            if last_seen:
 | 
				
			||||||
 | 
					                raw_data['last_seen'] = last_seen
 | 
				
			||||||
 | 
					                raw_data['relevance_timestamp'] = last_seen  # Standardized field for time-based coloring
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            # Create relationship from IP to ISP
 | 
					            # Create relationship from IP to ISP
 | 
				
			||||||
            result.add_relationship(
 | 
					            result.add_relationship(
 | 
				
			||||||
                source_node=ip,
 | 
					                source_node=ip,
 | 
				
			||||||
                target_node=isp_name,
 | 
					                target_node=isp_name,
 | 
				
			||||||
                relationship_type='shodan_isp',
 | 
					                relationship_type='shodan_isp',
 | 
				
			||||||
                provider=self.name,
 | 
					                provider=self.name,
 | 
				
			||||||
                confidence=0.9,
 | 
					                raw_data=raw_data
 | 
				
			||||||
                raw_data={'asn': asn_value, 'shodan_org': isp_name, 'ip_version': ip_version}
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            # Add ASN as attribute to the ISP node
 | 
					            # Add ASN as attribute to the ISP node
 | 
				
			||||||
@ -390,7 +403,6 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                value=asn_value,
 | 
					                value=asn_value,
 | 
				
			||||||
                attr_type='isp_info',
 | 
					                attr_type='isp_info',
 | 
				
			||||||
                provider=self.name,
 | 
					                provider=self.name,
 | 
				
			||||||
                confidence=0.9,
 | 
					 | 
				
			||||||
                metadata={'description': 'Autonomous System Number from Shodan', 'ip_version': ip_version}
 | 
					                metadata={'description': 'Autonomous System Number from Shodan', 'ip_version': ip_version}
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
@ -401,7 +413,6 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                value=isp_name,
 | 
					                value=isp_name,
 | 
				
			||||||
                attr_type='isp_info',
 | 
					                attr_type='isp_info',
 | 
				
			||||||
                provider=self.name,
 | 
					                provider=self.name,
 | 
				
			||||||
                confidence=0.9,
 | 
					 | 
				
			||||||
                metadata={'description': 'Organization name from Shodan', 'ip_version': ip_version}
 | 
					                metadata={'description': 'Organization name from Shodan', 'ip_version': ip_version}
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -416,20 +427,24 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                        else:
 | 
					                        else:
 | 
				
			||||||
                            relationship_type = 'shodan_a_record'
 | 
					                            relationship_type = 'shodan_a_record'
 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
 | 
					                        # Enhanced raw_data with last_seen timestamp
 | 
				
			||||||
 | 
					                        hostname_raw_data = {**data, 'ip_version': ip_version}
 | 
				
			||||||
 | 
					                        if last_seen:
 | 
				
			||||||
 | 
					                            hostname_raw_data['last_seen'] = last_seen
 | 
				
			||||||
 | 
					                            hostname_raw_data['relevance_timestamp'] = last_seen
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
                        result.add_relationship(
 | 
					                        result.add_relationship(
 | 
				
			||||||
                            source_node=ip,
 | 
					                            source_node=ip,
 | 
				
			||||||
                            target_node=hostname,
 | 
					                            target_node=hostname,
 | 
				
			||||||
                            relationship_type=relationship_type,
 | 
					                            relationship_type=relationship_type,
 | 
				
			||||||
                            provider=self.name,
 | 
					                            provider=self.name,
 | 
				
			||||||
                            confidence=0.8,
 | 
					                            raw_data=hostname_raw_data
 | 
				
			||||||
                            raw_data={**data, 'ip_version': ip_version}
 | 
					 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        self.log_relationship_discovery(
 | 
					                        self.log_relationship_discovery(
 | 
				
			||||||
                            source_node=ip,
 | 
					                            source_node=ip,
 | 
				
			||||||
                            target_node=hostname,
 | 
					                            target_node=hostname,
 | 
				
			||||||
                            relationship_type=relationship_type,
 | 
					                            relationship_type=relationship_type,
 | 
				
			||||||
                            confidence_score=0.8,
 | 
					                            raw_data=hostname_raw_data,
 | 
				
			||||||
                            raw_data={**data, 'ip_version': ip_version},
 | 
					 | 
				
			||||||
                            discovery_method=f"shodan_host_lookup_ipv{ip_version}"
 | 
					                            discovery_method=f"shodan_host_lookup_ipv{ip_version}"
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
            elif key == 'ports':
 | 
					            elif key == 'ports':
 | 
				
			||||||
@ -441,7 +456,6 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                        value=port,
 | 
					                        value=port,
 | 
				
			||||||
                        attr_type='shodan_network_info',
 | 
					                        attr_type='shodan_network_info',
 | 
				
			||||||
                        provider=self.name,
 | 
					                        provider=self.name,
 | 
				
			||||||
                        confidence=0.9,
 | 
					 | 
				
			||||||
                        metadata={'ip_version': ip_version}
 | 
					                        metadata={'ip_version': ip_version}
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
            elif isinstance(value, (str, int, float, bool)) and value is not None:
 | 
					            elif isinstance(value, (str, int, float, bool)) and value is not None:
 | 
				
			||||||
@ -452,7 +466,6 @@ class ShodanProvider(BaseProvider):
 | 
				
			|||||||
                    value=value,
 | 
					                    value=value,
 | 
				
			||||||
                    attr_type='shodan_info',
 | 
					                    attr_type='shodan_info',
 | 
				
			||||||
                    provider=self.name,
 | 
					                    provider=self.name,
 | 
				
			||||||
                    confidence=0.9,
 | 
					 | 
				
			||||||
                    metadata={'ip_version': ip_version}
 | 
					                    metadata={'ip_version': ip_version}
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -326,6 +326,20 @@ input[type="text"]:focus, select:focus {
 | 
				
			|||||||
    animation: progressGlow 2s ease-in-out infinite alternate;
 | 
					    animation: progressGlow 2s ease-in-out infinite alternate;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gradient-bar {
 | 
				
			||||||
 | 
					    height: 4px;
 | 
				
			||||||
 | 
					    background: linear-gradient(to right, #6b7280, #00bfff);
 | 
				
			||||||
 | 
					    border-radius: 2px;
 | 
				
			||||||
 | 
					    margin: 0.2rem 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gradient-labels {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    font-size: 0.6rem;
 | 
				
			||||||
 | 
					    color: #888;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@keyframes progressShimmer {
 | 
					@keyframes progressShimmer {
 | 
				
			||||||
    0% { transform: translateX(-100%); }
 | 
					    0% { transform: translateX(-100%); }
 | 
				
			||||||
    100% { transform: translateX(100%); }
 | 
					    100% { transform: translateX(100%); }
 | 
				
			||||||
@ -380,32 +394,59 @@ input[type="text"]:focus, select:focus {
 | 
				
			|||||||
    color: #999;
 | 
					    color: #999;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Graph Controls */
 | 
					/* Enhanced graph controls layout */
 | 
				
			||||||
.graph-controls {
 | 
					.graph-controls {
 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    top: 8px;
 | 
					 | 
				
			||||||
    right: 8px;
 | 
					 | 
				
			||||||
    z-index: 10;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
    gap: 0.3rem;
 | 
					    gap: 0.3rem;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 10px;
 | 
				
			||||||
 | 
					    left: 10px;
 | 
				
			||||||
 | 
					    background: rgba(26, 26, 26, 0.9);
 | 
				
			||||||
 | 
					    padding: 0.5rem;
 | 
				
			||||||
 | 
					    border-radius: 6px;
 | 
				
			||||||
 | 
					    border: 1px solid #444;
 | 
				
			||||||
 | 
					    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
 | 
				
			||||||
 | 
					    z-index: 100;
 | 
				
			||||||
 | 
					    min-width: 200px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.graph-control-btn, .btn-icon-small {
 | 
					.graph-control-btn {
 | 
				
			||||||
    background: rgba(42, 42, 42, 0.9);
 | 
					    background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
 | 
				
			||||||
    border: 1px solid #555;
 | 
					    border: 1px solid #555;
 | 
				
			||||||
    color: #c7c7c7;
 | 
					    color: #c7c7c7;
 | 
				
			||||||
    padding: 0.3rem 0.5rem;
 | 
					    padding: 0.4rem 0.8rem;
 | 
				
			||||||
    font-family: 'Roboto Mono', monospace;
 | 
					    border-radius: 4px;
 | 
				
			||||||
    font-size: 0.7rem;
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					    cursor: pointer;
 | 
				
			||||||
    transition: all 0.3s ease;
 | 
					    font-family: 'Roboto Mono', monospace;
 | 
				
			||||||
 | 
					    font-size: 0.8rem;
 | 
				
			||||||
 | 
					    transition: all 0.2s ease;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.graph-control-btn:hover, .btn-icon-small:hover {
 | 
					.graph-control-btn:hover {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #3a3a3a 0%, #2e2e2e 100%);
 | 
				
			||||||
    border-color: #00ff41;
 | 
					    border-color: #00ff41;
 | 
				
			||||||
    color: #00ff41;
 | 
					    color: #00ff41;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.graph-control-btn:disabled {
 | 
				
			||||||
 | 
					    opacity: 0.5;
 | 
				
			||||||
 | 
					    cursor: not-allowed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.manual-refresh-btn {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #4a4a2a 0%, #3e3e1e 100%);
 | 
				
			||||||
 | 
					    border-color: #ffaa00;
 | 
				
			||||||
 | 
					    color: #ffaa00;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.manual-refresh-btn:hover {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #5a5a3a 0%, #4e4e2e 100%);
 | 
				
			||||||
 | 
					    color: #ffcc33;
 | 
				
			||||||
 | 
					    border-color: #ffcc33;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.graph-filter-panel {
 | 
					.graph-filter-panel {
 | 
				
			||||||
    position: absolute;
 | 
					    position: absolute;
 | 
				
			||||||
    bottom: 8px;
 | 
					    bottom: 8px;
 | 
				
			||||||
@ -500,14 +541,6 @@ input[type="text"]:focus, select:focus {
 | 
				
			|||||||
    height: 2px;
 | 
					    height: 2px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.legend-edge.high-confidence {
 | 
					 | 
				
			||||||
    background: #00ff41;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.legend-edge.medium-confidence {
 | 
					 | 
				
			||||||
    background: #ff9900;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Provider Panel */
 | 
					/* Provider Panel */
 | 
				
			||||||
.provider-panel {
 | 
					.provider-panel {
 | 
				
			||||||
    grid-area: providers;
 | 
					    grid-area: providers;
 | 
				
			||||||
@ -987,11 +1020,6 @@ input[type="text"]:focus, select:focus {
 | 
				
			|||||||
    border-radius: 2px;
 | 
					    border-radius: 2px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.confidence-indicator {
 | 
					 | 
				
			||||||
    font-size: 0.6rem;
 | 
					 | 
				
			||||||
    letter-spacing: 1px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.node-link-compact {
 | 
					.node-link-compact {
 | 
				
			||||||
    color: #00aaff;
 | 
					    color: #00aaff;
 | 
				
			||||||
    text-decoration: none;
 | 
					    text-decoration: none;
 | 
				
			||||||
@ -1095,6 +1123,56 @@ input[type="text"]:focus, select:focus {
 | 
				
			|||||||
    border-left: 3px solid #00aaff;
 | 
					    border-left: 3px solid #00aaff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-control-container {
 | 
				
			||||||
 | 
					    margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					    padding: 0.5rem;
 | 
				
			||||||
 | 
					    background: rgba(42, 42, 42, 0.3);
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    border: 1px solid #444;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-control-label {
 | 
				
			||||||
 | 
					    font-size: 0.8rem;
 | 
				
			||||||
 | 
					    color: #c7c7c7;
 | 
				
			||||||
 | 
					    margin-bottom: 0.3rem;
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    font-family: 'Roboto Mono', monospace;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-control-input {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 0.3rem;
 | 
				
			||||||
 | 
					    background: #1a1a1a;
 | 
				
			||||||
 | 
					    border: 1px solid #555;
 | 
				
			||||||
 | 
					    border-radius: 3px;
 | 
				
			||||||
 | 
					    color: #c7c7c7;
 | 
				
			||||||
 | 
					    font-family: 'Roboto Mono', monospace;
 | 
				
			||||||
 | 
					    font-size: 0.75rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-control-input:focus {
 | 
				
			||||||
 | 
					    outline: none;
 | 
				
			||||||
 | 
					    border-color: #00ff41;
 | 
				
			||||||
 | 
					    box-shadow: 0 0 5px rgba(0, 255, 65, 0.3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-gradient-info {
 | 
				
			||||||
 | 
					    font-size: 0.7rem;
 | 
				
			||||||
 | 
					    color: #999;
 | 
				
			||||||
 | 
					    margin-top: 0.3rem;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    font-family: 'Roboto Mono', monospace;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Edge color legend for time-based gradient */
 | 
				
			||||||
 | 
					.time-gradient-legend {
 | 
				
			||||||
 | 
					    margin-top: 0.5rem;
 | 
				
			||||||
 | 
					    padding: 0.3rem;
 | 
				
			||||||
 | 
					    background: rgba(26, 26, 26, 0.5);
 | 
				
			||||||
 | 
					    border-radius: 3px;
 | 
				
			||||||
 | 
					    border: 1px solid #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Settings Modal Specific */
 | 
					/* Settings Modal Specific */
 | 
				
			||||||
.provider-toggle {
 | 
					.provider-toggle {
 | 
				
			||||||
    appearance: none !important;
 | 
					    appearance: none !important;
 | 
				
			||||||
@ -1324,16 +1402,16 @@ input[type="password"]:focus {
 | 
				
			|||||||
    .provider-list {
 | 
					    .provider-list {
 | 
				
			||||||
        grid-template-columns: 1fr;
 | 
					        grid-template-columns: 1fr;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .graph-controls {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        top: auto;
 | 
				
			||||||
 | 
					        left: auto;
 | 
				
			||||||
 | 
					        margin-bottom: 1rem;
 | 
				
			||||||
 | 
					        min-width: auto;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
.manual-refresh-btn {
 | 
					    .time-control-input {
 | 
				
			||||||
    background: rgba(92, 76, 44, 0.9) !important; /* Orange/amber background */
 | 
					        font-size: 0.7rem;
 | 
				
			||||||
    border: 1px solid #7a6a3a !important;
 | 
					 | 
				
			||||||
    color: #ffcc00 !important; /* Bright yellow text */
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
.manual-refresh-btn:hover {
 | 
					 | 
				
			||||||
    border-color: #ffcc00 !important;
 | 
					 | 
				
			||||||
    color: #fff !important;
 | 
					 | 
				
			||||||
    background: rgba(112, 96, 54, 0.9) !important;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Graph visualization module for DNScope
 | 
					 * Graph visualization module for DNScope
 | 
				
			||||||
 * Handles network graph rendering using vis.js with proper large entity node hiding
 | 
					 * Handles network graph rendering using vis.js with proper large entity node hiding
 | 
				
			||||||
 * UPDATED: Added manual refresh button for polling optimization when graph becomes large
 | 
					 * UPDATED: Added time-based blue gradient edge coloring system
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const contextMenuCSS = `
 | 
					const contextMenuCSS = `
 | 
				
			||||||
.graph-context-menu {
 | 
					.graph-context-menu {
 | 
				
			||||||
@ -53,6 +53,44 @@ const contextMenuCSS = `
 | 
				
			|||||||
.graph-context-menu ul li:last-child {
 | 
					.graph-context-menu ul li:last-child {
 | 
				
			||||||
    border-bottom: none;
 | 
					    border-bottom: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-control-container {
 | 
				
			||||||
 | 
					    margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					    padding: 0.5rem;
 | 
				
			||||||
 | 
					    background: rgba(42, 42, 42, 0.3);
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    border: 1px solid #444;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-control-label {
 | 
				
			||||||
 | 
					    font-size: 0.8rem;
 | 
				
			||||||
 | 
					    color: #c7c7c7;
 | 
				
			||||||
 | 
					    margin-bottom: 0.3rem;
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-control-input {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 0.3rem;
 | 
				
			||||||
 | 
					    background: #1a1a1a;
 | 
				
			||||||
 | 
					    border: 1px solid #555;
 | 
				
			||||||
 | 
					    border-radius: 3px;
 | 
				
			||||||
 | 
					    color: #c7c7c7;
 | 
				
			||||||
 | 
					    font-family: 'Roboto Mono', monospace;
 | 
				
			||||||
 | 
					    font-size: 0.75rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-control-input:focus {
 | 
				
			||||||
 | 
					    outline: none;
 | 
				
			||||||
 | 
					    border-color: #00ff41;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-gradient-info {
 | 
				
			||||||
 | 
					    font-size: 0.7rem;
 | 
				
			||||||
 | 
					    color: #999;
 | 
				
			||||||
 | 
					    margin-top: 0.3rem;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GraphManager {
 | 
					class GraphManager {
 | 
				
			||||||
@ -76,6 +114,16 @@ class GraphManager {
 | 
				
			|||||||
        this.manualRefreshButton = null;
 | 
					        this.manualRefreshButton = null;
 | 
				
			||||||
        this.manualRefreshHandler = null; // Store the handler
 | 
					        this.manualRefreshHandler = null; // Store the handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Time-based gradient settings
 | 
				
			||||||
 | 
					        this.timeOfInterest = new Date(); // Default to now
 | 
				
			||||||
 | 
					        this.edgeTimestamps = new Map(); // Store edge ID -> timestamp mapping
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Gradient colors: grey-ish dark to retina-melting light blue
 | 
				
			||||||
 | 
					        this.gradientColors = {
 | 
				
			||||||
 | 
					            dark: '#6b7280',     // Grey-ish dark
 | 
				
			||||||
 | 
					            light: '#00bfff'     // Retina-melting light blue
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.options = {
 | 
					        this.options = {
 | 
				
			||||||
            nodes: {
 | 
					            nodes: {
 | 
				
			||||||
                shape: 'dot',
 | 
					                shape: 'dot',
 | 
				
			||||||
@ -257,13 +305,25 @@ class GraphManager {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Add interactive graph controls
 | 
					     * Add interactive graph controls with time of interest control
 | 
				
			||||||
     * UPDATED: Added manual refresh button for polling optimization
 | 
					     * UPDATED: Added time-based edge coloring controls
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    addGraphControls() {
 | 
					    addGraphControls() {
 | 
				
			||||||
        const controlsContainer = document.createElement('div');
 | 
					        const controlsContainer = document.createElement('div');
 | 
				
			||||||
        controlsContainer.className = 'graph-controls';
 | 
					        controlsContainer.className = 'graph-controls';
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Format current date/time for the input
 | 
				
			||||||
 | 
					        const currentDateTime = this.formatDateTimeForInput(this.timeOfInterest);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        controlsContainer.innerHTML = `
 | 
					        controlsContainer.innerHTML = `
 | 
				
			||||||
 | 
					            <div class="time-control-container">
 | 
				
			||||||
 | 
					                <label class="time-control-label">Time of Interest (for edge coloring)</label>
 | 
				
			||||||
 | 
					                <input type="datetime-local" id="time-of-interest" class="time-control-input" 
 | 
				
			||||||
 | 
					                       value="${currentDateTime}" title="Reference time for edge color gradient">
 | 
				
			||||||
 | 
					                <div class="time-gradient-info">
 | 
				
			||||||
 | 
					                    Dark: Old data | Light Blue: Recent data
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
            <button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
 | 
					            <button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
 | 
				
			||||||
            <button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
 | 
					            <button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
 | 
				
			||||||
            <button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
 | 
					            <button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
 | 
				
			||||||
@ -283,6 +343,13 @@ class GraphManager {
 | 
				
			|||||||
        document.getElementById('graph-unhide').addEventListener('click', () => this.unhideAll());
 | 
					        document.getElementById('graph-unhide').addEventListener('click', () => this.unhideAll());
 | 
				
			||||||
        document.getElementById('graph-revert').addEventListener('click', () => this.revertLastAction());
 | 
					        document.getElementById('graph-revert').addEventListener('click', () => this.revertLastAction());
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					        // Time of interest control
 | 
				
			||||||
 | 
					        document.getElementById('time-of-interest').addEventListener('change', (e) => {
 | 
				
			||||||
 | 
					            this.timeOfInterest = new Date(e.target.value);
 | 
				
			||||||
 | 
					            console.log('Time of interest updated:', this.timeOfInterest);
 | 
				
			||||||
 | 
					            this.updateEdgeColors();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        // Manual refresh button - handler will be set by main app
 | 
					        // Manual refresh button - handler will be set by main app
 | 
				
			||||||
        this.manualRefreshButton = document.getElementById('graph-manual-refresh');
 | 
					        this.manualRefreshButton = document.getElementById('graph-manual-refresh');
 | 
				
			||||||
        // If a handler was set before the button existed, attach it now
 | 
					        // If a handler was set before the button existed, attach it now
 | 
				
			||||||
@ -291,6 +358,150 @@ class GraphManager {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Format date for datetime-local input
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    formatDateTimeForInput(date) {
 | 
				
			||||||
 | 
					        const year = date.getFullYear();
 | 
				
			||||||
 | 
					        const month = String(date.getMonth() + 1).padStart(2, '0');
 | 
				
			||||||
 | 
					        const day = String(date.getDate()).padStart(2, '0');
 | 
				
			||||||
 | 
					        const hours = String(date.getHours()).padStart(2, '0');
 | 
				
			||||||
 | 
					        const minutes = String(date.getMinutes()).padStart(2, '0');
 | 
				
			||||||
 | 
					        return `${year}-${month}-${day}T${hours}:${minutes}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Extract relevant timestamp from edge raw_data based on provider
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    extractEdgeTimestamp(edge) {
 | 
				
			||||||
 | 
					        const rawData = edge.raw_data || {};
 | 
				
			||||||
 | 
					        const provider = edge.source_provider || '';
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check for standardized relevance_timestamp first
 | 
				
			||||||
 | 
					        if (rawData.relevance_timestamp) {
 | 
				
			||||||
 | 
					            return new Date(rawData.relevance_timestamp);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Provider-specific timestamp extraction
 | 
				
			||||||
 | 
					        switch (provider.toLowerCase()) {
 | 
				
			||||||
 | 
					            case 'shodan':
 | 
				
			||||||
 | 
					                // Use last_seen timestamp for Shodan
 | 
				
			||||||
 | 
					                if (rawData.last_seen) {
 | 
				
			||||||
 | 
					                    return new Date(rawData.last_seen);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            case 'crtsh':
 | 
				
			||||||
 | 
					                // Use certificate issue date (not_before) for certificates
 | 
				
			||||||
 | 
					                if (rawData.cert_not_before) {
 | 
				
			||||||
 | 
					                    return new Date(rawData.cert_not_before);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            case 'dns':
 | 
				
			||||||
 | 
					            case 'correlation':
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                // Use discovery timestamp for DNS and correlation
 | 
				
			||||||
 | 
					                if (edge.discovery_timestamp) {
 | 
				
			||||||
 | 
					                    return new Date(edge.discovery_timestamp);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Fallback to discovery timestamp or current time
 | 
				
			||||||
 | 
					        if (edge.discovery_timestamp) {
 | 
				
			||||||
 | 
					            return new Date(edge.discovery_timestamp);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return new Date(); // Default to now if no timestamp available
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Calculate time-based blue gradient color
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    calculateTimeGradientColor(timestamp) {
 | 
				
			||||||
 | 
					        if (!timestamp || !this.timeOfInterest) {
 | 
				
			||||||
 | 
					            return this.gradientColors.dark; // Default to dark grey
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Calculate time difference in milliseconds
 | 
				
			||||||
 | 
					        const timeDiff = Math.abs(timestamp.getTime() - this.timeOfInterest.getTime());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Find maximum time difference across all edges for normalization
 | 
				
			||||||
 | 
					        let maxTimeDiff = 0;
 | 
				
			||||||
 | 
					        this.edgeTimestamps.forEach((edgeTimestamp) => {
 | 
				
			||||||
 | 
					            const diff = Math.abs(edgeTimestamp.getTime() - this.timeOfInterest.getTime());
 | 
				
			||||||
 | 
					            if (diff > maxTimeDiff) {
 | 
				
			||||||
 | 
					                maxTimeDiff = diff;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (maxTimeDiff === 0) {
 | 
				
			||||||
 | 
					            return this.gradientColors.light; // All timestamps are the same
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Calculate gradient position (0 = closest to time of interest, 1 = furthest)
 | 
				
			||||||
 | 
					        const gradientPosition = timeDiff / maxTimeDiff;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Interpolate between light blue (close) and dark grey (far)
 | 
				
			||||||
 | 
					        return this.interpolateColor(
 | 
				
			||||||
 | 
					            this.gradientColors.light,  // Close to time of interest
 | 
				
			||||||
 | 
					            this.gradientColors.dark,   // Far from time of interest
 | 
				
			||||||
 | 
					            gradientPosition
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Interpolate between two hex colors
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    interpolateColor(color1, color2, factor) {
 | 
				
			||||||
 | 
					        // Parse hex colors
 | 
				
			||||||
 | 
					        const hex1 = color1.replace('#', '');
 | 
				
			||||||
 | 
					        const hex2 = color2.replace('#', '');
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const r1 = parseInt(hex1.substring(0, 2), 16);
 | 
				
			||||||
 | 
					        const g1 = parseInt(hex1.substring(2, 4), 16);
 | 
				
			||||||
 | 
					        const b1 = parseInt(hex1.substring(4, 6), 16);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const r2 = parseInt(hex2.substring(0, 2), 16);
 | 
				
			||||||
 | 
					        const g2 = parseInt(hex2.substring(2, 4), 16);
 | 
				
			||||||
 | 
					        const b2 = parseInt(hex2.substring(4, 6), 16);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Interpolate
 | 
				
			||||||
 | 
					        const r = Math.round(r1 + (r2 - r1) * factor);
 | 
				
			||||||
 | 
					        const g = Math.round(g1 + (g2 - g1) * factor);
 | 
				
			||||||
 | 
					        const b = Math.round(b1 + (b2 - b1) * factor);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Convert back to hex
 | 
				
			||||||
 | 
					        return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update all edge colors based on current time of interest
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    updateEdgeColors() {
 | 
				
			||||||
 | 
					        const edgeUpdates = [];
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        this.edges.forEach((edge) => {
 | 
				
			||||||
 | 
					            const timestamp = this.edgeTimestamps.get(edge.id);
 | 
				
			||||||
 | 
					            const color = this.calculateTimeGradientColor(timestamp);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            edgeUpdates.push({
 | 
				
			||||||
 | 
					                id: edge.id,
 | 
				
			||||||
 | 
					                color: {
 | 
				
			||||||
 | 
					                    color: color,
 | 
				
			||||||
 | 
					                    highlight: '#00ff41',
 | 
				
			||||||
 | 
					                    hover: '#ff9900'
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (edgeUpdates.length > 0) {
 | 
				
			||||||
 | 
					            this.edges.update(edgeUpdates);
 | 
				
			||||||
 | 
					            console.log(`Updated ${edgeUpdates.length} edge colors based on time gradient`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Set the manual refresh button click handler
 | 
					     * Set the manual refresh button click handler
 | 
				
			||||||
     * @param {Function} handler - Function to call when manual refresh is clicked
 | 
					     * @param {Function} handler - Function to call when manual refresh is clicked
 | 
				
			||||||
@ -411,6 +622,7 @@ class GraphManager {
 | 
				
			|||||||
            if (!hasData) {
 | 
					            if (!hasData) {
 | 
				
			||||||
                this.nodes.clear();
 | 
					                this.nodes.clear();
 | 
				
			||||||
                this.edges.clear();
 | 
					                this.edges.clear();
 | 
				
			||||||
 | 
					                this.edgeTimestamps.clear();
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -464,6 +676,9 @@ class GraphManager {
 | 
				
			|||||||
            this.nodes.update(processedNodes);
 | 
					            this.nodes.update(processedNodes);
 | 
				
			||||||
            this.edges.update(processedEdges);
 | 
					            this.edges.update(processedEdges);
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
 | 
					            // Update edge timestamps and colors for time-based gradient
 | 
				
			||||||
 | 
					            this.updateEdgeTimestampsAndColors(graphData.edges);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            this.updateFilterControls();
 | 
					            this.updateFilterControls();
 | 
				
			||||||
            this.applyAllFilters();
 | 
					            this.applyAllFilters();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -481,6 +696,21 @@ class GraphManager {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update edge timestamps and apply time-based gradient colors
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    updateEdgeTimestampsAndColors(edgeData) {
 | 
				
			||||||
 | 
					        // Extract timestamps from raw edge data
 | 
				
			||||||
 | 
					        edgeData.forEach(edge => {
 | 
				
			||||||
 | 
					            const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
 | 
				
			||||||
 | 
					            const timestamp = this.extractEdgeTimestamp(edge);
 | 
				
			||||||
 | 
					            this.edgeTimestamps.set(edgeId, timestamp);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Update edge colors based on new timestamps
 | 
				
			||||||
 | 
					        this.updateEdgeColors();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    analyzeCertificateInfo(attributes) {
 | 
					    analyzeCertificateInfo(attributes) {
 | 
				
			||||||
        let hasCertificates = false;
 | 
					        let hasCertificates = false;
 | 
				
			||||||
        let hasValidCertificates = false;
 | 
					        let hasValidCertificates = false;
 | 
				
			||||||
@ -559,12 +789,6 @@ class GraphManager {
 | 
				
			|||||||
            processedNode.borderColor = '#ff0000'; // Red border for max depth
 | 
					            processedNode.borderColor = '#ff0000'; // Red border for max depth
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Add confidence-based styling
 | 
					 | 
				
			||||||
        if (node.confidence) {
 | 
					 | 
				
			||||||
            processedNode.borderWidth = Math.max(2, Math.floor(node.confidence * 5));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // FIXED: Certificate-based domain coloring
 | 
					        // FIXED: Certificate-based domain coloring
 | 
				
			||||||
        if (node.type === 'domain' && Array.isArray(node.attributes)) {
 | 
					        if (node.type === 'domain' && Array.isArray(node.attributes)) {
 | 
				
			||||||
            const certInfo = this.analyzeCertificateInfo(node.attributes);
 | 
					            const certInfo = this.analyzeCertificateInfo(node.attributes);
 | 
				
			||||||
@ -595,24 +819,33 @@ class GraphManager {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Process edge data with styling and metadata
 | 
					     * Process edge data with styling, metadata, and time-based gradient colors
 | 
				
			||||||
     * @param {Object} edge - Raw edge data
 | 
					     * @param {Object} edge - Raw edge data
 | 
				
			||||||
     * @returns {Object} Processed edge data
 | 
					     * @returns {Object} Processed edge data
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    processEdge(edge) {
 | 
					    processEdge(edge) {
 | 
				
			||||||
        const confidence = edge.confidence_score || 0;
 | 
					        const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Extract timestamp for this edge
 | 
				
			||||||
 | 
					        const timestamp = this.extractEdgeTimestamp(edge);
 | 
				
			||||||
 | 
					        this.edgeTimestamps.set(edgeId, timestamp);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Calculate time-based gradient color
 | 
				
			||||||
 | 
					        const timeGradientColor = this.calculateTimeGradientColor(timestamp);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        const processedEdge = {
 | 
					        const processedEdge = {
 | 
				
			||||||
            id: `${edge.from}-${edge.to}-${edge.label}`,
 | 
					            id: edgeId,
 | 
				
			||||||
            from: edge.from,
 | 
					            from: edge.from,
 | 
				
			||||||
            to: edge.to,
 | 
					            to: edge.to,
 | 
				
			||||||
            label: this.formatEdgeLabel(edge.label, confidence),
 | 
					            label: edge.label, // Correctly access the label directly
 | 
				
			||||||
            title: this.createEdgeTooltip(edge),
 | 
					            title: this.createEdgeTooltip(edge),
 | 
				
			||||||
            width: this.getEdgeWidth(confidence),
 | 
					            color: {
 | 
				
			||||||
            color: this.getEdgeColor(confidence),
 | 
					                color: timeGradientColor,
 | 
				
			||||||
            dashes: confidence < 0.6 ? [5, 5] : false,
 | 
					                highlight: '#00ff41',
 | 
				
			||||||
 | 
					                hover: '#ff9900'
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            metadata: {
 | 
					            metadata: {
 | 
				
			||||||
                relationship_type: edge.label,
 | 
					                relationship_type: edge.label,
 | 
				
			||||||
                confidence_score: confidence,
 | 
					 | 
				
			||||||
                source_provider: edge.source_provider,
 | 
					                source_provider: edge.source_provider,
 | 
				
			||||||
                discovery_timestamp: edge.discovery_timestamp
 | 
					                discovery_timestamp: edge.discovery_timestamp
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -635,18 +868,7 @@ class GraphManager {
 | 
				
			|||||||
        return nodeId;
 | 
					        return nodeId;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Format edge label for display
 | 
					 | 
				
			||||||
     * @param {string} relationshipType - Type of relationship
 | 
					 | 
				
			||||||
     * @param {number} confidence - Confidence score
 | 
					 | 
				
			||||||
     * @returns {string} Formatted label
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    formatEdgeLabel(relationshipType, confidence) {
 | 
					 | 
				
			||||||
        if (!relationshipType) return '';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '●' : '○';
 | 
					 | 
				
			||||||
        return `${relationshipType} ${confidenceText}`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get node color based on type
 | 
					     * Get node color based on type
 | 
				
			||||||
@ -716,44 +938,13 @@ class GraphManager {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get edge color based on confidence
 | 
					     * Create edge tooltip with correct provider information and timestamp
 | 
				
			||||||
     * @param {number} confidence - Confidence score
 | 
					 | 
				
			||||||
     * @returns {string} Edge color
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    getEdgeColor(confidence) {
 | 
					 | 
				
			||||||
        if (confidence >= 0.8) {
 | 
					 | 
				
			||||||
            return '#00ff41'; // High confidence - green
 | 
					 | 
				
			||||||
        } else if (confidence >= 0.6) {
 | 
					 | 
				
			||||||
            return '#ff9900'; // Medium confidence - amber
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            return '#666666'; // Low confidence - gray
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Get edge width based on confidence
 | 
					 | 
				
			||||||
     * @param {number} confidence - Confidence score
 | 
					 | 
				
			||||||
     * @returns {number} Edge width
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    getEdgeWidth(confidence) {
 | 
					 | 
				
			||||||
        if (confidence >= 0.8) {
 | 
					 | 
				
			||||||
            return 3;
 | 
					 | 
				
			||||||
        } else if (confidence >= 0.6) {
 | 
					 | 
				
			||||||
            return 2;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            return 1;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Create edge tooltip with correct provider information
 | 
					 | 
				
			||||||
     * @param {Object} edge - Edge data
 | 
					     * @param {Object} edge - Edge data
 | 
				
			||||||
     * @returns {string} HTML tooltip content
 | 
					     * @returns {string} HTML tooltip content
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    createEdgeTooltip(edge) {
 | 
					    createEdgeTooltip(edge) {
 | 
				
			||||||
        let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
 | 
					        let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
 | 
				
			||||||
        tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
 | 
					        tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
 | 
				
			||||||
        tooltip += `<div style="color: #999; margin-bottom: 2px;">Confidence: ${(edge.confidence_score * 100).toFixed(1)}%</div>`;
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if (edge.source_provider) {
 | 
					        if (edge.source_provider) {
 | 
				
			||||||
            tooltip += `<div style="color: #999; margin-bottom: 2px;">Provider: ${edge.source_provider}</div>`;
 | 
					            tooltip += `<div style="color: #999; margin-bottom: 2px;">Provider: ${edge.source_provider}</div>`;
 | 
				
			||||||
@ -764,6 +955,13 @@ class GraphManager {
 | 
				
			|||||||
            tooltip += `<div style="color: #666; font-size: 10px;">Discovered: ${date.toLocaleString()}</div>`;
 | 
					            tooltip += `<div style="color: #666; font-size: 10px;">Discovered: ${date.toLocaleString()}</div>`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					        // Add timestamp information for time-based coloring
 | 
				
			||||||
 | 
					        const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
 | 
				
			||||||
 | 
					        const timestamp = this.edgeTimestamps.get(edgeId);
 | 
				
			||||||
 | 
					        if (timestamp) {
 | 
				
			||||||
 | 
					            tooltip += `<div style="color: #888; font-size: 10px;">Data from: ${timestamp.toLocaleString()}</div>`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        tooltip += `</div>`;
 | 
					        tooltip += `</div>`;
 | 
				
			||||||
        return tooltip;
 | 
					        return tooltip;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -893,13 +1091,17 @@ class GraphManager {
 | 
				
			|||||||
                };
 | 
					                };
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // Reset highlighted edges
 | 
					            // Reset highlighted edges to time-based colors
 | 
				
			||||||
            const edgeUpdates = this.highlightedElements.edges.map(id => {
 | 
					            const edgeUpdates = this.highlightedElements.edges.map(id => {
 | 
				
			||||||
                const originalEdge = this.edges.get(id);
 | 
					                const timestamp = this.edgeTimestamps.get(id);
 | 
				
			||||||
 | 
					                const color = this.calculateTimeGradientColor(timestamp);
 | 
				
			||||||
                return {
 | 
					                return {
 | 
				
			||||||
                    id: id,
 | 
					                    id: id,
 | 
				
			||||||
                    color: this.getEdgeColor(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5),
 | 
					                    color: {
 | 
				
			||||||
                    width: this.getEdgeWidth(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5)
 | 
					                        color: color,
 | 
				
			||||||
 | 
					                        highlight: '#00ff41',
 | 
				
			||||||
 | 
					                        hover: '#ff9900'
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
@ -955,11 +1157,19 @@ class GraphManager {
 | 
				
			|||||||
                borderWidth: 2,
 | 
					                borderWidth: 2,
 | 
				
			||||||
            }));
 | 
					            }));
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            const edgeResets = newEdges.map(edge => ({
 | 
					            // Reset edges to time-based colors
 | 
				
			||||||
 | 
					            const edgeResets = newEdges.map(edge => {
 | 
				
			||||||
 | 
					                const timestamp = this.edgeTimestamps.get(edge.id);
 | 
				
			||||||
 | 
					                const color = this.calculateTimeGradientColor(timestamp);
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
                    id: edge.id,
 | 
					                    id: edge.id,
 | 
				
			||||||
                color: this.getEdgeColor(edge.metadata ? edge.metadata.confidence_score : 0.5),
 | 
					                    color: {
 | 
				
			||||||
                width: this.getEdgeWidth(edge.metadata ? edge.metadata.confidence_score : 0.5)
 | 
					                        color: color,
 | 
				
			||||||
            }));
 | 
					                        highlight: '#00ff41',
 | 
				
			||||||
 | 
					                        hover: '#ff9900'
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            this.nodes.update(nodeResets);
 | 
					            this.nodes.update(nodeResets);
 | 
				
			||||||
            this.edges.update(edgeResets);
 | 
					            this.edges.update(edgeResets);
 | 
				
			||||||
@ -1048,6 +1258,7 @@ class GraphManager {
 | 
				
			|||||||
    clear() {
 | 
					    clear() {
 | 
				
			||||||
        this.nodes.clear();
 | 
					        this.nodes.clear();
 | 
				
			||||||
        this.edges.clear();
 | 
					        this.edges.clear();
 | 
				
			||||||
 | 
					        this.edgeTimestamps.clear();
 | 
				
			||||||
        this.history = [];
 | 
					        this.history = [];
 | 
				
			||||||
        this.largeEntityMembers.clear();
 | 
					        this.largeEntityMembers.clear();
 | 
				
			||||||
        this.initialTargetIds.clear();
 | 
					        this.initialTargetIds.clear();
 | 
				
			||||||
 | 
				
			|||||||
@ -1722,17 +1722,9 @@ class DNScopeApp {
 | 
				
			|||||||
        return groups;
 | 
					        return groups;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    formatEdgeLabel(relationshipType, confidence) {
 | 
					 | 
				
			||||||
        if (!relationshipType) return '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
 | 
					 | 
				
			||||||
        return `${relationshipType} ${confidenceText}`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    createEdgeTooltip(edge) {
 | 
					    createEdgeTooltip(edge) {
 | 
				
			||||||
        let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
 | 
					        let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
 | 
				
			||||||
        tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
 | 
					        tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
 | 
				
			||||||
        tooltip += `<div style="color: #999; margin-bottom: 2px;">Confidence: ${(edge.confidence_score * 100).toFixed(1)}%</div>`;
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // UPDATED: Use raw provider name (no formatting)
 | 
					        // UPDATED: Use raw provider name (no formatting)
 | 
				
			||||||
        if (edge.source_provider) {
 | 
					        if (edge.source_provider) {
 | 
				
			||||||
@ -1872,7 +1864,7 @@ class DNScopeApp {
 | 
				
			|||||||
                html += `
 | 
					                html += `
 | 
				
			||||||
                    <div class="relationship-compact-item">
 | 
					                    <div class="relationship-compact-item">
 | 
				
			||||||
                        <span class="node-link-compact" data-node-id="${innerNodeId}">${innerNodeId}</span>
 | 
					                        <span class="node-link-compact" data-node-id="${innerNodeId}">${innerNodeId}</span>
 | 
				
			||||||
                        <button class="btn-icon-small extract-node-btn" 
 | 
					                        <button class="graph-control-btn extract-node-btn" 
 | 
				
			||||||
                                title="Extract to graph"
 | 
					                                title="Extract to graph"
 | 
				
			||||||
                                data-large-entity-id="${largeEntityId}" 
 | 
					                                data-large-entity-id="${largeEntityId}" 
 | 
				
			||||||
                                data-node-id="${innerNodeId}">[+]</button>
 | 
					                                data-node-id="${innerNodeId}">[+]</button>
 | 
				
			||||||
@ -1899,8 +1891,6 @@ class DNScopeApp {
 | 
				
			|||||||
            `;
 | 
					            `;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            node.incoming_edges.forEach(edge => {
 | 
					            node.incoming_edges.forEach(edge => {
 | 
				
			||||||
                const confidence = edge.data.confidence_score || 0;
 | 
					 | 
				
			||||||
                const confidenceClass = confidence >= 0.8 ? 'high' : confidence >= 0.6 ? 'medium' : 'low';
 | 
					 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                html += `
 | 
					                html += `
 | 
				
			||||||
                    <div class="relationship-item">
 | 
					                    <div class="relationship-item">
 | 
				
			||||||
@ -1909,9 +1899,6 @@ class DNScopeApp {
 | 
				
			|||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                        <div class="relationship-type">
 | 
					                        <div class="relationship-type">
 | 
				
			||||||
                            <span class="relation-label">${edge.data.relationship_type}</span>
 | 
					                            <span class="relation-label">${edge.data.relationship_type}</span>
 | 
				
			||||||
                            <span class="confidence-indicator confidence-${confidenceClass}" title="Confidence: ${(confidence * 100).toFixed(1)}%">
 | 
					 | 
				
			||||||
                                ${'●'.repeat(Math.ceil(confidence * 3))}
 | 
					 | 
				
			||||||
                            </span>
 | 
					 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                `;
 | 
					                `;
 | 
				
			||||||
@ -1930,9 +1917,6 @@ class DNScopeApp {
 | 
				
			|||||||
            `;
 | 
					            `;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            node.outgoing_edges.forEach(edge => {
 | 
					            node.outgoing_edges.forEach(edge => {
 | 
				
			||||||
                const confidence = edge.data.confidence_score || 0;
 | 
					 | 
				
			||||||
                const confidenceClass = confidence >= 0.8 ? 'high' : confidence >= 0.6 ? 'medium' : 'low';
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                html += `
 | 
					                html += `
 | 
				
			||||||
                    <div class="relationship-item">
 | 
					                    <div class="relationship-item">
 | 
				
			||||||
                        <div class="relationship-target node-link" data-node-id="${edge.to}">
 | 
					                        <div class="relationship-target node-link" data-node-id="${edge.to}">
 | 
				
			||||||
@ -1940,9 +1924,6 @@ class DNScopeApp {
 | 
				
			|||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                        <div class="relationship-type">
 | 
					                        <div class="relationship-type">
 | 
				
			||||||
                            <span class="relation-label">${edge.data.relationship_type}</span>
 | 
					                            <span class="relation-label">${edge.data.relationship_type}</span>
 | 
				
			||||||
                            <span class="confidence-indicator confidence-${confidenceClass}" title="Confidence: ${(confidence * 100).toFixed(1)}%">
 | 
					 | 
				
			||||||
                                ${'●'.repeat(Math.ceil(confidence * 3))}
 | 
					 | 
				
			||||||
                            </span>
 | 
					 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                `;
 | 
					                `;
 | 
				
			||||||
 | 
				
			|||||||
@ -188,7 +188,6 @@ class ExportManager:
 | 
				
			|||||||
                    f"  - Type: {domain_info['classification']}",
 | 
					                    f"  - Type: {domain_info['classification']}",
 | 
				
			||||||
                    f"  - Connected IPs: {len(domain_info['ips'])}",
 | 
					                    f"  - Connected IPs: {len(domain_info['ips'])}",
 | 
				
			||||||
                    f"  - Certificate Status: {domain_info['cert_status']}",
 | 
					                    f"  - Certificate Status: {domain_info['cert_status']}",
 | 
				
			||||||
                    f"  - Relationship Confidence: {domain_info['avg_confidence']:.2f}",
 | 
					 | 
				
			||||||
                ])
 | 
					                ])
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                if domain_info['security_notes']:
 | 
					                if domain_info['security_notes']:
 | 
				
			||||||
@ -247,11 +246,9 @@ class ExportManager:
 | 
				
			|||||||
            ])
 | 
					            ])
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            for rel in key_relationships[:8]:  # Top 8 relationships
 | 
					            for rel in key_relationships[:8]:  # Top 8 relationships
 | 
				
			||||||
                confidence_desc = self._describe_confidence(rel['confidence'])
 | 
					 | 
				
			||||||
                report.extend([
 | 
					                report.extend([
 | 
				
			||||||
                    f"• {rel['source']} → {rel['target']}",
 | 
					                    f"• {rel['source']} → {rel['target']}",
 | 
				
			||||||
                    f"  - Relationship: {self._humanize_relationship_type(rel['type'])}",
 | 
					                    f"  - Relationship: {self._humanize_relationship_type(rel['type'])}",
 | 
				
			||||||
                    f"  - Evidence Strength: {confidence_desc} ({rel['confidence']:.2f})",
 | 
					 | 
				
			||||||
                    f"  - Discovery Method: {rel['provider']}",
 | 
					                    f"  - Discovery Method: {rel['provider']}",
 | 
				
			||||||
                    ""
 | 
					                    ""
 | 
				
			||||||
                ])
 | 
					                ])
 | 
				
			||||||
@ -291,15 +288,6 @@ class ExportManager:
 | 
				
			|||||||
            "Data Quality Assessment:",
 | 
					            "Data Quality Assessment:",
 | 
				
			||||||
            f"• Total API Requests: {audit_trail.get('session_metadata', {}).get('total_requests', 0)}",
 | 
					            f"• Total API Requests: {audit_trail.get('session_metadata', {}).get('total_requests', 0)}",
 | 
				
			||||||
            f"• Data Providers Used: {len(audit_trail.get('session_metadata', {}).get('providers_used', []))}",
 | 
					            f"• Data Providers Used: {len(audit_trail.get('session_metadata', {}).get('providers_used', []))}",
 | 
				
			||||||
            f"• Relationship Confidence Distribution:",
 | 
					 | 
				
			||||||
        ])
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        # Confidence distribution
 | 
					 | 
				
			||||||
        confidence_dist = self._calculate_confidence_distribution(edges)
 | 
					 | 
				
			||||||
        for level, count in confidence_dist.items():
 | 
					 | 
				
			||||||
            percentage = (count / len(edges) * 100) if edges else 0
 | 
					 | 
				
			||||||
            report.extend([
 | 
					 | 
				
			||||||
                f"  - {level.title()} Confidence (≥{self._get_confidence_threshold(level)}): {count} ({percentage:.1f}%)",
 | 
					 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        report.extend([
 | 
					        report.extend([
 | 
				
			||||||
@ -375,9 +363,7 @@ class ExportManager:
 | 
				
			|||||||
            if len(connected_ips) > 5:
 | 
					            if len(connected_ips) > 5:
 | 
				
			||||||
                security_notes.append("Multiple IP endpoints")
 | 
					                security_notes.append("Multiple IP endpoints")
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            # Average confidence
 | 
					 | 
				
			||||||
            domain_edges = [e for e in edges if e['from'] == domain['id']]
 | 
					            domain_edges = [e for e in edges if e['from'] == domain['id']]
 | 
				
			||||||
            avg_confidence = sum(e['confidence_score'] for e in domain_edges) / len(domain_edges) if domain_edges else 0
 | 
					 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            domain_analysis.append({
 | 
					            domain_analysis.append({
 | 
				
			||||||
                'domain': domain['id'],
 | 
					                'domain': domain['id'],
 | 
				
			||||||
@ -385,7 +371,6 @@ class ExportManager:
 | 
				
			|||||||
                'ips': connected_ips,
 | 
					                'ips': connected_ips,
 | 
				
			||||||
                'cert_status': cert_status,
 | 
					                'cert_status': cert_status,
 | 
				
			||||||
                'security_notes': security_notes,
 | 
					                'security_notes': security_notes,
 | 
				
			||||||
                'avg_confidence': avg_confidence
 | 
					 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Sort by number of connections (most connected first)
 | 
					        # Sort by number of connections (most connected first)
 | 
				
			||||||
@ -480,7 +465,7 @@ class ExportManager:
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    def _identify_key_relationships(self, edges: List[Dict]) -> List[Dict[str, Any]]:
 | 
					    def _identify_key_relationships(self, edges: List[Dict]) -> List[Dict[str, Any]]:
 | 
				
			||||||
        """Identify the most significant relationships in the infrastructure."""
 | 
					        """Identify the most significant relationships in the infrastructure."""
 | 
				
			||||||
        # Score relationships by confidence and type importance
 | 
					        # Score relationships by type importance
 | 
				
			||||||
        relationship_importance = {
 | 
					        relationship_importance = {
 | 
				
			||||||
            'dns_a_record': 0.9,
 | 
					            'dns_a_record': 0.9,
 | 
				
			||||||
            'dns_aaaa_record': 0.9,
 | 
					            'dns_aaaa_record': 0.9,
 | 
				
			||||||
@ -493,15 +478,12 @@ class ExportManager:
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
        scored_edges = []
 | 
					        scored_edges = []
 | 
				
			||||||
        for edge in edges:
 | 
					        for edge in edges:
 | 
				
			||||||
            base_confidence = edge.get('confidence_score', 0)
 | 
					 | 
				
			||||||
            type_weight = relationship_importance.get(edge.get('label', ''), 0.5)
 | 
					            type_weight = relationship_importance.get(edge.get('label', ''), 0.5)
 | 
				
			||||||
            combined_score = (base_confidence * 0.7) + (type_weight * 0.3)
 | 
					 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            scored_edges.append({
 | 
					            scored_edges.append({
 | 
				
			||||||
                'source': edge['from'],
 | 
					                'source': edge['from'],
 | 
				
			||||||
                'target': edge['to'],
 | 
					                'target': edge['to'],
 | 
				
			||||||
                'type': edge.get('label', ''),
 | 
					                'type': edge.get('label', ''),
 | 
				
			||||||
                'confidence': base_confidence,
 | 
					 | 
				
			||||||
                'provider': edge.get('source_provider', ''),
 | 
					                'provider': edge.get('source_provider', ''),
 | 
				
			||||||
                'score': combined_score
 | 
					                'score': combined_score
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
@ -570,19 +552,6 @@ class ExportManager:
 | 
				
			|||||||
        else:
 | 
					        else:
 | 
				
			||||||
            return "Mixed Status"
 | 
					            return "Mixed Status"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def _describe_confidence(self, confidence: float) -> str:
 | 
					 | 
				
			||||||
        """Convert confidence score to descriptive text."""
 | 
					 | 
				
			||||||
        if confidence >= 0.9:
 | 
					 | 
				
			||||||
            return "Very High"
 | 
					 | 
				
			||||||
        elif confidence >= 0.8:
 | 
					 | 
				
			||||||
            return "High"
 | 
					 | 
				
			||||||
        elif confidence >= 0.6:
 | 
					 | 
				
			||||||
            return "Medium"
 | 
					 | 
				
			||||||
        elif confidence >= 0.4:
 | 
					 | 
				
			||||||
            return "Low"
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return "Very Low"
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    def _humanize_relationship_type(self, rel_type: str) -> str:
 | 
					    def _humanize_relationship_type(self, rel_type: str) -> str:
 | 
				
			||||||
        """Convert technical relationship types to human-readable descriptions."""
 | 
					        """Convert technical relationship types to human-readable descriptions."""
 | 
				
			||||||
        type_map = {
 | 
					        type_map = {
 | 
				
			||||||
@ -599,26 +568,6 @@ class ExportManager:
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return type_map.get(rel_type, rel_type.replace('_', ' ').title())
 | 
					        return type_map.get(rel_type, rel_type.replace('_', ' ').title())
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def _calculate_confidence_distribution(self, edges: List[Dict]) -> Dict[str, int]:
 | 
					 | 
				
			||||||
        """Calculate confidence score distribution."""
 | 
					 | 
				
			||||||
        distribution = {'high': 0, 'medium': 0, 'low': 0}
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        for edge in edges:
 | 
					 | 
				
			||||||
            confidence = edge.get('confidence_score', 0)
 | 
					 | 
				
			||||||
            if confidence >= 0.8:
 | 
					 | 
				
			||||||
                distribution['high'] += 1
 | 
					 | 
				
			||||||
            elif confidence >= 0.6:
 | 
					 | 
				
			||||||
                distribution['medium'] += 1
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                distribution['low'] += 1
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        return distribution
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    def _get_confidence_threshold(self, level: str) -> str:
 | 
					 | 
				
			||||||
        """Get confidence threshold for a level."""
 | 
					 | 
				
			||||||
        thresholds = {'high': '0.80', 'medium': '0.60', 'low': '0.00'}
 | 
					 | 
				
			||||||
        return thresholds.get(level, '0.00')
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    def _count_cross_validated_relationships(self, edges: List[Dict]) -> int:
 | 
					    def _count_cross_validated_relationships(self, edges: List[Dict]) -> int:
 | 
				
			||||||
        """Count relationships verified by multiple providers."""
 | 
					        """Count relationships verified by multiple providers."""
 | 
				
			||||||
        # Group edges by source-target pair
 | 
					        # Group edges by source-target pair
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user