♻️(malware) log filehash when a submission fail

We want to log the file has when a submission fail in order to aggregate
all this logs to send them to JCOP.
This commit is contained in:
Manuel Raynaud
2025-12-01 11:43:09 +01:00
parent b0b90be8fe
commit 3bdfdf2b16
4 changed files with 38 additions and 40 deletions

View File

@@ -173,6 +173,7 @@ class JCOPBackend(BaseBackend):
# start a new analysis
trigger_new_analysis.delay(
file_path,
file_hash,
**kwargs,
)
return False
@@ -211,7 +212,7 @@ class JCOPBackend(BaseBackend):
)
return False
def trigger_new_analysis(self, file_path: str, **kwargs):
def trigger_new_analysis(self, file_path: str, file_hash: str, **kwargs):
"""Trigger a new analysis for a file."""
with default_storage.open(file_path, "rb") as file:
encoder = MultipartEncoder(
@@ -230,8 +231,8 @@ class JCOPBackend(BaseBackend):
data=encoder,
timeout=(30, self.submit_timeout),
)
except requests.exceptions.RequestException as exc:
logger.error("Error submitting file to JCOP: %s", exc)
except requests.exceptions.RequestException:
logger.error("Error while sending file %s to JCOP with hash %s", file_path, file_hash)
raise
if response.status_code == HTTPStatus.OK:

View File

