howler-sentinel-plugin 0.2.0.dev31__py3-none-any.whl → 0.2.0.dev87__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.
sentinel/routes/ingest.py CHANGED
@@ -1,42 +1,171 @@
1
1
  import os
2
2
  import re
3
+ from typing import Any
3
4
 
4
5
  from flask import request
5
- from howler.api import make_subapi_blueprint, ok, unauthorized
6
+ from howler.api import bad_request, created, make_subapi_blueprint, unauthorized
7
+ from howler.common.exceptions import HowlerException
8
+ from howler.common.loader import datastore
6
9
  from howler.common.logging import get_logger
7
10
  from howler.common.swagger import generate_swagger_docs
11
+ from howler.services import action_service, analytic_service, hit_service
12
+
13
+ from ..mapping.sentinel_incident import SentinelIncident
14
+ from ..mapping.xdr_alert import XDRAlert
8
15
 
9
16
  SUB_API = "sentinel"
10
17
  sentinel_api = make_subapi_blueprint(SUB_API, api_version=1)
11
- sentinel_api._doc = "Interact with spellbook"
18
+ sentinel_api._doc = "Ingest Microsoft Sentinel XDR incidents into Howler"
12
19
 
13
20
  logger = get_logger(__file__)
14
21
 
15
- SECRET = os.environ["SENTINEL_LINK_KEY"]
22
+ # For testing purposes, replace with actual secret in production
23
+ SECRET = os.environ.get("SENTINEL_LINK_KEY", "abcdefghijklmnopqrstuvwxyz1234567890")
24
+
25
+ if SECRET.startswith("abcdef"):
26
+ logger.warning("Default secret used!")
16
27
 
17
28
 
18
29
  @generate_swagger_docs()
19
30
  @sentinel_api.route("/ingest", methods=["POST"])
20
- def ingest_alert(**kwargs):
21
- """Ingest a sentinel alert into howler
31
+ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
32
+ """Ingest a Microsoft Sentinel XDR incident into Howler.
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.
22
38
 
23
39
  Variables:
24
40
  None
25
41
 
26
- Optional Arguments:
27
- None
42
+ Data Body:
43
+ XDR incident JSON data to be ingested
28
44
 
29
45
  Result Example:
