data model refinement
This commit is contained in:
		
							parent
							
								
									930fdca500
								
							
						
					
					
						commit
						2974312278
					
				@ -152,21 +152,31 @@ class GraphManager:
 | 
			
		||||
                })
 | 
			
		||||
        return all_correlations
 | 
			
		||||
 | 
			
		||||
    def add_node(self, node_id: str, node_type: NodeType, metadata: Optional[Dict[str, Any]] = None) -> bool:
 | 
			
		||||
        """Add a node to the graph, update metadata, and process correlations."""
 | 
			
		||||
    def add_node(self, node_id: str, node_type: NodeType, attributes: Optional[Dict[str, Any]] = None, 
 | 
			
		||||
                 description: str = "", metadata: Optional[Dict[str, Any]] = None) -> bool:
 | 
			
		||||
        """Add a node to the graph, update attributes, and process correlations."""
 | 
			
		||||
        is_new_node = not self.graph.has_node(node_id)
 | 
			
		||||
        if is_new_node:
 | 
			
		||||
            self.graph.add_node(node_id, type=node_type.value,
 | 
			
		||||
                                added_timestamp=datetime.now(timezone.utc).isoformat(),
 | 
			
		||||
                                attributes=attributes or {},
 | 
			
		||||
                                description=description,
 | 
			
		||||
                                metadata=metadata or {})
 | 
			
		||||
        elif metadata:
 | 
			
		||||
            # Safely merge new metadata into existing metadata
 | 
			
		||||
            existing_metadata = self.graph.nodes[node_id].get('metadata', {})
 | 
			
		||||
            existing_metadata.update(metadata)
 | 
			
		||||
            self.graph.nodes[node_id]['metadata'] = existing_metadata
 | 
			
		||||
        else:
 | 
			
		||||
            # Safely merge new attributes into existing attributes
 | 
			
		||||
            if attributes:
 | 
			
		||||
                existing_attributes = self.graph.nodes[node_id].get('attributes', {})
 | 
			
		||||
                existing_attributes.update(attributes)
 | 
			
		||||
                self.graph.nodes[node_id]['attributes'] = existing_attributes
 | 
			
		||||
            if description:
 | 
			
		||||
                self.graph.nodes[node_id]['description'] = description
 | 
			
		||||
            if metadata:
 | 
			
		||||
                existing_metadata = self.graph.nodes[node_id].get('metadata', {})
 | 
			
		||||
                existing_metadata.update(metadata)
 | 
			
		||||
                self.graph.nodes[node_id]['metadata'] = existing_metadata
 | 
			
		||||
 | 
			
		||||
        if metadata and node_type != NodeType.CORRELATION_OBJECT:
 | 
			
		||||
            correlations = self._check_for_correlations(node_id, metadata)
 | 
			
		||||
        if attributes and node_type != NodeType.CORRELATION_OBJECT:
 | 
			
		||||
            correlations = self._check_for_correlations(node_id, attributes)
 | 
			
		||||
            for corr in correlations:
 | 
			
		||||
                value = corr['value']
 | 
			
		||||
 | 
			
		||||
