mirror of
https://github.com/usestrix/strix.git
synced 2026-08-25 12:22:37 +02:00
Claude models are served on /messages, which the OpenAI SDK can't speak, so those runs go through LiteLLM's Anthropic route instead. Prompt caching moves with them, since LiteLLM consumes the injection points the raw SDK rejects. Zen and Go now show up by name instead of both reading 'OpenCode subscription', and Zen keeps its cost tracked: it bills prepaid credits per request, so those runs were never actually free.
248 lines
7.7 KiB
Python
248 lines
7.7 KiB
Python
"""Tests for CLI target-list argument parsing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import sys
|
|
from types import SimpleNamespace
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import pytest
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
cli_main: Any = importlib.import_module("strix.interface.main")
|
|
|
|
|
|
def _stub_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
cli_main,
|
|
"load_settings",
|
|
lambda: SimpleNamespace(runtime=SimpleNamespace(max_local_copy_mb=1024)),
|
|
)
|
|
|
|
|
|
def test_parse_arguments_accepts_target_list_file(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
target_list = tmp_path / "targets.txt"
|
|
target_list.write_text(
|
|
"https://test1.com/\n\nhttp://test2.com:5789/\n",
|
|
encoding="utf-8",
|
|
)
|
|
_stub_settings(monkeypatch)
|
|
monkeypatch.setattr(sys, "argv", ["strix", "--target-list", str(target_list), "-n"])
|
|
|
|
args = cli_main.parse_arguments()
|
|
|
|
assert [target["original"] for target in args.targets_info] == [
|
|
"https://test1.com/",
|
|
"http://test2.com:5789/",
|
|
]
|
|
assert [target["type"] for target in args.targets_info] == [
|
|
"web_application",
|
|
"web_application",
|
|
]
|
|
|
|
|
|
def test_parse_arguments_combines_target_and_target_list(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
target_list = tmp_path / "targets.txt"
|
|
target_list.write_text("http://test2.com:5789/\n", encoding="utf-8")
|
|
_stub_settings(monkeypatch)
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
["strix", "-t", "https://test1.com/", "--target-list", str(target_list)],
|
|
)
|
|
|
|
args = cli_main.parse_arguments()
|
|
|
|
assert [target["original"] for target in args.targets_info] == [
|
|
"https://test1.com/",
|
|
"http://test2.com:5789/",
|
|
]
|
|
|
|
|
|
def test_parse_arguments_rejects_resume_with_target_list(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
target_list = tmp_path / "targets.txt"
|
|
target_list.write_text("https://test1.com/\n", encoding="utf-8")
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
["strix", "--resume", "old-run", "--target-list", str(target_list)],
|
|
)
|
|
|
|
with pytest.raises(SystemExit):
|
|
cli_main.parse_arguments()
|
|
|
|
assert "Cannot combine --resume with --target/--target-list" in capsys.readouterr().err
|
|
|
|
|
|
def _write_run_record(runs_dir: Path, run_name: str, record: dict[str, Any]) -> None:
|
|
"""Write a resumable run: its record plus the agent snapshot resume needs."""
|
|
run_dir = runs_dir / run_name
|
|
run_dir.mkdir(parents=True, exist_ok=True)
|
|
(run_dir / "run.json").write_text(json.dumps(record), encoding="utf-8")
|
|
state_dir = run_dir / ".state"
|
|
state_dir.mkdir(exist_ok=True)
|
|
(state_dir / "agents.json").write_text("{}", encoding="utf-8")
|
|
|
|
|
|
def test_resume_restores_a_target_less_workspace_mount(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""A run that only mounted a working directory is resumable."""
|
|
work = tmp_path / "project"
|
|
work.mkdir()
|
|
monkeypatch.chdir(tmp_path)
|
|
_write_run_record(
|
|
tmp_path / "strix_runs",
|
|
"pentest_abcd",
|
|
{
|
|
"run_name": "pentest_abcd",
|
|
"targets_info": [],
|
|
"local_sources": [],
|
|
"workspace_mount": str(work),
|
|
"instruction": "audit the auth flow",
|
|
"scan_mode": "deep",
|
|
},
|
|
)
|
|
monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"])
|
|
|
|
args = cli_main.parse_arguments()
|
|
|
|
# Still genuinely target-less, and the workspace is mounted again.
|
|
assert args.targets_info == []
|
|
assert args.workspace_mount == str(work)
|
|
assert args.local_sources == [
|
|
{"source_path": str(work), "workspace_subdir": "project", "protect_metadata": True}
|
|
]
|
|
assert args.instruction == "audit the auth flow"
|
|
|
|
|
|
def test_resume_revalidates_persisted_workspace_files(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Resume places the same files again, and drops ones that went away."""
|
|
work = tmp_path / "project"
|
|
work.mkdir()
|
|
kept = tmp_path / "wordlist.txt"
|
|
kept.write_text("admin\n", encoding="utf-8")
|
|
monkeypatch.chdir(tmp_path)
|
|
_write_run_record(
|
|
tmp_path / "strix_runs",
|
|
"pentest_abcd",
|
|
{
|
|
"run_name": "pentest_abcd",
|
|
"targets_info": [],
|
|
"local_sources": [],
|
|
"workspace_mount": str(work),
|
|
"workspace_files": [
|
|
{"source_path": str(kept), "workspace_path": "/workspace/lists/words.txt"},
|
|
{"source_path": str(tmp_path / "gone.txt"), "workspace_path": "/workspace/g.txt"},
|
|
],
|
|
},
|
|
)
|
|
monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"])
|
|
|
|
args = cli_main.parse_arguments()
|
|
|
|
assert args.workspace_files == [
|
|
{"source_path": str(kept), "workspace_path": "/workspace/lists/words.txt"}
|
|
]
|
|
|
|
|
|
def test_resume_rejects_an_edited_workspace_file_path(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""A hand-edited record cannot place a file outside the workspace."""
|
|
work = tmp_path / "project"
|
|
work.mkdir()
|
|
source = tmp_path / "wordlist.txt"
|
|
source.write_text("admin\n", encoding="utf-8")
|
|
monkeypatch.chdir(tmp_path)
|
|
_write_run_record(
|
|
tmp_path / "strix_runs",
|
|
"pentest_abcd",
|
|
{
|
|
"run_name": "pentest_abcd",
|
|
"targets_info": [],
|
|
"local_sources": [],
|
|
"workspace_mount": str(work),
|
|
"workspace_files": [
|
|
{"source_path": str(source), "workspace_path": "/etc/cron.d/payload"}
|
|
],
|
|
},
|
|
)
|
|
monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"])
|
|
|
|
with pytest.raises(SystemExit):
|
|
cli_main.parse_arguments()
|
|
|
|
assert "invalid workspace file" in capsys.readouterr().err
|
|
|
|
|
|
def test_resume_reports_a_missing_workspace_directory(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
_write_run_record(
|
|
tmp_path / "strix_runs",
|
|
"pentest_abcd",
|
|
{
|
|
"run_name": "pentest_abcd",
|
|
"targets_info": [],
|
|
"local_sources": [],
|
|
"workspace_mount": str(tmp_path / "deleted"),
|
|
},
|
|
)
|
|
monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"])
|
|
|
|
with pytest.raises(SystemExit):
|
|
cli_main.parse_arguments()
|
|
|
|
assert "is missing" in capsys.readouterr().err
|
|
|
|
|
|
def test_resume_still_requires_targets_or_a_workspace(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
_write_run_record(
|
|
tmp_path / "strix_runs",
|
|
"pentest_abcd",
|
|
{"run_name": "pentest_abcd", "targets_info": [], "local_sources": []},
|
|
)
|
|
monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"])
|
|
|
|
with pytest.raises(SystemExit):
|
|
cli_main.parse_arguments()
|
|
|
|
assert "has no targets_info" in capsys.readouterr().err
|
|
|
|
|
|
def test_resume_non_object_run_json_exits(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
run_dir = tmp_path / "strix_runs" / "pentest_abcd"
|
|
run_dir.mkdir(parents=True)
|
|
(run_dir / "run.json").write_text("[]", encoding="utf-8")
|
|
|
|
monkeypatch.setattr(sys, "argv", ["strix", "--resume", "pentest_abcd"])
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
cli_main.parse_arguments()
|
|
|
|
assert exc_info.value.code == 2
|
|
captured = capsys.readouterr()
|
|
assert "run.json unreadable" in captured.err
|
|
assert "not an object" in captured.err
|