Skip to content

Commit

Permalink
Merge pull request #1123 from SEKOIA-IO/lv/crowdstrike_falcon_add_ale…
Browse files Browse the repository at this point in the history
…rt_api

CrowdStrike Falcon - Add Alert API support
  • Loading branch information
squioc authored Oct 14, 2024
2 parents e7562ee + 48e2fad commit ee1a3b9
Show file tree
Hide file tree
Showing 10 changed files with 519 additions and 14 deletions.
6 changes: 6 additions & 0 deletions CrowdStrikeFalcon/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

## 2024-10-11 - 1.21.0

### Added

- Support of Alert API

## 2024-09-05 - 1.20.0

### Added
Expand Down
10 changes: 9 additions & 1 deletion CrowdStrikeFalcon/crowdstrike_falcon/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import enum
from collections.abc import Generator
from typing import Any
from posixpath import join as urljoin
from typing import Any

import requests
from requests.auth import AuthBase, HTTPBasicAuth
Expand Down Expand Up @@ -117,6 +117,14 @@ def get_detection_details(self, detection_ids: list[str], **kwargs) -> Generator
**kwargs,
)

def get_alert_details(self, composite_ids: list[str], **kwargs) -> Generator[dict, None, None]:
yield from self.request_endpoint(
"POST",
"/alerts/entities/alerts/v2",
json={"composite_ids": composite_ids},
**kwargs,
)

