diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja index 36800a8f..732cbb6d 100644 --- a/strix/agents/prompts/system_prompt.jinja +++ b/strix/agents/prompts/system_prompt.jinja @@ -77,7 +77,7 @@ SYSTEM-VERIFIED SCOPE: AUTHORIZED TARGETS: {% for target in system_prompt_context.authorized_targets %} {% if target.type == "web_host" %} -- web_host: {{ target.value }} (includes {{ target.value }} and *.{{ target.value }}) +- host: {{ target.value }} (includes {{ target.value }} and *.{{ target.value }}) {% elif target.type == "ip_address" %} - ip_address: {{ target.value }} (exact address) {% else %} diff --git a/strix/core/inputs.py b/strix/core/inputs.py index 3206350e..aaf034f4 100644 --- a/strix/core/inputs.py +++ b/strix/core/inputs.py @@ -268,6 +268,21 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]: } +def build_scope_target_labels(targets: list[dict[str, Any]]) -> list[str]: + """Build concise, deduplicated scope labels for CLI summaries.""" + labels: list[str] = [] + for target in build_scope_context({"targets": targets})["authorized_targets"]: + ttype = target["type"] + value = target["value"] + if ttype == "web_host": + labels.append(f"host: {value} (includes *.{value})") + elif ttype == "ip_address": + labels.append(f"ip: {value} (exact address)") + else: + labels.append(f"{ttype}: {value}") + return labels + + def make_model_settings( reasoning_effort: ReasoningEffort | None, *, diff --git a/strix/interface/cli.py b/strix/interface/cli.py index 42945c22..cfe6bb7a 100644 --- a/strix/interface/cli.py +++ b/strix/interface/cli.py @@ -20,6 +20,7 @@ from strix.runtime import session_manager from .utils import ( build_live_stats_text, + build_target_summary_text, format_vulnerability_report, has_model_response, read_workspace_files, @@ -44,16 +45,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915 start_text = Text() start_text.append("Penetration test initiated", style="bold #22c55e") - target_text = Text() - target_text.append("Target", style="dim") - target_text.append(" ") - if len(args.targets_info) == 1: - target_text.append(args.targets_info[0]["original"], style="bold white") - else: - target_text.append(f"{len(args.targets_info)} targets", style="bold white") - for target_info in args.targets_info: - target_text.append("\n ") - target_text.append(target_info["original"], style="white") + target_text = build_target_summary_text(args.targets_info) results_text = Text() results_text.append("Output", style="dim") diff --git a/strix/interface/main.py b/strix/interface/main.py index 06966f4c..2323d413 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -40,6 +40,7 @@ from strix.interface.update_check import ( ) from strix.interface.utils import ( build_final_stats_text, + build_target_summary_text, ) from strix.telemetry import posthog, scarf from strix.telemetry.logging import configure_dependency_logging @@ -270,16 +271,7 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) -> else: completion_text.append("SESSION ENDED", style="bold #eab308") - target_text = Text() - target_text.append("Target", style="dim") - target_text.append(" ") - if len(args.targets_info) == 1: - target_text.append(args.targets_info[0]["original"], style="bold white") - else: - target_text.append(f"{len(args.targets_info)} targets", style="bold white") - for target_info in args.targets_info: - target_text.append("\n ") - target_text.append(target_info["original"], style="white") + target_text = build_target_summary_text(args.targets_info) stats_text = build_final_stats_text(report_state) diff --git a/strix/interface/utils.py b/strix/interface/utils.py index 6789abe0..51747f80 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -21,12 +21,29 @@ from rich.panel import Panel from rich.text import Text from strix.config import load_settings +from strix.core.inputs import build_scope_target_labels from strix.utils.api_spec import detect_spec_format logger = logging.getLogger(__name__) +def build_target_summary_text(targets_info: list[dict[str, Any]]) -> Text: + """Render configured targets as their deduplicated prompt-level scope.""" + labels = build_scope_target_labels(targets_info) + target_text = Text() + target_text.append("Target", style="dim") + target_text.append(" ") + if len(labels) == 1: + target_text.append(labels[0], style="bold white") + else: + target_text.append(f"{len(labels)} targets", style="bold white") + for label in labels: + target_text.append("\n ") + target_text.append(label, style="white") + return target_text + + def get_severity_color(severity: str) -> str: severity_colors = { "critical": "#dc2626", diff --git a/tests/test_inputs.py b/tests/test_inputs.py index aa43be1a..8f8bb6a8 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -12,9 +12,11 @@ from strix.agents.prompt import render_system_prompt from strix.core.inputs import ( build_root_task, build_scope_context, + build_scope_target_labels, child_initial_input, make_model_settings, ) +from strix.interface.utils import build_target_summary_text def _child_kwargs(parent_history: list[Any]) -> dict[str, Any]: @@ -241,7 +243,7 @@ def test_build_scope_context_authorizes_nothing_without_targets() -> None: def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() -> None: - config = { + config: dict[str, Any] = { "targets": [ { "type": "web_application", @@ -263,8 +265,8 @@ def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() -> assert context["authorized_targets"] == [ {"type": "web_host", "value": "app.example.com", "workspace_path": ""} ] - assert "web_host: app.example.com (includes app.example.com and *.app.example.com)" in prompt - assert prompt.count("web_host: app.example.com") == 1 + assert "host: app.example.com (includes app.example.com and *.app.example.com)" in prompt + assert prompt.count("host: app.example.com") == 1 assert "https://app.example.com/search?q=test" not in prompt assert "https://app.example.com/search?q=test" in task assert "https://api.example.net/v1" in task @@ -273,6 +275,13 @@ def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() -> assert "scheme, port, path, query, or fragment" in prompt assert "not `example.com`, sibling hosts such as `api.example.com`" in prompt + assert build_scope_target_labels(config["targets"]) == [ + "host: app.example.com (includes *.app.example.com)" + ] + assert build_target_summary_text(config["targets"]).plain == ( + "Target host: app.example.com (includes *.app.example.com)" + ) + def test_scope_prompt_keeps_web_ip_targets_exact() -> None: context = build_scope_context( diff --git a/tests/test_runner_root_prompt.py b/tests/test_runner_root_prompt.py index ba59ca7d..4fbbe482 100644 --- a/tests/test_runner_root_prompt.py +++ b/tests/test_runner_root_prompt.py @@ -128,7 +128,7 @@ async def test_root_prompt_options_flow_into_root_agent( instructions_override = kwargs["instructions_override"] assert "SYSTEM-VERIFIED SCOPE" in instructions_override assert "AUTHORIZED TARGETS" in instructions_override - assert "web_host: example.com (includes example.com and *.example.com)" in instructions_override + assert "host: example.com (includes example.com and *.example.com)" in instructions_override assert "exact hostname and all of its descendant subdomains" in instructions_override assert "CUSTOM SCAN PROMPT" in instructions_override assert "Network hosts explicitly named in these root scan instructions" in instructions_override