Files
litterbox/app/analyzers/manager.py
T
2025-05-29 06:20:10 -07:00

450 lines
18 KiB
Python

# app/analyzers/manager.py
import logging
import subprocess
import time
import psutil
import json
from typing import Dict, Type, Optional, Tuple
from abc import ABC, abstractmethod
# Import analyzers
from .static.yara_analyzer import YaraStaticAnalyzer
from .static.checkplz_analyzer import CheckPlzAnalyzer
from .static.stringnalyzer_analyzer import StringsAnalyzer
from .dynamic.yara_analyzer import YaraDynamicAnalyzer
from .dynamic.pe_sieve_analyzer import PESieveAnalyzer
from .dynamic.moneta_analyzer import MonetaAnalyzer
from .dynamic.patriot_analyzer import PatriotAnalyzer
from .dynamic.hsb_analyzer import HSBAnalyzer
from .dynamic.rededr_analyzer import RedEdrAnalyzer
class BaseAnalyzer(ABC):
@abstractmethod
def analyze(self, target):
pass
@abstractmethod
def get_results(self):
pass
class AnalysisManager:
# Define analyzer mappings
STATIC_ANALYZERS = {
'yara': YaraStaticAnalyzer,
'checkplz': CheckPlzAnalyzer,
'stringnalyzer': StringsAnalyzer
}
DYNAMIC_ANALYZERS = {
'yara': YaraDynamicAnalyzer,
'pe_sieve': PESieveAnalyzer,
'moneta': MonetaAnalyzer,
'patriot': PatriotAnalyzer,
'hsb': HSBAnalyzer,
'rededr': RedEdrAnalyzer
}
def __init__(self, config: dict, logger: Optional[logging.Logger] = None):
self.logger = logger or logging.getLogger(__name__)
self.logger.debug("Initializing AnalysisManager")
self.config = config
self.static_analyzers: Dict[str, BaseAnalyzer] = {}
self.dynamic_analyzers: Dict[str, BaseAnalyzer] = {}
self._initialize_analyzers()
def _initialize_analyzer(self, name: str, analyzer_class: Type[BaseAnalyzer], config_section: dict) -> Optional[BaseAnalyzer]:
if not config_section.get('enabled', False):
self.logger.debug(f"Analyzer {name} is disabled in config")
return None
self.logger.debug(f"Initializing {name}")
try:
analyzer = analyzer_class(self.config)
self.logger.debug(f"{name} initialized successfully")
return analyzer
except Exception as e:
self.logger.error(f"Failed to initialize {name}: {e}", exc_info=True)
return None
def _initialize_analyzers(self):
self.logger.debug("Beginning analyzer initialization")
# Initialize static analyzers
static_config = self.config['analysis']['static']
for name, analyzer_class in self.STATIC_ANALYZERS.items():
if analyzer := self._initialize_analyzer(name, analyzer_class, static_config[name]):
self.static_analyzers[name] = analyzer
# Initialize dynamic analyzers
dynamic_config = self.config['analysis']['dynamic']
for name, analyzer_class in self.DYNAMIC_ANALYZERS.items():
if analyzer := self._initialize_analyzer(name, analyzer_class, dynamic_config[name]):
self.dynamic_analyzers[name] = analyzer
self.logger.debug(f"Initialized static analyzers: {list(self.static_analyzers.keys())}")
self.logger.debug(f"Initialized dynamic analyzers: {list(self.dynamic_analyzers.keys())}")
self.logger.debug("Analyzer initialization completed")
def _run_analyzers(self, analyzers: Dict[str, BaseAnalyzer], target, analysis_type: str) -> dict:
results = {}
if not analyzers:
self.logger.warning(f"No {analysis_type} analyzers are enabled")
return results
# For dynamic analysis, verify process exists first
if analysis_type == 'dynamic':
if not self._validate_dynamic_target(target):
return {'status': 'error', 'error': 'Process does not exist or is not running'}
self.logger.debug(f"Running {len(analyzers)} {analysis_type} analyzers")
for name, analyzer in analyzers.items():
try:
self.logger.debug(f"Running {name}")
analyzer.analyze(target)
results[name] = analyzer.get_results()
except Exception as e:
self.logger.error(f"Error in {name}: {str(e)}")
results[name] = {'status': 'error', 'error': str(e)}
return results
def _validate_dynamic_target(self, target) -> bool:
"""Validate that the target process exists for dynamic analysis"""
try:
process = psutil.Process(int(target))
return process.is_running()
except (ValueError, psutil.NoSuchProcess):
self.logger.error(f"Process {target} does not exist")
return False
def _create_metadata(self, start_time: float, **kwargs) -> dict:
"""Create analysis metadata with common fields"""
metadata = {
'total_duration': time.time() - start_time,
'timestamp': time.time()
}
metadata.update(kwargs)
return metadata
def run_static_analysis(self, file_path: str) -> dict:
start_time = time.time()
try:
results = self._run_analyzers(self.static_analyzers, file_path, 'static')
results['analysis_metadata'] = self._create_metadata(start_time)
except Exception as e:
self.logger.error(f"Error during static analysis: {str(e)}", exc_info=True)
results = {'analysis_metadata': self._create_metadata(start_time, error=str(e))}
self.logger.debug(f"Static analysis completed in {time.time() - start_time:.2f} seconds")
return results
def run_dynamic_analysis(self, target, is_pid: bool = False, cmd_args: list = None) -> dict:
self.logger.debug(f"Starting dynamic analysis - Target: {target}, is_pid: {is_pid}, args: {cmd_args}")
start_time = time.time()
try:
if is_pid:
return self._run_pid_analysis(target, start_time)
else:
return self._run_file_analysis(target, cmd_args, start_time)
except Exception as e:
self.logger.error(f"Error during dynamic analysis: {str(e)}", exc_info=True)
return self._create_error_result(start_time, str(e), cmd_args)
def _run_pid_analysis(self, target: str, start_time: float) -> dict:
"""Handle PID-based analysis"""
try:
process, pid = self._validate_process(target, True)
results = self._run_analyzers(self.dynamic_analyzers, pid, 'dynamic')
results['analysis_metadata'] = self._create_metadata(start_time, cmd_args=[])
return results
except Exception as e:
return self._create_error_result(start_time, str(e))
def _run_file_analysis(self, target: str, cmd_args: list, start_time: float) -> dict:
"""Handle file-based analysis with RedEdr integration"""
results = {}
process = None
rededr = None
try:
# 1. Start RedEdr if enabled
rededr = self._initialize_rededr(target, results)
# 2. Validate and start process
try:
process, pid = self._validate_process(target, False, cmd_args)
except Exception as e:
return self._handle_process_startup_error(e, start_time, cmd_args)
# 3. Run regular analyzers (excluding RedEdr)
regular_analyzers = {k: v for k, v in self.dynamic_analyzers.items() if k != 'rededr'}
other_results = self._run_analyzers(regular_analyzers, pid, 'dynamic')
results.update(other_results)
# 4. Capture process output
results['process_output'] = self._capture_process_output(process)
# 5. Get RedEdr results and cleanup
if rededr:
self.logger.debug("Getting RedEdr events")
results['rededr'] = rededr.get_results()
self._cleanup_rededr(rededr)
results['analysis_metadata'] = self._create_metadata(
start_time,
early_termination=False,
analysis_started=True,
cmd_args=cmd_args or []
)
return results
except Exception as e:
return self._create_error_result(start_time, str(e), cmd_args)
def _initialize_rededr(self, target: str, results: dict):
"""Initialize RedEdr if enabled"""
rededr_config = self.config['analysis']['dynamic'].get('rededr', {})
if not rededr_config.get('enabled'):
return None
self.logger.debug("Initializing RedEdr analyzer")
try:
target_name = target.split('\\')[-1]
rededr = RedEdrAnalyzer(self.config)
if rededr.start_tool(target_name):
etw_wait_time = rededr_config.get('etw_wait_time', 5)
self.logger.debug(f"RedEdr initialized, waiting {etw_wait_time} seconds for ETW setup")
time.sleep(etw_wait_time)
return rededr
else:
self.logger.error("Failed to start RedEdr")
results['rededr'] = {'status': 'error', 'error': 'Failed to start tool'}
return None
except Exception as e:
self.logger.error(f"Error initializing RedEdr: {e}")
results['rededr'] = {'status': 'error', 'error': str(e)}
return None
def _cleanup_rededr(self, rededr):
"""Cleanup RedEdr analyzer"""
self.logger.debug("Cleaning up RedEdr")
try:
rededr.cleanup()
except Exception as e:
self.logger.error(f"Error cleaning up RedEdr: {e}")
def _capture_process_output(self, process) -> dict:
"""Capture output from process"""
if not process:
return {'had_output': False, 'error': 'No process to capture output from'}
self.logger.debug("Capturing process output")
try:
stdout, stderr = process.communicate(timeout=1)
return {
'stdout': stdout.strip() if stdout else '',
'stderr': stderr.strip() if stderr else '',
'had_output': bool(stdout.strip() or stderr.strip()),
'output_truncated': False
}
except subprocess.TimeoutExpired:
self.logger.debug("Output capture timed out; killing the process")
self._cleanup_process(process, False)
stdout, stderr = process.communicate()
return {
'stdout': stdout.strip() if stdout else '',
'stderr': stderr.strip() if stderr else '',
'had_output': bool(stdout.strip() or stderr.strip()),
'output_truncated': False,
'note': 'Process killed after timeout'
}
except Exception as e:
self.logger.error(f"Error capturing process output: {e}")
return {'error': str(e), 'had_output': False, 'output_truncated': False}
def _handle_process_startup_error(self, error: Exception, start_time: float, cmd_args: list) -> dict:
"""Handle errors during process startup"""
error_msg = str(error)
self.logger.error(f"Process startup failed: {error_msg}")
if "terminated after" in error_msg:
init_wait = self.config.get('analysis', {}).get('process', {}).get('init_wait_time', 5)
return {
'status': 'early_termination',
'error': {
'message': f'Process terminated before initialization period ({init_wait}s)',
'details': error_msg,
'termination_time': error_msg.split('terminated after ')[1].split(' seconds')[0],
'cmd_args': cmd_args or []
},
'analysis_metadata': self._create_metadata(
start_time,
early_termination=True,
analysis_started=False,
cmd_args=cmd_args or []
)
}
else:
return self._create_error_result(start_time, error_msg, cmd_args)
def _create_error_result(self, start_time: float, error_msg: str, cmd_args: list = None) -> dict:
"""Create standardized error result"""
return {
'status': 'error',
'error': {
'message': 'Analysis failed',
'details': error_msg,
'cmd_args': cmd_args or []
},
'analysis_metadata': self._create_metadata(
start_time,
error=error_msg,
early_termination=False,
analysis_started=False,
cmd_args=cmd_args or []
)
}
def _validate_process(self, target, is_pid: bool, cmd_args: list = None) -> Tuple[subprocess.Popen, int]:
if is_pid:
return self._validate_existing_pid(target)
else:
return self._create_new_process(target, cmd_args)
def _validate_existing_pid(self, target: str) -> Tuple[psutil.Process, int]:
"""Validate existing PID"""
self.logger.debug(f"Validating PID: {target}")
try:
pid = int(target)
process = psutil.Process(pid)
if not process.is_running():
raise Exception(f"Process with PID {pid} is not running")
self.logger.debug(f"Successfully validated PID {pid}")
return process, pid
except (ValueError, psutil.NoSuchProcess) as e:
self.logger.error(f"Invalid or non-existent PID {target}: {e}")
raise Exception(f"Invalid or non-existent PID: {e}")
def _create_new_process(self, target: str, cmd_args: list) -> Tuple[subprocess.Popen, int]:
"""Create and validate new process"""
command = [target]
if cmd_args:
command.extend(cmd_args)
self.logger.debug(f"Starting new process: {command}")
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
try:
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
bufsize=1,
text=True,
)
pid = process.pid
self.logger.debug(f"Process started with PID: {pid}")
self._wait_for_process_initialization(process, pid, command)
return process, pid
except Exception as e:
raise Exception(f"Failed to start process: {str(e)}")
def _wait_for_process_initialization(self, process: subprocess.Popen, pid: int, command: list):
"""Wait for process to initialize and validate it's still running"""
try:
ps_process = psutil.Process(pid)
if not ps_process.is_running():
raise Exception(f"Process {pid} terminated immediately")
init_wait = self.config.get('analysis', {}).get('process', {}).get('init_wait_time', 5)
self.logger.debug(f"Waiting {init_wait} seconds for process initialization")
wait_interval = 0.1
elapsed = 0
while elapsed < init_wait:
time.sleep(wait_interval)
elapsed += wait_interval
if not ps_process.is_running():
cmd_str = ' '.join(command)
raise Exception(f"Process terminated after {elapsed:.1f} seconds (Command: {cmd_str})")
if not ps_process.is_running():
raise Exception(f"Process terminated during initialization")
except psutil.NoSuchProcess:
cmd_str = ' '.join(command)
raise Exception(f"Process {pid} terminated immediately after start (Command: {cmd_str})")
except Exception as e:
if process:
try:
process.kill()
except:
pass
raise e
def _cleanup_process(self, process, is_pid: bool):
if process and not is_pid:
self.logger.debug(f"Starting cleanup of process PID: {process.pid}")
try:
try:
parent = psutil.Process(process.pid)
if not parent.is_running():
self.logger.debug(f"Process {process.pid} has already terminated")
return
except psutil.NoSuchProcess:
self.logger.debug(f"Process {process.pid} no longer exists")
return
# Get and terminate children
try:
children = parent.children(recursive=True)
self.logger.debug(f"Found {len(children)} child processes to terminate")
for child in children:
try:
if child.is_running():
self.logger.debug(f"Terminating child process: {child.pid}")
child.terminate()
child.wait(timeout=3)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.TimeoutExpired):
try:
if child.is_running():
child.kill()
except psutil.NoSuchProcess:
pass
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
self.logger.error(f"Failed to get child processes: {e}")
# Terminate parent
try:
if parent.is_running():
self.logger.debug(f"Terminating parent process: {parent.pid}")
parent.terminate()
parent.wait(timeout=3)
if parent.is_running():
self.logger.debug(f"Force killing parent process: {parent.pid}")
parent.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.TimeoutExpired) as e:
self.logger.error(f"Failed to terminate parent process: {e}")
self.logger.debug("Process cleanup completed")
except Exception as e:
self.logger.error(f"Error during process cleanup: {str(e)}", exc_info=True)