diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index a8523a24..391e6941 100644 --- a/backend/secuscan/executor.py +++ b/backend/secuscan/executor.py @@ -898,8 +898,9 @@ async def read_stream(): line = await stdout.readline() if line: decoded_line = line.decode("utf-8", errors="replace") - output_lines.append(decoded_line) - await self._broadcast(task_id, "output", decoded_line) + safe_line = redact(decoded_line) + output_lines.append(safe_line) + await self._broadcast(task_id, "output", safe_line) try: await asyncio.wait_for(read_stream(), timeout=timeout) diff --git a/testing/backend/unit/test_executor.py b/testing/backend/unit/test_executor.py index 76bd070a..9af759f4 100644 --- a/testing/backend/unit/test_executor.py +++ b/testing/backend/unit/test_executor.py @@ -1446,3 +1446,54 @@ async def fake_command(*args, **kwargs): assert original_inputs_seen.get("domain") == "example.com" assert original_inputs_seen.get("__pinned_ip") == "93.184.216.34" await db.disconnect() + + +@pytest.mark.asyncio +async def test_execute_command_redacts_secrets_before_broadcast(): + executor = TaskExecutor() + task_id = "test-sse-redaction-1624" + queue = executor.subscribe(task_id) + + async def mock_readline(): + for line in [ + b"Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.secret123\n", + b"port 80 is open\n", + b"api_key=supersecretkey12345\n", + None, + ]: + yield line + + mock_stdout = AsyncMock() + mock_stdout.readline = mock_readline().__anext__ + mock_stdout.at_eof = MagicMock(side_effect=[False, False, False, True]) + + with patch.object(executor, "_process_pids", {}): + process = AsyncMock() + process.stdout = mock_stdout + process.returncode = 0 + + async def fake_create_subprocess(*args, **kwargs): + return process + + with patch("asyncio.create_subprocess_exec", side_effect=fake_create_subprocess): + output, exit_code = await executor._execute_command(["echo", "test"], task_id, timeout=30) + + events = [] + while not queue.empty(): + events.append(queue.get_nowait()) + + output_events = [e for e in events if e["type"] == "output"] + assert len(output_events) == 3 + + assert "[REDACTED]" in output_events[0]["data"] + assert "eyJhbGciOiJIUzI1NiJ9.secret123" not in output_events[0]["data"] + assert "Authorization: Bearer" in output_events[0]["data"] + + assert "port 80 is open" in output_events[1]["data"] + + assert "[REDACTED]" in output_events[2]["data"] + assert "supersecretkey12345" not in output_events[2]["data"] + + assert "[REDACTED]" in output + assert "eyJhbGciOiJIUzI1NiJ9.secret123" not in output + assert "supersecretkey12345" not in output