Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Powershell and API : Searching Intune Policies by Settings Content

If you manage Microsoft Intune, you've likely faced this frustrating scenario: You need to find all policies that configure a specific setting - maybe "location services", "wallpaper", or "trusted certificates" - but Intune's portal only lets you search by policy name.

When you have dozens or hundreds of policies, manually clicking through each one to check if it contains a particular setting is a massive time sink. What if a security audit asks "Which policies configure location settings?" or you need to track down all policies that deploy certificates? You're stuck clicking through every single policy.

I needed a way to search inside the actual policy configurations, not just their names. The Intune portal doesn't offer this, so I built a PowerShell script that uses the Microsoft Graph API to solve this problem.

The Challenge: It's Not Just About Names

My first attempt seemed straightforward - query the Graph API for policies and search through them. But I quickly discovered that searching policy names wasn't enough. A policy named "Device-TrustedCertificate" doesn't tell you it might contain location settings. The actual settings and their values are buried deep in the policy object structure.

Here's what a typical Device Configuration Profile looks like when retrieved via Graph API:

{
  "displayName": "Device-TrustedCertificate",
  "@odata.type": "#microsoft.graph.windows81TrustedRootCertificate",
  "trustedRootCertificate": "MIIFkDCCA3igAwIBAgIQBZo...",
  "destinationStore": "computerCertStoreRoot",
  "certFileName": "bearissuersrv1.cer.cer"
}

The word "certificate" appears in the odata.type and properties, but not necessarily in the display name. My search needed to dig into the entire object structure.

The Solution: Deep Object Searching

The core of my solution converts each policy object to JSON and searches through the entire structure:

function Search-InPolicyContent {
    param(
        [object]$PolicyObject,
        [string]$SearchTerm,
        [string]$PolicyName = "Unknown"
    )
    
    $json = $PolicyObject | ConvertTo-Json -Depth 100
    
    # Case-insensitive search for whole words only
    # Using negative lookbehind/lookahead for better word boundary detection
    $pattern = "(?i)(?<![a-zA-Z0-9])" + [regex]::Escape($SearchTerm) + "(?![a-zA-Z0-9])"
    
    $matches = [regex]::Matches($json, $pattern)
    
    if ($matches.Count -gt 0) {
        Write-Host "    → Found '$SearchTerm' $($matches.Count) time(s)" -ForegroundColor DarkYellow
        
        # Show context for debugging
        $firstMatch = $matches[0]
        $startIndex = [Math]::Max(0, $firstMatch.Index - 50)
        $length = [Math]::Min(150, $json.Length - $startIndex)
        $context = $json.Substring($startIndex, $length) -replace '\s+', ' ' -replace '[\\"]', ''
        Write-Host "    → Context: ...$context..." -ForegroundColor DarkGray
        
        return $true
    }
    
    return $false
}

Script Decisions

  1. Whole Word Matching: When searching for "location", I don't want to match "dislocation" or "lockscreen". The regex pattern (?<![a-zA-Z0-9])keyword(?![a-zA-Z0-9]) ensures we only match complete words.
  2. Case Insensitive: Settings might be "Location", "location", or "LOCATION". The (?i) flag handles this.
  3. Deep JSON Conversion: Using -Depth 100 ensures even deeply nested settings are captured in the search.
  4. Context Display: Showing where the match was found helps verify the results are relevant.

Searching Different Policy Types

Intune has multiple policy types, each with different Graph API endpoints and structures:

Device Configuration Profiles

These are the traditional configuration profiles:

$deviceConfigUri = "https://graph.microsoft.com/v1.0/deviceManagement/deviceConfigurations"
$deviceConfigs = Invoke-GraphRequest -Uri $deviceConfigUri -AccessToken $accessToken

Configuration Policies (Settings Catalog)

The newer Settings Catalog policies require a different endpoint:

