From 23e6daca8aa2bf59a8715c204ac1a61d57889656 Mon Sep 17 00:00:00 2001 From: Thibaut Rouxel Date: Wed, 22 Jul 2026 11:35:03 +0200 Subject: [PATCH] fix(hatching-triage-sandbox): handle search API errors gracefully (#4756) --- .../src/connector/connector.py | 37 +++++---- .../tests/tests_connector/test_connector.py | 81 +++++++++++++++++++ 2 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 internal-enrichment/hatching-triage-sandbox/tests/tests_connector/test_connector.py diff --git a/internal-enrichment/hatching-triage-sandbox/src/connector/connector.py b/internal-enrichment/hatching-triage-sandbox/src/connector/connector.py index 63bb8ee9c76..84cb022a5da 100644 --- a/internal-enrichment/hatching-triage-sandbox/src/connector/connector.py +++ b/internal-enrichment/hatching-triage-sandbox/src/connector/connector.py @@ -18,6 +18,7 @@ ) from stix2 import URL, DomainName, EmailAddress, IPv4Address from triage import Client +from triage.client import ServerError class HatchingTriageSandboxConnector: @@ -456,21 +457,27 @@ def _search_for_analysis(self, search_query): sample_id = None if self.use_existing_analysis: - search_paginator = self.triage_client.search(query=search_query, max=1) - for search in search_paginator: - existing_status = search["status"] - if existing_status == "reported": - sample_id = search["id"] - self.helper.connector_logger.info( - f"Found existing analysis with id {sample_id} and status {existing_status}." - ) - elif existing_status == "pending": - sample_id = search["id"] - self.helper.connector_logger.info( - f"Found existing analysis with id {sample_id} and status {existing_status}." - ) - self._wait_for_analysis(sample_id) - break + try: + search_paginator = self.triage_client.search(query=search_query, max=1) + for search in search_paginator: + existing_status = search["status"] + if existing_status == "reported": + sample_id = search["id"] + self.helper.connector_logger.info( + f"Found existing analysis with id {sample_id} and status {existing_status}." + ) + elif existing_status == "pending": + sample_id = search["id"] + self.helper.connector_logger.info( + f"Found existing analysis with id {sample_id} and status {existing_status}." + ) + self._wait_for_analysis(sample_id) + break + except ServerError as e: + self.helper.connector_logger.warning( + f"Search API returned an error for query '{search_query}': {e}. " + "Falling back to submitting a new sample." + ) return sample_id diff --git a/internal-enrichment/hatching-triage-sandbox/tests/tests_connector/test_connector.py b/internal-enrichment/hatching-triage-sandbox/tests/tests_connector/test_connector.py new file mode 100644 index 00000000000..9857dfc53c2 --- /dev/null +++ b/internal-enrichment/hatching-triage-sandbox/tests/tests_connector/test_connector.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock, patch + +import pytest +from connector.connector import HatchingTriageSandboxConnector +from triage.client import ServerError + + +@pytest.fixture +def connector(mock_opencti_connector_helper): + config = MagicMock() + config.opencti.url = "http://localhost:8080" + config.hatching_triage_sandbox.base_url = "https://tria.ge/api" + config.hatching_triage_sandbox.token.get_secret_value.return_value = "fake-token" + config.hatching_triage_sandbox.use_existing_analysis = True + config.hatching_triage_sandbox.family_color = "#0059f7" + config.hatching_triage_sandbox.botnet_color = "#f79e00" + config.hatching_triage_sandbox.campaign_color = "#7a01e5" + config.hatching_triage_sandbox.tag_color = "#54483b" + config.hatching_triage_sandbox.max_tlp = "TLP:AMBER" + + helper = MagicMock() + helper.api.identity.create.return_value = {"standard_id": "identity--fake"} + + with patch("connector.connector.Client"): + instance = HatchingTriageSandboxConnector(config, helper) + + return instance + + +class TestSearchForAnalysis: + def test_returns_sample_id_when_reported(self, connector): + """Should return sample_id when an existing reported analysis is found.""" + connector.triage_client.search.return_value = iter( + [{"id": "sample-123", "status": "reported"}] + ) + + result = connector._search_for_analysis("url:https://example.com") + + assert result == "sample-123" + connector.helper.connector_logger.info.assert_called() + + def test_returns_none_when_no_results(self, connector): + """Should return None when no existing analysis is found.""" + connector.triage_client.search.return_value = iter([]) + + result = connector._search_for_analysis("url:https://example.com") + + assert result is None + + def test_returns_none_on_server_error(self, connector): + """Should return None and log a warning when a ServerError occurs (e.g. 504 timeout).""" + import json + + mock_response = MagicMock() + mock_response.status_code = 504 + mock_response.json.side_effect = json.JSONDecodeError("Expecting value", "", 0) + + http_error = MagicMock() + http_error.response = mock_response + + connector.triage_client.search.return_value = _raise_server_error(http_error) + + result = connector._search_for_analysis("url:https://example.com") + + assert result is None + connector.helper.connector_logger.warning.assert_called_once() + + def test_returns_none_when_use_existing_analysis_disabled(self, connector): + """Should return None without searching when use_existing_analysis is False.""" + connector.use_existing_analysis = False + + result = connector._search_for_analysis("url:https://example.com") + + assert result is None + connector.triage_client.search.assert_not_called() + + +def _raise_server_error(http_error): + """Generator that raises a ServerError when iterated.""" + raise ServerError(http_error) + yield # noqa: unreachable - makes this a generator