Maximize RedEdr telemetry extraction

This commit is contained in:
BlackSnufkin
2026-04-29 03:40:59 -07:00
parent 78485740a0
commit 5759d76296
8 changed files with 902 additions and 124 deletions
+11
View File
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
- `/api/results/<target>/risk` endpoint and matching `grumpycat.get_risk_assessment()` client method
- `GrumpyCats/install_mcp.py` — installer for six MCP clients with auto-detected venv Python
- Command-line arguments input on the dynamic-analysis warning modal (pre-populated from last run)
- RedEdr now captures Microsoft-Windows-Kernel-File / -Network / -Audit-API-Calls / Antimalware-Engine ETW events; new tabs surface File Ops / Network / Audit API / Defender with Process Tree panel and ETW Provider Diagnostics
- Defender threat verdicts at runtime contribute +50 to the Detection Score (only verdicts; scan activity stays descriptive)
### Changed
- Backend split into Flask blueprints, services, and a `utils/` package; subprocess analyzers consolidated under `BaseSubprocessAnalyzer`
@@ -21,6 +23,11 @@ All notable changes to this project will be documented in this file.
- UI terminology reframed for operator-first reading: Detection Score, Triggering Indicators, Sensitive Imports, Signature triggered, Critical Imports, Payload Analysis
- Color palette softened across the app — severity tokens shifted -500 → -400, summary risk badges converted from solid bg to outlined chips, heavy rgba alphas tightened
- Analysis-type cards now show explicit `Run X Scan →` CTAs with stronger hover state
- RedEdr launch line is now `--etw --show --with-antimalwareengine --with-defendertrace --trace ...` (replaces broken `-e --trace` which RedEdr's cxxopts schema didn't recognize)
- Payload now fires as soon as RedEdr signals ETW-providers-attached (1-3s typical) instead of a fixed 15s sleep
- Module-load timeline deduplicates PEB-snapshot DLLs against ETW image_loads; kernel device paths stripped to basenames
- ETW timestamps shown as `HH:MM:SS.mmm` (FILETIME → local time) instead of raw 64-bit values
- Defender events split into threat / scan / internal categories; the noise table is collapsed by default with a verdict line summarizing what Defender did
### Fixed
- XSS hardening at user-data interpolation sites in results-page renderers
@@ -28,11 +35,15 @@ All notable changes to this project will be documented in this file.
- Per-tool render failures no longer suppress the rest of the rendering
- Office macro upload no longer throws on missing `macroDetectionNotes` element (upstream issue)
- `LitterBoxMCP.py` startup crash — broken import, removed `mcp.serve(...)` API, and stdout-corrupting logging all fixed
- RedEdr parser was reading PascalCase ETW field names (ProcessID, ImageName, ThreadID, etc.) but RedEdr lowercases all field names; Threads / Images / Child Processes / CPU Priority tabs now populate with real data instead of nulls
- Audit-API events now show `OpenProcess` / `OpenThread` (mapped from `etw_event_id`) instead of the placeholder task name `Info`
- RedEdr is now always cleaned up on dynamic-analysis failure paths (early termination, payload crash, analyzer exception); previously left orphaned subprocesses
### Removed
- Pre-redesign Tailwind utility chains and inline cyber-themed `<style>` blocks
- Tailwind CDN runtime dependency from `report.html`
- Dead code in `grumpycat.py` and `LitterBoxMCP.py` (cache, unused imports, exception envelope, lazy client wrapper)
- `etw_wait_time` config key (replaced by event-driven readiness signal)
### Notes
- No new dependencies; setup unchanged
+5 -2
View File
@@ -103,9 +103,12 @@ analysis:
rededr:
enabled: true
etw_wait_time: 15 # Time in seconds to wait for ETW setup
tool_path: ".\\Scanners\\RedEdr\\RedEdr.exe"
command: "{tool_path} -e --trace {process_name}"
# --etw: consume Kernel-Process / -File / -Network / -Audit-API ETW providers
# --show: emit JSON events to stdout (otherwise only the web/file output is on)
# --with-antimalwareengine: tap Microsoft-Antimalware-Engine ETW (Defender scan verdicts on our payload)
# --with-defendertrace: also track msmpeng.exe events touching the target process
command: "{tool_path} --etw --show --with-antimalwareengine --with-defendertrace --trace {process_name}"
timeout: 120
Binary file not shown.
+344 -84
View File
@@ -5,7 +5,81 @@ import logging
import traceback
from .base import DynamicAnalyzer
# Microsoft-Windows-Kernel-Audit-API-Calls — the task name in the JSON is just
# "Info" so we map etw_event_id → API name. RedEdr subscribes to ids {3,4,5,6}
# (RedEdr/etwreader.cpp:320).
_AUDIT_API_BY_ID = {
3: 'CreateSymbolicLink',
4: 'SetContextThread',
5: 'OpenProcess',
6: 'OpenThread',
}
# Microsoft-Antimalware-Engine emits "Behavior Monitoring Bm*" events.
# Three categories with different operator value:
# - Bm* "scan activity" (BmModuleLoad, BmNotificationHandleStart/Stop,
# BmOpenProcess) — Defender's behavior monitor actively engaging with
# our process. Count is signal: "Defender scanned the binary N times."
# - Bm* "internal state" (BmInternal, BmEtw) — Defender's own telemetry
# plumbing; not signal, hide by default.
# - Threat verdict events (ThreatFound, MalwareFound, etc.) or any event
# carrying a non-empty verdict field — actual detection.
_DEFENDER_INTERNAL_SUBSTRINGS = (
'bminternal',
'bmetw',
)
_DEFENDER_SCAN_SUBSTRINGS = (
'bmmoduleload',
'bmnotificationhandle',
'bmopenprocess',
)
# Substrings that indicate a real Defender detection (not just monitoring).
_DEFENDER_THREAT_SUBSTRINGS = (
'threatfound',
'threatdetect',
'detectionadded',
'malwarefound',
'protectionalert',
'detected',
)
def _classify_defender_event(event_name, verdict):
"""Return one of 'threat' / 'scan' / 'internal' / 'other'."""
if _is_defender_threat(event_name, verdict):
return 'threat'
lowered = (event_name or '').lower()
if any(s in lowered for s in _DEFENDER_INTERNAL_SUBSTRINGS):
return 'internal'
if any(s in lowered for s in _DEFENDER_SCAN_SUBSTRINGS):
return 'scan'
return 'other'
def _is_defender_threat(event_name, verdict):
if verdict and isinstance(verdict, str) and verdict.strip():
return True
if event_name:
lowered = event_name.lower()
if any(s in lowered for s in _DEFENDER_THREAT_SUBSTRINGS):
return True
return False
class RedEdrAnalyzer(DynamicAnalyzer):
# Readiness substring emitted by RedEdr.exe via loguru on stderr (which we
# merge into stdout) once all ETW providers are attached. See
# RedEdr/etwreader.cpp:431 — "ETW: All providers configured, ready to start
# collecting". Fires immediately before the threadReadynessEtw event is
# signaled, so when we observe this line ManagerStart has effectively
# returned and RedEdr is collecting events.
_READY_MARKER = 'All providers configured'
def __init__(self, config):
super().__init__(config)
self.tool_process = None
@@ -16,22 +90,58 @@ class RedEdrAnalyzer(DynamicAnalyzer):
self.logger = logging.getLogger(__name__)
self.output_thread = None
self._stop_reading = threading.Event()
self._ready_event = threading.Event()
def _reader_thread(self):
"""Thread to read RedEdr output without blocking"""
"""Thread to read RedEdr output without blocking.
Watches every line for the ETW-ready marker so callers can fire the
payload as soon as RedEdr is actually collecting. The same `_ready_event`
is also set on EOF (RedEdr exited) so `wait_for_ready()` never hangs
forever — callers distinguish real readiness from process-death by
checking `is_ready()`."""
try:
while not self._stop_reading.is_set():
line = self.tool_process.stdout.readline()
if not line:
break
break # EOF — process closed stdout (likely exited)
line = line.strip()
if line:
with self._output_lock:
self.collected_output.append(line)
if not self._ready_event.is_set() and self._READY_MARKER in line:
self._ready_event.set()
except Exception as e:
print(f"Error in reader thread: {e}")
finally:
# Unblock wait_for_ready() on any reader exit (EOF, exception,
# stop request) so callers never hang waiting on a dead process.
self._ready_event.set()
def wait_for_ready(self):
"""Block until RedEdr signals ETW-providers-attached, or until the
reader thread exits (process died / pipe closed / stop requested).
No timeout — RedEdr's normal startup is bounded by ETW provider
attachment (typically 1-3s) and any failure surfaces as a quick exit
which trips the EOF path in the reader thread.
Use `is_ready()` after returning to distinguish real readiness from
a dead-process unblock."""
self._ready_event.wait()
def is_ready(self):
"""True only if the readiness marker was actually seen on stdout."""
if not self._ready_event.is_set():
return False
# Distinguish real readiness from a dead-process unblock by checking
# the live process. If the subprocess exited before the marker fired,
# is_ready() must return False so callers can error out cleanly.
if self.tool_process is None:
return False
return self.tool_process.poll() is None
def start_tool(self, target_name):
"""Start the RedEdr tool in monitoring mode"""
@@ -75,8 +185,8 @@ class RedEdrAnalyzer(DynamicAnalyzer):
def _parse_output(self, output):
"""Parse RedEdr JSON output into structured data"""
findings = {
'events': [],
'process_info': {
'events': [],
'process_info': {
'commandline': None,
'image_path': None,
'working_dir': None,
@@ -85,13 +195,20 @@ class RedEdrAnalyzer(DynamicAnalyzer):
'is_protected_process': False,
'pid': None,
'start_time': None # Add start time field
},
'loaded_dlls': [],
'child_processes': [],
'threads': [],
'image_loads': [],
'image_unloads': [],
'cpu_priority_changes': []
},
'loaded_dlls': [],
'child_processes': [],
'threads': [],
'image_loads': [],
'image_unloads': [],
'cpu_priority_changes': [],
# Categories sourced from additional ETW providers RedEdr taps
# when launched with --etw / --with-antimalwareengine / --with-defendertrace.
# ETW field names are lowercased by RedEdr's KrabsEtwEventToJsonStr.
'file_operations': [], # Microsoft-Windows-Kernel-File
'network_activity': [], # Microsoft-Windows-Kernel-Network
'audit_api_calls': [], # Microsoft-Windows-Kernel-Audit-API-Calls
'defender_events': [], # Microsoft-Antimalware-Engine + msmpeng track
}
try:
@@ -158,53 +275,134 @@ class RedEdrAnalyzer(DynamicAnalyzer):
findings['loaded_dlls'].append(dlls)
# ETW Events
# Field names emitted by RedEdr are lowercased by
# KrabsEtwEventToJsonStr (RedEdrShared/etw_krabs.cpp:55).
# Standard fields (etw_pid, etw_tid, etw_time,
# etw_provider_name, etw_event_id, event, type, stack_trace)
# keep their casing; everything else is lowercased.
elif event_type == 'etw':
if event.get('ProcessID') and not findings['process_info']['pid']:
findings['process_info']['pid'] = event.get('ProcessID')
if event.get('etw_pid') and not findings['process_info']['pid']:
findings['process_info']['pid'] = event.get('etw_pid')
if event_name == 'ProcessStartStart':
findings['child_processes'].append({
'pid': event.get('ProcessID'),
'parent_pid': event.get('ParentProcessID'),
'image_name': event.get('ImageName'),
'create_time': event.get('CreateTime')
'pid': event.get('processid'),
'parent_pid': event.get('parentprocessid'),
'image_name': event.get('imagename'),
'create_time': event.get('createtime') or event.get('etw_time'),
})
elif event_name == 'ThreadStartStart':
findings['threads'].append({
'thread_id': event.get('ThreadID'),
'process_id': event.get('ProcessID'),
'start_addr': event.get('StartAddr'),
'stack_base': event.get('StackBase')
'thread_id': event.get('threadid'),
'process_id': event.get('processid'),
'start_addr': event.get('startaddr'),
'stack_base': event.get('stackbase'),
})
elif event_name == 'ImageLoadInfo':
findings['image_loads'].append({
'pid': event.get('ProcessID'),
'image_name': event.get('ImageName'),
'base': event.get('ImageBase'),
'size': event.get('ImageSize'),
'time_stamp': event.get('time'), # Use ETW time
'pid': event.get('processid'),
'image_name': event.get('imagename'),
'base': event.get('imagebase'),
'size': event.get('imagesize'),
'time_stamp': event.get('etw_time') or event.get('time'),
'stack_trace': event.get('stack_trace', []),
})
elif event_name == 'ImageUnloadInfo':
findings['image_unloads'].append({
'pid': event.get('ProcessID'),
'image_name': event.get('ImageName'),
'base': event.get('ImageBase'),
'size': event.get('ImageSize'),
'time_stamp': event.get('time'),
'pid': event.get('processid'),
'image_name': event.get('imagename'),
'base': event.get('imagebase'),
'size': event.get('imagesize'),
'time_stamp': event.get('etw_time') or event.get('time'),
'stack_trace': event.get('stack_trace', []),
})
elif event_name in ['CpuBasePriorityChangeInfo', 'CpuPriorityChangeInfo']:
findings['cpu_priority_changes'].append({
'pid': event.get('ProcessID'),
'thread_id': event.get('ThreadID'),
'old_priority': event.get('OldPriority'),
'new_priority': event.get('NewPriority'),
'time': event.get('time')
'pid': event.get('processid'),
'thread_id': event.get('threadid'),
'old_priority': event.get('oldpriority'),
'new_priority': event.get('newpriority'),
'time': event.get('etw_time') or event.get('time'),
})
# ETW provider-based dispatch for the categories the
# parser used to ignore. Routes by provider name; field
# names are lowercased by RedEdr — try a couple of
# likely keys per slot since the ETW manifest varies.
provider = event.get('etw_provider_name', '')
if provider == 'Microsoft-Windows-Kernel-File':
findings['file_operations'].append({
'path': event.get('filename') or event.get('filepath') or event.get('name'),
'operation': event_name,
'time': event.get('etw_time') or event.get('time'),
'thread_id': event.get('etw_tid'),
'pid': event.get('etw_pid'),
'stack_trace': event.get('stack_trace', []),
})
elif provider == 'Microsoft-Windows-Kernel-Network':
findings['network_activity'].append({
'proto': 'tcp' if 'tcp' in event_name.lower() else ('udp' if 'udp' in event_name.lower() else 'unknown'),
'operation': event_name,
'local_addr': event.get('saddr'),
'local_port': event.get('sport'),
'remote_addr': event.get('daddr'),
'remote_port': event.get('dport'),
'size': event.get('size'),
'time': event.get('etw_time') or event.get('time'),
'pid': event.get('etw_pid') or event.get('pid'),
'stack_trace': event.get('stack_trace', []),
})
elif provider == 'Microsoft-Windows-Kernel-Audit-API-Calls':
# The provider's task name is just "Info" — the
# actual API is identified by etw_event_id.
# See RedEdr/etwreader.cpp:320 (events 3,4,5,6).
api_name = _AUDIT_API_BY_ID.get(
event.get('etw_event_id'),
event_name or 'Unknown',
)
findings['audit_api_calls'].append({
'api': api_name,
'event_id': event.get('etw_event_id'),
'target_pid': event.get('targetprocessid'),
'target_tid': event.get('targetthreadid'),
'desired_access': event.get('desiredaccess'),
'return_code': event.get('returncode'),
'time': event.get('etw_time') or event.get('time'),
'caller_pid': event.get('etw_pid'),
'caller_tid': event.get('etw_tid'),
'stack_trace': event.get('stack_trace', []),
})
elif provider == 'Microsoft-Antimalware-Engine':
verdict = event.get('threatname') or event.get('threatid') or event.get('result')
category = _classify_defender_event(event_name, verdict)
findings['defender_events'].append({
'provider': 'antimalware_engine',
'event': event_name,
'event_id': event.get('etw_event_id'),
'scan_target': event.get('filename') or event.get('name') or event.get('path'),
'verdict': verdict,
'severity': event.get('severityid') or event.get('severity'),
'time': event.get('etw_time') or event.get('time'),
'category': category,
'is_threat': category == 'threat',
})
elif event.get('etw_process', '').lower() == 'msmpeng.exe':
# Captured via --with-defendertrace: msmpeng activity touching our payload.
verdict = event.get('threatname') or event.get('threatid') or event.get('result')
category = _classify_defender_event(event_name, verdict)
findings['defender_events'].append({
'provider': 'defender_trace',
'event': event_name,
'event_id': event.get('etw_event_id'),
'scan_target': event.get('filename') or event.get('name') or event.get('path'),
'verdict': verdict,
'time': event.get('etw_time') or event.get('time'),
'category': category,
'is_threat': category == 'threat',
})
# Store all valid JSON events
@@ -243,13 +441,21 @@ class RedEdrAnalyzer(DynamicAnalyzer):
'image_loads': [],
'image_unloads': [],
'cpu_priority_changes': [],
'file_operations': [],
'network_activity': [],
'audit_api_calls': [],
'defender_events': [],
'summary': {
'total_events': 0,
'total_dlls': 0,
'total_child_processes': 0,
'total_threads': 0,
'total_image_loads': 0,
'total_image_unloads': 0
'total_image_unloads': 0,
'total_file_operations': 0,
'total_network_activity': 0,
'total_audit_api_calls': 0,
'total_defender_events': 0,
}
}
@@ -280,18 +486,25 @@ class RedEdrAnalyzer(DynamicAnalyzer):
})
# Update lists with actual data if available
if 'loaded_dlls' in parsed_data:
findings['loaded_dlls'] = parsed_data['loaded_dlls']
if 'child_processes' in parsed_data:
findings['child_processes'] = parsed_data['child_processes']
if 'threads' in parsed_data:
findings['threads'] = parsed_data['threads']
if 'image_loads' in parsed_data:
findings['image_loads'] = parsed_data['image_loads']
if 'image_unloads' in parsed_data:
findings['image_unloads'] = parsed_data['image_unloads']
if 'cpu_priority_changes' in parsed_data:
findings['cpu_priority_changes'] = parsed_data['cpu_priority_changes']
for key in ('loaded_dlls', 'child_processes', 'threads',
'image_loads', 'image_unloads', 'cpu_priority_changes',
'file_operations', 'network_activity',
'audit_api_calls', 'defender_events'):
if key in parsed_data:
findings[key] = parsed_data[key]
# Per-provider event counts (diagnostic). Surfaces whether ETW
# actually delivered events from each provider RedEdr subscribes
# to. A 0 count for Microsoft-Windows-Kernel-Network when the
# payload made TCP connections almost always means the events
# fired but were attributed to System(4) / svchost rather than
# the payload PID — which RedEdr's event_callback_process drops.
# Reliable network capture would require RedEdr's --hook mode
# (kernel driver + DLL injection).
events_by_provider = {}
for ev in parsed_data.get('events', []):
provider = ev.get('etw_provider_name') or ev.get('type') or 'unknown'
events_by_provider[provider] = events_by_provider.get(provider, 0) + 1
# Update summary
findings['summary'] = {
@@ -300,7 +513,12 @@ class RedEdrAnalyzer(DynamicAnalyzer):
'total_child_processes': len(findings['child_processes']),
'total_threads': len(findings['threads']),
'total_image_loads': len(findings['image_loads']),
'total_image_unloads': len(findings['image_unloads'])
'total_image_unloads': len(findings['image_unloads']),
'total_file_operations': len(findings['file_operations']),
'total_network_activity': len(findings['network_activity']),
'total_audit_api_calls': len(findings['audit_api_calls']),
'total_defender_events': len(findings['defender_events']),
'events_by_provider': events_by_provider,
}
findings['timeline'] = self._generate_timeline(findings)
@@ -323,13 +541,31 @@ class RedEdrAnalyzer(DynamicAnalyzer):
'findings': default_findings
}
@staticmethod
def _module_basename(path):
"""Strip kernel device prefixes / directories down to a basename.
e.g. '\\Device\\HarddiskVolume3\\Windows\\System32\\cryptsp.dll' -> 'cryptsp.dll'."""
if not path:
return ''
return path.replace('/', '\\').rsplit('\\', 1)[-1]
def _generate_timeline(self, parsed_data):
"""
Generate a chronological timeline of significant events.
Each event must have a timestamp in a consistent format.
Module loads are de-duplicated across two sources that report the same
thing differently:
- ETW Microsoft-Windows-Kernel-Process ImageLoad events (one per
actual load, with real per-event timestamps and full kernel paths)
- The PEB-walk snapshot RedEdr emits when it first augments the
target (one event with all currently-loaded modules)
We trust ETW first (better timing) and only fall back to PEB entries
whose basename ETW didn't see. That covers the rare case where RedEdr
attaches AFTER the target has already loaded some modules.
"""
timeline = []
# Add process start if available
if parsed_data.get('process_info', {}).get('pid'):
timeline.append({
@@ -337,7 +573,7 @@ class RedEdrAnalyzer(DynamicAnalyzer):
'type': 'Process Start',
'details': f"Process started: PID {parsed_data['process_info']['pid']}"
})
# Add child process creations
for child in parsed_data.get('child_processes', []):
timeline.append({
@@ -346,20 +582,31 @@ class RedEdrAnalyzer(DynamicAnalyzer):
'details': f"Created child process: {child.get('image_name', 'Unknown')} (PID: {child.get('pid', 'Unknown')})"
})
# Add DLL loads
for dll in parsed_data.get('loaded_dlls', []):
timeline.append({
'time': dll.get('time', None),
'type': 'DLL Load',
'details': f"Loaded DLL: {dll.get('name', 'Unknown')}"
})
# Add image loads
# ETW image loads — primary source for module-load timeline entries.
seen_basenames = set()
for img in parsed_data.get('image_loads', []):
raw = img.get('image_name') or 'Unknown'
basename = self._module_basename(raw) or raw
seen_basenames.add(basename.lower())
timeline.append({
'time': img.get('time_stamp', None),
'type': 'Image Load',
'details': f"Loaded image: {img.get('image_name', 'Unknown')}"
'details': f"Loaded image: {basename}",
})
# PEB snapshot DLLs — only add ones ETW didn't already see, since the
# snapshot is largely redundant with ETW for any process that started
# under RedEdr's watch.
for dll in parsed_data.get('loaded_dlls', []):
name = dll.get('name') or ''
basename = self._module_basename(name) or name
if basename.lower() in seen_basenames:
continue
seen_basenames.add(basename.lower())
timeline.append({
'time': dll.get('time', None),
'type': 'DLL Load',
'details': f"Loaded DLL: {basename} (initial)",
})
# Sort timeline by timestamp if available, otherwise keep original order
@@ -376,23 +623,36 @@ class RedEdrAnalyzer(DynamicAnalyzer):
return sorted_timeline
def cleanup(self):
"""Stop the RedEdr process if it's still running"""
"""Stop the RedEdr process if it's still running.
Idempotent — safe to call multiple times. Manager calls this in a
try/finally so it can fire after the happy-path cleanup or after a
crashed payload's exception path."""
# Signal reader thread to stop
self._stop_reading.set()
if self.tool_process:
if self.tool_process is None:
return
proc = self.tool_process
self.tool_process = None # Mark cleaned up before any I/O — second
# call sees None and returns immediately.
try:
proc.terminate()
proc.wait(timeout=5)
# Wait for reader thread to finish
if self.output_thread and self.output_thread.is_alive():
self.output_thread.join(timeout=2)
except subprocess.TimeoutExpired:
proc.kill()
except Exception as e:
self.logger.warning(f"RedEdr cleanup: error while stopping subprocess: {e}")
finally:
try:
self.tool_process.terminate()
self.tool_process.wait(timeout=5)
# Wait for reader thread to finish
if self.output_thread and self.output_thread.is_alive():
self.output_thread.join(timeout=2)
except subprocess.TimeoutExpired:
self.tool_process.kill()
finally:
if self.tool_process.stdout:
self.tool_process.stdout.close()
if self.tool_process.stderr:
self.tool_process.stderr.close()
if proc.stdout:
proc.stdout.close()
if proc.stderr:
proc.stderr.close()
except Exception:
pass
+79 -25
View File
@@ -169,66 +169,120 @@ class AnalysisManager:
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"""
"""Handle file-based analysis with RedEdr integration.
RedEdr cleanup is in `finally` so an orphaned RedEdr can never outlive
a crashed/early-terminated payload. Whatever telemetry RedEdr managed
to collect is also attached to the response on failure paths, so the
user sees partial events instead of nothing.
"""
results = {}
process = None
rededr = None
response = 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)
response = self._handle_process_startup_error(e, start_time, cmd_args)
return response
# 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
# 5. Get RedEdr results cleanup is unconditional, in finally.
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,
start_time,
early_termination=False,
analysis_started=True,
cmd_args=cmd_args or []
)
return results
response = results
return response
except Exception as e:
return self._create_error_result(start_time, str(e), cmd_args)
response = self._create_error_result(start_time, str(e), cmd_args)
return response
finally:
# Always tear down RedEdr — even on early return / exception —
# so a crashed payload never leaves an orphaned RedEdr process.
# Cleanup is idempotent, so calling it after the happy-path
# get_results() is safe.
if rededr is not None:
# Attach partial RedEdr telemetry to the response on failure
# paths (early termination, generic exception). The happy
# path already populated results['rededr'] in step 5.
if isinstance(response, dict) and 'rededr' not in response:
try:
response['rededr'] = rededr.get_results()
except Exception as e:
self.logger.error(f"Failed to capture partial RedEdr telemetry: {e}")
try:
self._cleanup_rededr(rededr)
except Exception as e:
self.logger.error(f"Error during RedEdr cleanup: {e}")
def _initialize_rededr(self, target: str, results: dict):
"""Initialize RedEdr if enabled"""
"""Initialize RedEdr if enabled.
Blocks until RedEdr logs that all ETW providers are attached
(typically 1-3s). No timeout — failure surfaces as a quick subprocess
exit, which the reader thread also unblocks on. Callers that want a
hard deadline get it from the surrounding analysis-pipeline timeout.
"""
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:
if not rededr.start_tool(target_name):
self.logger.error("Failed to start RedEdr")
results['rededr'] = {'status': 'error', 'error': 'Failed to start tool'}
return None
ready_start = time.monotonic()
rededr.wait_for_ready()
elapsed = time.monotonic() - ready_start
if rededr.is_ready():
self.logger.debug(
f"RedEdr ready in {elapsed:.2f}s (ETW providers attached)"
)
return rededr
# Reader thread unblocked because RedEdr exited before signaling
# readiness. Capture whatever output we collected for diagnostics.
self.logger.error(
f"RedEdr exited after {elapsed:.2f}s without signaling readiness"
)
try:
rededr.cleanup()
except Exception:
pass
tail = '\n'.join(rededr.collected_output[-20:]) if rededr.collected_output else ''
results['rededr'] = {
'status': 'error',
'error': 'RedEdr exited before ETW providers attached',
'last_output': tail,
}
return None
except Exception as e:
self.logger.error(f"Error initializing RedEdr: {e}")
results['rededr'] = {'status': 'error', 'error': str(e)}
+318 -11
View File
@@ -2,6 +2,45 @@
import { errorPanel, cleanState, threatState, statRow, panel, kvGrid, tag, escapeHtml } from './_shared.js';
import { formatBytes } from '../renderers.js';
// Windows FILETIME → ISO-ish local time. RedEdr emits
// record.EventHeader.TimeStamp.QuadPart which is 100-ns intervals since
// 1601-01-01 UTC. Unix-epoch (1970) is 116444736000000000 of those.
// Values are too large for JS Number (1.34e17 > 9.0e15 MAX_SAFE_INTEGER),
// so use BigInt for the math.
const FILETIME_UNIX_OFFSET_100NS = 116444736000000000n;
function formatEtwTime(value) {
if (value === null || value === undefined || value === '' || value === 'N/A') {
return 'N/A';
}
let big;
try {
big = typeof value === 'bigint' ? value : BigInt(value);
} catch {
return String(value);
}
// Heuristic: FILETIME for the current era is ~1.3e17. Anything below
// 1e15 is more likely unix-epoch milliseconds (RedEdr's get_time()).
let unixMs;
if (big > 1000000000000000n) {
unixMs = Number((big - FILETIME_UNIX_OFFSET_100NS) / 10000n);
} else if (big > 1000000000000n) {
unixMs = Number(big); // already unix ms
} else if (big > 1000000000n) {
unixMs = Number(big) * 1000; // unix seconds
} else {
return String(value);
}
if (!Number.isFinite(unixMs) || unixMs <= 0) return String(value);
const d = new Date(unixMs);
if (Number.isNaN(d.getTime())) return String(value);
const hh = String(d.getHours()).padStart(2, '0');
const mm = String(d.getMinutes()).padStart(2, '0');
const ss = String(d.getSeconds()).padStart(2, '0');
const ms = String(d.getMilliseconds()).padStart(3, '0');
return `${hh}:${mm}:${ss}.${ms}`;
}
export default {
id: 'rededr',
elementId: 'redEdrResults',
@@ -37,12 +76,26 @@ export default {
const cpuChanges = findings.cpu_priority_changes || [];
const timeline = findings.timeline || [];
const summary = findings.summary || {};
const childProcesses = findings.child_processes || [];
const fileOps = findings.file_operations || [];
const networkActivity = findings.network_activity || [];
const auditApi = findings.audit_api_calls || [];
const defenderEvents = findings.defender_events || [];
const defenderThreats = defenderEvents.filter(e => e.category === 'threat');
const defenderScans = defenderEvents.filter(e => e.category === 'scan');
const defenderInternal = defenderEvents.filter(e => e.category === 'internal');
const defenderOther = defenderEvents.filter(e => e.category === 'other' || !e.category);
const hasThreatVerdict = defenderThreats.length > 0;
ctx.statsElement.innerHTML = statRow([
{ label: 'Total Events', value: summary.total_events || 0, severity: 'info' },
{ label: 'DLLs Loaded', value: summary.total_dlls || 0, severity: 'info' },
{ label: 'Child Processes', value: summary.total_child_processes || 0, severity: 'info' },
{ label: 'Active Threads', value: summary.total_threads || 0, severity: 'info' },
{ label: 'Total Events', value: summary.total_events || 0, severity: 'info' },
{ label: 'DLLs Loaded', value: summary.total_dlls || 0, severity: 'info' },
{ label: 'Child Processes', value: summary.total_child_processes || 0, severity: 'info' },
{ label: 'Threads', value: summary.total_threads || 0, severity: 'info' },
{ label: 'Network', value: summary.total_network_activity || 0, severity: networkActivity.length > 0 ? 'medium' : 'info' },
{ label: 'File Ops', value: summary.total_file_operations || 0, severity: 'info' },
{ label: 'Audit API', value: summary.total_audit_api_calls || 0, severity: auditApi.length > 0 ? 'medium' : 'info' },
{ label: 'Defender', value: summary.total_defender_events || 0, severity: hasThreatVerdict ? 'critical' : (defenderScans.length > 0 ? 'medium' : 'info') },
]);
let html = '';
@@ -69,13 +122,117 @@ export default {
</div>
`;
// Process Tree (parent + spawned children)
if (childProcesses.length) {
const escapedRoot = escapeHtml(proc.image_path?.split('\\').pop() || proc.commandline || 'Target');
html += panel('Process Tree', `
<div style="display: flex; flex-direction: column; gap: 4px; font-size: 12px;">
<div>
<span class="lb-mono lb-strong">${escapedRoot}</span>
<span class="lb-muted" style="margin-left: 8px;">PID ${escapeHtml(String(proc.pid ?? '?'))}</span>
</div>
<ul style="list-style: none; margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 3px;">
${childProcesses.map(c => `
<li style="font-size: 11px;">
<span class="lb-muted">└─</span>
<span class="lb-mono lb-strong" style="margin-left: 4px;">${escapeHtml((c.image_name || '').split('\\').pop() || 'Unknown')}</span>
<span class="lb-muted" style="margin-left: 6px;">PID ${escapeHtml(String(c.pid ?? '?'))}</span>
${c.parent_pid ? `<span class="lb-muted" style="margin-left: 6px;">parent ${escapeHtml(String(c.parent_pid))}</span>` : ''}
</li>
`).join('')}
</ul>
</div>
`, `${childProcesses.length} child${childProcesses.length === 1 ? '' : 'ren'}`);
}
// Defender events — verdict line up top, then a table only when there's
// actually something to show. Three buckets:
// threat = real detection (the loud signal)
// scan = Defender behavior monitor engaged with our process
// (BmModuleLoad / BmNotificationHandle* / BmOpenProcess)
// internal = Defender's own state plumbing (BmInternal / BmEtw)
// For most operator-flavored runs the answer is: lots of scans, no
// threats — meaning Defender looked at the binary and didn't flag it.
if (defenderEvents.length) {
const renderDefenderRows = (events) => events.map(e => `
<tr ${e.is_threat ? 'style="background: rgba(248, 113, 113, 0.04);"' : ''}>
<td class="lb-mono">${escapeHtml(e.provider || '')}</td>
<td class="lb-mono">${escapeHtml(e.event || '')}</td>
<td class="lb-mono lb-muted">${escapeHtml(e.scan_target || '—')}</td>
<td>${e.verdict ? tag(e.is_threat ? 'critical' : 'medium', String(e.verdict)) : '<span class="lb-muted">—</span>'}</td>
<td class="lb-mono lb-muted">${escapeHtml(formatEtwTime(e.time))}</td>
</tr>
`).join('');
// Headline verdict — the operator's bottom-line answer.
let verdictLine, verdictColor, headerBadge;
if (hasThreatVerdict) {
verdictLine = `Defender flagged the binary — ${defenderThreats.length} threat verdict${defenderThreats.length === 1 ? '' : 's'}.`;
verdictColor = 'var(--lb-accent-soft)';
headerBadge = `${defenderThreats.length} threat${defenderThreats.length === 1 ? '' : 's'}`;
} else if (defenderScans.length > 0) {
verdictLine = `Defender scanned the binary ${defenderScans.length} time${defenderScans.length === 1 ? '' : 's'} — no threat verdict.`;
verdictColor = 'var(--lb-sev-low)';
headerBadge = `${defenderScans.length} scan${defenderScans.length === 1 ? '' : 's'}, no verdict`;
} else {
verdictLine = `Defender did not actively scan the binary (${defenderInternal.length + defenderOther.length} internal event${(defenderInternal.length + defenderOther.length) === 1 ? '' : 's'} only).`;
verdictColor = 'var(--lb-text-dim)';
headerBadge = `${defenderEvents.length} internal`;
}
const breakdownLine = `
<div class="lb-muted" style="font-size: 11px; margin-top: 4px;">
Threats ${defenderThreats.length} · Scan activity ${defenderScans.length} · Internal ${defenderInternal.length}${defenderOther.length > 0 ? ' · Other ' + defenderOther.length : ''}
</div>`;
const verdictBlock = `
<div style="margin-bottom: 12px; padding: 10px 12px; border-left: 2px solid ${verdictColor}; background: var(--lb-bg);">
<div class="lb-strong" style="color: ${verdictColor}; font-size: 12px;">${escapeHtml(verdictLine)}</div>
${breakdownLine}
</div>`;
// Show actual rows only when there's something interesting (threat
// verdicts, or scan events with details). Internal Bm* state is
// stashed behind a toggle.
const interestingEvents = [...defenderThreats, ...defenderScans];
const interestingTable = interestingEvents.length > 0
? `<table class="lb-table">
<thead><tr><th>Provider</th><th>Event</th><th>Scan Target</th><th>Verdict</th><th>Time</th></tr></thead>
<tbody>${renderDefenderRows(interestingEvents.slice(0, 50))}</tbody>
</table>
${interestingEvents.length > 50 ? `<div class="lb-muted" style="font-size: 11px; padding: 6px 0; font-style: italic;">… and ${interestingEvents.length - 50} more</div>` : ''}`
: '';
const internalEvents = [...defenderInternal, ...defenderOther];
const internalToggle = internalEvents.length > 0
? `<div style="margin-top: 12px; border-top: 1px dashed var(--lb-border); padding-top: 10px;">
<span id="defenderNoiseToggle" style="cursor: pointer; text-decoration: underline; color: var(--lb-text-dim); font-size: 11px;">
Show ${internalEvents.length} internal Defender event${internalEvents.length === 1 ? '' : 's'} (Bm* state plumbing)
</span>
<div id="defenderNoiseTable" class="hidden" style="margin-top: 8px;">
<table class="lb-table">
<thead><tr><th>Provider</th><th>Event</th><th>Scan Target</th><th>Verdict</th><th>Time</th></tr></thead>
<tbody>${renderDefenderRows(internalEvents.slice(0, 50))}</tbody>
</table>
${internalEvents.length > 50 ? `<div class="lb-muted" style="font-size: 11px; padding: 6px 0; font-style: italic;">… and ${internalEvents.length - 50} more</div>` : ''}
</div>
</div>`
: '';
html += panel(
hasThreatVerdict ? 'Defender — Threat Verdicts' : 'Defender',
verdictBlock + interestingTable + internalToggle,
headerBadge
);
}
// Timeline
if (timeline.length) {
html += panel('Event Timeline', `
<div style="display: flex; flex-direction: column;">
${timeline.map(event => `
<div style="display: grid; grid-template-columns: 110px 130px 1fr; gap: 12px; padding: 6px 0; border-bottom: 1px dashed var(--lb-border); font-size: 11px;">
<span class="lb-mono lb-muted">${escapeHtml(event.time || 'N/A')}</span>
<span class="lb-mono lb-muted">${escapeHtml(formatEtwTime(event.time))}</span>
<span class="lb-strong">${escapeHtml(event.type || '')}</span>
<span class="lb-dim">${escapeHtml(event.details || '')}</span>
</div>
@@ -95,7 +252,7 @@ export default {
<td class="lb-mono">${escapeHtml(String(c.thread_id))}</td>
<td class="lb-mono">${escapeHtml(String(c.old_priority))}</td>
<td class="lb-mono">${escapeHtml(String(c.new_priority))}</td>
<td class="lb-mono lb-muted">${escapeHtml(c.time || 'N/A')}</td>
<td class="lb-mono lb-muted">${escapeHtml(formatEtwTime(c.time))}</td>
</tr>
`).join('')}
</tbody>
@@ -103,13 +260,77 @@ export default {
`, String(cpuChanges.length));
}
// DLLs / Images / Threads sub-tabs
// ETW Provider Diagnostics — collapsible, surfaces per-provider event
// counts so it's obvious whether a 0 in (e.g.) network_activity means
// "ETW didn't deliver any Kernel-Network events" or "events arrived
// but my parser missed them". User-mode ETW often attributes outbound
// TCP to System(4) or svchost, which RedEdr's filter drops — that
// would show as Microsoft-Windows-Kernel-Network: 0 here even when
// the payload made real network calls. Reliable capture needs
// RedEdr's --hook mode (kernel driver path).
const providerCounts = summary.events_by_provider || {};
const providerEntries = Object.entries(providerCounts).sort((a, b) => b[1] - a[1]);
if (providerEntries.length > 0) {
const knownProviders = new Set([
'Microsoft-Windows-Kernel-Process',
'Microsoft-Windows-Kernel-File',
'Microsoft-Windows-Kernel-Network',
'Microsoft-Windows-Kernel-Audit-API-Calls',
'Microsoft-Antimalware-Engine',
]);
const missingProviders = [...knownProviders].filter(p => !(p in providerCounts));
html += `
<div class="lb-panel">
<div class="lb-panel-hdr">
<span class="lb-glyph">▸</span>ETW Provider Diagnostics
<span class="lb-panel-badge">${providerEntries.length} provider${providerEntries.length === 1 ? '' : 's'}</span>
<button id="rededrDiagToggle" class="lb-btn lb-btn-ghost" style="margin-left: auto; padding: 2px 10px; font-size: 11px;">Show</button>
</div>
<div id="rededrDiagBody" class="lb-panel-body hidden">
<p class="lb-muted" style="font-size: 11px; margin-bottom: 10px;">
Per-provider event counts. A subscribed provider with <code>0</code>
events usually means ETW delivered events but RedEdr filtered them
out (e.g. Kernel-Network often attributes outbound TCP to System
or svchost, not the payload PID). Reliable capture for those
categories requires RedEdr's <code>--hook</code> kernel-driver path.
</p>
<table class="lb-table">
<thead><tr><th>Provider</th><th>Events delivered</th></tr></thead>
<tbody>
${providerEntries.map(([prov, count]) => `
<tr>
<td class="lb-mono">${escapeHtml(prov)}</td>
<td class="lb-mono">${count}</td>
</tr>
`).join('')}
${missingProviders.map(prov => `
<tr>
<td class="lb-mono lb-muted">${escapeHtml(prov)}</td>
<td class="lb-mono lb-muted">0 (no events received)</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>`;
}
// DLLs / Images / Threads / Network / File Ops / Audit API sub-tabs
const tabBtn = (name, label, count, active) => {
const baseStyle = 'padding: 8px 14px; background: transparent; border: 0; font-family: inherit; font-size: 12px; cursor: pointer;';
const activeStyle = 'color: var(--lb-text); border-bottom: 2px solid var(--lb-accent-soft);';
const inactiveStyle = 'color: var(--lb-text-dim); border-bottom: 2px solid transparent;';
return `<button onclick="switchInnerTab('${name}')" class="tab-button${active ? ' active' : ''}" style="${baseStyle} ${active ? activeStyle : inactiveStyle}">${label} (${count})</button>`;
};
html += `
<div class="lb-panel">
<div style="display: flex; border-bottom: 1px solid var(--lb-border);">
<button onclick="switchInnerTab('dlls')" class="tab-button active" style="padding: 8px 14px; background: transparent; color: var(--lb-text); border: 0; border-bottom: 2px solid var(--lb-accent-soft); font-family: inherit; font-size: 12px; cursor: pointer;">DLLs (${loadedDlls.length})</button>
<button onclick="switchInnerTab('images')" class="tab-button" style="padding: 8px 14px; background: transparent; color: var(--lb-text-dim); border: 0; border-bottom: 2px solid transparent; font-family: inherit; font-size: 12px; cursor: pointer;">Images (${imageLoads.length + imageUnloads.length})</button>
<button onclick="switchInnerTab('threads')" class="tab-button" style="padding: 8px 14px; background: transparent; color: var(--lb-text-dim); border: 0; border-bottom: 2px solid transparent; font-family: inherit; font-size: 12px; cursor: pointer;">Threads (${threads.length})</button>
<div style="display: flex; border-bottom: 1px solid var(--lb-border); flex-wrap: wrap;">
${tabBtn('dlls', 'DLLs', loadedDlls.length, true)}
${tabBtn('images', 'Images', imageLoads.length + imageUnloads.length, false)}
${tabBtn('threads', 'Threads', threads.length, false)}
${tabBtn('network', 'Network', networkActivity.length, false)}
${tabBtn('fileops', 'File Ops', fileOps.length, false)}
${tabBtn('auditapi', 'Audit API', auditApi.length, false)}
</div>
<div id="dlls-tab" class="tab-content">
@@ -183,6 +404,69 @@ export default {
</table>
</div>
</div>
<div id="network-tab" class="tab-content hidden">
<div class="lb-panel-body">
${networkActivity.length === 0
? '<div class="lb-muted" style="padding: 8px 0; font-size: 11px;">No network activity observed.</div>'
: `<table class="lb-table">
<thead><tr><th>Proto</th><th>Local</th><th>Remote</th><th>Op</th><th>Size</th><th>Time</th></tr></thead>
<tbody>
${networkActivity.map(n => `
<tr>
<td class="lb-mono">${escapeHtml(n.proto || '?')}</td>
<td class="lb-mono lb-muted">${escapeHtml(String(n.local_addr || '—'))}${n.local_port ? ':' + escapeHtml(String(n.local_port)) : ''}</td>
<td class="lb-mono">${escapeHtml(String(n.remote_addr || '—'))}${n.remote_port ? ':' + escapeHtml(String(n.remote_port)) : ''}</td>
<td class="lb-mono lb-muted">${escapeHtml(n.operation || '—')}</td>
<td class="lb-mono lb-muted">${n.size != null ? formatBytes(Number(n.size) || 0) : '—'}</td>
<td class="lb-mono lb-muted">${escapeHtml(formatEtwTime(n.time))}</td>
</tr>
`).join('')}
</tbody>
</table>`}
</div>
</div>
<div id="fileops-tab" class="tab-content hidden">
<div class="lb-panel-body">
${fileOps.length === 0
? '<div class="lb-muted" style="padding: 8px 0; font-size: 11px;">No file operations observed.</div>'
: `<table class="lb-table">
<thead><tr><th>Path</th><th>Operation</th><th>Thread</th><th>Time</th></tr></thead>
<tbody>
${fileOps.map(f => `
<tr>
<td class="lb-mono" style="word-break: break-all;">${escapeHtml(String(f.path || '—'))}</td>
<td class="lb-mono lb-muted">${escapeHtml(f.operation || '—')}</td>
<td class="lb-mono lb-muted">${escapeHtml(String(f.thread_id ?? '—'))}</td>
<td class="lb-mono lb-muted">${escapeHtml(formatEtwTime(f.time))}</td>
</tr>
`).join('')}
</tbody>
</table>`}
</div>
</div>
<div id="auditapi-tab" class="tab-content hidden">
<div class="lb-panel-body">
${auditApi.length === 0
? '<div class="lb-muted" style="padding: 8px 0; font-size: 11px;">No audit-API calls observed.</div>'
: `<table class="lb-table">
<thead><tr><th>API</th><th>Target PID</th><th>Target TID</th><th>Caller PID/TID</th><th>Time</th></tr></thead>
<tbody>
${auditApi.map(a => `
<tr>
<td class="lb-mono">${escapeHtml(a.api || '—')}</td>
<td class="lb-mono">${escapeHtml(String(a.target_pid ?? '—'))}</td>
<td class="lb-mono lb-muted">${escapeHtml(String(a.target_tid ?? '—'))}</td>
<td class="lb-mono lb-muted">${escapeHtml(String(a.caller_pid ?? '—'))} / ${escapeHtml(String(a.caller_tid ?? '—'))}</td>
<td class="lb-mono lb-muted">${escapeHtml(formatEtwTime(a.time))}</td>
</tr>
`).join('')}
</tbody>
</table>`}
</div>
</div>
</div>
`;
@@ -218,5 +502,28 @@ export default {
else { loads.classList.add('hidden'); unloads.classList.remove('hidden'); }
};
}
// Defender internal-events toggle — show/hide Bm* state plumbing.
const noiseToggle = document.getElementById('defenderNoiseToggle');
const noiseTable = document.getElementById('defenderNoiseTable');
if (noiseToggle && noiseTable) {
const internalCount = defenderInternal.length + defenderOther.length;
noiseToggle.addEventListener('click', () => {
const hidden = noiseTable.classList.toggle('hidden');
noiseToggle.textContent = hidden
? `Show ${internalCount} internal Defender event${internalCount === 1 ? '' : 's'} (Bm* state plumbing)`
: `Hide ${internalCount} internal Defender event${internalCount === 1 ? '' : 's'}`;
});
}
// ETW provider diagnostic toggle.
const diagToggle = document.getElementById('rededrDiagToggle');
const diagBody = document.getElementById('rededrDiagBody');
if (diagToggle && diagBody) {
diagToggle.addEventListener('click', () => {
const hidden = diagBody.classList.toggle('hidden');
diagToggle.textContent = hidden ? 'Show' : 'Hide';
});
}
},
};
+112 -2
View File
@@ -1149,21 +1149,131 @@
{# RedEdr #}
{% set re_ = dynamic_results.rededr %}
{% set re_findings = re_.findings if re_ and re_.findings else None %}
{% set re_summary = re_findings.summary if re_findings else None %}
{% set re_proc = re_findings.process_info if re_findings else None %}
{% set re_children = re_findings.child_processes if re_findings else [] %}
{% set re_defender = re_findings.defender_events if re_findings else [] %}
{% set re_net = re_findings.network_activity if re_findings else [] %}
{% set re_files = re_findings.file_operations if re_findings else [] %}
{% set re_audit = re_findings.audit_api_calls if re_findings else [] %}
{% set threat_signals = ['threatfound','threatdetect','detectionadded','malwarefound','protectionalert','detected'] %}
{% set ns = namespace(threat_hit=false) %}
{% for d in re_defender %}{% if d.verdict or d.event %}{% set evt = (d.event or '')|lower %}{% for s in threat_signals %}{% if s in evt %}{% set ns.threat_hit = true %}{% endif %}{% endfor %}{% if d.verdict %}{% set ns.threat_hit = true %}{% endif %}{% endif %}{% endfor %}
<div class="scanner">
<div class="scanner-name">RedEdr</div>
<div class="scanner-body">
{% set re_summary = re_.findings.summary if re_ and re_.findings and re_.findings.summary else None %}
<div class="scanner-headline">
{% if ns.threat_hit %}
<span class="threat-state">
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 1l7 14H1L8 1zm0 5v4m0 2v.5"/></svg>
Defender flagged the binary at runtime
</span>
{% endif %}
<span class="scanner-meta">
{% if re_summary %}
{{ re_summary.total_events or 0 }} events ·
{{ re_summary.total_dlls or 0 }} DLL loads ·
{{ re_summary.total_image_loads or 0 }} image loads
{{ re_summary.total_image_loads or 0 }} image loads ·
{{ re_summary.total_network_activity or 0 }} net ·
{{ re_summary.total_file_operations or 0 }} files ·
{{ re_summary.total_audit_api_calls or 0 }} audit-API
{% else %}
No telemetry recorded
{% endif %}
</span>
</div>
{% if re_defender %}
<h4 class="subsection-title mt-12">Defender Events ({{ re_defender|length }})</h4>
<table class="report-table">
<thead><tr><th>Provider</th><th>Event</th><th>Scan Target</th><th>Verdict</th></tr></thead>
<tbody>
{% for d in re_defender[:15] %}
{% set evt = (d.event or '')|lower %}
{% set is_threat = (d.verdict and (d.verdict|string)|trim) %}
{% if not is_threat %}{% for s in threat_signals %}{% if s in evt %}{% set is_threat = true %}{% endif %}{% endfor %}{% endif %}
<tr>
<td class="mono">{{ d.provider or '—' }}</td>
<td class="mono">{{ d.event or '—' }}</td>
<td class="mono muted">{{ (d.scan_target or '—')|truncate(80, true) }}</td>
<td>{% if d.verdict %}<span class="pill {% if is_threat %}critical{% else %}medium{% endif %}">{{ d.verdict }}</span>{% else %}<span class="muted"></span>{% endif %}</td>
</tr>
{% endfor %}
{% if re_defender|length > 15 %}
<tr><td colspan="4" class="center">… and {{ re_defender|length - 15 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% endif %}
{% if re_proc and (re_proc.pid or re_children) %}
<h4 class="subsection-title mt-12">Process Tree</h4>
<pre class="code">{{ (re_proc.image_path or re_proc.commandline or 'Target')|trim }} (PID {{ re_proc.pid or '?' }})
{% for c in re_children %} └─ {{ c.image_name or 'Unknown' }} (PID {{ c.pid or '?' }}{% if c.parent_pid %}, parent {{ c.parent_pid }}{% endif %})
{% endfor %}</pre>
{% endif %}
{% if re_net %}
<h4 class="subsection-title mt-12">Network Activity ({{ re_net|length }})</h4>
<table class="report-table">
<thead><tr><th>Proto</th><th>Local</th><th>Remote</th><th>Op</th><th>Size</th></tr></thead>
<tbody>
{% for n in re_net[:25] %}
<tr>
<td class="mono">{{ n.proto or '?' }}</td>
<td class="mono muted">{{ n.local_addr or '—' }}{% if n.local_port %}:{{ n.local_port }}{% endif %}</td>
<td class="mono">{{ n.remote_addr or '—' }}{% if n.remote_port %}:{{ n.remote_port }}{% endif %}</td>
<td class="mono muted">{{ n.operation or '—' }}</td>
<td class="mono muted">{{ n.size if n.size is not none else '—' }}</td>
</tr>
{% endfor %}
{% if re_net|length > 25 %}
<tr><td colspan="5" class="center">… and {{ re_net|length - 25 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% endif %}
{% if re_files %}
<h4 class="subsection-title mt-12">File Operations ({{ re_files|length }})</h4>
<table class="report-table">
<thead><tr><th>Path</th><th>Operation</th><th>Thread</th></tr></thead>
<tbody>
{% for f in re_files[:25] %}
<tr>
<td class="mono" style="word-break: break-all;">{{ (f.path or '—')|truncate(120, true) }}</td>
<td class="mono muted">{{ f.operation or '—' }}</td>
<td class="mono muted">{{ f.thread_id if f.thread_id is not none else '—' }}</td>
</tr>
{% endfor %}
{% if re_files|length > 25 %}
<tr><td colspan="3" class="center">… and {{ re_files|length - 25 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% endif %}
{% if re_audit %}
<h4 class="subsection-title mt-12">Audit-API Calls ({{ re_audit|length }})</h4>
<table class="report-table">
<thead><tr><th>API</th><th>Target PID</th><th>Target TID</th><th>Caller PID/TID</th></tr></thead>
<tbody>
{% for a in re_audit[:25] %}
<tr>
<td class="mono">{{ a.api or '—' }}</td>
<td class="mono">{{ a.target_pid if a.target_pid is not none else '—' }}</td>
<td class="mono muted">{{ a.target_tid if a.target_tid is not none else '—' }}</td>
<td class="mono muted">{{ a.caller_pid if a.caller_pid is not none else '—' }} / {{ a.caller_tid if a.caller_tid is not none else '—' }}</td>
</tr>
{% endfor %}
{% if re_audit|length > 25 %}
<tr><td colspan="4" class="center">… and {{ re_audit|length - 25 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% endif %}
</div>
</div>
+33
View File
@@ -328,10 +328,43 @@ def _calculate_dynamic_risk(dynamic_results, analysis_type):
dynamic_risk += _calculate_memory_anomaly_risk(dynamic_results, analysis_type, risk_factors)
dynamic_risk += _calculate_behavior_risk(dynamic_results, analysis_type, risk_factors)
dynamic_risk += _calculate_hsb_risk(dynamic_results, analysis_type, risk_factors)
dynamic_risk += _calculate_rededr_risk(dynamic_results, analysis_type, risk_factors)
return dynamic_risk, risk_factors
def _calculate_rededr_risk(dynamic_results, analysis_type, risk_factors):
"""Defender-only contribution from RedEdr telemetry.
The analyzer classifies every defender_events entry as one of:
threat — real detection (ThreatFound, non-empty verdict, etc.)
scan — Defender behavior monitor actively engaged with our process
(BmModuleLoad / BmNotificationHandle* / BmOpenProcess)
internal — Defender's own state plumbing (BmInternal / BmEtw)
other — anything else (e.g., msmpeng ThreadStop)
Only `threat` events bump the score. `scan` is descriptive (operator
knows Defender engaged but didn't flag — typically the win state). The
other RedEdr signals (network, audit-API, file ops, child processes)
stay descriptive too, per the design decision.
"""
rededr = dynamic_results.get('rededr', {}).get('findings', {})
defender = rededr.get('defender_events') or []
if not defender:
return 0
threat_hits = [e for e in defender if e.get('category') == 'threat']
if threat_hits:
# ThreatFound-class verdict at runtime is the strongest possible signal.
risk_factors.append(
f"Critical: Microsoft Defender flagged the binary at runtime "
f"({len(threat_hits)} threat verdict{'s' if len(threat_hits) != 1 else ''})"
)
return 50
return 0
def _calculate_memory_anomaly_risk(dynamic_results, analysis_type, risk_factors):
moneta_findings = dynamic_results.get('moneta', {}).get('findings', {})
if not moneta_findings: