Skip to content

Commit 707a647

Browse files
authored
Network - 25412 - Internet traffic is protected against threats using Global Secure Access threat intelligence filtering (#1510)
2 parents 2e52cf9 + 43d91d0 commit 707a647

2 files changed

Lines changed: 389 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Without threat intelligence filtering enabled in Global Secure Access, internet traffic from users and devices remains unprotected against connections to malicious destinations. Threat actors can leverage this gap to establish initial access through phishing sites that harvest credentials or deliver malware payloads. Once malware executes on a compromised endpoint, it can communicate with command-and-control (C2) infrastructure to receive instructions, download additional tools, or exfiltrate sensitive data. Microsoft defines high severity threats as domains or URLs associated with active malware distribution, phishing campaigns, C2 infrastructure, and other indicators identified by Microsoft and third-party threat intelligence feeds with high confidence. The threat intelligence policy in Global Secure Access blocks traffic based on domain and URL indicators from these feeds in real-time. When this protection is not enabled, threat actors maintain unrestricted network communication paths, enabling persistence mechanisms to survive detection and lateral movement to additional systems. The baseline security profile provides tenant-wide protection without requiring Conditional Access policy configuration, while custom security profiles enable context-aware enforcement through Conditional Access integration.
2+
3+
**Remediation action**
4+
5+
- [Configure threat intelligence filtering in Global Secure Access to block connections to destinations on the threat intelligence feed](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-configure-threat-intelligence)
6+
- [Understand security profiles, how filtering policies are grouped into them, and how policy priority is processed](https://learn.microsoft.com/en-us/entra/global-secure-access/concept-internet-access#security-profiles)
7+
- [Link a security profile to a Conditional Access policy using the Global Secure Access session control](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-session#use-global-secure-access-security-profile)
8+
- [Link a security policy to the baseline profile for tenant-wide protection without Conditional Access](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-apply-security-policies-remote-network)
9+
10+
<!--- Results --->
11+
%TestResult%
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
<#
2+
.SYNOPSIS
3+
Checks if internet traffic is protected against threats using Global Secure Access threat intelligence filtering.
4+
5+
.DESCRIPTION
6+
This test validates that threat intelligence filtering is enabled and associated with either the baseline profile
7+
or a security profile linked to a Conditional Access policy. Threat intelligence policies block traffic to malicious
8+
destinations identified by Microsoft and third-party threat intelligence feeds.
9+
10+
.NOTES
11+
Test ID: 25412
12+
Category: Global Secure Access
13+
Pillar: Networking
14+
Required API: networkAccess/threatIntelligencePolicies, networkAccess/filteringProfiles, identity/conditionalAccess/policies
15+
#>
16+
17+
function Test-Assessment-25412 {
18+
[ZtTest(
19+
Category = 'Global Secure Access',
20+
ImplementationCost = 'Medium',
21+
Service = ('Graph'),
22+
CompatibleLicense = ('Entra_Premium_Internet_Access'),
23+
Pillar = 'Network',
24+
RiskLevel = 'High',
25+
SfiPillar = 'Protect networks',
26+
TenantType = ('Workforce'),
27+
TestId = 25412,
28+
Title = 'Internet traffic is protected against threats using Global Secure Access threat intelligence filtering',
29+
UserImpact = 'Medium'
30+
)]
31+
[CmdletBinding()]
32+
param()
33+
34+
# Define constants
35+
[int]$BASELINE_PROFILE_PRIORITY = 65000
36+
[string]$THREAT_INTELLIGENCE_POLICY_LINK_TYPE = '#microsoft.graph.networkaccess.threatIntelligencePolicyLink'
37+
38+
#region Data Collection
39+
Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
40+
41+
$activity = 'Checking threat intelligence filtering configuration'
42+
43+
$threatIntelPolicies = $null
44+
$filteringProfiles = $null
45+
$caPolicies = $null
46+
$q3Evaluated = $false
47+
$q1Error = $null
48+
$q2Error = $null
49+
$q3Error = $null
50+
51+
# Q1: Get threat intelligence policies
52+
Write-ZtProgress -Activity $activity -Status 'Getting threat intelligence policies'
53+
try {
54+
$threatIntelPolicies = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/threatIntelligencePolicies' -ApiVersion beta -ErrorAction Stop
55+
Write-PSFMessage "Found $($threatIntelPolicies.Count) threat intelligence policies" -Level Verbose
56+
}
57+
catch {
58+
$q1Error = $_
59+
Write-PSFMessage "Failed to get threat intelligence policies: $_" -Tag Test -Level Warning
60+
}
61+
62+
# Q2 depends on Q1: only query filtering profiles when TI policies exist.
63+
if (-not $q1Error -and $threatIntelPolicies -and $threatIntelPolicies.Count -gt 0) {
64+
Write-ZtProgress -Activity $activity -Status 'Getting filtering profiles'
65+
try {
66+
$filteringProfiles = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/filteringProfiles' -QueryParameters @{ '$select' = 'id,name,description,state,version,priority'; '$expand' = 'policies($expand=policy)' } -ApiVersion beta -ErrorAction Stop
67+
Write-PSFMessage "Found $($filteringProfiles.Count) filtering profiles" -Level Verbose
68+
}
69+
catch {
70+
$q2Error = $_
71+
Write-PSFMessage "Failed to get filtering profiles: $_" -Tag Test -Level Warning
72+
}
73+
}
74+
#endregion Data Collection
75+
76+
#region Assessment Logic
77+
$passed = $false
78+
$customStatus = $null
79+
$testResultMarkdown = ''
80+
$investigateMessage = "⚠️ Unable to determine threat intelligence filtering status due to an API or access error. Re-run the assessment after verifying Microsoft Graph access and retrying the check.`n`n%TestResult%"
81+
$failMessage = "❌ No enabled threat intelligence policy link is enforced through the baseline profile or a security profile assigned by a Conditional Access policy.`n`n%TestResult%"
82+
$baselineProfileEvidence = $null
83+
$caPolicyEvidence = @()
84+
85+
# Step 1: Verify at least one threat intelligence policy exists.
86+
$hasRequiredQueryError = ($null -ne $q1Error) -or ($null -ne $q2Error)
87+
$threatIntelPolicyIds = [System.Collections.Generic.HashSet[string]]::new()
88+
if ($threatIntelPolicies) {
89+
foreach ($threatIntelPolicy in $threatIntelPolicies) {
90+
if ($threatIntelPolicy.id) {
91+
[void]$threatIntelPolicyIds.Add($threatIntelPolicy.id)
92+
}
93+
}
94+
}
95+
$hasThreatIntelPolicies = $threatIntelPolicyIds.Count -gt 0
96+
97+
# Q1/Q2 failures are always investigate because they block prerequisite evaluation.
98+
if ($hasRequiredQueryError) {
99+
$passed = $false
100+
$customStatus = 'Investigate'
101+
$testResultMarkdown = $investigateMessage
102+
}
103+
# If no threat intelligence policies exist, test fails regardless of profile linkage.
104+
elseif (-not $hasThreatIntelPolicies) {
105+
$passed = $false
106+
$testResultMarkdown = $failMessage
107+
}
108+
else {
109+
# Step 2: Check baseline profile for threat intelligence policy link
110+
$baselineProfile = $filteringProfiles | Where-Object { $_.priority -eq $BASELINE_PROFILE_PRIORITY }
111+
$baselineHasTI = $false
112+
113+
if ($baselineProfile) {
114+
$baselineTiPolicyLinks = @($baselineProfile.policies | Where-Object {
115+
$_.'@odata.type' -eq $THREAT_INTELLIGENCE_POLICY_LINK_TYPE -and
116+
$_.policy.id -and $threatIntelPolicyIds.Contains($_.policy.id)
117+
})
118+
$baselineEnabledTiLinks = @($baselineTiPolicyLinks | Where-Object { $_.state -eq 'enabled' })
119+
$baselinePolicyLinkState = if ($baselineEnabledTiLinks.Count -gt 0) {
120+
'enabled'
121+
}
122+
elseif ($baselineTiPolicyLinks.Count -gt 0) {
123+
if ($baselineTiPolicyLinks[0].state) { $baselineTiPolicyLinks[0].state } else { 'N/A' }
124+
}
125+
else {
126+
'N/A'
127+
}
128+
129+
$baselineProfileEvidence = [PSCustomObject]@{
130+
Profile = $baselineProfile
131+
TiPolicyLinks = $baselineTiPolicyLinks
132+
EnabledTiLinks = $baselineEnabledTiLinks
133+
HasTI = $baselineTiPolicyLinks.Count -gt 0
134+
PolicyLinkState= $baselinePolicyLinkState
135+
}
136+
$baselineHasTI = $baselineProfileEvidence.EnabledTiLinks.Count -gt 0 -and $baselineProfile.state -eq 'enabled'
137+
}
138+
139+
# Step 3: Decide whether Conditional Access fallback evaluation is required.
140+
$requiresConditionalAccessEvaluation = -not $baselineHasTI
141+
142+
if (-not $requiresConditionalAccessEvaluation) {
143+
# Baseline has enabled TI policy link - Pass
144+
$passed = $true
145+
}
146+
else {
147+
# Step 4: Baseline is not effective, so evaluate Conditional Access fallback.
148+
$q3Evaluated = $true
149+
Write-ZtProgress -Activity $activity -Status 'Getting Conditional Access policies'
150+
try {
151+
$caPolicies = Get-ZtConditionalAccessPolicy -ErrorAction Stop
152+
Write-PSFMessage "Found $($caPolicies.Count) Conditional Access policies" -Level Verbose
153+
}
154+
catch {
155+
$q3Error = $_
156+
Write-PSFMessage "Failed to get Conditional Access policies: $_" -Tag Test -Level Warning
157+
}
158+
159+
if ($q3Error) {
160+
$passed = $false
161+
$customStatus = 'Investigate'
162+
$testResultMarkdown = $investigateMessage
163+
}
164+
165+
$enabledCAPoliciesWithGSA = @()
166+
if (-not $q3Error -and $caPolicies) {
167+
$enabledCAPoliciesWithGSA = @($caPolicies | Where-Object {
168+
$_.state -eq 'enabled' -and
169+
$_.sessionControls.globalSecureAccessFilteringProfile.isEnabled -eq $true
170+
})
171+
}
172+
173+
# Step 5: Cross-reference profileIds with filtering profiles; collect full evidence for reporting.
174+
$foundValidProfile = $false
175+
if (-not $q3Error) {
176+
foreach ($caPolicy in $enabledCAPoliciesWithGSA) {
177+
$profileId = $caPolicy.sessionControls.globalSecureAccessFilteringProfile.profileId
178+
$linkedProfile = $filteringProfiles | Where-Object { $_.id -eq $profileId }
179+
$profileTiLinks = @()
180+
$enabledProfileTiLinks = @()
181+
$profilePolicyLinkState = 'N/A'
182+
$profileHasTI = $false
183+
$profileIsEffective = $false
184+
185+
if ($linkedProfile) {
186+
$profileTiLinks = @($linkedProfile.policies | Where-Object {
187+
$_.'@odata.type' -eq $THREAT_INTELLIGENCE_POLICY_LINK_TYPE -and
188+
$_.policy.id -and $threatIntelPolicyIds.Contains($_.policy.id)
189+
})
190+
$enabledProfileTiLinks = @($profileTiLinks | Where-Object { $_.state -eq 'enabled' })
191+
$profileHasTI = $profileTiLinks.Count -gt 0
192+
$profilePolicyLinkState = if ($enabledProfileTiLinks.Count -gt 0) {
193+
'enabled'
194+
}
195+
elseif ($profileTiLinks.Count -gt 0) {
196+
if ($profileTiLinks[0].state) { $profileTiLinks[0].state } else { 'N/A' }
197+
}
198+
else {
199+
'N/A'
200+
}
201+
$profileIsEffective = $linkedProfile.state -eq 'enabled' -and $enabledProfileTiLinks.Count -gt 0
202+
203+
if ($profileIsEffective -and -not $foundValidProfile) {
204+
$foundValidProfile = $true
205+
}
206+
}
207+
208+
$caPolicyEvidence += [PSCustomObject]@{
209+
CAPolicy = $caPolicy
210+
ProfileId = $profileId
211+
LinkedProfile = $linkedProfile
212+
ProfileTiLinks = $profileTiLinks
213+
EnabledProfileTiLinks= $enabledProfileTiLinks
214+
ProfileHasTI = $profileHasTI
215+
PolicyLinkState = $profilePolicyLinkState
216+
IsEffective = $profileIsEffective
217+
}
218+
}
219+
}
220+
221+
if (-not $q3Error) {
222+
$passed = $foundValidProfile
223+
}
224+
}
225+
226+
if ($passed) {
227+
$testResultMarkdown = "✅ Threat intelligence filtering is enabled and enforced through an enabled baseline profile or an enabled security profile assigned by a Conditional Access policy.`n`n%TestResult%"
228+
}
229+
elseif (-not $customStatus) {
230+
$testResultMarkdown = $failMessage
231+
}
232+
}
233+
#endregion Assessment Logic
234+
235+
#region Report Generation
236+
$mdInfo = ''
237+
238+
# Table 2: Baseline Profile Threat Intelligence Status
239+
$table2Title = 'Baseline Profile Threat Intelligence Status'
240+
$table2Link = 'https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/FilteringPolicyProfiles.ReactView'
241+
$threatPolicyLink = 'https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/ThreatIntelligencePolicy.ReactView'
242+
243+
if ($hasThreatIntelPolicies -and -not $q1Error -and -not $q2Error) {
244+
if ($baselineProfileEvidence) {
245+
$baselineProfile = $baselineProfileEvidence.Profile
246+
$baselineName = Get-SafeMarkdown $baselineProfile.name
247+
$baselineId = $baselineProfile.id
248+
$baselineStateDisplay = if ($baselineProfile.state -in @('enabled', 'disabled')) {
249+
'{0} {1}' -f (Get-ZtPassFail -Condition ($baselineProfile.state -eq 'enabled')), (Get-FormattedPolicyState -PolicyState $baselineProfile.state)
250+
}
251+
else {
252+
'N/A'
253+
}
254+
$baselineTitle = [System.Uri]::EscapeDataString("Edit $($baselineProfile.name)")
255+
$baselineProfileLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$baselineId/title/$baselineTitle/defaultMenuItemId/Basics"
256+
$baselineNameWithLink = "[$baselineName]($baselineProfileLink)"
257+
258+
$hasTI = $baselineProfileEvidence.HasTI
259+
$policyLinkState = $baselineProfileEvidence.PolicyLinkState
260+
$policyLinkStateDisplay = if ($policyLinkState -in @('enabled', 'disabled')) {
261+
'{0} {1}' -f (Get-ZtPassFail -Condition ($policyLinkState -eq 'enabled')), (Get-FormattedPolicyState -PolicyState $policyLinkState)
262+
}
263+
else {
264+
'N/A'
265+
}
266+
267+
$hasTIDisplay = if ($hasTI) { '✅ Yes' } else { '❌ No' }
268+
269+
$table2Template = @'
270+
271+
### [{0}]({1})
272+
273+
| Profile Name | [Has Threat Intelligence Policy]({6}) | Policy Link State | Profile State |
274+
| :----------- | :----------------------------- | :---------------- | :------------ |
275+
| {2} | {3} | {4} | {5} |
276+
'@
277+
278+
$table2 = $table2Template -f $table2Title, $table2Link, $baselineNameWithLink, $hasTIDisplay, $policyLinkStateDisplay, $baselineStateDisplay, $threatPolicyLink
279+
}
280+
else {
281+
$table2 = @"
282+
283+
### [$table2Title]($table2Link)
284+
285+
No baseline profile found.
286+
"@
287+
}
288+
289+
$mdInfo += $table2
290+
291+
# Table 3 is only shown when Q3 was needed and evaluated.
292+
if ($q3Evaluated) {
293+
$table3Title = 'Conditional Access Policies with Global Secure Access Session Control'
294+
$table3Link = 'https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies'
295+
296+
if ($q3Error) {
297+
$table3 = @"
298+
299+
### [$table3Title]($table3Link)
300+
301+
Unable to retrieve Conditional Access policies from Microsoft Graph, so Conditional Access enforcement could not be evaluated.
302+
"@
303+
}
304+
elseif ($caPolicyEvidence.Count -gt 0) {
305+
$table3Rows = foreach ($evidence in $caPolicyEvidence) {
306+
$caPolicy = $evidence.CAPolicy
307+
$caPolicyName = Get-SafeMarkdown $caPolicy.displayName
308+
$caPolicyId = $caPolicy.id
309+
$caPolicyStateDisplay = '✅ Enabled'
310+
$profileId = $evidence.ProfileId
311+
$linkedProfile = $evidence.LinkedProfile
312+
$profileName = if ($linkedProfile) { Get-SafeMarkdown $linkedProfile.name } else { 'N/A' }
313+
$profileStateDisplay = if ($linkedProfile -and $linkedProfile.state -in @('enabled', 'disabled')) {
314+
'{0} {1}' -f (Get-ZtPassFail -Condition ($linkedProfile.state -eq 'enabled')), (Get-FormattedPolicyState -PolicyState $linkedProfile.state)
315+
}
316+
else {
317+
'N/A'
318+
}
319+
$profileNameWithLink = if ($linkedProfile) {
320+
$profileTitle = [System.Uri]::EscapeDataString("Edit $($linkedProfile.name)")
321+
$profileLink = "https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/EditProfileMenuBlade.MenuView/~/basics/profileId/$profileId/title/$profileTitle/defaultMenuItemId/Basics"
322+
"[$profileName]($profileLink)"
323+
}
324+
else {
325+
'N/A'
326+
}
327+
328+
$policyLinkState = $evidence.PolicyLinkState
329+
$policyLinkStateDisplay = if ($policyLinkState -in @('enabled', 'disabled')) {
330+
'{0} {1}' -f (Get-ZtPassFail -Condition ($policyLinkState -eq 'enabled')), (Get-FormattedPolicyState -PolicyState $policyLinkState)
331+
}
332+
else {
333+
'N/A'
334+
}
335+
$profileHasTIDisplay = if ($evidence.ProfileHasTI) { '✅ Yes' } else { '❌ No' }
336+
337+
$caPolicyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$caPolicyId"
338+
339+
"| [$caPolicyName]($caPolicyLink) | $caPolicyStateDisplay | $profileNameWithLink | $profileStateDisplay | $profileHasTIDisplay | $policyLinkStateDisplay |"
340+
}
341+
342+
$table3Template = @'
343+
344+
### [{0}]({1})
345+
346+
| CA Policy Name | CA Policy State | Profile Name | Profile State | [Profile Has TI Policy]({3}) | Policy Link State |
347+
| :------------- | :-------------- | :----------- | :------------ | :-------------------- | :---------------- |
348+
{2}
349+
'@
350+
351+
$table3 = $table3Template -f $table3Title, $table3Link, ($table3Rows -join "`n"), $threatPolicyLink
352+
}
353+
else {
354+
$table3 = @"
355+
356+
### [$table3Title]($table3Link)
357+
358+
No Conditional Access policies with Global Secure Access session control found.
359+
"@
360+
}
361+
362+
$mdInfo += $table3
363+
}
364+
}
365+
366+
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
367+
#endregion Report Generation
368+
369+
$params = @{
370+
TestId = '25412'
371+
Status = $passed
372+
Result = $testResultMarkdown
373+
}
374+
if ($customStatus) {
375+
$params.CustomStatus = $customStatus
376+
}
377+
Add-ZtTestResultDetail @params
378+
}

0 commit comments

Comments
 (0)