Skip to main content

Microsoft Entra

Overview

This integration uses both a bash script and PowerShell script to execute. The PowerShell script manages conditional access policies and named locations in Microsoft Azure using the Microsoft Graph API. It supports adding, removing, and flushing IP-based named locations within conditional access policies. The script is designed to optimize API calls by caching policy and location data.

Prerequisites

• Azure PowerShell Module
• Microsoft Graph PowerShell Module
• Azure AD app registration with appropriate permissions to manage conditional access policies

Parameters

• **`$action`**: Specifies the action to perform. Acceptable values are `add`, `remove`, and `flush`.

• **`$acl`**: Name of the access control list (ACL) to modify.

• **`$ip`**: IP address to add or remove from the named location.

Functions

Get-ClientSecret

powershell
function Get-ClientSecret {
    param (
        [string]$secretName
    )
    return "your-secure-secret"  # Placeholder for testing
}

Retrieves a client secret securely from a storage system like Azure Key Vault.

Get-NamedLocationId

```powershell
function Get-NamedLocationId {
    param ([string]$displayName)
    # Implementation...
}
```

Retrieves the ID of a named location by its display name, caching the result to minimize repeated API calls.

Update-NamedLocationTrustStatus

```powershell
function Update-NamedLocationTrustStatus {
    param (
        [string]$NamedLocationId,
        [bool]$IsTrusted,
        [string]$LocationType = "#microsoft.graph.ipNamedLocation"
    )
    # Implementation...
}
```

Updates the trust status of a named location.

Get-ConditionalAccessPolicy

```powershell
function Get-ConditionalAccessPolicy {
    param ([string]$displayName)
    # Implementation...
}
```

Retrieves a conditional access policy by its display name, caching the result to minimize repeated API calls.

Update-ConditionalAccessPolicy

```powershell
function Update-ConditionalAccessPolicy {
    param (
        [string]$PolicyId,
        [array]$LocationIds
    )
    # Implementation...
}
```

Updates a conditional access policy with a list of named location IDs.

Add-NamedLocationAndModifyPolicy

```powershell
function Add-NamedLocationAndModifyPolicy {
    # Implementation...
}
```

Adds a named location if it does not exist and updates the specified conditional access policy to include the new named location.

Remove-LocationFromPolicies

```powershell
function Remove-LocationFromPolicies {
    param ([string]$NamedLocationId)
    # Implementation...
}
```

Removes a named location from all conditional access policies and then deletes the named location.

Flush-NamedLocationsFromPolicy

```powershell
function Flush-NamedLocationsFromPolicy {
    # Implementation...
}
```

Removes all named locations matching the pattern `knocknoc_*` from all conditional access policies and then deletes these named locations.

Usage

Add a Named Location and Modify Policy

```powershell
.\Script.ps1 -action add -acl "aclName" -ip "192.168.1.1"
```

Adds the specified IP address as a named location and modifies the specified access control list to include this named location.

Remove a Named Location from Policies

```powershell
.\Script.ps1 -action remove -acl "aclName" -ip "192.168.1.1"
```

Removes the specified IP address from the named locations in all policies.

Flush Named Locations from Policies

```powershell
.\Script.ps1 -action flush -acl "aclName"
```

Removes all named locations that match the pattern `knocknoc_*` from all policies.

Authentication

The script requires authentication with Microsoft Graph API. This is achieved by creating a PSCredential object using the client ID, tenant ID, and client secret.

Example

```powershell
param (
    [ValidateSet('add', 'remove', 'flush')][string]$action,
    [string]$acl,
    [ValidatePattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')][string]$ip
)

# Authentication
$ClientId = "client"
$TenantId = "tenant"
$ClientSecret = "secret"

try {
    $ClientSecretPass = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
    $ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $ClientSecretPass

    Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential -NoWelcome
} catch {
    Write-Host "Failed to authenticate with Microsoft Graph. Error: $_"
    exit 1
}

# Action map
$actionMap = @{
    'add'    = { Add-NamedLocationAndModifyPolicy }
    'remove' = {
        $NamedLocationId = Get-NamedLocationId "knocknoc_$ip"
        if ($null -eq $NamedLocationId) {
            Write-Host "Named location 'knocknoc_$ip' not found."
        } else {
            Remove-LocationFromPolicies -NamedLocationId $NamedLocationId
        }
    }
    'flush'  = { Flush-NamedLocationsFromPolicy }
}

# Execute action
if ($actionMap.ContainsKey($action)) {
    & $actionMap[$action]
} else {
    Write-Host "Invalid action specified"
}
```

Script

param (
[ValidateSet('add', 'remove', 'flush')][string]$action,
[string]$acl,
[ValidatePattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')][string]$ip
)


# Caching policy and named location data to reduce repeated API calls
$policyCache = @{}
$locationCache = @{}

# Securely retrieve client secret
function Get-ClientSecret {
param (
[string]$secretName
)
# Implementation for retrieving secret from a secure storage like Azure Key Vault
return "your-secure-secret" # Placeholder for testing
}

# Helper functions
function Get-NamedLocationId {
param ([string]$displayName)
if ($locationCache.ContainsKey($displayName)) {
return $locationCache[$displayName]
}

try {
$namedLocations = Get-MgIdentityConditionalAccessNamedLocation -Filter "DisplayName eq '$displayName'"
} catch {
Write-Host "Error occurred while querying named locations: $_"
return $null
}

if ($null -eq $namedLocations -or $namedLocations.Count -eq 0) {
Write-Host "Named location '$displayName' not found."
return $null
}

$namedLocation = $namedLocations | Select-Object -First 1
$namedLocationId = $namedLocation.Id

if ([string]::IsNullOrEmpty($namedLocationId)) {
Write-Host "Named location ID is null or empty."
return $null
}

$locationCache[$displayName] = $namedLocationId
return $namedLocationId
}

function Update-NamedLocationTrustStatus {
param (
[string]$NamedLocationId,
[bool]$IsTrusted,
[string]$LocationType = "#microsoft.graph.ipNamedLocation"
)

$body = @{
"@odata.type" = $LocationType
IsTrusted = $IsTrusted
}

try {
Update-MgIdentityConditionalAccessNamedLocation -NamedLocationId $NamedLocationId -BodyParameter $body
return $true
} catch {
Write-Host "Failed to update trust status for $NamedLocationId. Error: $_"
return $false
}
}

function Get-ConditionalAccessPolicy {
param ([string]$displayName)
if ($policyCache.ContainsKey($displayName)) {
return $policyCache[$displayName]
}

$policy = Get-MgIdentityConditionalAccessPolicy -Filter "DisplayName eq '$displayName'"
if ($null -eq $policy) {
Write-Host "Conditional Access Policy '$displayName' not found."
return $null
}
$policyCache[$displayName] = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policy.Id
return $policyCache[$displayName]
}

function Update-ConditionalAccessPolicy {
param (
[string]$PolicyId,
[array]$LocationIds
)

$validIds = $LocationIds | Where-Object { $_ -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' }
if ($validIds.Count -eq 0) {
Write-Host "No valid location IDs to update for policy $PolicyId."
return
}

try {
$params = @{
Conditions = @{
Locations = @{
IncludeLocations = $validIds
}
}
}
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $PolicyId -BodyParameter $params
Write-Host "Policy $PolicyId updated successfully."
} catch {
Write-Host "Failed to update policy $PolicyId. Error: $_"
}
}

function Add-NamedLocationAndModifyPolicy {
$namedLocationId = Get-NamedLocationId "knocknoc_$ip"
$policy = Get-ConditionalAccessPolicy "knocknoc_$acl"

if ($null -eq $namedLocationId) {
$params = @{
"@odata.type" = "#microsoft.graph.ipNamedLocation"
DisplayName = "knocknoc_$ip"
IsTrusted = $true
ipRanges = @(
@{
"@odata.type" = "#microsoft.graph.iPv4CidrRange"
cidrAddress = "$ip/32"
}
)
}
$newLocation = New-MgIdentityConditionalAccessNamedLocation -BodyParameter $params
$namedLocationId = $newLocation.Id
$locationCache["knocknoc_$ip"] = $namedLocationId
Write-Host "Created new named location 'knocknoc_$ip' with ID $namedLocationId."
} else {
Write-Host "Named location 'knocknoc_$ip' already exists. No action required."
return
}

if ($null -eq $policy) {
Write-Host "Policy not found"
return
}

$currentLocations = @($policy.Conditions.Locations.IncludeLocations)
if ($namedLocationId -notin $currentLocations) {
$updatedLocations = $currentLocations + $namedLocationId
Update-ConditionalAccessPolicy -PolicyId $policy.Id -LocationIds $updatedLocations
}
}

function Remove-LocationFromPolicies {
param ([string]$NamedLocationId)

if ([string]::IsNullOrEmpty($NamedLocationId)) {
Write-Host "Invalid named location ID."
return
}

$allPolicies = Get-MgIdentityConditionalAccessPolicy
$policiesToUpdate = @()

foreach ($policy in $allPolicies) {
if ($NamedLocationId -in $policy.Conditions.Locations.IncludeLocations) {
$updatedLocations = $policy.Conditions.Locations.IncludeLocations | Where-Object { $_ -ne $NamedLocationId }
$policiesToUpdate += [PSCustomObject]@{ PolicyId = $policy.Id; LocationIds = $updatedLocations }
}
}

foreach ($policyUpdate in $policiesToUpdate) {
Update-ConditionalAccessPolicy -PolicyId $policyUpdate.PolicyId -LocationIds $policyUpdate.LocationIds
}

# Validate updates
$allPolicies = Get-MgIdentityConditionalAccessPolicy
foreach ($policy in $allPolicies) {
if ($NamedLocationId -in $policy.Conditions.Locations.IncludeLocations) {
Write-Host "Location $NamedLocationId still exists in policy $($policy.Id) after update attempt."
return
}
}

# Update trust status before deletion
if (-not (Update-NamedLocationTrustStatus -NamedLocationId $NamedLocationId -IsTrusted $false)) {
Write-Host "Failed to update trust status for $NamedLocationId. Cannot proceed with deletion."
return
}

try {
Remove-MgIdentityConditionalAccessNamedLocation -NamedLocationId $NamedLocationId
Write-Host "Named location $NamedLocationId removed successfully."
} catch {
Write-Host "Failed to remove named location $NamedLocationId. Error: $_"
}
}

function Flush-NamedLocationsFromPolicy {
$namedLocations = Get-MgIdentityConditionalAccessNamedLocation | Where-Object { $_.DisplayName -like "*knocknoc_*" }

$jobs = @()
$throttleLimit = 5 # Adjust the throttle limit as needed

foreach ($location in $namedLocations) {
$jobs += Start-Job -ScriptBlock {
param (
$location,
$ClientId,
$TenantId,
$ClientSecret
)

function Update-NamedLocationTrustStatus {
param (
[string]$NamedLocationId,
[bool]$IsTrusted,
[string]$LocationType = "#microsoft.graph.ipNamedLocation"
)

$body = @{
"@odata.type" = $LocationType
IsTrusted = $IsTrusted
}

try {
Update-MgIdentityConditionalAccessNamedLocation -NamedLocationId $NamedLocationId -BodyParameter $body
return $true
} catch {
Write-Host "Failed to update trust status for $NamedLocationId. Error: $_"
return $false
}
}

function Remove-LocationFromPolicies {
param ([string]$NamedLocationId)

$allPolicies = Get-MgIdentityConditionalAccessPolicy
$policiesToUpdate = @()

foreach ($policy in $allPolicies) {
if ($NamedLocationId -in $policy.Conditions.Locations.IncludeLocations) {
$updatedLocations = $policy.Conditions.Locations.IncludeLocations | Where-Object { $_ -ne $NamedLocationId }
$policiesToUpdate += [PSCustomObject]@{ PolicyId = $policy.Id; LocationIds = $updatedLocations }
}
}

foreach ($policyUpdate in $policiesToUpdate) {
Update-ConditionalAccessPolicy -PolicyId $policyUpdate.PolicyId -LocationIds $policyUpdate.LocationIds
}

# Validate updates
$allPolicies = Get-MgIdentityConditionalAccessPolicy
foreach ($policy in $allPolicies) {
if ($NamedLocationId -in $policy.Conditions.Locations.IncludeLocations) {
Write-Host "Location $NamedLocationId still exists in policy $($policy.Id) after update attempt."
return $false
}
}
return $true
}

function Update-ConditionalAccessPolicy {
param (
[string]$PolicyId,
[array]$LocationIds
)

$validIds = $LocationIds | Where-Object { $_ -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' }
if ($validIds.Count -eq 0) {
Write-Host "No valid location IDs to update for policy $PolicyId."
return
}

try {
$params = @{
Conditions = @{
Locations = @{
IncludeLocations = $validIds
}
}
}
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $PolicyId -BodyParameter $params
} catch {
Write-Host "Failed to update policy $PolicyId. Error: $_"
}
}

# Re-establish connection inside job
$ClientSecretPass = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
$ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $ClientSecretPass

Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential -NoWelcome

$trustUpdateSuccess = Update-NamedLocationTrustStatus -NamedLocationId $location.Id -IsTrusted $false

if ($trustUpdateSuccess) {
$removeSuccess = $false
$retryCount = 0
$maxRetries = 5

while (-not $removeSuccess -and $retryCount -lt $maxRetries) {
$removeSuccess = Remove-LocationFromPolicies -NamedLocationId $location.Id
if (-not $removeSuccess) {
Start-Sleep -Seconds 5 # Adjust sleep time as needed
$retryCount++
}
}

if ($removeSuccess) {
try {
Remove-MgIdentityConditionalAccessNamedLocation -NamedLocationId $location.Id
} catch {
Write-Host "Failed to remove named location $location.Id after updates. Error: $_"
}
} else {
Write-Host "Could not confirm removal from policies for location $location.Id."
}
} else {
Write-Host "Skipping deletion due to failed trust status update for location $location.Id."
}
} -ArgumentList $location, $ClientId, $TenantId, $ClientSecret

while ($jobs.Count -ge $throttleLimit) {
$jobs | ForEach-Object { Wait-Job -Job $_ }
$jobs | ForEach-Object { Receive-Job -Job $_ }
$jobs | ForEach-Object { Remove-Job -Job $_ }
$jobs = $jobs | Where-Object { $_.State -ne 'Completed' }
Start-Sleep -Seconds 1
}
}

# Wait for all remaining jobs to complete
$jobs | ForEach-Object { Wait-Job -Job $_ }
$jobs | ForEach-Object { Receive-Job -Job $_ }
$jobs | ForEach-Object { Remove-Job -Job $_ }
}

# Authentication and connection setup
$ClientId = "client"
$TenantId = "tenant"
$ClientSecret = "secret"

try {
$ClientSecretPass = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
$ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $ClientSecretPass

Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential -NoWelcome
} catch {
Write-Host "Failed to authenticate with Microsoft Graph. Error: $_"
exit 1
}

# Define a hashtable with actions as keys and their corresponding functions as values
$actionMap = @{
'add' = { Add-NamedLocationAndModifyPolicy }
'remove' = {
$NamedLocationId = Get-NamedLocationId "knocknoc_$ip"
if ($null -eq $NamedLocationId) {
Write-Host "Named location 'knocknoc_$ip' not found."
} else {
Remove-LocationFromPolicies -NamedLocationId $NamedLocationId
}
}
'flush' = { Flush-NamedLocationsFromPolicy }
}

# Check if the action exists in the hashtable and invoke the corresponding function
if ($actionMap.ContainsKey($action)) {
& $actionMap[$action]
} else {
Write-Host "Invalid action specified"
}

Notes

• Ensure that the Azure and Microsoft Graph PowerShell modules are installed and imported in your environment.
• Adjust the throttle limit for concurrency as needed.
• Securely store and retrieve secrets using Azure Key Vault or another secure method.