$configPolicyUri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies"
$configPolicies = Invoke-GraphRequest -Uri $configPolicyUri -AccessToken $accessToken

# For each policy, get its detailed settings
foreach ($policy in $configPolicies.value) {
    $settingsUri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($policy.id)')/settings"
    $settings = Invoke-GraphRequest -Uri $settingsUri -AccessToken $accessToken
    # ... search through settings
}

Compliance Policies and Administrative Templates

Each policy type has its own endpoint and structure, but the search approach remains consistent - retrieve the full object and search through its JSON representation.

Getting Assignment Information

Once I find matching policies, I need to know which groups they're assigned to. This requires additional API calls:

function Get-PolicyAssignments {
    param(
        [string]$PolicyId,
        [string]$PolicyType,
        [string]$AccessToken
    )
    
    $assignmentUri = switch ($PolicyType) {
        "deviceConfigurations" { 
            "https://graph.microsoft.com/v1.0/deviceManagement/deviceConfigurations('$PolicyId')/assignments" 
        }
        "configurationPolicies" { 
            "https://graph.microsoft.com/v1.0/deviceManagement/configurationPolicies('$PolicyId')/assignments" 
        }
        # ... other policy types
    }
    
    $assignments = Invoke-GraphRequest -Uri $assignmentUri -AccessToken $accessToken
    
    # Resolve group names from IDs
    foreach ($assignment in $assignments.value) {
        if ($assignment.target.'@odata.type' -eq "#microsoft.graph.groupAssignmentTarget") {
            $groupUri = "https://graph.microsoft.com/v1.0/groups/$($assignment.target.groupId)"
            $group = Invoke-GraphRequest -Uri $groupUri -AccessToken $accessToken
            # Return group name and assignment type
        }
    }
}

API Permissions Required

The script requires an Azure AD App Registration with the following Microsoft Graph API permissions:

Permission Type Purpose
DeviceManagementConfiguration.Read.All Application Read Intune device configuration policies
Group.Read.All Application Resolve group names from group IDs

Setting Up the App Registration:

  1. Go to Azure Portal → Azure Active Directory → App Registrations
  2. Create a new registration
  3. Under API Permissions, add the above Microsoft Graph permissions
  4. Grant admin consent for your tenant
  5. Create a client secret under Certificates & Secrets
  6. Note your Tenant ID, Application (Client) ID, and Client Secret

Using the Script

Configuration

Update these variables at the top of the script with your values:

$TenantId = "YOUR-TENANT-ID-HERE"        
$ClientId = "YOUR-CLIENT-ID-HERE"        
$ClientSecret = "YOUR-CLIENT-SECRET-HERE"

Running the Script

# Search for policies containing "location"
.\Search-IntunePolicy.ps1 -Keyword "location"

# Search for policies containing "certificate"
.\Search-IntunePolicy.ps1 -Keyword "certificate"

# Search for policies containing "wallpaper"
.\Search-IntunePolicy.ps1 -Keyword "wallpaper"

Script : Search-IntunePolicy.ps1

# Intune Policy Search Script
# Searches through Intune policies for keywords in their configurations

# ============================================
# PARAMETERS - Pass keyword when running script
# ============================================
param(
    [Parameter(Mandatory=$true)]
    [string]$Keyword
)

# ============================================
# CONFIGURATION - Update these values
# ============================================
$TenantId = "YOUR-TENANT-ID-HERE"        # Example: "12345678-1234-1234-1234-123456789012"
$ClientId = "YOUR-CLIENT-ID-HERE"        # Example: "87654321-4321-4321-4321-210987654321"
$ClientSecret = "YOUR-CLIENT-SECRET-HERE" # Example: "your-secret-value-here"

# ============================================
# SCRIPT BEGINS - No need to modify below
# ============================================

Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "     Intune Policy Search Script" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan

# Validate configuration
if ($TenantId -eq "YOUR-TENANT-ID-HERE" -or 
    $ClientId -eq "YOUR-CLIENT-ID-HERE" -or 
    $ClientSecret -eq "YOUR-CLIENT-SECRET-HERE") {
    Write-Host "`nERROR: Please update the configuration variables in the script!" -ForegroundColor Red
    Write-Host "You need to set:" -ForegroundColor Yellow
    Write-Host "  - `$TenantId" -ForegroundColor Yellow
    Write-Host "  - `$ClientId" -ForegroundColor Yellow
    Write-Host "  - `$ClientSecret" -ForegroundColor Yellow
    Write-Host ""
    exit 1
}

Write-Host "`nSearch Parameters:" -ForegroundColor Yellow
Write-Host "  Keyword: '$Keyword'" -ForegroundColor White
Write-Host ""

# Install required module if not present
if (!(Get-Module -ListAvailable -Name "MSAL.PS")) {
    Write-Host "Installing MSAL.PS module..." -ForegroundColor Yellow
    Install-Module -Name MSAL.PS -Force -AllowClobber -Scope CurrentUser
}

Import-Module MSAL.PS

function Get-GraphAccessToken {
    param(
        [string]$TenantId,
        [string]$ClientId,
        [string]$ClientSecret
    )
    
    $authority = "https://login.microsoftonline.com/$TenantId"
    $scope = "https://graph.microsoft.com/.default"
    
    $secureSecret = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
    
    try {
        $token = Get-MsalToken -ClientId $ClientId `
                              -ClientSecret $secureSecret `
                              -TenantId $TenantId `
                              -Authority $authority `
                              -Scopes $scope
        
        return $token.AccessToken
    }
    catch {
        Write-Error "Failed to authenticate: $_"
        return $null
    }
}

function Invoke-GraphRequest {
    param(
        [string]$Uri,
        [string]$AccessToken,
        [string]$Method = "GET"
    )
    
    $headers = @{
        "Authorization" = "Bearer $AccessToken"
        "Content-Type" = "application/json"
    }
    
    try {
        $response = Invoke-RestMethod -Uri $Uri -Headers $headers -Method $Method
        return $response
    }
    catch {
        $errorDetails = $_.ErrorDetails.Message
        if ($errorDetails) {
            Write-Warning "Graph API Error: $errorDetails"
        } else {
            Write-Warning "Error calling Graph API: $_"
        }
        Write-Warning "Failed URI: $Uri"
        return $null
    }
}

function Search-InPolicyContent {
    param(
        [object]$PolicyObject,
        [string]$SearchTerm,
        [string]$PolicyName = "Unknown"
    )
    
    $json = $PolicyObject | ConvertTo-Json -Depth 100
    
    # Case-insensitive search for the whole word
    # Using lookahead and lookbehind for better word boundary detection
    # This handles cases where \b doesn't work properly with certain characters
    $pattern = "(?i)(?<![a-zA-Z0-9])" + [regex]::Escape($SearchTerm) + "(?![a-zA-Z0-9])"
    
    $matches = [regex]::Matches($json, $pattern)
    
    if ($matches.Count -gt 0) {
        # Debug: Show where the match was found
        Write-Host "    → Found '$SearchTerm' $($matches.Count) time(s)" -ForegroundColor DarkYellow
        
        # Show context of first match for debugging
        $firstMatch = $matches[0]
        $startIndex = [Math]::Max(0, $firstMatch.Index - 50)
        $length = [Math]::Min(150, $json.Length - $startIndex)
        $context = $json.Substring($startIndex, $length) -replace '\s+', ' ' -replace '[\\"]', ''
        Write-Host "    → Context: ...$context..." -ForegroundColor DarkGray
        
        return $true
    }
    
    return $false
}

function Get-PolicyAssignments {
    param(
        [string]$PolicyId,
        [string]$PolicyType,
        [string]$AccessToken
    )
    
    $assignmentUri = switch ($PolicyType) {
        "deviceConfigurations" { 
            "https://graph.microsoft.com/v1.0/deviceManagement/deviceConfigurations('$PolicyId')/assignments" 
        }
        "configurationPolicies" { 
            "https://graph.microsoft.com/v1.0/deviceManagement/configurationPolicies('$PolicyId')/assignments" 
        }
        "compliancePolicies" { 
            "https://graph.microsoft.com/v1.0/deviceManagement/deviceCompliancePolicies('$PolicyId')/assignments" 
        }
        "groupPolicyConfigurations" {
            "https://graph.microsoft.com/v1.0/deviceManagement/groupPolicyConfigurations('$PolicyId')/assignments"
        }
        default { $null }
    }
    
    if ($assignmentUri) {
        $assignments = Invoke-GraphRequest -Uri $assignmentUri -AccessToken $accessToken
        
        $assignmentDetails = @()
        foreach ($assignment in $assignments.value) {
            $target = $assignment.target
            
            $assignmentInfo = @{
                Type = $target.'@odata.type'
                GroupId = $null
                GroupName = "N/A"
            }
            
            # Handle different assignment types
            switch ($target.'@odata.type') {
                "#microsoft.graph.groupAssignmentTarget" {
                    $assignmentInfo.GroupId = $target.groupId
                    # Get group name
                    $groupUri = "https://graph.microsoft.com/v1.0/groups/$($target.groupId)"
                    $group = Invoke-GraphRequest -Uri $groupUri -AccessToken $accessToken
                    $assignmentInfo.GroupName = if ($group) { $group.displayName } else { "Unknown Group" }
                    $assignmentInfo.Type = "Group"
                }
                "#microsoft.graph.allLicensedUsersAssignmentTarget" {
                    $assignmentInfo.GroupName = "All Users"
                    $assignmentInfo.Type = "All Users"
                }
                "#microsoft.graph.allDevicesAssignmentTarget" {
                    $assignmentInfo.GroupName = "All Devices"
                    $assignmentInfo.Type = "All Devices"
                }
                "#microsoft.graph.exclusionGroupAssignmentTarget" {
                    $assignmentInfo.GroupId = $target.groupId
                    $groupUri = "https://graph.microsoft.com/v1.0/groups/$($target.groupId)"
                    $group = Invoke-GraphRequest -Uri $groupUri -AccessToken $accessToken
                    $assignmentInfo.GroupName = if ($group) { "Excluded: $($group.displayName)" } else { "Excluded: Unknown Group" }
                    $assignmentInfo.Type = "Exclusion"
                }
            }
            
            $assignmentDetails += $assignmentInfo
        }
        
        return $assignmentDetails
    }
    
    return @()
}

# Main script execution
Write-Host "Authenticating with Azure AD..." -ForegroundColor Green
$accessToken = Get-GraphAccessToken -TenantId $TenantId -ClientId $ClientId -ClientSecret $ClientSecret

if (!$accessToken) {
    Write-Error "Failed to obtain access token. Please check your credentials."
    exit 1
}

Write-Host "Authentication successful!" -ForegroundColor Green
Write-Host ""

$results = @()
$searchStartTime = Get-Date

# Search Device Configuration Profiles
Write-Host "Searching Device Configuration Profiles..." -ForegroundColor Green
$deviceConfigUri = "https://graph.microsoft.com/v1.0/deviceManagement/deviceConfigurations"
$deviceConfigs = Invoke-GraphRequest -Uri $deviceConfigUri -AccessToken $accessToken