@ -186,7 +196,7 @@ class GraphManager:
 | 
			
		||||
                    continue # Skip creating a redundant correlation node
 | 
			
		||||
 | 
			
		||||
                # Proceed to create a new correlation node if no major node was found.
 | 
			
		||||
                correlation_node_id = f"corr_{hash(value) & 0x7FFFFFFF}"
 | 
			
		||||
                correlation_node_id = f"{value}"
 | 
			
		||||
                if not self.graph.has_node(correlation_node_id):
 | 
			
		||||
                    self.add_node(correlation_node_id, NodeType.CORRELATION_OBJECT,
 | 
			
		||||
                                  metadata={'value': value, 'sources': corr['sources'],
 | 
			
		||||
@ -203,7 +213,7 @@ class GraphManager:
 | 
			
		||||
                for c_node_id in set(corr['nodes']):
 | 
			
		||||
                    self.add_edge(c_node_id, correlation_node_id, RelationshipType.CORRELATED_TO)
 | 
			
		||||
 | 
			
		||||
            self._update_correlation_index(node_id, metadata)
 | 
			
		||||
            self._update_correlation_index(node_id, attributes)
 | 
			
		||||
 | 
			
		||||
        self.last_modified = datetime.now(timezone.utc).isoformat()
 | 
			
		||||
        return is_new_node
 | 
			
		||||
@ -263,12 +273,14 @@ class GraphManager:
 | 
			
		||||
        nodes = []
 | 
			
		||||
        for node_id, attrs in self.graph.nodes(data=True):
 | 
			
		||||
            node_data = {'id': node_id, 'label': node_id, 'type': attrs.get('type', 'unknown'),
 | 
			
		||||
                         'attributes': attrs.get('attributes', {}),
 | 
			
		||||
                         'description': attrs.get('description', ''),
 | 
			
		||||
                         'metadata': attrs.get('metadata', {}),
 | 
			
		||||
                         'added_timestamp': attrs.get('added_timestamp')}
 | 
			
		||||
            # Customize node appearance based on type and metadata
 | 
			
		||||
            # Customize node appearance based on type and attributes
 | 
			
		||||
            node_type = node_data['type']
 | 
			
		||||
            metadata = node_data['metadata']
 | 
			
		||||
            if node_type == 'domain' and metadata.get('certificate_data', {}).get('has_valid_cert') is False:
 | 
			
		||||
            attributes = node_data['attributes']
 | 
			
		||||
            if node_type == 'domain' and attributes.get('certificates', {}).get('has_valid_cert') is False:
 | 
			
		||||
                node_data['color'] = {'background': '#c7c7c7', 'border': '#999'} # Gray for invalid cert
 | 
			
		||||
            nodes.append(node_data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -344,7 +344,7 @@ class Scanner:
 | 
			
		||||
        
 | 
			
		||||
        new_targets = set()
 | 
			
		||||
        large_entity_members = set()
 | 
			
		||||
        target_metadata = defaultdict(lambda: defaultdict(list))
 | 
			
		||||
        node_attributes = defaultdict(lambda: defaultdict(list))
 | 
			
		||||
 | 
			
		||||
        eligible_providers = self._get_eligible_providers(target, is_ip, dns_only)
 | 
			
		||||
        
 | 
			
		||||
@ -361,7 +361,7 @@ class Scanner:
 | 
			
		||||
                provider_results = self._query_single_provider_forensic(provider, target, is_ip, depth)
 | 
			
		||||
                if provider_results and not self._is_stop_requested():
 | 
			
		||||
                    discovered, is_large_entity = self._process_provider_results_forensic(
 | 
			
		||||
                        target, provider, provider_results, target_metadata, depth
 | 
			
		||||
                        target, provider, provider_results, node_attributes, depth
 | 
			
		||||
                    )
 | 
			
		||||
                    if is_large_entity:
 | 
			
		||||
                        large_entity_members.update(discovered)
 | 
			
		||||
@ -370,11 +370,11 @@ class Scanner:
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                self._log_provider_error(target, provider.get_name(), str(e))
 | 
			
		||||
 | 
			
		||||
        for node_id, metadata_dict in target_metadata.items():
 | 
			
		||||
        for node_id, attributes in node_attributes.items():
 | 
			
		||||
            if self.graph.graph.has_node(node_id):
 | 
			
		||||
                node_is_ip = _is_valid_ip(node_id)
 | 
			
		||||
                node_type_to_add = NodeType.IP if node_is_ip else NodeType.DOMAIN
 | 
			
		||||
                self.graph.add_node(node_id, node_type_to_add, metadata=metadata_dict)
 | 
			
		||||
                self.graph.add_node(node_id, node_type_to_add, attributes=attributes)
 | 
			
		||||
 | 
			
		||||
        return new_targets, large_entity_members
 | 
			
		||||
 | 
			
		||||
@ -485,7 +485,7 @@ class Scanner:
 | 
			
		||||
        self.logger.logger.info(f"Provider state updated: {target} -> {provider_name} -> {status} ({results_count} results)")
 | 
			
		||||
 | 
			
		||||
    def _process_provider_results_forensic(self, target: str, provider, results: List,
 | 
			
		||||
                                        target_metadata: Dict, current_depth: int) -> Tuple[Set[str], bool]:
 | 
			
		||||
                                        node_attributes: Dict, current_depth: int) -> Tuple[Set[str], bool]:
 | 
			
		||||
        """Process provider results, returns (discovered_targets, is_large_entity)."""
 | 
			
		||||
        provider_name = provider.get_name()
 | 
			
		||||
        discovered_targets = set()
 | 
			
		||||
@ -514,7 +514,7 @@ class Scanner:
 | 
			
		||||
                discovery_method=f"{provider_name}_query_depth_{current_depth}"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            self._collect_node_metadata_forensic(source, provider_name, rel_type, rel_target, raw_data, target_metadata[source])
 | 
			
		||||
            self._collect_node_attributes(source, provider_name, rel_type, rel_target, raw_data, node_attributes[source])
 | 
			
		||||
 | 
			
		||||
            if _is_valid_ip(rel_target):
 | 
			
		||||
                self.graph.add_node(rel_target, NodeType.IP)
 | 
			
		||||
@ -532,10 +532,10 @@ class Scanner:
 | 
			
		||||
                if self.graph.add_edge(source, rel_target, rel_type, confidence, provider_name, raw_data):
 | 
			
		||||
                    print(f"Added domain relationship: {source} -> {rel_target} ({rel_type.relationship_name})")
 | 
			
		||||
                discovered_targets.add(rel_target)
 | 
			
		||||
                self._collect_node_metadata_forensic(rel_target, provider_name, rel_type, source, raw_data, target_metadata[rel_target])
 | 
			
		||||
                self._collect_node_attributes(rel_target, provider_name, rel_type, source, raw_data, node_attributes[rel_target])
 | 
			
		||||
            
 | 
			
		||||
            else:
 | 
			
		||||
                self._collect_node_metadata_forensic(source, provider_name, rel_type, rel_target, raw_data, target_metadata[source])
 | 
			
		||||
                self._collect_node_attributes(source, provider_name, rel_type, rel_target, raw_data, node_attributes[source])
 | 
			
		||||
 | 
			
		||||
        return discovered_targets, False
 | 
			
		||||
 | 
			
		||||
@ -555,17 +555,17 @@ class Scanner:
 | 
			
		||||
        for target in targets:
 | 
			
		||||
            self.graph.add_node(target, NodeType.DOMAIN if node_type == 'domain' else NodeType.IP)
 | 
			
		||||
 | 
			
		||||
        metadata = {
 | 
			
		||||
        attributes = {
 | 
			
		||||
            'count': len(targets),
 | 
			
		||||
            'nodes': targets,
 | 
			
		||||
            'node_type': node_type,
 | 
			
		||||
            'source_provider': provider_name,
 | 
			
		||||
            'discovery_depth': current_depth,
 | 
			
		||||
            'threshold_exceeded': self.config.large_entity_threshold,
 | 
			
		||||
            'forensic_note': f'Large entity created due to {len(targets)} results from {provider_name}'
 | 
			
		||||
        }
 | 
			
		||||
        description = f'Large entity created due to {len(targets)} results from {provider_name}'
 | 
			
		||||
        
 | 
			
		||||
        self.graph.add_node(entity_id, NodeType.LARGE_ENTITY, metadata=metadata)
 | 
			
		||||
        self.graph.add_node(entity_id, NodeType.LARGE_ENTITY, attributes=attributes, description=description)
 | 
			
		||||
        
 | 
			
		||||
        if results:
 | 
			
		||||
            rel_type = results[0][2]
 | 
			
		||||
@ -577,49 +577,50 @@ class Scanner:
 | 
			
		||||
        
 | 
			
		||||
        return set(targets)
 | 
			
		||||
 | 
			
		||||
    def _collect_node_metadata_forensic(self, node_id: str, provider_name: str, rel_type: RelationshipType,
 | 
			
		||||
                                    target: str, raw_data: Dict[str, Any], metadata: Dict[str, Any]) -> None:
 | 
			
		||||
        """Collect and organize metadata for forensic tracking with enhanced logging."""
 | 
			
		||||
        self.logger.logger.debug(f"Collecting metadata for {node_id} from {provider_name}: {rel_type.relationship_name}")
 | 
			
		||||
    def _collect_node_attributes(self, node_id: str, provider_name: str, rel_type: RelationshipType,
 | 
			
		||||
                                    target: str, raw_data: Dict[str, Any], attributes: Dict[str, Any]) -> None:
 | 
			
		||||
        """Collect and organize attributes for a node."""
 | 
			
		||||
        self.logger.logger.debug(f"Collecting attributes for {node_id} from {provider_name}: {rel_type.relationship_name}")
 | 
			
		||||
 | 
			
		||||
        if provider_name == 'dns':
 | 
			
		||||
            record_type = raw_data.get('query_type', 'UNKNOWN')
 | 
			
		||||
            value = raw_data.get('value', target)
 | 
			
		||||
            dns_entry = f"{record_type}: {value}"
 | 
			
		||||
            if dns_entry not in metadata.get('dns_records', []):
 | 
			
		||||
                metadata.setdefault('dns_records', []).append(dns_entry)
 | 
			
		||||
            if dns_entry not in attributes.get('dns_records', []):
 | 
			
		||||
                attributes.setdefault('dns_records', []).append(dns_entry)
 | 
			
		||||
 | 
			
		||||
        elif provider_name == 'crtsh':
 | 
			
		||||
            if rel_type == RelationshipType.SAN_CERTIFICATE:
 | 
			
		||||
                domain_certs = raw_data.get('domain_certificates', {})
 | 
			
		||||
                if node_id in domain_certs:
 | 
			
		||||
                    cert_summary = domain_certs[node_id]
 | 
			
		||||
                    metadata['certificate_data'] = cert_summary
 | 
			
		||||
                    metadata['has_valid_cert'] = cert_summary.get('has_valid_cert', False)
 | 
			
		||||
                    if target not in metadata.get('related_domains_san', []):
 | 
			
		||||
                        metadata.setdefault('related_domains_san', []).append(target)
 | 
			
		||||
                    attributes['certificates'] = cert_summary
 | 
			
		||||
                    if target not in attributes.get('related_domains_san', []):
 | 
			
		||||
                        attributes.setdefault('related_domains_san', []).append(target)
 | 
			
		||||
 | 
			
		||||
        elif provider_name == 'shodan':
 | 
			
		||||
            shodan_attributes = attributes.setdefault('shodan', {})
 | 
			
		||||
            for key, value in raw_data.items():
 | 
			
		||||
                if key not in metadata.get('shodan', {}) or not metadata.get('shodan', {}).get(key):
 | 
			
		||||
                    metadata.setdefault('shodan', {})[key] = value
 | 
			
		||||
                if key not in shodan_attributes or not shodan_attributes.get(key):
 | 
			
		||||
                    shodan_attributes[key] = value
 | 
			
		||||
 | 
			
		||||
        if rel_type == RelationshipType.ASN_MEMBERSHIP:
 | 
			
		||||
            metadata['asn_data'] = {
 | 
			
		||||
                'asn': target,
 | 
			
		||||
            attributes['asn'] = {
 | 
			
		||||
                'id': target,
 | 
			
		||||
                'description': raw_data.get('org', ''),
 | 
			
		||||
                'isp': raw_data.get('isp', ''),
 | 
			
		||||
                'country': raw_data.get('country', '')
 | 
			
		||||
            }
 | 
			
		||||
        
 | 
			
		||||
        record_type_name = rel_type.relationship_name
 | 
			
		||||
        if record_type_name not in metadata:
 | 
			
		||||
            metadata[record_type_name] = []
 | 
			
		||||
        if record_type_name not in attributes:
 | 
			
		||||
            attributes[record_type_name] = []
 | 
			
		||||
        
 | 
			
		||||
        if isinstance(target, list):
 | 
			
		||||
            metadata[record_type_name].extend(target)
 | 
			
		||||
            attributes[record_type_name].extend(target)
 | 
			
		||||
        else:
 | 
			
		||||
            metadata[record_type_name].append(target)
 | 
			
		||||
            if target not in attributes[record_type_name]:
 | 
			
		||||
                attributes[record_type_name].append(target)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _log_target_processing_error(self, target: str, error: str) -> None:
 | 
			
		||||
 | 
			
		||||
@ -581,30 +581,6 @@ input[type="text"]:focus, select:focus {
 | 
			
		||||
    color: #555;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Modal */
 | 
			
		||||
.modal {
 | 
			
		||||
    display: none;
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    z-index: 1000;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.8);
 | 
			
		||||
    animation: fadeIn 0.3s ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-content {
 | 
			
		||||
    background-color: #2a2a2a;
 | 
			
		||||
    border: 1px solid #444;
 | 
			
		||||
    margin: 5% auto;
 | 
			
		||||
    width: 80%;
 | 
			
		||||
    max-width: 600px;
 | 
			
		||||
    max-height: 80vh;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    animation: slideInDown 0.3s ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slideInDown {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
@ -616,43 +592,6 @@ input[type="text"]:focus, select:focus {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-header {
 | 
			
		||||
    background-color: #1a1a1a;
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
    border-bottom: 1px solid #444;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-header h3 {
 | 
			
		||||
    color: #00ff41;
 | 
			
		||||
    font-size: 1.1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-close {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border: none;
 | 
			
		||||
    color: #c7c7c7;
 | 
			
		||||
    font-size: 1.2rem;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    font-family: 'Roboto Mono', monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-close:hover {
 | 
			
		||||
    color: #ff9900;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-body {
 | 
			
		||||
    padding: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-description {
 | 
			
		||||
    color: #999;
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
    line-height: 1.6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.detail-row {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
@ -801,12 +740,6 @@ input[type="text"]:focus, select:focus {
 | 
			
		||||
    color: #00ff41 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Animations */
 | 
			
		||||
@keyframes fadeIn {
 | 
			
		||||
    from { opacity: 0; transform: translateY(10px); }
 | 
			
		||||
    to { opacity: 1; transform: translateY(0); }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-in {
 | 
			
		||||
    animation: fadeIn 0.3s ease-out;
 | 
			
		||||
}
 | 
			
		||||
@ -970,4 +903,104 @@ input[type="text"]:focus, select:focus {
 | 
			
		||||
.large-entity-node-details .detail-section-header {
 | 
			
		||||
    margin-left: 1rem;
 | 
			
		||||
    margin-right: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* dnsrecon/static/css/main.css */
 | 
			
		||||
 | 
			
		||||
/* --- Add these styles for the modal --- */
 | 
			
		||||
 | 
			
		||||
.modal {
 | 
			
		||||
    display: none; /* Hidden by default */
 | 
			
		||||
    position: fixed; /* Stay in place */
 | 
			
		||||
    z-index: 1000; /* Sit on top */
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    overflow: auto; /* Enable scroll if needed */
 | 
			
		||||
    background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
 | 
			
		||||
    backdrop-filter: blur(5px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-content {
 | 
			
		||||
    background-color: #1e1e1e;
 | 
			
		||||
    margin: 10% auto;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    border: 1px solid #444;
 | 
			
		||||
    width: 60%;
 | 
			
		||||
    max-width: 800px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    box-shadow: 0 5px 15px rgba(0,0,0,0.5);
 | 
			
		||||
    animation: fadeIn 0.3s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-header {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    border-bottom: 1px solid #444;
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-header h3 {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    font-family: 'Special Elite', monospace;
 | 
			
		||||
    color: #00ff41;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-close {
 | 
			
		||||
    background: none;
 | 
			
		||||
    border: none;
 | 
			
		||||
    color: #c7c7c7;
 | 
			
		||||
    font-size: 24px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    padding: 0 10px;
 | 
			
		||||
}
 | 
			
		||||
.modal-close:hover {
 | 
			
		||||
    color: #ff6b6b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-body {
 | 
			
		||||
    max-height: 60vh;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Styles for the new data model display */
 | 
			
		||||
.modal-details-grid {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: 1fr;
 | 
			
		||||
    gap: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-section h4 {
 | 
			
		||||
    font-family: 'Special Elite', monospace;
 | 
			
		||||
    color: #ff9900;
 | 
			
		||||
    border-bottom: 1px dashed #555;
 | 
			
		||||
    padding-bottom: 5px;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-section ul {
 | 
			
		||||
    list-style-type: none;
 | 
			
		||||
    padding-left: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-section li {
 | 
			
		||||
    margin-bottom: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-section li > ul {
 | 
			
		||||
    padding-left: 20px;
 | 
			
		||||
    margin-top: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.description-text, .no-data {
 | 
			
		||||
    color: #aaa;
 | 
			
		||||
    font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes fadeIn {
 | 
			
		||||
    from {opacity: 0; transform: scale(0.95);}
 | 
			
		||||
    to {opacity: 1; transform: scale(1);}
 | 
			
		||||
}
 | 
			
		||||
@ -213,12 +213,12 @@ class GraphManager {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // TODO Context menu (right-click)
 | 
			
		||||
        // FIX: Comment out the problematic context menu handler
 | 
			
		||||
        this.network.on('oncontext', (params) => {
 | 
			
		||||
            params.event.preventDefault();
 | 
			
		||||
            if (params.nodes.length > 0) {
 | 
			
		||||
                this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]);
 | 
			
		||||
            }
 | 
			
		||||
            // if (params.nodes.length > 0) {
 | 
			
		||||
            //     this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]);
 | 
			
		||||
            // }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Stabilization events with progress
 | 
			
		||||
@ -256,8 +256,8 @@ class GraphManager {
 | 
			
		||||
 | 
			
		||||
            const largeEntityMap = new Map();
 | 
			
		||||
            graphData.nodes.forEach(node => {
 | 
			
		||||
                if (node.type === 'large_entity' && node.metadata && Array.isArray(node.metadata.nodes)) {
 | 
			
		||||
                    node.metadata.nodes.forEach(nodeId => {
 | 
			
		||||
                if (node.type === 'large_entity' && node.attributes && Array.isArray(node.attributes.nodes)) {
 | 
			
		||||
                    node.attributes.nodes.forEach(nodeId => {
 | 
			
		||||
                        largeEntityMap.set(nodeId, node.id);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
@ -274,12 +274,14 @@ class GraphManager {
 | 
			
		||||
            const mergedEdges = {};
 | 
			
		||||
            graphData.edges.forEach(edge => {
 | 
			
		||||
                const fromNode = largeEntityMap.has(edge.from) ? largeEntityMap.get(edge.from) : edge.from;
 | 
			
		||||
                const mergeKey = `${fromNode}-${edge.to}-${edge.label}`;
 | 
			
		||||
                const toNode = largeEntityMap.has(edge.to) ? largeEntityMap.get(edge.to) : edge.to;
 | 
			
		||||
                const mergeKey = `${fromNode}-${toNode}-${edge.label}`;
 | 
			
		||||
 | 
			
		||||
                if (!mergedEdges[mergeKey]) {
 | 
			
		||||
                    mergedEdges[mergeKey] = {
 | 
			
		||||
                        ...edge,
 | 
			
		||||
                        from: fromNode,
 | 
			
		||||
                        to: toNode,
 | 
			
		||||
                        count: 0,
 | 
			
		||||
                        confidence_score: 0
 | 
			
		||||
                    };
 | 
			
		||||
@ -341,6 +343,8 @@ class GraphManager {
 | 
			
		||||
            size: this.getNodeSize(node.type),
 | 
			
		||||
            borderColor: this.getNodeBorderColor(node.type),
 | 
			
		||||
            shape: this.getNodeShape(node.type),
 | 
			
		||||
            attributes: node.attributes || {},
 | 
			
		||||
            description: node.description || '',
 | 
			
		||||
            metadata: node.metadata || {},
 | 
			
		||||
            type: node.type
 | 
			
		||||
        };
 | 
			
		||||
@ -352,12 +356,8 @@ class GraphManager {
 | 
			
		||||
 | 
			
		||||
        // Style based on certificate validity
 | 
			
		||||
        if (node.type === 'domain') {
 | 
			
		||||
            if (node.metadata && node.metadata.certificate_data && node.metadata.certificate_data.has_valid_cert === true) {
 | 
			
		||||
                processedNode.color = '#00ff41'; // Bright green for valid cert
 | 
			
		||||
                processedNode.borderColor = '#00aa2e';
 | 
			
		||||
            } else if (node.metadata && node.metadata.certificate_data && node.metadata.certificate_data.has_valid_cert === false) {
 | 
			
		||||
                processedNode.color = '#888888'; // Muted grey color
 | 
			
		||||
                processedNode.borderColor = '#666666'; // Darker grey border
 | 
			
		||||
            if (node.attributes && node.attributes.certificates && node.attributes.certificates.has_valid_cert === false) {
 | 
			
		||||
                processedNode.color = { background: '#888888', border: '#666666' };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
@ -404,7 +404,7 @@ class GraphManager {
 | 
			
		||||
     * @returns {string} Formatted label
 | 
			
		||||
     */
 | 
			
		||||
    formatNodeLabel(nodeId, nodeType) {
 | 
			
		||||
        // Truncate long domain names
 | 
			
		||||
        if (typeof nodeId !== 'string') return '';
 | 
			
		||||
        if (nodeId.length > 20) {
 | 
			
		||||
            return nodeId.substring(0, 17) + '...';
 | 
			
		||||
        }
 | 
			
		||||
@ -564,7 +564,7 @@ class GraphManager {
 | 
			
		||||
 | 
			
		||||
        // Trigger custom event for main application to handle
 | 
			
		||||
        const event = new CustomEvent('nodeSelected', {
 | 
			
		||||
            detail: { nodeId, node }
 | 
			
		||||
            detail: { node }
 | 
			
		||||
        });
 | 
			
		||||
        document.dispatchEvent(event);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -193,9 +193,9 @@ class DNSReconApp {
 | 
			
		||||
                this.elements.resetApiKeys.addEventListener('click', () => this.resetApiKeys());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Custom events
 | 
			
		||||
            // ** FIX: Listen for the custom event from the graph **
 | 
			
		||||
            document.addEventListener('nodeSelected', (e) => {
 | 
			
		||||
                this.showNodeModal(e.detail.nodeId, e.detail.node);
 | 
			
		||||
                this.showNodeModal(e.detail.node);
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            // Keyboard shortcuts
 | 
			
		||||
@ -793,129 +793,86 @@ class DNSReconApp {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Generates the HTML for the node details view.
 | 
			
		||||
     * **FIX**: Generates the HTML for the node details view using the new data model.
 | 
			
		||||
     * @param {Object} node - The node object.
 | 
			
		||||
     * @returns {string} The HTML string for the node details.
 | 
			
		||||
     */
 | 
			
		||||
    generateNodeDetailsHtml(node) {
 | 
			
		||||
        if(!node) return '<div class="detail-row"><span class="detail-value">Details not available.</span></div>';
 | 
			
		||||
        let detailsHtml = '';
 | 
			
		||||
        const createDetailRow = (label, value, statusIcon = '') => {
 | 
			
		||||
            const baseId = `detail-${node.id.replace(/[^a-zA-Z0-9]/g, '-')}-${label.replace(/[^a-zA-Z0-9]/g, '-')}`;
 | 
			
		||||
        
 | 
			
		||||
        let detailsHtml = '<div class="modal-details-grid">';
 | 
			
		||||
 | 
			
		||||
            if (value === null || value === undefined ||
 | 
			
		||||
                (Array.isArray(value) && value.length === 0) ||
 | 
			
		||||
                (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)) {
 | 
			
		||||
                return `
 | 
			
		||||
                    <div class="detail-row">
 | 
			
		||||
                        <span class="detail-label">${label} <span class="status-icon text-warning">✗</span></span>
 | 
			
		||||
                        <span class="detail-value">N/A</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                `;
 | 
			
		||||
            }
 | 
			
		||||
        // Section for Attributes
 | 
			
		||||
        detailsHtml += '<div class="modal-section">';
 | 
			
		||||
        detailsHtml += '<h4>Attributes</h4>';
 | 
			
		||||
        detailsHtml += this.formatObjectToHtml(node.attributes);
 | 
			
		||||
        detailsHtml += '</div>';
 | 
			
		||||
 | 
			
		||||
            if (Array.isArray(value)) {
 | 
			
		||||
                return value.map((item, index) => {
 | 
			
		||||
                    const itemId = `${baseId}-${index}`;
 | 
			
		||||
                    const itemLabel = index === 0 ? `${label} <span class="status-icon text-success">✓</span>` : '';
 | 
			
		||||
                    return `
 | 
			
		||||
                        <div class="detail-row">
 | 
			
		||||
                            <span class="detail-label">${itemLabel}</span>
 | 
			
		||||
                            <span class="detail-value" id="${itemId}">${this.formatValue(item)}</span>
 | 
			
		||||
                            <button class="copy-btn" onclick="copyToClipboard('${itemId}')" title="Copy">📋</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    `;
 | 
			
		||||
                }).join('');
 | 
			
		||||
            } else {
 | 
			
		||||
                const valueId = `${baseId}-0`;
 | 
			
		||||
                const icon = statusIcon || '<span class="status-icon text-success">✓</span>';
 | 
			
		||||
                return `
 | 
			
		||||
                    <div class="detail-row">
 | 
			
		||||
                        <span class="detail-label">${label} ${icon}</span>
 | 
			
		||||
                        <span class="detail-value" id="${valueId}">${this.formatValue(value)}</span>
 | 
			
		||||
                        <button class="copy-btn" onclick="copyToClipboard('${valueId}')" title="Copy">📋</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                `;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        // Section for Description
 | 
			
		||||
        detailsHtml += '<div class="modal-section">';
 | 
			
		||||
        detailsHtml += '<h4>Description</h4>';
 | 
			
		||||
        detailsHtml += `<p class="description-text">${node.description || 'No description available.'}</p>`;
 | 
			
		||||
        detailsHtml += '</div>';
 | 
			
		||||
 | 
			
		||||
        const metadata = node.metadata || {};
 | 
			
		||||
 | 
			
		||||
        detailsHtml += createDetailRow('Node Descriptor', node.id);
 | 
			
		||||
 | 
			
		||||
        switch (node.type) {
 | 
			
		||||
            case 'domain':
 | 
			
		||||
                detailsHtml += createDetailRow('DNS Records', metadata.dns_records);
 | 
			
		||||
                detailsHtml += createDetailRow('Related Domains (SAN)', metadata.related_domains_san);
 | 
			
		||||
                detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns);
 | 
			
		||||
                detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
 | 
			
		||||
                break;
 | 
			
		||||
            case 'ip':
 | 
			
		||||
                detailsHtml += createDetailRow('Hostnames', metadata.hostnames);
 | 
			
		||||
                detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns);
 | 
			
		||||
                detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
 | 
			
		||||
                break;
 | 
			
		||||
            case 'correlation_object':
 | 
			
		||||
                detailsHtml += createDetailRow('Correlated Value', metadata.value);
 | 
			
		||||
                if (metadata.correlated_nodes) {
 | 
			
		||||
                    detailsHtml += createDetailRow('Correlated Nodes', metadata.correlated_nodes.join(', '));
 | 
			
		||||
                }
 | 
			
		||||
                if (metadata.sources) {
 | 
			
		||||
                    detailsHtml += `<div class="detail-section-header">Correlation Sources</div>`;
 | 
			
		||||
                    for (const source of metadata.sources) {
 | 
			
		||||
                        detailsHtml += createDetailRow(source.node_id, source.path);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (metadata.certificate_data && Object.keys(metadata.certificate_data).length > 0) {
 | 
			
		||||
            const cert = metadata.certificate_data;
 | 
			
		||||
            detailsHtml += `<div class="detail-section-header">Certificate Summary</div>`;
 | 
			
		||||
            detailsHtml += createDetailRow('Total Found', cert.total_certificates);
 | 
			
		||||
            detailsHtml += createDetailRow('Currently Valid', cert.valid_certificates);
 | 
			
		||||
            detailsHtml += createDetailRow('Expires Soon (<30d)', cert.expires_soon_count);
 | 
			
		||||
            detailsHtml += createDetailRow('Unique Issuers', cert.unique_issuers ? cert.unique_issuers.join(', ') : 'N/A');
 | 
			
		||||
 | 
			
		||||
            if (cert.latest_certificate) {
 | 
			
		||||
                detailsHtml += `<div class="detail-section-header">Latest Certificate</div>`;
 | 
			
		||||
                detailsHtml += createDetailRow('Common Name', cert.latest_certificate.common_name);
 | 
			
		||||
                detailsHtml += createDetailRow('Issuer', cert.latest_certificate.issuer_name);
 | 
			
		||||
                detailsHtml += createDetailRow('Valid From', new Date(cert.latest_certificate.not_before).toLocaleString());
 | 
			
		||||
                detailsHtml += createDetailRow('Valid Until', new Date(cert.latest_certificate.not_after).toLocaleString());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (metadata.asn_data && Object.keys(metadata.asn_data).length > 0) {
 | 
			
		||||
            detailsHtml += `<div class="detail-section-header">ASN Information</div>`;
 | 
			
		||||
            detailsHtml += createDetailRow('ASN', metadata.asn_data.asn);
 | 
			
		||||
            detailsHtml += createDetailRow('Organization', metadata.asn_data.description);
 | 
			
		||||
            detailsHtml += createDetailRow('ISP', metadata.asn_data.isp);
 | 
			
		||||
            detailsHtml += createDetailRow('Country', metadata.asn_data.country);
 | 
			
		||||
        }
 | 
			
		||||
        // Section for Metadata
 | 
			
		||||
        detailsHtml += '<div class="modal-section">';
 | 
			
		||||
        detailsHtml += '<h4>Metadata</h4>';
 | 
			
		||||
        detailsHtml += this.formatObjectToHtml(node.metadata);
 | 
			
		||||
        detailsHtml += '</div>';
 | 
			
		||||
 | 
			
		||||
        detailsHtml += '</div>';
 | 
			
		||||
        return detailsHtml;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Recursively formats a JavaScript object into an HTML unordered list.
 | 
			
		||||
     * @param {Object} obj - The object to format.
 | 
			
		||||
     * @returns {string} - An HTML string representing the object.
 | 
			
		||||
     */
 | 
			
		||||
    formatObjectToHtml(obj) {
 | 
			
		||||
        if (!obj || Object.keys(obj).length === 0) {
 | 
			
		||||
            return '<p class="no-data">No data available.</p>';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let html = '<ul>';
 | 
			
		||||
        for (const key in obj) {
 | 
			
		||||
            if (Object.hasOwnProperty.call(obj, key)) {
 | 
			
		||||
                const value = obj[key];
 | 
			
		||||
                const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
 | 
			
		||||
                html += `<li><strong>${formattedKey}:</strong>`;
 | 
			
		||||
 | 
			
		||||
                if (Array.isArray(value)) {
 | 
			
		||||
                    html += `<ul>${value.map(item => `<li>${this.formatValue(item)}</li>`).join('')}</ul>`;
 | 
			
		||||
                } else if (typeof value === 'object' && value !== null) {
 | 
			
		||||
                    html += this.formatObjectToHtml(value);
 | 
			
		||||
                } else {
 | 
			
		||||
                    html += ` ${this.formatValue(value)}`;
 | 
			
		||||
                }
 | 
			
		||||
                html += '</li>';
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        html += '</ul>';
 | 
			
		||||
        return html;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Show node details modal
 | 
			
		||||
     * @param {string} nodeId - Node identifier
 | 
			
		||||
     * @param {Object} node - Node data
 | 
			
		||||
     */
 | 
			
		||||
    showNodeModal(nodeId, node) {
 | 
			
		||||
        if (!this.elements.nodeModal) return;
 | 
			
		||||
    showNodeModal(node) {
 | 
			
		||||
        if (!this.elements.nodeModal || !node) return;
 | 
			
		||||
 | 
			
		||||
        if (this.elements.modalTitle) {
 | 
			
		||||
            this.elements.modalTitle.textContent = `Node Details`;
 | 
			
		||||
            this.elements.modalTitle.textContent = `${this.formatStatus(node.type)} Node: ${node.id}`;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let detailsHtml = '';
 | 
			
		||||
 | 
			
		||||
        if (node.type === 'large_entity') {
 | 
			
		||||
            const metadata = node.metadata || {};
 | 
			
		||||
            const nodes = metadata.nodes || [];
 | 
			
		||||
            const node_type = metadata.node_type || 'nodes';
 | 
			
		||||
            detailsHtml += `<div class="detail-section-header">Contains ${metadata.count} ${node_type}s</div>`;
 | 
			
		||||
            const attributes = node.attributes || {};
 | 
			
		||||
            const nodes = attributes.nodes || [];
 | 
			
		||||
            const node_type = attributes.node_type || 'nodes';
 | 
			
		||||
            detailsHtml += `<div class="detail-section-header">Contains ${attributes.count} ${node_type}s</div>`;
 | 
			
		||||
            detailsHtml += '<div class="large-entity-nodes-list">';
 | 
			
		||||
 | 
			
		||||
            for(const innerNodeId of nodes) {
 | 
			
		||||
@ -926,12 +883,10 @@ class DNSReconApp {
 | 
			
		||||
                detailsHtml += `</details>`;
 | 
			
		||||
            }
 | 
			
		||||
            detailsHtml += '</div>';
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
             detailsHtml = this.generateNodeDetailsHtml(node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (this.elements.modalDetails) {
 | 
			
		||||
            this.elements.modalDetails.innerHTML = detailsHtml;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user