Skip to content

Commit 1c6e1c4

Browse files
Merge branch 'release/3.5.10'
2 parents 8b2199b + aaaf084 commit 1c6e1c4

File tree

3 files changed

+101
-32
lines changed

3 files changed

+101
-32
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
## [3.5.10](https://github.com/TheHive-Project/Cortex-Analyzers/tree/3.5.10) (2025-04-11)
4+
5+
[Full Changelog](https://github.com/TheHive-Project/Cortex-Analyzers/compare/3.5.9...3.5.10)
6+
7+
**Closed issues:**
8+
9+
- \[Bug\] MSEntraID - User Principal Names may produce invalid key [\#1346](https://github.com/TheHive-Project/Cortex-Analyzers/issues/1346)
10+
11+
## [3.5.9](https://github.com/TheHive-Project/Cortex-Analyzers/tree/3.5.9) (2025-04-04)
12+
13+
[Full Changelog](https://github.com/TheHive-Project/Cortex-Analyzers/compare/3.5.8...3.5.9)
14+
15+
**Merged pull requests:**
16+
17+
- Various improvements & fixes [\#1345](https://github.com/TheHive-Project/Cortex-Analyzers/pull/1345) ([nusantara-self](https://github.com/nusantara-self))
18+
319
## [3.5.8](https://github.com/TheHive-Project/Cortex-Analyzers/tree/3.5.8) (2025-03-31)
420

521
[Full Changelog](https://github.com/TheHive-Project/Cortex-Analyzers/compare/3.5.7...3.5.8)

analyzers/MSEntraID/MSEntraID.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ def authenticate(self):
3838

3939
return token_r.json().get('access_token')
4040

41+
def resolve_user_guid(self, email, headers, base_url):
42+
"""Resolves a userPrincipalName (email) to an objectId (GUID) using direct lookup (most compatible)."""
43+
url = f"{base_url}users/{email}?$select=id"
44+
response = requests.get(url, headers=headers)
45+
46+
if response.status_code != 200:
47+
self.error(f"Failed to resolve GUID for user {email}: {response.content}")
48+
49+
user = response.json()
50+
user_id = user.get("id")
51+
if not user_id:
52+
self.error(f"ID not found in response for {email}")
53+
54+
return user_id
55+
56+
def ensure_user_guid(self, base_url, headers):
57+
if "@" in self.user:
58+
return self.resolve_user_guid(self.user, headers, base_url)
59+
return self.user
60+
4161
def handle_get_signins(self, headers, base_url):
4262
"""
4363
Retrieve sign-in logs for a userPrincipalName within a specified time range.
@@ -49,14 +69,15 @@ def handle_get_signins(self, headers, base_url):
4969
self.user = self.get_data()
5070
if not self.user:
5171
self.error("No user supplied")
52-
72+
self.guid = self.ensure_user_guid(base_url, headers)
73+
5374
# Build the filter time
5475
filter_time = datetime.utcnow() - timedelta(days=self.time_range)
5576
format_time = filter_time.strftime('%Y-%m-%dT00:00:00Z')
5677

5778
# Query sign-in logs
5879
endpoint = (
59-
f"auditLogs/signIns?$filter=startsWith(userPrincipalName,'{self.user}') "
80+
f"auditLogs/signIns?$filter=userId eq '{self.guid}'"
6081
f"and createdDateTime ge {format_time}&$top={self.lookup_limit}"
6182
)
6283
r = requests.get(base_url + endpoint, headers=headers)
@@ -168,13 +189,14 @@ def handle_get_userinfo(self, headers, base_url):
168189
self.user = self.get_data()
169190
if not self.user:
170191
self.error("No user supplied")
171-
192+
193+
self.guid = self.ensure_user_guid(base_url, headers)
172194
# Use select to retrieve many user attributes. Adjust as needed.
173195
params = {
174196
"$select": ",".join(self.params_list)
175197
}
176198

177-
user_info_url = f"{base_url}users/{self.user}"
199+
user_info_url = f"{base_url}users/{self.guid}"
178200
# user_info_url = f"{base_url}users/{self.user}"
179201

180202
user_response = requests.get(user_info_url, headers=headers, params=params)
@@ -211,7 +233,7 @@ def handle_get_userinfo(self, headers, base_url):
211233
}
212234

213235
# Fetch user's manager
214-
manager_url = f"{base_url}users/{self.user}/manager?$select=id,displayName,userPrincipalName"
236+
manager_url = f"{base_url}users/{self.guid}/manager?$select=id,displayName,userPrincipalName"
215237
manager_resp = requests.get(manager_url, headers=headers)
216238
if manager_resp.status_code == 200:
217239
manager_data = manager_resp.json()
@@ -224,7 +246,7 @@ def handle_get_userinfo(self, headers, base_url):
224246
}
225247

226248
# Fetch user's license details
227-
license_url = f"{base_url}users/{self.user}/licenseDetails"
249+
license_url = f"{base_url}users/{self.guid}/licenseDetails"
228250
license_resp = requests.get(license_url, headers=headers)
229251
if license_resp.status_code == 200:
230252
license_data = license_resp.json().get("value", [])
@@ -238,7 +260,7 @@ def handle_get_userinfo(self, headers, base_url):
238260
})
239261

240262
# Fetch user's group memberships
241-
member_of_url = f"{base_url}users/{self.user}/memberOf"
263+
member_of_url = f"{base_url}users/{self.guid}/memberOf"
242264
member_of_response = requests.get(member_of_url, headers=headers)
243265
if member_of_response.status_code == 200:
244266
memberships = member_of_response.json().get("value", [])
@@ -249,7 +271,7 @@ def handle_get_userinfo(self, headers, base_url):
249271
})
250272

251273
# MFA Methods
252-
mfa_url = f"{base_url}users/{self.user}/authentication/methods"
274+
mfa_url = f"{base_url}users/{self.guid}/authentication/methods"
253275
mfa_r = requests.get(mfa_url, headers=headers)
254276

255277
if mfa_r.status_code == 200:
@@ -374,9 +396,11 @@ def handle_get_directoryAuditLogs(self, headers, base_url):
374396
self.error('Incorrect dataType. "mail" expected.')
375397
try:
376398
# Pull the userPrincipalName from the observable data (data_type=mail)
377-
user_upn = self.get_data()
378-
if not user_upn:
399+
self.user = self.get_data()
400+
if not self.user:
379401
self.error("No user principal name supplied for directory audit logs")
402+
self.guid = self.ensure_user_guid(base_url, headers)
403+
380404
# Calculate time range (past X days)
381405
filter_time = datetime.utcnow() - timedelta(days=self.time_range)
382406
filter_time_str = filter_time.strftime('%Y-%m-%dT%H:%M:%SZ')
@@ -386,7 +410,7 @@ def handle_get_directoryAuditLogs(self, headers, base_url):
386410
endpoint = (
387411
"auditLogs/directoryAudits?"
388412
f"$filter=activityDateTime ge {filter_time_str} "
389-
f"and initiatedBy/user/userPrincipalName eq '{user_upn}'"
413+
f"and initiatedBy/user/id eq '{self.guid}'"
390414
f"&$top={self.lookup_limit}"
391415
)
392416

@@ -435,16 +459,19 @@ def handle_get_devices(self, headers, base_url):
435459
else:
436460
self.error("No user UPN supplied")
437461

438-
# Build the appropriate endpoint based on the observable type
439-
if self.data_type == 'hostname':
462+
if self.data_type == 'mail':
463+
# Resolve UPN to GUID and use exact match
464+
self.user = query_value
465+
guid = self.ensure_user_guid(base_url, headers)
440466
endpoint = (
441-
"deviceManagement/managedDevices?"
442-
f"$filter=startswith(deviceName,'{query_value}')"
467+
f"deviceManagement/managedDevices?"
468+
f"$filter=userId eq '{guid}'"
443469
)
444-
elif self.data_type == 'mail':
470+
else:
471+
# Use startswith for partial hostname matches
445472
endpoint = (
446-
"deviceManagement/managedDevices?"
447-
f"$filter=startswith(userPrincipalName,'{query_value}')"
473+
f"deviceManagement/managedDevices?"
474+
f"$filter=startswith(deviceName,'{query_value}')"
448475
)
449476

450477
# Perform the GET request

responders/MSEntraID/MSEntraID.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,29 @@ def authenticate(self):
3232

3333
return token_r.json().get('access_token')
3434

35+
def resolve_user_guid(self, email, headers, base_url):
36+
"""Resolves a userPrincipalName (email) to an objectId (GUID) using direct lookup (most compatible)."""
37+
url = f"{base_url}users/{email}?$select=id"
38+
response = requests.get(url, headers=headers)
39+
40+
if response.status_code != 200:
41+
self.error(f"Failed to resolve GUID for user {email}: {response.content}")
42+
43+
user = response.json()
44+
user_id = user.get("id")
45+
if not user_id:
46+
self.error(f"ID not found in response for {email}")
47+
48+
return user_id
49+
50+
def ensure_guid(self, headers, base_url):
51+
if "@" in self.user:
52+
self.guid = self.resolve_user_guid(self.user, headers, base_url)
53+
else:
54+
self.guid = self.user
55+
3556
def check_user_status(self, user, headers, base_url):
36-
r = requests.get(f"{base_url}{user}?$select=accountEnabled", headers=headers)
57+
r = requests.get(f"{base_url}users/{user}?$select=accountEnabled", headers=headers)
3758

3859
if r.status_code == 404:
3960
self.error(f'User {user} not found in Microsoft Entra ID')
@@ -58,16 +79,17 @@ def run(self):
5879
Responder.run(self)
5980
token = self.authenticate()
6081
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
61-
base_url = 'https://graph.microsoft.com/v1.0/users/'
82+
base_url = 'https://graph.microsoft.com/v1.0/'
6283

6384
if self.service == "revokeSignInSessions":
6485
if self.get_param('data.dataType') == 'mail':
6586
try:
6687
self.user = self.get_param('data.data', None, 'No UPN supplied to revoke credentials for')
6788
if not self.user:
6889
self.error("No user supplied")
69-
70-
r = requests.post(f"{base_url}{self.user}/revokeSignInSessions", headers=headers)
90+
self.ensure_guid(headers, base_url)
91+
92+
r = requests.post(f"{base_url}users/{self.guid}/revokeSignInSessions", headers=headers)
7193

7294
if r.status_code != 200:
7395
self.error(f'Failure to revoke access tokens of user {self.user}: {r.content}')
@@ -87,9 +109,10 @@ def run(self):
87109
self.user = self.get_param('data.data', None, 'No UPN supplied for password reset')
88110
if not self.user:
89111
self.error("No user supplied")
90-
112+
self.ensure_guid(headers, base_url)
113+
91114
data = {"passwordProfile": {"forceChangePasswordNextSignIn": True}}
92-
r = requests.patch(f"{base_url}{self.user}", headers=headers, json=data)
115+
r = requests.patch(f"{base_url}users/{self.guid}", headers=headers, json=data)
93116

94117
if r.status_code != 204:
95118
self.error(f'Failure to reset password for user {self.user}: {r.content}')
@@ -105,9 +128,10 @@ def run(self):
105128
self.user = self.get_param('data.data', None, 'No UPN supplied for password reset with MFA')
106129
if not self.user:
107130
self.error("No user supplied")
108-
131+
self.ensure_guid(headers, base_url)
132+
109133
data = {"passwordProfile": {"forceChangePasswordNextSignIn": True, "forceChangePasswordNextSignInWithMfa": True}}
110-
r = requests.patch(f"{base_url}{self.user}", headers=headers, json=data)
134+
r = requests.patch(f"{base_url}users/{self.guid}", headers=headers, json=data)
111135

112136
if r.status_code != 204:
113137
self.error(f'Failure to reset password with MFA for user {self.user}: {r.content}')
@@ -122,14 +146,15 @@ def run(self):
122146
self.user = self.get_param('data.data', None, 'No UPN supplied to enable user')
123147
if not self.user:
124148
self.error("No user supplied")
125-
126-
user_status = self.check_user_status(self.user, headers, base_url)
149+
self.ensure_guid(headers, base_url)
150+
151+
user_status = self.check_user_status(self.guid, headers, base_url)
127152
if user_status is True:
128153
self.report({"message": f"User {self.user} is already enabled"})
129154
return
130155

131156
data = {"accountEnabled": True}
132-
r = requests.patch(f"{base_url}{self.user}", headers=headers, json=data)
157+
r = requests.patch(f"{base_url}users/{self.guid}", headers=headers, json=data)
133158

134159
if r.status_code != 204:
135160
self.error(f'Failure to enable user {self.user}: {r.content}')
@@ -147,14 +172,15 @@ def run(self):
147172
self.user = self.get_param('data.data', None, 'No UPN supplied to disable user')
148173
if not self.user:
149174
self.error("No user supplied")
150-
151-
user_status = self.check_user_status(self.user, headers, base_url)
175+
self.ensure_guid(headers, base_url)
176+
177+
user_status = self.check_user_status(self.guid, headers, base_url)
152178
if user_status is False:
153179
self.report({"message": f"User {self.user} is already disabled"})
154180
return
155181

156182
data = {"accountEnabled": False}
157-
r = requests.patch(f"{base_url}{self.user}", headers=headers, json=data)
183+
r = requests.patch(f"{base_url}users/{self.guid}", headers=headers, json=data)
158184

159185
if r.status_code != 204:
160186
self.error(f'Failure to disable user {self.user}: {r.content}')

0 commit comments

Comments
 (0)