From 653efe9861f67a59a92dd8157049f5f843c936cd Mon Sep 17 00:00:00 2001 From: ionfwsrijan <201338831+ionfwsrijan@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:38:54 +0530 Subject: [PATCH 1/2] fix: apply redaction before SSE broadcast in _execute_command (#1624) - Redact each decoded line before appending to output_lines and before broadcasting to SSE stream listeners - Add test_execute_command_redacts_secrets_before_broadcast to verify that known secret patterns are redacted from stream events - The raw_output_path file redaction (line 599) remains as defense-in-depth --- backend/secuscan/executor.py | 5 +-- testing/backend/unit/test_executor.py | 49 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index a8523a245..391e6941b 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 76bd070a5..4d70b12cb 100644 --- a/testing/backend/unit/test_executor.py +++ b/testing/backend/unit/test_executor.py @@ -1446,3 +1446,52 @@ 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 = AsyncMock(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) == 2 + + 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 "[REDACTED]" in output_events[1]["data"] + assert "supersecretkey12345" not in output_events[1]["data"] + + assert "[REDACTED]" in output + assert "eyJhbGciOiJIUzI1NiJ9.secret123" not in output + assert "supersecretkey12345" not in output From 9602b5e057ad97131b6e1c9a6fc12587c657e394 Mon Sep 17 00:00:00 2001 From: ionfwsrijan Date: Fri, 10 Jul 2026 08:27:58 +0530 Subject: [PATCH 2/2] fix: correct SSE redaction test mock for sync at_eof call --- testing/backend/unit/test_executor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/testing/backend/unit/test_executor.py b/testing/backend/unit/test_executor.py index 4d70b12cb..9af759f4c 100644 --- a/testing/backend/unit/test_executor.py +++ b/testing/backend/unit/test_executor.py @@ -1465,7 +1465,7 @@ async def mock_readline(): mock_stdout = AsyncMock() mock_stdout.readline = mock_readline().__anext__ - mock_stdout.at_eof = AsyncMock(side_effect=[False, False, False, True]) + mock_stdout.at_eof = MagicMock(side_effect=[False, False, False, True]) with patch.object(executor, "_process_pids", {}): process = AsyncMock() @@ -1483,14 +1483,16 @@ async def fake_create_subprocess(*args, **kwargs): events.append(queue.get_nowait()) output_events = [e for e in events if e["type"] == "output"] - assert len(output_events) == 2 + 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 "[REDACTED]" in output_events[1]["data"] - assert "supersecretkey12345" not in output_events[1]["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