diff --git a/Sekoia.io/CHANGELOG.md b/Sekoia.io/CHANGELOG.md index 7973c24b8..507992f26 100644 --- a/Sekoia.io/CHANGELOG.md +++ b/Sekoia.io/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## 2026-07-22 - 2.74.5 + +### Fixed + +- Fix the way to handle absent or null values in the triggers + ## 2026-07-22 - 2.74.4 ### Fixed diff --git a/Sekoia.io/manifest.json b/Sekoia.io/manifest.json index 17e3e3756..cd9a44e2b 100644 --- a/Sekoia.io/manifest.json +++ b/Sekoia.io/manifest.json @@ -12,7 +12,7 @@ "name": "Sekoia.io", "uuid": "92d8bb47-7c51-445d-81de-ae04edbb6f0a", "slug": "sekoia.io", - "version": "2.74.4", + "version": "2.74.5", "categories": [ "Generic" ] diff --git a/Sekoia.io/sekoiaio/triggers/alerts.py b/Sekoia.io/sekoiaio/triggers/alerts.py index f37c10667..afd6ad9d1 100644 --- a/Sekoia.io/sekoiaio/triggers/alerts.py +++ b/Sekoia.io/sekoiaio/triggers/alerts.py @@ -33,7 +33,7 @@ def handle_event(self, message): Symphony workflow. """ - alert_attrs = message.get("attributes", {}) + alert_attrs = message.get("attributes") or {} event_type: str = message.get("type", "") event_action: str = message.get("action", "") @@ -80,26 +80,26 @@ def handle_event(self, message): "alert_uuid": alert_uuid, "short_id": alert_short_id, "status": { - "name": alert.get("status", {}).get("name"), - "uuid": alert.get("status", {}).get("uuid"), + "name": (alert.get("status") or {}).get("name"), + "uuid": (alert.get("status") or {}).get("uuid"), }, "custom_status": { - "name": alert.get("custom_status", {}).get("label"), - "level": alert.get("custom_status", {}).get("level"), - "stage": alert.get("custom_status", {}).get("stage"), + "name": (alert.get("custom_status") or {}).get("label"), + "level": (alert.get("custom_status") or {}).get("level"), + "stage": (alert.get("custom_status") or {}).get("stage"), "uuid": alert.get("custom_status_uuid"), }, "verdict": { - "name": alert.get("verdict", {}).get("label"), - "level": alert.get("verdict", {}).get("level"), - "stage": alert.get("verdict", {}).get("stage"), + "name": (alert.get("verdict") or {}).get("label"), + "level": (alert.get("verdict") or {}).get("level"), + "stage": (alert.get("verdict") or {}).get("stage"), "uuid": alert.get("verdict_uuid"), }, "created_at": alert.get("created_at"), - "urgency": alert.get("urgency", {}).get("current_value"), - "entity": alert.get("entity", {}), - "alert_type": alert.get("alert_type", {}), - "rule": {"name": alert.get("rule", {}).get("name"), "uuid": alert.get("rule", {}).get("uuid")}, + "urgency": (alert.get("urgency") or {}).get("current_value"), + "entity": (alert.get("entity") or {}), + "alert_type": (alert.get("alert_type") or {}), + "rule": {"name": (alert.get("rule") or {}).get("name"), "uuid": (alert.get("rule") or {}).get("uuid")}, "last_seen_at": alert.get("last_seen_at"), "first_seen_at": alert.get("first_seen_at"), } @@ -193,7 +193,7 @@ def handle_event(self, message): Symphony workflow. """ - alert_attrs = message.get("attributes", {}) + alert_attrs = message.get("attributes") or {} event_type: str = message.get("type", "") event_action: str = message.get("action", "") @@ -251,14 +251,14 @@ def handle_event(self, message): "alert_uuid": alert_uuid, "short_id": alert_short_id, "status": { - "name": alert.get("status", {}).get("name"), - "uuid": alert.get("status", {}).get("uuid"), + "name": (alert.get("status") or {}).get("name"), + "uuid": (alert.get("status") or {}).get("uuid"), }, "created_at": alert.get("created_at"), - "urgency": alert.get("urgency", {}).get("current_value"), - "entity": alert.get("entity", {}), - "alert_type": alert.get("alert_type", {}), - "rule": {"name": alert.get("rule", {}).get("name"), "uuid": alert.get("rule", {}).get("uuid")}, + "urgency": (alert.get("urgency") or {}).get("current_value"), + "entity": (alert.get("entity") or {}), + "alert_type": (alert.get("alert_type") or {}), + "rule": {"name": (alert.get("rule") or {}).get("name"), "uuid": (alert.get("rule") or {}).get("uuid")}, "last_seen_at": alert.get("last_seen_at"), "first_seen_at": alert.get("first_seen_at"), } @@ -605,8 +605,8 @@ def _trigger_time_threshold_for_alert(self, alert_state: dict[str, Any]) -> None self.state_manager.update_alert_state( alert_uuid=alert_uuid, alert_short_id=alert_short_id, - rule_uuid=alert.get("rule", {}).get("uuid", ""), - rule_name=alert.get("rule", {}).get("name", ""), + rule_uuid=(alert.get("rule") or {}).get("uuid", ""), + rule_name=(alert.get("rule") or {}).get("name", ""), event_count=current_count, ) except Exception as exp: @@ -681,7 +681,7 @@ def handle_event(self, message: dict[str, Any]) -> None: # and stale locks. The method self-throttles to once per day. self._cleanup_old_states() - alert_attrs = message.get("attributes", {}) + alert_attrs = message.get("attributes") or {} event_type: str = message.get("type", "") event_action: str = message.get("action", "") @@ -824,8 +824,8 @@ def _should_process_alert(self, alert: dict[str, Any]) -> bool: if not config.rule_filter and not config.rule_names_filter: return True - rule_name = alert.get("rule", {}).get("name") - rule_uuid = alert.get("rule", {}).get("uuid") + rule_name = (alert.get("rule") or {}).get("name") + rule_uuid = (alert.get("rule") or {}).get("uuid") if config.rule_filter: return rule_name == config.rule_filter or rule_uuid == config.rule_filter @@ -946,28 +946,28 @@ def _send_threshold_event( "alert_uuid": alert.get("uuid"), "short_id": alert_short_id, "status": { - "name": alert.get("status", {}).get("name"), - "uuid": alert.get("status", {}).get("uuid"), + "name": (alert.get("status") or {}).get("name"), + "uuid": (alert.get("status") or {}).get("uuid"), }, "custom_status": { - "name": alert.get("custom_status", {}).get("label"), - "level": alert.get("custom_status", {}).get("level"), - "stage": alert.get("custom_status", {}).get("stage"), + "name": (alert.get("custom_status") or {}).get("label"), + "level": (alert.get("custom_status") or {}).get("level"), + "stage": (alert.get("custom_status") or {}).get("stage"), "uuid": alert.get("custom_status_uuid"), }, "verdict": { - "name": alert.get("verdict", {}).get("label"), - "level": alert.get("verdict", {}).get("level"), - "stage": alert.get("verdict", {}).get("stage"), + "name": (alert.get("verdict") or {}).get("label"), + "level": (alert.get("verdict") or {}).get("level"), + "stage": (alert.get("verdict") or {}).get("stage"), "uuid": alert.get("verdict_uuid"), }, "created_at": alert.get("created_at"), - "urgency": alert.get("urgency", {}).get("current_value"), - "entity": alert.get("entity", {}), - "alert_type": alert.get("alert_type", {}), + "urgency": (alert.get("urgency") or {}).get("current_value"), + "entity": (alert.get("entity") or {}), + "alert_type": (alert.get("alert_type") or {}), "rule": { - "name": alert.get("rule", {}).get("name"), - "uuid": alert.get("rule", {}).get("uuid"), + "name": (alert.get("rule") or {}).get("name"), + "uuid": (alert.get("rule") or {}).get("uuid"), }, "last_seen_at": alert.get("last_seen_at"), "first_seen_at": alert.get("first_seen_at"), diff --git a/Sekoia.io/tests/ic_oc_triggers/test_alerts.py b/Sekoia.io/tests/ic_oc_triggers/test_alerts.py index c976e2385..30a0fcadb 100644 --- a/Sekoia.io/tests/ic_oc_triggers/test_alerts.py +++ b/Sekoia.io/tests/ic_oc_triggers/test_alerts.py @@ -56,6 +56,7 @@ def test_securityalertstrigger_handle_alert_invalid_message(alert_trigger): invalid_messages = [ {"event_version": "1", "event_type": "alert"}, {"event_version": "1", "event_type": "alert", "attributes": {}}, + {"event_version": "1", "event_type": "alert", "attributes": None}, { "event_version": "1", "event_type": "alert", @@ -198,6 +199,83 @@ def test_single_event_triggers_updated( trigger.send_event.assert_called_once() +@pytest.mark.parametrize( + "sample", + [ + { + "uuid": "af0a370f-2e44-433c-99b2-2d0e4b348d0c", + "source": "10.227.10.91", + "target": "192.0.0.241", + "short_id": "ALakbd8NXp9W", + "status": None, + "rule": {"name": "Suspicious PowerShell Activity", "uuid": "f3e1c8a0-4b2d-4e5b-9f3c-1a2b3c4d5e6f"}, + "urgency": {"current_value": 70}, + "entity": {"uuid": "e1f2g3h4-5i6j-7k8l-9m0n-o1p2q3r4s5t6", "name": "Test Entity"}, + "alert_type": {"value": "malware"}, + }, + { + "uuid": "af0a370f-2e44-433c-99b2-2d0e4b348d0c", + "source": "10.227.10.91", + "target": "192.0.0.241", + "short_id": "ALakbd8NXp9W", + "status": {"uuid": "1f2f88d5-ff5b-48bf-bbbc-00c2fff82d9f", "name": "Ongoing"}, + "rule": None, + "urgency": {"current_value": 70}, + "entity": {"uuid": "e1f2g3h4-5i6j-7k8l-9m0n-o1p2q3r4s5t6", "name": "Test Entity"}, + "alert_type": {"value": "malware"}, + }, + { + "uuid": "af0a370f-2e44-433c-99b2-2d0e4b348d0c", + "source": "10.227.10.91", + "target": "192.0.0.241", + "short_id": "ALakbd8NXp9W", + "status": {"uuid": "1f2f88d5-ff5b-48bf-bbbc-00c2fff82d9f", "name": "Ongoing"}, + "rule": {"name": "Suspicious PowerShell Activity", "uuid": "f3e1c8a0-4b2d-4e5b-9f3c-1a2b3c4d5e6f"}, + "verdict": None, + "urgency": {"current_value": 70}, + "entity": {"uuid": "e1f2g3h4-5i6j-7k8l-9m0n-o1p2q3r4s5t6", "name": "Test Entity"}, + "alert_type": {"value": "malware"}, + }, + { + "uuid": "af0a370f-2e44-433c-99b2-2d0e4b348d0c", + "source": "10.227.10.91", + "target": "192.0.0.241", + "short_id": "ALakbd8NXp9W", + "status": {"uuid": "1f2f88d5-ff5b-48bf-bbbc-00c2fff82d9f", "name": "Ongoing"}, + "rule": {"name": "Suspicious PowerShell Activity", "uuid": "f3e1c8a0-4b2d-4e5b-9f3c-1a2b3c4d5e6f"}, + "custom_status": None, + "urgency": {"current_value": 70}, + "entity": {"uuid": "e1f2g3h4-5i6j-7k8l-9m0n-o1p2q3r4s5t6", "name": "Test Entity"}, + "alert_type": {"value": "malware"}, + }, + ], +) +def test_trigger_updated_alert_with_null_parameters( + module_configuration, symphony_storage, samplenotif_alert_updated, sample_notifications, sample +): + trigger = AlertUpdatedTrigger() + trigger.configuration = {} + trigger._data_path = symphony_storage + trigger.module.configuration = module_configuration + trigger.module._community_uuid = "cc93fe3f-c26b-4eb1-82f7-082209cf1892" + trigger.send_event = MagicMock() + + alert_uuid = sample.get("uuid") + with requests_mock.Mocker() as mock: + mock.get(f"http://fake.url/api/v1/sic/alerts/{alert_uuid}", json=sample) + + # Calling the trigger with an alert updated notification should create an event + trigger.handle_event(samplenotif_alert_updated) + trigger.send_event.assert_called_once() + + # All other notification types should not + for notification in sample_notifications: + if not (notification["action"] == "updated" and notification["type"] == "alert"): + trigger.handle_event(notification) + + trigger.send_event.assert_called_once() + + def test_single_event_triggers_status_changed( alert_created_trigger, sample_sicalertapi_mock, @@ -271,6 +349,78 @@ def test_single_event_triggers_comments_added( trigger.send_event.assert_called_once() +@pytest.mark.parametrize( + "sample", + [ + { + "uuid": "af0a370f-2e44-433c-99b2-2d0e4b348d0c", + "source": "10.227.10.91", + "target": "192.0.0.241", + "short_id": "ALakbd8NXp9W", + "status": None, + "rule": {"name": "Suspicious PowerShell Activity", "uuid": "f3e1c8a0-4b2d-4e5b-9f3c-1a2b3c4d5e6f"}, + "urgency": {"current_value": 70}, + "entity": {"uuid": "e1f2g3h4-5i6j-7k8l-9m0n-o1p2q3r4s5t6", "name": "Test Entity"}, + "alert_type": {"value": "malware"}, + }, + { + "uuid": "af0a370f-2e44-433c-99b2-2d0e4b348d0c", + "source": "10.227.10.91", + "target": "192.0.0.241", + "short_id": "ALakbd8NXp9W", + "status": {"uuid": "1f2f88d5-ff5b-48bf-bbbc-00c2fff82d9f", "name": "Ongoing"}, + "rule": None, + "urgency": {"current_value": 70}, + "entity": {"uuid": "e1f2g3h4-5i6j-7k8l-9m0n-o1p2q3r4s5t6", "name": "Test Entity"}, + "alert_type": {"value": "malware"}, + }, + ], +) +def test_trigger_comment_created_with_null_parameters( + sample_sicalertapi, + module_configuration, + symphony_storage, + samplenotif_alert_comment_created, + sample_notifications, + sample, +): + trigger = AlertCommentCreatedTrigger() + trigger.configuration = {} + trigger._data_path = symphony_storage + trigger.module.configuration = module_configuration + trigger.module._community_uuid = "cc93fe3f-c26b-4eb1-82f7-082209cf1892" + trigger.send_event = MagicMock() + + alert_uuid = samplenotif_alert_comment_created.get("attributes").get("alert_uuid") + comment_uuid = samplenotif_alert_comment_created.get("attributes").get("uuid") + + with requests_mock.Mocker() as mock: + mock.get(f"http://fake.url/api/v1/sic/alerts/{alert_uuid}", json=sample) + + mock.get( + f"http://fake.url/api/v1/sic/alerts/{alert_uuid}/comments/{comment_uuid}", + json={ + "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", + "content": "string", + "author": "string", + "date": 0, + "created_by": "string", + "created_by_type": "string", + "unseen": True, + }, + ) + # Calling the trigger with an alert commentadded notification should create an event + trigger.handle_event(samplenotif_alert_comment_created) + trigger.send_event.assert_called_once() + + # All other notification types should not + for notification in sample_notifications: + if notification != samplenotif_alert_comment_created: + trigger.handle_event(notification) + + trigger.send_event.assert_called_once() + + def test_invalid_events_dont_triggers_comments_added( module_configuration, symphony_storage,