feat(safety): collect target-list files passed via -w/-l flags

Recon tools route their target list through a flag — `ffuf -w wordlist.txt`,
`httpx -l hosts.txt`, `nuclei --list targets.txt` — not the `<` redirect the
input-file collector already handled, so the reviewer kept blocking "probes
every host listed in hosts.txt" because the list was never in the packet. Parse
the value of the common list-file flags and collect it the same bounded way,
alongside redirect inputs. The value is only read when it resolves to a
workspace file, so a boolean `-l` (grep, wc) whose next token is not a file
collects nothing and never makes the packet incomplete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
oyasumi
2026-08-08 17:57:08 +00:00
co-authored by Claude Opus 5
parent dad35b9e5f
commit 3c4be45d34
3 changed files with 91 additions and 6 deletions
+43
View File
@@ -916,3 +916,46 @@ def test_data_tools_reading_script_named_files_are_not_execution(command: str) -
"""A read/transfer/text tool takes a script-named file as data, not as a program to
run, so it must not trip the unresolved-execution guard."""
assert parse_command(command).parse_error is None
@pytest.mark.parametrize(
("command", "expected"),
[
("ffuf -w /workspace/words.txt -u https://x/FUZZ", ["/workspace/words.txt"]),
("httpx -l hosts.txt -sc -title", ["hosts.txt"]),
("nuclei --list targets.txt -severity high", ["targets.txt"]),
("ffuf -w=words.txt -u https://x/FUZZ", ["words.txt"]),
("subfinder -d x -o out.txt", []), # -o is output, not a list input
],
)
def test_list_flag_files_are_parsed(command: str, expected: list[str]) -> None:
assert parse_command(command).input_files == expected
@pytest.mark.asyncio
async def test_wordlist_flag_file_is_attached_for_scope_review() -> None:
"""Recon tools route their target list through `-w`/`-l`, not a `<` redirect, so the
same evidence must be collected for the reviewer to check it against scope."""
bundle = await _compile(
"ffuf -w /workspace/paths.txt -u https://api.fiuu.com/FUZZ -mc all",
{"/workspace/paths.txt": "admin\napi\nlogin\n"},
workdir="/workspace",
)
try:
inputs = [a for a in bundle.packet["artifacts"] if a.get("role") == "input"]
assert [a["path"] for a in inputs] == ["/workspace/paths.txt"]
assert "admin" in inputs[0]["source"]
finally:
bundle.cleanup()
@pytest.mark.asyncio
async def test_list_flag_value_that_is_not_a_workspace_file_collects_nothing() -> None:
"""A boolean `-l` (grep, wc) whose next token is not a workspace file must not make
the packet incomplete or attach anything."""
bundle = await _compile("grep -l pattern /workspace/app.py", workdir="/workspace")
try:
assert [a for a in bundle.packet["artifacts"] if a.get("role") == "input"] == []
assert bundle.deterministic_block is None
finally:
bundle.cleanup()