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.
- {howler_sentinel_plugin-0.2.0.dev31.dist-info → howler_sentinel_plugin-0.2.0.dev87.dist-info}/METADATA +2 -1
- howler_sentinel_plugin-0.2.0.dev87.dist-info/RECORD +15 -0
- sentinel/mapping/sentinel_incident.py +234 -0
- sentinel/mapping/xdr_alert.py +421 -0
- sentinel/mapping/xdr_alert_evidence.py +779 -0
- sentinel/routes/ingest.py +140 -11
- howler_sentinel_plugin-0.2.0.dev31.dist-info/RECORD +0 -12
- {howler_sentinel_plugin-0.2.0.dev31.dist-info → howler_sentinel_plugin-0.2.0.dev87.dist-info}/LICENSE +0 -0
- {howler_sentinel_plugin-0.2.0.dev31.dist-info → howler_sentinel_plugin-0.2.0.dev87.dist-info}/WHEEL +0 -0
- {howler_sentinel_plugin-0.2.0.dev31.dist-info → howler_sentinel_plugin-0.2.0.dev87.dist-info}/entry_points.txt +0 -0
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
|
|
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 = "
|
|
18
|
+
sentinel_api._doc = "Ingest Microsoft Sentinel XDR incidents into Howler"
|
|
12
19
|
|
|
13
20
|
logger = get_logger(__file__)
|
|
14
21
|
|
|
15
|
-
|
|
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
|
|
21
|
-
"""Ingest a
|
|
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
|
-
|
|
27
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,,
|
|
File without changes
|
{howler_sentinel_plugin-0.2.0.dev31.dist-info → howler_sentinel_plugin-0.2.0.dev87.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|