howler-sentinel-plugin 0.2.0.dev104__py3-none-any.whl → 0.2.0.dev113__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.dev104.dist-info → howler_sentinel_plugin-0.2.0.dev113.dist-info}/METADATA +1 -1
- howler_sentinel_plugin-0.2.0.dev113.dist-info/RECORD +18 -0
- sentinel/actions/azure_emit_hash.py +9 -0
- sentinel/actions/send_to_sentinel.py +1 -1
- sentinel/actions/update_defender_xdr_alert.py +9 -9
- sentinel/actions/update_defender_xdr_incident.py +194 -0
- sentinel/mapping/xdr_alert_evidence.py +44 -41
- sentinel/routes/ingest.py +35 -7
- howler_sentinel_plugin-0.2.0.dev104.dist-info/RECORD +0 -17
- {howler_sentinel_plugin-0.2.0.dev104.dist-info → howler_sentinel_plugin-0.2.0.dev113.dist-info}/LICENSE +0 -0
- {howler_sentinel_plugin-0.2.0.dev104.dist-info → howler_sentinel_plugin-0.2.0.dev113.dist-info}/WHEEL +0 -0
- {howler_sentinel_plugin-0.2.0.dev104.dist-info → howler_sentinel_plugin-0.2.0.dev113.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
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=RRmUSiPDKr7oQjA4f-iSGjEtziUQH77O2s-578pb1Uc,4022
|
|
4
|
+
sentinel/actions/update_defender_xdr_alert.py,sha256=bHBHEZwAAT1pHTF2L4JK5gttzguq12NdV58fx0tj4dQ,6799
|
|
5
|
+
sentinel/actions/update_defender_xdr_incident.py,sha256=HjvK8yTDafYePY5LoMkYat_cFUHnFWK3tzpCfAO3JNE,6854
|
|
6
|
+
sentinel/mapping/sentinel_incident.py,sha256=U7fIh8N4Jdr1A4z1E0jPRP28Ll0Cq7u9Q6292AnyRDI,9548
|
|
7
|
+
sentinel/mapping/xdr_alert.py,sha256=UPoqdZsjUXmJz0dCf_qMlh9Jr0D2HcSNOFvbg8lE4wY,18250
|
|
8
|
+
sentinel/mapping/xdr_alert_evidence.py,sha256=iMn9Wd5NB7Wi9l0Fl0vmJhugX8L6hAO9jYA9AtLLX2o,31429
|
|
9
|
+
sentinel/odm/hit.py,sha256=hAuO2ONMK3Ml8Xu6E7tHrmZ7M6HG5tT38RD9ZxwY254,666
|
|
10
|
+
sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
|
|
11
|
+
sentinel/routes/__init__.py,sha256=JYmKRwIfEsiPos1XuMQ2mlGDbxk6TN_cVEM0K_RNze4,130
|
|
12
|
+
sentinel/routes/ingest.py,sha256=c7GtZakMizDaubXDm_qtw4SXLJyvsHoMheADTIjIiTY,8821
|
|
13
|
+
sentinel/utils/tenant_utils.py,sha256=nGOCbLzUx9OyATLAZ5UbW0WNao_1ioW4wL-htn2ltKU,1324
|
|
14
|
+
howler_sentinel_plugin-0.2.0.dev113.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
|
|
15
|
+
howler_sentinel_plugin-0.2.0.dev113.dist-info/METADATA,sha256=21QcysnK4MtoG5BZ9VBzUYcQA5hAOrDYpmArynPt9HA,749
|
|
16
|
+
howler_sentinel_plugin-0.2.0.dev113.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
+
howler_sentinel_plugin-0.2.0.dev113.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
|
|
18
|
+
howler_sentinel_plugin-0.2.0.dev113.dist-info/RECORD,,
|
|
@@ -71,6 +71,14 @@ def execute(
|
|
|
71
71
|
},
|
|
72
72
|
timeout=5.0,
|
|
73
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
|
+
)
|
|
74
82
|
except Exception:
|
|
75
83
|
logger.exception("Exception on network call for alert %s", hit.howler.id)
|
|
76
84
|
report.append(
|
|
@@ -109,6 +117,7 @@ def specification():
|
|
|
109
117
|
{
|
|
110
118
|
"args": {"url": [], "field": []},
|
|
111
119
|
"options": {"field": [field for field in Hit.flat_fields().keys() if field.endswith("sha256")]},
|
|
120
|
+
"validation": {"warn": {"query": "-_exists_:$field"}},
|
|
112
121
|
}
|
|
113
122
|
],
|
|
114
123
|
"triggers": VALID_TRIGGERS,
|
|
@@ -101,7 +101,7 @@ def execute(query: str, **kwargs) -> list[dict[str, Any]]:
|
|
|
101
101
|
"query": f"howler.id:{hit.howler.id}",
|
|
102
102
|
"outcome": "success",
|
|
103
103
|
"title": "Alert updated in Sentinel",
|
|
104
|
-
"message": "Howler has
|
|
104
|
+
"message": "Howler has successfully propagated changes to this alert to Sentinel.",
|
|
105
105
|
}
|
|
106
106
|
)
|
|
107
107
|
|
|
@@ -165,15 +165,15 @@ def execute(query: str, **kwargs):
|
|
|
165
165
|
"message": f"PATCH request to Microsoft Graph failed with status code {response.status_code}.",
|
|
166
166
|
}
|
|
167
167
|
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
177
|
|
|
178
178
|
return report
|
|
179
179
|
|
|
@@ -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_incident"
|
|
14
|
+
|
|
15
|
+
properties_map = {
|
|
16
|
+
"graph": {
|
|
17
|
+
"status": {
|
|
18
|
+
HitStatus.OPEN: "active",
|
|
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 incident.
|
|
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 incident 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")[0]
|
|
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 incident details
|
|
106
|
+
incident_url = f"https://graph.microsoft.com/v1.0/security/incidents/{hit.sentinel.id}"
|
|
107
|
+
response = requests.get(incident_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
|
+
incident_data = response.json()
|
|
125
|
+
|
|
126
|
+
# Update incident
|
|
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 = incident_data["classification"]
|
|
136
|
+
determination = incident_data["determination"]
|
|
137
|
+
|
|
138
|
+
status = properties_map["graph"]["status"][hit.howler.status]
|
|
139
|
+
assigned_to = incident_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
|
+
incident_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": "Incident updated in XDR Defender",
|
|
174
|
+
"message": "Howler has successfully propagated changes to this incident 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 incident",
|
|
186
|
+
"priority": 8,
|
|
187
|
+
"description": {
|
|
188
|
+
"short": "Update Microsoft Defender XDR incident",
|
|
189
|
+
"long": execute.__doc__,
|
|
190
|
+
},
|
|
191
|
+
"roles": ["automation_basic"],
|
|
192
|
+
"steps": [{"args": {}, "options": {}, "validation": {}}],
|
|
193
|
+
"triggers": VALID_TRIGGERS,
|
|
194
|
+
}
|
|
@@ -232,10 +232,13 @@ class XDRAlertEvidence:
|
|
|
232
232
|
@staticmethod
|
|
233
233
|
def device_evidence(evidence: dict[str, Any]) -> dict[str, Any]:
|
|
234
234
|
"""Convert device evidence to howler evidence format."""
|
|
235
|
+
hostname = evidence.get("hostName")
|
|
236
|
+
if not hostname:
|
|
237
|
+
hostname = evidence.get("deviceDnsName")
|
|
235
238
|
return {
|
|
236
239
|
"host": {
|
|
237
240
|
"id": evidence.get("mdeDeviceId"),
|
|
238
|
-
"name":
|
|
241
|
+
"name": hostname,
|
|
239
242
|
"domain": [evidence.get("ntDomain", "unknown")],
|
|
240
243
|
"os": {
|
|
241
244
|
"platform": evidence.get("osPlatform"),
|
|
@@ -668,51 +671,51 @@ class XDRAlertEvidence:
|
|
|
668
671
|
"status_remediation_details": evidence.get("remediationStatusDetails"),
|
|
669
672
|
},
|
|
670
673
|
"teams": {
|
|
671
|
-
"campaign_id": evidence.get("
|
|
672
|
-
"channel_id": evidence.get("
|
|
673
|
-
"delivery_action": evidence.get("
|
|
674
|
-
"delivery_location": evidence.get("
|
|
675
|
-
"detailed_roles": evidence.get("
|
|
674
|
+
"campaign_id": evidence.get("campaignId"),
|
|
675
|
+
"channel_id": evidence.get("channelId"),
|
|
676
|
+
"delivery_action": evidence.get("deliveryAction"),
|
|
677
|
+
"delivery_location": evidence.get("deliveryLocation"),
|
|
678
|
+
"detailed_roles": evidence.get("detailedRoles"),
|
|
676
679
|
"files": [
|
|
677
680
|
{
|
|
678
|
-
"path": file.get("
|
|
679
|
-
"name": file.get("
|
|
681
|
+
"path": file.get("fileDetails", {}).get("filePath"),
|
|
682
|
+
"name": file.get("fileDetails", {}).get("fileName"),
|
|
680
683
|
"hash": {
|
|
681
|
-
"sha1": file.get("
|
|
682
|
-
"sha256": file.get("
|
|
683
|
-
"md5": file.get("
|
|
684
|
+
"sha1": file.get("fileDetails", {}).get("sha1"),
|
|
685
|
+
"sha256": file.get("fileDetails", {}).get("sha256"),
|
|
686
|
+
"md5": file.get("fileDetails", {}).get("md5"),
|
|
684
687
|
},
|
|
685
688
|
"code_signature": {
|
|
686
|
-
"signing_id": file.get("
|
|
687
|
-
"team_id": file.get("
|
|
689
|
+
"signing_id": file.get("fileDetails", {}).get("signer"),
|
|
690
|
+
"team_id": file.get("fileDetails", {}).get("issuer"),
|
|
688
691
|
},
|
|
689
|
-
"size": file.get("
|
|
692
|
+
"size": file.get("fileDetails", {}).get("fileSize"),
|
|
690
693
|
}
|
|
691
|
-
for file in evidence.get("
|
|
694
|
+
for file in evidence.get("files", [])
|
|
692
695
|
],
|
|
693
|
-
"group_id": evidence.get("
|
|
694
|
-
"is_external": evidence.get("
|
|
695
|
-
"is_owned": evidence.get("
|
|
696
|
-
"last_modified": evidence.get("
|
|
697
|
-
"message_direction": evidence.get("
|
|
698
|
-
"message_id": evidence.get("
|
|
699
|
-
"owning_tenant_id": evidence.get("
|
|
700
|
-
"parent_message_id": evidence.get("
|
|
701
|
-
"received": evidence.get("
|
|
702
|
-
"recipients": evidence.get("
|
|
703
|
-
"sender_from_address": evidence.get("
|
|
704
|
-
"sender_ip": evidence.get("
|
|
705
|
-
"source_add_name": evidence.get("
|
|
706
|
-
"source_id": evidence.get("
|
|
707
|
-
"subject": evidence.get("
|
|
708
|
-
"suspicious_recipients": evidence.get("
|
|
709
|
-
"thread_id": evidence.get("
|
|
710
|
-
"thread_type": evidence.get("
|
|
696
|
+
"group_id": evidence.get("groupId"),
|
|
697
|
+
"is_external": evidence.get("isExternal"),
|
|
698
|
+
"is_owned": evidence.get("isOwned"),
|
|
699
|
+
"last_modified": evidence.get("lastModified"),
|
|
700
|
+
"message_direction": evidence.get("messageDirection"),
|
|
701
|
+
"message_id": evidence.get("messageId"),
|
|
702
|
+
"owning_tenant_id": evidence.get("owningTenantId"),
|
|
703
|
+
"parent_message_id": evidence.get("parentMessageId"),
|
|
704
|
+
"received": evidence.get("received"),
|
|
705
|
+
"recipients": evidence.get("recipients"),
|
|
706
|
+
"sender_from_address": evidence.get("senderFromAddress"),
|
|
707
|
+
"sender_ip": evidence.get("senderIp"),
|
|
708
|
+
"source_add_name": evidence.get("sourceAddName"),
|
|
709
|
+
"source_id": evidence.get("sourceId"),
|
|
710
|
+
"subject": evidence.get("subject"),
|
|
711
|
+
"suspicious_recipients": evidence.get("suspiciousRecipients"),
|
|
712
|
+
"thread_id": evidence.get("threadId"),
|
|
713
|
+
"thread_type": evidence.get("threadType"),
|
|
711
714
|
"urls": [
|
|
712
715
|
{
|
|
713
|
-
"full": url.get("
|
|
716
|
+
"full": url.get("url"),
|
|
714
717
|
}
|
|
715
|
-
for url in evidence.get("
|
|
718
|
+
for url in evidence.get("urls", [])
|
|
716
719
|
],
|
|
717
720
|
},
|
|
718
721
|
}
|
|
@@ -722,22 +725,22 @@ class XDRAlertEvidence:
|
|
|
722
725
|
"""Convert URL evidence to howler evidence format."""
|
|
723
726
|
return {
|
|
724
727
|
"url": {
|
|
725
|
-
"full": evidence.get("
|
|
728
|
+
"full": evidence.get("url"),
|
|
726
729
|
},
|
|
727
730
|
}
|
|
728
731
|
|
|
729
732
|
@staticmethod
|
|
730
733
|
def user_evidence(evidence: dict[str, Any]) -> dict[str, Any]:
|
|
731
734
|
"""Convert user evidence to howler evidence format."""
|
|
732
|
-
user = evidence.get("
|
|
735
|
+
user = evidence.get("userAccount", {})
|
|
733
736
|
if not user:
|
|
734
737
|
user = {}
|
|
735
738
|
return {
|
|
736
739
|
"user": {
|
|
737
|
-
"name": user.get("
|
|
738
|
-
"full_name": user.get("
|
|
739
|
-
"id": user.get("
|
|
740
|
-
"domain": user.get("
|
|
740
|
+
"name": user.get("accountName"),
|
|
741
|
+
"full_name": user.get("userPrincipalName"),
|
|
742
|
+
"id": user.get("azureAdUserId"),
|
|
743
|
+
"domain": user.get("domainName"),
|
|
741
744
|
},
|
|
742
745
|
}
|
|
743
746
|
|
sentinel/routes/ingest.py
CHANGED
|
@@ -3,7 +3,7 @@ import re
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from flask import request
|
|
6
|
-
from howler.api import bad_request, created, make_subapi_blueprint, unauthorized
|
|
6
|
+
from howler.api import bad_request, created, make_subapi_blueprint, ok, unauthorized
|
|
7
7
|
from howler.common.exceptions import HowlerException
|
|
8
8
|
from howler.common.loader import datastore
|
|
9
9
|
from howler.common.logging import get_logger
|
|
@@ -49,7 +49,7 @@ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
|
|
|
49
49
|
"hit_count": 1
|
|
50
50
|
}
|
|
51
51
|
"""
|
|
52
|
-
#
|
|
52
|
+
# TODO this endpoint need to be refactored to make it more readable and maintainable
|
|
53
53
|
apikey = request.headers.get("Authorization", "Basic ", type=str).split(" ")[1]
|
|
54
54
|
|
|
55
55
|
if not apikey or apikey != SECRET:
|
|
@@ -57,7 +57,6 @@ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
|
|
|
57
57
|
|
|
58
58
|
logger.info("Received authorization header with value %s", re.sub(r"^(.{3}).+(.{3})$", r"\1...\2", apikey))
|
|
59
59
|
|
|
60
|
-
# Validate JSON payload
|
|
61
60
|
xdr_incident = request.json
|
|
62
61
|
if not xdr_incident:
|
|
63
62
|
return bad_request(err="No JSON data provided in request body")
|
|
@@ -65,7 +64,6 @@ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
|
|
|
65
64
|
logger.info("XDR Incident received")
|
|
66
65
|
|
|
67
66
|
try:
|
|
68
|
-
# Configure tenant mapping for both mappers
|
|
69
67
|
tenant_mapping = {"020cd98f-1002-45b7-90ff-69fc68bdd027": "Acme Corporation"}
|
|
70
68
|
|
|
71
69
|
incident_mapper = SentinelIncident(tid_mapping=tenant_mapping)
|
|
@@ -74,6 +72,35 @@ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
|
|
|
74
72
|
if bundle_hit is None:
|
|
75
73
|
return bad_request(err="Failed to map XDR incident to Howler bundle format")
|
|
76
74
|
|
|
75
|
+
sentinel_id = xdr_incident.get("id")
|
|
76
|
+
if sentinel_id:
|
|
77
|
+
existing_bundles = datastore().hit.search(f"sentinel.id:{sentinel_id}", as_obj=True)["items"]
|
|
78
|
+
if existing_bundles:
|
|
79
|
+
existing_bundle = existing_bundles[0]
|
|
80
|
+
new_status = xdr_incident.get("status")
|
|
81
|
+
if new_status:
|
|
82
|
+
existing_bundle.howler.status = incident_mapper.map_sentinel_status_to_howler(new_status)
|
|
83
|
+
datastore().hit.save(existing_bundle.howler.id, existing_bundle)
|
|
84
|
+
for child_id in getattr(existing_bundle.howler, "hits", []):
|
|
85
|
+
child_hit = datastore().hit.get(child_id, as_obj=True)
|
|
86
|
+
if child_hit:
|
|
87
|
+
child_hit.howler.status = incident_mapper.map_sentinel_status_to_howler(new_status)
|
|
88
|
+
datastore().hit.save(child_id, child_hit)
|
|
89
|
+
datastore().hit.commit()
|
|
90
|
+
logger.info("Updated status for existing bundle %s and its child hits", existing_bundle.howler.id)
|
|
91
|
+
return ok(
|
|
92
|
+
{
|
|
93
|
+
"success": True,
|
|
94
|
+
"bundle_hit_id": existing_bundle.howler.id,
|
|
95
|
+
"bundle_id": existing_bundle.sentinel.id if hasattr(existing_bundle, "sentinel") else None,
|
|
96
|
+
"individual_hit_ids": getattr(existing_bundle.howler, "hits", []),
|
|
97
|
+
"total_hits_updated": 1 + len(getattr(existing_bundle.howler, "hits", [])),
|
|
98
|
+
"bundle_size": len(getattr(existing_bundle.howler, "hits", [])),
|
|
99
|
+
"organization": getattr(existing_bundle, "organization", {}).get("name", ""),
|
|
100
|
+
"updated": True,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
77
104
|
logger.info("Successfully mapped XDR incident to bundle")
|
|
78
105
|
|
|
79
106
|
alerts = xdr_incident.get("alerts", [])
|
|
@@ -116,8 +143,9 @@ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
|
|
|
116
143
|
bundle_odm.howler.hits.append(hit_id)
|
|
117
144
|
|
|
118
145
|
if len(bundle_odm.howler.hits) < 1:
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
# TODO figure out how to handle incidens without alerts
|
|
147
|
+
logger.info("No valid child hits were created from the XDR incident alerts")
|
|
148
|
+
return ok("Incident contains no valid alerts to create hits from")
|
|
121
149
|
|
|
122
150
|
bundle_odm.howler.bundle_size = len(bundle_odm.howler.hits)
|
|
123
151
|
|
|
@@ -151,7 +179,7 @@ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
|
|
|
151
179
|
"bundle_hit_id": bundle_odm.howler.id,
|
|
152
180
|
"bundle_id": bundle_hit["howler"].get("xdr.incident.id"),
|
|
153
181
|
"individual_hit_ids": child_hit_ids,
|
|
154
|
-
"total_hits_created": len(child_hit_ids) + 1,
|
|
182
|
+
"total_hits_created": len(child_hit_ids) + 1,
|
|
155
183
|
"bundle_size": len(child_hit_ids),
|
|
156
184
|
"organization": bundle_hit["organization"]["name"],
|
|
157
185
|
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
sentinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
sentinel/actions/azure_emit_hash.py,sha256=MeprMe58pac2ntnDanadsPNjXKmIEBSPchT55XMGFWs,3557
|
|
3
|
-
sentinel/actions/send_to_sentinel.py,sha256=s07DTPSVTSC4JwmUQe2q6W6MabfHtE43dmRj3nuE7EM,4021
|
|
4
|
-
sentinel/actions/update_defender_xdr_alert.py,sha256=gXuVZfr7Ou0Lc6BuECmo18E8hfJPhUlM1VMKVCkofk4,6753
|
|
5
|
-
sentinel/mapping/sentinel_incident.py,sha256=U7fIh8N4Jdr1A4z1E0jPRP28Ll0Cq7u9Q6292AnyRDI,9548
|
|
6
|
-
sentinel/mapping/xdr_alert.py,sha256=UPoqdZsjUXmJz0dCf_qMlh9Jr0D2HcSNOFvbg8lE4wY,18250
|
|
7
|
-
sentinel/mapping/xdr_alert_evidence.py,sha256=q622G4eZwFR3TCj418ZCpE83DGVicrWIQZo8Gkj_3FM,31323
|
|
8
|
-
sentinel/odm/hit.py,sha256=hAuO2ONMK3Ml8Xu6E7tHrmZ7M6HG5tT38RD9ZxwY254,666
|
|
9
|
-
sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
|
|
10
|
-
sentinel/routes/__init__.py,sha256=JYmKRwIfEsiPos1XuMQ2mlGDbxk6TN_cVEM0K_RNze4,130
|
|
11
|
-
sentinel/routes/ingest.py,sha256=_9OdOw_9nBJseKIBnmHDLjnqZ_bDdM4wfLpLrek4-ak,7018
|
|
12
|
-
sentinel/utils/tenant_utils.py,sha256=nGOCbLzUx9OyATLAZ5UbW0WNao_1ioW4wL-htn2ltKU,1324
|
|
13
|
-
howler_sentinel_plugin-0.2.0.dev104.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
|
|
14
|
-
howler_sentinel_plugin-0.2.0.dev104.dist-info/METADATA,sha256=ZLe75ogUfw2jpQnn5HFe-im3p0YJpzrg7_p7QjhinAE,749
|
|
15
|
-
howler_sentinel_plugin-0.2.0.dev104.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
16
|
-
howler_sentinel_plugin-0.2.0.dev104.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
|
|
17
|
-
howler_sentinel_plugin-0.2.0.dev104.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|