feat: Expose Caido proxy port to host for human-in-the-loop interaction
Users can now access the Caido web UI from their browser to inspect traffic, replay requests, and perform manual testing alongside the automated scan. - Map Caido port (48080) to a random host port in DockerRuntime - Add caido_port to SandboxInfo and track across container lifecycle - Display Caido URL in TUI sidebar stats panel with selectable text - Bind Caido to 0.0.0.0 in entrypoint (requires image rebuild) - Bump sandbox image to 0.1.12 - Restore discord link in exit screen
This commit is contained in:
@@ -9,7 +9,7 @@ if [ ! -f /app/certs/ca.p12 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
caido-cli --listen 127.0.0.1:${CAIDO_PORT} \
|
caido-cli --listen 0.0.0.0:${CAIDO_PORT} \
|
||||||
--allow-guests \
|
--allow-guests \
|
||||||
--no-logging \
|
--no-logging \
|
||||||
--no-open \
|
--no-open \
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Configure Strix using environment variables or a config file.
|
|||||||
|
|
||||||
## Docker Configuration
|
## Docker Configuration
|
||||||
|
|
||||||
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:0.1.11" type="string">
|
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:0.1.12" type="string">
|
||||||
Docker image to use for the sandbox container.
|
Docker image to use for the sandbox container.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
APP=strix
|
APP=strix
|
||||||
REPO="usestrix/strix"
|
REPO="usestrix/strix"
|
||||||
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:0.1.11"
|
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:0.1.12"
|
||||||
|
|
||||||
MUTED='\033[0;2m'
|
MUTED='\033[0;2m'
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|||||||
@@ -333,6 +333,14 @@ class BaseAgent(metaclass=AgentMeta):
|
|||||||
|
|
||||||
if "agent_id" in sandbox_info:
|
if "agent_id" in sandbox_info:
|
||||||
self.state.sandbox_info["agent_id"] = sandbox_info["agent_id"]
|
self.state.sandbox_info["agent_id"] = sandbox_info["agent_id"]
|
||||||
|
|
||||||
|
caido_port = sandbox_info.get("caido_port")
|
||||||
|
if caido_port:
|
||||||
|
from strix.telemetry.tracer import get_global_tracer
|
||||||
|
|
||||||
|
tracer = get_global_tracer()
|
||||||
|
if tracer:
|
||||||
|
tracer.caido_url = f"localhost:{caido_port}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from strix.telemetry import posthog
|
from strix.telemetry import posthog
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Config:
|
|||||||
strix_disable_browser = "false"
|
strix_disable_browser = "false"
|
||||||
|
|
||||||
# Runtime Configuration
|
# Runtime Configuration
|
||||||
strix_image = "ghcr.io/usestrix/strix-sandbox:0.1.11"
|
strix_image = "ghcr.io/usestrix/strix-sandbox:0.1.12"
|
||||||
strix_runtime_backend = "docker"
|
strix_runtime_backend = "docker"
|
||||||
strix_sandbox_execution_timeout = "120"
|
strix_sandbox_execution_timeout = "120"
|
||||||
strix_sandbox_connect_timeout = "10"
|
strix_sandbox_connect_timeout = "10"
|
||||||
|
|||||||
@@ -77,12 +77,21 @@ Toast.-information .toast--title {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#stats_display {
|
#stats_scroll {
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 15;
|
max-height: 15;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
border: round #333333;
|
||||||
|
scrollbar-size: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stats_display {
|
||||||
|
height: auto;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0 1;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#vulnerabilities_panel {
|
#vulnerabilities_panel {
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
|
|||||||
console.print("\n")
|
console.print("\n")
|
||||||
console.print(panel)
|
console.print(panel)
|
||||||
console.print()
|
console.print()
|
||||||
console.print("[#60a5fa]models.strix.ai[/]")
|
console.print("[#60a5fa]models.strix.ai[/] [dim]·[/] [#60a5fa]discord.gg/strix-ai[/]")
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -829,11 +829,11 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
agents_tree.guide_style = "dashed"
|
agents_tree.guide_style = "dashed"
|
||||||
|
|
||||||
stats_display = Static("", id="stats_display")
|
stats_display = Static("", id="stats_display")
|
||||||
stats_display.ALLOW_SELECT = False
|
stats_scroll = VerticalScroll(stats_display, id="stats_scroll")
|
||||||
|
|
||||||
vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel")
|
vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel")
|
||||||
|
|
||||||
sidebar = Vertical(agents_tree, vulnerabilities_panel, stats_display, id="sidebar")
|
sidebar = Vertical(agents_tree, vulnerabilities_panel, stats_scroll, id="sidebar")
|
||||||
|
|
||||||
content_container.mount(chat_area_container)
|
content_container.mount(chat_area_container)
|
||||||
content_container.mount(sidebar)
|
content_container.mount(sidebar)
|
||||||
@@ -1272,6 +1272,9 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
if not self._is_widget_safe(stats_display):
|
if not self._is_widget_safe(stats_display):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.screen.selections:
|
||||||
|
return
|
||||||
|
|
||||||
stats_content = Text()
|
stats_content = Text()
|
||||||
|
|
||||||
stats_text = build_tui_stats_text(self.tracer, self.agent_config)
|
stats_text = build_tui_stats_text(self.tracer, self.agent_config)
|
||||||
@@ -1281,15 +1284,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
version = get_package_version()
|
version = get_package_version()
|
||||||
stats_content.append(f"\nv{version}", style="white")
|
stats_content.append(f"\nv{version}", style="white")
|
||||||
|
|
||||||
from rich.panel import Panel
|
self._safe_widget_operation(stats_display.update, stats_content)
|
||||||
|
|
||||||
stats_panel = Panel(
|
|
||||||
stats_content,
|
|
||||||
border_style="#333333",
|
|
||||||
padding=(0, 1),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._safe_widget_operation(stats_display.update, stats_panel)
|
|
||||||
|
|
||||||
def _update_vulnerabilities_panel(self) -> None:
|
def _update_vulnerabilities_panel(self) -> None:
|
||||||
"""Update the vulnerabilities panel with current vulnerability data."""
|
"""Update the vulnerabilities panel with current vulnerability data."""
|
||||||
|
|||||||
@@ -390,6 +390,12 @@ def build_tui_stats_text(tracer: Any, agent_config: dict[str, Any] | None = None
|
|||||||
stats_text.append(" · ", style="white")
|
stats_text.append(" · ", style="white")
|
||||||
stats_text.append(f"${total_stats['cost']:.2f}", style="white")
|
stats_text.append(f"${total_stats['cost']:.2f}", style="white")
|
||||||
|
|
||||||
|
caido_url = getattr(tracer, "caido_url", None)
|
||||||
|
if caido_url:
|
||||||
|
stats_text.append("\n")
|
||||||
|
stats_text.append("Caido: ", style="bold white")
|
||||||
|
stats_text.append(caido_url, style="white")
|
||||||
|
|
||||||
return stats_text
|
return stats_text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from .runtime import AbstractRuntime, SandboxInfo
|
|||||||
HOST_GATEWAY_HOSTNAME = "host.docker.internal"
|
HOST_GATEWAY_HOSTNAME = "host.docker.internal"
|
||||||
DOCKER_TIMEOUT = 60
|
DOCKER_TIMEOUT = 60
|
||||||
CONTAINER_TOOL_SERVER_PORT = 48081
|
CONTAINER_TOOL_SERVER_PORT = 48081
|
||||||
|
CONTAINER_CAIDO_PORT = 48080
|
||||||
|
|
||||||
|
|
||||||
class DockerRuntime(AbstractRuntime):
|
class DockerRuntime(AbstractRuntime):
|
||||||
@@ -37,6 +38,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
self._scan_container: Container | None = None
|
self._scan_container: Container | None = None
|
||||||
self._tool_server_port: int | None = None
|
self._tool_server_port: int | None = None
|
||||||
self._tool_server_token: str | None = None
|
self._tool_server_token: str | None = None
|
||||||
|
self._caido_port: int | None = None
|
||||||
|
|
||||||
def _find_available_port(self) -> int:
|
def _find_available_port(self) -> int:
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
@@ -78,6 +80,10 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
if port_bindings.get(port_key):
|
if port_bindings.get(port_key):
|
||||||
self._tool_server_port = int(port_bindings[port_key][0]["HostPort"])
|
self._tool_server_port = int(port_bindings[port_key][0]["HostPort"])
|
||||||
|
|
||||||
|
caido_port_key = f"{CONTAINER_CAIDO_PORT}/tcp"
|
||||||
|
if port_bindings.get(caido_port_key):
|
||||||
|
self._caido_port = int(port_bindings[caido_port_key][0]["HostPort"])
|
||||||
|
|
||||||
def _wait_for_tool_server(self, max_retries: int = 30, timeout: int = 5) -> None:
|
def _wait_for_tool_server(self, max_retries: int = 30, timeout: int = 5) -> None:
|
||||||
host = self._resolve_docker_host()
|
host = self._resolve_docker_host()
|
||||||
health_url = f"http://{host}:{self._tool_server_port}/health"
|
health_url = f"http://{host}:{self._tool_server_port}/health"
|
||||||
@@ -121,6 +127,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
self._tool_server_port = self._find_available_port()
|
self._tool_server_port = self._find_available_port()
|
||||||
|
self._caido_port = self._find_available_port()
|
||||||
self._tool_server_token = secrets.token_urlsafe(32)
|
self._tool_server_token = secrets.token_urlsafe(32)
|
||||||
execution_timeout = Config.get("strix_sandbox_execution_timeout") or "120"
|
execution_timeout = Config.get("strix_sandbox_execution_timeout") or "120"
|
||||||
|
|
||||||
@@ -130,7 +137,10 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
detach=True,
|
detach=True,
|
||||||
name=container_name,
|
name=container_name,
|
||||||
hostname=container_name,
|
hostname=container_name,
|
||||||
ports={f"{CONTAINER_TOOL_SERVER_PORT}/tcp": self._tool_server_port},
|
ports={
|
||||||
|
f"{CONTAINER_TOOL_SERVER_PORT}/tcp": self._tool_server_port,
|
||||||
|
f"{CONTAINER_CAIDO_PORT}/tcp": self._caido_port,
|
||||||
|
},
|
||||||
cap_add=["NET_ADMIN", "NET_RAW"],
|
cap_add=["NET_ADMIN", "NET_RAW"],
|
||||||
labels={"strix-scan-id": scan_id},
|
labels={"strix-scan-id": scan_id},
|
||||||
environment={
|
environment={
|
||||||
@@ -152,6 +162,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
if attempt < max_retries:
|
if attempt < max_retries:
|
||||||
self._tool_server_port = None
|
self._tool_server_port = None
|
||||||
self._tool_server_token = None
|
self._tool_server_token = None
|
||||||
|
self._caido_port = None
|
||||||
time.sleep(2**attempt)
|
time.sleep(2**attempt)
|
||||||
else:
|
else:
|
||||||
return container
|
return container
|
||||||
@@ -173,6 +184,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
self._scan_container = None
|
self._scan_container = None
|
||||||
self._tool_server_port = None
|
self._tool_server_port = None
|
||||||
self._tool_server_token = None
|
self._tool_server_token = None
|
||||||
|
self._caido_port = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
container = self.client.containers.get(container_name)
|
container = self.client.containers.get(container_name)
|
||||||
@@ -260,7 +272,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
raise RuntimeError("Docker container ID is unexpectedly None")
|
raise RuntimeError("Docker container ID is unexpectedly None")
|
||||||
|
|
||||||
token = existing_token or self._tool_server_token
|
token = existing_token or self._tool_server_token
|
||||||
if self._tool_server_port is None or token is None:
|
if self._tool_server_port is None or self._caido_port is None or token is None:
|
||||||
raise RuntimeError("Tool server not initialized")
|
raise RuntimeError("Tool server not initialized")
|
||||||
|
|
||||||
host = self._resolve_docker_host()
|
host = self._resolve_docker_host()
|
||||||
@@ -273,6 +285,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
"api_url": api_url,
|
"api_url": api_url,
|
||||||
"auth_token": token,
|
"auth_token": token,
|
||||||
"tool_server_port": self._tool_server_port,
|
"tool_server_port": self._tool_server_port,
|
||||||
|
"caido_port": self._caido_port,
|
||||||
"agent_id": agent_id,
|
"agent_id": agent_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +327,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
self._scan_container = None
|
self._scan_container = None
|
||||||
self._tool_server_port = None
|
self._tool_server_port = None
|
||||||
self._tool_server_token = None
|
self._tool_server_token = None
|
||||||
|
self._caido_port = None
|
||||||
except (NotFound, DockerException):
|
except (NotFound, DockerException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -323,6 +337,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
self._scan_container = None
|
self._scan_container = None
|
||||||
self._tool_server_port = None
|
self._tool_server_port = None
|
||||||
self._tool_server_token = None
|
self._tool_server_token = None
|
||||||
|
self._caido_port = None
|
||||||
|
|
||||||
if container_name is None:
|
if container_name is None:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class SandboxInfo(TypedDict):
|
|||||||
api_url: str
|
api_url: str
|
||||||
auth_token: str | None
|
auth_token: str | None
|
||||||
tool_server_port: int
|
tool_server_port: int
|
||||||
|
caido_port: int
|
||||||
agent_id: str
|
agent_id: str
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class Tracer:
|
|||||||
self._next_message_id = 1
|
self._next_message_id = 1
|
||||||
self._saved_vuln_ids: set[str] = set()
|
self._saved_vuln_ids: set[str] = set()
|
||||||
|
|
||||||
|
self.caido_url: str | None = None
|
||||||
self.vulnerability_found_callback: Callable[[dict[str, Any]], None] | None = None
|
self.vulnerability_found_callback: Callable[[dict[str, Any]], None] | None = None
|
||||||
|
|
||||||
def set_run_name(self, run_name: str) -> None:
|
def set_run_name(self, run_name: str) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user