mirror of
https://github.com/usestrix/strix.git
synced 2026-10-08 03:08:08 +00:00
fix(scope): render configured URLs as hosts
This commit is contained in:
parent
ee82875ea6
commit
7b41840e4d
5 changed files with 76 additions and 14 deletions
|
|
@ -76,7 +76,13 @@ 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 }})
|
||||
{% elif target.type == "ip_address" %}
|
||||
- ip_address: {{ target.value }} (exact address)
|
||||
{% else %}
|
||||
- {{ target.type }}: {{ target.value }}{% if target.workspace_path %} (workspace: {{ target.workspace_path }}){% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from agents.model_settings import ModelSettings
|
||||
from openai.types.shared import Reasoning
|
||||
|
|
@ -206,8 +208,30 @@ def build_root_task(scan_config: dict[str, Any]) -> str:
|
|||
)
|
||||
|
||||
|
||||
def _network_scope_target(value: str) -> tuple[str, str]:
|
||||
"""Reduce a web URL to its hostname-level prompt scope."""
|
||||
hostname = (urlsplit(value).hostname or "").rstrip(".").lower()
|
||||
if not hostname:
|
||||
return "web_application", value
|
||||
try:
|
||||
ipaddress.ip_address(hostname)
|
||||
except ValueError:
|
||||
return "web_host", hostname
|
||||
return "ip_address", hostname
|
||||
|
||||
|
||||
def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
|
||||
authorized: list[dict[str, str]] = []
|
||||
authorized_keys: set[tuple[str, str, str]] = set()
|
||||
|
||||
def add_authorized(ttype: str, value: str, workspace_path: str = "") -> None:
|
||||
key = (ttype, value, workspace_path)
|
||||
if key not in authorized_keys:
|
||||
authorized.append(
|
||||
{"type": ttype, "value": value, "workspace_path": workspace_path},
|
||||
)
|
||||
authorized_keys.add(key)
|
||||
|
||||
value_keys = {
|
||||
"repository": "target_repo",
|
||||
"local_code": "target_path",
|
||||
|
|
@ -223,17 +247,18 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
|
|||
|
||||
workspace_subdir = details.get("workspace_subdir")
|
||||
workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else ""
|
||||
authorized.append(
|
||||
{"type": ttype, "value": value, "workspace_path": workspace_path},
|
||||
)
|
||||
if ttype == "web_application":
|
||||
scope_type, scope_value = _network_scope_target(str(value or ""))
|
||||
add_authorized(scope_type, scope_value)
|
||||
else:
|
||||
add_authorized(str(ttype), str(value or ""), workspace_path)
|
||||
|
||||
# An API spec authorizes the hosts it declares as in-scope web targets
|
||||
# so the agent can exercise every endpoint without expanding scope.
|
||||
if ttype == "api_spec":
|
||||
authorized.extend(
|
||||
{"type": "web_application", "value": base_url, "workspace_path": ""}
|
||||
for base_url in details.get("base_urls") or []
|
||||
)
|
||||
for base_url in details.get("base_urls") or []:
|
||||
scope_type, scope_value = _network_scope_target(str(base_url))
|
||||
add_authorized(scope_type, scope_value)
|
||||
|
||||
return {
|
||||
"scope_source": "system_scan_config",
|
||||
|
|
|
|||
|
|
@ -148,5 +148,5 @@ def test_build_scope_context_authorizes_base_urls(tmp_path: Path) -> None:
|
|||
|
||||
types = {a["type"] for a in authorized}
|
||||
assert "api_spec" in types
|
||||
assert "web_application" in types
|
||||
assert any(a["value"] == "https://api.shop.test/v1" for a in authorized)
|
||||
assert "web_host" in types
|
||||
assert any(a["value"] == "api.shop.test" for a in authorized)
|
||||
|
|
|
|||
|
|
@ -246,7 +246,11 @@ def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() ->
|
|||
{
|
||||
"type": "web_application",
|
||||
"details": {"target_url": "https://app.example.com/search?q=test"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "web_application",
|
||||
"details": {"target_url": "https://app.example.com/blog/"},
|
||||
},
|
||||
],
|
||||
"user_instructions": "Also test https://api.example.net/v1.",
|
||||
}
|
||||
|
|
@ -256,7 +260,13 @@ def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() ->
|
|||
task = build_root_task(config)
|
||||
|
||||
assert "SYSTEM-VERIFIED SCOPE" in prompt
|
||||
assert "https://app.example.com/search?q=test" in prompt
|
||||
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 "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
|
||||
assert "Every network host explicitly named in the user's root scan task" in prompt
|
||||
assert "exact hostname and all of its descendant subdomains" in prompt
|
||||
|
|
@ -264,6 +274,27 @@ def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() ->
|
|||
assert "not `example.com`, sibling hosts such as `api.example.com`" in prompt
|
||||
|
||||
|
||||
def test_scope_prompt_keeps_web_ip_targets_exact() -> None:
|
||||
context = build_scope_context(
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"type": "web_application",
|
||||
"details": {"target_url": "https://192.0.2.10:8443/admin"},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
prompt = render_system_prompt(scan_mode="quick", is_root=True, system_prompt_context=context)
|
||||
|
||||
assert context["authorized_targets"] == [
|
||||
{"type": "ip_address", "value": "192.0.2.10", "workspace_path": ""}
|
||||
]
|
||||
assert "ip_address: 192.0.2.10 (exact address)" in prompt
|
||||
assert "https://192.0.2.10:8443/admin" not in prompt
|
||||
|
||||
|
||||
def test_scope_prompt_does_not_make_repository_origin_a_live_target() -> None:
|
||||
context = build_scope_context(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ async def test_root_prompt_options_flow_into_root_agent(
|
|||
"authorization_source": "strix_platform_verified_targets",
|
||||
"authorized_targets": [
|
||||
{
|
||||
"type": "web_application",
|
||||
"value": "https://example.com",
|
||||
"type": "web_host",
|
||||
"value": "example.com",
|
||||
"workspace_path": "",
|
||||
},
|
||||
],
|
||||
|
|
@ -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 "https://example.com" in instructions_override
|
||||
assert "web_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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue