diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index e8f0bbea..3dda63eb 100644 --- a/backend/secuscan/executor.py +++ b/backend/secuscan/executor.py @@ -601,7 +601,12 @@ async def read_stream(): return f"Execution error: {str(e)}", -1 def _resolve_execution_timeout(self, inputs: Dict[str, Any]) -> int: - """Resolve per-task process timeout from plugin inputs.""" + """Resolve per-task process timeout from plugin inputs. + + The caller may request a shorter timeout than the operator cap, but + never a longer one. ``settings.sandbox_timeout`` is the hard ceiling + and is always enforced regardless of what the client supplies. + """ for key in ("max_scan_time", "timeout"): raw_value = inputs.get(key) try: @@ -609,7 +614,7 @@ def _resolve_execution_timeout(self, inputs: Dict[str, Any]) -> int: except (TypeError, ValueError): continue if timeout > 0: - return timeout + return min(timeout, settings.sandbox_timeout) return settings.sandbox_timeout def _classify_command_result(self, plugin, output: str, exit_code: int) -> tuple[str, Optional[str]]: diff --git a/testing/backend/unit/test_executor.py b/testing/backend/unit/test_executor.py index f5a45c0f..f9c887ae 100644 --- a/testing/backend/unit/test_executor.py +++ b/testing/backend/unit/test_executor.py @@ -197,6 +197,38 @@ def test_classify_command_result_fails_on_undefined_flag_even_with_zero_exit(set assert error is not None +def test_resolve_execution_timeout_clamps_requested_timeout(monkeypatch): + monkeypatch.setattr(settings, "sandbox_timeout", 600) + + executor = TaskExecutor() + + assert executor._resolve_execution_timeout({"timeout": 9999}) == 600 + + +def test_resolve_execution_timeout_allows_shorter_requested_timeout(monkeypatch): + monkeypatch.setattr(settings, "sandbox_timeout", 600) + + executor = TaskExecutor() + + assert executor._resolve_execution_timeout({"timeout": 120}) == 120 + + +def test_resolve_execution_timeout_ignores_invalid_values(monkeypatch): + monkeypatch.setattr(settings, "sandbox_timeout", 600) + + executor = TaskExecutor() + + assert executor._resolve_execution_timeout({"timeout": "invalid"}) == 600 + + +def test_resolve_execution_timeout_prefers_max_scan_time(monkeypatch): + monkeypatch.setattr(settings, "sandbox_timeout", 600) + + executor = TaskExecutor() + + assert executor._resolve_execution_timeout({"max_scan_time": 90, "timeout": 120}) == 90 + + @pytest.mark.asyncio async def test_execute_task_sets_cancelled_status_in_db(setup_test_environment): """