feat(logging): per-scan `{run_dir}/strix.log` with scan/agent context tagging

Every scan now writes a complete log file at ``{run_dir}/strix.log``
captured from the moment ``run_dir`` is resolved through teardown.
Stdlib ``logging`` only — no parallel framework.

New ``strix/telemetry/logging.py``:
  * ``setup_scan_logging(run_dir, debug=)`` attaches a ``FileHandler``
    (DEBUG, all ``strix.*``) plus a ``StreamHandler`` (ERROR by
    default; DEBUG via ``STRIX_DEBUG=1``).
  * ``ContextVar``-backed ``scan_id`` and ``agent_id`` injected by a
    ``Filter`` so every line is auto-tagged across asyncio tasks
    without callers passing them explicitly.
  * Third-party noise (``httpx``, ``litellm``, ``openai``,
    ``anthropic``, ``urllib3``, ``httpcore``) capped at WARNING.
  * Returns a teardown handle for ``finally`` cleanup.

Wiring:
  * ``orchestration/scan.py`` calls ``setup_scan_logging`` once per
    scan after ``run_dir`` resolves; sets scan_id; tears down in
    ``finally``. Adds INFO logs for sandbox bring-up + scan
    start/end.
  * ``orchestration/hooks.py`` sets/clears ``agent_id`` ContextVar in
    ``on_agent_start`` / ``on_agent_end`` and emits INFO for agent
    lifecycle, DEBUG for every tool start/end and LLM call.
  * ``interface/main.py`` drops the ``setLevel(ERROR)`` silencer.

Coverage expanded across ~20 files (orchestration, agents, runtime,
llm, tools, interface, config, skills) with INFO for lifecycle and
DEBUG for verbose detail. Per the system instructions in
``logger.warning(f"…{e}")`` were converted to module logger calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 23:35:01 -07:00
parent 9d7f754b59
commit 46ff025209
22 changed files with 415 additions and 34 deletions
+17
View File
@@ -550,6 +550,15 @@ async def create_agent(
async with bus._lock:
bus.tasks[child_id] = task_handle
logger.info(
"create_agent: spawned %s (%s) parent=%s skills=%d task_len=%d",
child_id,
name,
parent_id or "-",
len(skills or []),
len(task or ""),
)
return json.dumps(
{
"success": True,
@@ -659,6 +668,14 @@ async def agent_finish(
)
parent_notified = True
logger.info(
"agent_finish: %s success=%s findings=%d parent_notified=%s",
me,
success,
len(findings or []),
parent_notified,
)
return json.dumps(
{
"success": True,
+10 -3
View File
@@ -59,14 +59,21 @@ def _do_finish(
technical_analysis=technical_analysis.strip(),
recommendations=recommendations.strip(),
)
vuln_count = len(tracer.vulnerability_reports)
except (ImportError, AttributeError) as e:
logger.exception("finish_scan persistence failed")
return {"success": False, "message": f"Failed to complete scan: {e!s}"}
else:
logger.info(
"finish_scan: completed scan with %d vulnerability report(s)",
vuln_count,
)
return {
"success": True,
"scan_completed": True,
"message": "Scan completed successfully",
"vulnerabilities_found": len(tracer.vulnerability_reports),
"vulnerabilities_found": vuln_count,
}
except (ImportError, AttributeError) as e:
return {"success": False, "message": f"Failed to complete scan: {e!s}"}
@function_tool(timeout=60)
+1
View File
@@ -247,6 +247,7 @@ async def python_action(
sentinel = "__STRIX_PY_RESULT_" + uuid.uuid4().hex + "__"
user_code_b64 = base64.b64encode(code.encode("utf-8")).decode("ascii")
driver = _DRIVER_TEMPLATE.format(sentinel=sentinel, user_code_b64=user_code_b64)
logger.info("python_action: invoking driver (code_len=%d, timeout=%ds)", len(code), timeout)
# /tmp inside the sandbox container is single-user (pentester) and
# disposable per scan; the multi-user race B108/S108 warns about
# doesn't apply.
+8
View File
@@ -275,8 +275,16 @@ async def _do_create( # noqa: PLR0912
code_locations=parsed_locations,
)
except (ImportError, AttributeError) as e:
logger.exception("create_vulnerability_report persistence failed")
return {"success": False, "message": f"Failed to create vulnerability report: {e!s}"}
else:
logger.info(
"Vulnerability report created: id=%s severity=%s cvss=%.1f title=%s",
report_id,
severity,
cvss_score,
title,
)
return {
"success": True,
"message": f"Vulnerability report '{title}' created successfully",
+11 -1
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
import json
import logging
from typing import Any
import requests
@@ -12,6 +13,9 @@ from agents import RunContextWrapper, function_tool
from strix.config import load_settings
logger = logging.getLogger(__name__)
_SYSTEM_PROMPT = """You are assisting a cybersecurity agent specialized in vulnerability scanning
and security assessment running on Kali Linux. When responding to search queries:
@@ -40,11 +44,13 @@ security implications and details."""
def _do_search(query: str) -> dict[str, Any]:
api_key = load_settings().integrations.perplexity_api_key
if not api_key:
logger.warning("web_search invoked without PERPLEXITY_API_KEY configured")
return {
"success": False,
"message": "PERPLEXITY_API_KEY environment variable not set",
"results": [],
}
logger.info("web_search query (len=%d): %s", len(query), query[:120])
url = "https://api.perplexity.ai/chat/completions"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
@@ -61,16 +67,20 @@ def _do_search(query: str) -> dict[str, Any]:
response.raise_for_status()
content = response.json()["choices"][0]["message"]["content"]
except requests.exceptions.Timeout:
logger.warning("web_search timed out")
return {"success": False, "message": "Request timed out", "results": []}
except requests.exceptions.RequestException as e:
logger.exception("web_search API request failed")
return {"success": False, "message": f"API request failed: {e!s}", "results": []}
except KeyError as e:
logger.exception("web_search response shape unexpected")
return {
"success": False,
"message": f"Unexpected API response format: missing {e!s}",
"results": [],
}
except Exception as e: # noqa: BLE001
except Exception as e:
logger.exception("web_search failed")
return {"success": False, "message": f"Web search failed: {e!s}", "results": []}
else:
return {