if ($deviceConfigs) {
    Write-Host "  Checking $($deviceConfigs.value.Count) policies..." -ForegroundColor Gray
    foreach ($config in $deviceConfigs.value) {
        if (Search-InPolicyContent -PolicyObject $config -SearchTerm $Keyword -PolicyName $config.displayName) {
            Write-Host "  ✓ Found match in: $($config.displayName)" -ForegroundColor Yellow
            
            $assignments = Get-PolicyAssignments -PolicyId $config.id -PolicyType "deviceConfigurations" -AccessToken $accessToken
            
            $results += [PSCustomObject]@{
                PolicyType = "Device Configuration"
                PolicyName = $config.displayName
                PolicyId = $config.id
                Platform = $config.'@odata.type' -replace '#microsoft.graph.', ''
                Assignments = $assignments
            }
        }
    }
} else {
    Write-Host "  ⚠ No policies retrieved or error occurred" -ForegroundColor Red
}

# Search Configuration Policies (Settings Catalog)
Write-Host "Searching Configuration Policies (Settings Catalog)..." -ForegroundColor Green
$configPolicyUri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies"
$configPolicies = Invoke-GraphRequest -Uri $configPolicyUri -AccessToken $accessToken

if ($configPolicies) {
    Write-Host "  Checking $($configPolicies.value.Count) policies..." -ForegroundColor Gray
    foreach ($policy in $configPolicies.value) {
        # Get detailed settings for this policy
        $settingsUri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($policy.id)')/settings"
        $settings = Invoke-GraphRequest -Uri $settingsUri -AccessToken $accessToken
        
        $policyData = @{
            Policy = $policy
            Settings = $settings
        }
        
        if (Search-InPolicyContent -PolicyObject $policyData -SearchTerm $Keyword -PolicyName $policy.name) {
            Write-Host "  ✓ Found match in: $($policy.name)" -ForegroundColor Yellow
            
            $assignments = Get-PolicyAssignments -PolicyId $policy.id -PolicyType "configurationPolicies" -AccessToken $accessToken
            
            $results += [PSCustomObject]@{
                PolicyType = "Configuration Policy (Settings Catalog)"
                PolicyName = $policy.name
                PolicyId = $policy.id
                Platform = $policy.platforms -join ", "
                Assignments = $assignments
            }
        }
    }
}

# Search Compliance Policies
Write-Host "Searching Compliance Policies..." -ForegroundColor Green
$complianceUri = "https://graph.microsoft.com/v1.0/deviceManagement/deviceCompliancePolicies"
$compliancePolicies = Invoke-GraphRequest -Uri $complianceUri -AccessToken $accessToken

if ($compliancePolicies) {
    Write-Host "  Checking $($compliancePolicies.value.Count) policies..." -ForegroundColor Gray
    foreach ($policy in $compliancePolicies.value) {
        if (Search-InPolicyContent -PolicyObject $policy -SearchTerm $Keyword -PolicyName $policy.displayName) {
            Write-Host "  ✓ Found match in: $($policy.displayName)" -ForegroundColor Yellow
            
            $assignments = Get-PolicyAssignments -PolicyId $policy.id -PolicyType "compliancePolicies" -AccessToken $accessToken
            
            $results += [PSCustomObject]@{
                PolicyType = "Compliance Policy"
                PolicyName = $policy.displayName
                PolicyId = $policy.id
                Platform = $policy.'@odata.type' -replace '#microsoft.graph.', ''
                Assignments = $assignments
            }
        }
    }
}

# Search Group Policy Configurations (Administrative Templates)
Write-Host "Searching Administrative Templates..." -ForegroundColor Green
$groupPolicyUri = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations?`$expand=definitionValues"
$groupPolicies = Invoke-GraphRequest -Uri $groupPolicyUri -AccessToken $accessToken

if ($groupPolicies) {
    Write-Host "  Checking $($groupPolicies.value.Count) policies..." -ForegroundColor Gray
    foreach ($policy in $groupPolicies.value) {
        if (Search-InPolicyContent -PolicyObject $policy -SearchTerm $Keyword -PolicyName $policy.displayName) {
            Write-Host "  ✓ Found match in: $($policy.displayName)" -ForegroundColor Yellow
            
            $assignments = Get-PolicyAssignments -PolicyId $policy.id -PolicyType "groupPolicyConfigurations" -AccessToken $accessToken
            
            $results += [PSCustomObject]@{
                PolicyType = "Administrative Template"
                PolicyName = $policy.displayName
                PolicyId = $policy.id
                Platform = "Windows"
                Assignments = $assignments
            }
        }
    }
}

$searchEndTime = Get-Date
$searchDuration = $searchEndTime - $searchStartTime

# Display results
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "           SEARCH RESULTS" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Search completed in: $($searchDuration.TotalSeconds) seconds" -ForegroundColor Gray
Write-Host "Found $($results.Count) policies containing '$Keyword'`n" -ForegroundColor Green

