Skip to content

Commit ea60f2d

Browse files
andoniafHugoPBrito
andauthored
feat(m365): add defenderxdr_critical_asset_management_pending_approvals security check (#10085)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com> Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
1 parent e8c0a37 commit ea60f2d

File tree

8 files changed

+432
-7
lines changed

8 files changed

+432
-7
lines changed

docs/user-guide/providers/microsoft365/authentication.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ When using service principal authentication, add these **Application Permissions
4646
- `SecurityIdentitiesHealth.Read.All`: Required for `defenderidentity_health_issues_no_open` check.
4747
- `SecurityIdentitiesSensors.Read.All`: Required for `defenderidentity_health_issues_no_open` check.
4848
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
49-
- `ThreatHunting.Read.All`: Required for Entra checks that use Defender XDR Advanced Hunting (e.g., unused privileged permissions detection). Also requires App Governance to be enabled in Microsoft Defender for Cloud Apps.
49+
- `ThreatHunting.Read.All`: Required for Defender XDR checks (`defenderxdr_endpoint_privileged_user_exposed_credentials`, `defenderxdr_critical_asset_management_pending_approvals`).
5050

5151
**External API Permissions:**
5252

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
2323
- CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061)
2424
- ECS Exec (ECS-006) privilege escalation detection via `ecs:ExecuteCommand` + `ecs:DescribeTasks` [(#10066)](https://github.com/prowler-cloud/prowler/pull/10066)
2525
- `defenderxdr_endpoint_privileged_user_exposed_credentials` check for M365 provider [(#10084)](https://github.com/prowler-cloud/prowler/pull/10084)
26+
- `defenderxdr_critical_asset_management_pending_approvals` check for M365 provider [(#10085)](https://github.com/prowler-cloud/prowler/pull/10085)
2627
- `entra_seamless_sso_disabled` check for m365 provider [(#10086)](https://github.com/prowler-cloud/prowler/pull/10086)
2728
- Registry scan mode for `image` provider: enumerate and scan all images from OCI standard, Docker Hub, and ECR [(#9985)](https://github.com/prowler-cloud/prowler/pull/9985)
2829
- Add file descriptor limits (`ulimits`) to Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107)

prowler/compliance/m365/iso27001_2022_m365.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
}
157157
],
158158
"Checks": [
159+
"defenderxdr_critical_asset_management_pending_approvals",
159160
"sharepoint_external_sharing_managed",
160161
"exchange_external_email_tagging_enabled"
161162
]
@@ -455,6 +456,7 @@
455456
"defender_antispam_outbound_policy_configured",
456457
"defender_antispam_outbound_policy_forwarding_disabled",
457458
"defender_antispam_policy_inbound_no_allowed_domains",
459+
"defenderxdr_critical_asset_management_pending_approvals",
458460
"defender_chat_report_policy_configured",
459461
"defender_malware_policy_common_attachments_filter_enabled",
460462
"defender_malware_policy_comprehensive_attachments_filter_applied",

prowler/providers/m365/services/defenderxdr/defenderxdr_critical_asset_management_pending_approvals/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"Provider": "m365",
3+
"CheckID": "defenderxdr_critical_asset_management_pending_approvals",
4+
"CheckTitle": "Ensure all Critical Asset Management classifications are reviewed and approved in Microsoft Defender XDR",
5+
"CheckType": [],
6+
"ServiceName": "defenderxdr",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "Defender XDR Critical Asset Management",
11+
"ResourceGroup": "security",
12+
"Description": "Assets with a lower classification confidence score in Microsoft Defender XDR must be approved by a security administrator.\n\nAsset classifications that have not yet been reviewed and approved may result in incomplete **critical asset** visibility.",
13+
"Risk": "Stale pending approvals lead to limited visibility in Microsoft Defender XDR. **Critical assets** that are not properly identified and classified may not receive appropriate security monitoring and protections, creating gaps in the organization's security posture.",
14+
"RelatedUrl": "",
15+
"AdditionalURLs": [
16+
"https://learn.microsoft.com/en-us/security-exposure-management/classify-critical-assets",
17+
"https://learn.microsoft.com/en-us/security-exposure-management/classify-critical-assets#review-critical-assets"
18+
],
19+
"Remediation": {
20+
"Code": {
21+
"CLI": "",
22+
"NativeIaC": "",
23+
"Other": "1. Navigate to **Microsoft Defender** at https://security.microsoft.com/\n2. Go to **Settings** > **Microsoft Defender XDR** > **Critical asset management**\n3. Review each pending approval listed in the check results\n4. Verify the correct classification for each asset\n5. Approve or reject the classification as appropriate",
24+
"Terraform": ""
25+
},
26+
"Recommendation": {
27+
"Text": "Regularly review and approve pending critical asset classifications to ensure accurate asset visibility in Microsoft Defender XDR. Stale approvals reduce the effectiveness of security monitoring and incident response for critical assets.",
28+
"Url": "https://hub.prowler.com/check/defenderxdr_critical_asset_management_pending_approvals"
29+
}
30+
},
31+
"Categories": [
32+
"e5"
33+
],
34+
"DependsOn": [],
35+
"RelatedTo": [],
36+
"Notes": "This check requires Microsoft Defender XDR with Security Exposure Management enabled. The ThreatHunting.Read.All permission is required to query the ExposureGraphNodes table via the Advanced Hunting API. Approved assets will be reflected in the classification table within 24 hours."
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Check for pending Critical Asset Management approvals in Defender XDR.
2+
3+
This check identifies asset classifications with low confidence scores
4+
that require security administrator review and approval.
5+
"""
6+
7+
from typing import List
8+
9+
from prowler.lib.check.models import Check, CheckReportM365
10+
from prowler.providers.m365.services.defenderxdr.defenderxdr_client import (
11+
defenderxdr_client,
12+
)
13+
14+
15+
class defenderxdr_critical_asset_management_pending_approvals(Check):
16+
"""Check for pending Critical Asset Management approvals in Microsoft Defender XDR.
17+
18+
This check queries Advanced Hunting to identify assets with low classification
19+
confidence scores that have not been reviewed by a security administrator.
20+
21+
Prerequisites:
22+
1. ThreatHunting.Read.All permission granted
23+
2. Microsoft Defender XDR with Security Exposure Management enabled
24+
25+
Results:
26+
- PASS: No pending approvals for Critical Asset Management are found.
27+
- FAIL: At least one asset classification has pending approvals.
28+
"""
29+
30+
def execute(self) -> List[CheckReportM365]:
31+
"""Execute the check for pending Critical Asset Management approvals.
32+
33+
Evaluates whether there are any pending Critical Asset Management
34+
approvals that require administrator review.
35+
36+
Returns:
37+
A list of reports containing the result of the check.
38+
"""
39+
findings = []
40+
pending_approvals = defenderxdr_client.pending_cam_approvals
41+
42+
# API call failed - likely missing ThreatHunting.Read.All permission
43+
if pending_approvals is None:
44+
report = CheckReportM365(
45+
metadata=self.metadata(),
46+
resource={},
47+
resource_name="Critical Asset Management",
48+
resource_id="criticalAssetManagement",
49+
)
50+
report.status = "FAIL"
51+
report.status_extended = (
52+
"Unable to query Critical Asset Management status. "
53+
"Verify that ThreatHunting.Read.All permission is granted."
54+
)
55+
findings.append(report)
56+
return findings
57+
58+
if not pending_approvals:
59+
report = CheckReportM365(
60+
metadata=self.metadata(),
61+
resource={},
62+
resource_name="Critical Asset Management",
63+
resource_id="criticalAssetManagement",
64+
)
65+
report.status = "PASS"
66+
report.status_extended = "No pending approvals for Critical Asset Management classifications are found."
67+
findings.append(report)
68+
else:
69+
for approval in pending_approvals:
70+
report = CheckReportM365(
71+
metadata=self.metadata(),
72+
resource=approval,
73+
resource_name=f"CAM Classification: {approval.classification}",
74+
resource_id=f"cam/{approval.classification}",
75+
)
76+
report.status = "FAIL"
77+
assets_summary = ", ".join(approval.assets[:5])
78+
if len(approval.assets) > 5:
79+
assets_summary += f" and {len(approval.assets) - 5} more"
80+
report.status_extended = (
81+
f"Critical Asset Management classification '{approval.classification}' "
82+
f"has {approval.pending_count} asset(s) pending approval: {assets_summary}."
83+
)
84+
findings.append(report)
85+
86+
return findings

prowler/providers/m365/services/defenderxdr/defenderxdr_service.py

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ class DefenderXDR(M365Service):
2828
- Device security posture
2929
- Exposed credentials detection
3030
- Vulnerability assessments
31+
- Critical Asset Management approvals
3132
3233
Attributes:
3334
mde_status: Status of MDE deployment
3435
(None, "not_enabled", "no_devices", "active")
3536
exposed_credentials_privileged_users: List of privileged users
3637
with exposed credentials
38+
pending_cam_approvals: List of pending Critical Asset Management
39+
approvals (None if API error)
3740
"""
3841

3942
def __init__(self, provider: M365Provider):
@@ -52,15 +55,19 @@ def __init__(self, provider: M365Provider):
5255
self.exposed_credentials_privileged_users: Optional[
5356
List[ExposedCredentialPrivilegedUser]
5457
] = []
58+
self.pending_cam_approvals: Optional[List[PendingCAMApproval]] = []
5559

5660
loop = self._get_event_loop()
5761
try:
58-
self.mde_status, self.exposed_credentials_privileged_users = (
59-
loop.run_until_complete(
60-
asyncio.gather(
61-
self._check_mde_status(),
62-
self._get_exposed_credentials_privileged_users(),
63-
)
62+
(
63+
self.mde_status,
64+
self.exposed_credentials_privileged_users,
65+
self.pending_cam_approvals,
66+
) = loop.run_until_complete(
67+
asyncio.gather(
68+
self._check_mde_status(),
69+
self._get_exposed_credentials_privileged_users(),
70+
self._get_pending_cam_approvals(),
6471
)
6572
)
6673
finally:
@@ -222,6 +229,63 @@ def _parse_exposed_credential(self, row: Dict) -> "ExposedCredentialPrivilegedUs
222229
target_categories=target_categories,
223230
)
224231

232+
async def _get_pending_cam_approvals(
233+
self,
234+
) -> Optional[List["PendingCAMApproval"]]:
235+
"""Query for pending Critical Asset Management approvals.
236+
237+
Queries the ExposureGraphNodes table to find assets with low criticality
238+
confidence scores that require administrator approval.
239+
240+
Returns:
241+
List of PendingCAMApproval objects, or None if API call failed.
242+
"""
243+
logger.info(
244+
"DefenderXDR - Querying for pending Critical Asset Management approvals..."
245+
)
246+
247+
query = """
248+
ExposureGraphNodes
249+
| where isnotempty(parse_json(NodeProperties)['rawData']['criticalityConfidenceLow'])
250+
| mv-expand parse_json(NodeProperties)['rawData']['criticalityConfidenceLow']
251+
| extend Classification = tostring(NodeProperties_rawData_criticalityConfidenceLow)
252+
| summarize PendingApproval = count(), Assets = array_sort_asc(make_set(NodeName)) by Classification
253+
| sort by Classification asc
254+
"""
255+
256+
results, _ = await self._run_hunting_query(query)
257+
258+
if results is None:
259+
return None
260+
261+
pending_approvals = []
262+
for row in results:
263+
if not row:
264+
continue
265+
classification = row.get("Classification", "")
266+
pending_count = int(row.get("PendingApproval", 0))
267+
assets_raw = row.get("Assets", "[]")
268+
269+
if isinstance(assets_raw, str):
270+
try:
271+
assets = json.loads(assets_raw)
272+
except (json.JSONDecodeError, ValueError):
273+
assets = []
274+
elif isinstance(assets_raw, list):
275+
assets = assets_raw
276+
else:
277+
assets = []
278+
279+
pending_approvals.append(
280+
PendingCAMApproval(
281+
classification=classification,
282+
pending_count=pending_count,
283+
assets=assets,
284+
)
285+
)
286+
287+
return pending_approvals
288+
225289

226290
class ExposedCredentialPrivilegedUser(BaseModel):
227291
"""Model for exposed credential data of a privileged user.
@@ -239,3 +303,20 @@ class ExposedCredentialPrivilegedUser(BaseModel):
239303
target_node_label: str
240304
credential_type: Optional[str] = None
241305
target_categories: list = []
306+
307+
308+
class PendingCAMApproval(BaseModel):
309+
"""Model for a pending Critical Asset Management approval classification.
310+
311+
Represents assets with low criticality confidence scores that require
312+
security administrator review and approval.
313+
314+
Attributes:
315+
classification: The asset classification name pending approval.
316+
pending_count: The number of assets pending approval for this classification.
317+
assets: List of asset names pending approval.
318+
"""
319+
320+
classification: str
321+
pending_count: int
322+
assets: List[str]

0 commit comments

Comments
 (0)