Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions backend/secuscan/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions testing/backend/unit/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
66 changes: 41 additions & 25 deletions testing/backend/unit/test_tls_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,39 @@ def test_env_override_true(self, monkeypatch):


class TestCrawlerVerifySsl:
@pytest.mark.asyncio
async def test_crawl_target_passes_verify_ssl(self):
def _make_mock_stream_response(self):
mock_response = MagicMock()
mock_response.text = "<html></html>"
mock_response.url = "https://example.com/"
mock_response.status_code = 200
mock_response.headers = {}
mock_response.headers = MagicMock()
mock_response.headers.get = MagicMock(return_value=None)
mock_response.headers.get_list = MagicMock(return_value=[])
mock_response.history = []
return mock_response

def _make_mock_client(self, mock_response):
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)

mock_stream_ctx = AsyncMock()
mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response)
mock_stream_ctx.__aexit__ = AsyncMock(return_value=False)
mock_client.stream = MagicMock(return_value=mock_stream_ctx)
async def _aiter_bytes():
yield b"<html></html>"

mock_response.aiter_bytes = _aiter_bytes

return mock_client

@pytest.mark.asyncio
async def test_crawl_target_passes_verify_ssl(self):
mock_response = self._make_mock_stream_response()
mock_client = self._make_mock_client(mock_response)

with patch("backend.secuscan.crawler.httpx.AsyncClient", return_value=mock_client) as mock_cls:
with patch("backend.secuscan.crawler.settings") as mock_settings:
mock_settings.verify_ssl = True
Expand All @@ -79,17 +98,8 @@ async def test_crawl_target_passes_verify_ssl(self):

@pytest.mark.asyncio
async def test_crawl_target_verify_ssl_false_when_disabled(self):
mock_response = MagicMock()
mock_response.text = "<html></html>"
mock_response.url = "https://example.com/"
mock_response.status_code = 200
mock_response.headers = {}
mock_response.history = []

mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_response = self._make_mock_stream_response()
mock_client = self._make_mock_client(mock_response)

with patch("backend.secuscan.crawler.httpx.AsyncClient", return_value=mock_client) as mock_cls:
with patch("backend.secuscan.crawler.settings") as mock_settings:
Expand Down Expand Up @@ -119,16 +129,19 @@ async def test_api_scanner_passes_verify_ssl(self):
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)

mock_crawl = AsyncMock(return_value={"api_hints": []})

with patch("backend.secuscan.scanners.api_scanner.httpx.AsyncClient", return_value=mock_client) as mock_cls:
with patch("backend.secuscan.scanners.api_scanner.settings") as mock_settings:
mock_settings.verify_ssl = True
with patch.object(scanner, "_fetch_spec", return_value=None):
with patch.object(scanner, "_probe_graphql", return_value=([], [])):
await scanner.run("https://example.com", {})
with patch("backend.secuscan.scanners.api_scanner.crawl_target", mock_crawl):
with patch.object(scanner, "_fetch_spec", return_value=None):
with patch.object(scanner, "_probe_graphql", return_value=([], [])):
await scanner.run("https://example.com", {})

mock_cls.assert_called()
_, kwargs = mock_cls.call_args
assert kwargs["verify"] is True
mock_cls.assert_called()
_, kwargs = mock_cls.call_args
assert kwargs["verify"] is True

@pytest.mark.asyncio
async def test_api_scanner_verify_ssl_false_when_disabled(self):
Expand All @@ -141,15 +154,18 @@ async def test_api_scanner_verify_ssl_false_when_disabled(self):
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)

mock_crawl = AsyncMock(return_value={"api_hints": []})

with patch("backend.secuscan.scanners.api_scanner.httpx.AsyncClient", return_value=mock_client) as mock_cls:
with patch("backend.secuscan.scanners.api_scanner.settings") as mock_settings:
mock_settings.verify_ssl = False
with patch.object(scanner, "_fetch_spec", return_value=None):
with patch.object(scanner, "_probe_graphql", return_value=([], [])):
await scanner.run("https://example.com", {})
with patch("backend.secuscan.scanners.api_scanner.crawl_target", mock_crawl):
with patch.object(scanner, "_fetch_spec", return_value=None):
with patch.object(scanner, "_probe_graphql", return_value=([], [])):
await scanner.run("https://example.com", {})

_, kwargs = mock_cls.call_args
assert kwargs["verify"] is False
_, kwargs = mock_cls.call_args
assert kwargs["verify"] is False


# ---------------------------------------------------------------------------
Expand Down
Loading