diff --git a/pyrit/cli/pyrit_scan.py b/pyrit/cli/pyrit_scan.py index 8aafc0f93c..e2ce641a43 100644 --- a/pyrit/cli/pyrit_scan.py +++ b/pyrit/cli/pyrit_scan.py @@ -18,6 +18,7 @@ from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter from pathlib import Path from typing import TYPE_CHECKING, Any, get_args, get_origin +from urllib.parse import urlparse from pyrit.cli._cli_args import ( ARG_HELP, @@ -444,21 +445,30 @@ async def _resolve_server_url_async(*, parsed_args: Namespace) -> str | None: # Auto-start if requested if parsed_args.start_server: - # The launcher can only bind localhost:8000. If the user explicitly - # configured a different URL we can't honor it — refuse rather than - # silently start a server the user can't reach. - if base_url != DEFAULT_SERVER_URL: + parsed_url = urlparse(base_url) + if ( + parsed_url.scheme != "http" + or parsed_url.hostname not in {"localhost", "127.0.0.1"} + or parsed_url.username is not None + or parsed_url.password is not None + or parsed_url.path not in {"", "/"} + or parsed_url.query + or parsed_url.fragment + ): print( f"Error: cannot --start-server because the configured server URL ({base_url}) " - f"does not match the launcher default ({DEFAULT_SERVER_URL}). " - "Either remove --server-url / the server.url config entry, " - "or start the backend manually with `pyrit_backend --host ... --port ...`.", + "is not a plain local HTTP URL. Use localhost or 127.0.0.1, " + "or start a remote backend separately.", file=sys.stderr, ) return None launcher = ServerLauncher() try: - return await launcher.start_async(config_file=parsed_args.config_file) + return await launcher.start_async( + host=parsed_url.hostname, + port=parsed_url.port or 80, + config_file=parsed_args.config_file, + ) except RuntimeError as exc: print(f"Error: {exc}") return None @@ -504,8 +514,6 @@ async def _handle_stop_server_async(*, parsed_args: Namespace) -> int: Returns: int: Exit code (always ``0``). """ - from urllib.parse import urlparse - from pyrit.cli._server_launcher import ServerLauncher, stop_server_on_port base_url = _resolve_configured_server_url(parsed_args=parsed_args) diff --git a/tests/unit/cli/test_pyrit_scan.py b/tests/unit/cli/test_pyrit_scan.py index d78ec4073c..53d827b02c 100644 --- a/tests/unit/cli/test_pyrit_scan.py +++ b/tests/unit/cli/test_pyrit_scan.py @@ -764,9 +764,7 @@ async def test_returns_none_when_start_server_raises(self, capsys): assert await pyrit_scan._resolve_server_url_async(parsed_args=parsed) is None assert "nope" in capsys.readouterr().out - async def test_start_server_refuses_when_url_differs_from_default(self, capsys): - # User explicitly configured a non-default URL but asks us to launch the bundled - # backend. The launcher only knows how to bind localhost:8000, so we must refuse. + async def test_start_server_refuses_remote_url(self, capsys): parsed = Namespace(server_url="http://other:9999", start_server=True, config_file=None) start_async_mock = AsyncMock() with ( @@ -786,6 +784,23 @@ async def test_start_server_refuses_when_url_differs_from_default(self, capsys): assert "cannot --start-server" in err assert "http://other:9999" in err + async def test_start_server_uses_custom_local_port(self): + parsed = Namespace(server_url="http://127.0.0.1:8765", start_server=True, config_file=None) + start_async_mock = AsyncMock(return_value="http://127.0.0.1:8765") + with ( + patch( + "pyrit.cli._server_launcher.ServerLauncher.probe_health_async", + new=AsyncMock(return_value=False), + ), + patch( + "pyrit.cli._server_launcher.ServerLauncher.start_async", + new=start_async_mock, + ), + ): + result = await pyrit_scan._resolve_server_url_async(parsed_args=parsed) + assert result == "http://127.0.0.1:8765" + start_async_mock.assert_awaited_once_with(host="127.0.0.1", port=8765, config_file=None) + async def test_resolution_order_cli_beats_config_beats_default(self): """CLI flag > config-file value > built-in default.""" # 1) CLI flag wins even when config has a different value.