diff --git a/comfy_cli/command/custom_nodes/command.py b/comfy_cli/command/custom_nodes/command.py index 5972aa25..d3612801 100644 --- a/comfy_cli/command/custom_nodes/command.py +++ b/comfy_cli/command/custom_nodes/command.py @@ -706,6 +706,27 @@ def publish( typer.echo("Validating node configuration...") config = extract_node_configuration() + # Run security checks first + typer.echo("Running security checks...") + try: + # Run ruff check with security rules and --exit-zero to only warn + cmd = ["ruff", "check", ".", "-q", "--select", "S102,S307", "--exit-zero"] + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.stdout: # Changed from checking returncode to checking if there's output + print("[yellow]Security warnings found:[/yellow]") # Changed from red to yellow to indicate warning + print(result.stdout) + print("[bold yellow]We will soon disable exec and eval, so this will be an error soon.[/bold yellow]") + # TODO: re-enable exit when we disable exec and eval + # raise typer.Exit(code=1) + + except FileNotFoundError: + print("[red]Ruff is not installed. Please install it with 'pip install ruff'[/red]") + raise typer.Exit(code=1) + except Exception as e: + print(f"[red]Error running security check: {e}[/red]") + raise typer.Exit(code=1) + # Prompt for API Key if not token: token = typer.prompt( diff --git a/pyproject.toml b/pyproject.toml index 9faf509a..a782f936 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "typing-extensions>=4.7.0", "uv", "websocket-client", + "ruff", "semver~=3.0.2", ] diff --git a/tests/comfy_cli/command/nodes/test_publish.py b/tests/comfy_cli/command/nodes/test_publish.py new file mode 100644 index 00000000..5b9d2c69 --- /dev/null +++ b/tests/comfy_cli/command/nodes/test_publish.py @@ -0,0 +1,89 @@ +from unittest.mock import MagicMock, patch + +from typer.testing import CliRunner + +from comfy_cli.command.custom_nodes.command import app + +runner = CliRunner() + + +def test_publish_fails_on_security_violations(): + # Mock subprocess.run to simulate security violations + mock_result = MagicMock() + mock_result.returncode = 1 + mock_result.stdout = "S102 Use of exec() detected" + + with ( + patch("subprocess.run", return_value=mock_result), + patch("typer.prompt", return_value="test-token"), + ): + result = runner.invoke(app, ["publish"]) + + # TODO: re-enable exit when we disable exec and eval + # assert result.exit_code == 1 + # assert "Security issues found" in result.stdout + assert "Security warnings found" in result.stdout + + +def test_publish_continues_on_no_security_violations(): + # Mock subprocess.run to simulate no violations + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "" + + with ( + patch("subprocess.run", return_value=mock_result), + patch("comfy_cli.command.custom_nodes.command.extract_node_configuration") as mock_extract, + patch("typer.prompt") as mock_prompt, + patch("comfy_cli.command.custom_nodes.command.registry_api.publish_node_version") as mock_publish, + patch("comfy_cli.command.custom_nodes.command.zip_files") as mock_zip, + patch("comfy_cli.command.custom_nodes.command.upload_file_to_signed_url") as mock_upload, + ): + # Setup the mocks + mock_extract.return_value = {"name": "test-node"} + mock_prompt.return_value = "test-token" + mock_publish.return_value = MagicMock(signedUrl="https://test.url") + + # Run the publish command + _result = runner.invoke(app, ["publish"]) + + # Verify the publish flow continued + assert mock_extract.called + assert mock_publish.called + assert mock_zip.called + assert mock_upload.called + + +def test_publish_handles_missing_ruff(): + with patch("subprocess.run", side_effect=FileNotFoundError()): + result = runner.invoke(app, ["publish"]) + + assert result.exit_code == 1 + assert "Ruff is not installed" in result.stdout + + +def test_publish_with_token_option(): + # Mock subprocess.run to simulate no violations + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "" + + with ( + patch("subprocess.run", return_value=mock_result), + patch("comfy_cli.command.custom_nodes.command.extract_node_configuration") as mock_extract, + patch("comfy_cli.command.custom_nodes.command.registry_api.publish_node_version") as mock_publish, + patch("comfy_cli.command.custom_nodes.command.zip_files") as mock_zip, + patch("comfy_cli.command.custom_nodes.command.upload_file_to_signed_url") as mock_upload, + ): + # Setup the mocks + mock_extract.return_value = {"name": "test-node"} + mock_publish.return_value = MagicMock(signedUrl="https://test.url") + + # Run the publish command with token + _result = runner.invoke(app, ["publish", "--token", "test-token"]) + + # Verify the publish flow worked with provided token + assert mock_extract.called + assert mock_publish.called + assert mock_zip.called + assert mock_upload.called