diff --git a/README.md b/README.md index 8381b95..390d04d 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ strix --target https://your-app.com --instruction "Perform authenticated testing # Multi-target testing (source code + deployed app) strix -t https://github.com/org/app -t https://your-app.com -# Targets from a file, one target per non-empty line +# Targets from a file, one target per non-empty, non-comment line strix --target-list ./targets.txt # White-box source-aware scan (local repository) diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 4c74105..ea9ddd8 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -63,7 +63,7 @@ strix --target https://your-app.com # Multiple targets (white-box testing) strix -t https://github.com/org/repo -t https://your-app.com -# Targets from a file, one target per non-empty line +# Targets from a file, one target per non-empty, non-comment line strix --target-list ./targets.txt ``` diff --git a/docs/usage/cli.mdx b/docs/usage/cli.mdx index fb612ab..68f7d5e 100644 --- a/docs/usage/cli.mdx +++ b/docs/usage/cli.mdx @@ -16,7 +16,7 @@ strix (--target | --target-list | --mount ) [options] - Path to a file containing targets, one per non-empty line. Can be specified multiple times and combined with `--target`. + Path to a file containing targets, one per non-empty, non-comment line. Lines starting with `#` are ignored. Can be specified multiple times and combined with `--target`. diff --git a/strix/interface/main.py b/strix/interface/main.py index d26ef7a..15301ba 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -345,7 +345,7 @@ Examples: strix --target https://github.com/user/repo --target https://example.com strix --target ./my-project --target https://staging.example.com --target https://prod.example.com - # Targets from a file, one target per line + # Targets from a file, one target per non-empty, non-comment line strix --target-list ./targets.txt # Custom instructions (inline) @@ -378,8 +378,8 @@ Examples: type=str, action="append", metavar="PATH", - help="Path to a file containing targets, one per non-empty line. Can be specified " - "multiple times and combined with --target.", + help="Path to a file containing targets, one per non-empty, non-comment line. " + "Can be specified multiple times and combined with --target.", ) parser.add_argument( "--mount", diff --git a/strix/interface/utils.py b/strix/interface/utils.py index 892520a..27c10a4 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -1132,7 +1132,7 @@ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR09 def read_target_list_file(path_str: str) -> list[str]: - """Read scan targets from a file, one target per non-empty line.""" + """Read scan targets from a file, one target per non-empty, non-comment line.""" if not path_str or not path_str.strip(): raise ValueError("--target-list path must not be empty.") @@ -1141,7 +1141,15 @@ def read_target_list_file(path_str: str) -> list[str]: raise ValueError(f"Target list file '{path_str}' is not an existing file.") try: - targets = [line.strip() for line in path.read_text(encoding="utf-8").splitlines()] + targets = [ + target + for line in path.read_text(encoding="utf-8").splitlines() + if (target := line.strip()) and not target.startswith("#") + ] + except UnicodeDecodeError as e: + raise ValueError( + f"Target list file '{path_str}' must be valid UTF-8 text: {e!s}" + ) from e except OSError as e: raise ValueError(f"Failed to read target list file '{path_str}': {e!s}") from e diff --git a/tests/test_local_sources.py b/tests/test_local_sources.py index 71b538f..22ed4b9 100644 --- a/tests/test_local_sources.py +++ b/tests/test_local_sources.py @@ -175,9 +175,25 @@ def test_read_target_list_file_strips_blank_lines(tmp_path: Path) -> None: ] +def test_read_target_list_file_ignores_comment_lines(tmp_path: Path) -> None: + target_list = tmp_path / "targets.txt" + target_list.write_text( + "# production targets\n" + "https://test1.com/\n" + " # staging targets\n" + "http://test2.com:5789/\n", + encoding="utf-8", + ) + + assert read_target_list_file(str(target_list)) == [ + "https://test1.com/", + "http://test2.com:5789/", + ] + + def test_read_target_list_file_rejects_empty_file(tmp_path: Path) -> None: target_list = tmp_path / "targets.txt" - target_list.write_text(" \n\n", encoding="utf-8") + target_list.write_text(" \n# no targets yet\n\n", encoding="utf-8") with pytest.raises(ValueError, match="is empty"): read_target_list_file(str(target_list)) @@ -188,6 +204,14 @@ def test_read_target_list_file_rejects_missing_path(tmp_path: Path) -> None: read_target_list_file(str(tmp_path / "missing.txt")) +def test_read_target_list_file_rejects_non_utf8_file(tmp_path: Path) -> None: + target_list = tmp_path / "targets.txt" + target_list.write_bytes(b"https://test1.com/\xff\n") + + with pytest.raises(ValueError, match="must be valid UTF-8 text"): + read_target_list_file(str(target_list)) + + @pytest.mark.parametrize("empty", ["", " "]) def test_read_target_list_file_rejects_empty_path(empty: str) -> None: with pytest.raises(ValueError, match="must not be empty"):