From edbb8fd908235d2c29cff230f949686c6fa9dff6 Mon Sep 17 00:00:00 2001 From: Samir Hanna Verza Date: Tue, 6 Oct 2026 19:24:21 -0300 Subject: [PATCH] fix(core): treat a cloned repository as a source-aware (white-box) scan A `repository` target is cloned into the sandbox workspace and collected into `local_sources` exactly like a `local_code` target, but the two white-box checks only looked at `type == "local_code"`. Scanning a GitHub repo, alone or beside a domain, therefore ran with is_whitebox=False: the source-aware skills were not preloaded for the root agent (coordination/source_aware_whitebox is not loadable on demand) and telemetry recorded the scan as black-box. The docs describe this invocation as white-box testing. Regressed in 1e641e5, which replaced `bool(args.local_sources)` with the `local_code` check. Add one `is_whitebox_scan` in a dependency-free `strix.core.targets` (the interface layer imports it before the agents SDK may be imported, so it cannot live in core.inputs) and use it from both the runner and scan setup. A repository that was not cloned still does not count, as it has no source. Tests: unit cases for each target mix, and a runner-level case asserting the root agent is built with is_whitebox=True for a cloned repository (it fails on main for the repository cases and passes for the local_code and web controls). Fixes #884. Supersedes #885, which no longer applies to main. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb --- strix/core/runner.py | 3 +- strix/core/targets.py | 26 ++++++++++++++++++ strix/interface/scan_setup.py | 2 +- strix/interface/utils.py | 5 ---- tests/test_runner_root_prompt.py | 46 +++++++++++++++++++++++++++++++ tests/test_scan_targets.py | 47 ++++++++++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 strix/core/targets.py create mode 100644 tests/test_scan_targets.py diff --git a/strix/core/runner.py b/strix/core/runner.py index f43b1d7f..32baf762 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -44,6 +44,7 @@ from strix.core.inputs import ( ) from strix.core.paths import run_dir_for, runtime_state_dir from strix.core.sessions import open_agent_session +from strix.core.targets import is_whitebox_scan from strix.report.state import get_global_report_state from strix.runtime import session_manager from strix.telemetry import set_scan_phase @@ -363,7 +364,7 @@ async def run_strix_scan( try: targets = scan_config.get("targets") or [] scan_mode = str(scan_config.get("scan_mode") or "deep") - is_whitebox = any(t.get("type") == "local_code" for t in targets) + is_whitebox = is_whitebox_scan(targets) diff_scope = scan_config.get("diff_scope") is_diff_scoped = bool(isinstance(diff_scope, dict) and diff_scope.get("active")) skills = list(scan_config.get("skills") or []) diff --git a/strix/core/targets.py b/strix/core/targets.py new file mode 100644 index 00000000..189625d5 --- /dev/null +++ b/strix/core/targets.py @@ -0,0 +1,26 @@ +"""Helpers that read the resolved scan targets. + +Kept free of heavy imports on purpose: the interface layer needs them before the +agents SDK may be imported. +""" + +from __future__ import annotations + +from typing import Any + + +def is_whitebox_scan(targets_info: list[dict[str, Any]] | None) -> bool: + """True when any target puts source code in front of the agent (a source-aware scan). + + That is a local directory, and also a repository that was cloned for the scan: both + end up in the sandbox workspace, as ``collect_local_sources`` in + ``strix.interface.utils`` collects them. A repository that was not cloned has no + source to read, so it does not count. + """ + for target in targets_info or []: + target_type = target.get("type") + if target_type == "local_code": + return True + if target_type == "repository" and (target.get("details") or {}).get("cloned_repo_path"): + return True + return False diff --git a/strix/interface/scan_setup.py b/strix/interface/scan_setup.py index 0af126d0..8264ab82 100644 --- a/strix/interface/scan_setup.py +++ b/strix/interface/scan_setup.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any from strix.config import Settings, codex, load_settings from strix.core.paths import run_dir_for +from strix.core.targets import is_whitebox_scan from strix.interface.utils import ( assign_workspace_subdirs, clone_repository, @@ -25,7 +26,6 @@ from strix.interface.utils import ( derive_local_base_name, generate_run_name, infer_target_type, - is_whitebox_scan, read_target_list_file, resolve_diff_scope_context, rewrite_localhost_targets, diff --git a/strix/interface/utils.py b/strix/interface/utils.py index 7ab55d8e..e09a80e3 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -1305,11 +1305,6 @@ def assign_workspace_subdirs(targets_info: list[dict[str, Any]]) -> None: details["workspace_subdir"] = workspace_subdir -def is_whitebox_scan(targets_info: list[dict[str, Any]]) -> bool: - """True iff any target is a local source tree (whitebox / source-aware).""" - return any(t.get("type") == "local_code" for t in targets_info or []) - - def collect_local_sources(targets_info: list[dict[str, Any]]) -> list[dict[str, Any]]: local_sources: list[dict[str, Any]] = [] diff --git a/tests/test_runner_root_prompt.py b/tests/test_runner_root_prompt.py index 0e24908d..0c50258c 100644 --- a/tests/test_runner_root_prompt.py +++ b/tests/test_runner_root_prompt.py @@ -187,6 +187,52 @@ async def test_root_prompt_options_default_to_none( assert kwargs["system_prompt_context"] == {"scope": "built-in"} +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("targets", "expected"), + [ + pytest.param( + [{"type": "repository", "details": {"cloned_repo_path": "/work/clones/repo"}}], + True, + id="cloned-repository", + ), + pytest.param( + [ + {"type": "web_application", "details": {}}, + {"type": "repository", "details": {"cloned_repo_path": "/work/clones/repo"}}, + ], + True, + id="repository-beside-a-domain", + ), + pytest.param( + [{"type": "local_code", "details": {"target_path": "/src/app"}}], + True, + id="local-directory", + ), + pytest.param([{"type": "web_application", "details": {}}], False, id="web-only"), + ], +) +async def test_root_agent_is_whitebox_when_source_is_in_the_workspace( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Any, + targets: list[dict[str, Any]], + *, + expected: bool, +) -> None: + """A cloned repository is source-aware like a local directory, so the root agent gets + the source-aware skills; the runner used to look at ``local_code`` targets only.""" + captured = _patch_engine_scaffold(monkeypatch, tmp_path, {"scope": "built-in"}) + + await runner.run_strix_scan( + scan_config={"targets": targets, "scan_mode": "deep"}, + scan_id="scan-whitebox", + image="img", + coordinator=AgentCoordinator(), + ) + + assert captured["kwargs"]["is_whitebox"] is expected + + @pytest.mark.asyncio async def test_mcp_available_flag_set_when_a_connection_attaches( monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_scan_targets.py b/tests/test_scan_targets.py new file mode 100644 index 00000000..0ba718e7 --- /dev/null +++ b/tests/test_scan_targets.py @@ -0,0 +1,47 @@ +"""Which scans count as source-aware (white-box).""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from strix.core.targets import is_whitebox_scan + + +def _web() -> dict[str, Any]: + return {"type": "web_application", "details": {"target_url": "https://app.example"}} + + +def _local() -> dict[str, Any]: + return {"type": "local_code", "details": {"target_path": "/src/app"}} + + +def _repo(*, cloned: bool = True) -> dict[str, Any]: + details: dict[str, Any] = {"target_repo": "https://github.com/org/repo"} + if cloned: + details["cloned_repo_path"] = "/work/clones/repo" + return {"type": "repository", "details": details} + + +@pytest.mark.parametrize( + ("targets", "expected"), + [ + pytest.param([], False, id="no-targets"), + pytest.param(None, False, id="targets-missing"), + pytest.param([_web()], False, id="web-only"), + pytest.param([_local()], True, id="local-directory"), + pytest.param([_repo()], True, id="cloned-repository"), + pytest.param([_repo(cloned=False)], False, id="repository-that-was-not-cloned"), + pytest.param([_repo(), _web()], True, id="repository-beside-a-domain"), + pytest.param([_web(), _repo()], True, id="domain-before-repository"), + ], +) +def test_source_in_front_of_the_agent_makes_the_scan_whitebox( + targets: list[dict[str, Any]] | None, *, expected: bool +) -> None: + assert is_whitebox_scan(targets) is expected + + +def test_a_target_without_details_does_not_break_the_check() -> None: + assert is_whitebox_scan([{"type": "repository"}]) is False