diff --git a/providers/crtsh_provider.py b/providers/crtsh_provider.py index 731cfd2..e4bda95 100644 --- a/providers/crtsh_provider.py +++ b/providers/crtsh_provider.py @@ -3,7 +3,7 @@ import json import re 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 datetime import datetime, timezone import requests @@ -285,6 +285,17 @@ class CrtShProvider(BaseProvider): if self._stop_event and self._stop_event.is_set(): self.logger.logger.info(f"CrtSh processing cancelled before processing for domain: {query_domain}") 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() processed_issuers = set() @@ -577,4 +588,30 @@ class CrtShProvider(BaseProvider): elif query_domain.endswith(f'.{cert_domain}'): return 'parent_domain' else: - return 'related_domain' \ No newline at end of file + 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 \ No newline at end of file diff --git a/static/js/graph.js b/static/js/graph.js index 3fb5216..89efeae 100644 --- a/static/js/graph.js +++ b/static/js/graph.js @@ -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() { const allHiddenNodes = this.nodes.get({ filter: (node) => { - // Condition: Node is hidden AND it is NOT part of a large entity. - return node.hidden === true && !(node.metadata && node.metadata.large_entity_id); + // Skip nodes that are part of a large entity + 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) { + console.log(`Unhiding ${allHiddenNodes.length} nodes with valid connections`); const updates = allHiddenNodes.map(node => ({ id: node.id, hidden: false })); this.nodes.update(updates); + } else { + console.log('No eligible nodes to unhide'); } } diff --git a/static/js/main.js b/static/js/main.js index c073f7b..cc0c148 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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) { 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 += ` + + `; + } + // Relationships sections html += this.generateRelationshipsSection(node); - + // UPDATED: Enhanced attributes section with intelligent grouping (no formatting) if (node.attributes && Array.isArray(node.attributes) && node.attributes.length > 0) { html += this.generateOrganizedAttributesSection(node.attributes, node.type); } - + // Description section html += this.generateDescriptionSection(node); - + // Metadata section (collapsed by default) html += this.generateMetadataSection(node); - + 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) { if (!Array.isArray(attributes) || attributes.length === 0) { return '';