howler-sentinel-plugin 0.2.0.dev170__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.
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: howler-sentinel-plugin
3
+ Version: 0.2.0.dev170
4
+ Summary: A howler plugin for integration with Microsoft's Sentinel API
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: CCCS
8
+ Author-email: analysis-development@cyber.gc.ca
9
+ Requires-Python: >=3.9.17, <4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Howler Sentinel Plugin
21
+
22
+ This plugin contains modules for Microsoft Sentinel integration in Howler.
23
+
@@ -0,0 +1,21 @@
1
+ sentinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sentinel/actions/azure_emit_hash.py,sha256=ES9u3iIkm18qcwVT7-f3r8K5d-haCnjSTC7cyOKxH7A,4000
3
+ sentinel/actions/send_to_sentinel.py,sha256=wrpE-ML19CPFU7l5aVWsZpiNvdyQImelt0hFwhtmXPw,4588
4
+ sentinel/actions/update_defender_xdr_alert.py,sha256=_AS6fL0lWgrKIfd2fgVfWXyLias1uX89bZ0RgjNsB7c,6796
5
+ sentinel/actions/update_defender_xdr_incident.py,sha256=HjvK8yTDafYePY5LoMkYat_cFUHnFWK3tzpCfAO3JNE,6854
6
+ sentinel/config.py,sha256=RLt65CZhD0VxygWngU6RjDkrS24h1H9OMLieV4iTf9A,1788
7
+ sentinel/manifest.yml,sha256=Ps5qp5PjcY6MY9U8wQFt-18VDpEySI0lrj25wMlxod4,222
8
+ sentinel/mapping/sentinel_incident.py,sha256=GkOMbFLeHgKd00H8Bc42yQylr4fvO8yq8y2bM1x7c3s,9741
9
+ sentinel/mapping/xdr_alert.py,sha256=J-o76G6gJy2R_bZO7FBgG3Ks8QToIkr5Uy7HRHzmb6w,17994
10
+ sentinel/mapping/xdr_alert_evidence.py,sha256=iMn9Wd5NB7Wi9l0Fl0vmJhugX8L6hAO9jYA9AtLLX2o,31429
11
+ sentinel/odm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ sentinel/odm/hit.py,sha256=XEStYFIfiEUzcNVYo2Z1s6U7j6B-IDQOhRZR6YRUJn0,867
13
+ sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
14
+ sentinel/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ sentinel/routes/ingest.py,sha256=XlQqOaZT-6_cBUuhe_DAdD81brKIgG1B4d3ZH8JJ0NA,10788
16
+ sentinel/utils/tenant_utils.py,sha256=Wc7v2A8pQ69YdK9LsNCb2Vmg77kV2teHq9Sqn2EKoqc,1725
17
+ howler_sentinel_plugin-0.2.0.dev170.dist-info/METADATA,sha256=b1us70TJd8yQBmLcYWTGBcZVeIdSoGNzqxQjTj4SqDU,822
18
+ howler_sentinel_plugin-0.2.0.dev170.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
19
+ howler_sentinel_plugin-0.2.0.dev170.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
20
+ howler_sentinel_plugin-0.2.0.dev170.dist-info/licenses/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
21
+ howler_sentinel_plugin-0.2.0.dev170.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ type_check=build_scripts.type_check:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Canadian Centre for Cyber Security
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sentinel/__init__.py ADDED
File without changes
@@ -0,0 +1,124 @@
1
+ import os
2
+ from typing import Any, Optional
3
+
4
+ import requests
5
+ from howler.common.loader import datastore
6
+ from howler.common.logging import get_logger
7
+ from howler.odm.models.action import VALID_TRIGGERS
8
+ from howler.odm.models.hit import Hit
9
+ from pydash import get
10
+
11
+ logger = get_logger(__file__)
12
+
13
+ OPERATION_ID = "azure_emit_hash"
14
+
15
+
16
+ def execute(
17
+ query: str,
18
+ url: Optional[str] = os.environ.get("SHA256_LOGIC_APP_URL", None),
19
+ field: str = "file.hash.sha256",
20
+ **kwargs,
21
+ ) -> list[dict[str, Any]]:
22
+ "Emit hashes to sentinel"
23
+ result = datastore().hit.search(query, rows=1)
24
+ hits = result["items"]
25
+
26
+ if not url:
27
+ return [
28
+ {
29
+ "query": query,
30
+ "outcome": "error",
31
+ "title": "Action is not properly configured",
32
+ "message": "url argument cannot be empty.",
33
+ }
34
+ ]
35
+
36
+ if len(hits) < 1:
37
+ return [
38
+ {
39
+ "query": query,
40
+ "outcome": "error",
41
+ "title": "No alert found",
42
+ "message": "No alerts exist in this query.",
43
+ }
44
+ ]
45
+
46
+ report = []
47
+ hit = hits[0]
48
+
49
+ if result["total"] > 1:
50
+ report.append(
51
+ {
52
+ "query": f"{query} AND -howler.id:{hit.howler.id}",
53
+ "outcome": "skipped",
54
+ "title": "Action applies to a single alert",
55
+ "message": "This action supports execution against a single alert at once, not bulk execution.",
56
+ }
57
+ )
58
+
59
+ for hit in hits:
60
+ hash_value = get(hit, field)
61
+ if hash_value:
62
+ try:
63
+ requests.post(
64
+ url, # noqa: F821
65
+ json={
66
+ "indicator": hash_value,
67
+ "type": "FileSha256",
68
+ "description": "Sent from Howler",
69
+ "action": "alert",
70
+ "severity": "high",
71
+ },
72
+ timeout=5.0,
73
+ )
74
+ report.append(
75
+ {
76
+ "query": f"howler.id:{hit.howler.id}",
77
+ "outcome": "success",
78
+ "title": "Webhook Triggered",
79
+ "message": f"Field {field} from alert {hit.howler.id} was successfully sent to url {url}.",
80
+ }
81
+ )
82
+ except Exception:
83
+ logger.exception("Exception on network call for alert %s", hit.howler.id)
84
+ report.append(
85
+ {
86
+ "query": f"howler.id:{hit.howler.id}",
87
+ "outcome": "error",
88
+ "title": "Network error on execution",
89
+ "message": "Alert processing failed due to network errors.",
90
+ }
91
+ )
92
+ else:
93
+ report.append(
94
+ {
95
+ "query": f"howler.id:{hit.howler.id}",
96
+ "outcome": "error",
97
+ "title": "Hash does not exist on alert",
98
+ "message": f"The specified alert does not have a valid sha256 hash at path {field}.",
99
+ }
100
+ )
101
+
102
+ return report
103
+
104
+
105
+ def specification():
106
+ "Specify various properties of the action, such as title, descriptions, permissions and input steps."
107
+ return {
108
+ "id": OPERATION_ID,
109
+ "title": "Emit sha256 hash to Sentinel",
110
+ "priority": 28,
111
+ "description": {
112
+ "short": "Emit sha256 hash to Sentinel",
113
+ "long": execute.__doc__,
114
+ },
115
+ "roles": ["automation_basic"],
116
+ "steps": [
117
+ {
118
+ "args": {"url": [], "field": []},
119
+ "options": {"field": [field for field in Hit.flat_fields().keys() if field.endswith("sha256")]},
120
+ "validation": {"warn": {"query": "-_exists_:$field"}},
121
+ }
122
+ ],
123
+ "triggers": VALID_TRIGGERS,
124
+ }
@@ -0,0 +1,141 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+ from howler.common.exceptions import HowlerRuntimeError
5
+ from howler.common.loader import datastore
6
+ from howler.common.logging import get_logger
7
+ from howler.odm.models.action import VALID_TRIGGERS
8
+ from howler.odm.models.hit import Hit
9
+
10
+ from sentinel.utils.tenant_utils import get_token
11
+
12
+ logger = get_logger(__file__)
13
+
14
+ OPERATION_ID = "send_to_sentinel"
15
+
16
+
17
+ def execute(query: str, **kwargs) -> list[dict[str, Any]]:
18
+ """Send hit to Microsoft Sentinel.
19
+
20
+ Args:
21
+ query (str): The query on which to apply this automation.
22
+ """
23
+ report = []
24
+ ds = datastore()
25
+
26
+ hits: list[Hit] = ds.hit.search(query, as_obj=True)["items"]
27
+ if not hits:
28
+ report.append(
29
+ {
30
+ "query": query,
31
+ "outcome": "error",
32
+ "title": "No hits returned by query",
33
+ "message": f"No hits returned by '{query}'",
34
+ }
35
+ )
36
+ return report
37
+
38
+ from sentinel.config import config
39
+
40
+ for hit in hits:
41
+ if hit.azure and hit.azure.tenant_id:
42
+ tenant_id = hit.azure.tenant_id
43
+ elif hit.organization.id:
44
+ tenant_id = hit.organization.id
45
+ else:
46
+ report.append(
47
+ {
48
+ "query": f"howler.id:{hit.howler.id}",
49
+ "outcome": "skipped",
50
+ "title": "Azure Tenant ID is missing",
51
+ "message": "This alert does not have a set tenant ID.",
52
+ }
53
+ )
54
+ continue
55
+
56
+ try:
57
+ token = get_token(tenant_id, "https://monitor.azure.com/.default")
58
+ except HowlerRuntimeError as err:
59
+ logger.exception("Error on token fetching")
60
+ report.append(
61
+ {
62
+ "query": f"howler.id:{hit.howler.id}",
63
+ "outcome": "error",
64
+ "title": "Invalid Credentials",
65
+ "message": err.message,
66
+ }
67
+ )
68
+ continue
69
+
70
+ ingestor = next((ingestor for ingestor in config.ingestors if ingestor.tenant_id == tenant_id), None)
71
+
72
+ if not ingestor:
73
+ report.append(
74
+ {
75
+ "query": f"howler.id:{hit.howler.id}",
76
+ "outcome": "error",
77
+ "title": "Invalid Tenant ID",
78
+ "message": (
79
+ f"The tenant ID ({tenant_id}) associated with this alert has not been correctly configured."
80
+ ),
81
+ }
82
+ )
83
+ continue
84
+
85
+ uri = (
86
+ f"https://{ingestor.dce}.ingest.monitor.azure.com/dataCollectionRules/{ingestor.dcr}/"
87
+ + f"streams/{ingestor.table}?api-version=2021-11-01-preview"
88
+ )
89
+
90
+ payload = [
91
+ {
92
+ "TimeGenerated": hit.event.ingested.isoformat(),
93
+ "Title": hit.howler.analytic,
94
+ "RawData": {"Hit": hit.as_primitives(), "From": "Howler"},
95
+ }
96
+ ]
97
+ headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
98
+
99
+ response = requests.post(uri, headers=headers, json=payload, timeout=5.0)
100
+ if not response.ok:
101
+ logger.warning(
102
+ "POST request to Azure Monitor failed with status code %s. Content:\n%s",
103
+ response.status_code,
104
+ response.text,
105
+ )
106
+ report.append(
107
+ {
108
+ "query": f"howler.id:{hit.howler.id}",
109
+ "outcome": "error",
110
+ "title": "Azure Monitor API request failed",
111
+ "message": f"POST request to Azure Monitor failed with status code {response.status_code}.",
112
+ }
113
+ )
114
+ continue
115
+
116
+ report.append(
117
+ {
118
+ "query": f"howler.id:{hit.howler.id}",
119
+ "outcome": "success",
120
+ "title": "Alert updated in Sentinel",
121
+ "message": "Howler has successfully propagated changes to this alert to Sentinel.",
122
+ }
123
+ )
124
+
125
+ return report
126
+
127
+
128
+ def specification():
129
+ "Send to Sentinel action specification"
130
+ return {
131
+ "id": OPERATION_ID,
132
+ "title": "Send hit to Microsoft Sentinel",
133
+ "priority": 8,
134
+ "description": {
135
+ "short": "Send hit to Microsoft Sentinel",
136
+ "long": execute.__doc__,
137
+ },
138
+ "roles": ["automation_basic"],
139
+ "steps": [{"args": {}, "options": {}, "validation": {}}],
140
+ "triggers": VALID_TRIGGERS,
141
+ }
@@ -0,0 +1,194 @@
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
+ if hit.azure and hit.azure.tenant_id:
77
+ tenant_id = hit.azure.tenant_id
78
+ elif hit.organization.id:
79
+ tenant_id = hit.organization.id
80
+ else:
81
+ report.append(
82
+ {
83
+ "query": f"howler.id:{hit.howler.id}",
84
+ "outcome": "skipped",
85
+ "title": "Azure Tenant ID is missing",
86
+ "message": "This alert does not have a set tenant ID.",
87
+ }
88
+ )
89
+ continue
90
+
91
+ try:
92
+ token = get_token(tenant_id, "https://graph.microsoft.com/.default")
93
+ except HowlerRuntimeError as err:
94
+ logger.exception("Error on token fetching")
95
+ report.append(
96
+ {
97
+ "query": f"howler.id:{hit.howler.id}",
98
+ "outcome": "error",
99
+ "title": "Invalid Credentials",
100
+ "message": err.message,
101
+ }
102
+ )
103
+ continue
104
+
105
+ # Fetch alert details
106
+ alert_url = f"https://graph.microsoft.com/v1.0/security/alerts_v2/{hit.rule.id}"
107
+ response = requests.get(alert_url, headers={"Authorization": f"Bearer {token}"}, timeout=5.0)
108
+ if not response.ok:
109
+ logger.warning(
110
+ "GET request to Microsoft Graph failed with status code %s. Content:\n%s",
111
+ response.status_code,
112
+ response.text,
113
+ )
114
+ report.append(
115
+ {
116
+ "query": query,
117
+ "outcome": "error",
118
+ "title": "Microsoft Graph API request failed",
119
+ "message": f"GET request to Microsoft Graph failed with status code {response.status_code}.",
120
+ }
121
+ )
122
+ continue
123
+
124
+ alert_data = response.json()
125
+
126
+ # Update alert
127
+ if (
128
+ "assessment" in hit.howler
129
+ and hit.howler.assessment in properties_map["graph"]["classification"]
130
+ and hit.howler.assessment in properties_map["graph"]["determination"]
131
+ ):
132
+ classification = properties_map["graph"]["classification"][hit.howler.assessment]
133
+ determination = properties_map["graph"]["determination"][hit.howler.assessment]
134
+ else:
135
+ classification = alert_data["classification"]
136
+ determination = alert_data["determination"]
137
+
138
+ status = properties_map["graph"]["status"][hit.howler.status]
139
+ assigned_to = alert_data["assignedTo"]
140
+
141
+ data = {
142
+ "assignedTo": assigned_to,
143
+ "classification": classification,
144
+ "determination": determination,
145
+ "status": status,
146
+ }
147
+
148
+ response = requests.patch(
149
+ alert_url,
150
+ json=data,
151
+ headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
152
+ timeout=5.0,
153
+ )
154
+ if not response.ok:
155
+ logger.warning(
156
+ "PATCH request to Microsoft Graph failed with status code %s. Content:\n%s",
157
+ response.status_code,
158
+ response.text,
159
+ )
160
+ report.append(
161
+ {
162
+ "query": query,
163
+ "outcome": "error",
164
+ "title": "Microsoft Graph API request failed",
165
+ "message": f"PATCH request to Microsoft Graph failed with status code {response.status_code}.",
166
+ }
167
+ )
168
+ else:
169
+ report.append(
170
+ {
171
+ "query": f"howler.id:{hit.howler.id}",
172
+ "outcome": "success",
173
+ "title": "Alert updated in XDR Defender",
174
+ "message": "Howler has successfully propagated changes to this alert to XDR Defender.",
175
+ }
176
+ )
177
+
178
+ return report
179
+
180
+
181
+ def specification():
182
+ "Update Defender action specification"
183
+ return {
184
+ "id": OPERATION_ID,
185
+ "title": "Update Microsoft Defender XDR alert",
186
+ "priority": 8,
187
+ "description": {
188
+ "short": "Update Microsoft Defender XDR alert",
189
+ "long": execute.__doc__,
190
+ },
191
+ "roles": ["automation_basic"],
192
+ "steps": [{"args": {}, "options": {}, "validation": {}}],
193
+ "triggers": VALID_TRIGGERS,
194
+ }