Add Codex-powered TraceTree workbench - #53
Conversation
…ndering as caution build_behavior_receipt only ever emitted "suspicious"/"clean" — "malicious" was never surfaced, so the API's decision->level map (malicious->danger) was dead code and high-confidence detections downgraded to yellow "caution" in the UI. Now emits "malicious" when risk_score >= 75. Mirrored the same lowercase clean/suspicious/malicious contract in the TS orchestrator's SQLite scan persistence, which previously wrote MALICIOUS/CLEAN. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe pull request expands TraceTree with a Workbench frontend, broader FastAPI and CLI workflows, standardized analysis adapters, CVE synchronization, quarantine-backed evidence handling, updated detection semantics, and new model retraining and disassembly capabilities. ChangesAnalysis platform
CVE operations
Workbench UI
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoAdd TraceTree Workbench dashboard with supporting API, analyzers, and CVE/YARA tooling
AI Description
Diagram
High-Level Assessment
Files changed (45)
|
Code Review by Qodo
1. Analysis tuple arity crash
|
| def perform_analysis(target: str, target_type: str, progress, console, workspace_root: str = None, controlled_network: bool = False) -> Tuple[bool, float, dict, dict, list, list, list, dict, str]: | ||
| """Helper to run the full sandbox → parse → graph → ML pipeline for a single target. | ||
|
|
||
| Returns: | ||
| (is_malicious, confidence, graph_data, parsed_data, signature_matches, temporal_patterns, yara_matches, ngram_data, log_path) | ||
| """ | ||
| _purge_stale_quarantine() | ||
| from sandbox.sandbox import run_sandbox |
There was a problem hiding this comment.
1. Analysis tuple arity crash 🐞 Bug ≡ Correctness
cli.perform_analysis() now returns 9 values (including log_path) and callers unpack 9, but several early error returns still return only 8 values, causing `ValueError: not enough values to unpack` and aborting the CLI on sandbox/parser/graph failures.
Agent Prompt
### Issue description
`perform_analysis()` was expanded to return 9 values (now including `log_path`), and callers were updated to unpack 9 values. However, multiple early `return` statements still return 8 values, which will raise during unpack and crash the CLI on common failure paths.
### Issue Context
This affects error handling for sandbox failure, parser failure, and graph-building failure; ML failure was already updated to return 9 values.
### Fix Focus Areas
- cli.py[320-431]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| def _purge_stale_quarantine(max_age_days: int = 7) -> None: | ||
| """Delete quarantine session dirs older than max_age_days. | ||
|
|
||
| Runs silently at CLI startup — quarantine holds live malware binaries and | ||
| must not accumulate indefinitely. 7-day default suits single-user local use. | ||
| Users who need longer retention can set TRACETREE_QUARANTINE_TTL_DAYS env var. | ||
| """ | ||
| import shutil as _shutil | ||
| import time as _time | ||
| ttl = int(os.environ.get("TRACETREE_QUARANTINE_TTL_DAYS", max_age_days)) | ||
| quarantine_root = Path.cwd() / ".tracetree" / "quarantine" |
There was a problem hiding this comment.
2. Quarantine ttl env crashes 🐞 Bug ☼ Reliability
_purge_stale_quarantine() parses TRACETREE_QUARANTINE_TTL_DAYS with int() without handling ValueError, so a malformed env var will crash every analysis before it starts.
Agent Prompt
### Issue description
`_purge_stale_quarantine()` converts `TRACETREE_QUARANTINE_TTL_DAYS` via `int(...)` without validation. Any non-integer value (including empty string) raises `ValueError` and aborts CLI runs.
### Issue Context
This function is invoked unconditionally at the start of `perform_analysis()`, so the crash happens before sandboxing.
### Fix Focus Areas
- cli.py[297-327]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if log_path and Path(log_path).exists(): | ||
| files.append(Path(log_path)) | ||
| lp = Path(log_path) | ||
| if lp.suffix.lower() not in SCAN_EXCLUDE_EXTENSIONS: | ||
| files.append(lp) |
There was a problem hiding this comment.
4. Yara skipped for pip 🐞 Bug ≡ Correctness
monitor.yara._collect_files() now excludes .log files (including strace logs), and cli.perform_analysis() passes only log_path for pip analyses (no package_dir), so YARA scanning returns an empty result for pip targets and the YARA analyzer reports BENIGN.
Agent Prompt
### Issue description
YARA scanning now skips any `log_path` with suffix `.log`. TraceTree’s strace outputs are `*_strace.log`, so for normal pip analyses (where no `package_dir` is provided), the YARA scan has zero files to scan and always returns `[]`.
### Issue Context
The intent comment suggests avoiding false positives from scanning logs, but the current wiring provides no alternative artifact path for pip targets.
### Fix Focus Areas
- monitor/yara.py[23-28]
- monitor/yara.py[452-463]
- cli.py[386-392]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| proc = subprocess.Popen( | ||
| argv, | ||
| shell=False, | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.STDOUT, | ||
| text=True, | ||
| bufsize=1, | ||
| stdin=subprocess.DEVNULL, | ||
| stdout=slave_fd, | ||
| stderr=slave_fd, | ||
| close_fds=True, | ||
| env=env, | ||
| ) | ||
| os.close(slave_fd) | ||
|
|
||
| # Set master non-blocking | ||
| fl = fcntl.fcntl(master_fd, fcntl.F_GETFL) | ||
| fcntl.fcntl(master_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) | ||
|
|
||
| buf = "" | ||
| last_output = time.time() | ||
|
|
||
| try: | ||
| while True: | ||
| ready, _, _ = select.select([master_fd], [], [], 0.1) | ||
| if ready: | ||
| try: | ||
| chunk = os.read(master_fd, 4096) | ||
| if not chunk: | ||
| break | ||
| last_output = time.time() | ||
| text = chunk.decode("utf-8", errors="replace") | ||
| # Collapse carriage returns: spinner emits \r to overwrite the | ||
| # current line in a real terminal. Split on \n first, then for | ||
| # each line keep only the LAST frame after any \r so the browser | ||
| # sees only the final settled state, not every intermediate frame. | ||
| text = text.replace("\r\n", "\n") | ||
| segments = text.split("\n") | ||
| segments = [s.split("\r")[-1] for s in segments] | ||
| text = "\n".join(segments) | ||
| buf += text | ||
| while "\n" in buf: | ||
| line, buf = buf.split("\n", 1) | ||
| clean = _strip_ansi(line).rstrip() | ||
| if clean: | ||
| yield f"data: {clean}\n\n" | ||
| except OSError: | ||
| break | ||
| else: | ||
| if proc.poll() is not None: | ||
| break | ||
| # Send keepalive dot so user sees it's running | ||
| if time.time() - last_output > 3: | ||
| yield "data: ...\n\n" | ||
| last_output = time.time() | ||
| finally: | ||
| try: | ||
| os.close(master_fd) | ||
| except OSError: | ||
| pass | ||
|
|
||
| for line in iter(proc.stdout.readline, ""): | ||
| yield f"data: {line}\n\n" | ||
|
|
||
| proc.stdout.close() | ||
| proc.wait() | ||
| yield f"data: \n--- PROCESS FINISHED WITH CODE {proc.returncode} ---\n\n" | ||
| yield f"data: --- FINISHED (exit {proc.returncode}) ---\n\n" |
There was a problem hiding this comment.
5. Sse cli process orphaning 🐞 Bug ☼ Reliability
POST /api/execute spawns a CLI subprocess and streams output, but on client disconnect/cancellation it only closes the PTY FD and never terminates the subprocess, allowing long-running scans to continue headless and exhaust host/Docker resources.
Agent Prompt
### Issue description
The SSE generator cleans up file descriptors but never terminates the spawned subprocess when the streaming response is cancelled (e.g., browser tab closed). This can leave expensive analyses running without any client.
### Issue Context
The subprocess is started via `subprocess.Popen(...)` and the generator does `proc.wait()` unconditionally after the loop.
### Fix Focus Areas
- api/main.py[272-342]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| try: | ||
| r2 = r2pipe.open(file_path, flags=["-2"]) | ||
|
|
||
| resolved = _resolve_offset(r2, offset, file_path) | ||
| if resolved is None: | ||
| result["error"] = "could not resolve a disassembly offset (no symbols, no YARA offset)" | ||
| r2.quit() | ||
| return result | ||
|
|
||
| result["offset_used"] = resolved | ||
|
|
||
| # Request only the bounded instruction window. Do not run whole-program | ||
| # analysis or request complete function JSON for untrusted binaries. | ||
| pdj_raw = r2.cmd(f"pdj {max_insns} @ {resolved}") | ||
| if pdj_raw.strip(): | ||
| try: | ||
| insns = json.loads(pdj_raw) | ||
| result["instructions"] = [ | ||
| { | ||
| "offset": insn.get("offset", resolved), | ||
| "disasm": insn.get("disasm", ""), | ||
| "type": insn.get("type", ""), | ||
| "bytes": insn.get("bytes", ""), | ||
| } | ||
| for insn in insns[:max_insns] | ||
| ] | ||
| except json.JSONDecodeError as e: | ||
| result["error"] = f"pdj parse error: {e}" | ||
|
|
||
| r2.quit() | ||
| except Exception as e: | ||
| result["error"] = str(e) | ||
|
|
There was a problem hiding this comment.
6. Radare2 session leak 🐞 Bug ☼ Reliability
disassemble_at() opens an r2pipe session but does not call r2.quit() if an exception occurs after r2pipe.open(), leaking radare2 processes/file descriptors under error conditions.
Agent Prompt
### Issue description
If `_resolve_offset()` or `r2.cmd(...)` raises after `r2pipe.open(...)`, the broad `except Exception` sets `result["error"]` and returns without closing the r2 session.
### Issue Context
Normal completion and one early-return path call `r2.quit()`, but the error path does not.
### Fix Focus Areas
- monitor/analyzers/static_disassembly_analyzer.py[103-136]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
data/mcp_rce_signatures.yara (2)
68-80: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
MCP_UI_CSS_Exfiltrationcondition now fires on a benign, ubiquitous CSS pattern alone.Condition changed from requiring
$css_import_urltoany of them, so$css_attribute_selector(/\[class\s*[\^$*]?=\s*["'][^"']*["']\s*\]/, matching ordinary attribute selectors like[class*="foo"]) alone now triggers a "high" severity CSS exfiltration finding. This pattern is extremely common in normal CSS/HTML and unrelated to exfiltration, which real exfiltration requires (@import url(https://...)). This will generate false positives at high severity.🐛 Proposed fix — require the actual exfiltration indicator
condition: - any of them + $css_import_url or ($css_attribute_selector and $css_import_url)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/mcp_rce_signatures.yara` around lines 68 - 80, Update the condition in rule MCP_UI_CSS_Exfiltration to require the $css_import_url indicator instead of matching any string, while preserving the existing detection strings and metadata.
22-36: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winGreedy
$html_script_tagregex over-matches across unrelated<script>blocks.
/<script[^>]*>[\s\S]*<\/script>/greedily spans from the first<script>to the last</script>in the scanned content, conflating unrelated script blocks into one oversized match and inflatingmatched_strings/matched_offsets(permonitor/yara.py'sscan_with_yaracontract) for files with multiple script tags. Consider a non-greedy/bounded variant to keep matches scoped to a single tag.♻️ Proposed fix
- $html_script_tag = /<script[^>]*>[\s\S]*<\/script>/ nocase + $html_script_tag = /<script[^>]*>[\s\S]{0,4096}?<\/script>/ nocase🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/mcp_rce_signatures.yara` around lines 22 - 36, Update the `$html_script_tag` pattern in `MCP_UI_HTML_Injection` to use a non-greedy or otherwise bounded body match, so each match spans only one `<script>...</script>` block and does not combine unrelated script tags. Preserve the existing case-insensitive detection and surrounding tag matching.ml/detector.py (1)
389-398: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winNormalize RF confidence to the shared 0–1 scale.
monitor/analyzers/base.pydefinesAnalysisFinding.confidenceas0.0–1.0, and the other analyzers already follow that contract.RandomForestAnalyzerstill forwardsml_probabilitydirectly, so RF findings come out on a 0–100 scale while the fallback path divides by 100. Normalize the RF branch here, and update the wrapper test if needed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ml/detector.py` around lines 389 - 398, Normalize the RandomForestClassifier confidence in the RF branch of RandomForestAnalyzer by converting the percentage-based ml_probability to the shared 0.0–1.0 scale before assigning ml_confidence. Keep the malicious-probability calculation unchanged, ensure the fallback path remains consistent, and update the wrapper test expectations if they assert the old 0–100 value.
🧹 Nitpick comments (4)
frontend/app/page.tsx (1)
291-294: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant verdict re-normalization.
verdictToScanRow(Lines 86-96) already coercesverdictinto the allowed enum before rows enterscan_history; re-validating it again here is unnecessary duplication.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/app/page.tsx` around lines 291 - 294, Remove the redundant verdict validation and cast from the recentScans mapping, relying on verdictToScanRow to provide the normalized ScanRow verdict. Preserve the existing scan_history slicing and object spreading while assigning r.verdict directly.monitor/parser.py (2)
450-484: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant condition, but logic is otherwise sound.
is_benign = _is_benign_binary(target_bin) or is_root_wrapper or is_failedalready folds inis_root_wrapper, so the subsequentif not is_benign and not is_root_wrapper:(line 464) has a redundantand not is_root_wrapperclause (it can never be false there sinceis_root_wrapperbeing true would already makeis_benigntrue). Harmless but confusing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@monitor/parser.py` around lines 450 - 484, Remove the redundant “and not is_root_wrapper” condition from the suspicious-binary check in the execve handling block, since is_benign already includes is_root_wrapper. Keep the existing severity update and suspicious_flags behavior unchanged.
590-602: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor:
sendto's emitted severity is hardcoded rather than derived fromseverity.
severity = max(severity, 2.0)is computed but then_make_event(syscall, "AF_INET", 2.0, {})hardcodes2.0directly instead of passingseverity. Functionally equivalent today (base weight is0.0), but ifSEVERITY_WEIGHTS["sendto"]is ever raised,total_severityand the recorded event severity would silently diverge.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@monitor/parser.py` around lines 590 - 602, Update the sendto branch in the parser’s syscall handling to pass the computed severity value into _make_event instead of hardcoding 2.0. Keep the existing severity = max(severity, 2.0) calculation and AF_INET behavior unchanged so recorded event severity stays aligned with total severity if the base weight changes.tests/unit/test_phase_a_wrappers.py (1)
169-175: 🚀 Performance & Scalability | 🔵 Trivial
test_confidence_is_ml_probabilitymay be testing a cache hit, not independent computation.
detect_anomalymemoizes results in_PREDICTION_CACHEkeyed by a feature hash. This callsRandomForestAnalyzer().analyze()(which internally callsdetect_anomaly) and then callsdetect_anomalyagain directly with the same inputs, so the second call likely returns the same cachedAnomalyVerdictobject rather than an independently recomputed one, weakening this as a regression test for the confidence-derivation logic itself.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_phase_a_wrappers.py` around lines 169 - 175, The test_confidence_is_ml_probability currently risks comparing two references to the same cached AnomalyVerdict from _PREDICTION_CACHE. Update the test to obtain an independently computed expected verdict, such as by clearing or bypassing the prediction cache before the direct detect_anomaly call, then derive expected_conf from that result while preserving the existing confidence assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/main.py`:
- Around line 604-616: Update the Docker status request used by the UserMenu
component to include the required X-API-Key header so it satisfies
verify_api_key and avoids a 422 response; alternatively, explicitly make
docker_status public only if that is the intended access policy.
In `@cli.py`:
- Line 429: Update all early-return branches in perform_analysis, including the
sandbox-failure, parser-failure, and graph-failure paths, to return the same
9-element tuple as the successful and existing updated returns. Preserve each
branch’s current fallback values, add the missing ngram_data and trailing
log_path fields in the correct order, and ensure callers can consistently unpack
the result.
In `@frontend/app/page.tsx`:
- Around line 151-242: Update the WebSocket useEffect containing the
ai_summary_completed handler so it does not retain the initial
refreshHistory/apiKey closure. Prefer matching LiveMonitor.tsx by adding apiKey
to the effect dependencies, allowing the socket handler to rebind when the key
changes; preserve the existing cleanup and reconnect behavior.
In `@frontend/components/workbench/CommandConsole.tsx`:
- Around line 64-76: Convert each OSV severity vector to a numeric CVSS score
before assigning CVEResult.score, using the existing CVSS parsing/conversion
utility. Apply this in both mapper sites at
frontend/components/workbench/CommandConsole.tsx lines 64-76 and 174-183, while
preserving the existing fallback behavior when severity data is missing.
In `@frontend/components/workbench/DataTable.tsx`:
- Line 55: Update the comparison in the table sorting logic to pass the numeric
option to String.localeCompare, ensuring numeric-looking values such as IDs and
confidence scores sort numerically while preserving text sorting behavior.
In `@frontend/components/workbench/screens/Evidence.tsx`:
- Around line 261-268: Update the offset rendering in the
disasmResult.instructions mapping to check explicitly for a missing offset
rather than relying on truthiness, so a valid ins.offset of 0 displays as 0x0
while genuinely absent offsets remain blank.
In `@frontend/components/workbench/screens/ScanHistory.tsx`:
- Around line 108-115: Update the findings rendering in ScanHistory’s RawWell
props to handle JSON.parse failures without throwing during React rendering.
Safely parse selected.findings and fall back to the existing
target/verdict/confidence/SHA256 lines when the value is malformed or not valid
JSON, while preserving the formatted JSON output for valid findings.
In `@frontend/components/workbench/search.ts`:
- Around line 52-54: Update the token matching logic around tokVal and regex to
escape all user-provided regular-expression metacharacters before translating *
into the intended wildcard pattern, while preserving wildcard behavior. Wrap
RegExp construction and testing in try-catch so invalid input cannot escape the
render path; treat failures as non-matches and keep rendering stable.
In `@frontend/components/workbench/UserMenu.tsx`:
- Around line 114-117: Update the API status row in UserMenu to use an apiOnline
prop instead of the hardcoded dot(true), and add the corresponding prop to the
component’s type/signature. In page.tsx, pass apiOnline={stats.api_online} when
rendering UserMenu so the indicator reflects the tracked gateway health.
- Around line 16-23: Update fetchDocker in UserMenu to include the configured
X-API-Key when requesting /api/docker/status, preserving the existing
unavailable fallback on failure. Then update the menu’s API status rendering to
pass the fetched Docker/health state to dot instead of hardcoding true, so the
indicator reflects the real backend status.
In `@monitor/analyzers/rf_analyzer.py`:
- Around line 17-33: Update the confidence calculation in analyze to normalize
result.ml_probability from its documented 0–100 scale to 0.0–1.0 by dividing by
100.0, matching the existing risk_score fallback while preserving the None
handling.
In `@monitor/analyzers/static_disassembly_analyzer.py`:
- Around line 103-134: Ensure the radare2 process opened in the disassembly
analysis flow is always closed by moving cleanup for r2.quit() into a finally
block surrounding _resolve_offset and the pdj command processing. Preserve the
existing early return and exception error handling, while avoiding duplicate
quit calls on every exit path.
In `@monitor/analyzers/syscall_analyzer.py`:
- Around line 25-27: Update the confidence calculation near SyscallAnalyzer’s
verdict logic so any non-empty sig_matches producing a MALICIOUS verdict cannot
report zero confidence; derive confidence from the signature-driven malicious
condition as well as severity, while preserving the existing severity-based cap
and behavior for non-signature findings.
In `@monitor/cve_sync.py`:
- Around line 246-301: Preserve CVEs beyond the AI batch in the success path of
the function containing `batch` and `parsed`: apply enrichment only to the first
20 entries, then return those enriched entries combined with the untouched
remainder of `cves`. Keep the existing failure behavior and ensure the returned
list retains the original CVE order and full length.
In `@monitor/parser.py`:
- Around line 296-297: Harden _is_benign_path and the is_pip_internal check so
BENIGN_PATH_SUBSTRINGS entries match only complete path components or validated
path prefixes, never arbitrary substrings within a filename or directory name.
Preserve legitimate libc.so, ld.so, locale, distutils, and pip/_internal
exemptions while ensuring crafted paths such as payload_libc.so_dropper cannot
suppress sensitive-file detection.
- Around line 539-568: Update the mmap/mprotect event construction in the
syscall parsing block so details["has_prot_exec"] stores has_prot_exec rather
than is_rwx. Also build the event target/flags string from the protection-flags
argument that contains PROT_EXEC, instead of the first address token, so the
_check_sequence_condition fallback can detect it. Preserve the existing RWX
severity and suspicious-flag behavior.
- Around line 742-759: Fix root-wrapper cleanup in the syscall event-processing
flow so it removes an event only when the current iteration actually appended
one; do not rely on the module-level sequence_id, which may refer to an older
event. Preserve legitimate root-wrapper clone/fork/vfork events. Also add chown
to the file-access syscall tuple so it uses sensitive-path evaluation and
records severity consistently with total_severity.
In `@monitor/yara.py`:
- Around line 156-178: Remove PowerShellDropper from the production-compiled
YARA ruleset, or gate it behind the project’s existing
experimental/unvalidated-rule mechanism so scan_with_yara cannot report it
through the normal critical-severity path. Preserve the rule definition for
end-to-end verification, and ensure YaraAnalyzer does not classify its matches
as MALICIOUS until validation is complete.
- Around line 222-243: Update the YARA match processing that builds instances
and matched_offsets to support both modern StringMatch objects and legacy tuple
results from yara-python, or enforce yara-python 4.3+ through the project
dependency configuration. Preserve matched_strings and byte-offset output for
supported API shapes, ensuring scans do not fail when older results are
returned.
In `@sandbox/sandbox.py`:
- Around line 652-653: Replace the undefined log.warning call in the quarantine
extraction exception handler with the module’s in-scope console.print output
path, preserving the error message and ensuring this fallback remains non-fatal.
---
Outside diff comments:
In `@data/mcp_rce_signatures.yara`:
- Around line 68-80: Update the condition in rule MCP_UI_CSS_Exfiltration to
require the $css_import_url indicator instead of matching any string, while
preserving the existing detection strings and metadata.
- Around line 22-36: Update the `$html_script_tag` pattern in
`MCP_UI_HTML_Injection` to use a non-greedy or otherwise bounded body match, so
each match spans only one `<script>...</script>` block and does not combine
unrelated script tags. Preserve the existing case-insensitive detection and
surrounding tag matching.
In `@ml/detector.py`:
- Around line 389-398: Normalize the RandomForestClassifier confidence in the RF
branch of RandomForestAnalyzer by converting the percentage-based ml_probability
to the shared 0.0–1.0 scale before assigning ml_confidence. Keep the
malicious-probability calculation unchanged, ensure the fallback path remains
consistent, and update the wrapper test expectations if they assert the old
0–100 value.
---
Nitpick comments:
In `@frontend/app/page.tsx`:
- Around line 291-294: Remove the redundant verdict validation and cast from the
recentScans mapping, relying on verdictToScanRow to provide the normalized
ScanRow verdict. Preserve the existing scan_history slicing and object spreading
while assigning r.verdict directly.
In `@monitor/parser.py`:
- Around line 450-484: Remove the redundant “and not is_root_wrapper” condition
from the suspicious-binary check in the execve handling block, since is_benign
already includes is_root_wrapper. Keep the existing severity update and
suspicious_flags behavior unchanged.
- Around line 590-602: Update the sendto branch in the parser’s syscall handling
to pass the computed severity value into _make_event instead of hardcoding 2.0.
Keep the existing severity = max(severity, 2.0) calculation and AF_INET behavior
unchanged so recorded event severity stays aligned with total severity if the
base weight changes.
In `@tests/unit/test_phase_a_wrappers.py`:
- Around line 169-175: The test_confidence_is_ml_probability currently risks
comparing two references to the same cached AnomalyVerdict from
_PREDICTION_CACHE. Update the test to obtain an independently computed expected
verdict, such as by clearing or bypassing the prediction cache before the direct
detect_anomaly call, then derive expected_conf from that result while preserving
the existing confidence assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: db0a94dd-6d5e-431f-a67b-b42cdb8c2601
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (46)
.gitignoreapi/main.pycli.pydata/mcp_rce_signatures.yaradocs/CODEX_UI_PROMPT.mdfrontend/app/globals.cssfrontend/app/layout.tsxfrontend/app/page.tsxfrontend/components/MSDosPrompt.tsxfrontend/components/workbench/CommandConsole.tsxfrontend/components/workbench/DataTable.tsxfrontend/components/workbench/FilterBar.tsxfrontend/components/workbench/Panel.tsxfrontend/components/workbench/RawWell.tsxfrontend/components/workbench/SearchBar.tsxfrontend/components/workbench/SeverityTag.tsxfrontend/components/workbench/SplitPane.tsxfrontend/components/workbench/UserMenu.tsxfrontend/components/workbench/screens/Dashboard.tsxfrontend/components/workbench/screens/Evidence.tsxfrontend/components/workbench/screens/LiveMonitor.tsxfrontend/components/workbench/screens/ScanHistory.tsxfrontend/components/workbench/screens/Settings.tsxfrontend/components/workbench/screens/Signatures.tsxfrontend/components/workbench/search.tsfrontend/tailwind.config.tsml/detector.pymonitor/analyzers/__init__.pymonitor/analyzers/base.pymonitor/analyzers/legs_analyzer.pymonitor/analyzers/rf_analyzer.pymonitor/analyzers/static_disassembly_analyzer.pymonitor/analyzers/syscall_analyzer.pymonitor/analyzers/yara_analyzer.pymonitor/cve_sync.pymonitor/parser.pymonitor/signatures.pymonitor/utils.pymonitor/yara.pypyproject.tomlretrain_model.pysandbox/sandbox.pysetup.pytests/unit/test_anomaly_semantics.pytests/unit/test_phase_a_wrappers.pytracetree_mcp_server.py
💤 Files with no reviewable changes (1)
- tracetree_mcp_server.py
| @app.get("/api/docker/status") | ||
| async def docker_status(api_key: str = Depends(verify_api_key)): | ||
| """Check if Docker daemon is reachable.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["docker", "info", "--format", "{{.ServerVersion}}"], | ||
| capture_output=True, text=True, timeout=5 | ||
| ) | ||
| if result.returncode == 0: | ||
| return {"available": True, "version": result.stdout.strip()} | ||
| return {"available": False, "version": None, "error": result.stderr.strip()} | ||
| except (FileNotFoundError, subprocess.TimeoutExpired) as e: | ||
| return {"available": False, "version": None, "error": str(e)} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP 'api/docker/status' frontend -A2 -B2Repository: tejasprasad2008-afk/TraceTree
Length of output: 584
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- verify_api_key in api/main.py ---'
rg -n "def verify_api_key|verify_api_key|X-API-Key|Header\(" api/main.py -A4 -B4
echo
echo '--- UserMenu.tsx ---'
cat -n frontend/components/workbench/UserMenu.tsx | sed -n '1,120p'
echo
echo '--- X-API-Key usage in frontend ---'
rg -n '"X-API-Key"|X-API-Key|api/docker/status|fetch\(' frontend -A2 -B2Repository: tejasprasad2008-afk/TraceTree
Length of output: 20477
/api/docker/status is behind auth, but the UI call omits X-API-Key.
verify_api_key requires a header, so frontend/components/workbench/UserMenu.tsx gets a 422 here and the Docker status badge stays offline. Add the header to this fetch or make the status route public if that’s intended.
🧰 Tools
🪛 ast-grep (0.44.1)
[error] 607-610: Command coming from incoming request
Context: subprocess.run(
["docker", "info", "--format", "{{.ServerVersion}}"],
capture_output=True, text=True, timeout=5
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🪛 Ruff (0.15.21)
[error] 609-609: Starting a process with a partial executable path
(S607)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api/main.py` around lines 604 - 616, Update the Docker status request used by
the UserMenu component to include the required X-API-Key header so it satisfies
verify_api_key and avoids a 422 response; alternatively, explicitly make
docker_status public only if that is the intended access policy.
| except Exception as e: | ||
| progress.update(task4, description=f"[bold red]✖[/] [dim]ML failed: {e}[/]") | ||
| return False, 0.0, graph_data, parsed_data, signature_matches, temporal_patterns, yara_matches, ngram_data | ||
| return False, 0.0, graph_data, parsed_data, signature_matches, temporal_patterns, yara_matches, ngram_data, log_path |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Early-return paths still return 8-tuples — caller unpack at Line 544 will ValueError.
perform_analysis now returns 9 elements (adding log_path) here and at Line 431, and the caller at Line 544 unpacks 9. But the other early returns still yield 8 elements:
- Line 352 (sandbox failed):
return False, 0.0, {}, {}, [], [], {}, {} - Line 365 (parser failed): same 8-tuple
- Line 421 (graph failed): missing trailing
log_path
The sandbox-failure path (Line 352) is a common outcome (e.g. Docker unavailable), so this crashes the analyze command with ValueError: not enough values to unpack (expected 9, got 8).
🐛 Proposed fix (apply to Lines 352, 365, 421)
- return False, 0.0, {}, {}, [], [], {}, {}
+ return False, 0.0, {}, {}, [], [], [], {}, log_path- return False, 0.0, {}, parsed_data, signature_matches, temporal_patterns, yara_matches, ngram_data
+ return False, 0.0, {}, parsed_data, signature_matches, temporal_patterns, yara_matches, ngram_data, log_pathNote: the first two also only supply 7 collection/element slots before ngram_data; align all returns to the 9-field contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cli.py` at line 429, Update all early-return branches in perform_analysis,
including the sandbox-failure, parser-failure, and graph-failure paths, to
return the same 9-element tuple as the successful and existing updated returns.
Preserve each branch’s current fallback values, add the missing ngram_data and
trailing log_path fields in the correct order, and ensure callers can
consistently unpack the result.
| // WebSocket — mirrors old page.tsx logic | ||
| useEffect(() => { | ||
| let socketTimeout: ReturnType<typeof setTimeout>; | ||
|
|
||
| const connectWebSocket = () => { | ||
| console.log("Connecting to Orchestrator WebSocket..."); | ||
| const connect = () => { | ||
| const ws = new WebSocket("ws://localhost:3000/ws/live"); | ||
| wsRef.current = ws; | ||
|
|
||
| ws.onopen = () => { | ||
| console.log("Orchestrator WebSocket connected successfully!"); | ||
| setData(prev => ({ | ||
| ...prev, | ||
| stats: { ...prev.stats, orchestrator_online: true, ollama_online: true } | ||
| })); | ||
| setStats((p) => ({ ...p, orchestrator_online: true, ollama_online: true })); | ||
| }; | ||
|
|
||
| ws.onmessage = (event) => { | ||
| try { | ||
| const raw = JSON.parse(event.data); | ||
| const { event: wsEvent, payload } = raw; | ||
| console.log("WS Telemetry Event:", wsEvent, payload); | ||
| const { event: wsEvent, payload } = JSON.parse(event.data); | ||
|
|
||
| if (wsEvent === "investigation_started") { | ||
| const pkgName = (payload.prompt || "unknown").replace("CLI Analysis: ", ""); | ||
| setData(prev => ({ | ||
| ...prev, | ||
| package_name: pkgName, | ||
| const pkg = (payload.prompt || "unknown").replace("CLI Analysis: ", ""); | ||
| setScanData((p) => ({ | ||
| ...p, | ||
| package_name: pkg, | ||
| status: "scanning", | ||
| stage: "sandbox", | ||
| confidence: 0, | ||
| total_severity: 0.0, | ||
| total_severity: 0, | ||
| events: [], | ||
| temporal_patterns: [], | ||
| network_connections: [], | ||
| ollama_triage: "AI Engine is analyzing the package behavioral trace..." | ||
| ollama_triage: "AI Engine analyzing...", | ||
| })); | ||
| } else if (wsEvent === "step_started") { | ||
| if (payload.stepId === "sandbox") setScanData((p) => ({ ...p, stage: "sandbox", confidence: 20 })); | ||
| else if (payload.stepId === "analysis") setScanData((p) => ({ ...p, stage: "randomforest", confidence: 60 })); | ||
| } else if (wsEvent === "step_completed" && payload.stepId === "analysis" && payload.findings) { | ||
| let findings = payload.findings; | ||
| if (typeof findings === "string") { try { findings = JSON.parse(findings); } catch { findings = {}; } } | ||
|
|
||
| const mappedEvents: SyscallEvent[] = (findings.events || []).map((e: any, idx: number) => ({ | ||
| id: idx + 1, | ||
| syscall: e.syscall || "openat", | ||
| pid: e.pid || 0, | ||
| target: e.target || e.target_path || "unspecified", | ||
| severity: e.severity || "LOW", | ||
| flag: e.flag || false, | ||
| })); | ||
| } | ||
|
|
||
| else if (wsEvent === "step_started") { | ||
| if (payload.stepId === "sandbox") { | ||
| setData(prev => ({ ...prev, stage: "sandbox", confidence: 20 })); | ||
| } else if (payload.stepId === "analysis") { | ||
| setData(prev => ({ ...prev, stage: "randomforest", confidence: 60 })); | ||
| } | ||
| } | ||
|
|
||
| else if (wsEvent === "step_completed") { | ||
| if (payload.stepId === "sandbox" && payload.status === "completed") { | ||
| setData(prev => ({ ...prev, stage: "parser", confidence: 40 })); | ||
| } else if (payload.stepId === "analysis" && payload.findings) { | ||
| let findings = payload.findings; | ||
| if (typeof findings === "string") { | ||
| try { | ||
| findings = JSON.parse(findings); | ||
| } catch (e) { | ||
| findings = {}; | ||
| } | ||
| } | ||
|
|
||
| // Map suspicious events | ||
| const mappedEvents = (findings.events || []).map((e: any, idx: number) => ({ | ||
| id: idx + 1, | ||
| syscall: e.syscall || "openat", | ||
| pid: e.pid || 0, | ||
| target: e.target || e.target_path || "unspecified", | ||
| severity: e.severity || "LOW", | ||
| flag: e.flag || false | ||
| })); | ||
|
|
||
| // Map signatures | ||
| const mappedPatterns = (findings.behavioral_signatures || []).map((s: any) => ({ | ||
| pattern: s.name || "suspicious_signature", | ||
| severity: s.severity === "HIGH" ? 9 : s.severity === "MEDIUM" ? 6 : 3, | ||
| window: s.evidence || "within execution window" | ||
| })); | ||
|
|
||
| // Map network | ||
| const mappedNetwork = (findings.network_destinations || []).map((n: any) => ({ | ||
| destination: n.ip || n.host || "unknown", | ||
| port: n.port || 443, | ||
| classification: n.classification || "OUTBOUND", | ||
| verdict: n.is_malicious ? "MALICIOUS" : "CLEAN" | ||
| })); | ||
|
|
||
| setData(prev => ({ | ||
| ...prev, | ||
| status: findings.is_malicious ? "malicious" : "clean", | ||
| confidence: Math.round((findings.confidence_score || 0) * (findings.confidence_score <= 1 ? 100 : 1)), | ||
| stage: "ollama", | ||
| total_severity: findings.total_severity || 0.0, | ||
| events: mappedEvents, | ||
| temporal_patterns: mappedPatterns, | ||
| network_connections: mappedNetwork | ||
| })); | ||
| } | ||
| } | ||
| const mappedPatterns = (findings.behavioral_signatures || []).map((s: any) => ({ | ||
| pattern: s.name || "suspicious_signature", | ||
| severity: s.severity === "HIGH" ? 9 : s.severity === "MEDIUM" ? 6 : 3, | ||
| window: s.evidence || "within execution window", | ||
| })); | ||
|
|
||
| else if (wsEvent === "ai_summary_started") { | ||
| setData(prev => ({ | ||
| ...prev, | ||
| ollama_triage: "Ollama is drafting final verification summary..." | ||
| const mappedNetwork = (findings.network_destinations || []).map((n: any) => ({ | ||
| destination: n.ip || n.host || "unknown", | ||
| port: n.port || 443, | ||
| classification: n.classification || "OUTBOUND", | ||
| verdict: n.is_malicious ? "MALICIOUS" : "CLEAN", | ||
| })); | ||
| } | ||
|
|
||
| else if (wsEvent === "ai_summary_completed") { | ||
| setData(prev => ({ | ||
| ...prev, | ||
| stage: "complete", | ||
| ollama_triage: payload.summary || "AI Triage analysis complete." | ||
| const conf = Math.round((findings.confidence_score || 0) * (findings.confidence_score <= 1 ? 100 : 1)); | ||
|
|
||
| setScanData((p) => ({ | ||
| ...p, | ||
| status: findings.is_malicious ? "malicious" : "clean", | ||
| confidence: conf, | ||
| stage: "ollama", | ||
| total_severity: findings.total_severity || 0, | ||
| events: mappedEvents, | ||
| temporal_patterns: mappedPatterns, | ||
| network_connections: mappedNetwork, | ||
| })); | ||
| // Refresh history | ||
| refreshScanHistory(); | ||
| } else if (wsEvent === "ai_summary_completed") { | ||
| setScanData((p) => ({ ...p, stage: "complete", ollama_triage: payload.summary || "" })); | ||
| refreshHistory(); | ||
| } | ||
|
|
||
| } catch (e) { | ||
| console.error("Error parsing WS event", e); | ||
| } | ||
| } catch { /* ignore bad ws frames */ } | ||
| }; | ||
|
|
||
| ws.onclose = () => { | ||
| console.warn("Orchestrator WebSocket disconnected. Retrying in 3s..."); | ||
| setData(prev => ({ | ||
| ...prev, | ||
| stats: { ...prev.stats, orchestrator_online: false, ollama_online: false } | ||
| })); | ||
| socketTimeout = setTimeout(connectWebSocket, 3000); | ||
| setStats((p) => ({ ...p, orchestrator_online: false, ollama_online: false })); | ||
| socketTimeout = setTimeout(connect, 3000); | ||
| }; | ||
|
|
||
| ws.onerror = () => { | ||
| ws.close(); | ||
| }; | ||
| ws.onerror = () => ws.close(); | ||
| }; | ||
|
|
||
| connectWebSocket(); | ||
| connect(); | ||
| return () => { | ||
| clearTimeout(socketTimeout); | ||
| if (wsRef.current) wsRef.current.close(); | ||
| wsRef.current?.close(); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Stale-closure bug: WS-triggered refreshHistory() keeps using the API key from mount time.
The WS effect has an empty dependency array (Line 242), so its ai_summary_completed handler (Line 222-224) permanently closes over the refreshHistory instance — and thus the apiKey — from the initial render. When the user later changes the API key (handleApiKeyChange), refreshHistory is redefined with the new key on re-render, but the long-lived WS handler never picks it up, so live-triggered history refreshes keep sending the stale key. LiveMonitor.tsx's equivalent WS effect correctly depends on [apiKey] and reconnects/rebinds on key change — this file diverges from that pattern.
🐛 Proposed fix: reconnect on apiKey change (mirrors LiveMonitor.tsx)
- }, []);
+ }, [apiKey]);Alternatively, keep the effect key-independent but read apiKey via a ref updated each render, so the long-lived closure always sees the latest value.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // WebSocket — mirrors old page.tsx logic | |
| useEffect(() => { | |
| let socketTimeout: ReturnType<typeof setTimeout>; | |
| const connectWebSocket = () => { | |
| console.log("Connecting to Orchestrator WebSocket..."); | |
| const connect = () => { | |
| const ws = new WebSocket("ws://localhost:3000/ws/live"); | |
| wsRef.current = ws; | |
| ws.onopen = () => { | |
| console.log("Orchestrator WebSocket connected successfully!"); | |
| setData(prev => ({ | |
| ...prev, | |
| stats: { ...prev.stats, orchestrator_online: true, ollama_online: true } | |
| })); | |
| setStats((p) => ({ ...p, orchestrator_online: true, ollama_online: true })); | |
| }; | |
| ws.onmessage = (event) => { | |
| try { | |
| const raw = JSON.parse(event.data); | |
| const { event: wsEvent, payload } = raw; | |
| console.log("WS Telemetry Event:", wsEvent, payload); | |
| const { event: wsEvent, payload } = JSON.parse(event.data); | |
| if (wsEvent === "investigation_started") { | |
| const pkgName = (payload.prompt || "unknown").replace("CLI Analysis: ", ""); | |
| setData(prev => ({ | |
| ...prev, | |
| package_name: pkgName, | |
| const pkg = (payload.prompt || "unknown").replace("CLI Analysis: ", ""); | |
| setScanData((p) => ({ | |
| ...p, | |
| package_name: pkg, | |
| status: "scanning", | |
| stage: "sandbox", | |
| confidence: 0, | |
| total_severity: 0.0, | |
| total_severity: 0, | |
| events: [], | |
| temporal_patterns: [], | |
| network_connections: [], | |
| ollama_triage: "AI Engine is analyzing the package behavioral trace..." | |
| ollama_triage: "AI Engine analyzing...", | |
| })); | |
| } else if (wsEvent === "step_started") { | |
| if (payload.stepId === "sandbox") setScanData((p) => ({ ...p, stage: "sandbox", confidence: 20 })); | |
| else if (payload.stepId === "analysis") setScanData((p) => ({ ...p, stage: "randomforest", confidence: 60 })); | |
| } else if (wsEvent === "step_completed" && payload.stepId === "analysis" && payload.findings) { | |
| let findings = payload.findings; | |
| if (typeof findings === "string") { try { findings = JSON.parse(findings); } catch { findings = {}; } } | |
| const mappedEvents: SyscallEvent[] = (findings.events || []).map((e: any, idx: number) => ({ | |
| id: idx + 1, | |
| syscall: e.syscall || "openat", | |
| pid: e.pid || 0, | |
| target: e.target || e.target_path || "unspecified", | |
| severity: e.severity || "LOW", | |
| flag: e.flag || false, | |
| })); | |
| } | |
| else if (wsEvent === "step_started") { | |
| if (payload.stepId === "sandbox") { | |
| setData(prev => ({ ...prev, stage: "sandbox", confidence: 20 })); | |
| } else if (payload.stepId === "analysis") { | |
| setData(prev => ({ ...prev, stage: "randomforest", confidence: 60 })); | |
| } | |
| } | |
| else if (wsEvent === "step_completed") { | |
| if (payload.stepId === "sandbox" && payload.status === "completed") { | |
| setData(prev => ({ ...prev, stage: "parser", confidence: 40 })); | |
| } else if (payload.stepId === "analysis" && payload.findings) { | |
| let findings = payload.findings; | |
| if (typeof findings === "string") { | |
| try { | |
| findings = JSON.parse(findings); | |
| } catch (e) { | |
| findings = {}; | |
| } | |
| } | |
| // Map suspicious events | |
| const mappedEvents = (findings.events || []).map((e: any, idx: number) => ({ | |
| id: idx + 1, | |
| syscall: e.syscall || "openat", | |
| pid: e.pid || 0, | |
| target: e.target || e.target_path || "unspecified", | |
| severity: e.severity || "LOW", | |
| flag: e.flag || false | |
| })); | |
| // Map signatures | |
| const mappedPatterns = (findings.behavioral_signatures || []).map((s: any) => ({ | |
| pattern: s.name || "suspicious_signature", | |
| severity: s.severity === "HIGH" ? 9 : s.severity === "MEDIUM" ? 6 : 3, | |
| window: s.evidence || "within execution window" | |
| })); | |
| // Map network | |
| const mappedNetwork = (findings.network_destinations || []).map((n: any) => ({ | |
| destination: n.ip || n.host || "unknown", | |
| port: n.port || 443, | |
| classification: n.classification || "OUTBOUND", | |
| verdict: n.is_malicious ? "MALICIOUS" : "CLEAN" | |
| })); | |
| setData(prev => ({ | |
| ...prev, | |
| status: findings.is_malicious ? "malicious" : "clean", | |
| confidence: Math.round((findings.confidence_score || 0) * (findings.confidence_score <= 1 ? 100 : 1)), | |
| stage: "ollama", | |
| total_severity: findings.total_severity || 0.0, | |
| events: mappedEvents, | |
| temporal_patterns: mappedPatterns, | |
| network_connections: mappedNetwork | |
| })); | |
| } | |
| } | |
| const mappedPatterns = (findings.behavioral_signatures || []).map((s: any) => ({ | |
| pattern: s.name || "suspicious_signature", | |
| severity: s.severity === "HIGH" ? 9 : s.severity === "MEDIUM" ? 6 : 3, | |
| window: s.evidence || "within execution window", | |
| })); | |
| else if (wsEvent === "ai_summary_started") { | |
| setData(prev => ({ | |
| ...prev, | |
| ollama_triage: "Ollama is drafting final verification summary..." | |
| const mappedNetwork = (findings.network_destinations || []).map((n: any) => ({ | |
| destination: n.ip || n.host || "unknown", | |
| port: n.port || 443, | |
| classification: n.classification || "OUTBOUND", | |
| verdict: n.is_malicious ? "MALICIOUS" : "CLEAN", | |
| })); | |
| } | |
| else if (wsEvent === "ai_summary_completed") { | |
| setData(prev => ({ | |
| ...prev, | |
| stage: "complete", | |
| ollama_triage: payload.summary || "AI Triage analysis complete." | |
| const conf = Math.round((findings.confidence_score || 0) * (findings.confidence_score <= 1 ? 100 : 1)); | |
| setScanData((p) => ({ | |
| ...p, | |
| status: findings.is_malicious ? "malicious" : "clean", | |
| confidence: conf, | |
| stage: "ollama", | |
| total_severity: findings.total_severity || 0, | |
| events: mappedEvents, | |
| temporal_patterns: mappedPatterns, | |
| network_connections: mappedNetwork, | |
| })); | |
| // Refresh history | |
| refreshScanHistory(); | |
| } else if (wsEvent === "ai_summary_completed") { | |
| setScanData((p) => ({ ...p, stage: "complete", ollama_triage: payload.summary || "" })); | |
| refreshHistory(); | |
| } | |
| } catch (e) { | |
| console.error("Error parsing WS event", e); | |
| } | |
| } catch { /* ignore bad ws frames */ } | |
| }; | |
| ws.onclose = () => { | |
| console.warn("Orchestrator WebSocket disconnected. Retrying in 3s..."); | |
| setData(prev => ({ | |
| ...prev, | |
| stats: { ...prev.stats, orchestrator_online: false, ollama_online: false } | |
| })); | |
| socketTimeout = setTimeout(connectWebSocket, 3000); | |
| setStats((p) => ({ ...p, orchestrator_online: false, ollama_online: false })); | |
| socketTimeout = setTimeout(connect, 3000); | |
| }; | |
| ws.onerror = () => { | |
| ws.close(); | |
| }; | |
| ws.onerror = () => ws.close(); | |
| }; | |
| connectWebSocket(); | |
| connect(); | |
| return () => { | |
| clearTimeout(socketTimeout); | |
| if (wsRef.current) wsRef.current.close(); | |
| wsRef.current?.close(); | |
| }; | |
| }, []); | |
| // WebSocket — mirrors old page.tsx logic | |
| useEffect(() => { | |
| let socketTimeout: ReturnType<typeof setTimeout>; | |
| const connect = () => { | |
| const ws = new WebSocket("ws://localhost:3000/ws/live"); | |
| wsRef.current = ws; | |
| ws.onopen = () => { | |
| setStats((p) => ({ ...p, orchestrator_online: true, ollama_online: true })); | |
| }; | |
| ws.onmessage = (event) => { | |
| try { | |
| const { event: wsEvent, payload } = JSON.parse(event.data); | |
| if (wsEvent === "investigation_started") { | |
| const pkg = (payload.prompt || "unknown").replace("CLI Analysis: ", ""); | |
| setScanData((p) => ({ | |
| ...p, | |
| package_name: pkg, | |
| status: "scanning", | |
| stage: "sandbox", | |
| confidence: 0, | |
| total_severity: 0, | |
| events: [], | |
| temporal_patterns: [], | |
| network_connections: [], | |
| ollama_triage: "AI Engine analyzing...", | |
| })); | |
| } else if (wsEvent === "step_started") { | |
| if (payload.stepId === "sandbox") setScanData((p) => ({ ...p, stage: "sandbox", confidence: 20 })); | |
| else if (payload.stepId === "analysis") setScanData((p) => ({ ...p, stage: "randomforest", confidence: 60 })); | |
| } else if (wsEvent === "step_completed" && payload.stepId === "analysis" && payload.findings) { | |
| let findings = payload.findings; | |
| if (typeof findings === "string") { try { findings = JSON.parse(findings); } catch { findings = {}; } } | |
| const mappedEvents: SyscallEvent[] = (findings.events || []).map((e: any, idx: number) => ({ | |
| id: idx + 1, | |
| syscall: e.syscall || "openat", | |
| pid: e.pid || 0, | |
| target: e.target || e.target_path || "unspecified", | |
| severity: e.severity || "LOW", | |
| flag: e.flag || false, | |
| })); | |
| const mappedPatterns = (findings.behavioral_signatures || []).map((s: any) => ({ | |
| pattern: s.name || "suspicious_signature", | |
| severity: s.severity === "HIGH" ? 9 : s.severity === "MEDIUM" ? 6 : 3, | |
| window: s.evidence || "within execution window", | |
| })); | |
| const mappedNetwork = (findings.network_destinations || []).map((n: any) => ({ | |
| destination: n.ip || n.host || "unknown", | |
| port: n.port || 443, | |
| classification: n.classification || "OUTBOUND", | |
| verdict: n.is_malicious ? "MALICIOUS" : "CLEAN", | |
| })); | |
| const conf = Math.round((findings.confidence_score || 0) * (findings.confidence_score <= 1 ? 100 : 1)); | |
| setScanData((p) => ({ | |
| ...p, | |
| status: findings.is_malicious ? "malicious" : "clean", | |
| confidence: conf, | |
| stage: "ollama", | |
| total_severity: findings.total_severity || 0, | |
| events: mappedEvents, | |
| temporal_patterns: mappedPatterns, | |
| network_connections: mappedNetwork, | |
| })); | |
| } else if (wsEvent === "ai_summary_completed") { | |
| setScanData((p) => ({ ...p, stage: "complete", ollama_triage: payload.summary || "" })); | |
| refreshHistory(); | |
| } | |
| } catch { /* ignore bad ws frames */ } | |
| }; | |
| ws.onclose = () => { | |
| setStats((p) => ({ ...p, orchestrator_online: false, ollama_online: false })); | |
| socketTimeout = setTimeout(connect, 3000); | |
| }; | |
| ws.onerror = () => ws.close(); | |
| }; | |
| connect(); | |
| return () => { | |
| clearTimeout(socketTimeout); | |
| wsRef.current?.close(); | |
| }; | |
| }, [apiKey]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/app/page.tsx` around lines 151 - 242, Update the WebSocket useEffect
containing the ai_summary_completed handler so it does not retain the initial
refreshHistory/apiKey closure. Prefer matching LiveMonitor.tsx by adding apiKey
to the effect dependencies, allowing the socket handler to rebind when the key
changes; preserve the existing cleanup and reconnect behavior.
| const data = await res.json(); | ||
| const vulns: CVEResult[] = (data.vulns || []).slice(0, 10).map((v: any) => { | ||
| const sev = v.severity?.[0]; | ||
| const score = sev?.score ?? 0; | ||
| return { | ||
| id: v.id, | ||
| summary: v.summary || v.details?.slice(0, 120) || "", | ||
| severity: sev?.type || "UNKNOWN", | ||
| score, | ||
| url: `https://osv.dev/vulnerability/${v.id}`, | ||
| }; | ||
| }); | ||
| setCves(vulns); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- CommandConsole.tsx relevant slices ---'
nl -ba frontend/components/workbench/CommandConsole.tsx | sed -n '1,240p'
echo
echo '--- CVEResult definition and score consumers ---'
rg -n "interface CVEResult|type CVEResult|score\.toFixed|cvssToLevel|severity\\?\\.\\[0\\]\\.score|CVEResult" frontend -SRepository: tejasprasad2008-afk/TraceTree
Length of output: 250
🏁 Script executed:
#!/bin/bash
set -euo pipefail
pwd
git ls-files frontend/components/workbench/CommandConsole.tsxRepository: tejasprasad2008-afk/TraceTree
Length of output: 235
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- CommandConsole.tsx slices ---'
sed -n '1,240p' frontend/components/workbench/CommandConsole.tsx | cat -n
echo
echo '--- CVEResult definition and score consumers ---'
rg -n "interface CVEResult|type CVEResult|score\.toFixed|cvssToLevel|severity\\?\\.\\[0\\]\\.score|CVEResult" frontend -SRepository: tejasprasad2008-afk/TraceTree
Length of output: 10936
🌐 Web query:
OSV API severity score CVSS vector string documentation
💡 Result:
In the Open Source Vulnerability (OSV) schema, severity information is represented using a JSON array named severity [1][2]. This array contains objects that define the severity of a vulnerability using quantitative scoring methods, typically CVSS vector strings [1][2]. The severity object contains the following properties: type: A string specifying the quantitative method used [1][2]. Valid types include: CVSS_V2: For CVSS version 2.0 (e.g., "AV:L/AC:M/Au:N/C:N/I:P/A:C") [1][2]. CVSS_V3: For CVSS versions 3.0 or 3.1 (e.g., "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N") [1][2]. CVSS_V4: For CVSS version 4.0 (e.g., "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N") [1][2]. score: A string representing the actual CVSS vector string associated with the selected type [1][2]. source: An optional string indicating the origin of the severity assessment [2]. If omitted, the rating is implicitly attributed to the database that published the record [2]. The OSV API does not provide calculated numerical scores (e.g., "9.8") directly in its response; instead, it provides the CVSS vector strings [3]. Consumers requiring numerical scores must typically calculate them from the provided vector strings using external libraries or tools [3]. The severity field can be defined at the top level of the OSV record, and it may also be defined within the affected array to provide package-specific severity overrides [2][4][5].
Citations:
- 1: https://github.com/ossf/osv-schema/blob/main/docs/schema.md
- 2: https://ossf.github.io/osv-schema/
- 3: How to get an integer severity score from osv Rest API google/osv.dev#2643
- 4: https://google.github.io/osv.dev/data_quality.html
- 5: Severity does not support multiple differing severities for one ID ossf/osv-schema#40
🌐 Web query:
OSV vulnerability severity score field CVSS vector string
💡 Result:
In the Open Source Vulnerability (OSV) schema, vulnerability severity information is captured using the severity field, which is a JSON array of objects [1][2]. Each object within this array contains a type and a score [2]. The severity[].score property is a string that represents the quantitative severity score based on the chosen severity[].type [1][2]. When the type is a Common Vulnerability Scoring System (CVSS) format, the score field contains the corresponding CVSS vector string [1][2]. Supported CVSS types include: - CVSS_V2: Uses a CVSS vector string (version 2.0) [1][2]. - CVSS_V3: Uses a CVSS vector string (versions >= 3.0 and < 4.0, such as 3.1) [1][2]. - CVSS_V4: Uses a CVSS vector string (versions >= 4.0 and < 5.0) [1][2]. The schema additionally supports an optional source property within each severity object to specify the origin of the severity data [2]. The structure is defined both in the JSON schema and the canonical Protocol Buffer definition (vulnerability.proto) [1][3].
Citations:
- 1: https://github.com/ossf/osv-schema/blob/main/docs/schema.md
- 2: https://ossf.github.io/osv-schema/
- 3: https://github.com/ossf/osv-schema/blob/main/proto/vulnerability.proto
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' frontend/components/workbench/SeverityTag.tsx | cat -nRepository: tejasprasad2008-afk/TraceTree
Length of output: 1603
Convert OSV severity vectors before assigning to CVEResult.score. severity[].score is a CVSS vector string, so both mappers are putting a string into a number field; render paths then hit cve.score.toFixed(1) and cvssToLevel(...) with the wrong type.
frontend/components/workbench/CommandConsole.tsx#L64-L76frontend/components/workbench/CommandConsole.tsx#L174-L183
📍 Affects 1 file
frontend/components/workbench/CommandConsole.tsx#L64-L76(this comment)frontend/components/workbench/CommandConsole.tsx#L174-L183
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/components/workbench/CommandConsole.tsx` around lines 64 - 76,
Convert each OSV severity vector to a numeric CVSS score before assigning
CVEResult.score, using the existing CVSS parsing/conversion utility. Apply this
in both mapper sites at frontend/components/workbench/CommandConsole.tsx lines
64-76 and 174-183, while preserving the existing fallback behavior when severity
data is missing.
| ? [...rows].sort((a, b) => { | ||
| const av = a[sortKey]; | ||
| const bv = b[sortKey]; | ||
| const cmp = String(av ?? "").localeCompare(String(bv ?? "")); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix numeric sorting for table columns.
The default localeCompare performs an alphabetical sort, which will incorrectly sort numerical values like confidence scores or IDs (e.g., "100" will sort before "92"). Supplying the { numeric: true } option handles both text and numbers seamlessly.
💡 Proposed fix
- const cmp = String(av ?? "").localeCompare(String(bv ?? ""));
+ const cmp = String(av ?? "").localeCompare(String(bv ?? ""), undefined, { numeric: true });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const cmp = String(av ?? "").localeCompare(String(bv ?? "")); | |
| const cmp = String(av ?? "").localeCompare(String(bv ?? ""), undefined, { numeric: true }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/components/workbench/DataTable.tsx` at line 55, Update the
comparison in the table sorting logic to pass the numeric option to
String.localeCompare, ensuring numeric-looking values such as IDs and confidence
scores sort numerically while preserving text sorting behavior.
| elif syscall in ("mmap", "mprotect"): | ||
| # mprotect with PROT_EXEC on RW memory is a red flag | ||
| has_prot_exec = "PROT_EXEC" in args_raw | ||
| is_rwx = False | ||
| if has_prot_exec: | ||
| # Low base severity — shared library loading does this constantly. | ||
| # The process_injection signature captures PROT_EXEC + non-standard | ||
| # binary together, which is the real high-severity pattern. | ||
| severity = max(severity, 3.0) | ||
| syscalls_executed.append(_make_event( | ||
| syscall, | ||
| f"flags={args_raw.split(',')[0] if ',' in args_raw else args_raw[:80]}", | ||
| severity, | ||
| {"has_prot_exec": has_prot_exec}, | ||
| )) | ||
| # DYNAMIC LINKER BASELINE: Do NOT flag mprotect on library memory regions as code injection | ||
| # UNLESS it explicitly sets PROT_EXEC on a previously writable (PROT_WRITE) region. | ||
| # Non-RWX mprotect (PROT_READ|PROT_EXEC) is the dynamic linker marking .text | ||
| # segments executable — completely routine for every shared library load. | ||
| # Adding severity 1.0 per call accumulated to 27.5+ on a benign pip install | ||
| # (which loads libc, libm, libz, etc.) and falsely crossed the high-severity boost | ||
| # threshold of 15.0, adding +30 to risk_score. | ||
| if "PROT_WRITE" in args_raw: | ||
| is_rwx = True | ||
| severity = max(severity, 8.0) | ||
| if not is_root_wrapper: | ||
| suspicious_flags.append( | ||
| f"Suspicious memory protection (RWX) in PID {pid}" | ||
| ) | ||
|
|
||
| if not is_root_wrapper: | ||
| syscalls_executed.append(_make_event( | ||
| syscall, | ||
| f"flags={args_raw.split(',')[0] if ',' in args_raw else args_raw[:80]}", | ||
| severity, | ||
| {"has_prot_exec": is_rwx}, | ||
| )) | ||
| else: | ||
| severity = 0.0 | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
has_prot_exec detail key stores the wrong value and target string captures the wrong argument — both break the PROT_EXEC signature condition.
Two issues in this block:
- The emitted detail is
{"has_prot_exec": is_rwx}, butis_rwxis onlyTruewhen bothPROT_EXECandPROT_WRITEare present.monitor/signatures.py's_check_sequence_conditionfor"PROT_EXEC"readsdetails.get("has_prot_exec")expecting it to reflect plain PROT_EXEC presence per its own docstring ("PROT_EXEC" — mprotect with PROT_EXEC flag). Mislabeling it asis_rwxmeans mprotect/mmap calls withPROT_EXECalone (noPROT_WRITE) will never satisfy this detail-based check. - The fallback target string
f"flags={args_raw.split(',')[0] if ',' in args_raw else args_raw[:80]}"takes the first comma-separated token ofargs_raw, which formmap/mprotectsyscalls is the memory address argument, not the protection flags. So the_check_sequence_conditionfallbackreturn "PROT_EXEC" in target(inmonitor/signatures.py) will essentially never match either, sincetargetnever actually contains"PROT_EXEC".
Combined, this means the "PROT_EXEC" signature condition can only ever fire for RWX (PROT_EXEC+PROT_WRITE) mappings, silently missing exec-only mprotect/mmap calls — a real detection gap for a security-relevant syscall.
🐛 Suggested fix
- if not is_root_wrapper:
- syscalls_executed.append(_make_event(
- syscall,
- f"flags={args_raw.split(',')[0] if ',' in args_raw else args_raw[:80]}",
- severity,
- {"has_prot_exec": is_rwx},
- ))
+ if not is_root_wrapper:
+ prot_match = re.search(r'PROT_[A-Z|]+', args_raw)
+ syscalls_executed.append(_make_event(
+ syscall,
+ f"flags={prot_match.group(0) if prot_match else args_raw[:80]}",
+ severity,
+ {"has_prot_exec": has_prot_exec, "is_rwx": is_rwx},
+ ))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| elif syscall in ("mmap", "mprotect"): | |
| # mprotect with PROT_EXEC on RW memory is a red flag | |
| has_prot_exec = "PROT_EXEC" in args_raw | |
| is_rwx = False | |
| if has_prot_exec: | |
| # Low base severity — shared library loading does this constantly. | |
| # The process_injection signature captures PROT_EXEC + non-standard | |
| # binary together, which is the real high-severity pattern. | |
| severity = max(severity, 3.0) | |
| syscalls_executed.append(_make_event( | |
| syscall, | |
| f"flags={args_raw.split(',')[0] if ',' in args_raw else args_raw[:80]}", | |
| severity, | |
| {"has_prot_exec": has_prot_exec}, | |
| )) | |
| # DYNAMIC LINKER BASELINE: Do NOT flag mprotect on library memory regions as code injection | |
| # UNLESS it explicitly sets PROT_EXEC on a previously writable (PROT_WRITE) region. | |
| # Non-RWX mprotect (PROT_READ|PROT_EXEC) is the dynamic linker marking .text | |
| # segments executable — completely routine for every shared library load. | |
| # Adding severity 1.0 per call accumulated to 27.5+ on a benign pip install | |
| # (which loads libc, libm, libz, etc.) and falsely crossed the high-severity boost | |
| # threshold of 15.0, adding +30 to risk_score. | |
| if "PROT_WRITE" in args_raw: | |
| is_rwx = True | |
| severity = max(severity, 8.0) | |
| if not is_root_wrapper: | |
| suspicious_flags.append( | |
| f"Suspicious memory protection (RWX) in PID {pid}" | |
| ) | |
| if not is_root_wrapper: | |
| syscalls_executed.append(_make_event( | |
| syscall, | |
| f"flags={args_raw.split(',')[0] if ',' in args_raw else args_raw[:80]}", | |
| severity, | |
| {"has_prot_exec": is_rwx}, | |
| )) | |
| else: | |
| severity = 0.0 | |
| elif syscall in ("mmap", "mprotect"): | |
| # mprotect with PROT_EXEC on RW memory is a red flag | |
| has_prot_exec = "PROT_EXEC" in args_raw | |
| is_rwx = False | |
| if has_prot_exec: | |
| # DYNAMIC LINKER BASELINE: Do NOT flag mprotect on library memory regions as code injection | |
| # UNLESS it explicitly sets PROT_EXEC on a previously writable (PROT_WRITE) region. | |
| # Non-RWX mprotect (PROT_READ|PROT_EXEC) is the dynamic linker marking .text | |
| # segments executable — completely routine for every shared library load. | |
| # Adding severity 1.0 per call accumulated to 27.5+ on a benign pip install | |
| # (which loads libc, libm, libz, etc.) and falsely crossed the high-severity boost | |
| # threshold of 15.0, adding +30 to risk_score. | |
| if "PROT_WRITE" in args_raw: | |
| is_rwx = True | |
| severity = max(severity, 8.0) | |
| if not is_root_wrapper: | |
| suspicious_flags.append( | |
| f"Suspicious memory protection (RWX) in PID {pid}" | |
| ) | |
| if not is_root_wrapper: | |
| prot_match = re.search(r'PROT_[A-Z|]+', args_raw) | |
| syscalls_executed.append(_make_event( | |
| syscall, | |
| f"flags={prot_match.group(0) if prot_match else args_raw[:80]}", | |
| severity, | |
| {"has_prot_exec": has_prot_exec, "is_rwx": is_rwx}, | |
| )) | |
| else: | |
| severity = 0.0 |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@monitor/parser.py` around lines 539 - 568, Update the mmap/mprotect event
construction in the syscall parsing block so details["has_prot_exec"] stores
has_prot_exec rather than is_rwx. Also build the event target/flags string from
the protection-flags argument that contains PROT_EXEC, instead of the first
address token, so the _check_sequence_condition fallback can detect it. Preserve
the existing RWX severity and suspicious-flag behavior.
| else: | ||
| syscalls_executed.append(_make_event( | ||
| syscall, | ||
| "other", | ||
| 0.0, | ||
| {}, | ||
| )) | ||
| if not is_root_wrapper: | ||
| syscalls_executed.append(_make_event( | ||
| syscall, | ||
| "other", | ||
| 0.0, | ||
| {}, | ||
| )) | ||
|
|
||
| # Filter out remaining events from root_wrapper so they don't pollute logs | ||
| if is_root_wrapper and syscall not in ("execve", "mmap", "mprotect", "clone", "fork", "vfork"): | ||
| # Already handled some above, for others we just pop the last appended event if it was appended | ||
| # Actually, to be safer, we can just intercept them in each branch. | ||
| # But the simplest is to only add severity if not root_wrapper. | ||
| severity = 0.0 | ||
| # Remove from syscalls_executed if it was just added | ||
| if syscalls_executed and syscalls_executed[-1]["pid"] == pid and syscalls_executed[-1]["sequence_id"] == sequence_id: | ||
| syscalls_executed.pop() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
Root-wrapper event cleanup can pop an unrelated, legitimately-kept event when no event was appended this iteration.
The pop-guard at lines 757-759 relies on syscalls_executed[-1]["sequence_id"] == sequence_id, but sequence_id is a module-level counter only incremented inside _make_event. If the current root-wrapper syscall doesn't reach a branch that calls _make_event this iteration (e.g. a malformed connect/file-access line where the regex doesn't match, or any syscall hitting the "other" branch with is_root_wrapper — no new event pushed), sequence_id still equals whatever the last successfully created event's id was. If that last event's pid also happens to equal root_pid — which is true for legitimately-kept root-wrapper clone/fork/vfork events (exempted from this cleanup at their own iteration) — this check spuriously matches and pops that legitimate prior event, corrupting parent_map/process-tree data that build_cascade_graph relies on.
Also note the separate, unrelated "chown" gap: chown isn't included in the file-access elif tuple at line 516, so it falls through to this "other" branch, where the event's recorded severity is hardcoded to 0.0 even though total_severity still accumulates the 0.5 weight from SEVERITY_WEIGHTS["chown"] — an event/total-severity mismatch.
🐛 Suggested fix for the stale sequence_id pop bug
+ _pre_branch_len = len(syscalls_executed)
+
# ------------------------------------------------------------------ #
# Any other syscall — track it but don't flag
# ------------------------------------------------------------------ #
else:
if not is_root_wrapper:
syscalls_executed.append(_make_event(
syscall,
"other",
- 0.0,
+ severity,
{},
))
# Filter out remaining events from root_wrapper so they don't pollute logs
- if is_root_wrapper and syscall not in ("execve", "mmap", "mprotect", "clone", "fork", "vfork"):
+ if (is_root_wrapper
+ and syscall not in ("execve", "mmap", "mprotect", "clone", "fork", "vfork")
+ and len(syscalls_executed) > _pre_branch_len):
severity = 0.0
- if syscalls_executed and syscalls_executed[-1]["pid"] == pid and syscalls_executed[-1]["sequence_id"] == sequence_id:
- syscalls_executed.pop()
+ syscalls_executed.pop()Also add "chown" to the file-access tuple at line 516 so it gets proper sensitive-path evaluation instead of falling into the generic "other" bucket.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@monitor/parser.py` around lines 742 - 759, Fix root-wrapper cleanup in the
syscall event-processing flow so it removes an event only when the current
iteration actually appended one; do not rely on the module-level sequence_id,
which may refer to an older event. Preserve legitimate root-wrapper
clone/fork/vfork events. Also add chown to the file-access syscall tuple so it
uses sensitive-path evaluation and records severity consistently with
total_severity.
|
|
||
| /* TODO: Written 2026-07-15 to unblock LummaStealer PS1-dropper end-to-end | ||
| verification test. Needs a deliberate pass before relying on it as a | ||
| real detection rule: false-positive rate unvalidated (BITSTransfer is | ||
| used by legitimate Windows software), pattern coverage narrow | ||
| (only covers BITSTransfer+RunKey variant; omits mshta, wscript, regsvr32, | ||
| certutil, and other common PS1 dropper LOLBins). */ | ||
| rule PowerShellDropper { | ||
| meta: | ||
| description = "Detects PowerShell download-execute-persist dropper pattern" | ||
| severity = "critical" | ||
| mitre = "T1059.001,T1547.001,T1197" | ||
| strings: | ||
| $bits = "BITSTransfer" nocase | ||
| $bits2 = "Start-BitsTransfer" nocase | ||
| $reg_run = "CurrentVersion\\Run" nocase | ||
| $appdata_exe = /APPDATA.*\.exe/ nocase | ||
| $expand_archive = "Expand-Archive" nocase | ||
| $start_process = "Start-Process" nocase | ||
| condition: | ||
| ($bits or $bits2) and $reg_run | ||
| or ($appdata_exe and $expand_archive and $start_process) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Self-flagged unvalidated rule shipped into the production ruleset.
The TODO explicitly states the rule "Needs a deliberate pass before relying on it as a real detection rule: false-positive rate unvalidated (BITSTransfer is used by legitimate Windows software), pattern coverage narrow". As written, PowerShellDropper participates in scan_with_yara's normal match path and will surface as a "critical" severity finding (feeding into YaraAnalyzer's MALICIOUS verdict) despite the author's own acknowledgment that it's unvalidated.
Would you like this gated behind a lower initial severity (or excluded from the compiled ruleset) until the false-positive rate is validated? I can open a tracking issue if useful.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@monitor/yara.py` around lines 156 - 178, Remove PowerShellDropper from the
production-compiled YARA ruleset, or gate it behind the project’s existing
experimental/unvalidated-rule mechanism so scan_with_yara cannot report it
through the normal critical-severity path. Preserve the rule definition for
end-to-end verification, and ensure YaraAnalyzer does not classify its matches
as MALICIOUS until validation is complete.
| instances = [ | ||
| instance | ||
| for string_match in m.strings | ||
| for instance in string_match.instances | ||
| ] | ||
| results.append({ | ||
| "rule_name": m.rule, | ||
| "severity": m.meta.get("severity", "unknown"), | ||
| "description": m.meta.get("description", ""), | ||
| "file_path": str(fpath), | ||
| "matched_strings": [ | ||
| instance.matched_data.decode('utf-8', errors='replace') | ||
| for string_match in m.strings | ||
| for instance in string_match.instances | ||
| inst.matched_data.decode('utf-8', errors='replace') | ||
| for inst in instances | ||
| ], | ||
| # Parallel field preserving byte offsets for static analysis. | ||
| # matched_strings kept as plain strings for backward compat. | ||
| "matched_offsets": [ | ||
| {"text": inst.matched_data.decode('utf-8', errors='replace'), | ||
| "offset": inst.offset} | ||
| for inst in instances | ||
| ], | ||
| }) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check pinned yara-python version across manifest files.
rg -n 'yara-python' pyproject.toml setup.py requirements*.txt 2>/dev/nullRepository: tejasprasad2008-afk/TraceTree
Length of output: 167
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== candidate files ==\n'
git ls-files | rg '(^|/)(pyproject\.toml|setup\.py|requirements.*\.txt|Pipfile|poetry\.lock|uv\.lock|monitor/yara\.py)$'
printf '\n== yara references ==\n'
rg -n 'yara|StringMatch|matched_data|fallback_regex_scan|scan_with_yara' .
printf '\n== monitor/yara.py outline ==\n'
ast-grep outline monitor/yara.py --view expanded
printf '\n== monitor/yara.py relevant section ==\n'
sed -n '1,320p' monitor/yara.pyRepository: tejasprasad2008-afk/TraceTree
Length of output: 27250
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files | rg '(^|/)(pyproject\.toml|setup\.py|requirements.*\.txt|Pipfile|poetry\.lock|uv\.lock|monitor/yara\.py)$'
rg -n 'yara|StringMatch|matched_data|fallback_regex_scan|scan_with_yara' .
sed -n '1,320p' monitor/yara.pyRepository: tejasprasad2008-afk/TraceTree
Length of output: 26713
🏁 Script executed:
#!/bin/bash
set -euo pipefail
pwd
git ls-files | head -n 50Repository: tejasprasad2008-afk/TraceTree
Length of output: 1696
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== pyproject excerpt ==\n'
rg -n -C 3 'yara|dependencies|install_requires|packages' pyproject.toml setup.py requirements-ingest.txt
printf '\n== full dependency blocks ==\n'
python3 - <<'PY'
from pathlib import Path
for path in ["pyproject.toml", "setup.py", "requirements-ingest.txt"]:
p = Path(path)
if p.exists():
print(f"\n--- {path} ---")
text = p.read_text(encoding="utf-8", errors="replace").splitlines()
for i, line in enumerate(text, 1):
if any(k in line.lower() for k in ("yara", "dependencies", "install_requires", "extras_require")):
start = max(1, i-4)
end = min(len(text), i+12)
for j in range(start, end+1):
print(f"{j}:{text[j-1]}")
print()
PY
printf '\n== scan_with_yara control flow ==\n'
sed -n '186,250p' monitor/yara.pyRepository: tejasprasad2008-afk/TraceTree
Length of output: 4965
Support both YARA APIs or pin yara-python to 4.3+
monitor/yara.py:222-243 assumes Match.strings yields StringMatch objects. Older yara-python releases return tuples here, so string_match.instances / inst.matched_data will fail and the scan can return no YARA findings for every file. Either pin the dependency or handle both result shapes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@monitor/yara.py` around lines 222 - 243, Update the YARA match processing
that builds instances and matched_offsets to support both modern StringMatch
objects and legacy tuple results from yara-python, or enforce yara-python 4.3+
through the project dependency configuration. Preserve matched_strings and
byte-offset output for supported API shapes, ensuring scans do not fail when
older results are returned.
| except Exception as _qe: | ||
| log.warning("Quarantine extraction failed (non-fatal): %s", _qe) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP '^\s*(import logging|from logging|log\s*=|import\s+.*\bas\s+log\b)' sandbox/sandbox.pyRepository: tejasprasad2008-afk/TraceTree
Length of output: 167
log is undefined here — the quarantine fallback can crash instead of staying non-fatal.
sandbox/sandbox.py only uses console in this module, so log.warning(...) raises NameError inside the except block. Use the in-scope output path (console.print(...)) or a real logger.
🧰 Tools
🪛 Ruff (0.15.21)
[warning] 652-652: Do not catch blind exception: Exception
(BLE001)
[error] 653-653: Undefined name log
(F821)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sandbox/sandbox.py` around lines 652 - 653, Replace the undefined log.warning
call in the quarantine extraction exception handler with the module’s in-scope
console.print output path, preserving the error message and ensuring this
fallback remains non-fatal.
Source: Linters/SAST tools
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
sandbox/sandbox.py (2)
665-671: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftBound quarantine archive size before writing and extracting it.
The archive comes from attacker-controlled ZIP contents, but this copies the entire tar to the host and extracts it without byte, file-count, or member-count limits. A decompression bomb or archive containing millions of files can exhaust host disk or memory before the non-fatal handler runs. Enforce bounded streaming and extraction limits.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sandbox/sandbox.py` around lines 665 - 671, Bound the quarantine archive handling around _extract_tar_safely: while streaming q_stream into q_tar_tmp, track bytes and abort once a defined maximum archive size is exceeded, and enforce limits on extracted file count and total extracted bytes (including tar members) before writing files. Ensure temporary resources are cleaned up and the existing non-fatal error path handles limit violations.
665-672: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winClean up the temporary quarantine tar on failure.
q_tar_tmpis removed only after successful parsing and extraction. Exceptions leave temporary archives in the system temp directory, allowing repeated malformed scans to accumulate disk usage. Remove the file in afinallyblock.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sandbox/sandbox.py` around lines 665 - 672, Wrap the temporary quarantine tar download, parsing, and extraction flow around q_tar_tmp in a finally block, and remove q_tar_tmp.name there so cleanup occurs on both success and failure. Preserve the existing archive processing behavior while ensuring cleanup also handles exceptions from get_archive, tarfile.open, or _extract_tar_safely.cli.py (1)
308-313: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winGuard
TRACETREE_QUARANTINE_TTL_DAYSagainst non-numeric values.
int(os.environ.get("TRACETREE_QUARANTINE_TTL_DAYS", max_age_days))raisesValueErrorwhen the env var is set to a non-integer. Since_purge_stale_quarantine()runs at the top ofperform_analysis, a malformed value crashes everyanalyzeinvocation rather than degrading gracefully.🛡️ Proposed guard
- ttl = int(os.environ.get("TRACETREE_QUARANTINE_TTL_DAYS", max_age_days)) + try: + ttl = int(os.environ.get("TRACETREE_QUARANTINE_TTL_DAYS", max_age_days)) + except (TypeError, ValueError): + ttl = max_age_days🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cli.py` around lines 308 - 313, Update the TTL parsing in _purge_stale_quarantine so a non-numeric TRACETREE_QUARANTINE_TTL_DAYS value does not raise ValueError or abort perform_analysis. Catch the conversion failure and fall back to max_age_days, preserving the existing cutoff and quarantine cleanup behavior for valid values.
🧹 Nitpick comments (1)
orchestrator/src/llm/index.ts (1)
234-240: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNormalize the provider name configuration to lowercase.
Users might mistakenly supply uppercase or mixed-case values for the
LLM_PROVIDERenvironment variable (e.g.,OpenAIinstead ofopenai). Using.toLowerCase()will make the configuration more resilient and prevent fallback to the default mock provider or throwing an unsupported provider error.💡 Proposed refactor
- const provider = process.env.LLM_PROVIDER || 'mock'; + const provider = (process.env.LLM_PROVIDER || 'mock').toLowerCase(); logger.info('llm', `Initializing provider: ${provider}`);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@orchestrator/src/llm/index.ts` around lines 234 - 240, Normalize the LLM_PROVIDER value to lowercase when assigning provider, while preserving the existing mock fallback when the environment variable is unset. Keep the logger.info initialization flow unchanged so mixed-case provider names are passed consistently to downstream provider selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/main.py`:
- Around line 120-133: Update _update_env_file to restrict the persisted .env
file to owner-only permissions after path.write_text completes, applying the
mode for both newly created and existing files without changing the current
content-update behavior.
In `@frontend/components/workbench/FirstRunSetup.tsx`:
- Line 64: Update the displayStep calculation in FirstRunSetup to handle step
=== 1 by mapping it to visual progress step 2, while preserving the existing
mappings for steps 0, 2, and 3.
---
Outside diff comments:
In `@cli.py`:
- Around line 308-313: Update the TTL parsing in _purge_stale_quarantine so a
non-numeric TRACETREE_QUARANTINE_TTL_DAYS value does not raise ValueError or
abort perform_analysis. Catch the conversion failure and fall back to
max_age_days, preserving the existing cutoff and quarantine cleanup behavior for
valid values.
In `@sandbox/sandbox.py`:
- Around line 665-671: Bound the quarantine archive handling around
_extract_tar_safely: while streaming q_stream into q_tar_tmp, track bytes and
abort once a defined maximum archive size is exceeded, and enforce limits on
extracted file count and total extracted bytes (including tar members) before
writing files. Ensure temporary resources are cleaned up and the existing
non-fatal error path handles limit violations.
- Around line 665-672: Wrap the temporary quarantine tar download, parsing, and
extraction flow around q_tar_tmp in a finally block, and remove q_tar_tmp.name
there so cleanup occurs on both success and failure. Preserve the existing
archive processing behavior while ensuring cleanup also handles exceptions from
get_archive, tarfile.open, or _extract_tar_safely.
---
Nitpick comments:
In `@orchestrator/src/llm/index.ts`:
- Around line 234-240: Normalize the LLM_PROVIDER value to lowercase when
assigning provider, while preserving the existing mock fallback when the
environment variable is unset. Keep the logger.info initialization flow
unchanged so mixed-case provider names are passed consistently to downstream
provider selection.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2c034779-9a20-4cea-a32e-45c9df3d8397
📒 Files selected for processing (13)
.env.example.gitignoreREADME.mdapi/main.pycli.pyfrontend/app/page.tsxfrontend/components/workbench/FirstRunSetup.tsxorchestrator/ai_mesh.pyorchestrator/src/llm/index.test.tsorchestrator/src/llm/index.tsorchestrator/src/server.tsorchestrator/src/store/index.tssandbox/sandbox.py
🚧 Files skipped from review as they are similar to previous changes (2)
- .gitignore
- frontend/app/page.tsx
| def _update_env_file(path: Path, values: Dict[str, str]) -> None: | ||
| """Update selected values without echoing secrets or replacing unrelated config.""" | ||
| existing = path.read_text() if path.exists() else "" | ||
| lines = existing.splitlines() | ||
| pending = dict(values) | ||
| output: List[str] = [] | ||
| for line in lines: | ||
| key = line.split("=", 1)[0].strip() if "=" in line else "" | ||
| if key in pending: | ||
| output.append(f"{key}={pending.pop(key)}") | ||
| else: | ||
| output.append(line) | ||
| output.extend(f"{key}={value}" for key, value in pending.items()) | ||
| path.write_text("\n".join(output).rstrip() + "\n") |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Restrict .env permissions after writing provider secrets.
_update_env_file persists API keys (e.g. OPENAI_API_KEY) via write_text, which on first creation leaves default (typically world-readable 0644) permissions. Since this file holds cloud credentials, tighten it to owner-only after writing.
🔒 Proposed hardening
output.extend(f"{key}={value}" for key, value in pending.items())
path.write_text("\n".join(output).rstrip() + "\n")
+ try:
+ path.chmod(0o600)
+ except OSError:
+ pass📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _update_env_file(path: Path, values: Dict[str, str]) -> None: | |
| """Update selected values without echoing secrets or replacing unrelated config.""" | |
| existing = path.read_text() if path.exists() else "" | |
| lines = existing.splitlines() | |
| pending = dict(values) | |
| output: List[str] = [] | |
| for line in lines: | |
| key = line.split("=", 1)[0].strip() if "=" in line else "" | |
| if key in pending: | |
| output.append(f"{key}={pending.pop(key)}") | |
| else: | |
| output.append(line) | |
| output.extend(f"{key}={value}" for key, value in pending.items()) | |
| path.write_text("\n".join(output).rstrip() + "\n") | |
| def _update_env_file(path: Path, values: Dict[str, str]) -> None: | |
| """Update selected values without echoing secrets or replacing unrelated config.""" | |
| existing = path.read_text() if path.exists() else "" | |
| lines = existing.splitlines() | |
| pending = dict(values) | |
| output: List[str] = [] | |
| for line in lines: | |
| key = line.split("=", 1)[0].strip() if "=" in line else "" | |
| if key in pending: | |
| output.append(f"{key}={pending.pop(key)}") | |
| else: | |
| output.append(line) | |
| output.extend(f"{key}={value}" for key, value in pending.items()) | |
| path.write_text("\n".join(output).rstrip() + "\n") | |
| try: | |
| path.chmod(0o600) | |
| except OSError: | |
| pass |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api/main.py` around lines 120 - 133, Update _update_env_file to restrict the
persisted .env file to owner-only permissions after path.write_text completes,
applying the mode for both newly created and existing files without changing the
current content-update behavior.
| const selected = PROVIDERS.find((item) => item.id === provider)!; | ||
| const apiBase = process.env.NEXT_PUBLIC_TRACETREE_API_URL || "http://127.0.0.1:8000"; | ||
| const totalSteps = selected.requiresApiKey ? 4 : 3; | ||
| const displayStep = step === 0 ? 1 : step === 2 ? (selected.requiresApiKey ? 3 : 2) : step === 3 ? totalSteps : 0; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix progress step calculation for the API key step.
When a provider requires an API key, the setup flows to step === 1. The current ternary logic does not handle step === 1, causing displayStep to fall back to 0. This results in the UI displaying "STEP 0 / 4" and coloring zero segments in the progress bar on this screen.
Add a condition for step === 1 to correctly map it to step 2 visually.
🐛 Proposed fix
- const displayStep = step === 0 ? 1 : step === 2 ? (selected.requiresApiKey ? 3 : 2) : step === 3 ? totalSteps : 0;
+ const displayStep = step === 0 ? 1 : step === 1 ? 2 : step === 2 ? (selected.requiresApiKey ? 3 : 2) : step === 3 ? totalSteps : 0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const displayStep = step === 0 ? 1 : step === 2 ? (selected.requiresApiKey ? 3 : 2) : step === 3 ? totalSteps : 0; | |
| const displayStep = step === 0 ? 1 : step === 1 ? 2 : step === 2 ? (selected.requiresApiKey ? 3 : 2) : step === 3 ? totalSteps : 0; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/components/workbench/FirstRunSetup.tsx` at line 64, Update the
displayStep calculation in FirstRunSetup to handle step === 1 by mapping it to
visual progress step 2, while preserving the existing mappings for steps 0, 2,
and 3.
What changed
Adds the TraceTree Workbench dashboard and its supporting API, orchestration, analyzer, model, YARA, sandbox, and test wiring.
The dashboard provides:
Why
This branch records the dashboard feature developed during the Codex Hackathon as a single reviewable timeline, without merging it into
mainduring the event.Validation
npm run buildinfrontend/✅pytest.Local testing
git clone --branch codex/dashboard-hackathon https://github.com/tejasprasad2008-afk/TraceTree.git cd TraceTree cascade-analyze dashboardFor controlled malware-research samples, use an authorized isolated environment and follow the access requirements at MalwareBazaar. Do not download or execute unknown samples on a personal host.
Scope exclusions
This PR intentionally excludes the incomplete macOS app, local runtime state, generated artifacts, virtual environments, sample/model binaries, and experimental scripts.
Summary by CodeRabbit