@@ -54,12 +54,13 @@ def analyse_file_async(
def trigger_new_analysis(
self,
file_path: str,
file_hash: str,
**kwargs,
) -> None:
"""Trigger a new analysis for a file."""
backend = MalwareDetectionHandler()() # JCOPBackend
try:
backend.trigger_new_analysis(file_path, **kwargs)
backend.trigger_new_analysis(file_path, file_hash, **kwargs)
except requests.exceptions.RequestException as exc:
if self.request.retries >= self.max_retries:
backend.failed_analysis(

View File

@@ -515,10 +515,7 @@ def test_jcop_backend_analyse_file_async_request_error(
@responses.activate
@pytest.mark.parametrize("used_kwargs", [{}, {"foo": "bar"}])
@pytest.mark.parametrize("with_file_hash", [True, False])
def test_jcop_backend_analyse_file_async_no_existing_result(
jcop_generate_file_path, jcop_backend, used_kwargs, with_file_hash
):
def test_jcop_backend_analyse_file_async_no_existing_result(jcop_generate_file_path, jcop_backend, used_kwargs):
"""Test when no result found should start the trigger_new_analysis task."""
file_path, file_hash = jcop_generate_file_path
@@ -533,13 +530,12 @@ def test_jcop_backend_analyse_file_async_no_existing_result(
status=404,
)
with mock.patch.object(trigger_new_analysis, "delay") as mock_trigger_new_analysis:
should_retry = jcop_backend.check_analysis(
file_path, file_hash=file_hash if with_file_hash else None, **used_kwargs
)
should_retry = jcop_backend.check_analysis(file_path, file_hash=file_hash, **used_kwargs)
assert should_retry is False
mock_trigger_new_analysis.assert_called_once_with(
file_path,
file_hash,
**used_kwargs,
)
@@ -563,7 +559,7 @@ def test_jcop_backend_trigger_new_analysis_success(jcop_generate_file_path, jcop
)
with mock.patch.object(analyse_file_async, "apply_async") as mock_apply_async:
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
mock_apply_async.assert_called_once_with(
countdown=5,
args=(file_path,),
@@ -578,7 +574,7 @@ def test_jcop_backend_trigger_new_analysis_success(jcop_generate_file_path, jcop
@pytest.mark.parametrize("used_kwargs", [{}, {"foo": "bar"}])
def test_jcop_backend_trigger_new_analysis_unauthorized(jcop_generate_file_path, jcop_backend, used_kwargs):
"""Test submission with invalid API key."""
file_path, _ = jcop_generate_file_path
file_path, file_hash = jcop_generate_file_path
# Mock the submit endpoint
responses.add(
@@ -592,7 +588,7 @@ def test_jcop_backend_trigger_new_analysis_unauthorized(jcop_generate_file_path,
)
with pytest.raises(exceptions.MalwareDetectionInvalidAuthenticationError):
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
jcop_callback.assert_called_once_with(
file_path,
@@ -611,7 +607,7 @@ def test_jcop_backend_trigger_new_analysis_unauthorized_complete_flow(
jcop_generate_file_path, jcop_backend, used_kwargs
):
"""Test submission with invalid API key."""
file_path, _ = jcop_generate_file_path
file_path, file_hash = jcop_generate_file_path
malware_detection = factories.MalwareDetectionFactory(path=file_path, status=MalwareDetectionStatus.PROCESSING)
next_record = factories.MalwareDetectionFactory(status=MalwareDetectionStatus.PENDING)
@@ -630,7 +626,7 @@ def test_jcop_backend_trigger_new_analysis_unauthorized_complete_flow(
mock.patch.object(analyse_file_async, "delay") as mock_apply_async,
pytest.raises(exceptions.MalwareDetectionInvalidAuthenticationError),
):
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
mock_apply_async.assert_called_once_with(next_record.path)
@@ -655,7 +651,7 @@ def test_jcop_backend_trigger_new_analysis_unauthorized_complete_flow(
@pytest.mark.parametrize("used_kwargs", [{}, {"foo": "bar"}])
def test_jcop_backend_trigger_new_analysis_timeout_max_retries(jcop_generate_file_path, jcop_backend, used_kwargs):
"""Test submission with timeout after max retries."""
file_path, _ = jcop_generate_file_path
file_path, file_hash = jcop_generate_file_path
# Mock the submit endpoint
responses.add(
@@ -669,7 +665,7 @@ def test_jcop_backend_trigger_new_analysis_timeout_max_retries(jcop_generate_fil
)
with pytest.raises(TimeoutError):
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
jcop_callback.assert_not_called()
@@ -678,7 +674,7 @@ def test_jcop_backend_trigger_new_analysis_timeout_max_retries(jcop_generate_fil
@pytest.mark.parametrize("used_kwargs", [{}, {"foo": "bar"}])
def test_jcop_backend_trigger_new_analysis_file_too_large(jcop_generate_file_path, jcop_backend, used_kwargs):
"""Test submission with file too large."""
file_path, _ = jcop_generate_file_path
file_path, file_hash = jcop_generate_file_path
# Mock the submit endpoint
responses.add(
@@ -691,7 +687,7 @@ def test_jcop_backend_trigger_new_analysis_file_too_large(jcop_generate_file_pat
status=413,
)
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
jcop_callback.assert_called_once_with(
file_path,
@@ -710,7 +706,7 @@ def test_jcop_backend_trigger_new_analysis_file_too_large_complete_flow(
jcop_generate_file_path, jcop_backend, used_kwargs
):
"""Test submission with file too large."""
file_path, _ = jcop_generate_file_path
file_path, file_hash = jcop_generate_file_path
malware_detection = factories.MalwareDetectionFactory(path=file_path, status=MalwareDetectionStatus.PROCESSING)
next_record = factories.MalwareDetectionFactory(status=MalwareDetectionStatus.PENDING)
# Mock the submit endpoint
@@ -725,7 +721,7 @@ def test_jcop_backend_trigger_new_analysis_file_too_large_complete_flow(
)
with mock.patch.object(analyse_file_async, "delay") as mock_apply_async:
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
mock_apply_async.assert_called_once_with(next_record.path)
jcop_callback.assert_called_once_with(
@@ -749,7 +745,7 @@ def test_jcop_backend_trigger_new_analysis_file_too_large_complete_flow(
@pytest.mark.parametrize("used_kwargs", [{}, {"foo": "bar"}])
def test_jcop_backend_trigger_new_analysis_request_error(jcop_generate_file_path, jcop_backend, used_kwargs):
"""Test submission with request error."""
file_path, _ = jcop_generate_file_path
file_path, file_hash = jcop_generate_file_path
# Mock the submit endpoint
responses.add(
@@ -763,7 +759,7 @@ def test_jcop_backend_trigger_new_analysis_request_error(jcop_generate_file_path
status=200,
)
with pytest.raises(requests.exceptions.RequestException):
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
jcop_callback.assert_not_called()
@@ -772,7 +768,7 @@ def test_jcop_backend_trigger_new_analysis_request_error(jcop_generate_file_path
@pytest.mark.parametrize("used_kwargs", [{}, {"foo": "bar"}])
def test_jcop_backend_trigger_new_analysis_unknown_status(jcop_generate_file_path, jcop_backend, used_kwargs):
"""Test submission with unknown status code."""
file_path, _ = jcop_generate_file_path
file_path, file_hash = jcop_generate_file_path
# Mock the submit endpoint
responses.add(
@@ -785,7 +781,7 @@ def test_jcop_backend_trigger_new_analysis_unknown_status(jcop_generate_file_pat
status=500,
)
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
jcop_callback.assert_called_once_with(
file_path,
@@ -804,7 +800,7 @@ def test_jcop_backend_trigger_new_analysis_unknown_status_complete_flow(
jcop_generate_file_path, jcop_backend, used_kwargs
):
"""Test submission with unknown status code."""
file_path, _ = jcop_generate_file_path
file_path, file_hash = jcop_generate_file_path
malware_detection = factories.MalwareDetectionFactory(path=file_path, status=MalwareDetectionStatus.PROCESSING)
next_record = factories.MalwareDetectionFactory(status=MalwareDetectionStatus.PENDING)
# Mock the submit endpoint
@@ -819,7 +815,7 @@ def test_jcop_backend_trigger_new_analysis_unknown_status_complete_flow(
)
with mock.patch.object(analyse_file_async, "delay") as mock_apply_async:
jcop_backend.trigger_new_analysis(file_path, **used_kwargs)
jcop_backend.trigger_new_analysis(file_path, file_hash, **used_kwargs)
mock_apply_async.assert_called_once_with(next_record.path)
jcop_callback.assert_called_once_with(

View File

@@ -97,9 +97,9 @@ def test_trigger_new_analysis_success(mock_backend):
backend, handler = mock_backend
with mock.patch("lasuite.malware_detection.tasks.jcop.MalwareDetectionHandler", return_value=handler):
trigger_new_analysis("file.txt", extra_param="value")
trigger_new_analysis("file.txt", "file_hash", extra_param="value")
backend.trigger_new_analysis.assert_called_once_with("file.txt", extra_param="value")
backend.trigger_new_analysis.assert_called_once_with("file.txt", "file_hash", extra_param="value")
def test_trigger_new_analysis_request_exception_retry(mock_backend):
@@ -111,9 +111,9 @@ def test_trigger_new_analysis_request_exception_retry(mock_backend):
mock.patch("lasuite.malware_detection.tasks.jcop.MalwareDetectionHandler", return_value=handler),
pytest.raises(requests.exceptions.RequestException),
):
trigger_new_analysis("file.txt")
trigger_new_analysis("file.txt", "file_hash")
backend.trigger_new_analysis.assert_called_once_with("file.txt")
backend.trigger_new_analysis.assert_called_once_with("file.txt", "file_hash")
backend.failed_analysis.assert_not_called()
@@ -126,9 +126,9 @@ def test_trigger_new_analysis_timeout_retry(mock_backend):
mock.patch("lasuite.malware_detection.tasks.jcop.MalwareDetectionHandler", return_value=handler),
pytest.raises(TimeoutError),
):
trigger_new_analysis("file.txt")
trigger_new_analysis("file.txt", "file_hash")
backend.trigger_new_analysis.assert_called_once_with("file.txt")
backend.trigger_new_analysis.assert_called_once_with("file.txt", "file_hash")
backend.failed_analysis.assert_not_called()
@@ -139,9 +139,9 @@ def test_trigger_new_analysis_request_exception_max_retries(mock_backend):
backend.failed_analysis = mock.MagicMock()
with mock.patch("lasuite.malware_detection.tasks.jcop.MalwareDetectionHandler", return_value=handler):
trigger_new_analysis.max_retries = 0
trigger_new_analysis("file.txt")
trigger_new_analysis("file.txt", "file_hash")
backend.trigger_new_analysis.assert_called_once_with("file.txt")
backend.trigger_new_analysis.assert_called_once_with("file.txt", "file_hash")
backend.failed_analysis.assert_called_once_with(
"file.txt",
error_code=MaxRetriesErrorCodes.TRIGGER_NEW_ANALYSIS,
@@ -156,9 +156,9 @@ def test_trigger_new_analysis_timeout_max_retries(mock_backend):
backend.failed_analysis = mock.MagicMock()
with mock.patch("lasuite.malware_detection.tasks.jcop.MalwareDetectionHandler", return_value=handler):
trigger_new_analysis.max_retries = 0
trigger_new_analysis("file.txt")
trigger_new_analysis("file.txt", "file_hash")
backend.trigger_new_analysis.assert_called_once_with("file.txt")
backend.trigger_new_analysis.assert_called_once_with("file.txt", "file_hash")
backend.failed_analysis.assert_called_once_with(
"file.txt",
error_code=MaxRetriesErrorCodes.TRIGGER_NEW_ANALYSIS_TIMEOUT,
@@ -175,7 +175,7 @@ def test_trigger_new_analysis_with_auth_error_no_retry(mock_backend):
mock.patch("lasuite.malware_detection.tasks.jcop.MalwareDetectionHandler", return_value=handler),
pytest.raises(MalwareDetectionInvalidAuthenticationError),
):
trigger_new_analysis("file.txt")
trigger_new_analysis("file.txt", "file_hash")
backend.trigger_new_analysis.assert_called_once_with("file.txt")
backend.trigger_new_analysis.assert_called_once_with("file.txt", "file_hash")
backend.failed_analysis.assert_not_called()