Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@
import sys
import uuid
from datetime import timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING

import arrow
from EnvironmentCommon import GetEnvironmentCommonFactory
from soar_sdk.SiemplifyConnectors import SiemplifyConnectorExecution
from soar_sdk.SiemplifyConnectorsDataModel import AlertInfo
from soar_sdk.SiemplifyUtils import (
Expand All @@ -39,6 +38,11 @@
from TIPCommon.transformation import dict_to_flat
from TIPCommon.utils import is_overflowed

try:
from EnvironmentCommon import EnvironmentHandle, GetEnvironmentCommonFactory
except ImportError:
from TIPCommon.envcommon import EnvironmentHandle, GetEnvironmentCommonFactory

from ..core.fire_eye_etp_constants import (
ACCEPTABLE_TIME_INTERVAL_IN_MINUTES,
ALERT_ID_FIELD,
Expand All @@ -54,6 +58,8 @@
from ..core.fire_eye_etp_manager import FireEyeETPConfig, FireEyeETPManager

if TYPE_CHECKING:
from TIPCommon.types import SingleJson

from ..core.datamodels import Alert

MIN_REQUIRED_ARGS = 2
Expand All @@ -76,9 +82,11 @@ def filter_recent_alerts(

"""
filtered_groups: list[list[Alert]] = []
cutoff_time = arrow.utcnow().shift(minutes=-max_minutes_backwards).timestamp
cutoff_timestamp_ms = int((cutoff_time() if callable(cutoff_time) else cutoff_time) * 1000)

for group in alert_groups:
if group[0].occurred_time_unix < arrow.utcnow().shift(minutes=-max_minutes_backwards).timestamp * 1000:
if group[0].occurred_time_unix < cutoff_timestamp_ms:
filtered_groups.append(group)

else:
Expand Down Expand Up @@ -154,7 +162,7 @@ def calculate_priority(alerts_group: list[Alert]) -> int:


def create_alert_info(
environment: Any, # noqa: ANN401
environment: EnvironmentHandle,
alerts_group: list[Alert],
) -> AlertInfo:
"""Create a Siemplify AlertInfo object from a group of alerts.
Expand All @@ -167,7 +175,8 @@ def create_alert_info(
The constructed AlertInfo object.

"""
sorted_alerts_group: list[Alert] = sorted(alerts_group, key=lambda alert: alert.occurred_time_unix)
valid_alerts = [a for a in alerts_group if a.timestamp] or alerts_group
sorted_alerts_group: list[Alert] = sorted(valid_alerts, key=lambda alert: alert.occurred_time_unix)

alert_info: AlertInfo = AlertInfo()
alert_info.display_id = str(uuid.uuid4())
Expand All @@ -181,7 +190,7 @@ def create_alert_info(
alert_info.device_vendor = DEVICE_VENDOR
alert_info.device_product = DEVICE_PRODUCT

events: list[dict[str, Any]] = []
events: list[SingleJson] = []
for alert in sorted_alerts_group:
events.extend(alert.events)

Expand All @@ -197,8 +206,8 @@ def process_single_alert_group(
siemplify: SiemplifyConnectorExecution,
etp_manager: FireEyeETPManager,
alert_group: list[Alert],
params: dict[str, Any],
context: dict[str, Any],
params: SingleJson,
context: SingleJson,
) -> AlertInfo | None:
"""Process a single alert group and return AlertInfo if successful.

Expand Down Expand Up @@ -239,7 +248,7 @@ def process_single_alert_group(
detailed_alert_group.append(detailed_alert)

siemplify.LOGGER.info(f"Creating AlertInfo for alert group {alert_group[0].etp_message_id}")
environment_common: Any = GetEnvironmentCommonFactory.create_environment_manager(
environment_common: EnvironmentHandle = GetEnvironmentCommonFactory.create_environment_manager(
siemplify,
environment_field_name=params["environment_field_name"],
environment_regex_pattern=params["environment_regex_pattern"],
Expand All @@ -266,8 +275,8 @@ def process_alert_groups(
siemplify: SiemplifyConnectorExecution,
etp_manager: FireEyeETPManager,
alert_groups: list[list[Alert]],
params: dict[str, Any],
context: dict[str, Any],
params: SingleJson,
context: SingleJson,
) -> list[AlertInfo]:
"""Process a list of alert groups and return created AlertInfo.

Expand Down Expand Up @@ -342,8 +351,8 @@ def update_connector_timestamp(

def run_connector_cycle(
siemplify: SiemplifyConnectorExecution,
params: dict[str, Any],
context: dict[str, Any],
params: SingleJson,
context: SingleJson,
) -> tuple[list[AlertInfo], list[Alert], list[Alert]]:
"""Run the main connector cycle (fetch, filter, process) and return results.

Expand Down
150 changes: 121 additions & 29 deletions content/response_integrations/google/fire_eye_etp/core/datamodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,21 @@
from __future__ import annotations

import copy
from typing import Any
from typing import TYPE_CHECKING, Any

from soar_sdk.SiemplifyUtils import convert_datetime_to_unix_time

from .fire_eye_etp_constants import ALERT_NAME
from .utils_manager import naive_time_converted_to_aware

if TYPE_CHECKING:
from TIPCommon.types import SingleJson


class BaseModel:
"""Base model for inheritance."""

def __init__(self, raw_data: dict[str, Any]) -> None:
def __init__(self, raw_data: SingleJson) -> None:
"""Initialize the BaseModel.

Args:
Expand All @@ -37,7 +40,7 @@ def __init__(self, raw_data: dict[str, Any]) -> None:
"""
self.raw_data = raw_data

def to_json(self) -> dict[str, Any]:
def to_json(self) -> SingleJson:
"""Convert the model to JSON.

Returns:
Expand All @@ -52,7 +55,7 @@ class Alert(BaseModel):

def __init__(
self,
raw_data: dict[str, Any],
raw_data: SingleJson,
timezone_offset: str | None = None,
) -> None:
"""Initialize the Alert.
Expand All @@ -63,23 +66,92 @@ def __init__(

"""
super().__init__(raw_data)
self.id: str | None = raw_data.get("id")
alert_sub: SingleJson = (
raw_data.get("alert", {}) if isinstance(raw_data.get("alert"), dict) else {}
)
email_header: SingleJson = (
alert_sub.get("email-header")
or raw_data.get("email-header", {})
or raw_data.get("attributes", {}).get("email", {}).get("headers", {})
)
smtp_message: SingleJson = (
alert_sub.get("smtp-message")
or raw_data.get("smtp-message", {})
or raw_data.get("attributes", {}).get("email", {}).get("smtp", {})
)
explanation: SingleJson = (
alert_sub.get("explanation", {})
or raw_data.get("attributes", {}).get("alert", {}).get("explanation", {})
)
malware_detected: SingleJson = (
explanation.get("malware-detected", {})
or explanation.get("malware_detected", {})
)

self.id: str | None = (
raw_data.get("id")
or alert_sub.get("uuid")
or raw_data.get("report_id")
or raw_data.get("attributes", {}).get("alert", {}).get("uuid")
)
self.timestamp: str | None = (
raw_data.get("attributes", {}).get("email", {}).get("timestamp", {}).get("accepted")
alert_sub.get("occurred")
or alert_sub.get("attack-time")
or raw_data.get("alert_date")
or raw_data.get("accepted_time")
or raw_data.get("attributes", {}).get("email", {}).get("timestamp", {}).get("accepted")
or raw_data.get("attributes", {}).get("alert", {}).get("occurred")
)
self.severity: str | None = raw_data.get("attributes", {}).get("alert", {}).get("severity")
self.etp_message_id: str | None = raw_data.get("attributes", {}).get("email", {}).get("etp_message_id")
self.malwares: list[dict[str, Any]] = (
raw_data
.get("attributes", {})

mitre_mappings: list[Any] = raw_data.get("mitre_mapping", [])
mitre_severity: str | None = None
if isinstance(mitre_mappings, list):
for mapping in mitre_mappings:
if isinstance(mapping, dict) and mapping.get("severity"):
mitre_severity = str(mapping.get("severity"))
break

self.severity: str | None = (
mitre_severity
or alert_sub.get("severity")
or raw_data.get("severity")
or raw_data.get("attributes", {}).get("alert", {}).get("severity")
)
self.etp_message_id: str = (
email_header.get("message-id")
or smtp_message.get("queue-id")
or raw_data.get("mta_msg_id")
or raw_data.get("attributes", {}).get("email", {}).get("etp_message_id")
or (self.id or "")
)
legacy_malwares = (
raw_data.get("attributes", {})
.get("alert", {})
.get("explanation", {})
.get("malware_detected", {})
.get("malware", [])
)
self.recipients: list[str] = (
self.malwares: list[SingleJson] = (
malware_detected.get("malware", [])
or raw_data.get("malware", [])
or legacy_malwares
)

smtp_to: Any = smtp_message.get("to") or smtp_message.get("rcpt_to")
if isinstance(smtp_to, list):
smtp_recipients: list[str] = [str(r) for r in smtp_to if r]
elif isinstance(smtp_to, str):
smtp_recipients = smtp_to.split()
else:
smtp_recipients = []

if not smtp_recipients and alert_sub.get("dst", {}).get("smtp-to"):
smtp_recipients = [str(alert_sub["dst"]["smtp-to"])]

legacy_recipients: list[str] = (
raw_data.get("attributes", {}).get("email", {}).get("smtp", {}).get("rcpt_to", "").split()
)
self.recipients: list[str] = smtp_recipients or legacy_recipients
self.name: str = ALERT_NAME
self.timezone_offset: str | None = timezone_offset

Expand All @@ -91,48 +163,63 @@ def priority(self) -> int:
The priority value (60, 80, or 100).

"""
if self.severity == "majr":
if self.severity in {"majr", "high"}:
return 80
if self.severity == "crit":
if self.severity in {"crit", "critical"}:
return 100
if self.severity in {"minr", "medium"}:
return 60
if self.severity in {"info", "low"}:
return 40

return 60

@property
def events(self) -> list[dict[str, Any]]:
def events(self) -> list[SingleJson]:
"""The events from the alert.

Returns:
The list of events.

"""
events: list[dict[str, Any]] = []

for malware in self.malwares:
alert = copy.deepcopy(self.raw_data)
alert.get("attributes", {}).get("alert", {}).get("explanation", {}).pop("os_changes", None)
alert.get("attributes", {}).get("alert", {}).get("explanation", {}).get("malware_detected", {}).pop(
"malware", None
)
malware["alert"] = alert
events.append(malware)
events: list[SingleJson] = []

if self.malwares:
for malware in self.malwares:
malware_copy = copy.deepcopy(malware)
alert_copy = copy.deepcopy(self.raw_data)
if isinstance(alert_copy, dict):
if isinstance(alert_copy.get("alert"), dict):
alert_copy["alert"].get("explanation", {}).get("malware-detected", {}).pop("malware", None)
alert_copy["alert"].get("explanation", {}).get("malware_detected", {}).pop("malware", None)
if isinstance(alert_copy.get("attributes"), dict):
alert_copy["attributes"].get("alert", {}).get("explanation", {}).pop("os_changes", None)
explanation_node = alert_copy["attributes"].get("alert", {}).get("explanation", {})
explanation_node.get("malware_detected", {}).pop("malware", None)
if isinstance(malware_copy, dict):
malware_copy["alert"] = alert_copy
events.append(malware_copy)
else:
events.append({"name": str(malware_copy), "alert": alert_copy})
else:
events.append(copy.deepcopy(self.raw_data))

return events

@property
def recipient_events(self) -> list[dict[str, Any]]:
def recipient_events(self) -> list[SingleJson]:
"""The recipient events from the alert.

Returns:
The list of recipient events.

"""
events: list[dict[str, Any]] = []
events: list[SingleJson] = []

for recipient in self.recipients:
event = {
"event_name": "FireEye ETP Recipient",
"description": ("This is a custom Siemplify Event created for mapping of the recipients"),
"description": "This is a custom Siemplify Event created for mapping of the recipients",
"recipient": recipient,
}
events.append(event)
Expand All @@ -147,4 +234,9 @@ def occurred_time_unix(self) -> int:
The occurred time in Unix time.

"""
return convert_datetime_to_unix_time(naive_time_converted_to_aware(self.timestamp, self.timezone_offset))
if not self.timestamp:
return 0
try:
return convert_datetime_to_unix_time(naive_time_converted_to_aware(self.timestamp, self.timezone_offset))
except (ValueError, TypeError, AttributeError):
return 0
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@

PRINT_TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
API_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
COMPACT_TIMESTAMP_LENGTH = 14
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
if TYPE_CHECKING:
import logging

from TIPCommon.types import SingleJson

from .datamodels import Alert


Expand Down Expand Up @@ -144,7 +146,7 @@ def _get_full_url(self, url_id: str, **kwargs: object) -> str:
def test_connectivity(self) -> None:
"""Test connectivity to the FireEye ETP API."""
request_url: str = self._get_full_url("test_connectivity")
payload: dict[str, Any] = {
payload: SingleJson = {
"date_range": {
"from": "2026-01-01T00:00:00.000Z",
"to": "2026-01-01T00:01:00.000Z",
Expand All @@ -168,7 +170,7 @@ def get_alerts(self, start_time: datetime.datetime, timezone_offset: str) -> lis
request_url: str = self._get_full_url("get_alerts")
start_time_str: str = self._convert_datetime_to_api_format(start_time)
end_time_str: str = self._convert_datetime_to_api_format(datetime.datetime.now(datetime.UTC))
payload: dict[str, Any] = {
payload: SingleJson = {
"date_range": {"from": start_time_str, "to": end_time_str},
"size": DEFAULT_FETCH_SIZE,
}
Expand All @@ -191,8 +193,14 @@ def get_alert_details(self, alert_id: str, timezone_offset: str) -> Alert:
request_url: str = self._get_full_url("get_alert_details", alert_id=alert_id)
response: requests.Response = self.session.get(request_url, timeout=30)
validate_response(response, f"Unable to get alert details for {alert_id}")
res_json: SingleJson = response.json()
alert_data: SingleJson = (
res_json.get("data")
if isinstance(res_json, dict) and "data" in res_json and isinstance(res_json.get("data"), dict)
else res_json
)
return self.parser.build_siemplify_alert_obj(
alert_data=response.json().get("data", {}),
alert_data=alert_data,
timezone_offset=timezone_offset,
)

Expand Down
Loading
Loading