From cceecd5c7e19ed16eb5591ca3eb057f67e8ee535 Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Mon, 20 Jul 2026 11:25:36 +0200 Subject: [PATCH 1/4] feat(contracts): declare expected security platform types on expectations (#313) --- pyoaev/__init__.py | 2 +- pyoaev/contracts/contract_config.py | 24 +++++++++ test/contracts/test_contract_expectations.py | 57 +++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/pyoaev/__init__.py b/pyoaev/__init__.py index 73afac8..abbdc3c 100644 --- a/pyoaev/__init__.py +++ b/pyoaev/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "2.260717.0" +__version__ = "2.260720.0" from pyoaev._version import ( # noqa: F401 __author__, diff --git a/pyoaev/contracts/contract_config.py b/pyoaev/contracts/contract_config.py index b2ea50f..dbe15ac 100644 --- a/pyoaev/contracts/contract_config.py +++ b/pyoaev/contracts/contract_config.py @@ -74,6 +74,24 @@ class ExpectationType(str, Enum): vulnerability: str = "VULNERABILITY" +class SecurityPlatformType(str, Enum): + """Categories of security platform expected to fulfil a technical expectation. + + When an expectation declares one or more of these, the platform focuses the + detection/prevention result on collectors of those types only (instead of every + connected security platform). An empty list means "any security platform". + """ + + EDR: str = "EDR" + XDR: str = "XDR" + SIEM: str = "SIEM" + SOAR: str = "SOAR" + NDR: str = "NDR" + ISPM: str = "ISPM" + LLM_FIREWALL: str = "LLM_FIREWALL" + AI_GATEWAY: str = "AI_GATEWAY" + + @dataclass class Expectation: expectation_type: ExpectationType @@ -83,6 +101,12 @@ class Expectation: expectation_expectation_group: bool expectation_is_predefined: bool = False expectation_is_multi_selectable: bool = False + # Security platform types expected to fulfil this expectation. Empty = any + # platform (unchanged behaviour). Typically set for DETECTION / PREVENTION, + # left empty for MANUAL expectations. + expectation_expected_security_platform_types: List[SecurityPlatformType] = field( + default_factory=list + ) @dataclass diff --git a/test/contracts/test_contract_expectations.py b/test/contracts/test_contract_expectations.py index f054517..f10d219 100644 --- a/test/contracts/test_contract_expectations.py +++ b/test/contracts/test_contract_expectations.py @@ -7,10 +7,11 @@ ContractExpectations, Expectation, ExpectationType, + SecurityPlatformType, ) -def _expectation(expectation_type, name, is_predefined): +def _expectation(expectation_type, name, is_predefined, expected_platforms=None): return Expectation( expectation_type=expectation_type, expectation_name=name, @@ -18,6 +19,7 @@ def _expectation(expectation_type, name, is_predefined): expectation_score=100, expectation_expectation_group=False, expectation_is_predefined=is_predefined, + expectation_expected_security_platform_types=expected_platforms or [], ) @@ -104,6 +106,59 @@ def test_no_flag_yields_no_predefined(self): self.assertEqual([], data["predefinedExpectations"]) + def test_expected_security_platform_types_default_empty(self): + """An expectation without declared platforms serializes an empty list.""" + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[ + _expectation(ExpectationType.detection, "Detection", True), + ], + ) + + data = _serialize(field) + + self.assertEqual( + [], + data["availableExpectations"][0][ + "expectation_expected_security_platform_types" + ], + ) + + def test_expected_security_platform_types_serialize(self): + """Declared platform types serialize as their string values on both + the available and predefined arrays.""" + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[ + _expectation( + ExpectationType.detection, + "Detection", + True, + expected_platforms=[ + SecurityPlatformType.EDR, + SecurityPlatformType.XDR, + ], + ), + ], + ) + + data = _serialize(field) + + self.assertEqual( + ["EDR", "XDR"], + data["availableExpectations"][0][ + "expectation_expected_security_platform_types" + ], + ) + self.assertEqual( + ["EDR", "XDR"], + data["predefinedExpectations"][0][ + "expectation_expected_security_platform_types" + ], + ) + if __name__ == "__main__": unittest.main() From 4bc78b6da3a1494b0cdb6263f52c93316f753a3f Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Mon, 20 Jul 2026 11:33:59 +0200 Subject: [PATCH 2/4] docs(contracts): cover VULNERABILITY in security platform types wording (#313) The SecurityPlatformType docstring and the Expectation field comment only mentioned detection/prevention while VULNERABILITY is also a technical expectation seeded per security platform. --- pyoaev/contracts/contract_config.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyoaev/contracts/contract_config.py b/pyoaev/contracts/contract_config.py index dbe15ac..8de69ad 100644 --- a/pyoaev/contracts/contract_config.py +++ b/pyoaev/contracts/contract_config.py @@ -78,8 +78,9 @@ class SecurityPlatformType(str, Enum): """Categories of security platform expected to fulfil a technical expectation. When an expectation declares one or more of these, the platform focuses the - detection/prevention result on collectors of those types only (instead of every - connected security platform). An empty list means "any security platform". + technical (DETECTION / PREVENTION / VULNERABILITY) result on collectors of + those types only (instead of every connected security platform). An empty + list means "any security platform". """ EDR: str = "EDR" @@ -102,8 +103,8 @@ class Expectation: expectation_is_predefined: bool = False expectation_is_multi_selectable: bool = False # Security platform types expected to fulfil this expectation. Empty = any - # platform (unchanged behaviour). Typically set for DETECTION / PREVENTION, - # left empty for MANUAL expectations. + # platform (unchanged behaviour). Typically set for technical expectations + # (DETECTION / PREVENTION / VULNERABILITY), left empty for MANUAL ones. expectation_expected_security_platform_types: List[SecurityPlatformType] = field( default_factory=list ) From a58b99d384c56543ecc8a7fd7257c92ca08a0a11 Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Mon, 20 Jul 2026 11:49:14 +0200 Subject: [PATCH 3/4] chore(contracts): drop manual version bump, releases bump it automatically (#313) --- pyoaev/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyoaev/__init__.py b/pyoaev/__init__.py index abbdc3c..73afac8 100644 --- a/pyoaev/__init__.py +++ b/pyoaev/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "2.260720.0" +__version__ = "2.260717.0" from pyoaev._version import ( # noqa: F401 __author__, From 5d1d17fc45e390abcd5a033ec32acf67fb778020 Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Mon, 20 Jul 2026 11:59:16 +0200 Subject: [PATCH 4/4] test(contracts): exercise the real default_factory path for platform types (#313) The test helper forced expectation_expected_security_platform_types to [] even when omitted, so the default-empty test never covered the dataclass default. The field is now only passed when declared. --- test/contracts/test_contract_expectations.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/contracts/test_contract_expectations.py b/test/contracts/test_contract_expectations.py index f10d219..7618575 100644 --- a/test/contracts/test_contract_expectations.py +++ b/test/contracts/test_contract_expectations.py @@ -12,6 +12,11 @@ def _expectation(expectation_type, name, is_predefined, expected_platforms=None): + # Only pass the field when declared so the default (None) path exercises + # the dataclass default_factory instead of an explicit empty list. + kwargs = {} + if expected_platforms is not None: + kwargs["expectation_expected_security_platform_types"] = expected_platforms return Expectation( expectation_type=expectation_type, expectation_name=name, @@ -19,7 +24,7 @@ def _expectation(expectation_type, name, is_predefined, expected_platforms=None) expectation_score=100, expectation_expectation_group=False, expectation_is_predefined=is_predefined, - expectation_expected_security_platform_types=expected_platforms or [], + **kwargs, )