46
+ {
47
+ "success": true,
48
+ "bundle_id": "generated_bundle_id",
49
+ "hit_count": 1
50
+ }
30
51
  """
52
+ # API Key authentication
31
53
  apikey = request.headers.get("Authorization", "Basic ", type=str).split(" ")[1]
32
54
 
33
55
  if not apikey or apikey != SECRET:
34
56
  return unauthorized(err="API Key does not match expected value.")
35
57
 
36
- logger.info("Recieved authorization header with value %s", re.sub(r"^(.{3}).+(.{3})$", r"\1...\2", apikey))
58
+ logger.info("Received authorization header with value %s", re.sub(r"^(.{3}).+(.{3})$", r"\1...\2", apikey))
59
+
60
+ # Validate JSON payload
61
+ xdr_incident = request.json
62
+ if not xdr_incident:
63
+ return bad_request(err="No JSON data provided in request body")
64
+
65
+ logger.info("XDR Incident received")
66
+
67
+ try:
68
+ # Configure tenant mapping for both mappers
69
+ tenant_mapping = {"020cd98f-1002-45b7-90ff-69fc68bdd027": "Acme Corporation"}
70
+
71
+ incident_mapper = SentinelIncident(tid_mapping=tenant_mapping)
72
+ bundle_hit = incident_mapper.map_incident_to_bundle(xdr_incident)
73
+
74
+ if bundle_hit is None:
75
+ return bad_request(err="Failed to map XDR incident to Howler bundle format")
76
+
77
+ logger.info("Successfully mapped XDR incident to bundle")
78
+
79
+ alerts = xdr_incident.get("alerts", [])
80
+ tenant_id = xdr_incident.get("tenantId", "")
81
+
82
+ alert_mapper = XDRAlert(tid_mapping=tenant_mapping)
83
+
84
+ # Create individual hits from alerts first
85
+ child_hit_ids = []
86
+
87
+ for i, alert in enumerate(alerts):
88
+ try:
89
+ mapped_hit = alert_mapper.map_alert(alert, tenant_id)
90
+ if mapped_hit:
91
+ alert_hit_odm, _ = hit_service.convert_hit(mapped_hit, unique=True, ignore_extra_values=True)
92
+
93
+ if alert_hit_odm.event is not None:
94
+ alert_hit_odm.event.id = alert_hit_odm.howler.id
95
+
96
+ logger.info("Creating individual alert hit %s with ID %s", i, alert_hit_odm.howler.id)
97
+ hit_service.create_hit(alert_hit_odm.howler.id, alert_hit_odm, user="system")
98
+ analytic_service.save_from_hit(alert_hit_odm, {"uname": "system"})
99
+
100
+ child_hit_ids.append(alert_hit_odm.howler.id)
101
+ logger.debug("Successfully created alert hit %s: %s", i, alert_hit_odm.howler.id)
102
+ else:
103
+ logger.warning("Alert mapper returned None for alert %s: %s", i, alert.get("id", "unknown"))
104
+ except Exception:
105
+ logger.exception("Failed to create individual alert hit %s", i)
106
+ continue
107
+
108
+ try:
109
+ bundle_odm, _ = hit_service.convert_hit(bundle_hit, unique=True)
110
+ bundle_odm.howler.is_bundle = True
111
+
112
+ if not hasattr(bundle_odm.howler, "hits") or not isinstance(bundle_odm.howler.hits, list):
113
+ bundle_odm.howler.hits = []
114
+ for hit_id in child_hit_ids:
115
+ if hit_id not in bundle_odm.howler.hits:
116
+ bundle_odm.howler.hits.append(hit_id)
117
+
118
+ if len(bundle_odm.howler.hits) < 1:
119
+ logger.error("No valid child hits were created from the XDR incident alerts")
120
+ return bad_request(err="No valid child hits were created from the XDR incident alerts.")
121
+
122
+ bundle_odm.howler.bundle_size = len(bundle_odm.howler.hits)
123
+
124
+ if bundle_odm.event is not None:
125
+ bundle_odm.event.id = bundle_odm.howler.id
126
+
127
+ logger.info("Creating bundle hit with ID %s", bundle_odm.howler.id)
128
+ hit_service.create_hit(bundle_odm.howler.id, bundle_odm, user="system")
129
+ analytic_service.save_from_hit(bundle_odm, {"uname": "system"})
130
+
131
+ # Link child hits to bundle (same as create_bundle)
132
+ for hit_id in bundle_odm.howler.hits:
133
+ child_hit = hit_service.get_hit(hit_id, as_odm=True)
134
+
135
+ if child_hit.howler.is_bundle:
136
+ logger.warning("Child hit %s is a bundle - skipping bundle assignment", child_hit.howler.id)
137
+ continue
138
+
139
+ new_bundle_list = child_hit.howler.get("bundles", [])
140
+ new_bundle_list.append(bundle_odm.howler.id)
141
+ child_hit.howler.bundles = new_bundle_list
142
+ datastore().hit.save(child_hit.howler.id, child_hit)
143
+
144
+ datastore().hit.commit()
145
+ action_service.bulk_execute_on_query(f"howler.id:{bundle_odm.howler.id}", user={"uname": "system"})
146
+
147
+ logger.info("Successfully completed XDR incident ingestion")
148
+
149
+ response_body = {
150
+ "success": True,
151
+ "bundle_hit_id": bundle_odm.howler.id,
152
+ "bundle_id": bundle_hit["howler"].get("xdr.incident.id"),
153
+ "individual_hit_ids": child_hit_ids,
154
+ "total_hits_created": len(child_hit_ids) + 1, # +1 for the bundle itself
155
+ "bundle_size": len(child_hit_ids),
156
+ "organization": bundle_hit["organization"]["name"],
157
+ }
158
+
159
+ return created(response_body)
37
160
 
38
- sentinel_alert = request.json
161
+ except HowlerException as e:
162
+ logger.exception("Failed to create bundle")
163
+ return bad_request(err=f"Failed to create bundle: {str(e)}")
39
164
 
40
- logger.info("Sentinel Alert:\n%s", sentinel_alert)
165
+ except HowlerException as e:
166
+ logger.exception("Failed to process XDR incident")
167
+ return bad_request(err=f"Failed to process XDR incident: {str(e)}")
41
168
 
42
- return ok()
169
+ except Exception as e:
170
+ logger.exception("Unexpected error during XDR incident ingestion")
171
+ return bad_request(err=f"Internal error occurred during ingestion: {str(e)}")
@@ -1,12 +0,0 @@
1
- sentinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sentinel/actions/ingestion.py,sha256=_t7wrmozhoT_MmktDmNxYiXrA1Q0LCiDt0rTwHBkwbc,1669
3
- sentinel/actions/synchronization.py,sha256=g5c34410zINWb4fSEzj94drnk5alRj_ju9xMrB39z0s,1818
4
- sentinel/odm/hit.py,sha256=hAuO2ONMK3Ml8Xu6E7tHrmZ7M6HG5tT38RD9ZxwY254,666
5
- sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
6
- sentinel/routes/__init__.py,sha256=JYmKRwIfEsiPos1XuMQ2mlGDbxk6TN_cVEM0K_RNze4,130
7
- sentinel/routes/ingest.py,sha256=zcKQmLStIh1uU_kWO6KBgqr-ZBkAGcFwvXWBiwjOuC8,1067
8
- howler_sentinel_plugin-0.2.0.dev31.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
9
- howler_sentinel_plugin-0.2.0.dev31.dist-info/METADATA,sha256=ZYY3PQKPTdpwQc5x55fldX7rRx5EfTcSuEChFiDwfdo,694
10
- howler_sentinel_plugin-0.2.0.dev31.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
11
- howler_sentinel_plugin-0.2.0.dev31.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
12
- howler_sentinel_plugin-0.2.0.dev31.dist-info/RECORD,,