UX improvements
This commit is contained in:
parent
d0ee415f0d
commit
8ae4fdbf80
@ -31,4 +31,4 @@ LARGE_ENTITY_THRESHOLD=100
|
|||||||
# The number of times to retry a target if a provider fails.
|
# The number of times to retry a target if a provider fails.
|
||||||
MAX_RETRIES_PER_TARGET=8
|
MAX_RETRIES_PER_TARGET=8
|
||||||
# How long cached provider responses are stored (in hours).
|
# How long cached provider responses are stored (in hours).
|
||||||
CACHE_EXPIRY_HOURS=12
|
CACHE_TIMEOUT_HOURS=12
|
||||||
|
|||||||
116
app.py
116
app.py
@ -10,6 +10,7 @@ import traceback
|
|||||||
from flask import Flask, render_template, request, jsonify, send_file, session
|
from flask import Flask, render_template, request, jsonify, send_file, session
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
from core.session_manager import session_manager
|
from core.session_manager import session_manager
|
||||||
from config import config
|
from config import config
|
||||||
@ -304,21 +305,6 @@ def export_results():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': f'Export failed: {str(e)}'}), 500
|
return jsonify({'success': False, 'error': f'Export failed: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/providers', methods=['GET'])
|
|
||||||
def get_providers():
|
|
||||||
"""Get information about available providers."""
|
|
||||||
try:
|
|
||||||
user_session_id, scanner = get_user_scanner()
|
|
||||||
provider_info = scanner.get_provider_info()
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'providers': provider_info, 'user_session_id': user_session_id})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
return jsonify({'success': False, 'error': f'Internal server error: {str(e)}'}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/config/api-keys', methods=['POST'])
|
@app.route('/api/config/api-keys', methods=['POST'])
|
||||||
def set_api_keys():
|
def set_api_keys():
|
||||||
"""Set API keys for the current session."""
|
"""Set API keys for the current session."""
|
||||||
@ -355,6 +341,106 @@ def set_api_keys():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': f'Internal server error: {str(e)}'}), 500
|
return jsonify({'success': False, 'error': f'Internal server error: {str(e)}'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/providers', methods=['GET'])
|
||||||
|
def get_providers():
|
||||||
|
"""Get enhanced information about available providers including API key sources."""
|
||||||
|
try:
|
||||||
|
user_session_id, scanner = get_user_scanner()
|
||||||
|
base_provider_info = scanner.get_provider_info()
|
||||||
|
|
||||||
|
# Enhance provider info with API key source information
|
||||||
|
enhanced_provider_info = {}
|
||||||
|
|
||||||
|
for provider_name, info in base_provider_info.items():
|
||||||
|
enhanced_info = dict(info) # Copy base info
|
||||||
|
|
||||||
|
if info['requires_api_key']:
|
||||||
|
# Determine API key source and configuration status
|
||||||
|
api_key = scanner.config.get_api_key(provider_name)
|
||||||
|
backend_api_key = os.getenv(f'{provider_name.upper()}_API_KEY')
|
||||||
|
|
||||||
|
if backend_api_key:
|
||||||
|
# API key configured via backend/environment
|
||||||
|
enhanced_info.update({
|
||||||
|
'api_key_configured': True,
|
||||||
|
'api_key_source': 'backend',
|
||||||
|
'api_key_help': f'API key configured via environment variable {provider_name.upper()}_API_KEY'
|
||||||
|
})
|
||||||
|
elif api_key:
|
||||||
|
# API key configured via web interface
|
||||||
|
enhanced_info.update({
|
||||||
|
'api_key_configured': True,
|
||||||
|
'api_key_source': 'frontend',
|
||||||
|
'api_key_help': f'API key set via web interface (session-only)'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# No API key configured
|
||||||
|
enhanced_info.update({
|
||||||
|
'api_key_configured': False,
|
||||||
|
'api_key_source': None,
|
||||||
|
'api_key_help': f'Requires API key to enable {info["display_name"]} integration'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Provider doesn't require API key
|
||||||
|
enhanced_info.update({
|
||||||
|
'api_key_configured': True, # Always "configured" for non-API providers
|
||||||
|
'api_key_source': None,
|
||||||
|
'api_key_help': None
|
||||||
|
})
|
||||||
|
|
||||||
|
enhanced_provider_info[provider_name] = enhanced_info
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'providers': enhanced_provider_info,
|
||||||
|
'user_session_id': user_session_id
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': f'Internal server error: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/config/providers', methods=['POST'])
|
||||||
|
def configure_providers():
|
||||||
|
"""Configure provider settings (enable/disable)."""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if data is None:
|
||||||
|
return jsonify({'success': False, 'error': 'No provider settings provided'}), 400
|
||||||
|
|
||||||
|
user_session_id, scanner = get_user_scanner()
|
||||||
|
session_config = scanner.config
|
||||||
|
|
||||||
|
updated_providers = []
|
||||||
|
|
||||||
|
for provider_name, settings in data.items():
|
||||||
|
provider_name_clean = provider_name.lower().strip()
|
||||||
|
|
||||||
|
if 'enabled' in settings:
|
||||||
|
# Update the enabled state in session config
|
||||||
|
session_config.enabled_providers[provider_name_clean] = settings['enabled']
|
||||||
|
updated_providers.append(provider_name_clean)
|
||||||
|
|
||||||
|
if updated_providers:
|
||||||
|
# Reinitialize providers with new settings
|
||||||
|
scanner._initialize_providers()
|
||||||
|
session_manager.update_session_scanner(user_session_id, scanner)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Provider settings updated for: {", ".join(updated_providers)}',
|
||||||
|
'user_session_id': user_session_id
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({'success': False, 'error': 'No valid provider settings were provided.'}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': f'Internal server error: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(error):
|
def not_found(error):
|
||||||
"""Handle 404 errors."""
|
"""Handle 404 errors."""
|
||||||
|
|||||||
56
config.py
56
config.py
@ -25,7 +25,6 @@ class Config:
|
|||||||
self.max_concurrent_requests = 1
|
self.max_concurrent_requests = 1
|
||||||
self.large_entity_threshold = 100
|
self.large_entity_threshold = 100
|
||||||
self.max_retries_per_target = 8
|
self.max_retries_per_target = 8
|
||||||
self.cache_expiry_hours = 12
|
|
||||||
|
|
||||||
# --- Provider Caching Settings ---
|
# --- Provider Caching Settings ---
|
||||||
self.cache_timeout_hours = 6 # Provider-specific cache timeout
|
self.cache_timeout_hours = 6 # Provider-specific cache timeout
|
||||||
@ -69,7 +68,6 @@ class Config:
|
|||||||
self.max_concurrent_requests = int(os.getenv('MAX_CONCURRENT_REQUESTS', self.max_concurrent_requests))
|
self.max_concurrent_requests = int(os.getenv('MAX_CONCURRENT_REQUESTS', self.max_concurrent_requests))
|
||||||
self.large_entity_threshold = int(os.getenv('LARGE_ENTITY_THRESHOLD', self.large_entity_threshold))
|
self.large_entity_threshold = int(os.getenv('LARGE_ENTITY_THRESHOLD', self.large_entity_threshold))
|
||||||
self.max_retries_per_target = int(os.getenv('MAX_RETRIES_PER_TARGET', self.max_retries_per_target))
|
self.max_retries_per_target = int(os.getenv('MAX_RETRIES_PER_TARGET', self.max_retries_per_target))
|
||||||
self.cache_expiry_hours = int(os.getenv('CACHE_EXPIRY_HOURS', self.cache_expiry_hours))
|
|
||||||
self.cache_timeout_hours = int(os.getenv('CACHE_TIMEOUT_HOURS', self.cache_timeout_hours))
|
self.cache_timeout_hours = int(os.getenv('CACHE_TIMEOUT_HOURS', self.cache_timeout_hours))
|
||||||
|
|
||||||
# Override Flask and session settings
|
# Override Flask and session settings
|
||||||
@ -87,6 +85,60 @@ class Config:
|
|||||||
self.enabled_providers[provider] = True
|
self.enabled_providers[provider] = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def set_provider_enabled(self, provider: str, enabled: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Set provider enabled status for the session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
provider: Provider name
|
||||||
|
enabled: Whether the provider should be enabled
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the setting was applied successfully
|
||||||
|
"""
|
||||||
|
provider_key = provider.lower()
|
||||||
|
self.enabled_providers[provider_key] = enabled
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_provider_enabled(self, provider: str) -> bool:
|
||||||
|
"""
|
||||||
|
Get provider enabled status.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
provider: Provider name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the provider is enabled
|
||||||
|
"""
|
||||||
|
provider_key = provider.lower()
|
||||||
|
return self.enabled_providers.get(provider_key, True) # Default to enabled
|
||||||
|
|
||||||
|
def bulk_set_provider_settings(self, provider_settings: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Set multiple provider settings at once.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
provider_settings: Dict of provider_name -> {'enabled': bool, ...}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with results for each provider
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for provider_name, settings in provider_settings.items():
|
||||||
|
provider_key = provider_name.lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if 'enabled' in settings:
|
||||||
|
self.enabled_providers[provider_key] = settings['enabled']
|
||||||
|
results[provider_key] = {'success': True, 'enabled': settings['enabled']}
|
||||||
|
else:
|
||||||
|
results[provider_key] = {'success': False, 'error': 'No enabled setting provided'}
|
||||||
|
except Exception as e:
|
||||||
|
results[provider_key] = {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
def get_api_key(self, provider: str) -> Optional[str]:
|
def get_api_key(self, provider: str) -> Optional[str]:
|
||||||
"""Get API key for a provider."""
|
"""Get API key for a provider."""
|
||||||
return self.api_keys.get(provider)
|
return self.api_keys.get(provider)
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
Graph data model for DNSRecon using NetworkX.
|
Graph data model for DNSRecon using NetworkX.
|
||||||
Manages in-memory graph storage with confidence scoring and forensic metadata.
|
Manages in-memory graph storage with confidence scoring and forensic metadata.
|
||||||
Now fully compatible with the unified ProviderResult data model.
|
Now fully compatible with the unified ProviderResult data model.
|
||||||
UPDATED: Fixed certificate styling and correlation edge labeling.
|
UPDATED: Fixed correlation exclusion keys to match actual attribute names.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@ -41,7 +41,30 @@ class GraphManager:
|
|||||||
self.correlation_index = {}
|
self.correlation_index = {}
|
||||||
# Compile regex for date filtering for efficiency
|
# Compile regex for date filtering for efficiency
|
||||||
self.date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}')
|
self.date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}')
|
||||||
self.EXCLUDED_KEYS = ['crtsh_cert_validity_period_days','crtsh_cert_source','crtsh_cert_common_name']
|
|
||||||
|
# These are the actual attribute names created in providers, WITHOUT provider prefix
|
||||||
|
self.EXCLUDED_KEYS = [
|
||||||
|
# Certificate metadata that creates noise
|
||||||
|
'cert_source', # Always 'crtsh' for crtsh provider
|
||||||
|
'cert_common_name',
|
||||||
|
'cert_validity_period_days', # Numerical, not useful for correlation
|
||||||
|
#'cert_certificate_id', # Unique per certificate
|
||||||
|
#'cert_serial_number', # Unique per certificate
|
||||||
|
'cert_entry_timestamp', # Timestamp, filtered by date regex anyway
|
||||||
|
'cert_not_before', # Date, filtered by date regex anyway
|
||||||
|
'cert_not_after', # Date, filtered by date regex anyway
|
||||||
|
# DNS metadata that creates noise
|
||||||
|
'dns_ttl', # TTL values are not meaningful for correlation
|
||||||
|
# Shodan metadata that might create noise
|
||||||
|
'timestamp', # Generic timestamp fields
|
||||||
|
'last_update', # Generic timestamp fields
|
||||||
|
#'org', # Too generic, causes false correlations
|
||||||
|
#'isp', # Too generic, causes false correlations
|
||||||
|
# Generic noisy attributes
|
||||||
|
'updated_timestamp', # Any timestamp field
|
||||||
|
'discovery_timestamp', # Any timestamp field
|
||||||
|
'query_timestamp', # Any timestamp field
|
||||||
|
]
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
"""Prepare GraphManager for pickling, excluding compiled regex."""
|
"""Prepare GraphManager for pickling, excluding compiled regex."""
|
||||||
@ -72,14 +95,31 @@ class GraphManager:
|
|||||||
attr_value = attr.get('value')
|
attr_value = attr.get('value')
|
||||||
attr_provider = attr.get('provider', 'unknown')
|
attr_provider = attr.get('provider', 'unknown')
|
||||||
|
|
||||||
# Skip excluded attributes and invalid values
|
# IMPROVED: More comprehensive exclusion logic
|
||||||
if any(excluded_key in attr_name for excluded_key in self.EXCLUDED_KEYS) or not isinstance(attr_value, (str, int, float, bool)) or attr_value is None:
|
should_exclude = (
|
||||||
continue
|
# Check against excluded keys (exact match or substring)
|
||||||
|
any(excluded_key in attr_name or attr_name == excluded_key for excluded_key in self.EXCLUDED_KEYS) or
|
||||||
|
# Invalid value types
|
||||||
|
not isinstance(attr_value, (str, int, float, bool)) or
|
||||||
|
attr_value is None or
|
||||||
|
# Boolean values are not useful for correlation
|
||||||
|
isinstance(attr_value, bool) or
|
||||||
|
# String values that are too short or are dates
|
||||||
|
(isinstance(attr_value, str) and (
|
||||||
|
len(attr_value) < 4 or
|
||||||
|
self.date_pattern.match(attr_value) or
|
||||||
|
# Exclude common generic values that create noise
|
||||||
|
attr_value.lower() in ['unknown', 'none', 'null', 'n/a', 'true', 'false', '0', '1']
|
||||||
|
)) or
|
||||||
|
# Numerical values that are likely to be unique identifiers
|
||||||
|
(isinstance(attr_value, (int, float)) and (
|
||||||
|
attr_value == 0 or # Zero values are not meaningful
|
||||||
|
attr_value == 1 or # One values are too common
|
||||||
|
abs(attr_value) > 1000000 # Very large numbers are likely IDs
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(attr_value, bool):
|
if should_exclude:
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(attr_value, str) and (len(attr_value) < 4 or self.date_pattern.match(attr_value)):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Initialize correlation tracking for this value
|
# Initialize correlation tracking for this value
|
||||||
@ -149,7 +189,7 @@ class GraphManager:
|
|||||||
|
|
||||||
if self.graph.has_node(node_id) and not self.graph.has_edge(node_id, correlation_node_id):
|
if self.graph.has_node(node_id) and not self.graph.has_edge(node_id, correlation_node_id):
|
||||||
# Format relationship label as "corr_provider_attribute"
|
# Format relationship label as "corr_provider_attribute"
|
||||||
relationship_label = f"{provider}_{attribute}"
|
relationship_label = f"corr_{provider}_{attribute}"
|
||||||
|
|
||||||
self.add_edge(
|
self.add_edge(
|
||||||
source_id=node_id,
|
source_id=node_id,
|
||||||
@ -170,7 +210,7 @@ class GraphManager:
|
|||||||
def _has_direct_edge_bidirectional(self, node_a: str, node_b: str) -> bool:
|
def _has_direct_edge_bidirectional(self, node_a: str, node_b: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if there's a direct edge between two nodes in either direction.
|
Check if there's a direct edge between two nodes in either direction.
|
||||||
Returns True if node_a→node_b OR node_b→node_a exists.
|
Returns True if node_aâ†'node_b OR node_bâ†'node_a exists.
|
||||||
"""
|
"""
|
||||||
return (self.graph.has_edge(node_a, node_b) or
|
return (self.graph.has_edge(node_a, node_b) or
|
||||||
self.graph.has_edge(node_b, node_a))
|
self.graph.has_edge(node_b, node_a))
|
||||||
@ -410,12 +450,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_neighbors(self, node_id: str) -> List[str]:
|
|
||||||
"""Get all unique neighbors (predecessors and successors) for a node."""
|
|
||||||
if not self.graph.has_node(node_id):
|
|
||||||
return []
|
|
||||||
return list(set(self.graph.predecessors(node_id)) | set(self.graph.successors(node_id)))
|
|
||||||
|
|
||||||
def get_high_confidence_edges(self, min_confidence: float = 0.8) -> List[Tuple[str, str, Dict]]:
|
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."""
|
"""Get edges with confidence score above a given threshold."""
|
||||||
return [(u, v, d) for u, v, d in self.graph.edges(data=True)
|
return [(u, v, d) for u, v, d in self.graph.edges(data=True)
|
||||||
|
|||||||
@ -101,6 +101,7 @@ class ProviderResult:
|
|||||||
"""Get the total number of attributes in this result."""
|
"""Get the total number of attributes in this result."""
|
||||||
return len(self.attributes)
|
return len(self.attributes)
|
||||||
|
|
||||||
def is_large_entity(self, threshold: int) -> bool:
|
##TODO
|
||||||
"""Check if this result qualifies as a large entity based on relationship count."""
|
#def is_large_entity(self, threshold: int) -> bool:
|
||||||
return self.get_relationship_count() > threshold
|
# """Check if this result qualifies as a large entity based on relationship count."""
|
||||||
|
# return self.get_relationship_count() > threshold
|
||||||
@ -370,6 +370,7 @@ class Scanner:
|
|||||||
task_tuple = (provider_name, target_item)
|
task_tuple = (provider_name, target_item)
|
||||||
if task_tuple in processed_tasks:
|
if task_tuple in processed_tasks:
|
||||||
self.tasks_skipped += 1
|
self.tasks_skipped += 1
|
||||||
|
self.indicators_completed +=1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if depth > max_depth:
|
if depth > max_depth:
|
||||||
@ -405,7 +406,7 @@ class Scanner:
|
|||||||
if self.target_retries[task_tuple] <= self.config.max_retries_per_target:
|
if self.target_retries[task_tuple] <= self.config.max_retries_per_target:
|
||||||
self.task_queue.put((priority, (provider_name, target_item, depth)))
|
self.task_queue.put((priority, (provider_name, target_item, depth)))
|
||||||
self.tasks_re_enqueued += 1
|
self.tasks_re_enqueued += 1
|
||||||
self.total_tasks_ever_enqueued += 1
|
#self.total_tasks_ever_enqueued += 1
|
||||||
else:
|
else:
|
||||||
self.scan_failed_due_to_retries = True
|
self.scan_failed_due_to_retries = True
|
||||||
self._log_target_processing_error(str(task_tuple), "Max retries exceeded")
|
self._log_target_processing_error(str(task_tuple), "Max retries exceeded")
|
||||||
|
|||||||
@ -108,64 +108,6 @@ class SessionManager:
|
|||||||
print(f"ERROR: Failed to create session {session_id}: {e}")
|
print(f"ERROR: Failed to create session {session_id}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def clone_session_preserving_config(self, source_session_id: str) -> str:
|
|
||||||
"""
|
|
||||||
FIXED: Create a new session that preserves the configuration (including API keys) from an existing session.
|
|
||||||
This is used when we need a fresh scanner but want to keep user configuration.
|
|
||||||
"""
|
|
||||||
with self.creation_lock:
|
|
||||||
print(f"=== CLONING SESSION {source_session_id} (PRESERVING CONFIG) ===")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get the source session data
|
|
||||||
source_session_data = self._get_session_data(source_session_id)
|
|
||||||
if not source_session_data:
|
|
||||||
print(f"ERROR: Source session {source_session_id} not found for cloning")
|
|
||||||
return self.create_session() # Fallback to new session
|
|
||||||
|
|
||||||
# Create new session ID
|
|
||||||
new_session_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Get the preserved configuration
|
|
||||||
preserved_config = source_session_data.get('config')
|
|
||||||
if not preserved_config:
|
|
||||||
print(f"WARNING: No config found in source session, creating new")
|
|
||||||
from core.session_config import create_session_config
|
|
||||||
preserved_config = create_session_config()
|
|
||||||
|
|
||||||
print(f"Preserving config with API keys: {list(preserved_config.api_keys.keys())}")
|
|
||||||
|
|
||||||
# Create new scanner with preserved config
|
|
||||||
new_scanner = Scanner(session_config=preserved_config)
|
|
||||||
new_scanner.session_id = new_session_id
|
|
||||||
|
|
||||||
|
|
||||||
new_session_data = {
|
|
||||||
'scanner': new_scanner,
|
|
||||||
'config': preserved_config,
|
|
||||||
'created_at': time.time(),
|
|
||||||
'last_activity': time.time(),
|
|
||||||
'status': 'active',
|
|
||||||
'cloned_from': source_session_id
|
|
||||||
}
|
|
||||||
|
|
||||||
# Store in Redis
|
|
||||||
serialized_data = pickle.dumps(new_session_data)
|
|
||||||
session_key = self._get_session_key(new_session_id)
|
|
||||||
self.redis_client.setex(session_key, self.session_timeout, serialized_data)
|
|
||||||
|
|
||||||
# Initialize stop signal
|
|
||||||
stop_key = self._get_stop_signal_key(new_session_id)
|
|
||||||
self.redis_client.setex(stop_key, self.session_timeout, b'0')
|
|
||||||
|
|
||||||
print(f"Cloned session {new_session_id} with preserved configuration")
|
|
||||||
return new_session_id
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Failed to clone session {source_session_id}: {e}")
|
|
||||||
# Fallback to creating a new session
|
|
||||||
return self.create_session()
|
|
||||||
|
|
||||||
def set_stop_signal(self, session_id: str) -> bool:
|
def set_stop_signal(self, session_id: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Set the stop signal for a session (cross-process safe).
|
Set the stop signal for a session (cross-process safe).
|
||||||
|
|||||||
@ -502,76 +502,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 _find_shared_certificates(self, certs1: List[Dict[str, Any]], certs2: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
||||||
"""Find certificates that are shared between two domain certificate lists."""
|
|
||||||
shared = []
|
|
||||||
|
|
||||||
cert1_ids = set()
|
|
||||||
for cert in certs1:
|
|
||||||
cert_id = cert.get('certificate_id')
|
|
||||||
if cert_id and isinstance(cert_id, (int, str, float, bool, tuple)):
|
|
||||||
cert1_ids.add(cert_id)
|
|
||||||
|
|
||||||
for cert in certs2:
|
|
||||||
cert_id = cert.get('certificate_id')
|
|
||||||
if cert_id and isinstance(cert_id, (int, str, float, bool, tuple)):
|
|
||||||
if cert_id in cert1_ids:
|
|
||||||
shared.append(cert)
|
|
||||||
|
|
||||||
return shared
|
|
||||||
|
|
||||||
def _summarize_certificates(self, certificates: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
||||||
"""Create a summary of certificates for a domain."""
|
|
||||||
if not certificates:
|
|
||||||
return {
|
|
||||||
'total_certificates': 0,
|
|
||||||
'valid_certificates': 0,
|
|
||||||
'expired_certificates': 0,
|
|
||||||
'expires_soon_count': 0,
|
|
||||||
'unique_issuers': [],
|
|
||||||
'latest_certificate': None,
|
|
||||||
'has_valid_cert': False,
|
|
||||||
'certificate_details': []
|
|
||||||
}
|
|
||||||
|
|
||||||
valid_count = sum(1 for cert in certificates if cert.get('is_currently_valid'))
|
|
||||||
expired_count = len(certificates) - valid_count
|
|
||||||
expires_soon_count = sum(1 for cert in certificates if cert.get('expires_soon'))
|
|
||||||
|
|
||||||
unique_issuers = list(set(cert.get('issuer_name') for cert in certificates if cert.get('issuer_name')))
|
|
||||||
|
|
||||||
# Find the most recent certificate
|
|
||||||
latest_cert = None
|
|
||||||
latest_date = None
|
|
||||||
|
|
||||||
for cert in certificates:
|
|
||||||
try:
|
|
||||||
if cert.get('not_before'):
|
|
||||||
cert_date = self._parse_certificate_date(cert['not_before'])
|
|
||||||
if latest_date is None or cert_date > latest_date:
|
|
||||||
latest_date = cert_date
|
|
||||||
latest_cert = cert
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Sort certificates by date for better display (newest first)
|
|
||||||
sorted_certificates = sorted(
|
|
||||||
certificates,
|
|
||||||
key=lambda c: self._get_certificate_sort_date(c),
|
|
||||||
reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'total_certificates': len(certificates),
|
|
||||||
'valid_certificates': valid_count,
|
|
||||||
'expired_certificates': expired_count,
|
|
||||||
'expires_soon_count': expires_soon_count,
|
|
||||||
'unique_issuers': unique_issuers,
|
|
||||||
'latest_certificate': latest_cert,
|
|
||||||
'has_valid_cert': valid_count > 0,
|
|
||||||
'certificate_details': sorted_certificates
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_certificate_sort_date(self, cert: Dict[str, Any]) -> datetime:
|
def _get_certificate_sort_date(self, cert: Dict[str, Any]) -> datetime:
|
||||||
"""Get a sortable date from certificate data for chronological ordering."""
|
"""Get a sortable date from certificate data for chronological ordering."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -2115,3 +2115,184 @@ input[type="text"]:focus, select:focus {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add these styles to the existing main.css file - they use existing design patterns */
|
||||||
|
|
||||||
|
/* Settings Modal Enhancements - using existing provider-item styles */
|
||||||
|
#settings-modal .modal-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-modal .modal-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Provider configuration section using existing provider-item styling */
|
||||||
|
#provider-config-list .provider-item {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#provider-config-list .provider-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#provider-config-list .provider-item:hover {
|
||||||
|
border-color: #444;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Provider toggle styling - extends existing checkbox pattern */
|
||||||
|
.provider-toggle {
|
||||||
|
appearance: none !important;
|
||||||
|
width: 16px !important;
|
||||||
|
height: 16px !important;
|
||||||
|
border: 2px solid #555 !important;
|
||||||
|
background: #1a1a1a !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
position: relative !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-toggle:checked {
|
||||||
|
background: #00ff41 !important;
|
||||||
|
border-color: #00ff41 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-toggle:checked::after {
|
||||||
|
content: '✓' !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: -3px !important;
|
||||||
|
left: 1px !important;
|
||||||
|
color: #1a1a1a !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-toggle:hover {
|
||||||
|
border-color: #00ff41 !important;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 255, 65, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* API Key status row styling - uses existing status-row pattern */
|
||||||
|
.api-key-status-row {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-status-row.configured {
|
||||||
|
background: rgba(0, 255, 65, 0.1);
|
||||||
|
border-color: rgba(0, 255, 65, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-status-row.not-configured {
|
||||||
|
background: rgba(255, 153, 0, 0.1);
|
||||||
|
border-color: rgba(255, 153, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings modal button styling - uses existing button classes */
|
||||||
|
#settings-modal .button-group {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear button specific styling - extends btn-secondary */
|
||||||
|
.clear-api-key-btn {
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
padding: 0.4rem 0.8rem !important;
|
||||||
|
min-width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-api-key-btn:disabled {
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Provider configuration checkbox label styling */
|
||||||
|
#provider-config-list .status-row label {
|
||||||
|
color: #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#provider-config-list .status-row label:hover {
|
||||||
|
color: #00ff41;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings section count badges - uses existing merge-badge styling */
|
||||||
|
#provider-count,
|
||||||
|
#api-key-count {
|
||||||
|
background: #444;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* API key help text styling - uses existing patterns */
|
||||||
|
#settings-modal .apikey-help {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal section content spacing */
|
||||||
|
#settings-modal .modal-section-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-modal .modal-section-content .provider-item {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition effects for API key clearing */
|
||||||
|
.api-key-clearing {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-cleared {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings modal responsiveness - extends existing responsive patterns */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#settings-modal .modal-content {
|
||||||
|
width: 95%;
|
||||||
|
margin: 5% auto;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#provider-config-list .provider-item {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-modal .provider-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-modal .button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,8 +34,8 @@ class DNSReconApp {
|
|||||||
this.updateStatus();
|
this.updateStatus();
|
||||||
this.loadProviders();
|
this.loadProviders();
|
||||||
this.initializeEnhancedModals();
|
this.initializeEnhancedModals();
|
||||||
|
this.addCheckboxStyling();
|
||||||
|
|
||||||
// FIXED: Force initial graph update to handle empty sessions properly
|
|
||||||
this.updateGraph();
|
this.updateGraph();
|
||||||
|
|
||||||
console.log('DNSRecon application initialized successfully');
|
console.log('DNSRecon application initialized successfully');
|
||||||
@ -81,9 +81,7 @@ class DNSReconApp {
|
|||||||
// Settings Modal elements
|
// Settings Modal elements
|
||||||
settingsModal: document.getElementById('settings-modal'),
|
settingsModal: document.getElementById('settings-modal'),
|
||||||
settingsModalClose: document.getElementById('settings-modal-close'),
|
settingsModalClose: document.getElementById('settings-modal-close'),
|
||||||
apiKeyInputs: document.getElementById('api-key-inputs'),
|
|
||||||
saveApiKeys: document.getElementById('save-api-keys'),
|
|
||||||
resetApiKeys: document.getElementById('reset-api-keys'),
|
|
||||||
|
|
||||||
// Other elements
|
// Other elements
|
||||||
sessionId: document.getElementById('session-id'),
|
sessionId: document.getElementById('session-id'),
|
||||||
@ -182,10 +180,21 @@ class DNSReconApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.elements.saveApiKeys) {
|
if (this.elements.saveApiKeys) {
|
||||||
this.elements.saveApiKeys.addEventListener('click', () => this.saveApiKeys());
|
this.elements.saveApiKeys.removeEventListener('click', this.saveApiKeys);
|
||||||
}
|
}
|
||||||
if (this.elements.resetApiKeys) {
|
if (this.elements.resetApiKeys) {
|
||||||
this.elements.resetApiKeys.addEventListener('click', () => this.resetApiKeys());
|
this.elements.resetApiKeys.removeEventListener('click', this.resetApiKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup new handlers
|
||||||
|
const saveSettingsBtn = document.getElementById('save-settings');
|
||||||
|
const resetSettingsBtn = document.getElementById('reset-settings');
|
||||||
|
|
||||||
|
if (saveSettingsBtn) {
|
||||||
|
saveSettingsBtn.addEventListener('click', () => this.saveSettings());
|
||||||
|
}
|
||||||
|
if (resetSettingsBtn) {
|
||||||
|
resetSettingsBtn.addEventListener('click', () => this.resetSettings());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for the custom event from the graph
|
// Listen for the custom event from the graph
|
||||||
@ -724,7 +733,7 @@ class DNSReconApp {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.updateProviderDisplay(response.providers);
|
this.updateProviderDisplay(response.providers);
|
||||||
this.buildApiKeyModal(response.providers);
|
this.buildSettingsModal(response.providers); // Updated to use new function
|
||||||
console.log('Providers loaded successfully');
|
console.log('Providers loaded successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -733,6 +742,411 @@ class DNSReconApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the enhanced settings modal with provider configuration and API keys
|
||||||
|
* @param {Object} providers - Provider information from backend
|
||||||
|
*/
|
||||||
|
buildSettingsModal(providers) {
|
||||||
|
this.buildProviderConfigSection(providers);
|
||||||
|
this.buildApiKeySection(providers);
|
||||||
|
this.updateSettingsCounts(providers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the provider configuration section with enable/disable checkboxes
|
||||||
|
* @param {Object} providers - Provider information
|
||||||
|
*/
|
||||||
|
buildProviderConfigSection(providers) {
|
||||||
|
const providerConfigList = document.getElementById('provider-config-list');
|
||||||
|
if (!providerConfigList) return;
|
||||||
|
|
||||||
|
providerConfigList.innerHTML = '';
|
||||||
|
|
||||||
|
for (const [name, info] of Object.entries(providers)) {
|
||||||
|
const providerConfig = document.createElement('div');
|
||||||
|
providerConfig.className = 'provider-item';
|
||||||
|
|
||||||
|
const statusClass = info.enabled ? 'enabled' : 'disabled';
|
||||||
|
const statusIcon = info.enabled ? '✓' : '✗';
|
||||||
|
|
||||||
|
providerConfig.innerHTML = `
|
||||||
|
<div class="provider-header">
|
||||||
|
<div class="provider-name">${info.display_name}</div>
|
||||||
|
<div class="provider-status ${statusClass}">
|
||||||
|
${statusIcon} ${info.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-row">
|
||||||
|
<div class="status-label">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<input type="checkbox"
|
||||||
|
data-provider="${name}"
|
||||||
|
class="provider-toggle"
|
||||||
|
${info.enabled ? 'checked' : ''}
|
||||||
|
style="appearance: none; width: 16px; height: 16px; border: 2px solid #555; background: #1a1a1a; cursor: pointer; position: relative;">
|
||||||
|
<span>Auto-process with this provider</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
providerConfigList.appendChild(providerConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add checkbox styling and event handlers
|
||||||
|
this.setupProviderCheckboxes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup provider checkbox styling and event handlers
|
||||||
|
*/
|
||||||
|
setupProviderCheckboxes() {
|
||||||
|
const checkboxes = document.querySelectorAll('.provider-toggle');
|
||||||
|
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
// Apply existing checkbox styling
|
||||||
|
checkbox.style.cssText = `
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid #555;
|
||||||
|
background: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Update visual state
|
||||||
|
this.updateCheckboxAppearance(checkbox);
|
||||||
|
|
||||||
|
// Add change event handler
|
||||||
|
checkbox.addEventListener('change', (e) => {
|
||||||
|
this.updateCheckboxAppearance(e.target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add CSS for checkbox styling since we're using existing styles
|
||||||
|
*/
|
||||||
|
addCheckboxStyling() {
|
||||||
|
// Add CSS for the checkboxes to work with existing styles
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.provider-toggle[data-checked="true"]::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: 2px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-toggle:hover {
|
||||||
|
border-color: #00ff41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-status-row {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-item {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update checkbox appearance based on checked state
|
||||||
|
*/
|
||||||
|
updateCheckboxAppearance(checkbox) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
checkbox.style.background = '#00ff41';
|
||||||
|
checkbox.style.borderColor = '#00ff41';
|
||||||
|
checkbox.style.setProperty('content', '"✓"', 'important');
|
||||||
|
|
||||||
|
// Add checkmark via pseudo-element simulation
|
||||||
|
checkbox.setAttribute('data-checked', 'true');
|
||||||
|
} else {
|
||||||
|
checkbox.style.background = '#1a1a1a';
|
||||||
|
checkbox.style.borderColor = '#555';
|
||||||
|
checkbox.removeAttribute('data-checked');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced API key section builder - FIXED to always allow API key input
|
||||||
|
* @param {Object} providers - Provider information
|
||||||
|
*/
|
||||||
|
buildApiKeySection(providers) {
|
||||||
|
const apiKeyInputs = document.getElementById('api-key-inputs');
|
||||||
|
if (!apiKeyInputs) return;
|
||||||
|
|
||||||
|
apiKeyInputs.innerHTML = '';
|
||||||
|
let hasApiKeyProviders = false;
|
||||||
|
|
||||||
|
for (const [name, info] of Object.entries(providers)) {
|
||||||
|
if (info.requires_api_key) {
|
||||||
|
hasApiKeyProviders = true;
|
||||||
|
|
||||||
|
const inputGroup = document.createElement('div');
|
||||||
|
inputGroup.className = 'provider-item';
|
||||||
|
|
||||||
|
// Check if API key is set via backend (not clearable) or frontend (clearable)
|
||||||
|
const isBackendConfigured = info.api_key_source === 'backend';
|
||||||
|
|
||||||
|
if (info.api_key_configured && isBackendConfigured) {
|
||||||
|
// API key is configured via backend - show status only
|
||||||
|
inputGroup.innerHTML = `
|
||||||
|
<div class="provider-header">
|
||||||
|
<div class="provider-name">${info.display_name}</div>
|
||||||
|
<div class="provider-status enabled">✓ Backend Configured</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-key-status-row" style="padding: 0.75rem; background: rgba(0, 255, 65, 0.1); border-radius: 4px; border: 1px solid rgba(0, 255, 65, 0.3);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<div class="status-value">API Key Active</div>
|
||||||
|
<div class="status-label" style="font-size: 0.8rem;">
|
||||||
|
Configured via environment variable
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (info.api_key_configured && !isBackendConfigured) {
|
||||||
|
// API key is configured via frontend - show status with clear option
|
||||||
|
inputGroup.innerHTML = `
|
||||||
|
<div class="provider-header">
|
||||||
|
<div class="provider-name">${info.display_name}</div>
|
||||||
|
<div class="provider-status enabled">✓ Web Configured</div>
|
||||||
|
</div>
|
||||||
|
<div class="api-key-status-row" style="padding: 0.75rem; background: rgba(0, 255, 65, 0.1); border-radius: 4px; border: 1px solid rgba(0, 255, 65, 0.3);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<div class="status-value">API Key Active</div>
|
||||||
|
<div class="status-label" style="font-size: 0.8rem;">
|
||||||
|
Set via web interface (session-only)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="clear-api-key-btn btn-secondary" data-provider="${name}" style="padding: 0.4rem 0.8rem; font-size: 0.8rem;">
|
||||||
|
<span class="btn-icon">[×]</span>
|
||||||
|
<span>Clear</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// API key not configured - ALWAYS show input field
|
||||||
|
const statusClass = info.enabled ? 'enabled' : 'api-key-required';
|
||||||
|
const statusText = info.enabled ? '○ Ready for API Key' : '⚠️ API Key Required';
|
||||||
|
|
||||||
|
inputGroup.innerHTML = `
|
||||||
|
<div class="provider-header">
|
||||||
|
<div class="provider-name">${info.display_name}</div>
|
||||||
|
<div class="provider-status ${statusClass}">
|
||||||
|
${statusText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="${name}-api-key">API Key</label>
|
||||||
|
<input type="password"
|
||||||
|
id="${name}-api-key"
|
||||||
|
data-provider="${name}"
|
||||||
|
placeholder="Enter ${info.display_name} API Key"
|
||||||
|
autocomplete="off">
|
||||||
|
<div class="apikey-help">
|
||||||
|
${info.api_key_help || `Provides enhanced ${info.display_name.toLowerCase()} data and context.`}
|
||||||
|
${!info.enabled ? ' Enable the provider above to use this API key.' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKeyInputs.appendChild(inputGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasApiKeyProviders) {
|
||||||
|
apiKeyInputs.innerHTML = `
|
||||||
|
<div class="status-row">
|
||||||
|
<div class="status-label">No providers require API keys</div>
|
||||||
|
<div class="status-value">All Active</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup clear button event handlers
|
||||||
|
this.setupApiKeyClearHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup API key clear button handlers
|
||||||
|
*/
|
||||||
|
setupApiKeyClearHandlers() {
|
||||||
|
document.querySelectorAll('.clear-api-key-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const provider = e.currentTarget.dataset.provider;
|
||||||
|
this.clearSingleApiKey(provider, e.currentTarget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a single API key with immediate feedback
|
||||||
|
*/
|
||||||
|
async clearSingleApiKey(provider, buttonElement) {
|
||||||
|
try {
|
||||||
|
// Show immediate feedback
|
||||||
|
const originalContent = buttonElement.innerHTML;
|
||||||
|
buttonElement.innerHTML = '<span class="btn-icon">[...]</span><span>Clearing...</span>';
|
||||||
|
buttonElement.disabled = true;
|
||||||
|
|
||||||
|
const response = await this.apiCall('/api/config/api-keys', 'POST', { [provider]: '' });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Find the parent container and update it
|
||||||
|
const providerContainer = buttonElement.closest('.provider-item');
|
||||||
|
const statusRow = providerContainer.querySelector('.api-key-status-row');
|
||||||
|
|
||||||
|
// Animate out the current status
|
||||||
|
statusRow.style.transition = 'all 0.3s ease';
|
||||||
|
statusRow.style.opacity = '0';
|
||||||
|
statusRow.style.transform = 'translateX(-10px)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Replace with input field
|
||||||
|
const providerName = buttonElement.dataset.provider;
|
||||||
|
const apiKeySection = this.elements.apiKeyInputs;
|
||||||
|
|
||||||
|
// Rebuild the API key section to reflect changes
|
||||||
|
this.loadProviders();
|
||||||
|
|
||||||
|
this.showSuccess(`API key for ${provider} has been cleared.`);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Failed to clear API key');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Restore button on error
|
||||||
|
buttonElement.innerHTML = originalContent;
|
||||||
|
buttonElement.disabled = false;
|
||||||
|
this.showError(`Error clearing API key: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update settings modal counts
|
||||||
|
*/
|
||||||
|
updateSettingsCounts(providers) {
|
||||||
|
const providerCount = Object.keys(providers).length;
|
||||||
|
const apiKeyCount = Object.values(providers).filter(p => p.requires_api_key).length;
|
||||||
|
|
||||||
|
const providerCountElement = document.getElementById('provider-count');
|
||||||
|
const apiKeyCountElement = document.getElementById('api-key-count');
|
||||||
|
|
||||||
|
if (providerCountElement) providerCountElement.textContent = providerCount;
|
||||||
|
if (apiKeyCountElement) apiKeyCountElement.textContent = apiKeyCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced save settings function
|
||||||
|
*/
|
||||||
|
async saveSettings() {
|
||||||
|
try {
|
||||||
|
const settings = {
|
||||||
|
apiKeys: {},
|
||||||
|
providerSettings: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collect API key inputs
|
||||||
|
const apiKeyInputs = document.querySelectorAll('#api-key-inputs input[type="password"]');
|
||||||
|
apiKeyInputs.forEach(input => {
|
||||||
|
const provider = input.dataset.provider;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (provider && value) {
|
||||||
|
settings.apiKeys[provider] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect provider enable/disable settings
|
||||||
|
const providerCheckboxes = document.querySelectorAll('.provider-toggle');
|
||||||
|
providerCheckboxes.forEach(checkbox => {
|
||||||
|
const provider = checkbox.dataset.provider;
|
||||||
|
if (provider) {
|
||||||
|
settings.providerSettings[provider] = {
|
||||||
|
enabled: checkbox.checked
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save API keys if any
|
||||||
|
if (Object.keys(settings.apiKeys).length > 0) {
|
||||||
|
const apiKeyResponse = await this.apiCall('/api/config/api-keys', 'POST', settings.apiKeys);
|
||||||
|
if (!apiKeyResponse.success) {
|
||||||
|
throw new Error(apiKeyResponse.error || 'Failed to save API keys');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save provider settings if any
|
||||||
|
if (Object.keys(settings.providerSettings).length > 0) {
|
||||||
|
const providerResponse = await this.apiCall('/api/config/providers', 'POST', settings.providerSettings);
|
||||||
|
if (!providerResponse.success) {
|
||||||
|
throw new Error(providerResponse.error || 'Failed to save provider settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSuccess('Settings saved successfully');
|
||||||
|
this.hideSettingsModal();
|
||||||
|
|
||||||
|
// Reload providers to reflect changes
|
||||||
|
this.loadProviders();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`Error saving settings: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset settings to defaults
|
||||||
|
*/
|
||||||
|
async resetSettings() {
|
||||||
|
try {
|
||||||
|
// Clear all API key inputs
|
||||||
|
const apiKeyInputs = document.querySelectorAll('#api-key-inputs input[type="password"]');
|
||||||
|
apiKeyInputs.forEach(input => {
|
||||||
|
input.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset all provider checkboxes to enabled (default)
|
||||||
|
const providerCheckboxes = document.querySelectorAll('.provider-toggle');
|
||||||
|
providerCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = true;
|
||||||
|
this.updateCheckboxAppearance(checkbox);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset recursion depth to default
|
||||||
|
const depthSelect = document.getElementById('max-depth');
|
||||||
|
if (depthSelect) {
|
||||||
|
depthSelect.value = '2';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showInfo('Settings reset to defaults');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`Error resetting settings: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update provider display
|
* Update provider display
|
||||||
* @param {Object} providers - Provider information
|
* @param {Object} providers - Provider information
|
||||||
@ -1501,33 +1915,6 @@ class DNSReconApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy text to clipboard with user feedback
|
|
||||||
*/
|
|
||||||
async copyToClipboard(text) {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
this.showMessage('Copied to clipboard', 'success');
|
|
||||||
} catch (err) {
|
|
||||||
// Fallback for older browsers
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = text;
|
|
||||||
textArea.style.position = 'fixed';
|
|
||||||
textArea.style.opacity = '0';
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.select();
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
this.showMessage('Copied to clipboard', 'success');
|
|
||||||
} catch (fallbackErr) {
|
|
||||||
this.showMessage('Failed to copy text', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhanced keyboard navigation for modals
|
* Enhanced keyboard navigation for modals
|
||||||
*/
|
*/
|
||||||
@ -1690,40 +2077,7 @@ class DNSReconApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if graph data has changed
|
|
||||||
* @param {Object} graphData - New graph data
|
|
||||||
* @returns {boolean} True if data has changed
|
|
||||||
*/
|
|
||||||
hasGraphChanged(graphData) {
|
|
||||||
// Simple check based on node and edge counts and timestamps
|
|
||||||
const currentStats = this.graphManager.getStatistics();
|
|
||||||
const newNodeCount = graphData.nodes ? graphData.nodes.length : 0;
|
|
||||||
const newEdgeCount = graphData.edges ? graphData.edges.length : 0;
|
|
||||||
|
|
||||||
// FIXED: Always update if we currently have no data (ensures placeholder is handled correctly)
|
|
||||||
if (currentStats.nodeCount === 0 && currentStats.edgeCount === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if counts changed
|
|
||||||
const countsChanged = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount;
|
|
||||||
|
|
||||||
// Also check if we have new timestamp data
|
|
||||||
const hasNewTimestamp = graphData.statistics &&
|
|
||||||
graphData.statistics.last_modified &&
|
|
||||||
graphData.statistics.last_modified !== this.lastGraphTimestamp;
|
|
||||||
|
|
||||||
if (hasNewTimestamp) {
|
|
||||||
this.lastGraphTimestamp = graphData.statistics.last_modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
const changed = countsChanged || hasNewTimestamp;
|
|
||||||
|
|
||||||
console.log(`Graph change check: Current(${currentStats.nodeCount}n, ${currentStats.edgeCount}e) vs New(${newNodeCount}n, ${newEdgeCount}e) = ${changed}`);
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make API call to server
|
* Make API call to server
|
||||||
@ -1823,15 +2177,6 @@ class DNSReconApp {
|
|||||||
return statusMap[status] || status;
|
return statusMap[status] || status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format label for display
|
|
||||||
* @param {string} label - Raw label
|
|
||||||
* @returns {string} Formatted label
|
|
||||||
*/
|
|
||||||
formatLabel(label) {
|
|
||||||
return label.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format value for display
|
* Format value for display
|
||||||
* @param {*} value - Raw value
|
* @param {*} value - Raw value
|
||||||
@ -1955,73 +2300,7 @@ class DNSReconApp {
|
|||||||
return colors[type] || colors.info;
|
return colors[type] || colors.info;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the API key modal dynamically
|
|
||||||
* @param {Object} providers - Provider information
|
|
||||||
*/
|
|
||||||
buildApiKeyModal(providers) {
|
|
||||||
if (!this.elements.apiKeyInputs) return;
|
|
||||||
this.elements.apiKeyInputs.innerHTML = ''; // Clear existing inputs
|
|
||||||
let hasApiKeyProviders = false;
|
|
||||||
|
|
||||||
for (const [name, info] of Object.entries(providers)) {
|
|
||||||
if (info.requires_api_key) {
|
|
||||||
hasApiKeyProviders = true;
|
|
||||||
const inputGroup = document.createElement('div');
|
|
||||||
inputGroup.className = 'apikey-section';
|
|
||||||
|
|
||||||
if (info.enabled) {
|
|
||||||
// If the API key is set and the provider is enabled
|
|
||||||
inputGroup.innerHTML = `
|
|
||||||
<label for="${name}-api-key">${info.display_name} API Key</label>
|
|
||||||
<div class="api-key-set-message">
|
|
||||||
<span class="api-key-set-text">API Key is set</span>
|
|
||||||
<button class="clear-api-key-btn" data-provider="${name}">Clear</button>
|
|
||||||
</div>
|
|
||||||
<p class="apikey-help">Provides infrastructure context and service information.</p>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
// If the API key is not set
|
|
||||||
inputGroup.innerHTML = `
|
|
||||||
<label for="${name}-api-key">${info.display_name} API Key</label>
|
|
||||||
<input type="password" id="${name}-api-key" data-provider="${name}" placeholder="Enter ${info.display_name} API Key">
|
|
||||||
<p class="apikey-help">Provides infrastructure context and service information.</p>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
this.elements.apiKeyInputs.appendChild(inputGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listeners for the new clear buttons
|
|
||||||
this.elements.apiKeyInputs.querySelectorAll('.clear-api-key-btn').forEach(button => {
|
|
||||||
button.addEventListener('click', (e) => {
|
|
||||||
const provider = e.target.dataset.provider;
|
|
||||||
this.clearApiKey(provider);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasApiKeyProviders) {
|
|
||||||
this.elements.apiKeyInputs.innerHTML = '<p>No providers require API keys.</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear an API key for a specific provider
|
|
||||||
* @param {string} provider The name of the provider to clear the API key for
|
|
||||||
*/
|
|
||||||
async clearApiKey(provider) {
|
|
||||||
try {
|
|
||||||
const response = await this.apiCall('/api/config/api-keys', 'POST', { [provider]: '' });
|
|
||||||
if (response.success) {
|
|
||||||
this.showSuccess(`API key for ${provider} has been cleared.`);
|
|
||||||
this.loadProviders(); // This will rebuild the modal with the updated state
|
|
||||||
} else {
|
|
||||||
throw new Error(response.error || 'Failed to clear API key');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.showError(`Error clearing API key: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add CSS animations for message toasts
|
// Add CSS animations for message toasts
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@ -7,8 +8,11 @@
|
|||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.css" rel="stylesheet" type="text/css">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.css" rel="stylesheet" type="text/css">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&family=Special+Elite&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&family=Special+Elite&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
@ -95,11 +99,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="progress-placeholder">
|
<div class="progress-placeholder">
|
||||||
<span class="status-label">
|
<span class="status-label">
|
||||||
⚠️ <strong>Important:</strong> Scanning large public services (e.g., Google, Cloudflare, AWS) is
|
⚠️ <strong>Important:</strong> Scanning large public services (e.g., Google, Cloudflare,
|
||||||
|
AWS) is
|
||||||
<strong>discouraged</strong> due to rate limits (e.g., crt.sh).
|
<strong>discouraged</strong> due to rate limits (e.g., crt.sh).
|
||||||
<br><br>
|
<br><br>
|
||||||
Our task scheduler operates on a <strong>priority-based queue</strong>:
|
Our task scheduler operates on a <strong>priority-based queue</strong>:
|
||||||
Short, targeted tasks like DNS are processed first, while resource-intensive requests (e.g., crt.sh)
|
Short, targeted tasks like DNS are processed first, while resource-intensive requests (e.g.,
|
||||||
|
crt.sh)
|
||||||
are <strong>automatically deprioritized</strong> and may be processed later.
|
are <strong>automatically deprioritized</strong> and may be processed later.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -116,7 +122,8 @@
|
|||||||
<div class="placeholder-content">
|
<div class="placeholder-content">
|
||||||
<div class="placeholder-icon">[○]</div>
|
<div class="placeholder-icon">[○]</div>
|
||||||
<div class="placeholder-text">Infrastructure map will appear here</div>
|
<div class="placeholder-text">Infrastructure map will appear here</div>
|
||||||
<div class="placeholder-subtext">Start a reconnaissance scan to visualize relationships</div>
|
<div class="placeholder-subtext">Start a reconnaissance scan to visualize relationships
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -159,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="provider-list" class="provider-list">
|
<div id="provider-list" class="provider-list">
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -181,61 +188,93 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div id="modal-details">
|
<div id="modal-details">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Replace the existing settings modal section in index.html -->
|
||||||
<div id="settings-modal" class="modal">
|
<div id="settings-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>Settings</h3>
|
<h3>Scanner Configuration</h3>
|
||||||
<button id="settings-modal-close" class="modal-close">[×]</button>
|
<button id="settings-modal-close" class="modal-close">[×]</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p class="modal-description">
|
<div class="modal-details">
|
||||||
Configure scan settings and API keys. Keys are stored in memory for the current session only.
|
<!-- Scan Settings Section -->
|
||||||
Only provide API-keys you dont use for anything else. Don´t enter an API-key if you don´t trust me (best practice would that you don´t).
|
<section class="modal-section">
|
||||||
</p>
|
<details open>
|
||||||
<br>
|
<summary>
|
||||||
<div class="input-group">
|
<span>⚙️ Scan Settings</span>
|
||||||
<label for="max-depth">Recursion Depth</label>
|
</summary>
|
||||||
<select id="max-depth">
|
<div class="modal-section-content">
|
||||||
<option value="1">Depth 1 - Direct relationships</option>
|
<div class="input-group">
|
||||||
<option value="2" selected>Depth 2 - Recommended</option>
|
<label for="max-depth">Recursion Depth</label>
|
||||||
<option value="3">Depth 3 - Extended analysis</option>
|
<select id="max-depth">
|
||||||
<option value="4">Depth 4 - Deep reconnaissance</option>
|
<option value="1">Depth 1 - Direct relationships</option>
|
||||||
<option value="5">Depth 5 - Maximum depth</option>
|
<option value="2" selected>Depth 2 - Recommended</option>
|
||||||
</select>
|
<option value="3">Depth 3 - Extended analysis</option>
|
||||||
</div>
|
<option value="4">Depth 4 - Deep reconnaissance</option>
|
||||||
<div id="api-key-inputs">
|
<option value="5">Depth 5 - Maximum depth</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Provider Configuration Section -->
|
||||||
|
<section class="modal-section">
|
||||||
|
<details open>
|
||||||
|
<summary>
|
||||||
|
<span>🔧 Provider Configuration</span>
|
||||||
|
<span class="merge-badge" id="provider-count">0</span>
|
||||||
|
</summary>
|
||||||
|
<div class="modal-section-content">
|
||||||
|
<div id="provider-config-list">
|
||||||
|
<!-- Dynamically populated -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- API Keys Section -->
|
||||||
|
<section class="modal-section">
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<span>🔑 API Keys</span>
|
||||||
|
<span class="merge-badge" id="api-key-count">0</span>
|
||||||
|
</summary>
|
||||||
|
<div class="modal-section-content">
|
||||||
|
<p class="placeholder-subtext" style="margin-bottom: 1rem;">
|
||||||
|
⚠️ API keys are stored in memory for the current session only.
|
||||||
|
Only provide API keys you don't use for anything else.
|
||||||
|
</p>
|
||||||
|
<div id="api-key-inputs">
|
||||||
|
<!-- Dynamically populated -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="button-group" style="margin-top: 1.5rem;">
|
||||||
|
<button id="save-settings" class="btn btn-primary">
|
||||||
|
<span class="btn-icon">[SAVE]</span>
|
||||||
|
<span>Save Configuration</span>
|
||||||
|
</button>
|
||||||
|
<button id="reset-settings" class="btn btn-secondary">
|
||||||
|
<span class="btn-icon">[RESET]</span>
|
||||||
|
<span>Reset to Defaults</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group" style="flex-direction: row; justify-content: flex-end;">
|
|
||||||
<button id="reset-api-keys" class="btn btn-secondary">
|
|
||||||
<span>Reset</span>
|
|
||||||
</button>
|
|
||||||
<button id="save-api-keys" class="btn btn-primary">
|
|
||||||
<span>Save API-Keys</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
function copyToClipboard(elementId) {
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
const textToCopy = element.innerText;
|
|
||||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
||||||
// Optional: Show a success message
|
|
||||||
console.log('Copied to clipboard');
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Failed to copy: ', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script src="{{ url_for('static', filename='js/graph.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/graph.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user