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.
- howler_sentinel_plugin-0.2.0.dev170.dist-info/METADATA +23 -0
- howler_sentinel_plugin-0.2.0.dev170.dist-info/RECORD +21 -0
- howler_sentinel_plugin-0.2.0.dev170.dist-info/WHEEL +4 -0
- howler_sentinel_plugin-0.2.0.dev170.dist-info/entry_points.txt +3 -0
- howler_sentinel_plugin-0.2.0.dev170.dist-info/licenses/LICENSE +21 -0
- sentinel/__init__.py +0 -0
- sentinel/actions/azure_emit_hash.py +124 -0
- sentinel/actions/send_to_sentinel.py +141 -0
- sentinel/actions/update_defender_xdr_alert.py +194 -0
- sentinel/actions/update_defender_xdr_incident.py +194 -0
- sentinel/config.py +70 -0
- sentinel/manifest.yml +13 -0
- sentinel/mapping/sentinel_incident.py +240 -0
- sentinel/mapping/xdr_alert.py +415 -0
- sentinel/mapping/xdr_alert_evidence.py +782 -0
- sentinel/odm/__init__.py +0 -0
- sentinel/odm/hit.py +33 -0
- sentinel/odm/models/sentinel.py +15 -0
- sentinel/routes/__init__.py +0 -0
- sentinel/routes/ingest.py +260 -0
- sentinel/utils/tenant_utils.py +56 -0
|
@@ -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,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
|
+
}
|