howler-sentinel-plugin 0.2.0.dev113__py3-none-any.whl → 0.2.0.dev137__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: howler-sentinel-plugin
3
- Version: 0.2.0.dev113
3
+ Version: 0.2.0.dev137
4
4
  Summary: A howler plugin for integration with Microsoft's Sentinel API
5
5
  License: MIT
6
6
  Author: CCCS
@@ -3,16 +3,16 @@ sentinel/actions/azure_emit_hash.py,sha256=ES9u3iIkm18qcwVT7-f3r8K5d-haCnjSTC7cy
3
3
  sentinel/actions/send_to_sentinel.py,sha256=RRmUSiPDKr7oQjA4f-iSGjEtziUQH77O2s-578pb1Uc,4022
4
4
  sentinel/actions/update_defender_xdr_alert.py,sha256=bHBHEZwAAT1pHTF2L4JK5gttzguq12NdV58fx0tj4dQ,6799
5
5
  sentinel/actions/update_defender_xdr_incident.py,sha256=HjvK8yTDafYePY5LoMkYat_cFUHnFWK3tzpCfAO3JNE,6854
6
- sentinel/mapping/sentinel_incident.py,sha256=U7fIh8N4Jdr1A4z1E0jPRP28Ll0Cq7u9Q6292AnyRDI,9548
7
- sentinel/mapping/xdr_alert.py,sha256=UPoqdZsjUXmJz0dCf_qMlh9Jr0D2HcSNOFvbg8lE4wY,18250
6
+ sentinel/mapping/sentinel_incident.py,sha256=UtC430gqxV8r_7L56TCXdvwQKgjgXaNzmWmKmgVV3mI,9740
7
+ sentinel/mapping/xdr_alert.py,sha256=J-o76G6gJy2R_bZO7FBgG3Ks8QToIkr5Uy7HRHzmb6w,17994
8
8
  sentinel/mapping/xdr_alert_evidence.py,sha256=iMn9Wd5NB7Wi9l0Fl0vmJhugX8L6hAO9jYA9AtLLX2o,31429
9
9
  sentinel/odm/hit.py,sha256=hAuO2ONMK3Ml8Xu6E7tHrmZ7M6HG5tT38RD9ZxwY254,666
10
10
  sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
11
11
  sentinel/routes/__init__.py,sha256=JYmKRwIfEsiPos1XuMQ2mlGDbxk6TN_cVEM0K_RNze4,130
12
- sentinel/routes/ingest.py,sha256=c7GtZakMizDaubXDm_qtw4SXLJyvsHoMheADTIjIiTY,8821
12
+ sentinel/routes/ingest.py,sha256=lVBr6I5p5WUAUCkgHe4UBUwdjsJCGb_FjwkeXQLXcaI,10902
13
13
  sentinel/utils/tenant_utils.py,sha256=nGOCbLzUx9OyATLAZ5UbW0WNao_1ioW4wL-htn2ltKU,1324
14
- howler_sentinel_plugin-0.2.0.dev113.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
15
- howler_sentinel_plugin-0.2.0.dev113.dist-info/METADATA,sha256=21QcysnK4MtoG5BZ9VBzUYcQA5hAOrDYpmArynPt9HA,749
16
- howler_sentinel_plugin-0.2.0.dev113.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- howler_sentinel_plugin-0.2.0.dev113.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
18
- howler_sentinel_plugin-0.2.0.dev113.dist-info/RECORD,,
14
+ howler_sentinel_plugin-0.2.0.dev137.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
15
+ howler_sentinel_plugin-0.2.0.dev137.dist-info/METADATA,sha256=1lrkeLVxG_TuAedKYDmEKGj9n3OS5ZXPz0SSOYvUo2c,749
16
+ howler_sentinel_plugin-0.2.0.dev137.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ howler_sentinel_plugin-0.2.0.dev137.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
18
+ howler_sentinel_plugin-0.2.0.dev137.dist-info/RECORD,,
@@ -1,5 +1,6 @@
1
- """Sentiel Incident mapper for converting Microsoft Sentinel Sentiel Incidents to Howler bundles."""
1
+ """Sentinel Incident mapper for converting Microsoft Sentinel Incidents to Howler bundles."""
2
2
 
3
+ import json
3
4
  import logging
4
5
  from typing import Any, Optional
