fixes to hint for incomplete data
This commit is contained in:
		
							parent
							
								
									eabb532557
								
							
						
					
					
						commit
						7472e6f416
					
				@ -3,7 +3,7 @@
 | 
				
			|||||||
import json
 | 
					import json
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
from typing import List, Dict, Any, Set
 | 
					from typing import List, Dict, Any, Set, Optional
 | 
				
			||||||
from urllib.parse import quote
 | 
					from urllib.parse import quote
 | 
				
			||||||
from datetime import datetime, timezone
 | 
					from datetime import datetime, timezone
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
@ -285,6 +285,17 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
        if self._stop_event and self._stop_event.is_set():
 | 
					        if self._stop_event and self._stop_event.is_set():
 | 
				
			||||||
            self.logger.logger.info(f"CrtSh processing cancelled before processing for domain: {query_domain}")
 | 
					            self.logger.logger.info(f"CrtSh processing cancelled before processing for domain: {query_domain}")
 | 
				
			||||||
            return result
 | 
					            return result
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        incompleteness_warning = self._check_for_incomplete_data(query_domain, certificates)
 | 
				
			||||||
 | 
					        if incompleteness_warning:
 | 
				
			||||||
 | 
					            result.add_attribute(
 | 
				
			||||||
 | 
					                target_node=query_domain,
 | 
				
			||||||
 | 
					                name="crtsh_data_warning",
 | 
				
			||||||
 | 
					                value=incompleteness_warning,
 | 
				
			||||||
 | 
					                attr_type='metadata',
 | 
				
			||||||
 | 
					                provider=self.name,
 | 
				
			||||||
 | 
					                confidence=1.0
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        all_discovered_domains = set()
 | 
					        all_discovered_domains = set()
 | 
				
			||||||
        processed_issuers = set()
 | 
					        processed_issuers = set()
 | 
				
			||||||
@ -577,4 +588,30 @@ class CrtShProvider(BaseProvider):
 | 
				
			|||||||
        elif query_domain.endswith(f'.{cert_domain}'):
 | 
					        elif query_domain.endswith(f'.{cert_domain}'):
 | 
				
			||||||
            return 'parent_domain'
 | 
					            return 'parent_domain'
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            return 'related_domain'
 | 
					            return 'related_domain'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    def _check_for_incomplete_data(self, domain: str, certificates: List[Dict[str, Any]]) -> Optional[str]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Analyzes the certificate list to heuristically detect if the data from crt.sh is incomplete.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        cert_count = len(certificates)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Heuristic 1: Check if the number of certs hits a known hard limit.
 | 
				
			||||||
 | 
					        if cert_count >= 10000:
 | 
				
			||||||
 | 
					            return f"Result likely truncated; received {cert_count} certificates, which may be the maximum limit."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Heuristic 2: Check if all returned certificates are old.
 | 
				
			||||||
 | 
					        if cert_count > 1000: # Only apply this for a reasonable number of certs
 | 
				
			||||||
 | 
					            latest_expiry = None
 | 
				
			||||||
 | 
					            for cert in certificates:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    not_after = self._parse_certificate_date(cert.get('not_after'))
 | 
				
			||||||
 | 
					                    if latest_expiry is None or not_after > latest_expiry:
 | 
				
			||||||
 | 
					                        latest_expiry = not_after
 | 
				
			||||||
 | 
					                except (ValueError, TypeError):
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if latest_expiry and (datetime.now(timezone.utc) - latest_expiry).days > 365:
 | 
				
			||||||
 | 
					                 return f"Incomplete data suspected: The latest certificate expired more than a year ago ({latest_expiry.strftime('%Y-%m-%d')})."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
@ -1565,19 +1565,42 @@ class GraphManager {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Unhide all hidden nodes, excluding those within a large entity.
 | 
					     * FIXED: Unhide all hidden nodes, excluding large entity members and disconnected nodes.
 | 
				
			||||||
 | 
					     * This prevents orphaned large entity members from appearing as free-floating nodes.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    unhideAll() {
 | 
					    unhideAll() {
 | 
				
			||||||
        const allHiddenNodes = this.nodes.get({
 | 
					        const allHiddenNodes = this.nodes.get({
 | 
				
			||||||
            filter: (node) => {
 | 
					            filter: (node) => {
 | 
				
			||||||
                // Condition: Node is hidden AND it is NOT part of a large entity.
 | 
					                // Skip nodes that are part of a large entity
 | 
				
			||||||
                return node.hidden === true && !(node.metadata && node.metadata.large_entity_id);
 | 
					                if (node.metadata && node.metadata.large_entity_id) {
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Skip nodes that are not hidden
 | 
				
			||||||
 | 
					                if (node.hidden !== true) {
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Skip nodes that have no edges (would appear disconnected)
 | 
				
			||||||
 | 
					                const nodeId = node.id;
 | 
				
			||||||
 | 
					                const hasIncomingEdges = this.edges.get().some(edge => edge.to === nodeId && !edge.hidden);
 | 
				
			||||||
 | 
					                const hasOutgoingEdges = this.edges.get().some(edge => edge.from === nodeId && !edge.hidden);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (!hasIncomingEdges && !hasOutgoingEdges) {
 | 
				
			||||||
 | 
					                    console.log(`Skipping disconnected node ${nodeId} from unhide`);
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if (allHiddenNodes.length > 0) {
 | 
					        if (allHiddenNodes.length > 0) {
 | 
				
			||||||
 | 
					            console.log(`Unhiding ${allHiddenNodes.length} nodes with valid connections`);
 | 
				
			||||||
            const updates = allHiddenNodes.map(node => ({ id: node.id, hidden: false }));
 | 
					            const updates = allHiddenNodes.map(node => ({ id: node.id, hidden: false }));
 | 
				
			||||||
            this.nodes.update(updates);
 | 
					            this.nodes.update(updates);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            console.log('No eligible nodes to unhide');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
				
			|||||||
@ -1397,28 +1397,62 @@ class DNSReconApp {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * UPDATED: Generate details for standard nodes with organized attribute grouping
 | 
					     * UPDATED: Generate details for standard nodes with organized attribute grouping and data warnings
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    generateStandardNodeDetails(node) {
 | 
					    generateStandardNodeDetails(node) {
 | 
				
			||||||
        let html = '';
 | 
					        let html = '';
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
 | 
					        // Check for and display a crt.sh data warning if it exists
 | 
				
			||||||
 | 
					        const crtshWarningAttr = this.findAttributeByName(node.attributes, 'crtsh_data_warning');
 | 
				
			||||||
 | 
					        if (crtshWarningAttr) {
 | 
				
			||||||
 | 
					            html += `
 | 
				
			||||||
 | 
					                <div class="modal-section" style="border-left: 3px solid #ff9900; background: rgba(255, 153, 0, 0.05);">
 | 
				
			||||||
 | 
					                    <details open>
 | 
				
			||||||
 | 
					                        <summary style="color: #ff9900;">
 | 
				
			||||||
 | 
					                            <span>⚠️ Data Integrity Warning</span>
 | 
				
			||||||
 | 
					                        </summary>
 | 
				
			||||||
 | 
					                        <div class="modal-section-content">
 | 
				
			||||||
 | 
					                            <p class="placeholder-subtext" style="color: #e0e0e0; font-size: 0.8rem; line-height: 1.5;">
 | 
				
			||||||
 | 
					                                ${this.escapeHtml(crtshWarningAttr.value)}
 | 
				
			||||||
 | 
					                                <br><br>
 | 
				
			||||||
 | 
					                                This can occur for very large domains (e.g., google.com) where crt.sh may return a limited subset of all available certificates. As a result, the certificate status may not be fully representative.
 | 
				
			||||||
 | 
					                            </p>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </details>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            `;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Relationships sections
 | 
					        // Relationships sections
 | 
				
			||||||
        html += this.generateRelationshipsSection(node);
 | 
					        html += this.generateRelationshipsSection(node);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // UPDATED: Enhanced attributes section with intelligent grouping (no formatting)
 | 
					        // UPDATED: Enhanced attributes section with intelligent grouping (no formatting)
 | 
				
			||||||
        if (node.attributes && Array.isArray(node.attributes) && node.attributes.length > 0) {
 | 
					        if (node.attributes && Array.isArray(node.attributes) && node.attributes.length > 0) {
 | 
				
			||||||
            html += this.generateOrganizedAttributesSection(node.attributes, node.type);
 | 
					            html += this.generateOrganizedAttributesSection(node.attributes, node.type);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // Description section
 | 
					        // Description section
 | 
				
			||||||
        html += this.generateDescriptionSection(node);
 | 
					        html += this.generateDescriptionSection(node);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // Metadata section (collapsed by default)
 | 
					        // Metadata section (collapsed by default)
 | 
				
			||||||
        html += this.generateMetadataSection(node);
 | 
					        html += this.generateMetadataSection(node);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        return html;
 | 
					        return html;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Helper method to find an attribute by name in the standardized attributes list
 | 
				
			||||||
 | 
					     * @param {Array} attributes - List of StandardAttribute objects
 | 
				
			||||||
 | 
					     * @param {string} name - Attribute name to find
 | 
				
			||||||
 | 
					     * @returns {Object|null} The attribute object if found, null otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    findAttributeByName(attributes, name) {
 | 
				
			||||||
 | 
					        if (!Array.isArray(attributes)) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return attributes.find(attr => attr.name === name) || null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    generateOrganizedAttributesSection(attributes, nodeType) {
 | 
					    generateOrganizedAttributesSection(attributes, nodeType) {
 | 
				
			||||||
        if (!Array.isArray(attributes) || attributes.length === 0) {
 | 
					        if (!Array.isArray(attributes) || attributes.length === 0) {
 | 
				
			||||||
            return '';
 | 
					            return '';
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user