def find_indicators(self, fql_filter, **kwargs) -> Generator[dict, None, None]:
yield from self.request_endpoint(
"GET",
Expand Down
4 changes: 2 additions & 2 deletions CrowdStrikeFalcon/crowdstrike_falcon/custom_iocs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from collections import defaultdict
from typing import Dict, List
from posixpath import join as urljoin
from datetime import date, timedelta
from posixpath import join as urljoin
from typing import Dict, List

from crowdstrike_falcon.action import CrowdstrikeAction
from crowdstrike_falcon.helpers import stix_to_indicators
Expand Down
113 changes: 110 additions & 3 deletions CrowdStrikeFalcon/crowdstrike_falcon/event_stream_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from functools import cached_property

import orjson
import requests.exceptions
from requests.auth import AuthBase
from requests.exceptions import HTTPError, StreamConsumedError
from sekoia_automation.connector import Connector
Expand All @@ -16,10 +15,15 @@
from crowdstrike_falcon import CrowdStrikeFalconModule
from crowdstrike_falcon.client import CrowdstrikeFalconClient, CrowdstrikeThreatGraphClient
from crowdstrike_falcon.exceptions import StreamNotAvailable
from crowdstrike_falcon.helpers import get_detection_id, group_edges_by_verticle_type, compute_refresh_interval
from crowdstrike_falcon.helpers import (
compute_refresh_interval,
get_detection_id,
get_epp_detection_composite_id,
group_edges_by_verticle_type,
)
from crowdstrike_falcon.logging import get_logger
from crowdstrike_falcon.metrics import EVENTS_LAG, INCOMING_DETECTIONS, INCOMING_VERTICLES, OUTCOMING_EVENTS
from crowdstrike_falcon.models import CrowdStrikeFalconEventStreamConfiguration
from crowdstrike_falcon.logging import get_logger

logger = get_logger()

Expand Down Expand Up @@ -73,6 +77,29 @@ def get_graph_ids_from_detection(self, detection_details: dict) -> set[str]:

return graph_ids

def get_graph_ids_from_alert(self, alert_details: dict) -> set[str]:
"""
Extract graph ids from an alert
:param dict alert_details: The alert
:return: A set of graph ids extracted from the alert
:rtype: set
"""
# extract graph_ids from the resources
graph_ids = set()

# Get the triggering process graph id
triggering_process_graph_id = alert_details.get("triggering_process_graph_id")
if triggering_process_graph_id is not None:
graph_ids.add(triggering_process_graph_id)

# Get the parent process graph id
parent_process_graph_id = alert_details.get("parent_details", {}).get("process_graph_id")
if parent_process_graph_id is not None:
graph_ids.add(parent_process_graph_id)

return graph_ids

def collect_verticles_from_graph_ids(self, graph_ids: set[str]) -> Generator[tuple[str, str, dict], None, None]:
"""
Collect verticles from a list of graph ids
Expand Down Expand Up @@ -131,6 +158,42 @@ def collect_verticles_from_detection(self, detection_id: str) -> Generator[tuple
message=f"Failed to collect verticles for detection {detection_id}",
)

def collect_verticles_from_alert(self, composite_id: str) -> Generator[tuple[str, str, dict], None, None]:
"""
Collect the verticles (events) from an alert
:param str detection_id: The identifier of the alert
"""
try:
# get alert details from its identifier
alert_details = next(self.falcon_client.get_alert_details(composite_ids=[composite_id]))

# get graph ids from detection
graph_ids = self.get_graph_ids_from_alert(alert_details)

# for each group, get their verticles
yield from self.collect_verticles_from_graph_ids(graph_ids)

except HTTPError as error:
if error.response.status_code == 403:
# we don't have proper permissions - roll back to the old API
self.connector.use_alert_api = False
self.log(level="error", message="Not enough permissions to use Alert API - rollback to Detection API")

self.log_exception(
error,
message=(
f"Failed to collect verticles for alert {composite_id}: "
f"{error.response.status_code} {error.response.reason}"
),
)

except Exception as error:
self.log_exception(
error,
message=f"Failed to collect verticles for alert {composite_id}",
)


class EventStreamAuthentication(AuthBase):
def __init__(self, session_token: str):
Expand Down Expand Up @@ -263,6 +326,10 @@ def run(self) -> None:
intake_key=self.connector.configuration.intake_key
).inc()

if self.connector.use_alert_api:
alert_id = get_epp_detection_composite_id(event)
self.collect_verticles_for_epp_detection(alert_id, event)

detection_id = get_detection_id(event)
self.collect_verticles(detection_id, event)

Expand Down Expand Up @@ -329,6 +396,41 @@ def collect_verticles(self, detection_id: str | None, detection_event: dict):

self.log(message=f"Collected {nb_verticles} vertex", level="info")

def collect_verticles_for_epp_detection(self, composite_id: str | None, detection_event: dict):
if composite_id is None:
logger.info("Not a epp detection")
return

if self.verticles_collector is None:
logger.info("verticles collection disabled")
return

logger.info("Collect verticles for detection", composite_id=composite_id)

event_content = detection_event.get("event", {})
severity_name = event_content.get("SeverityName")
severity_code = event_content.get("Severity")

nb_verticles = 0
for (
source_vertex_id,
edge_type,
vertex,
) in self.verticles_collector.collect_verticles_from_alert(composite_id):
nb_verticles += 1
event = {
"metadata": {
"detectionIdString": composite_id,
"eventType": "Vertex",
"edge": {"sourceVertexId": source_vertex_id, "type": edge_type},
"severity": {"name": severity_name, "code": severity_code},
},
"event": vertex,
}
self.events_queue.put((self.stream_root_url, orjson.dumps(event).decode()))

self.log(message=f"Collected {nb_verticles} vertex", level="info")


class EventForwarder(threading.Thread):
def __init__(
Expand Down Expand Up @@ -430,6 +532,11 @@ def __init__(self, *args, **kwargs) -> None:

self._network_sleep_on_retry = 60

# Detect API will be taken down in favor of the Alert API as well as that
# DetectionSummaryEvent will be replaced by EppDetectionSummaryEvent. By default,
# we'll try to use the new API, but we'll fail if permissions are not set.
self.use_alert_api = True

def generate_app_id(self):
return f"sio-{time.time()}"

Expand Down
17 changes: 17 additions & 0 deletions CrowdStrikeFalcon/crowdstrike_falcon/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ def get_detection_id(event: dict) -> str | None:
return event.get("event", {}).get("DetectId")


def get_epp_detection_composite_id(event: dict) -> str | None:
"""
For EPP detection summary event, return the identifier of the detection.
Otherwise, return None
:param dict event: The event
:return: identifier of the detection if the event is a detection summary event, None otherwise
:rtype: str | None
"""
# Is a epp detection summary event?
event_type = event.get("metadata", {}).get("eventType")
if event_type != "EppDetectionSummaryEvent":
return None

return event.get("event", {}).get("CompositeId")


def is_a_supported_stix_indicator(stix_object):
# Check if object is an indicator
if stix_object.get("type") != "indicator":
Expand Down
2 changes: 1 addition & 1 deletion CrowdStrikeFalcon/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "CrowdStrike Falcon",
"slug": "crowdstrike-falcon",
"description": "Integrates with CrowdStrike Falcon EDR",
"version": "1.20.0",
"version": "1.21.0",
"configuration": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
Expand Down
5 changes: 1 addition & 4 deletions CrowdStrikeFalcon/tests/test_actions_hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

from crowdstrike_falcon import CrowdStrikeFalconModule
from crowdstrike_falcon.action import CrowdstrikeAction
from crowdstrike_falcon.host_actions import (
CrowdstrikeActionIsolateHosts,
CrowdstrikeActionDeIsolateHosts,
)
from crowdstrike_falcon.host_actions import CrowdstrikeActionDeIsolateHosts, CrowdstrikeActionIsolateHosts


def configured_action(action: CrowdstrikeAction):
Expand Down
2 changes: 1 addition & 1 deletion CrowdStrikeFalcon/tests/test_actions_iocs.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import os
from datetime import date, timedelta

import pytest
import requests_mock
from datetime import date, timedelta

from crowdstrike_falcon import CrowdStrikeFalconModule
from crowdstrike_falcon.action import CrowdstrikeAction
Expand Down
Loading

0 comments on commit ee1a3b9

Please sign in to comment.