From 77ed106832ce74bfffe12a6f3efb1b79f9f14310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20MIGUEL?= Date: Mon, 20 Jul 2026 18:08:57 +0200 Subject: [PATCH] fix(expectations): tolerate null inject_expectation_signatures The platform serializes inject_expectation_signatures as null when an expectation has no computed signatures. The model required a list, so parsing raised a ValidationError that aborted the entire expectation batch fetch in collectors, leaving detection/prevention expectations unprocessed (no alerts matched). Coerce null to an empty list via a before-validator and default the field to an empty list, so a signature-less expectation is parsed instead of crashing the whole batch. Refs: OpenAEV-Platform/collectors#490 --- .../inject_expectation/model/expectation.py | 16 ++++++-- test/apis/expectation/test_expectation.py | 37 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pyoaev/apis/inject_expectation/model/expectation.py b/pyoaev/apis/inject_expectation/model/expectation.py index e8827745..3e64ce0d 100644 --- a/pyoaev/apis/inject_expectation/model/expectation.py +++ b/pyoaev/apis/inject_expectation/model/expectation.py @@ -1,8 +1,8 @@ from enum import Enum -from typing import List +from typing import Any, List from uuid import UUID -from pydantic import BaseModel +from pydantic import BaseModel, Field, field_validator from thefuzz import fuzz from pyoaev.signatures.signature_type import SignatureType @@ -41,11 +41,21 @@ class Expectation(BaseModel): """ inject_expectation_id: UUID - inject_expectation_signatures: List[ExpectationSignature] + inject_expectation_signatures: List[ExpectationSignature] = Field( + default_factory=list + ) success_label: str = "Success" failure_label: str = "Failure" + @field_validator("inject_expectation_signatures", mode="before") + @classmethod + def _coerce_none_signatures_to_empty(cls, value: Any) -> Any: + # The platform serializes this field as null when an expectation has no + # signatures yet. Without this, a single such expectation raises a + # ValidationError that aborts the whole batch fetch (collectors#490). + return [] if value is None else value + def __init__(self, *a, **kw): super().__init__(*a, **kw) self.__api_client = kw["api_client"] diff --git a/test/apis/expectation/test_expectation.py b/test/apis/expectation/test_expectation.py index 22ca9921..65016dd6 100644 --- a/test/apis/expectation/test_expectation.py +++ b/test/apis/expectation/test_expectation.py @@ -333,6 +333,43 @@ def test_when_relevant_signatures_when_some_alert_data_dont_match_return_false( self.assertFalse(matched) + def test_when_signatures_is_none_it_is_coerced_to_empty_list(self): + # The platform may serialize inject_expectation_signatures as null; parsing + # must not raise, otherwise a single such expectation aborts the whole + # batch fetch (collectors#490). + for model_cls in (DetectionExpectation, PreventionExpectation): + model = model_cls( + **{ + "inject_expectation_id": uuid4(), + "inject_expectation_signatures": None, + }, + api_client=create_mock_api_client(), + ) + self.assertEqual(model.inject_expectation_signatures, []) + + def test_when_signatures_is_missing_it_defaults_to_empty_list(self): + model = DetectionExpectation( + **{"inject_expectation_id": uuid4()}, + api_client=create_mock_api_client(), + ) + self.assertEqual(model.inject_expectation_signatures, []) + + def test_when_signatures_provided_they_are_preserved(self): + model = DetectionExpectation( + **{ + "inject_expectation_id": uuid4(), + "inject_expectation_signatures": [ + { + "type": SignatureTypes.SIG_TYPE_PARENT_PROCESS_NAME, + "value": "parent.exe", + }, + ], + }, + api_client=create_mock_api_client(), + ) + self.assertEqual(len(model.inject_expectation_signatures), 1) + self.assertEqual(model.inject_expectation_signatures[0].value, "parent.exe") + if __name__ == "__main__": unittest.main()