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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: howler-sentinel-plugin
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev87
|
|
4
4
|
Summary: A howler plugin for integration with Microsoft's Sentinel API
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: CCCS
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
|
|
15
16
|
Description-Content-Type: text/markdown
|
|
16
17
|
|
|
17
18
|
# Howler Sentinel Plugin
|
|
@@ -0,0 +1,15 @@
|
|
|
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/mapping/sentinel_incident.py,sha256=3QBnP6qFpJgE3pHvx5VvFnB3m2TVOoWxs8OysDlJVV8,9547
|
|
5
|
+
sentinel/mapping/xdr_alert.py,sha256=UPoqdZsjUXmJz0dCf_qMlh9Jr0D2HcSNOFvbg8lE4wY,18250
|
|
6
|
+
sentinel/mapping/xdr_alert_evidence.py,sha256=q622G4eZwFR3TCj418ZCpE83DGVicrWIQZo8Gkj_3FM,31323
|
|
7
|
+
sentinel/odm/hit.py,sha256=hAuO2ONMK3Ml8Xu6E7tHrmZ7M6HG5tT38RD9ZxwY254,666
|
|
8
|
+
sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
|
|
9
|
+
sentinel/routes/__init__.py,sha256=JYmKRwIfEsiPos1XuMQ2mlGDbxk6TN_cVEM0K_RNze4,130
|
|
10
|
+
sentinel/routes/ingest.py,sha256=_9OdOw_9nBJseKIBnmHDLjnqZ_bDdM4wfLpLrek4-ak,7018
|
|
11
|
+
howler_sentinel_plugin-0.2.0.dev87.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
|
|
12
|
+
howler_sentinel_plugin-0.2.0.dev87.dist-info/METADATA,sha256=pjJIO2K9im-8T5bIEUMxXy92S07JkF-9cpRs84dQpys,748
|
|
13
|
+
howler_sentinel_plugin-0.2.0.dev87.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
14
|
+
howler_sentinel_plugin-0.2.0.dev87.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
|
|
15
|
+
howler_sentinel_plugin-0.2.0.dev87.dist-info/RECORD,,
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Sentiel Incident mapper for converting Microsoft Sentinel Sentiel Incidents to Howler bundles."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from dateutil import parser
|
|
7
|
+
|
|
8
|
+
# Use standard logging for now
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SentinelIncident:
|
|
13
|
+
"""Class to handle mapping of Sentiel Incidents to Howler bundles."""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CUSTOMER_NAME = "Unknown Customer"
|
|
16
|
+
|
|
17
|
+
def __init__(self, tid_mapping: Optional[dict[str, str]] = None):
|
|
18
|
+
"""Initialize the Sentiel Incident mapper.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
tid_mapping: Mapping of tenant IDs to customer names
|
|
22
|
+
"""
|
|
23
|
+
self.tid_mapping = tid_mapping or {}
|
|
24
|
+
|
|
25
|
+
# --- Public mapping methods ---
|
|
26
|
+
|
|
27
|
+
def map_incident_to_bundle(self, sentinel_incident: dict[str, Any]) -> Optional[dict[str, Any]]:
|
|
28
|
+
"""Map an Sentiel Incident to a Howler bundle.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
sentinel_incident (dict[str, Any]): The Sentiel Incident data from Microsoft Graph.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Optional[dict[str, Any]]: Mapped bundle dictionary or None if mapping fails.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> mapper = SentinelIncident()
|
|
38
|
+
>>> bundle = mapper.map_incident_to_bundle(sentinel_incident)
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
if not sentinel_incident:
|
|
42
|
+
logger.error("Empty incident data provided")
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
required_fields = ["id", "tenantId", "createdDateTime"]
|
|
46
|
+
for field in required_fields:
|
|
47
|
+
if not sentinel_incident.get(field):
|
|
48
|
+
logger.warning("Missing required field '%s' in incident: %s", field, sentinel_incident)
|
|
49
|
+
|
|
50
|
+
tenant_id: str = sentinel_incident.get("tenantId", "")
|
|
51
|
+
customer_name: str = self.get_customer_name(tenant_id)
|
|
52
|
+
|
|
53
|
+
incident_id: Optional[str] = sentinel_incident.get("id")
|
|
54
|
+
status: str = sentinel_incident.get("status", "active")
|
|
55
|
+
display_name: str = sentinel_incident.get("displayName", "")
|
|
56
|
+
created_datetime: Optional[str] = sentinel_incident.get("createdDateTime")
|
|
57
|
+
assigned_to: Optional[str] = sentinel_incident.get("assignedTo")
|
|
58
|
+
classification: str = sentinel_incident.get("classification", "unknown")
|
|
59
|
+
severity: str = sentinel_incident.get("severity", "medium")
|
|
60
|
+
custom_tags: list[str] = sentinel_incident.get("customTags") or []
|
|
61
|
+
system_tags: list[str] = sentinel_incident.get("systemTags") or []
|
|
62
|
+
description: str = sentinel_incident.get("description", "")
|
|
63
|
+
resolving_comment: str = sentinel_incident.get("resolvingComment", "")
|
|
64
|
+
if not isinstance(custom_tags, list):
|
|
65
|
+
logger.warning("customTags is not a list: %s", custom_tags)
|
|
66
|
+
custom_tags = []
|
|
67
|
+
if not isinstance(system_tags, list):
|
|
68
|
+
logger.warning("systemTags is not a list: %s", system_tags)
|
|
69
|
+
system_tags = []
|
|
70
|
+
|
|
71
|
+
bundle: dict[str, Any] = {
|
|
72
|
+
"howler": {
|
|
73
|
+
"status": self.map_sentinel_status_to_howler(status),
|
|
74
|
+
"detection": display_name,
|
|
75
|
+
"assignment": self.map_sentinel_user_to_howler(assigned_to),
|
|
76
|
+
"score": self.map_severity_to_score(severity),
|
|
77
|
+
"outline.summary": description,
|
|
78
|
+
"rationale": resolving_comment,
|
|
79
|
+
"analytic": "MSGraph",
|
|
80
|
+
"is_bundle": True,
|
|
81
|
+
"bundle_size": 0,
|
|
82
|
+
"hits": [],
|
|
83
|
+
"labels.generic": self._build_labels(custom_tags, system_tags),
|
|
84
|
+
},
|
|
85
|
+
"organization": {"name": customer_name, "id": tenant_id},
|
|
86
|
+
"sentinel": {
|
|
87
|
+
"id": incident_id,
|
|
88
|
+
},
|
|
89
|
+
"evidence": {"cloud": {"account": {"id": tenant_id}}},
|
|
90
|
+
"event": {
|
|
91
|
+
"created": created_datetime,
|
|
92
|
+
"start": created_datetime,
|
|
93
|
+
"end": created_datetime,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
self._map_graph_host_link(sentinel_incident, bundle)
|
|
97
|
+
self._map_timestamps(sentinel_incident, bundle)
|
|
98
|
+
# Add assessment conditionally if classification is not null
|
|
99
|
+
if classification is not None:
|
|
100
|
+
bundle["howler"]["assessment"] = self.map_classification(classification)
|
|
101
|
+
logger.info("Successfully mapped Sentiel Incident %s", incident_id)
|
|
102
|
+
return bundle
|
|
103
|
+
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
logger.error("Failed to map Sentiel Incident: %s", exc, exc_info=True)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def get_customer_name(self, tid: str) -> str:
|
|
109
|
+
"""Get customer name from tenant ID, return default if not found.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
tid (str): Tenant ID.
|
|
113
|
+
Returns:
|
|
114
|
+
str: Customer name or default.
|
|
115
|
+
"""
|
|
116
|
+
return self.tid_mapping.get(tid, self.DEFAULT_CUSTOMER_NAME)
|
|
117
|
+
|
|
118
|
+
def map_sentinel_status_to_howler(self, sentinel_status: str) -> str:
|
|
119
|
+
"""Map Sentiel Incident status to Howler status.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
sentinel_status (str): Sentinel status string.
|
|
123
|
+
Returns:
|
|
124
|
+
str: Howler status string.
|
|
125
|
+
"""
|
|
126
|
+
status_mapping: dict[str, str] = {
|
|
127
|
+
"new": "open",
|
|
128
|
+
"active": "in-progress",
|
|
129
|
+
"inProgress": "in-progress",
|
|
130
|
+
"resolved": "resolved",
|
|
131
|
+
"closed": "resolved",
|
|
132
|
+
}
|
|
133
|
+
return status_mapping.get(sentinel_status, "open")
|
|
134
|
+
|
|
135
|
+
def map_sentinel_user_to_howler(self, sentinel_user: Optional[str]) -> str:
|
|
136
|
+
"""Map Sentinel user assignment to Howler format.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
sentinel_user (Optional[str]): Sentinel user assignment.
|
|
140
|
+
Returns:
|
|
141
|
+
str: Howler assignment string.
|
|
142
|
+
"""
|
|
143
|
+
if not sentinel_user or sentinel_user in ["null", "None"]:
|
|
144
|
+
return "unassigned"
|
|
145
|
+
return sentinel_user
|
|
146
|
+
|
|
147
|
+
def map_severity_to_score(self, severity: str) -> int:
|
|
148
|
+
"""Map string severity to numeric score.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
severity (str): Severity string.
|
|
152
|
+
Returns:
|
|
153
|
+
int: Numeric score.
|
|
154
|
+
"""
|
|
155
|
+
severity_mapping: dict[str, int] = {"low": 25, "medium": 50, "high": 75, "critical": 100}
|
|
156
|
+
return severity_mapping.get(severity.lower() if severity else "medium", 50)
|
|
157
|
+
|
|
158
|
+
def map_classification(self, classification: str) -> str:
|
|
159
|
+
"""Map Sentinel classification to Howler assessment.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
classification (str): Sentinel classification string.
|
|
163
|
+
Returns:
|
|
164
|
+
str: Howler assessment string.
|
|
165
|
+
"""
|
|
166
|
+
classification_mapping: dict[str, str] = {
|
|
167
|
+
"unknown": "ambiguous",
|
|
168
|
+
"truePositive": "compromise",
|
|
169
|
+
"falsePositive": "false-positive",
|
|
170
|
+
"informationalExpectedActivity": "legitimate",
|
|
171
|
+
"benignPositive": "legitimate",
|
|
172
|
+
}
|
|
173
|
+
return classification_mapping.get(classification, "")
|
|
174
|
+
|
|
175
|
+
# --- Private helper methods ---
|
|
176
|
+
|
|
177
|
+
def _map_graph_host_link(self, graph_alert: dict[str, Any], howler_hit: dict[str, Any]) -> None:
|
|
178
|
+
"""Map Graph host link from Graph alert to Howler hit.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
graph_alert (dict[str, Any]): Graph alert data.
|
|
182
|
+
howler_hit (dict[str, Any]): Howler hit/bundle to update.
|
|
183
|
+
"""
|
|
184
|
+
link: dict[str, str] = {
|
|
185
|
+
"icon": "https://security.microsoft.com/favicon.ico",
|
|
186
|
+
"title": "Open in Microsoft Sentinel portal",
|
|
187
|
+
"href": graph_alert.get("incidentWebUrl", ""),
|
|
188
|
+
}
|
|
189
|
+
if graph_alert.get("incidentWebUrl"):
|
|
190
|
+
howler_hit["howler"]["links"] = howler_hit["howler"].get("links", [])
|
|
191
|
+
howler_hit["howler"]["links"].append(link)
|
|
192
|
+
|
|
193
|
+
def _map_timestamps(self, graph_alert: dict[str, Any], howler_hit: dict[str, Any]) -> None:
|
|
194
|
+
"""Map timestamps from Graph alert to Howler hit.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
graph_alert (dict[str, Any]): Graph alert data.
|
|
198
|
+
howler_hit (dict[str, Any]): Howler hit/bundle to update.
|
|
199
|
+
"""
|
|
200
|
+
for time_field in [
|
|
201
|
+
"createdDateTime",
|
|
202
|
+
"lastUpdateDateTime",
|
|
203
|
+
"firstActivityDateTime",
|
|
204
|
+
"lastActivityDateTime",
|
|
205
|
+
]:
|
|
206
|
+
timestamp: Optional[str] = graph_alert.get(time_field)
|
|
207
|
+
if timestamp:
|
|
208
|
+
try:
|
|
209
|
+
dt_obj = parser.isoparse(timestamp)
|
|
210
|
+
if time_field == "createdDateTime":
|
|
211
|
+
howler_hit["event"]["created"] = dt_obj.isoformat()
|
|
212
|
+
elif time_field == "firstActivityDateTime":
|
|
213
|
+
howler_hit["event"]["start"] = dt_obj.isoformat()
|
|
214
|
+
elif time_field == "lastActivityDateTime":
|
|
215
|
+
howler_hit["event"]["end"] = dt_obj.isoformat()
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
logger.warning("Invalid timestamp format for %s: %s (%s)", time_field, timestamp, exc)
|
|
218
|
+
|
|
219
|
+
def _build_labels(self, custom_tags: list[str], system_tags: list[str]) -> list[str]:
|
|
220
|
+
"""Build combined labels from custom and system tags.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
custom_tags (list[str]): Custom tags.
|
|
224
|
+
system_tags (list[str]): System tags.
|
|
225
|
+
Returns:
|
|
226
|
+
list[str]: Combined label list.
|
|
227
|
+
"""
|
|
228
|
+
labels: list[str] = []
|
|
229
|
+
if custom_tags:
|
|
230
|
+
labels.extend(custom_tags)
|
|
231
|
+
if system_tags:
|
|
232
|
+
labels.extend(["system_%s" % tag for tag in system_tags])
|
|
233
|
+
labels.append("sentinel_incident")
|
|
234
|
+
return labels
|