mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 14:20:21 -05:00
Add backend modules and frontend components for StrikePackageGPT expansion
Co-authored-by: mblanke <9078342+mblanke@users.noreply.github.com>
This commit is contained in:
516
services/hackgpt-api/app/config_validator.py
Normal file
516
services/hackgpt-api/app/config_validator.py
Normal file
@@ -0,0 +1,516 @@
|
||||
"""
|
||||
Configuration Validator Module
|
||||
Validates configurations before save/change with plain-English warnings.
|
||||
Provides backup/restore functionality and auto-fix suggestions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import copy
|
||||
|
||||
|
||||
# Configuration storage (in production, use persistent storage)
|
||||
config_backups: Dict[str, List[Dict[str, Any]]] = {}
|
||||
BACKUP_DIR = os.getenv("CONFIG_BACKUP_DIR", "/workspace/config_backups")
|
||||
|
||||
|
||||
def validate_config(
|
||||
config_data: Dict[str, Any],
|
||||
config_type: str = "general"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate configuration data before applying changes.
|
||||
|
||||
Args:
|
||||
config_data: Configuration dictionary to validate
|
||||
config_type: Type of configuration (scan, system, security, network)
|
||||
|
||||
Returns:
|
||||
Dictionary with validation results
|
||||
{
|
||||
"valid": bool,
|
||||
"warnings": List[str],
|
||||
"errors": List[str],
|
||||
"suggestions": List[Dict],
|
||||
"safe_to_apply": bool
|
||||
}
|
||||
"""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Type-specific validation
|
||||
if config_type == "scan":
|
||||
errors, warnings, suggestions = _validate_scan_config(config_data)
|
||||
elif config_type == "network":
|
||||
errors, warnings, suggestions = _validate_network_config(config_data)
|
||||
elif config_type == "security":
|
||||
errors, warnings, suggestions = _validate_security_config(config_data)
|
||||
else:
|
||||
errors, warnings, suggestions = _validate_general_config(config_data)
|
||||
|
||||
# Check for common issues across all config types
|
||||
common_errors, common_warnings = _check_common_issues(config_data)
|
||||
errors.extend(common_errors)
|
||||
warnings.extend(common_warnings)
|
||||
|
||||
return {
|
||||
"valid": len(errors) == 0,
|
||||
"warnings": warnings,
|
||||
"errors": errors,
|
||||
"suggestions": suggestions,
|
||||
"safe_to_apply": len(errors) == 0 and len([w for w in warnings if "critical" in w.lower()]) == 0,
|
||||
"config_type": config_type
|
||||
}
|
||||
|
||||
|
||||
def _validate_scan_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
|
||||
"""Validate scan configuration."""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Check timeout
|
||||
timeout = config_data.get("timeout", 300)
|
||||
if not isinstance(timeout, (int, float)):
|
||||
errors.append("Timeout must be a number (seconds)")
|
||||
elif timeout < 1:
|
||||
errors.append("Timeout must be at least 1 second")
|
||||
elif timeout < 10:
|
||||
warnings.append("Very short timeout (< 10s) may cause scans to fail prematurely")
|
||||
elif timeout > 3600:
|
||||
warnings.append("Very long timeout (> 1 hour) may cause scans to hang indefinitely")
|
||||
|
||||
# Check target
|
||||
target = config_data.get("target", "")
|
||||
if not target or not isinstance(target, str):
|
||||
errors.append("Target must be specified (IP address, hostname, or network range)")
|
||||
elif not _is_valid_target(target):
|
||||
warnings.append(f"Target '{target}' may not be valid - ensure it's a valid IP, hostname, or CIDR")
|
||||
|
||||
# Check scan intensity
|
||||
intensity = config_data.get("intensity", 3)
|
||||
if isinstance(intensity, (int, float)):
|
||||
if intensity < 1 or intensity > 5:
|
||||
warnings.append("Scan intensity should be between 1 (stealth) and 5 (aggressive)")
|
||||
if intensity >= 4:
|
||||
warnings.append("High intensity scans may trigger IDS/IPS systems")
|
||||
suggestions.append({
|
||||
"field": "intensity",
|
||||
"suggestion": 3,
|
||||
"reason": "Balanced intensity for stealth and speed"
|
||||
})
|
||||
|
||||
# Check port range
|
||||
ports = config_data.get("ports", "")
|
||||
if ports:
|
||||
if not _is_valid_port_spec(str(ports)):
|
||||
errors.append(f"Invalid port specification: {ports}")
|
||||
|
||||
return errors, warnings, suggestions
|
||||
|
||||
|
||||
def _validate_network_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
|
||||
"""Validate network configuration."""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Check port
|
||||
port = config_data.get("port")
|
||||
if port is not None:
|
||||
if not isinstance(port, int):
|
||||
errors.append("Port must be an integer")
|
||||
elif port < 1 or port > 65535:
|
||||
errors.append("Port must be between 1 and 65535")
|
||||
elif port < 1024:
|
||||
warnings.append("Ports below 1024 require elevated privileges")
|
||||
|
||||
# Check host/bind address
|
||||
host = config_data.get("host", "")
|
||||
if host and not _is_valid_ip_or_hostname(host):
|
||||
warnings.append(f"Host '{host}' may not be a valid IP address or hostname")
|
||||
|
||||
# Check max connections
|
||||
max_conn = config_data.get("max_connections")
|
||||
if max_conn is not None:
|
||||
if not isinstance(max_conn, int) or max_conn < 1:
|
||||
errors.append("max_connections must be a positive integer")
|
||||
elif max_conn > 1000:
|
||||
warnings.append("Very high max_connections (> 1000) may exhaust system resources")
|
||||
|
||||
return errors, warnings, suggestions
|
||||
|
||||
|
||||
def _validate_security_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
|
||||
"""Validate security configuration."""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Check for exposed secrets
|
||||
for key, value in config_data.items():
|
||||
if any(secret_word in key.lower() for secret_word in ['password', 'secret', 'token', 'key', 'credential']):
|
||||
if isinstance(value, str):
|
||||
if len(value) < 8:
|
||||
warnings.append(f"SECURITY: {key} appears weak (< 8 characters)")
|
||||
if value in ['password', '123456', 'admin', 'default']:
|
||||
errors.append(f"SECURITY: {key} is using a default/weak value")
|
||||
|
||||
# Check SSL/TLS settings
|
||||
ssl_enabled = config_data.get("ssl_enabled", False)
|
||||
if not ssl_enabled:
|
||||
warnings.append("SECURITY: SSL/TLS is disabled - data will be transmitted unencrypted")
|
||||
|
||||
# Check authentication
|
||||
auth_enabled = config_data.get("authentication_enabled", True)
|
||||
if not auth_enabled:
|
||||
warnings.append("SECURITY: Authentication is disabled - system will be exposed")
|
||||
|
||||
return errors, warnings, suggestions
|
||||
|
||||
|
||||
def _validate_general_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
|
||||
"""Validate general configuration."""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Check for valid JSON structure
|
||||
if not isinstance(config_data, dict):
|
||||
errors.append("Configuration must be a JSON object")
|
||||
return errors, warnings, suggestions
|
||||
|
||||
# Check for empty config
|
||||
if not config_data:
|
||||
warnings.append("Configuration is empty")
|
||||
|
||||
return errors, warnings, suggestions
|
||||
|
||||
|
||||
def _check_common_issues(config_data: Dict[str, Any]) -> Tuple[List[str], List[str]]:
|
||||
"""Check for common configuration issues."""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Check for null/undefined values
|
||||
for key, value in config_data.items():
|
||||
if value is None:
|
||||
warnings.append(f"Value for '{key}' is null - will use default")
|
||||
|
||||
# Check for suspicious paths
|
||||
for key, value in config_data.items():
|
||||
if isinstance(value, str):
|
||||
if value.startswith('/root/') or value.startswith('C:\\Windows\\'):
|
||||
warnings.append(f"SECURITY: '{key}' points to a sensitive system path")
|
||||
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def backup_config(config_name: str, config_data: Dict[str, Any], description: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
Create a backup of current configuration.
|
||||
|
||||
Args:
|
||||
config_name: Name/ID of the configuration
|
||||
config_data: Configuration data to backup
|
||||
description: Optional description of the backup
|
||||
|
||||
Returns:
|
||||
Dictionary with backup information
|
||||
"""
|
||||
timestamp = datetime.utcnow().isoformat()
|
||||
backup_id = f"{config_name}_{timestamp}"
|
||||
|
||||
backup = {
|
||||
"backup_id": backup_id,
|
||||
"config_name": config_name,
|
||||
"timestamp": timestamp,
|
||||
"description": description or "Automatic backup",
|
||||
"config_data": copy.deepcopy(config_data),
|
||||
"size_bytes": len(json.dumps(config_data))
|
||||
}
|
||||
|
||||
# Store in memory
|
||||
if config_name not in config_backups:
|
||||
config_backups[config_name] = []
|
||||
config_backups[config_name].append(backup)
|
||||
|
||||
# Keep only last 10 backups per config
|
||||
if len(config_backups[config_name]) > 10:
|
||||
config_backups[config_name] = config_backups[config_name][-10:]
|
||||
|
||||
# Also save to disk if backup directory exists
|
||||
try:
|
||||
os.makedirs(BACKUP_DIR, exist_ok=True)
|
||||
backup_file = os.path.join(BACKUP_DIR, f"{backup_id}.json")
|
||||
with open(backup_file, 'w') as f:
|
||||
json.dump(backup, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not save backup to disk: {e}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"backup_id": backup_id,
|
||||
"timestamp": timestamp,
|
||||
"message": f"Configuration backed up successfully"
|
||||
}
|
||||
|
||||
|
||||
def restore_config(backup_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Restore configuration from a backup.
|
||||
|
||||
Args:
|
||||
backup_id: ID of the backup to restore
|
||||
|
||||
Returns:
|
||||
Dictionary with restored configuration and metadata
|
||||
"""
|
||||
# Search in memory backups
|
||||
for config_name, backups in config_backups.items():
|
||||
for backup in backups:
|
||||
if backup["backup_id"] == backup_id:
|
||||
return {
|
||||
"success": True,
|
||||
"backup_id": backup_id,
|
||||
"config_name": config_name,
|
||||
"config_data": copy.deepcopy(backup["config_data"]),
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup["description"],
|
||||
"message": "Configuration restored successfully"
|
||||
}
|
||||
|
||||
# Try loading from disk
|
||||
try:
|
||||
backup_file = os.path.join(BACKUP_DIR, f"{backup_id}.json")
|
||||
if os.path.exists(backup_file):
|
||||
with open(backup_file, 'r') as f:
|
||||
backup = json.load(f)
|
||||
return {
|
||||
"success": True,
|
||||
"backup_id": backup_id,
|
||||
"config_name": backup["config_name"],
|
||||
"config_data": backup["config_data"],
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup.get("description", ""),
|
||||
"message": "Configuration restored from disk backup"
|
||||
}
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"backup_id": backup_id,
|
||||
"error": "Backup not found",
|
||||
"message": f"No backup found with ID: {backup_id}"
|
||||
}
|
||||
|
||||
|
||||
def suggest_autofix(validation_result: Dict[str, Any], config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Suggest automatic fixes for configuration issues.
|
||||
|
||||
Args:
|
||||
validation_result: Result from validate_config()
|
||||
config_data: Original configuration data
|
||||
|
||||
Returns:
|
||||
Dictionary with auto-fix suggestions
|
||||
"""
|
||||
if validation_result.get("valid") and not validation_result.get("warnings"):
|
||||
return {
|
||||
"has_fixes": False,
|
||||
"message": "Configuration is valid, no fixes needed"
|
||||
}
|
||||
|
||||
fixed_config = copy.deepcopy(config_data)
|
||||
fixes_applied = []
|
||||
|
||||
# Apply suggestions from validation
|
||||
for suggestion in validation_result.get("suggestions", []):
|
||||
field = suggestion.get("field")
|
||||
suggested_value = suggestion.get("suggestion")
|
||||
reason = suggestion.get("reason")
|
||||
|
||||
if field in fixed_config:
|
||||
old_value = fixed_config[field]
|
||||
fixed_config[field] = suggested_value
|
||||
fixes_applied.append({
|
||||
"field": field,
|
||||
"old_value": old_value,
|
||||
"new_value": suggested_value,
|
||||
"reason": reason
|
||||
})
|
||||
|
||||
# Apply common fixes based on errors
|
||||
for error in validation_result.get("errors", []):
|
||||
if "timeout must be" in error.lower():
|
||||
if "timeout" in fixed_config:
|
||||
fixed_config["timeout"] = 300 # Default safe timeout
|
||||
fixes_applied.append({
|
||||
"field": "timeout",
|
||||
"old_value": config_data.get("timeout"),
|
||||
"new_value": 300,
|
||||
"reason": "Reset to safe default value"
|
||||
})
|
||||
|
||||
if "port must be" in error.lower():
|
||||
if "port" in fixed_config:
|
||||
fixed_config["port"] = 8080 # Default safe port
|
||||
fixes_applied.append({
|
||||
"field": "port",
|
||||
"old_value": config_data.get("port"),
|
||||
"new_value": 8080,
|
||||
"reason": "Reset to safe default port"
|
||||
})
|
||||
|
||||
return {
|
||||
"has_fixes": len(fixes_applied) > 0,
|
||||
"fixes_applied": fixes_applied,
|
||||
"fixed_config": fixed_config,
|
||||
"message": f"Applied {len(fixes_applied)} automatic fixes"
|
||||
}
|
||||
|
||||
|
||||
def list_backups(config_name: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
List available configuration backups.
|
||||
|
||||
Args:
|
||||
config_name: Optional config name to filter by
|
||||
|
||||
Returns:
|
||||
Dictionary with list of backups
|
||||
"""
|
||||
all_backups = []
|
||||
|
||||
# Get from memory
|
||||
if config_name:
|
||||
backups = config_backups.get(config_name, [])
|
||||
for backup in backups:
|
||||
all_backups.append({
|
||||
"backup_id": backup["backup_id"],
|
||||
"config_name": backup["config_name"],
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup["description"],
|
||||
"size_bytes": backup["size_bytes"]
|
||||
})
|
||||
else:
|
||||
for cfg_name, backups in config_backups.items():
|
||||
for backup in backups:
|
||||
all_backups.append({
|
||||
"backup_id": backup["backup_id"],
|
||||
"config_name": backup["config_name"],
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup["description"],
|
||||
"size_bytes": backup["size_bytes"]
|
||||
})
|
||||
|
||||
# Also check disk backups
|
||||
try:
|
||||
if os.path.exists(BACKUP_DIR):
|
||||
for filename in os.listdir(BACKUP_DIR):
|
||||
if filename.endswith('.json'):
|
||||
backup_id = filename[:-5] # Remove .json
|
||||
# Check if already in list (avoid duplicates)
|
||||
if not any(b["backup_id"] == backup_id for b in all_backups):
|
||||
try:
|
||||
filepath = os.path.join(BACKUP_DIR, filename)
|
||||
with open(filepath, 'r') as f:
|
||||
backup = json.load(f)
|
||||
if not config_name or backup["config_name"] == config_name:
|
||||
all_backups.append({
|
||||
"backup_id": backup["backup_id"],
|
||||
"config_name": backup["config_name"],
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup.get("description", ""),
|
||||
"size_bytes": os.path.getsize(filepath)
|
||||
})
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read disk backups: {e}")
|
||||
|
||||
# Sort by timestamp (newest first)
|
||||
all_backups.sort(key=lambda x: x["timestamp"], reverse=True)
|
||||
|
||||
return {
|
||||
"backups": all_backups,
|
||||
"count": len(all_backups),
|
||||
"config_name": config_name
|
||||
}
|
||||
|
||||
|
||||
# Validation helper functions
|
||||
|
||||
def _is_valid_target(target: str) -> bool:
|
||||
"""Check if target is a valid IP, hostname, or CIDR."""
|
||||
import re
|
||||
|
||||
# IP address
|
||||
ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||
if re.match(ip_pattern, target):
|
||||
parts = target.split('.')
|
||||
return all(0 <= int(part) <= 255 for part in parts)
|
||||
|
||||
# CIDR notation
|
||||
if '/' in target:
|
||||
cidr_pattern = r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$'
|
||||
if re.match(cidr_pattern, target):
|
||||
ip_part = target.split('/')[0]
|
||||
return _is_valid_target(ip_part)
|
||||
|
||||
# IP range
|
||||
if '-' in target:
|
||||
range_pattern = r'^(\d{1,3}\.){3}\d{1,3}-\d{1,3}$'
|
||||
if re.match(range_pattern, target):
|
||||
return True
|
||||
|
||||
# Hostname/domain
|
||||
hostname_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$'
|
||||
if re.match(hostname_pattern, target):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _is_valid_port_spec(ports: str) -> bool:
|
||||
"""Check if port specification is valid."""
|
||||
import re
|
||||
|
||||
# Single port
|
||||
if ports.isdigit():
|
||||
port_num = int(ports)
|
||||
return 1 <= port_num <= 65535
|
||||
|
||||
# Port range
|
||||
if '-' in ports:
|
||||
range_pattern = r'^\d+-\d+$'
|
||||
if re.match(range_pattern, ports):
|
||||
start, end = map(int, ports.split('-'))
|
||||
return 1 <= start <= end <= 65535
|
||||
|
||||
# Comma-separated ports
|
||||
if ',' in ports:
|
||||
port_list = ports.split(',')
|
||||
return all(_is_valid_port_spec(p.strip()) for p in port_list)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _is_valid_ip_or_hostname(host: str) -> bool:
|
||||
"""Check if host is a valid IP address or hostname."""
|
||||
import re
|
||||
|
||||
# IP address
|
||||
ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||
if re.match(ip_pattern, host):
|
||||
parts = host.split('.')
|
||||
return all(0 <= int(part) <= 255 for part in parts)
|
||||
|
||||
# Hostname
|
||||
hostname_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$'
|
||||
return bool(re.match(hostname_pattern, host))
|
||||
547
services/hackgpt-api/app/explain.py
Normal file
547
services/hackgpt-api/app/explain.py
Normal file
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
Explain Module
|
||||
Provides "Explain this" functionality for configs, logs, errors, and onboarding.
|
||||
Generates plain-English explanations and suggestions for fixes.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
def explain_config(config_key: str, config_value: Any, context: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Explain a configuration setting in plain English.
|
||||
|
||||
Args:
|
||||
config_key: Configuration key/name
|
||||
config_value: Current value of the configuration
|
||||
context: Additional context about the configuration
|
||||
|
||||
Returns:
|
||||
Dictionary with explanation and recommendations
|
||||
"""
|
||||
# Common configuration patterns and their explanations
|
||||
config_patterns = {
|
||||
r'.*timeout.*': {
|
||||
'description': 'Controls how long the system waits before giving up on an operation',
|
||||
'example': 'A timeout of 30 seconds means operations will be cancelled after 30s',
|
||||
'recommendations': [
|
||||
'Increase timeout for slow networks or large scans',
|
||||
'Decrease timeout for faster detection of unavailable services',
|
||||
'Typical values: 10-300 seconds'
|
||||
]
|
||||
},
|
||||
r'.*port.*': {
|
||||
'description': 'Specifies which network port to use for communication',
|
||||
'example': 'Port 8080 is commonly used for web applications',
|
||||
'recommendations': [
|
||||
'Use standard ports (80/443) for production',
|
||||
'Use high ports (8000+) for development',
|
||||
'Ensure port is not blocked by firewall'
|
||||
]
|
||||
},
|
||||
r'.*api[_-]?key.*': {
|
||||
'description': 'Authentication key for accessing external services',
|
||||
'example': 'API keys should be kept secret and not shared publicly',
|
||||
'recommendations': [
|
||||
'Store API keys in environment variables',
|
||||
'Never commit API keys to version control',
|
||||
'Rotate keys regularly for security'
|
||||
]
|
||||
},
|
||||
r'.*thread.*|.*worker.*': {
|
||||
'description': 'Controls parallel processing and concurrency',
|
||||
'example': '4 workers means 4 operations can run simultaneously',
|
||||
'recommendations': [
|
||||
'More workers = faster but more resource usage',
|
||||
'Typical range: number of CPU cores or 2x CPU cores',
|
||||
'Too many workers can overwhelm the system'
|
||||
]
|
||||
},
|
||||
r'.*rate[_-]?limit.*': {
|
||||
'description': 'Limits the frequency of operations to prevent overload',
|
||||
'example': 'Rate limit of 100/minute means max 100 requests per minute',
|
||||
'recommendations': [
|
||||
'Set based on target system capabilities',
|
||||
'Lower for sensitive or production targets',
|
||||
'Higher for testing environments'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Find matching pattern
|
||||
explanation = {
|
||||
'description': 'Configuration setting',
|
||||
'example': '',
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
for pattern, details in config_patterns.items():
|
||||
if re.search(pattern, config_key, re.IGNORECASE):
|
||||
explanation = details
|
||||
break
|
||||
|
||||
# Value-specific analysis
|
||||
value_analysis = _analyze_config_value(config_key, config_value)
|
||||
|
||||
return {
|
||||
'config_key': config_key,
|
||||
'current_value': str(config_value),
|
||||
'description': explanation['description'],
|
||||
'example': explanation['example'],
|
||||
'recommendations': explanation['recommendations'],
|
||||
'value_analysis': value_analysis,
|
||||
'safe_to_change': _is_safe_to_change(config_key),
|
||||
'requires_restart': _requires_restart(config_key)
|
||||
}
|
||||
|
||||
|
||||
def explain_error(error_message: str, error_type: Optional[str] = None, context: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Explain an error message in plain English with suggested fixes.
|
||||
|
||||
Args:
|
||||
error_message: The error message text
|
||||
error_type: Type/category of error (if known)
|
||||
context: Additional context about where/when the error occurred
|
||||
|
||||
Returns:
|
||||
Dictionary with explanation and fix suggestions
|
||||
"""
|
||||
# Common error patterns
|
||||
error_patterns = [
|
||||
{
|
||||
'pattern': r'connection\s+(refused|timed?\s?out|failed|reset)',
|
||||
'plain_english': 'Unable to connect to the target',
|
||||
'likely_causes': [
|
||||
'Target is offline or unreachable',
|
||||
'Firewall blocking the connection',
|
||||
'Wrong IP address or port',
|
||||
'Network connectivity issues'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Verify target IP address is correct',
|
||||
'Check if target is online (ping test)',
|
||||
'Ensure no firewall is blocking the connection',
|
||||
'Try a different port or protocol'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'permission\s+denied|access\s+denied|forbidden',
|
||||
'plain_english': 'You don\'t have permission to perform this action',
|
||||
'likely_causes': [
|
||||
'Insufficient user privileges',
|
||||
'Authentication failed',
|
||||
'Resource is protected',
|
||||
'Rate limiting in effect'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Run with appropriate privileges (sudo if needed)',
|
||||
'Check authentication credentials',
|
||||
'Verify you have permission to access this resource',
|
||||
'Wait before retrying (if rate limited)'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'not\s+found|does\s+not\s+exist|no\s+such',
|
||||
'plain_english': 'The requested resource could not be found',
|
||||
'likely_causes': [
|
||||
'Resource has been moved or deleted',
|
||||
'Incorrect path or name',
|
||||
'Typo in the request',
|
||||
'Resource not yet created'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Check spelling and capitalization',
|
||||
'Verify the resource exists',
|
||||
'Check if path or URL is correct',
|
||||
'Create the resource if needed'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'invalid\s+(argument|parameter|input|syntax)',
|
||||
'plain_english': 'The input provided is not valid or in the wrong format',
|
||||
'likely_causes': [
|
||||
'Wrong data type or format',
|
||||
'Missing required parameter',
|
||||
'Value out of valid range',
|
||||
'Syntax error in command'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Check documentation for correct format',
|
||||
'Verify all required parameters are provided',
|
||||
'Ensure values are within valid ranges',
|
||||
'Check for typos in the command'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'timeout|timed\s+out',
|
||||
'plain_english': 'The operation took too long and was cancelled',
|
||||
'likely_causes': [
|
||||
'Network is slow or congested',
|
||||
'Target is responding slowly',
|
||||
'Timeout setting is too low',
|
||||
'Large operation needs more time'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Increase timeout value in settings',
|
||||
'Check network connectivity',
|
||||
'Try again during off-peak hours',
|
||||
'Break operation into smaller parts'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'out\s+of\s+memory|memory\s+error',
|
||||
'plain_english': 'The system ran out of available memory',
|
||||
'likely_causes': [
|
||||
'Too many concurrent operations',
|
||||
'Processing too much data at once',
|
||||
'Memory leak in the application',
|
||||
'Insufficient system resources'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Reduce number of concurrent operations',
|
||||
'Process data in smaller batches',
|
||||
'Restart the application',
|
||||
'Add more RAM to the system'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# Find matching pattern
|
||||
match_result = {
|
||||
'plain_english': 'An error occurred',
|
||||
'likely_causes': ['Unknown error condition'],
|
||||
'suggested_fixes': ['Check logs for more details', 'Try the operation again']
|
||||
}
|
||||
|
||||
error_lower = error_message.lower()
|
||||
for pattern_info in error_patterns:
|
||||
if re.search(pattern_info['pattern'], error_lower):
|
||||
match_result = {
|
||||
'plain_english': pattern_info['plain_english'],
|
||||
'likely_causes': pattern_info['likely_causes'],
|
||||
'suggested_fixes': pattern_info['suggested_fixes']
|
||||
}
|
||||
break
|
||||
|
||||
return {
|
||||
'original_error': error_message,
|
||||
'error_type': error_type or 'unknown',
|
||||
'plain_english': match_result['plain_english'],
|
||||
'likely_causes': match_result['likely_causes'],
|
||||
'suggested_fixes': match_result['suggested_fixes'],
|
||||
'severity': _assess_error_severity(error_message),
|
||||
'context': context or {}
|
||||
}
|
||||
|
||||
|
||||
def explain_log_entry(log_entry: str, log_level: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Explain a log entry in plain English.
|
||||
|
||||
Args:
|
||||
log_entry: The log message text
|
||||
log_level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
|
||||
Returns:
|
||||
Dictionary with explanation of the log entry
|
||||
"""
|
||||
# Detect log level if not provided
|
||||
if not log_level:
|
||||
log_level = _detect_log_level(log_entry)
|
||||
|
||||
# Extract key information from log
|
||||
extracted_info = _extract_log_info(log_entry)
|
||||
|
||||
# Determine if action is needed
|
||||
action_needed = log_level in ['ERROR', 'CRITICAL', 'WARNING']
|
||||
|
||||
explanation = {
|
||||
'log_entry': log_entry,
|
||||
'log_level': log_level,
|
||||
'timestamp': extracted_info.get('timestamp'),
|
||||
'component': extracted_info.get('component'),
|
||||
'message': extracted_info.get('message', log_entry),
|
||||
'action_needed': action_needed,
|
||||
'explanation': _generate_log_explanation(log_entry, log_level),
|
||||
'next_steps': _suggest_log_next_steps(log_entry, log_level) if action_needed else []
|
||||
}
|
||||
|
||||
return explanation
|
||||
|
||||
|
||||
def get_wizard_step_help(wizard_type: str, step_number: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Get help text for a specific wizard step.
|
||||
|
||||
Args:
|
||||
wizard_type: Type of wizard (create_operation, onboard_agent, run_scan, first_time_setup)
|
||||
step_number: Current step number (1-indexed)
|
||||
|
||||
Returns:
|
||||
Dictionary with help information for the step
|
||||
"""
|
||||
wizard_help = {
|
||||
'create_operation': {
|
||||
1: {
|
||||
'title': 'Operation Name and Type',
|
||||
'description': 'Give your operation a memorable name and select the type of security assessment',
|
||||
'tips': [
|
||||
'Use descriptive names like "Q4 External Assessment" or "Web App Pentest"',
|
||||
'Choose the operation type that matches your goals',
|
||||
'You can change these later in settings'
|
||||
],
|
||||
'example': 'Example: "Internal Network Audit - Production"'
|
||||
},
|
||||
2: {
|
||||
'title': 'Define Target Scope',
|
||||
'description': 'Specify which systems, networks, or applications to include in the assessment',
|
||||
'tips': [
|
||||
'Use CIDR notation for network ranges (e.g., 192.168.1.0/24)',
|
||||
'Add individual hosts or domains as needed',
|
||||
'Clearly define what is in-scope and out-of-scope'
|
||||
],
|
||||
'example': 'Example: 192.168.1.0/24, app.example.com'
|
||||
},
|
||||
3: {
|
||||
'title': 'Configure Assessment Tools',
|
||||
'description': 'Select which security tools to use and configure their settings',
|
||||
'tips': [
|
||||
'Start with reconnaissance tools (nmap, whatweb)',
|
||||
'Add vulnerability scanners based on target type',
|
||||
'Adjust scan intensity based on target sensitivity'
|
||||
],
|
||||
'example': 'Example: nmap (aggressive), nikto (web servers only)'
|
||||
}
|
||||
},
|
||||
'run_scan': {
|
||||
1: {
|
||||
'title': 'Select Scan Tool',
|
||||
'description': 'Choose the security tool appropriate for your target',
|
||||
'tips': [
|
||||
'nmap: Network scanning and service detection',
|
||||
'nikto: Web server vulnerability scanning',
|
||||
'gobuster: Directory and file discovery',
|
||||
'sqlmap: SQL injection testing'
|
||||
],
|
||||
'example': 'For a web server, use nikto or gobuster'
|
||||
},
|
||||
2: {
|
||||
'title': 'Specify Target',
|
||||
'description': 'Enter the IP address, hostname, or network range to scan',
|
||||
'tips': [
|
||||
'Single host: 192.168.1.100 or example.com',
|
||||
'Network range: 192.168.1.0/24',
|
||||
'Multiple hosts: 192.168.1.1-50'
|
||||
],
|
||||
'example': 'Example: 192.168.1.0/24 for entire subnet'
|
||||
},
|
||||
3: {
|
||||
'title': 'Scan Options',
|
||||
'description': 'Configure scan parameters and intensity',
|
||||
'tips': [
|
||||
'Quick scan: Fast but less thorough',
|
||||
'Full scan: Comprehensive but slower',
|
||||
'Stealth: Slower but harder to detect'
|
||||
],
|
||||
'example': 'Use quick scan for initial reconnaissance'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
steps = wizard_help.get(wizard_type, {})
|
||||
step_help = steps.get(step_number, {
|
||||
'title': f'Step {step_number}',
|
||||
'description': 'Complete this step to continue',
|
||||
'tips': ['Fill in the required information'],
|
||||
'example': ''
|
||||
})
|
||||
|
||||
return {
|
||||
'wizard_type': wizard_type,
|
||||
'step_number': step_number,
|
||||
'total_steps': len(steps),
|
||||
**step_help
|
||||
}
|
||||
|
||||
|
||||
def suggest_fix(issue_description: str, context: Optional[Dict] = None) -> List[str]:
|
||||
"""
|
||||
Suggest fixes for a described issue.
|
||||
|
||||
Args:
|
||||
issue_description: Description of the problem
|
||||
context: Additional context (error codes, logs, etc.)
|
||||
|
||||
Returns:
|
||||
List of suggested fix actions
|
||||
"""
|
||||
issue_lower = issue_description.lower()
|
||||
fixes = []
|
||||
|
||||
# Connectivity issues
|
||||
if any(word in issue_lower for word in ['connect', 'network', 'reach', 'timeout']):
|
||||
fixes.extend([
|
||||
'Verify target is online with ping test',
|
||||
'Check firewall rules and network connectivity',
|
||||
'Ensure correct IP address and port number',
|
||||
'Try increasing timeout value in settings'
|
||||
])
|
||||
|
||||
# Permission issues
|
||||
if any(word in issue_lower for word in ['permission', 'access', 'denied', 'forbidden']):
|
||||
fixes.extend([
|
||||
'Run with elevated privileges (sudo)',
|
||||
'Check file/directory permissions',
|
||||
'Verify authentication credentials',
|
||||
'Ensure user has required roles/permissions'
|
||||
])
|
||||
|
||||
# Configuration issues
|
||||
if any(word in issue_lower for word in ['config', 'setting', 'option']):
|
||||
fixes.extend([
|
||||
'Review configuration file for errors',
|
||||
'Restore default configuration',
|
||||
'Check configuration documentation',
|
||||
'Validate configuration format (JSON/YAML)'
|
||||
])
|
||||
|
||||
# Tool/command issues
|
||||
if any(word in issue_lower for word in ['command', 'tool', 'not found', 'install']):
|
||||
fixes.extend([
|
||||
'Install the required tool or package',
|
||||
'Check if tool is in system PATH',
|
||||
'Verify tool name spelling',
|
||||
'Update tool to latest version'
|
||||
])
|
||||
|
||||
# Default suggestions if no specific fix found
|
||||
if not fixes:
|
||||
fixes = [
|
||||
'Check system logs for more details',
|
||||
'Restart the affected service',
|
||||
'Review recent configuration changes',
|
||||
'Consult documentation or support'
|
||||
]
|
||||
|
||||
return fixes[:5] # Return top 5 suggestions
|
||||
|
||||
|
||||
# Helper functions
|
||||
|
||||
def _analyze_config_value(key: str, value: Any) -> str:
|
||||
"""Analyze a configuration value and provide feedback."""
|
||||
if isinstance(value, int):
|
||||
if 'timeout' in key.lower():
|
||||
if value < 10:
|
||||
return 'Very low - may cause premature failures'
|
||||
elif value > 300:
|
||||
return 'Very high - operations may take long to fail'
|
||||
else:
|
||||
return 'Reasonable value'
|
||||
elif 'port' in key.lower():
|
||||
if value < 1024:
|
||||
return 'System port - requires elevated privileges'
|
||||
else:
|
||||
return 'User port - no special privileges needed'
|
||||
|
||||
return 'Current value seems valid'
|
||||
|
||||
|
||||
def _is_safe_to_change(config_key: str) -> bool:
|
||||
"""Determine if a config is safe to change without risk."""
|
||||
unsafe_keys = ['database', 'credential', 'key', 'secret', 'password']
|
||||
return not any(unsafe in config_key.lower() for unsafe in unsafe_keys)
|
||||
|
||||
|
||||
def _requires_restart(config_key: str) -> bool:
|
||||
"""Determine if changing this config requires a restart."""
|
||||
restart_keys = ['port', 'host', 'database', 'worker', 'thread']
|
||||
return any(key in config_key.lower() for key in restart_keys)
|
||||
|
||||
|
||||
def _assess_error_severity(error_message: str) -> str:
|
||||
"""Assess the severity of an error."""
|
||||
error_lower = error_message.lower()
|
||||
|
||||
if any(word in error_lower for word in ['critical', 'fatal', 'crash', 'panic']):
|
||||
return 'critical'
|
||||
elif any(word in error_lower for word in ['error', 'fail', 'exception']):
|
||||
return 'high'
|
||||
elif any(word in error_lower for word in ['warning', 'warn']):
|
||||
return 'medium'
|
||||
else:
|
||||
return 'low'
|
||||
|
||||
|
||||
def _detect_log_level(log_entry: str) -> str:
|
||||
"""Detect log level from log entry."""
|
||||
levels = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']
|
||||
for level in levels:
|
||||
if level in log_entry.upper():
|
||||
return level
|
||||
return 'INFO'
|
||||
|
||||
|
||||
def _extract_log_info(log_entry: str) -> Dict[str, str]:
|
||||
"""Extract structured information from a log entry."""
|
||||
info = {}
|
||||
|
||||
# Try to extract timestamp
|
||||
timestamp_pattern = r'\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}'
|
||||
timestamp_match = re.search(timestamp_pattern, log_entry)
|
||||
if timestamp_match:
|
||||
info['timestamp'] = timestamp_match.group()
|
||||
|
||||
# Try to extract component/module name
|
||||
component_pattern = r'\[(\w+)\]'
|
||||
component_match = re.search(component_pattern, log_entry)
|
||||
if component_match:
|
||||
info['component'] = component_match.group(1)
|
||||
|
||||
# Extract the main message
|
||||
parts = log_entry.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
info['message'] = parts[1].strip()
|
||||
else:
|
||||
info['message'] = log_entry
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def _generate_log_explanation(log_entry: str, log_level: str) -> str:
|
||||
"""Generate a plain English explanation of a log entry."""
|
||||
if log_level == 'ERROR':
|
||||
return 'An error occurred that may require attention. Check the details to understand what went wrong.'
|
||||
elif log_level == 'WARNING':
|
||||
return 'A potential issue was detected. It may not be critical but should be reviewed.'
|
||||
elif log_level == 'INFO':
|
||||
return 'Normal operational message providing status information.'
|
||||
elif log_level == 'DEBUG':
|
||||
return 'Detailed diagnostic information useful for troubleshooting.'
|
||||
else:
|
||||
return 'Log entry documenting system activity.'
|
||||
|
||||
|
||||
def _suggest_log_next_steps(log_entry: str, log_level: str) -> List[str]:
|
||||
"""Suggest next steps based on log entry."""
|
||||
steps = []
|
||||
|
||||
if log_level in ['ERROR', 'CRITICAL']:
|
||||
steps.append('Review the error details and check related logs')
|
||||
steps.append('Check if the issue is repeating or isolated')
|
||||
steps.append('Consider rolling back recent changes if applicable')
|
||||
|
||||
if log_level == 'WARNING':
|
||||
steps.append('Monitor for repeated warnings')
|
||||
steps.append('Check if this indicates a trend or pattern')
|
||||
|
||||
if 'connection' in log_entry.lower():
|
||||
steps.append('Verify network connectivity to the target')
|
||||
|
||||
if 'timeout' in log_entry.lower():
|
||||
steps.append('Consider increasing timeout values')
|
||||
|
||||
return steps
|
||||
435
services/hackgpt-api/app/llm_help.py
Normal file
435
services/hackgpt-api/app/llm_help.py
Normal file
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
LLM Help Module
|
||||
Provides LLM-powered assistance including chat help, autocomplete, and config suggestions.
|
||||
Maintains conversation context for persistent help sessions.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
import os
|
||||
import httpx
|
||||
import json
|
||||
|
||||
|
||||
# Store conversation history per session
|
||||
conversation_contexts: Dict[str, List[Dict[str, str]]] = {}
|
||||
|
||||
|
||||
async def chat_completion(
|
||||
message: str,
|
||||
session_id: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
provider: str = "ollama",
|
||||
model: str = "llama3.2",
|
||||
system_prompt: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get LLM chat completion with context awareness.
|
||||
|
||||
Args:
|
||||
message: User message
|
||||
session_id: Session ID for maintaining conversation context
|
||||
context: Additional context about current page/operation
|
||||
provider: LLM provider (ollama, openai, anthropic)
|
||||
model: Model name
|
||||
system_prompt: Custom system prompt (uses default if not provided)
|
||||
|
||||
Returns:
|
||||
Dictionary with LLM response and metadata
|
||||
"""
|
||||
# Default system prompt for help
|
||||
if not system_prompt:
|
||||
system_prompt = """You are a helpful AI assistant for StrikePackageGPT, a security testing platform.
|
||||
You help users with:
|
||||
- Understanding security tools and concepts
|
||||
- Writing and understanding nmap, nikto, and other security tool commands
|
||||
- Interpreting scan results and vulnerabilities
|
||||
- Best practices for penetration testing
|
||||
- Navigation and usage of the platform
|
||||
|
||||
Provide clear, concise, and actionable advice. Include command examples when relevant.
|
||||
Always emphasize ethical hacking practices and legal considerations."""
|
||||
|
||||
# Build messages with conversation history
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
|
||||
# Add conversation history if session_id provided
|
||||
if session_id and session_id in conversation_contexts:
|
||||
messages.extend(conversation_contexts[session_id][-10:]) # Last 10 messages
|
||||
|
||||
# Add context if provided
|
||||
if context:
|
||||
messages.append({"role": "system", "content": f"Current context: {context}"})
|
||||
|
||||
# Add user message
|
||||
messages.append({"role": "user", "content": message})
|
||||
|
||||
# Get LLM response
|
||||
try:
|
||||
llm_router_url = os.getenv("LLM_ROUTER_URL", "http://strikepackage-llm-router:8000")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{llm_router_url}/chat",
|
||||
json={
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2048
|
||||
},
|
||||
timeout=120.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
assistant_message = result.get("content", "")
|
||||
|
||||
# Store in conversation history
|
||||
if session_id:
|
||||
if session_id not in conversation_contexts:
|
||||
conversation_contexts[session_id] = []
|
||||
conversation_contexts[session_id].append({"role": "user", "content": message})
|
||||
conversation_contexts[session_id].append({"role": "assistant", "content": assistant_message})
|
||||
|
||||
return {
|
||||
"message": assistant_message,
|
||||
"session_id": session_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"success": True
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"message": "I'm having trouble connecting to the LLM service. Please try again.",
|
||||
"error": response.text,
|
||||
"success": False
|
||||
}
|
||||
|
||||
except httpx.ConnectError:
|
||||
return {
|
||||
"message": "LLM service is not available. Please check your connection.",
|
||||
"error": "Connection failed",
|
||||
"success": False
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"message": "An error occurred while processing your request.",
|
||||
"error": str(e),
|
||||
"success": False
|
||||
}
|
||||
|
||||
|
||||
async def get_autocomplete(
|
||||
partial_text: str,
|
||||
context_type: str = "command",
|
||||
max_suggestions: int = 5
|
||||
) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Get autocomplete suggestions for commands or configurations.
|
||||
|
||||
Args:
|
||||
partial_text: Partial text entered by user
|
||||
context_type: Type of autocomplete (command, config, target)
|
||||
max_suggestions: Maximum number of suggestions to return
|
||||
|
||||
Returns:
|
||||
List of suggestion dictionaries with text and description
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
if context_type == "command":
|
||||
suggestions = _get_command_suggestions(partial_text)
|
||||
elif context_type == "config":
|
||||
suggestions = _get_config_suggestions(partial_text)
|
||||
elif context_type == "target":
|
||||
suggestions = _get_target_suggestions(partial_text)
|
||||
|
||||
return suggestions[:max_suggestions]
|
||||
|
||||
|
||||
def _get_command_suggestions(partial_text: str) -> List[Dict[str, str]]:
|
||||
"""Get command autocomplete suggestions."""
|
||||
# Common security tool commands
|
||||
commands = [
|
||||
{"text": "nmap -sV -sC", "description": "Service version detection with default scripts"},
|
||||
{"text": "nmap -p- -T4", "description": "Scan all ports with aggressive timing"},
|
||||
{"text": "nmap -sS -O", "description": "SYN stealth scan with OS detection"},
|
||||
{"text": "nmap --script vuln", "description": "Run vulnerability detection scripts"},
|
||||
{"text": "nikto -h", "description": "Web server vulnerability scan"},
|
||||
{"text": "gobuster dir -u", "description": "Directory brute-forcing"},
|
||||
{"text": "sqlmap -u", "description": "SQL injection testing"},
|
||||
{"text": "whatweb", "description": "Web technology fingerprinting"},
|
||||
{"text": "searchsploit", "description": "Search exploit database"},
|
||||
{"text": "hydra -l", "description": "Network login cracking"}
|
||||
]
|
||||
|
||||
# Filter based on partial text
|
||||
partial_lower = partial_text.lower()
|
||||
return [cmd for cmd in commands if cmd["text"].lower().startswith(partial_lower)]
|
||||
|
||||
|
||||
def _get_config_suggestions(partial_text: str) -> List[Dict[str, str]]:
|
||||
"""Get configuration autocomplete suggestions."""
|
||||
configs = [
|
||||
{"text": "timeout", "description": "Command execution timeout in seconds"},
|
||||
{"text": "max_workers", "description": "Maximum parallel workers"},
|
||||
{"text": "scan_intensity", "description": "Scan aggressiveness (1-5)"},
|
||||
{"text": "rate_limit", "description": "Requests per second limit"},
|
||||
{"text": "default_ports", "description": "Default ports to scan"},
|
||||
{"text": "output_format", "description": "Output format (json, xml, text)"},
|
||||
{"text": "log_level", "description": "Logging verbosity (debug, info, warning, error)"},
|
||||
{"text": "retry_count", "description": "Number of retries on failure"}
|
||||
]
|
||||
|
||||
partial_lower = partial_text.lower()
|
||||
return [cfg for cfg in configs if cfg["text"].lower().startswith(partial_lower)]
|
||||
|
||||
|
||||
def _get_target_suggestions(partial_text: str) -> List[Dict[str, str]]:
|
||||
"""Get target specification autocomplete suggestions."""
|
||||
suggestions = [
|
||||
{"text": "192.168.1.0/24", "description": "Scan entire /24 subnet"},
|
||||
{"text": "192.168.1.1-50", "description": "Scan IP range"},
|
||||
{"text": "10.0.0.0/8", "description": "Scan entire /8 network"},
|
||||
{"text": "localhost", "description": "Scan local machine"},
|
||||
{"text": "example.com", "description": "Scan domain name"}
|
||||
]
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
async def explain_anything(
|
||||
item: str,
|
||||
item_type: str = "auto",
|
||||
context: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Explain anything using LLM - commands, configs, errors, concepts.
|
||||
|
||||
Args:
|
||||
item: The item to explain
|
||||
item_type: Type of item (auto, command, config, error, concept)
|
||||
context: Additional context
|
||||
|
||||
Returns:
|
||||
Dictionary with explanation
|
||||
"""
|
||||
# Auto-detect type if not specified
|
||||
if item_type == "auto":
|
||||
item_type = _detect_item_type(item)
|
||||
|
||||
# Build appropriate prompt based on type
|
||||
prompts = {
|
||||
"command": f"Explain this security command in plain English:\n{item}\n\nInclude: what it does, any flags/options, expected output, and safety considerations.",
|
||||
"config": f"Explain this configuration setting:\n{item}\n\nInclude: purpose, typical values, and recommendations.",
|
||||
"error": f"Explain this error message:\n{item}\n\nInclude: what went wrong, likely causes, and how to fix it.",
|
||||
"concept": f"Explain this security concept:\n{item}\n\nProvide a clear, beginner-friendly explanation with examples.",
|
||||
"scan_result": f"Explain this scan result:\n{item}\n\nInclude: significance, risk level, and recommended actions."
|
||||
}
|
||||
|
||||
prompt = prompts.get(item_type, f"Explain: {item}")
|
||||
|
||||
# Get explanation from LLM
|
||||
result = await chat_completion(
|
||||
message=prompt,
|
||||
system_prompt="You are a security education assistant. Provide clear, concise explanations suitable for both beginners and experts. Use plain English and include practical examples."
|
||||
)
|
||||
|
||||
return {
|
||||
"item": item,
|
||||
"item_type": item_type,
|
||||
"explanation": result.get("message", ""),
|
||||
"success": result.get("success", False)
|
||||
}
|
||||
|
||||
|
||||
def _detect_item_type(item: str) -> str:
|
||||
"""Detect what type of item is being explained."""
|
||||
item_lower = item.lower()
|
||||
|
||||
# Check for command patterns
|
||||
if any(tool in item_lower for tool in ['nmap', 'nikto', 'gobuster', 'sqlmap', 'hydra']):
|
||||
return "command"
|
||||
|
||||
# Check for error patterns
|
||||
if any(word in item_lower for word in ['error', 'exception', 'failed', 'denied']):
|
||||
return "error"
|
||||
|
||||
# Check for config patterns
|
||||
if '=' in item or ':' in item or 'config' in item_lower:
|
||||
return "config"
|
||||
|
||||
# Check for scan result patterns
|
||||
if any(word in item_lower for word in ['open', 'closed', 'filtered', 'vulnerability', 'port']):
|
||||
return "scan_result"
|
||||
|
||||
# Default to concept
|
||||
return "concept"
|
||||
|
||||
|
||||
async def suggest_config(
|
||||
config_type: str,
|
||||
current_values: Optional[Dict] = None,
|
||||
use_case: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get LLM-powered configuration suggestions.
|
||||
|
||||
Args:
|
||||
config_type: Type of configuration (scan, system, security)
|
||||
current_values: Current configuration values
|
||||
use_case: Specific use case or scenario
|
||||
|
||||
Returns:
|
||||
Dictionary with configuration suggestions
|
||||
"""
|
||||
prompt_parts = [f"Suggest optimal configuration for {config_type}."]
|
||||
|
||||
if current_values:
|
||||
prompt_parts.append(f"\nCurrent configuration:\n{json.dumps(current_values, indent=2)}")
|
||||
|
||||
if use_case:
|
||||
prompt_parts.append(f"\nUse case: {use_case}")
|
||||
|
||||
prompt_parts.append("\nProvide recommended values with explanations. Format as JSON if possible.")
|
||||
|
||||
result = await chat_completion(
|
||||
message="\n".join(prompt_parts),
|
||||
system_prompt="You are a security configuration expert. Provide optimal, secure, and practical configuration recommendations."
|
||||
)
|
||||
|
||||
# Try to extract JSON from response
|
||||
response_text = result.get("message", "")
|
||||
suggested_config = _extract_json_from_text(response_text)
|
||||
|
||||
return {
|
||||
"config_type": config_type,
|
||||
"suggestions": suggested_config or {},
|
||||
"explanation": response_text,
|
||||
"success": result.get("success", False)
|
||||
}
|
||||
|
||||
|
||||
def _extract_json_from_text(text: str) -> Optional[Dict]:
|
||||
"""Try to extract JSON object from text."""
|
||||
try:
|
||||
# Look for JSON object in text
|
||||
start = text.find('{')
|
||||
end = text.rfind('}')
|
||||
if start != -1 and end != -1:
|
||||
json_str = text[start:end+1]
|
||||
return json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def get_step_by_step(
|
||||
task: str,
|
||||
skill_level: str = "intermediate"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get step-by-step instructions for a task.
|
||||
|
||||
Args:
|
||||
task: The task to get instructions for
|
||||
skill_level: User skill level (beginner, intermediate, advanced)
|
||||
|
||||
Returns:
|
||||
Dictionary with step-by-step instructions
|
||||
"""
|
||||
skill_context = {
|
||||
"beginner": "Explain in simple terms, avoid jargon, include screenshots references",
|
||||
"intermediate": "Provide clear steps with command examples",
|
||||
"advanced": "Be concise, focus on efficiency and best practices"
|
||||
}
|
||||
|
||||
context = skill_context.get(skill_level, skill_context["intermediate"])
|
||||
|
||||
prompt = f"""Provide step-by-step instructions for: {task}
|
||||
|
||||
User skill level: {skill_level}
|
||||
{context}
|
||||
|
||||
Format as numbered steps with clear actions. Include any commands to run."""
|
||||
|
||||
result = await chat_completion(
|
||||
message=prompt,
|
||||
system_prompt="You are an expert security instructor. Provide clear, actionable step-by-step guidance."
|
||||
)
|
||||
|
||||
# Parse steps from response
|
||||
steps = _parse_steps_from_text(result.get("message", ""))
|
||||
|
||||
return {
|
||||
"task": task,
|
||||
"skill_level": skill_level,
|
||||
"steps": steps,
|
||||
"full_explanation": result.get("message", ""),
|
||||
"success": result.get("success", False)
|
||||
}
|
||||
|
||||
|
||||
def _parse_steps_from_text(text: str) -> List[Dict[str, str]]:
|
||||
"""Parse numbered steps from text."""
|
||||
steps = []
|
||||
lines = text.split('\n')
|
||||
|
||||
for line in lines:
|
||||
# Match patterns like "1.", "Step 1:", "1)"
|
||||
import re
|
||||
match = re.match(r'^(?:Step\s+)?(\d+)[.):]\s*(.+)$', line.strip(), re.IGNORECASE)
|
||||
if match:
|
||||
step_num = int(match.group(1))
|
||||
step_text = match.group(2).strip()
|
||||
steps.append({
|
||||
"number": step_num,
|
||||
"instruction": step_text
|
||||
})
|
||||
|
||||
return steps
|
||||
|
||||
|
||||
def clear_conversation_context(session_id: str) -> bool:
|
||||
"""
|
||||
Clear conversation context for a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to clear
|
||||
|
||||
Returns:
|
||||
True if cleared, False if session didn't exist
|
||||
"""
|
||||
if session_id in conversation_contexts:
|
||||
del conversation_contexts[session_id]
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_conversation_summary(session_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get summary of conversation for a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID
|
||||
|
||||
Returns:
|
||||
Dictionary with conversation summary
|
||||
"""
|
||||
if session_id not in conversation_contexts:
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"exists": False,
|
||||
"message_count": 0
|
||||
}
|
||||
|
||||
messages = conversation_contexts[session_id]
|
||||
user_messages = [m for m in messages if m["role"] == "user"]
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"exists": True,
|
||||
"message_count": len(messages),
|
||||
"user_message_count": len(user_messages),
|
||||
"last_messages": messages[-5:] if messages else []
|
||||
}
|
||||
505
services/hackgpt-api/app/nmap_parser.py
Normal file
505
services/hackgpt-api/app/nmap_parser.py
Normal file
@@ -0,0 +1,505 @@
|
||||
"""
|
||||
Nmap Parser Module
|
||||
Parses Nmap XML or JSON output to extract host information including:
|
||||
- IP addresses, hostnames
|
||||
- Operating system detection
|
||||
- Device type classification (workstation/server/appliance)
|
||||
- MAC vendor information
|
||||
- Open ports and services
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
from typing import Dict, List, Any, Optional
|
||||
import re
|
||||
|
||||
|
||||
def parse_nmap_xml(xml_content: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parse Nmap XML output and extract host information.
|
||||
|
||||
Args:
|
||||
xml_content: Raw XML string from nmap -oX output
|
||||
|
||||
Returns:
|
||||
List of host dictionaries with parsed information
|
||||
"""
|
||||
hosts = []
|
||||
|
||||
try:
|
||||
# Clean up XML content - remove any non-XML content before the declaration
|
||||
xml_start = xml_content.find('<?xml')
|
||||
if xml_start == -1:
|
||||
xml_start = xml_content.find('<nmaprun')
|
||||
if xml_start > 0:
|
||||
xml_content = xml_content[xml_start:]
|
||||
|
||||
root = ET.fromstring(xml_content)
|
||||
|
||||
for host_elem in root.findall('.//host'):
|
||||
# Check if host is up
|
||||
status = host_elem.find('status')
|
||||
if status is None or status.get('state') != 'up':
|
||||
continue
|
||||
|
||||
host = _parse_host_element(host_elem)
|
||||
if host.get('ip'):
|
||||
hosts.append(host)
|
||||
|
||||
except ET.ParseError as e:
|
||||
print(f"XML parsing error: {e}")
|
||||
# Return empty list on parse error
|
||||
return []
|
||||
|
||||
return hosts
|
||||
|
||||
|
||||
def parse_nmap_json(json_content: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parse Nmap JSON output and extract host information.
|
||||
|
||||
Args:
|
||||
json_content: JSON string from nmap with JSON output
|
||||
|
||||
Returns:
|
||||
List of host dictionaries with parsed information
|
||||
"""
|
||||
hosts = []
|
||||
|
||||
try:
|
||||
data = json.loads(json_content)
|
||||
|
||||
# Handle different JSON structures
|
||||
if isinstance(data, list):
|
||||
scan_results = data
|
||||
elif isinstance(data, dict):
|
||||
# Try common JSON nmap output structures
|
||||
scan_results = data.get('hosts', data.get('scan', []))
|
||||
else:
|
||||
return []
|
||||
|
||||
for host_data in scan_results:
|
||||
host = _parse_host_json(host_data)
|
||||
if host.get('ip'):
|
||||
hosts.append(host)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON parsing error: {e}")
|
||||
return []
|
||||
|
||||
return hosts
|
||||
|
||||
|
||||
def _parse_host_element(host_elem: ET.Element) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse an individual host XML element.
|
||||
|
||||
Args:
|
||||
host_elem: XML Element representing a single host
|
||||
|
||||
Returns:
|
||||
Dictionary with host information
|
||||
"""
|
||||
host = {
|
||||
'ip': '',
|
||||
'hostname': '',
|
||||
'mac': '',
|
||||
'vendor': '',
|
||||
'os_type': '',
|
||||
'os_details': '',
|
||||
'device_type': '',
|
||||
'ports': [],
|
||||
'os_accuracy': 0
|
||||
}
|
||||
|
||||
# Extract IP address
|
||||
addr = host_elem.find("address[@addrtype='ipv4']")
|
||||
if addr is not None:
|
||||
host['ip'] = addr.get('addr', '')
|
||||
|
||||
# Extract MAC address and vendor
|
||||
mac = host_elem.find("address[@addrtype='mac']")
|
||||
if mac is not None:
|
||||
host['mac'] = mac.get('addr', '')
|
||||
host['vendor'] = mac.get('vendor', '')
|
||||
|
||||
# Extract hostname
|
||||
hostname_elem = host_elem.find(".//hostname")
|
||||
if hostname_elem is not None:
|
||||
host['hostname'] = hostname_elem.get('name', '')
|
||||
|
||||
# Extract OS information
|
||||
osmatch = host_elem.find(".//osmatch")
|
||||
if osmatch is not None:
|
||||
os_name = osmatch.get('name', '')
|
||||
host['os_details'] = os_name
|
||||
host['os_type'] = detect_os_type(os_name)
|
||||
try:
|
||||
host['os_accuracy'] = int(osmatch.get('accuracy', 0))
|
||||
except (ValueError, TypeError):
|
||||
host['os_accuracy'] = 0
|
||||
else:
|
||||
# Try osclass as fallback
|
||||
osclass = host_elem.find(".//osclass")
|
||||
if osclass is not None:
|
||||
osfamily = osclass.get('osfamily', '')
|
||||
osgen = osclass.get('osgen', '')
|
||||
host['os_type'] = detect_os_type(osfamily)
|
||||
host['os_details'] = f"{osfamily} {osgen}".strip()
|
||||
try:
|
||||
host['os_accuracy'] = int(osclass.get('accuracy', 0))
|
||||
except (ValueError, TypeError):
|
||||
host['os_accuracy'] = 0
|
||||
|
||||
# Extract ports
|
||||
for port_elem in host_elem.findall(".//port"):
|
||||
port_info = {
|
||||
'port': int(port_elem.get('portid', 0)),
|
||||
'protocol': port_elem.get('protocol', 'tcp'),
|
||||
'state': '',
|
||||
'service': '',
|
||||
'product': '',
|
||||
'version': ''
|
||||
}
|
||||
|
||||
state_elem = port_elem.find('state')
|
||||
if state_elem is not None:
|
||||
port_info['state'] = state_elem.get('state', '')
|
||||
|
||||
service_elem = port_elem.find('service')
|
||||
if service_elem is not None:
|
||||
port_info['service'] = service_elem.get('name', '')
|
||||
port_info['product'] = service_elem.get('product', '')
|
||||
port_info['version'] = service_elem.get('version', '')
|
||||
|
||||
# Use service info to help detect OS if not already detected
|
||||
if not host['os_type']:
|
||||
product = service_elem.get('product', '').lower()
|
||||
if 'microsoft' in product or 'windows' in product:
|
||||
host['os_type'] = 'Windows'
|
||||
elif 'apache' in product or 'nginx' in product or 'linux' in product:
|
||||
host['os_type'] = 'Linux'
|
||||
|
||||
if port_info['state'] == 'open':
|
||||
host['ports'].append(port_info)
|
||||
|
||||
# Infer OS from ports if still unknown
|
||||
if not host['os_type'] and host['ports']:
|
||||
host['os_type'] = _infer_os_from_ports(host['ports'])
|
||||
|
||||
# Classify device type
|
||||
host['device_type'] = classify_device_type(host)
|
||||
|
||||
return host
|
||||
|
||||
|
||||
def _parse_host_json(host_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse host data from JSON format.
|
||||
|
||||
Args:
|
||||
host_data: Dictionary containing host information
|
||||
|
||||
Returns:
|
||||
Standardized host dictionary
|
||||
"""
|
||||
host = {
|
||||
'ip': host_data.get('ip', host_data.get('address', '')),
|
||||
'hostname': host_data.get('hostname', host_data.get('name', '')),
|
||||
'mac': host_data.get('mac', ''),
|
||||
'vendor': host_data.get('vendor', ''),
|
||||
'os_type': '',
|
||||
'os_details': '',
|
||||
'device_type': '',
|
||||
'ports': [],
|
||||
'os_accuracy': 0
|
||||
}
|
||||
|
||||
# Extract OS information
|
||||
os_info = host_data.get('os', host_data.get('osmatch', {}))
|
||||
if isinstance(os_info, dict):
|
||||
host['os_details'] = os_info.get('name', os_info.get('details', ''))
|
||||
host['os_accuracy'] = int(os_info.get('accuracy', 0))
|
||||
elif isinstance(os_info, str):
|
||||
host['os_details'] = os_info
|
||||
|
||||
host['os_type'] = detect_os_type(host['os_details'])
|
||||
|
||||
# Extract ports
|
||||
ports_data = host_data.get('ports', host_data.get('tcp', {}))
|
||||
if isinstance(ports_data, list):
|
||||
host['ports'] = ports_data
|
||||
elif isinstance(ports_data, dict):
|
||||
for port_num, port_info in ports_data.items():
|
||||
if isinstance(port_info, dict):
|
||||
host['ports'].append({
|
||||
'port': int(port_num),
|
||||
'protocol': 'tcp',
|
||||
'state': port_info.get('state', ''),
|
||||
'service': port_info.get('service', port_info.get('name', '')),
|
||||
'product': port_info.get('product', ''),
|
||||
'version': port_info.get('version', '')
|
||||
})
|
||||
|
||||
# Infer OS from ports if unknown
|
||||
if not host['os_type'] and host['ports']:
|
||||
host['os_type'] = _infer_os_from_ports(host['ports'])
|
||||
|
||||
# Classify device type
|
||||
host['device_type'] = classify_device_type(host)
|
||||
|
||||
return host
|
||||
|
||||
|
||||
def detect_os_type(os_string: str) -> str:
|
||||
"""
|
||||
Detect OS type from an OS description string.
|
||||
|
||||
Args:
|
||||
os_string: OS description from nmap
|
||||
|
||||
Returns:
|
||||
Standardized OS type string
|
||||
"""
|
||||
if not os_string:
|
||||
return 'Unknown'
|
||||
|
||||
os_lower = os_string.lower()
|
||||
|
||||
# Windows detection
|
||||
if any(keyword in os_lower for keyword in ['windows', 'microsoft', 'win7', 'win10', 'win11', 'server 20']):
|
||||
return 'Windows'
|
||||
|
||||
# Linux detection
|
||||
elif any(keyword in os_lower for keyword in ['linux', 'ubuntu', 'debian', 'centos', 'red hat', 'rhel', 'fedora', 'arch', 'gentoo', 'suse']):
|
||||
return 'Linux'
|
||||
|
||||
# macOS detection
|
||||
elif any(keyword in os_lower for keyword in ['mac os', 'darwin', 'apple', 'macos']):
|
||||
return 'macOS'
|
||||
|
||||
# Unix variants
|
||||
elif any(keyword in os_lower for keyword in ['freebsd', 'openbsd', 'netbsd', 'unix', 'solaris', 'aix']):
|
||||
return 'Unix'
|
||||
|
||||
# Network devices
|
||||
elif any(keyword in os_lower for keyword in ['cisco', 'ios']):
|
||||
return 'Cisco'
|
||||
elif 'juniper' in os_lower or 'junos' in os_lower:
|
||||
return 'Juniper'
|
||||
elif 'fortinet' in os_lower or 'fortigate' in os_lower:
|
||||
return 'Fortinet'
|
||||
elif 'palo alto' in os_lower or 'panos' in os_lower:
|
||||
return 'Palo Alto'
|
||||
elif any(keyword in os_lower for keyword in ['switch', 'router', 'firewall', 'gateway']):
|
||||
return 'Network Device'
|
||||
|
||||
# Virtualization
|
||||
elif 'vmware' in os_lower or 'esxi' in os_lower:
|
||||
return 'VMware'
|
||||
elif 'hyper-v' in os_lower:
|
||||
return 'Hyper-V'
|
||||
|
||||
# Mobile
|
||||
elif 'android' in os_lower:
|
||||
return 'Android'
|
||||
elif 'ios' in os_lower and 'apple' in os_lower:
|
||||
return 'iOS'
|
||||
|
||||
# Printers and IoT
|
||||
elif any(keyword in os_lower for keyword in ['printer', 'hp jetdirect', 'canon', 'epson', 'xerox']):
|
||||
return 'Printer'
|
||||
elif 'iot' in os_lower or 'embedded' in os_lower:
|
||||
return 'IoT Device'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def classify_device_type(host: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Classify the device type based on OS, ports, and services.
|
||||
|
||||
Args:
|
||||
host: Host dictionary with OS and port information
|
||||
|
||||
Returns:
|
||||
Device type classification (workstation, server, network, appliance, etc.)
|
||||
"""
|
||||
os_type = host.get('os_type', '').lower()
|
||||
os_details = host.get('os_details', '').lower()
|
||||
ports = host.get('ports', [])
|
||||
vendor = host.get('vendor', '').lower()
|
||||
|
||||
port_numbers = {p['port'] for p in ports}
|
||||
services = {p.get('service', '').lower() for p in ports}
|
||||
|
||||
# Network infrastructure
|
||||
if os_type in ['cisco', 'juniper', 'fortinet', 'palo alto', 'network device']:
|
||||
if 'switch' in os_details or 'catalyst' in os_details:
|
||||
return 'Network Switch'
|
||||
elif 'router' in os_details or 'ios' in os_details:
|
||||
return 'Router'
|
||||
elif 'firewall' in os_details or 'fortigate' in os_details:
|
||||
return 'Firewall'
|
||||
else:
|
||||
return 'Network Device'
|
||||
|
||||
# Check for SNMP (common on network devices)
|
||||
if 161 in port_numbers or 162 in port_numbers:
|
||||
return 'Network Device'
|
||||
|
||||
# Printers
|
||||
if os_type == 'printer' or 9100 in port_numbers or 631 in port_numbers:
|
||||
return 'Printer'
|
||||
|
||||
# IoT devices
|
||||
if os_type == 'iot device':
|
||||
return 'IoT Device'
|
||||
|
||||
# Servers - check for common server ports and services
|
||||
server_indicators = {
|
||||
# Web servers
|
||||
80, 443, 8080, 8443,
|
||||
# Database servers
|
||||
3306, 5432, 1433, 27017, 6379,
|
||||
# Mail servers
|
||||
25, 587, 465, 110, 995, 143, 993,
|
||||
# File servers
|
||||
21, 22, 139, 445, 2049,
|
||||
# Directory services
|
||||
389, 636, 88, 464,
|
||||
# Application servers
|
||||
8000, 8001, 8888, 9000, 3000, 5000,
|
||||
# Virtualization
|
||||
902, 443
|
||||
}
|
||||
|
||||
server_services = {
|
||||
'http', 'https', 'apache', 'nginx', 'iis',
|
||||
'mysql', 'postgresql', 'mssql', 'mongodb', 'redis',
|
||||
'smtp', 'pop3', 'imap',
|
||||
'ftp', 'ssh', 'smb', 'nfs',
|
||||
'ldap', 'ldaps', 'kerberos',
|
||||
'vmware'
|
||||
}
|
||||
|
||||
# Check if it's explicitly a server OS
|
||||
if 'server' in os_details:
|
||||
return 'Server'
|
||||
|
||||
# Check for server ports/services
|
||||
if port_numbers & server_indicators or services & server_services:
|
||||
# More than 3 server ports suggests a server
|
||||
if len(port_numbers & server_indicators) >= 3:
|
||||
return 'Server'
|
||||
# Specific database or web server services
|
||||
if any(svc in services for svc in ['mysql', 'postgresql', 'mongodb', 'apache', 'nginx', 'iis']):
|
||||
return 'Server'
|
||||
|
||||
# Virtualization hosts
|
||||
if os_type in ['vmware', 'hyper-v'] or 'esxi' in os_details:
|
||||
return 'Virtualization Host'
|
||||
|
||||
# Workstations
|
||||
if os_type in ['windows', 'macos', 'linux']:
|
||||
# Windows/macOS are typically workstations unless server indicators
|
||||
if os_type in ['windows', 'macos']:
|
||||
if 3389 in port_numbers: # RDP
|
||||
# Could be either, but default to workstation
|
||||
return 'Workstation'
|
||||
return 'Workstation'
|
||||
# Linux could be either
|
||||
elif os_type == 'linux':
|
||||
# Desktop Linux if few ports open
|
||||
if len(port_numbers) <= 3:
|
||||
return 'Workstation'
|
||||
else:
|
||||
return 'Server'
|
||||
|
||||
# Mobile devices
|
||||
if os_type in ['android', 'ios']:
|
||||
return 'Mobile Device'
|
||||
|
||||
# Default classification
|
||||
if len(port_numbers) >= 5:
|
||||
return 'Server'
|
||||
elif len(port_numbers) >= 1:
|
||||
return 'Workstation'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def _infer_os_from_ports(ports: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
Infer OS type from open ports and services.
|
||||
|
||||
Args:
|
||||
ports: List of port dictionaries
|
||||
|
||||
Returns:
|
||||
Inferred OS type
|
||||
"""
|
||||
port_numbers = {p['port'] for p in ports}
|
||||
services = [p.get('service', '').lower() for p in ports]
|
||||
products = [p.get('product', '').lower() for p in ports]
|
||||
|
||||
# Windows indicators
|
||||
windows_ports = {135, 139, 445, 3389, 5985, 5986}
|
||||
if windows_ports & port_numbers:
|
||||
return 'Windows'
|
||||
|
||||
if any('microsoft' in p or 'windows' in p for p in products):
|
||||
return 'Windows'
|
||||
|
||||
# Linux indicators (SSH is common)
|
||||
if 22 in port_numbers and 'ssh' in services:
|
||||
# Could be Linux or Unix
|
||||
return 'Linux'
|
||||
|
||||
# Network device indicators
|
||||
if 161 in port_numbers or 162 in port_numbers: # SNMP
|
||||
return 'Network Device'
|
||||
if 23 in port_numbers: # Telnet (often network devices)
|
||||
return 'Network Device'
|
||||
|
||||
# Printer indicators
|
||||
if 9100 in port_numbers or 631 in port_numbers:
|
||||
return 'Printer'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def get_os_icon_name(host: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Get the appropriate icon name for a host based on OS and device type.
|
||||
|
||||
Args:
|
||||
host: Host dictionary
|
||||
|
||||
Returns:
|
||||
Icon filename (without extension)
|
||||
"""
|
||||
os_type = host.get('os_type', '').lower()
|
||||
device_type = host.get('device_type', '').lower()
|
||||
|
||||
# Device type takes precedence for specialized devices
|
||||
if 'server' in device_type:
|
||||
return 'server'
|
||||
elif 'network' in device_type or 'router' in device_type or 'switch' in device_type or 'firewall' in device_type:
|
||||
return 'network'
|
||||
elif 'printer' in device_type:
|
||||
return 'printer'
|
||||
elif 'workstation' in device_type:
|
||||
return 'workstation'
|
||||
|
||||
# Fall back to OS type
|
||||
if 'windows' in os_type:
|
||||
return 'windows'
|
||||
elif 'linux' in os_type or 'unix' in os_type:
|
||||
return 'linux'
|
||||
elif 'mac' in os_type:
|
||||
return 'mac'
|
||||
elif any(net in os_type for net in ['cisco', 'juniper', 'fortinet', 'network']):
|
||||
return 'network'
|
||||
|
||||
return 'unknown'
|
||||
508
services/hackgpt-api/app/voice.py
Normal file
508
services/hackgpt-api/app/voice.py
Normal file
@@ -0,0 +1,508 @@
|
||||
"""
|
||||
Voice Control Module
|
||||
Handles speech-to-text and text-to-speech functionality, plus voice command routing.
|
||||
Supports local Whisper (preferred) and OpenAI API as fallback.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
def transcribe_audio(audio_data: bytes, format: str = "wav") -> Dict[str, Any]:
|
||||
"""
|
||||
Transcribe audio to text using Whisper (local preferred) or OpenAI API.
|
||||
|
||||
Args:
|
||||
audio_data: Raw audio bytes
|
||||
format: Audio format (wav, mp3, webm, etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary with transcription result and metadata
|
||||
{
|
||||
"text": "transcribed text",
|
||||
"language": "en",
|
||||
"confidence": 0.95,
|
||||
"method": "whisper-local" or "openai"
|
||||
}
|
||||
"""
|
||||
# Try local Whisper first
|
||||
try:
|
||||
return _transcribe_with_local_whisper(audio_data, format)
|
||||
except Exception as e:
|
||||
print(f"Local Whisper failed: {e}, falling back to OpenAI API")
|
||||
|
||||
# Fallback to OpenAI API if configured
|
||||
if os.getenv("OPENAI_API_KEY"):
|
||||
try:
|
||||
return _transcribe_with_openai(audio_data, format)
|
||||
except Exception as e:
|
||||
print(f"OpenAI transcription failed: {e}")
|
||||
return {
|
||||
"text": "",
|
||||
"error": f"Transcription failed: {str(e)}",
|
||||
"method": "none"
|
||||
}
|
||||
|
||||
return {
|
||||
"text": "",
|
||||
"error": "No transcription service available. Install Whisper or configure OPENAI_API_KEY.",
|
||||
"method": "none"
|
||||
}
|
||||
|
||||
|
||||
def _transcribe_with_local_whisper(audio_data: bytes, format: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Transcribe using local Whisper model.
|
||||
|
||||
Args:
|
||||
audio_data: Raw audio bytes
|
||||
format: Audio format
|
||||
|
||||
Returns:
|
||||
Transcription result dictionary
|
||||
"""
|
||||
try:
|
||||
import whisper
|
||||
|
||||
# Save audio to temporary file
|
||||
with tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) as temp_audio:
|
||||
temp_audio.write(audio_data)
|
||||
temp_audio_path = temp_audio.name
|
||||
|
||||
try:
|
||||
# Load model (use base model by default for speed/accuracy balance)
|
||||
model_size = os.getenv("WHISPER_MODEL", "base")
|
||||
model = whisper.load_model(model_size)
|
||||
|
||||
# Transcribe
|
||||
result = model.transcribe(temp_audio_path)
|
||||
|
||||
return {
|
||||
"text": result["text"].strip(),
|
||||
"language": result.get("language", "unknown"),
|
||||
"confidence": 1.0, # Whisper doesn't provide confidence scores
|
||||
"method": "whisper-local",
|
||||
"model": model_size
|
||||
}
|
||||
finally:
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(temp_audio_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
except ImportError:
|
||||
raise Exception("Whisper not installed. Install with: pip install openai-whisper")
|
||||
|
||||
|
||||
def _transcribe_with_openai(audio_data: bytes, format: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Transcribe using OpenAI Whisper API.
|
||||
|
||||
Args:
|
||||
audio_data: Raw audio bytes
|
||||
format: Audio format
|
||||
|
||||
Returns:
|
||||
Transcription result dictionary
|
||||
"""
|
||||
try:
|
||||
import httpx
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
raise Exception("OPENAI_API_KEY not configured")
|
||||
|
||||
# Prepare multipart form data
|
||||
files = {
|
||||
'file': (f'audio.{format}', audio_data, f'audio/{format}')
|
||||
}
|
||||
data = {
|
||||
'model': 'whisper-1',
|
||||
'language': 'en' # Can be auto-detected by omitting this
|
||||
}
|
||||
|
||||
# Make API request
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
'https://api.openai.com/v1/audio/transcriptions',
|
||||
headers={'Authorization': f'Bearer {api_key}'},
|
||||
files=files,
|
||||
data=data,
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return {
|
||||
"text": result.get("text", "").strip(),
|
||||
"language": "en",
|
||||
"confidence": 1.0,
|
||||
"method": "openai"
|
||||
}
|
||||
else:
|
||||
raise Exception(f"OpenAI API error: {response.status_code} - {response.text}")
|
||||
|
||||
except ImportError:
|
||||
raise Exception("httpx not installed")
|
||||
|
||||
|
||||
def speak_text(text: str, voice: str = "alloy", format: str = "mp3") -> Optional[bytes]:
|
||||
"""
|
||||
Convert text to speech using OpenAI TTS, Coqui, or browser fallback.
|
||||
|
||||
Args:
|
||||
text: Text to convert to speech
|
||||
voice: Voice selection (depends on TTS engine)
|
||||
format: Audio format (mp3, wav, opus)
|
||||
|
||||
Returns:
|
||||
Audio bytes or None if TTS not available
|
||||
"""
|
||||
# Try OpenAI TTS if configured
|
||||
if os.getenv("OPENAI_API_KEY"):
|
||||
try:
|
||||
return _tts_with_openai(text, voice, format)
|
||||
except Exception as e:
|
||||
print(f"OpenAI TTS failed: {e}")
|
||||
|
||||
# Try local Coqui TTS
|
||||
try:
|
||||
return _tts_with_coqui(text)
|
||||
except Exception as e:
|
||||
print(f"Coqui TTS failed: {e}")
|
||||
|
||||
# Return None to signal browser should handle TTS
|
||||
return None
|
||||
|
||||
|
||||
def _tts_with_openai(text: str, voice: str, format: str) -> bytes:
|
||||
"""
|
||||
Text-to-speech using OpenAI TTS API.
|
||||
|
||||
Args:
|
||||
text: Text to speak
|
||||
voice: Voice name (alloy, echo, fable, onyx, nova, shimmer)
|
||||
format: Audio format
|
||||
|
||||
Returns:
|
||||
Audio bytes
|
||||
"""
|
||||
try:
|
||||
import httpx
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
raise Exception("OPENAI_API_KEY not configured")
|
||||
|
||||
# Valid voices for OpenAI TTS
|
||||
valid_voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
|
||||
if voice not in valid_voices:
|
||||
voice = "alloy"
|
||||
|
||||
# Valid formats
|
||||
valid_formats = ["mp3", "opus", "aac", "flac"]
|
||||
if format not in valid_formats:
|
||||
format = "mp3"
|
||||
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
'https://api.openai.com/v1/audio/speech',
|
||||
headers={
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json={
|
||||
'model': 'tts-1', # or 'tts-1-hd' for higher quality
|
||||
'input': text[:4096], # Max 4096 characters
|
||||
'voice': voice,
|
||||
'response_format': format
|
||||
},
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.content
|
||||
else:
|
||||
raise Exception(f"OpenAI TTS error: {response.status_code} - {response.text}")
|
||||
|
||||
except ImportError:
|
||||
raise Exception("httpx not installed")
|
||||
|
||||
|
||||
def _tts_with_coqui(text: str) -> bytes:
|
||||
"""
|
||||
Text-to-speech using Coqui TTS (local).
|
||||
|
||||
Args:
|
||||
text: Text to speak
|
||||
|
||||
Returns:
|
||||
Audio bytes (WAV format)
|
||||
"""
|
||||
try:
|
||||
from TTS.api import TTS
|
||||
import numpy as np
|
||||
import io
|
||||
import wave
|
||||
|
||||
# Initialize TTS with a fast model
|
||||
tts = TTS(model_name="tts_models/en/ljspeech/tacotron2-DDC", progress_bar=False)
|
||||
|
||||
# Generate speech
|
||||
wav = tts.tts(text)
|
||||
|
||||
# Convert to WAV bytes
|
||||
wav_io = io.BytesIO()
|
||||
with wave.open(wav_io, 'wb') as wav_file:
|
||||
wav_file.setnchannels(1)
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(22050)
|
||||
wav_file.writeframes(np.array(wav * 32767, dtype=np.int16).tobytes())
|
||||
|
||||
return wav_io.getvalue()
|
||||
|
||||
except ImportError:
|
||||
raise Exception("Coqui TTS not installed. Install with: pip install TTS")
|
||||
|
||||
|
||||
def parse_voice_command(text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse voice command text to extract intent and parameters.
|
||||
|
||||
Args:
|
||||
text: Transcribed voice command text
|
||||
|
||||
Returns:
|
||||
Dictionary with command intent and parameters
|
||||
{
|
||||
"intent": "list_agents" | "summarize" | "deploy_agent" | "run_scan" | "unknown",
|
||||
"parameters": {...},
|
||||
"confidence": 0.0-1.0
|
||||
}
|
||||
"""
|
||||
text_lower = text.lower().strip()
|
||||
|
||||
# Command patterns
|
||||
patterns = [
|
||||
# List commands
|
||||
(r'\b(list|show|display)\s+(agents|scans|findings|results)\b', 'list', lambda m: {'target': m.group(2)}),
|
||||
|
||||
# Summarize commands
|
||||
(r'\b(summarize|summary of|sum up)\s+(findings|results|scan)\b', 'summarize', lambda m: {'target': m.group(2)}),
|
||||
|
||||
# Deploy/start commands
|
||||
(r'\b(deploy|start|launch|run)\s+agent\s+(?:on\s+)?(.+)', 'deploy_agent', lambda m: {'target': m.group(2).strip()}),
|
||||
|
||||
# Scan commands
|
||||
(r'\b(scan|nmap|enumerate)\s+(.+?)(?:\s+(?:using|with)\s+(\w+))?$', 'run_scan',
|
||||
lambda m: {'target': m.group(2).strip(), 'tool': m.group(3) if m.group(3) else 'nmap'}),
|
||||
|
||||
# Status commands
|
||||
(r'\b(status|what\'?s\s+(?:the\s+)?status)\b', 'get_status', lambda m: {}),
|
||||
|
||||
# Help commands
|
||||
(r'\b(help|how\s+do\s+i|assist)\b', 'help', lambda m: {'query': text}),
|
||||
|
||||
# Clear/stop commands
|
||||
(r'\b(stop|cancel|clear)\s+(scan|all|everything)\b', 'stop', lambda m: {'target': m.group(2)}),
|
||||
|
||||
# Navigate commands
|
||||
(r'\b(go\s+to|open|navigate\s+to)\s+(.+)', 'navigate', lambda m: {'destination': m.group(2).strip()}),
|
||||
]
|
||||
|
||||
# Try to match patterns
|
||||
for pattern, intent, param_func in patterns:
|
||||
match = re.search(pattern, text_lower)
|
||||
if match:
|
||||
try:
|
||||
parameters = param_func(match)
|
||||
return {
|
||||
"intent": intent,
|
||||
"parameters": parameters,
|
||||
"confidence": 0.85,
|
||||
"raw_text": text
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error parsing command parameters: {e}")
|
||||
|
||||
# No pattern matched
|
||||
return {
|
||||
"intent": "unknown",
|
||||
"parameters": {},
|
||||
"confidence": 0.0,
|
||||
"raw_text": text
|
||||
}
|
||||
|
||||
|
||||
def route_command(command_result: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Route a parsed voice command to the appropriate action.
|
||||
|
||||
Args:
|
||||
command_result: Result from parse_voice_command()
|
||||
|
||||
Returns:
|
||||
Dictionary with routing information
|
||||
{
|
||||
"action": "api_call" | "navigate" | "notify" | "error",
|
||||
"endpoint": "/api/...",
|
||||
"method": "GET" | "POST",
|
||||
"data": {...},
|
||||
"message": "Human-readable action description"
|
||||
}
|
||||
"""
|
||||
intent = command_result.get("intent")
|
||||
params = command_result.get("parameters", {})
|
||||
|
||||
if intent == "list":
|
||||
target = params.get("target", "")
|
||||
endpoint_map = {
|
||||
"agents": "/api/agents",
|
||||
"scans": "/api/scans",
|
||||
"findings": "/api/findings",
|
||||
"results": "/api/results"
|
||||
}
|
||||
endpoint = endpoint_map.get(target, "/api/scans")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": endpoint,
|
||||
"method": "GET",
|
||||
"data": {},
|
||||
"message": f"Fetching {target}..."
|
||||
}
|
||||
|
||||
elif intent == "summarize":
|
||||
target = params.get("target", "findings")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/summarize",
|
||||
"method": "POST",
|
||||
"data": {"target": target},
|
||||
"message": f"Summarizing {target}..."
|
||||
}
|
||||
|
||||
elif intent == "deploy_agent":
|
||||
target = params.get("target", "")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/agents/deploy",
|
||||
"method": "POST",
|
||||
"data": {"target": target},
|
||||
"message": f"Deploying agent to {target}..."
|
||||
}
|
||||
|
||||
elif intent == "run_scan":
|
||||
target = params.get("target", "")
|
||||
tool = params.get("tool", "nmap")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/scan",
|
||||
"method": "POST",
|
||||
"data": {
|
||||
"tool": tool,
|
||||
"target": target,
|
||||
"scan_type": "quick"
|
||||
},
|
||||
"message": f"Starting {tool} scan of {target}..."
|
||||
}
|
||||
|
||||
elif intent == "get_status":
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/status",
|
||||
"method": "GET",
|
||||
"data": {},
|
||||
"message": "Checking system status..."
|
||||
}
|
||||
|
||||
elif intent == "help":
|
||||
query = params.get("query", "")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/llm/chat",
|
||||
"method": "POST",
|
||||
"data": {"message": query, "context": "help_request"},
|
||||
"message": "Getting help..."
|
||||
}
|
||||
|
||||
elif intent == "stop":
|
||||
target = params.get("target", "all")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/scans/clear" if target in ["all", "everything"] else "/api/scan/stop",
|
||||
"method": "DELETE",
|
||||
"data": {},
|
||||
"message": f"Stopping {target}..."
|
||||
}
|
||||
|
||||
elif intent == "navigate":
|
||||
destination = params.get("destination", "")
|
||||
# Map common destinations
|
||||
destination_map = {
|
||||
"dashboard": "/",
|
||||
"home": "/",
|
||||
"terminal": "/terminal",
|
||||
"scans": "/scans",
|
||||
"settings": "/settings"
|
||||
}
|
||||
path = destination_map.get(destination, f"/{destination}")
|
||||
return {
|
||||
"action": "navigate",
|
||||
"endpoint": path,
|
||||
"method": "GET",
|
||||
"data": {},
|
||||
"message": f"Navigating to {destination}..."
|
||||
}
|
||||
|
||||
else:
|
||||
# Unknown intent - return error
|
||||
return {
|
||||
"action": "error",
|
||||
"endpoint": "",
|
||||
"method": "",
|
||||
"data": {},
|
||||
"message": "I didn't understand that command. Try 'help' for available commands.",
|
||||
"error": "unknown_intent"
|
||||
}
|
||||
|
||||
|
||||
def get_voice_command_help() -> Dict[str, list]:
|
||||
"""
|
||||
Get list of available voice commands.
|
||||
|
||||
Returns:
|
||||
Dictionary categorized by command type
|
||||
"""
|
||||
return {
|
||||
"navigation": [
|
||||
"Go to dashboard",
|
||||
"Open terminal",
|
||||
"Navigate to scans"
|
||||
],
|
||||
"scanning": [
|
||||
"Scan 192.168.1.1",
|
||||
"Run nmap scan on example.com",
|
||||
"Start scan of 10.0.0.0/24"
|
||||
],
|
||||
"information": [
|
||||
"List scans",
|
||||
"Show agents",
|
||||
"Display findings",
|
||||
"What's the status"
|
||||
],
|
||||
"actions": [
|
||||
"Deploy agent on target.com",
|
||||
"Stop all scans",
|
||||
"Clear everything",
|
||||
"Summarize findings"
|
||||
],
|
||||
"help": [
|
||||
"Help me with nmap",
|
||||
"How do I scan a network",
|
||||
"Assist with reconnaissance"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user