howler-sentinel-plugin 0.2.0.dev87__py3-none-any.whl → 0.2.0.dev95__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.dev87
3
+ Version: 0.2.0.dev95
4
4
  Summary: A howler plugin for integration with Microsoft's Sentinel API
5
5
  License: MIT
6
6
  Author: CCCS
@@ -1,6 +1,8 @@
1
1
  sentinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sentinel/actions/ingestion.py,sha256=_t7wrmozhoT_MmktDmNxYiXrA1Q0LCiDt0rTwHBkwbc,1669
3
+ sentinel/actions/send_to_sentinel.py,sha256=REclVHxuG0Q5PIXZ-13crQhoz8BmJJJyTc6suzpq0m0,3227
3
4
  sentinel/actions/synchronization.py,sha256=g5c34410zINWb4fSEzj94drnk5alRj_ju9xMrB39z0s,1818
5
+ sentinel/actions/update_defender_xdr_alert.py,sha256=y6xCUFp6xpR1u1uAZd9CeK_sV9acwHamkXBTD_3cHg8,6848
4
6
  sentinel/mapping/sentinel_incident.py,sha256=3QBnP6qFpJgE3pHvx5VvFnB3m2TVOoWxs8OysDlJVV8,9547
5
7
  sentinel/mapping/xdr_alert.py,sha256=UPoqdZsjUXmJz0dCf_qMlh9Jr0D2HcSNOFvbg8lE4wY,18250
6
8
  sentinel/mapping/xdr_alert_evidence.py,sha256=q622G4eZwFR3TCj418ZCpE83DGVicrWIQZo8Gkj_3FM,31323
@@ -8,8 +10,9 @@ sentinel/odm/hit.py,sha256=hAuO2ONMK3Ml8Xu6E7tHrmZ7M6HG5tT38RD9ZxwY254,666
8
10
  sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
9
11
  sentinel/routes/__init__.py,sha256=JYmKRwIfEsiPos1XuMQ2mlGDbxk6TN_cVEM0K_RNze4,130
10
12
  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,,
