fix large entity

This commit is contained in:
overcuriousity 2025-09-13 15:38:05 +02:00
parent 53baf2e291
commit 612f414d2a
11 changed files with 45 additions and 385 deletions

View File

@ -148,7 +148,6 @@ export FLASK_ENV='production'
export FLASK_DEBUG=False export FLASK_DEBUG=False
# API keys (optional, but recommended for full functionality) # API keys (optional, but recommended for full functionality)
export VIRUSTOTAL_API_KEY="your_virustotal_key"
export SHODAN_API_KEY="your_shodan_key" export SHODAN_API_KEY="your_shodan_key"
``` ```
@ -224,7 +223,6 @@ Restart=always
Environment="SECRET_KEY=your-super-secret-and-random-key" Environment="SECRET_KEY=your-super-secret-and-random-key"
Environment="FLASK_ENV=production" Environment="FLASK_ENV=production"
Environment="FLASK_DEBUG=False" Environment="FLASK_DEBUG=False"
Environment="VIRUSTOTAL_API_KEY=your_virustotal_key"
Environment="SHODAN_API_KEY=your_shodan_key" Environment="SHODAN_API_KEY=your_shodan_key"
[Install] [Install]

4
app.py
View File

@ -384,7 +384,7 @@ def get_providers():
'statistics': stats, 'statistics': stats,
'enabled': config.is_provider_enabled(provider_name), 'enabled': config.is_provider_enabled(provider_name),
'rate_limit': config.get_rate_limit(provider_name), 'rate_limit': config.get_rate_limit(provider_name),
'requires_api_key': provider_name in ['shodan', 'virustotal'] 'requires_api_key': provider_name in ['shodan']
} }
return jsonify({ return jsonify({
@ -423,7 +423,7 @@ def set_api_keys():
updated_providers = [] updated_providers = []
for provider, api_key in data.items(): for provider, api_key in data.items():
if provider in ['shodan', 'virustotal'] and api_key.strip(): if provider in ['shodan'] and api_key.strip():
success = session_config.set_api_key(provider, api_key.strip()) success = session_config.set_api_key(provider, api_key.strip())
if success: if success:
updated_providers.append(provider) updated_providers.append(provider)

View File

@ -13,8 +13,7 @@ class Config:
def __init__(self): def __init__(self):
"""Initialize configuration with default values.""" """Initialize configuration with default values."""
self.api_keys: Dict[str, Optional[str]] = { self.api_keys: Dict[str, Optional[str]] = {
'shodan': None, 'shodan': None
'virustotal': None
} }
# Default settings # Default settings
@ -26,7 +25,6 @@ class Config:
# Rate limiting settings (requests per minute) # Rate limiting settings (requests per minute)
self.rate_limits = { self.rate_limits = {
'crtsh': 60, # Free service, be respectful 'crtsh': 60, # Free service, be respectful
'virustotal': 4, # Free tier limit
'shodan': 60, # API dependent 'shodan': 60, # API dependent
'dns': 100 # Local DNS queries 'dns': 100 # Local DNS queries
} }
@ -35,7 +33,6 @@ class Config:
self.enabled_providers = { self.enabled_providers = {
'crtsh': True, # Always enabled (free) 'crtsh': True, # Always enabled (free)
'dns': True, # Always enabled (free) 'dns': True, # Always enabled (free)
'virustotal': False, # Requires API key
'shodan': False # Requires API key 'shodan': False # Requires API key
} }
@ -53,7 +50,7 @@ class Config:
Set API key for a provider. Set API key for a provider.
Args: Args:
provider: Provider name (shodan, virustotal) provider: Provider name (shodan, etc)
api_key: API key string api_key: API key string
Returns: Returns:
@ -103,9 +100,6 @@ class Config:
def load_from_env(self): def load_from_env(self):
"""Load configuration from environment variables.""" """Load configuration from environment variables."""
if os.getenv('VIRUSTOTAL_API_KEY'):
self.set_api_key('virustotal', os.getenv('VIRUSTOTAL_API_KEY'))
if os.getenv('SHODAN_API_KEY'): if os.getenv('SHODAN_API_KEY'):
self.set_api_key('shodan', os.getenv('SHODAN_API_KEY')) self.set_api_key('shodan', os.getenv('SHODAN_API_KEY'))

View File

@ -14,7 +14,6 @@ from utils.helpers import _is_valid_ip, _is_valid_domain
from providers.crtsh_provider import CrtShProvider from providers.crtsh_provider import CrtShProvider
from providers.dns_provider import DNSProvider from providers.dns_provider import DNSProvider
from providers.shodan_provider import ShodanProvider from providers.shodan_provider import ShodanProvider
from providers.virustotal_provider import VirusTotalProvider
class ScanStatus: class ScanStatus:
@ -66,8 +65,7 @@ class Scanner:
self.provider_eligibility = { self.provider_eligibility = {
'dns': {'domains': True, 'ips': True}, 'dns': {'domains': True, 'ips': True},
'crtsh': {'domains': True, 'ips': False}, 'crtsh': {'domains': True, 'ips': False},
'shodan': {'domains': True, 'ips': True}, 'shodan': {'domains': True, 'ips': True}
'virustotal': {'domains': True, 'ips': True}
} }
# Initialize providers with session config # Initialize providers with session config
@ -169,8 +167,7 @@ class Scanner:
provider_classes = { provider_classes = {
'dns': DNSProvider, 'dns': DNSProvider,
'crtsh': CrtShProvider, 'crtsh': CrtShProvider,
'shodan': ShodanProvider, 'shodan': ShodanProvider
'virustotal': VirusTotalProvider
} }
for provider_name, provider_class in provider_classes.items(): for provider_name, provider_class in provider_classes.items():
@ -757,18 +754,24 @@ class Scanner:
def _create_large_entity(self, source: str, provider_name: str, results: List, current_depth: int) -> None: def _create_large_entity(self, source: str, provider_name: str, results: List, current_depth: int) -> None:
"""Create a large entity node for forensic tracking.""" """Create a large entity node for forensic tracking."""
entity_id = f"large_entity_{provider_name}_{hash(source) & 0x7FFFFFFF}" entity_id = f"Large Entity: {provider_name}"
# Extract targets from results # Extract targets from results
targets = [rel[1] for rel in results if len(rel) > 1] targets = []
# Determine node type
node_type = 'unknown' node_type = 'unknown'
if targets:
if _is_valid_domain(targets[0]): for rel in results:
node_type = 'domain' if len(rel) > 1:
elif _is_valid_ip(targets[0]): target = rel[1]
node_type = 'ip' targets.append(target)
# Determine node type and add node to graph
if _is_valid_domain(target):
node_type = 'domain'
self.graph.add_node(target, NodeType.DOMAIN)
elif _is_valid_ip(target):
node_type = 'ip'
self.graph.add_node(target, NodeType.IP)
# Create large entity metadata # Create large entity metadata
metadata = { metadata = {

View File

@ -17,8 +17,7 @@ class SessionConfig:
"""Initialize session config with global defaults.""" """Initialize session config with global defaults."""
# Copy all attributes from global config # Copy all attributes from global config
self.api_keys: Dict[str, Optional[str]] = { self.api_keys: Dict[str, Optional[str]] = {
'shodan': None, 'shodan': None
'virustotal': None
} }
# Default settings (copied from global config) # Default settings (copied from global config)
@ -30,7 +29,6 @@ class SessionConfig:
# Rate limiting settings (per session) # Rate limiting settings (per session)
self.rate_limits = { self.rate_limits = {
'crtsh': 60, 'crtsh': 60,
'virustotal': 4,
'shodan': 60, 'shodan': 60,
'dns': 100 'dns': 100
} }
@ -39,7 +37,6 @@ class SessionConfig:
self.enabled_providers = { self.enabled_providers = {
'crtsh': True, 'crtsh': True,
'dns': True, 'dns': True,
'virustotal': False,
'shodan': False 'shodan': False
} }
@ -57,7 +54,7 @@ class SessionConfig:
Set API key for a provider in this session. Set API key for a provider in this session.
Args: Args:
provider: Provider name (shodan, virustotal) provider: Provider name (shodan, etc)
api_key: API key string api_key: API key string
Returns: Returns:
@ -107,9 +104,6 @@ class SessionConfig:
def load_from_env(self): def load_from_env(self):
"""Load configuration from environment variables (only if not already set).""" """Load configuration from environment variables (only if not already set)."""
if os.getenv('VIRUSTOTAL_API_KEY') and not self.api_keys['virustotal']:
self.set_api_key('virustotal', os.getenv('VIRUSTOTAL_API_KEY'))
if os.getenv('SHODAN_API_KEY') and not self.api_keys['shodan']: if os.getenv('SHODAN_API_KEY') and not self.api_keys['shodan']:
self.set_api_key('shodan', os.getenv('SHODAN_API_KEY')) self.set_api_key('shodan', os.getenv('SHODAN_API_KEY'))

BIN
dump.rdb Normal file

Binary file not shown.

View File

@ -7,15 +7,13 @@ from .base_provider import BaseProvider, RateLimiter
from .crtsh_provider import CrtShProvider from .crtsh_provider import CrtShProvider
from .dns_provider import DNSProvider from .dns_provider import DNSProvider
from .shodan_provider import ShodanProvider from .shodan_provider import ShodanProvider
from .virustotal_provider import VirusTotalProvider
__all__ = [ __all__ = [
'BaseProvider', 'BaseProvider',
'RateLimiter', 'RateLimiter',
'CrtShProvider', 'CrtShProvider',
'DNSProvider', 'DNSProvider',
'ShodanProvider', 'ShodanProvider'
'VirusTotalProvider'
] ]
__version__ = "1.0.0-phase2" __version__ = "1.0.0-phase2"

View File

@ -1,333 +0,0 @@
"""
VirusTotal provider for DNSRecon.
Discovers domain relationships through passive DNS and URL analysis.
"""
import json
from typing import List, Dict, Any, Tuple
from .base_provider import BaseProvider
from utils.helpers import _is_valid_ip, _is_valid_domain
from core.graph_manager import RelationshipType
class VirusTotalProvider(BaseProvider):
"""
Provider for querying VirusTotal API for passive DNS and domain reputation data.
Now uses session-specific API keys and rate limits.
"""
def __init__(self, session_config=None):
"""Initialize VirusTotal provider with session-specific configuration."""
super().__init__(
name="virustotal",
rate_limit=4, # Free tier: 4 requests per minute
timeout=30,
session_config=session_config
)
self.base_url = "https://www.virustotal.com/vtapi/v2"
self.api_key = self.config.get_api_key('virustotal')
def is_available(self) -> bool:
"""Check if VirusTotal provider is available (has valid API key in this session)."""
return self.api_key is not None and len(self.api_key.strip()) > 0
def get_name(self) -> str:
"""Return the provider name."""
return "virustotal"
def query_domain(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""
Query VirusTotal for domain information including passive DNS.
Args:
domain: Domain to investigate
Returns:
List of relationships discovered from VirusTotal data
"""
if not _is_valid_domain(domain) or not self.is_available():
return []
relationships = []
# Query domain report
domain_relationships = self._query_domain_report(domain)
relationships.extend(domain_relationships)
# Query passive DNS for the domain
passive_dns_relationships = self._query_passive_dns_domain(domain)
relationships.extend(passive_dns_relationships)
return relationships
def query_ip(self, ip: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""
Query VirusTotal for IP address information including passive DNS.
Args:
ip: IP address to investigate
Returns:
List of relationships discovered from VirusTotal IP data
"""
if not _is_valid_ip(ip) or not self.is_available():
return []
relationships = []
# Query IP report
ip_relationships = self._query_ip_report(ip)
relationships.extend(ip_relationships)
# Query passive DNS for the IP
passive_dns_relationships = self._query_passive_dns_ip(ip)
relationships.extend(passive_dns_relationships)
return relationships
def _query_domain_report(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query VirusTotal domain report."""
relationships = []
try:
url = f"{self.base_url}/domain/report"
params = {
'apikey': self.api_key,
'domain': domain,
'allinfo': 1 # Get comprehensive information
}
response = self.make_request(url, method="GET", params=params, target_indicator=domain)
if not response or response.status_code != 200:
return []
data = response.json()
if data.get('response_code') != 1:
return []
# Extract resolved IPs
resolutions = data.get('resolutions', [])
for resolution in resolutions:
ip_address = resolution.get('ip_address')
last_resolved = resolution.get('last_resolved')
if ip_address and _is_valid_ip(ip_address):
raw_data = {
'domain': domain,
'ip_address': ip_address,
'last_resolved': last_resolved,
'source': 'virustotal_domain_report'
}
relationships.append((
domain,
ip_address,
RelationshipType.PASSIVE_DNS,
RelationshipType.PASSIVE_DNS.default_confidence,
raw_data
))
self.log_relationship_discovery(
source_node=domain,
target_node=ip_address,
relationship_type=RelationshipType.PASSIVE_DNS,
confidence_score=RelationshipType.PASSIVE_DNS.default_confidence,
raw_data=raw_data,
discovery_method="virustotal_domain_resolution"
)
# Extract subdomains
subdomains = data.get('subdomains', [])
for subdomain in subdomains:
if subdomain != domain and _is_valid_domain(subdomain):
raw_data = {
'parent_domain': domain,
'subdomain': subdomain,
'source': 'virustotal_subdomain_discovery'
}
relationships.append((
domain,
subdomain,
RelationshipType.PASSIVE_DNS,
0.7, # Medium-high confidence for subdomains
raw_data
))
self.log_relationship_discovery(
source_node=domain,
target_node=subdomain,
relationship_type=RelationshipType.PASSIVE_DNS,
confidence_score=0.7,
raw_data=raw_data,
discovery_method="virustotal_subdomain_discovery"
)
except json.JSONDecodeError as e:
self.logger.logger.error(f"Failed to parse JSON response from VirusTotal: {e}")
except Exception as e:
self.logger.logger.error(f"Error querying VirusTotal domain report for {domain}: {e}")
return relationships
def _query_ip_report(self, ip: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query VirusTotal IP report."""
relationships = []
try:
url = f"{self.base_url}/ip-address/report"
params = {
'apikey': self.api_key,
'ip': ip
}
response = self.make_request(url, method="GET", params=params, target_indicator=ip)
if not response or response.status_code != 200:
return []
data = response.json()
if data.get('response_code') != 1:
return []
# Extract resolved domains
resolutions = data.get('resolutions', [])
for resolution in resolutions:
hostname = resolution.get('hostname')
last_resolved = resolution.get('last_resolved')
if hostname and _is_valid_domain(hostname):
raw_data = {
'ip_address': ip,
'hostname': hostname,
'last_resolved': last_resolved,
'source': 'virustotal_ip_report'
}
relationships.append((
ip,
hostname,
RelationshipType.PASSIVE_DNS,
RelationshipType.PASSIVE_DNS.default_confidence,
raw_data
))
self.log_relationship_discovery(
source_node=ip,
target_node=hostname,
relationship_type=RelationshipType.PASSIVE_DNS,
confidence_score=RelationshipType.PASSIVE_DNS.default_confidence,
raw_data=raw_data,
discovery_method="virustotal_ip_resolution"
)
except json.JSONDecodeError as e:
self.logger.logger.error(f"Failed to parse JSON response from VirusTotal: {e}")
except Exception as e:
self.logger.logger.error(f"Error querying VirusTotal IP report for {ip}: {e}")
return relationships
def _query_passive_dns_domain(self, domain: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query VirusTotal passive DNS for domain."""
# Note: VirusTotal's passive DNS API might require a premium subscription
# This is a placeholder for the endpoint structure
return []
def _query_passive_dns_ip(self, ip: str) -> List[Tuple[str, str, RelationshipType, float, Dict[str, Any]]]:
"""Query VirusTotal passive DNS for IP."""
# Note: VirusTotal's passive DNS API might require a premium subscription
# This is a placeholder for the endpoint structure
return []
def get_domain_reputation(self, domain: str) -> Dict[str, Any]:
"""
Get domain reputation information from VirusTotal.
Args:
domain: Domain to check reputation for
Returns:
Dictionary containing reputation data
"""
if not _is_valid_domain(domain) or not self.is_available():
return {}
try:
url = f"{self.base_url}/domain/report"
params = {
'apikey': self.api_key,
'domain': domain
}
response = self.make_request(url, method="GET", params=params, target_indicator=domain)
if response and response.status_code == 200:
data = response.json()
if data.get('response_code') == 1:
return {
'positives': data.get('positives', 0),
'total': data.get('total', 0),
'scan_date': data.get('scan_date', ''),
'permalink': data.get('permalink', ''),
'reputation_score': self._calculate_reputation_score(data)
}
except Exception as e:
self.logger.logger.error(f"Error getting VirusTotal reputation for domain {domain}: {e}")
return {}
def get_ip_reputation(self, ip: str) -> Dict[str, Any]:
"""
Get IP reputation information from VirusTotal.
Args:
ip: IP address to check reputation for
Returns:
Dictionary containing reputation data
"""
if not _is_valid_ip(ip) or not self.is_available():
return {}
try:
url = f"{self.base_url}/ip-address/report"
params = {
'apikey': self.api_key,
'ip': ip
}
response = self.make_request(url, method="GET", params=params, target_indicator=ip)
if response and response.status_code == 200:
data = response.json()
if data.get('response_code') == 1:
return {
'positives': data.get('positives', 0),
'total': data.get('total', 0),
'scan_date': data.get('scan_date', ''),
'permalink': data.get('permalink', ''),
'reputation_score': self._calculate_reputation_score(data)
}
except Exception as e:
self.logger.logger.error(f"Error getting VirusTotal reputation for IP {ip}: {e}")
return {}
def _calculate_reputation_score(self, data: Dict[str, Any]) -> float:
"""Calculate a normalized reputation score (0.0 to 1.0)."""
positives = data.get('positives', 0)
total = data.get('total', 1) # Avoid division by zero
if total == 0:
return 1.0 # No data means neutral
# Score is inverse of detection ratio (lower detection = higher reputation)
return max(0.0, 1.0 - (positives / total))

View File

@ -270,8 +270,23 @@ class GraphManager {
this.initialize(); this.initialize();
} }
// Process nodes with enhanced attributes // Find all aggregated node IDs first
const processedNodes = graphData.nodes.map(node => this.processNode(node)); const aggregatedNodeIds = new Set();
graphData.nodes.forEach(node => {
if (node.type === 'large_entity' && node.metadata && Array.isArray(node.metadata.nodes)) {
node.metadata.nodes.forEach(nodeId => aggregatedNodeIds.add(nodeId));
}
});
// Process nodes, hiding the ones that are aggregated
const processedNodes = graphData.nodes.map(node => {
const processed = this.processNode(node);
if (aggregatedNodeIds.has(node.id)) {
processed.hidden = true; // Mark node as hidden
}
return processed;
});
const processedEdges = graphData.edges.map(edge => this.processEdge(edge)); const processedEdges = graphData.edges.map(edge => this.processEdge(edge));
// Update datasets with animation // Update datasets with animation
@ -441,7 +456,8 @@ class GraphManager {
'domain': 12, 'domain': 12,
'ip': 14, 'ip': 14,
'asn': 16, 'asn': 16,
'correlation_object': 8 'correlation_object': 8,
'large_entity': 12
}; };
return sizes[nodeType] || 12; return sizes[nodeType] || 12;
} }
@ -456,7 +472,8 @@ class GraphManager {
'domain': 'dot', 'domain': 'dot',
'ip': 'square', 'ip': 'square',
'asn': 'triangle', 'asn': 'triangle',
'correlation_object': 'hexagon' 'correlation_object': 'hexagon',
'large_entity': 'database'
}; };
return shapes[nodeType] || 'dot'; return shapes[nodeType] || 'dot';
} }

View File

@ -80,7 +80,6 @@ class DNSReconApp {
// API Key Modal elements // API Key Modal elements
apiKeyModal: document.getElementById('api-key-modal'), apiKeyModal: document.getElementById('api-key-modal'),
apiKeyModalClose: document.getElementById('api-key-modal-close'), apiKeyModalClose: document.getElementById('api-key-modal-close'),
virustotalApiKey: document.getElementById('virustotal-api-key'),
shodanApiKey: document.getElementById('shodan-api-key'), shodanApiKey: document.getElementById('shodan-api-key'),
saveApiKeys: document.getElementById('save-api-keys'), saveApiKeys: document.getElementById('save-api-keys'),
resetApiKeys: document.getElementById('reset-api-keys'), resetApiKeys: document.getElementById('reset-api-keys'),
@ -851,13 +850,11 @@ class DNSReconApp {
detailsHtml += createDetailRow('Related Domains (SAN)', metadata.related_domains_san); detailsHtml += createDetailRow('Related Domains (SAN)', metadata.related_domains_san);
detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns); detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns);
detailsHtml += createDetailRow('Shodan Data', metadata.shodan); detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
detailsHtml += createDetailRow('VirusTotal Data', metadata.virustotal);
break; break;
case 'ip': case 'ip':
detailsHtml += createDetailRow('Hostnames', metadata.hostnames); detailsHtml += createDetailRow('Hostnames', metadata.hostnames);
detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns); detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns);
detailsHtml += createDetailRow('Shodan Data', metadata.shodan); detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
detailsHtml += createDetailRow('VirusTotal Data', metadata.virustotal);
break; break;
case 'correlation_object': case 'correlation_object':
detailsHtml += createDetailRow('Correlated Value', metadata.value); detailsHtml += createDetailRow('Correlated Value', metadata.value);
@ -974,11 +971,9 @@ class DNSReconApp {
*/ */
async saveApiKeys() { async saveApiKeys() {
const shodanKey = this.elements.shodanApiKey.value.trim(); const shodanKey = this.elements.shodanApiKey.value.trim();
const virustotalKey = this.elements.virustotalApiKey.value.trim();
const keys = {}; const keys = {};
if (shodanKey) keys.shodan = shodanKey; if (shodanKey) keys.shodan = shodanKey;
if (virustotalKey) keys.virustotal = virustotalKey;
if (Object.keys(keys).length === 0) { if (Object.keys(keys).length === 0) {
this.showWarning('No API keys were entered.'); this.showWarning('No API keys were entered.');
@ -1004,7 +999,6 @@ class DNSReconApp {
*/ */
resetApiKeys() { resetApiKeys() {
this.elements.shodanApiKey.value = ''; this.elements.shodanApiKey.value = '';
this.elements.virustotalApiKey.value = '';
} }
/** /**

View File

@ -217,11 +217,6 @@
<p class="modal-description"> <p class="modal-description">
Enter your API keys for enhanced data providers. Keys are stored in memory for the current session only and are never saved to disk. Enter your API keys for enhanced data providers. Keys are stored in memory for the current session only and are never saved to disk.
</p> </p>
<div class="apikey-section">
<label for="virustotal-api-key">VirusTotal API Key</label>
<input type="password" id="virustotal-api-key" placeholder="Enter VirusTotal API Key">
<p class="apikey-help">Enables passive DNS and domain reputation lookups.</p>
</div>
<div class="apikey-section"> <div class="apikey-section">
<label for="shodan-api-key">Shodan API Key</label> <label for="shodan-api-key">Shodan API Key</label>
<input type="password" id="shodan-api-key" placeholder="Enter Shodan API Key"> <input type="password" id="shodan-api-key" placeholder="Enter Shodan API Key">