if ($results.Count -eq 0) {
    Write-Host "No policies found containing the keyword '$Keyword'" -ForegroundColor Yellow
}
else {
    foreach ($result in $results) {
        Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
        
        Write-Host "Policy Name: " -NoNewline -ForegroundColor Yellow
        Write-Host $result.PolicyName -ForegroundColor White
        
        Write-Host "Policy Type: " -NoNewline -ForegroundColor Yellow
        Write-Host $result.PolicyType -ForegroundColor Gray
        
        Write-Host "Platform: " -NoNewline -ForegroundColor Yellow
        Write-Host $result.Platform -ForegroundColor Gray
        
        Write-Host "Policy ID: " -NoNewline -ForegroundColor Yellow
        Write-Host $result.PolicyId -ForegroundColor DarkGray
        
        Write-Host "Assigned to:" -ForegroundColor Yellow
        if ($result.Assignments.Count -eq 0) {
            Write-Host "  • No assignments" -ForegroundColor DarkGray
        }
        else {
            foreach ($assignment in $result.Assignments) {
                $icon = switch ($assignment.Type) {
                    "Group" { "►" }
                    "All Users" { "◉" }
                    "All Devices" { "◈" }
                    "Exclusion" { "✗" }
                    default { "•" }
                }
                Write-Host "  $icon $($assignment.GroupName) [$($assignment.Type)]" -ForegroundColor Cyan
            }
        }
        
        Write-Host ""
    }
}

# Export to CSV
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$exportPath = "$PSScriptRoot\IntuneSearchResults_${Keyword}_${timestamp}.csv"
$exportData = @()

foreach ($result in $results) {
    foreach ($assignment in $result.Assignments) {
        $exportData += [PSCustomObject]@{
            SearchKeyword = $Keyword
            PolicyName = $result.PolicyName
            PolicyType = $result.PolicyType
            Platform = $result.Platform
            PolicyId = $result.PolicyId
            AssignedTo = $assignment.GroupName
            AssignmentType = $assignment.Type
            GroupId = $assignment.GroupId
        }
    }
    
    if ($result.Assignments.Count -eq 0) {
        $exportData += [PSCustomObject]@{
            SearchKeyword = $Keyword
            PolicyName = $result.PolicyName
            PolicyType = $result.PolicyType
            Platform = $result.Platform
            PolicyId = $result.PolicyId
            AssignedTo = "Not Assigned"
            AssignmentType = "None"
            GroupId = ""
        }
    }
}

if ($exportData.Count -gt 0) {
    $exportData | Export-Csv -Path $exportPath -NoTypeInformation
    Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
    Write-Host "`n✓ Results exported to:" -ForegroundColor Green
    Write-Host "  $exportPath" -ForegroundColor Cyan
}

Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "         Search Complete!" -ForegroundColor Green
Output 

It will then output a CSV file with the relevant policy details and assignment groups that meet the search criteria, below you can see the search for location and desktop:


Conclusion

This script has saved me countless hours when tracking down policy settings in Intune. Whether it's for security audits, troubleshooting, or just understanding which policies configure specific settings, being able to search inside policy content rather than just by name is invaluable.

Previous Post Next Post

نموذج الاتصال