13
+ sentinel/utils/tenant_utils.py,sha256=W7kBtxYNhs3vcgMf78eIRqiTpDtqjzEI2H2d0papQ_Q,1224
14
+ howler_sentinel_plugin-0.2.0.dev95.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
15
+ howler_sentinel_plugin-0.2.0.dev95.dist-info/METADATA,sha256=ivfqDB5AuHiiNXVlA5CcbdB0N-yS1qQuZ86gau729mM,748
16
+ howler_sentinel_plugin-0.2.0.dev95.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ howler_sentinel_plugin-0.2.0.dev95.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
18
+ howler_sentinel_plugin-0.2.0.dev95.dist-info/RECORD,,
@@ -0,0 +1,102 @@
1
+ import requests
2
+ from howler.common.exceptions import HowlerRuntimeError
3
+ from howler.common.loader import datastore
4
+ from howler.common.logging import get_logger
5
+ from howler.odm.models.action import VALID_TRIGGERS
6
+ from howler.odm.models.hit import Hit
7
+
8
+ from sentinel.utils.tenant_utils import get_token
9
+
10
+ logger = get_logger(__file__)
11
+
12
+ OPERATION_ID = "send_to_sentinel"
13
+
14
+
15
+ def execute(query: str, **kwargs):
16
+ """Send hit to Microsoft Sentinel.
17
+
18
+ Args:
19
+ query (str): The query on which to apply this automation.
20
+ """
21
+ report = []
22
+ ds = datastore()
23
+
24
+ hits: list[Hit] = ds.hit.search(query, as_obj=True)["items"]
25
+ if not hits:
26
+ report.append(
27
+ {
28
+ "query": query,
29
+ "outcome": "error",
30
+ "title": "No hits returned by query",
31
+ "message": f"No hits returned by '{query}'",
32
+ }
33
+ )
34
+ return report
35
+
36
+ for hit in hits:
37
+ try:
38
+ token, credentials = get_token(hit.azure.tenant_id)
39
+ except HowlerRuntimeError as err:
40
+ logger.exception("Error on token fetching")
41
+ report.append(
42
+ {
43
+ "query": f"howler.id:{hit.howler.id}",
44
+ "outcome": "error",
45
+ "title": "Invalid Credentials",
46
+ "message": err.message,
47
+ }
48
+ )
49
+ continue
50
+
51
+ uri = (
52
+ f"https://{credentials['dce']}.ingest.monitor.azure.com/dataCollectionRules/{credentials['dcr']}/"
53
+ + f"streams/{credentials['table']}?api-version=2021-11-01-preview"
54
+ )
55
+
56
+ payload = [
57
+ {
58
+ "TimeGenerated": hit.event.ingested.isoformat(),
59
+ "Title": hit.howler.analytic,
60
+ "RawData": {"Hit": hit.as_primitives(), "From": "Howler"},
61
+ }
62
+ ]
63
+ headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
64
+
65
+ response = requests.post(uri, headers=headers, json=payload, timeout=5.0)
66
+ if not response.ok:
67
+ report.append(
68
+ {
69
+ "query": f"howler.id:{hit.howler.id}",
70
+ "outcome": "error",
71
+ "title": "Azure Monitor API request failed",
72
+ "message": f"POST request to Azure Monitor failed with status code {response.status_code}.",
73
+ }
74
+ )
75
+ continue
76
+
77
+ report.append(
78
+ {
79
+ "query": f"howler.id:{hit.howler.id}",
80
+ "outcome": "success",
81
+ "title": "Alert updated in Sentinel",
82
+ "message": "Howler has successfuly propagated changes to this alert to Sentinel.",
83
+ }
84
+ )
85
+
86
+ return report
87
+
88
+
89
+ def specification():
90
+ "Send to Sentinel action specification"
91
+ return {
92
+ "id": OPERATION_ID,
93
+ "title": "Send hit to Microsoft Sentinel",
94
+ "priority": 8,
95
+ "description": {
96
+ "short": "Send hit to Microsoft Sentinel",
97
+ "long": execute.__doc__,
98
+ },
99
+ "roles": ["automation_basic"],
100
+ "steps": [{"args": {}, "options": {}, "validation": {}}],
101
+ "triggers": VALID_TRIGGERS,
102
+ }
@@ -0,0 +1,192 @@
1
+ import requests
2
+ from howler.common.exceptions import HowlerRuntimeError
3
+ from howler.common.loader import datastore
4
+ from howler.common.logging import get_logger
5
+ from howler.odm.models.action import VALID_TRIGGERS
6
+ from howler.odm.models.hit import Hit
7
+ from howler.odm.models.howler_data import Assessment, HitStatus
8
+
9
+ from sentinel.utils.tenant_utils import get_token
10
+
11
+ logger = get_logger(__file__)
12
+
13
+ OPERATION_ID = "update_defender_xdr_alert"
14
+
15
+ properties_map = {
16
+ "graph": {
17
+ "status": {
18
+ HitStatus.OPEN: "new",
19
+ HitStatus.IN_PROGRESS: "inProgress",
20
+ HitStatus.ON_HOLD: "inProgress",
21
+ HitStatus.RESOLVED: "resolved",
22
+ },
23
+ "classification": {
24
+ Assessment.AMBIGUOUS: "unknown",
25
+ Assessment.SECURITY: "informationalExpectedActivity",
26
+ Assessment.DEVELOPMENT: "informationalExpectedActivity",
27
+ Assessment.FALSE_POSITIVE: "falsePositive",
28
+ Assessment.LEGITIMATE: "informationalExpectedActivity",
29
+ Assessment.TRIVIAL: "falsePositive",
30
+ Assessment.RECON: "truePositive",
31
+ Assessment.ATTEMPT: "truePositive",
32
+ Assessment.COMPROMISE: "truePositive",
33
+ Assessment.MITIGATED: "truePositive",
34
+ None: "unknown",
35
+ },
36
+ "determination": {
37
+ Assessment.AMBIGUOUS: "unknown",
38
+ Assessment.SECURITY: "securityTesting",
39
+ Assessment.DEVELOPMENT: "confirmedUserActivity",
40
+ Assessment.FALSE_POSITIVE: "other",
41
+ Assessment.LEGITIMATE: "lineOfBusinessApplication",
42
+ Assessment.TRIVIAL: "other",
43
+ Assessment.RECON: "multiStagedAttack",
44
+ Assessment.ATTEMPT: "other",
45
+ Assessment.COMPROMISE: "maliciousUserActivity",
46
+ Assessment.MITIGATED: "other",
47
+ None: "unknown",
48
+ },
49
+ },
50
+ }
51
+
52
+
53
+ def execute(query: str, **kwargs):
54
+ """Update Microsoft Defender XDR alert.
55
+
56
+ Args:
57
+ query (str): The query on which to apply this automation.
58
+ """
59
+ report = []
60
+ ds = datastore()
61
+
62
+ hits: list[Hit] = ds.hit.search(query, as_obj=True)["items"]
63
+
64
+ if not hits:
65
+ report.append(
66
+ {
67
+ "query": query,
68
+ "outcome": "error",
69
+ "title": "No hits returned by query",
70
+ "message": f"No hits returned by '{query}'",
71
+ }
72
+ )
73
+ return report
74
+
75
+ for hit in hits:
76
+ try:
77
+ token, credentials = get_token(hit.azure.tenant_id)
78
+ except HowlerRuntimeError as err:
79
+ logger.exception("Error on token fetching")
80
+ report.append(
81
+ {
82
+ "query": f"howler.id:{hit.howler.id}",
83
+ "outcome": "error",
84
+ "title": "Invalid Credentials",
85
+ "message": err.message,
86
+ }
87
+ )
88
+ continue
89
+
90
+ token_request_url = f"https://login.microsoftonline.com/{hit.azure.tenant_id}/oauth2/v2.0/token"
91
+ data = {
92
+ "grant_type": "client_credentials",
93
+ "client_id": credentials["client_id"],
94
+ "client_secret": credentials["client_secret"],
95
+ "scope": "https://graph.microsoft.com/.default",
96
+ }
97
+ response = requests.post(token_request_url, data=data, timeout=5.0)
98
+
99
+ if not response.ok:
100
+ logger.warning("Failed to authenticate to Microsoft Graph.")
101
+ report.append(
102
+ {
103
+ "query": query,
104
+ "outcome": "error",
105
+ "title": "Authentication failed",
106
+ "message": f"Authentication to Microsoft Graph API failed with status code {response.status_code}.",
107
+ }
108
+ )
109
+ continue
110
+
111
+ token = response.json()["access_token"]
112
+
113
+ # Fetch alert details
114
+ alert_url = f"https://graph.microsoft.com/v1.0/security/alerts_v2/{hit.rule.id}"
115
+ response = requests.get(alert_url, headers={"Authorization": f"Bearer {token}"}, timeout=5.0)
116
+ if not response.ok:
117
+ logger.warning("GET request to Microsoft Graph failed with status code %s.", response.status_code)
118
+ report.append(
119
+ {
120
+ "query": query,
121
+ "outcome": "error",
122
+ "title": "Microsoft Graph API request failed",
123
+ "message": f"GET request to Microsoft Graph failed with status code {response.status_code}.",
124
+ }
125
+ )
126
+ continue
127
+ alert_data = response.json()
128
+
129
+ # Update alert
130
+ if (
131
+ "assessment" in hit.howler
132
+ and hit.howler.assessment in properties_map["graph"]["classification"]
133
+ and hit.howler.assessment in properties_map["graph"]["determination"]
134
+ ):
135
+ classification = properties_map["graph"]["classification"][hit.howler.assessment]
136
+ determination = properties_map["graph"]["determination"][hit.howler.assessment]
137
+ else:
138
+ classification = alert_data["classification"]
139
+ determination = alert_data["determination"]
140
+
141
+ status = properties_map["graph"]["status"][hit.howler.status]
142
+ assigned_to = alert_data["assignedTo"]
143
+
144
+ data = {
145
+ "assignedTo": assigned_to,
146
+ "classification": classification,
147
+ "determination": determination,
148
+ "status": status,
149
+ }
150
+
151
+ response = requests.patch(
152
+ alert_url,
153
+ json=data,
154
+ headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
155
+ timeout=5.0,
156
+ )
157
+ if not response.ok:
158
+ report.append(
159
+ {
160
+ "query": query,
161
+ "outcome": "error",
162
+ "title": "Microsoft Graph API request failed",
163
+ "message": f"PATCH request to Microsoft Graph failed with status code {response.status_code}.",
164
+ }
165
+ )
166
+
167
+ report.append(
168
+ {
169
+ "query": f"howler.id:{hit.howler.id}",
170
+ "outcome": "success",
171
+ "title": "Alert updated in XDR Defender",
172
+ "message": "Howler has successfuly propagated changes to this alert to XDR Defender.",
173
+ }
174
+ )
175
+
176
+ return report
177
+
178
+
179
+ def specification():
180
+ "Update Defender action specification"
181
+ return {
182
+ "id": OPERATION_ID,
183
+ "title": "Update Microsoft Defender XDR alert",
184
+ "priority": 8,
185
+ "description": {
186
+ "short": "Update Microsoft Defender XDR alert",
187
+ "long": execute.__doc__,
188
+ },
189
+ "roles": ["automation_basic"],
190
+ "steps": [{"args": {}, "options": {}, "validation": {}}],
191
+ "triggers": VALID_TRIGGERS,
192
+ }
@@ -0,0 +1,38 @@
1
+ import json
2
+ import os
3
+
4
+ import requests
5
+ from howler.common.exceptions import HowlerRuntimeError
6
+ from howler.common.logging import get_logger
7
+ from howler.config import cache
8
+
9
+ logger = get_logger(__file__)
10
+
11
+
12
+ @cache.memoize(15 * 60)
13
+ def get_token(tenant_id: str) -> tuple[str, dict[str, str]]:
14
+ """Get a borealis token based on the current howler token"""
15
+ # Get bearer token
16
+ try:
17
+ credentials = json.loads(os.environ["HOWLER_SENTINEL_INGEST_CREDENTIALS"])
18
+ except (KeyError, json.JSONDecodeError):
19
+ raise HowlerRuntimeError("Credential data not configured.")
20
+
21
+ token_request_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
22
+ response = requests.post(
23
+ token_request_url,
24
+ data={
25
+ "grant_type": "client_credentials",
26
+ "client_id": credentials["client_id"],
27
+ "client_secret": credentials["client_secret"],
28
+ "scope": "https://monitor.azure.com/.default",
29
+ },
30
+ timeout=5.0,
31
+ )
32
+
33
+ if not response.ok:
34
+ raise HowlerRuntimeError(f"Authentication to Azure Monitor API failed with status code {response.status_code}.")
35
+
36
+ token = response.json()["access_token"]
37
+
38
+ return token, credentials