Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Sekoia.io/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (typo): Consider rephrasing this entry for clearer grammar.

The current wording is slightly awkward. Consider "Fix handling of absent or null values in triggers" or "Fix how absent or null values are handled in triggers" for smoother readability.

Suggested change
- Fix the way to handle absent or null values in the triggers
- Fix handling of absent or null values in triggers


## 2026-07-22 - 2.74.4

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Sekoia.io/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down
76 changes: 38 additions & 38 deletions Sekoia.io/sekoiaio/triggers/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")

Expand Down Expand Up @@ -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"),
Comment on lines +99 to 103
"first_seen_at": alert.get("first_seen_at"),
}
Expand Down Expand Up @@ -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", "")

Expand Down Expand Up @@ -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"),
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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", "")

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down
150 changes: 150 additions & 0 deletions Sekoia.io/tests/ic_oc_triggers/test_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down