5
6
 
@@ -81,6 +82,7 @@ class SentinelIncident:
81
82
  "bundle_size": 0,
82
83
  "hits": [],
83
84
  "labels.generic": self._build_labels(custom_tags, system_tags),
85
+ "data": [json.dumps(sentinel_incident)],
84
86
  },
85
87
  "organization": {"name": customer_name, "id": tenant_id},
86
88
  "sentinel": {
@@ -115,14 +117,18 @@ class SentinelIncident:
115
117
  """
116
118
  return self.tid_mapping.get(tid, self.DEFAULT_CUSTOMER_NAME)
117
119
 
118
- def map_sentinel_status_to_howler(self, sentinel_status: str) -> str:
119
- """Map Sentiel Incident status to Howler status.
120
+ def map_sentinel_status_to_howler(self, sentinel_status: Optional[str]) -> str:
121
+ """Map Sentinel Incident status to Howler status.
120
122
 
121
123
  Args:
122
- sentinel_status (str): Sentinel status string.
124
+ sentinel_status (str | None): Sentinel status string or None.
125
+
123
126
  Returns:
124
127
  str: Howler status string.
125
128
  """
129
+ if not isinstance(sentinel_status, str) or not sentinel_status:
130
+ return "open"
131
+
126
132
  status_mapping: dict[str, str] = {
127
133
  "new": "open",
128
134
  "active": "in-progress",
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import logging
2
3
  from datetime import datetime
3
4
  from typing import Any, Optional
@@ -149,7 +150,7 @@ class XDRAlert:
149
150
  # TODO evaluate if we should set this field if new alert
150
151
  return classification.get(graph_classification, "ambiguous")
151
152
 
152
- def map_alert(self, graph_alert: dict[str, Any], customer_id: str) -> dict[str, Any] | None:
153
+ def map_alert(self, graph_alert: dict[str, Any], customer_id: str) -> Optional[dict[str, Any]]:
153
154
  """Transform a Microsoft Graph alert into a Howler hit format.
154
155
 
155
156
  This is the main mapping function that converts a Graph API alert object
@@ -231,14 +232,7 @@ class XDRAlert:
231
232
  "operation": [],
232
233
  "generic": [],
233
234
  },
234
- "dossier": [
235
- {
236
- "content": [display_name],
237
- "metadata": [graph_alert],
238
- "label": {"en": "MSGraph Alert", "fr": "Alerte MSGraph"},
239
- "format": "markdown",
240
- }
241
- ],
235
+ "data": [json.dumps(graph_alert)],
242
236
  },
243
237
  "evidence": {"data": []},
244
238
  "event": {
sentinel/routes/ingest.py CHANGED
@@ -3,7 +3,7 @@ import re
3
3
  from typing import Any
4
4
 
5
5
  from flask import request
6
- from howler.api import bad_request, created, make_subapi_blueprint, ok, unauthorized
6
+ from howler.api import bad_request, created, internal_error, make_subapi_blueprint, ok, unauthorized
7
7
  from howler.common.exceptions import HowlerException
8
8
  from howler.common.loader import datastore
9
9
  from howler.common.logging import get_logger
@@ -31,27 +31,53 @@ if SECRET.startswith("abcdef"):
31
31
  def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
32
32
  """Ingest a Microsoft Sentinel XDR incident into Howler.
33
33
 
34
- This endpoint receives an XDR incident as JSON, maps it to Howler format using XDRIncidentMapper,
35
- and creates a bundle following the same pattern as the create_bundle endpoint.
36
-
37
- Uses API key authentication via Authorization header.
38
-
39
34
  Variables:
40
- None
41
-
42
- Data Body:
43
- XDR incident JSON data to be ingested
44
-
45
- Result Example:
46
- {
47
- "success": true,
48
- "bundle_id": "generated_bundle_id",
49
- "hit_count": 1
50
- }
35
+ None
36
+
37
+ Arguments:
38
+ None
39
+
40
+ Data Block:
41
+ {
42
+ ...Sentinel XDR incident JSON...
43
+ }
44
+
45
+ Headers:
46
+ Authorization: API in the format "Basic <key>"
47
+
48
+ Result Example (201 Created):
49
+ {
50
+ "success": True,
51
+ "bundle_hit_id": "howler-bundle-id",
52
+ "bundle_id": "sentinel-incident-id",
53
+ "individual_hit_ids": ["alert-hit-id-1", "alert-hit-id-2"],
54
+ "total_hits_created": 3,
55
+ "bundle_size": 2,
56
+ "organization": "Acme Corporation"
57
+ }
58
+
59
+ Result Example (200 OK, update):
60
+ {
61
+ "success": True,
62
+ "bundle_hit_id": "howler-bundle-id",
63
+ "bundle_id": "sentinel-incident-id",
64
+ "individual_hit_ids": ["alert-hit-id-1", "alert-hit-id-2"],
65
+ "total_hits_updated": 3,
66
+ "bundle_size": 2,
67
+ "organization": "Acme Corporation",
68
+ "updated": True
69
+ }
70
+
71
+ Error Codes:
72
+ 400 - Bad request (e.g., missing JSON)
73
+ 401 - Unauthorized (invalid API key)
74
+ 500 - Internal server error
75
+
76
+ Description:
77
+ Receives a Microsoft Sentinel XDR incident as JSON, maps it to Howler format, and creates or updates a bundle
78
+ and its underlying alerts in Howler. Returns details about the created or updated bundle and alerts.
51
79
  """
52
- # TODO this endpoint need to be refactored to make it more readable and maintainable
53
80
  apikey = request.headers.get("Authorization", "Basic ", type=str).split(" ")[1]
54
-
55
81
  if not apikey or apikey != SECRET:
56
82
  return unauthorized(err="API Key does not match expected value.")
57
83
 
@@ -61,139 +87,175 @@ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
61
87
  if not xdr_incident:
62
88
  return bad_request(err="No JSON data provided in request body")
63
89
 
64
- logger.info("XDR Incident received")
65
-
66
90
  try:
91
+ # TODO needs to be replaced with actual tenant mapping logic
67
92
  tenant_mapping = {"020cd98f-1002-45b7-90ff-69fc68bdd027": "Acme Corporation"}
68
-
69
93
  incident_mapper = SentinelIncident(tid_mapping=tenant_mapping)
70
94
  bundle_hit = incident_mapper.map_incident_to_bundle(xdr_incident)
71
-
72
95
  if bundle_hit is None:
73
- return bad_request(err="Failed to map XDR incident to Howler bundle format")
96
+ return internal_error(err="Failed to map XDR incident to Howler bundle format")
74
97
 
75
98
  sentinel_id = xdr_incident.get("id")
76
99
  if sentinel_id:
77
100
  existing_bundles = datastore().hit.search(f"sentinel.id:{sentinel_id}", as_obj=True)["items"]
78
101
  if existing_bundles:
79
- existing_bundle = existing_bundles[0]
80
- new_status = xdr_incident.get("status")
81
- if new_status:
82
- existing_bundle.howler.status = incident_mapper.map_sentinel_status_to_howler(new_status)
83
- datastore().hit.save(existing_bundle.howler.id, existing_bundle)
84
- for child_id in getattr(existing_bundle.howler, "hits", []):
85
- child_hit = datastore().hit.get(child_id, as_obj=True)
86
- if child_hit:
87
- child_hit.howler.status = incident_mapper.map_sentinel_status_to_howler(new_status)
88
- datastore().hit.save(child_id, child_hit)
89
- datastore().hit.commit()
90
- logger.info("Updated status for existing bundle %s and its child hits", existing_bundle.howler.id)
91
- return ok(
92
- {
93
- "success": True,
94
- "bundle_hit_id": existing_bundle.howler.id,
95
- "bundle_id": existing_bundle.sentinel.id if hasattr(existing_bundle, "sentinel") else None,
96
- "individual_hit_ids": getattr(existing_bundle.howler, "hits", []),
97
- "total_hits_updated": 1 + len(getattr(existing_bundle.howler, "hits", [])),
98
- "bundle_size": len(getattr(existing_bundle.howler, "hits", [])),
99
- "organization": getattr(existing_bundle, "organization", {}).get("name", ""),
100
- "updated": True,
101
- }
102
- )
103
-
104
- logger.info("Successfully mapped XDR incident to bundle")
105
-
106
- alerts = xdr_incident.get("alerts", [])
107
- tenant_id = xdr_incident.get("tenantId", "")
108
-
109
- alert_mapper = XDRAlert(tid_mapping=tenant_mapping)
110
-
111
- # Create individual hits from alerts first
112
- child_hit_ids = []
113
-
114
- for i, alert in enumerate(alerts):
115
- try:
116
- mapped_hit = alert_mapper.map_alert(alert, tenant_id)
117
- if mapped_hit:
118
- alert_hit_odm, _ = hit_service.convert_hit(mapped_hit, unique=True, ignore_extra_values=True)
119
-
120
- if alert_hit_odm.event is not None:
121
- alert_hit_odm.event.id = alert_hit_odm.howler.id
122
-
123
- logger.info("Creating individual alert hit %s with ID %s", i, alert_hit_odm.howler.id)
124
- hit_service.create_hit(alert_hit_odm.howler.id, alert_hit_odm, user="system")
125
- analytic_service.save_from_hit(alert_hit_odm, {"uname": "system"})
126
-
127
- child_hit_ids.append(alert_hit_odm.howler.id)
128
- logger.debug("Successfully created alert hit %s: %s", i, alert_hit_odm.howler.id)
129
- else:
130
- logger.warning("Alert mapper returned None for alert %s: %s", i, alert.get("id", "unknown"))
131
- except Exception:
132
- logger.exception("Failed to create individual alert hit %s", i)
133
- continue
102
+ return _update_existing_incident(existing_bundles[0], xdr_incident, incident_mapper)
134
103
 
135
- try:
136
- bundle_odm, _ = hit_service.convert_hit(bundle_hit, unique=True)
137
- bundle_odm.howler.is_bundle = True
104
+ return _create_new_incident(bundle_hit, xdr_incident, tenant_mapping)
138
105
 
139
- if not hasattr(bundle_odm.howler, "hits") or not isinstance(bundle_odm.howler.hits, list):
140
- bundle_odm.howler.hits = []
141
- for hit_id in child_hit_ids:
142
- if hit_id not in bundle_odm.howler.hits:
143
- bundle_odm.howler.hits.append(hit_id)
144
-
145
- if len(bundle_odm.howler.hits) < 1:
146
- # TODO figure out how to handle incidens without alerts
147
- logger.info("No valid child hits were created from the XDR incident alerts")
148
- return ok("Incident contains no valid alerts to create hits from")
149
-
150
- bundle_odm.howler.bundle_size = len(bundle_odm.howler.hits)
151
-
152
- if bundle_odm.event is not None:
153
- bundle_odm.event.id = bundle_odm.howler.id
106
+ except HowlerException as e:
107
+ logger.exception("Failed to process XDR incident")
108
+ return internal_error(err=f"Failed to process XDR incident: {str(e)}")
109
+ except Exception as e:
110
+ logger.exception("Unexpected error during XDR incident ingestion")
111
+ return internal_error(err=f"Internal error occurred during ingestion: {str(e)}")
154
112
 
155
- logger.info("Creating bundle hit with ID %s", bundle_odm.howler.id)
156
- hit_service.create_hit(bundle_odm.howler.id, bundle_odm, user="system")
157
- analytic_service.save_from_hit(bundle_odm, {"uname": "system"})
158
113
 
159
- # Link child hits to bundle (same as create_bundle)
160
- for hit_id in bundle_odm.howler.hits:
161
- child_hit = hit_service.get_hit(hit_id, as_odm=True)
114
+ def _update_existing_incident(
115
+ existing_bundle: Any, xdr_incident: dict[str, Any], incident_mapper: SentinelIncident
116
+ ) -> tuple[dict[str, Any], int]:
117
+ """Update an existing incident and its underlying alerts in Howler.
162
118
 
163
- if child_hit.howler.is_bundle:
164
- logger.warning("Child hit %s is a bundle - skipping bundle assignment", child_hit.howler.id)
165
- continue
119
+ Args:
120
+ existing_bundle: The existing Howler bundle object.
121
+ xdr_incident: The incoming XDR incident data.
122
+ incident_mapper: The incident mapper instance.
166
123
 
167
- new_bundle_list = child_hit.howler.get("bundles", [])
168
- new_bundle_list.append(bundle_odm.howler.id)
169
- child_hit.howler.bundles = new_bundle_list
170
- datastore().hit.save(child_hit.howler.id, child_hit)
124
+ Returns:
125
+ Tuple containing response dictionary and HTTP status code.
126
+ """
127
+ new_status = xdr_incident.get("status")
128
+ if new_status:
129
+ existing_bundle.howler.status = incident_mapper.map_sentinel_status_to_howler(new_status)
130
+ datastore().hit.save(existing_bundle.howler.id, existing_bundle)
131
+ for child_id in getattr(existing_bundle.howler, "hits", []):
132
+ child_hit = datastore().hit.get(child_id, as_obj=True)
133
+ if child_hit:
134
+ child_hit.howler.status = incident_mapper.map_sentinel_status_to_howler(new_status)
135
+ datastore().hit.save(child_id, child_hit)
136
+ datastore().hit.commit()
137
+ logger.info("Updated status for existing bundle %s and its child hits", existing_bundle.howler.id)
138
+ return ok(
139
+ {
140
+ "success": True,
141
+ "bundle_hit_id": existing_bundle.howler.id,
142
+ "bundle_id": existing_bundle.sentinel.id if hasattr(existing_bundle, "sentinel") else None,
143
+ "individual_hit_ids": getattr(existing_bundle.howler, "hits", []),
144
+ "total_hits_updated": 1 + len(getattr(existing_bundle.howler, "hits", [])),
145
+ "bundle_size": len(getattr(existing_bundle.howler, "hits", [])),
146
+ "organization": getattr(existing_bundle, "organization", {}).get("name", ""),
147
+ "updated": True,
148
+ }
149
+ )
150
+
151
+
152
+ def _create_alert_hits(alerts: list[dict[str, Any]], tenant_id: str, alert_mapper: XDRAlert) -> list[str]:
153
+ """Create alert hits from the provided alerts and return their IDs.
154
+
155
+ Args:
156
+ alerts: List of alert dictionaries.
157
+ tenant_id: The tenant ID string.
158
+ alert_mapper: The alert mapper instance.
159
+
160
+ Returns:
161
+ List of created alert hit IDs.
162
+ """
163
+ child_hit_ids = []
164
+ for i, alert in enumerate(alerts):
165
+ try:
166
+ mapped_hit = alert_mapper.map_alert(alert, tenant_id)
167
+ if mapped_hit:
168
+ alert_hit_odm, _ = hit_service.convert_hit(mapped_hit, unique=True, ignore_extra_values=True)
169
+ if alert_hit_odm.event is not None:
170
+ alert_hit_odm.event.id = alert_hit_odm.howler.id
171
+ logger.info("Creating individual alert hit %s with ID %s", i, alert_hit_odm.howler.id)
172
+ hit_service.create_hit(alert_hit_odm.howler.id, alert_hit_odm, user="system")
173
+ analytic_service.save_from_hit(alert_hit_odm, {"uname": "system"})
174
+ child_hit_ids.append(alert_hit_odm.howler.id)
175
+ logger.debug("Successfully created alert hit %s: %s", i, alert_hit_odm.howler.id)
176
+ else:
177
+ logger.warning("Alert mapper returned None for alert %s: %s", i, alert.get("id", "unknown"))
178
+ except Exception:
179
+ logger.exception("Failed to create individual alert hit %s", i)
180
+ continue
181
+ return child_hit_ids
182
+
183
+
184
+ def _link_child_hits_to_bundle(bundle_odm: Any, child_hit_ids: list[str]) -> None:
185
+ """Link child hits to the bundle and update their bundle references.
186
+
187
+ Args:
188
+ bundle_odm: The bundle ODM object.
189
+ child_hit_ids: List of child hit IDs to link.
190
+ """
191
+ for hit_id in bundle_odm.howler.hits:
192
+ child_hit = hit_service.get_hit(hit_id, as_odm=True)
171
193
 
172
- datastore().hit.commit()
173
- action_service.bulk_execute_on_query(f"howler.id:{bundle_odm.howler.id}", user={"uname": "system"})
194
+ if child_hit.howler.is_bundle:
195
+ logger.warning("Child hit %s is a bundle - skipping bundle assignment", child_hit.howler.id)
196
+ continue
174
197
 
175
- logger.info("Successfully completed XDR incident ingestion")
198
+ new_bundle_list = child_hit.howler.get("bundles", [])
199
+ new_bundle_list.append(bundle_odm.howler.id)
200
+ child_hit.howler.bundles = new_bundle_list
201
+ datastore().hit.save(child_hit.howler.id, child_hit)
176
202
 
177
- response_body = {
178
- "success": True,
179
- "bundle_hit_id": bundle_odm.howler.id,
180
- "bundle_id": bundle_hit["howler"].get("xdr.incident.id"),
181
- "individual_hit_ids": child_hit_ids,
182
- "total_hits_created": len(child_hit_ids) + 1,
183
- "bundle_size": len(child_hit_ids),
184
- "organization": bundle_hit["organization"]["name"],
185
- }
186
203
 
187
- return created(response_body)
204
+ def _create_new_incident(
205
+ bundle_hit: dict[str, Any], xdr_incident: dict[str, Any], tenant_mapping: dict[str, str]
206
+ ) -> tuple[dict[str, Any], int]:
207
+ """Create a new incident bundle and its underlying alerts in Howler.
188
208
 
189
- except HowlerException as e:
190
- logger.exception("Failed to create bundle")
191
- return bad_request(err=f"Failed to create bundle: {str(e)}")
209
+ Args:
210
+ bundle_hit: The mapped Howler bundle data.
211
+ xdr_incident: The incoming XDR incident data.
212
+ tenant_mapping: The tenant mapping dictionary.
192
213
 
214
+ Returns:
215
+ Tuple containing response dictionary and HTTP status code.
216
+ """
217
+ alerts = xdr_incident.get("alerts", [])
218
+ tenant_id = xdr_incident.get("tenantId", "")
219
+ alert_mapper = XDRAlert(tid_mapping=tenant_mapping)
220
+ child_hit_ids = _create_alert_hits(alerts, tenant_id, alert_mapper)
221
+ try:
222
+ bundle_odm, _ = hit_service.convert_hit(bundle_hit, unique=True)
223
+ # If there are no alerts, do not treat as bundle
224
+ if child_hit_ids:
225
+ bundle_odm.howler.is_bundle = True
226
+ if not hasattr(bundle_odm.howler, "hits") or not isinstance(bundle_odm.howler.hits, list):
227
+ bundle_odm.howler.hits = []
228
+ for hit_id in child_hit_ids:
229
+ if hit_id not in bundle_odm.howler.hits:
230
+ bundle_odm.howler.hits.append(hit_id)
231
+ bundle_odm.howler.bundle_size = len(bundle_odm.howler.hits)
232
+ else:
233
+ bundle_odm.howler.is_bundle = False
234
+ bundle_odm.howler.hits = []
235
+ bundle_odm.howler.bundle_size = 0
236
+
237
+ if bundle_odm.event is not None:
238
+ bundle_odm.event.id = bundle_odm.howler.id
239
+
240
+ logger.info("Creating incident hit with ID %s", bundle_odm.howler.id)
241
+ hit_service.create_hit(bundle_odm.howler.id, bundle_odm, user="system")
242
+ analytic_service.save_from_hit(bundle_odm, {"uname": "system"})
243
+ if child_hit_ids:
244
+ _link_child_hits_to_bundle(bundle_odm, child_hit_ids)
245
+ datastore().hit.commit()
246
+ if child_hit_ids:
247
+ action_service.bulk_execute_on_query(f"howler.id:{bundle_odm.howler.id}", user={"uname": "system"})
248
+ logger.info("Successfully completed XDR incident ingestion")
249
+ response_body = {
250
+ "success": True,
251
+ "bundle_hit_id": bundle_odm.howler.id,
252
+ "bundle_id": bundle_hit["howler"].get("xdr.incident.id"),
253
+ "individual_hit_ids": child_hit_ids,
254
+ "total_hits_created": len(child_hit_ids) + 1,
255
+ "bundle_size": len(child_hit_ids),
256
+ "organization": bundle_hit["organization"]["name"],
257
+ }
258
+ return created(response_body)
193
259
  except HowlerException as e:
194
- logger.exception("Failed to process XDR incident")
195
- return bad_request(err=f"Failed to process XDR incident: {str(e)}")
196
-
197
- except Exception as e:
198
- logger.exception("Unexpected error during XDR incident ingestion")
199
- return bad_request(err=f"Internal error occurred during ingestion: {str(e)}")
260
+ logger.exception("Failed to create bundle")
261
+ return internal_error(err=f"Failed to create bundle: {str(e)}")