Public/Search-IntuneSetting.ps1

function Search-IntuneSetting {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, HelpMessage = "Keyword to search for in setting definitions")]
        [string]$Keyword,

        [Parameter(HelpMessage = "Show all matching definitions, including those not configured in any policy")]
        [switch]$ShowAll,

        [Parameter()]
        [switch]$ExportToCSV,

        [Parameter()]
        [string]$ExportPath
    )

    # Requires active Graph connection
    if (-not $script:GraphEndpoint) {
        Write-Host "Not connected. Run Connect-IntuneAssignmentChecker first." -ForegroundColor Red
        return
    }

    if ([string]::IsNullOrWhiteSpace($Keyword)) {
        Write-Host "Enter a setting keyword to search for (e.g., BitLocker, encryption, password): " -ForegroundColor Cyan
        $Keyword = Read-Host
    }

    if ([string]::IsNullOrWhiteSpace($Keyword)) {
        Write-Host "No keyword provided. Please try again." -ForegroundColor Red
        return
    }

    # ── Expand common abbreviations ────────────────────────────────────
    $abbreviations = @{
        'psso'     = 'platform sso'
        'mdatp'    = 'defender for endpoint'
        'wdac'     = 'application control'
        'asr'      = 'attack surface reduction'
        'edr'      = 'endpoint detection'
        'av'       = 'antivirus'
        'laps'     = 'local administrator password'
        'whfb'     = 'windows hello for business'
        'wufb'     = 'windows update for business'
        'esp'      = 'enrollment status page'
        'mfa'      = 'multi-factor authentication'
    }

    $expandedKeyword = $null
    if ($abbreviations.ContainsKey($Keyword.ToLower())) {
        $expandedKeyword = $abbreviations[$Keyword.ToLower()]
        Write-Host "Expanding '$Keyword' to '$expandedKeyword'" -ForegroundColor DarkGray
    }

    # ── Load setting definitions ─────────────────────────────────────────
    $dataPath = Join-Path $PSScriptRoot ".." "Data" "SettingDefinitions.json"
    if (-not (Test-Path $dataPath)) {
        Write-Host "Setting definitions file not found at: $dataPath" -ForegroundColor Red
        Write-Host "Run Update-IntuneSettingDefinition first to download the catalog." -ForegroundColor Yellow
        return
    }

    $rawJson = Get-Content -Path $dataPath -Raw -Encoding UTF8
    $definitions = $rawJson | ConvertFrom-Json

    if ($null -eq $definitions -or $definitions.Count -eq 0) {
        Write-Host "Setting definitions file is empty." -ForegroundColor Red
        Write-Host "Run Update-IntuneSettingDefinition first to download the catalog." -ForegroundColor Yellow
        return
    }

    # ── Search definitions by keyword ────────────────────────────────────
    # Build list of search terms: original keyword + expanded abbreviation (if any)
    $searchTerms = @($Keyword.ToLower())
    if ($expandedKeyword) { $searchTerms += $expandedKeyword.ToLower() }

    $matchedDefinitions = [System.Collections.ArrayList]::new()

    foreach ($def in $definitions) {
        $match = $false

        foreach ($term in $searchTerms) {
            if ($match) { break }
            $termNormalized = ($term -replace '[_\-\.\s]', '')

            # Check displayName (exact substring)
            if ($def.displayName -and $def.displayName.ToLower().Contains($term)) {
                $match = $true
            }

            # Check id (exact substring)
            if (-not $match -and $def.id -and $def.id.ToLower().Contains($term)) {
                $match = $true
            }

            # Check id with normalized form (spaces/separators removed)
            if (-not $match -and $def.id) {
                $idNormalized = ($def.id.ToLower() -replace '[_\-\.\s]', '')
                if ($idNormalized.Contains($termNormalized)) {
                    $match = $true
                }
            }

            # Check displayName with normalized form
            if (-not $match -and $def.displayName) {
                $nameNormalized = ($def.displayName.ToLower() -replace '[_\-\.\s]', '')
                if ($nameNormalized.Contains($termNormalized)) {
                    $match = $true
                }
            }

            # Check keywords array
            if (-not $match -and $def.keywords) {
                foreach ($kw in $def.keywords) {
                    if ($kw -and $kw.ToLower().Contains($term)) {
                        $match = $true
                        break
                    }
                }
            }
        }

        if ($match) {
            $null = $matchedDefinitions.Add($def)
        }
    }

    if ($matchedDefinitions.Count -eq 0) {
        Write-Host "`nNo setting definitions found matching '$Keyword'." -ForegroundColor Yellow
        return
    }

    # Build a lookup set of matched definition IDs for fast checking
    $matchedIdSet = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::OrdinalIgnoreCase
    )
    foreach ($def in $matchedDefinitions) {
        $null = $matchedIdSet.Add($def.id)
    }

    # ── Header ───────────────────────────────────────────────────────────
    Write-Host ""
    Write-Host (Get-Separator -Character "=") -ForegroundColor Cyan
    Write-Host " SETTING SEARCH RESULTS" -ForegroundColor Cyan
    Write-Host " Search term: '$Keyword'" -ForegroundColor White
    Write-Host " Found $($matchedDefinitions.Count) matching setting $(if ($matchedDefinitions.Count -eq 1) { 'definition' } else { 'definitions' })" -ForegroundColor White
    Write-Host (Get-Separator -Character "=") -ForegroundColor Cyan

    # ── Fetch all configuration policies (Settings Catalog + Endpoint Security) ──
    Write-Host "`nFetching configuration policies..." -ForegroundColor Yellow

    $allPolicies = [System.Collections.ArrayList]::new()
    $policyUri = "$($script:GraphEndpoint)/beta/deviceManagement/configurationPolicies?`$select=id,name,description,templateReference"
    do {
        try {
            $policyResponse = Invoke-MgGraphRequest -Uri $policyUri -Method Get
            if ($policyResponse.value) {
                foreach ($p in $policyResponse.value) {
                    $null = $allPolicies.Add($p)
                }
            }
            $policyUri = $policyResponse.'@odata.nextLink'
        }
        catch {
            Write-Host "Error fetching policies: $($_.Exception.Message)" -ForegroundColor Red
            return
        }
    } while (![string]::IsNullOrEmpty($policyUri))

    Write-Host "Found $($allPolicies.Count) configuration policies. Scanning settings..." -ForegroundColor Gray

    # ── Scan each policy for matching settings ───────────────────────────
    # Structure: definitionId -> list of { PolicyName, PolicyId, ConfiguredValue }
    $settingResults = @{}

    $totalPolicies = $allPolicies.Count
    $currentPolicy = 0

    foreach ($policy in $allPolicies) {
        $currentPolicy++
        $policyName = if (-not [string]::IsNullOrWhiteSpace($policy.name)) { $policy.name } else { "Unnamed Policy" }
        $lineWidth = try { $Host.UI.RawUI.WindowSize.Width - 1 } catch { 120 }
        $progressText = "[$currentPolicy/$totalPolicies] Scanning: $policyName"
        if ($progressText.Length -gt $lineWidth) { $progressText = $progressText.Substring(0, $lineWidth - 3) + "..." }
        Write-Host "`r$($progressText.PadRight($lineWidth))" -NoNewline

        $settingsUri = "$($script:GraphEndpoint)/beta/deviceManagement/configurationPolicies('$($policy.id)')/settings"
        try {
            $settingsResponse = Invoke-MgGraphRequest -Uri $settingsUri -Method Get
            if ($settingsResponse.value) {
                foreach ($setting in $settingsResponse.value) {
                    $instance = $setting.settingInstance
                    if ($null -eq $instance) { continue }

                    $defId = $instance.settingDefinitionId
                    if ([string]::IsNullOrEmpty($defId)) { continue }

                    if ($matchedIdSet.Contains($defId)) {
                        $configuredValue = Get-SettingValue -SettingInstance $instance

                        if (-not $settingResults.ContainsKey($defId)) {
                            $settingResults[$defId] = [System.Collections.ArrayList]::new()
                        }
                        $null = $settingResults[$defId].Add([PSCustomObject]@{
                            PolicyName      = $policyName
                            PolicyId        = $policy.id
                            ConfiguredValue = $configuredValue
                        })
                    }
                }
            }
        }
        catch {
            # Skip policies where settings cannot be fetched
        }
    }

    Write-Host "`r$((' ' * 120))" -NoNewline
    Write-Host "`rPolicy scan complete." -ForegroundColor Green

    # ── Display results per definition ───────────────────────────────────
    $exportData = [System.Collections.ArrayList]::new()
    $separator = Get-Separator

    $configuredCount = 0
    $skippedCount = 0

    foreach ($def in $matchedDefinitions) {
        $defId = $def.id
        $defName = if ($def.displayName) { $def.displayName } else { $defId }
        $defDesc = if ($def.description) { $def.description } else { "(no description)" }
        $hasConfigured = $settingResults.ContainsKey($defId) -and $settingResults[$defId].Count -gt 0

        # By default, only show definitions that are configured in at least one policy
        if (-not $hasConfigured -and -not $ShowAll) {
            $skippedCount++
            continue
        }

        Write-Host ""
        Write-Host "===== $defName =====" -ForegroundColor White
        Write-Host "Definition ID: $defId" -ForegroundColor Gray
        Write-Host "Description: $defDesc" -ForegroundColor Gray

        if ($hasConfigured) {
            $configuredCount++
            $policies = $settingResults[$defId]

            Write-Host ""
            Write-Host "Policies configuring this setting:" -ForegroundColor Yellow
            Write-Host (" {0,-80} {1}" -f "Policy Name", "Configured Value") -ForegroundColor Cyan
            Write-Host " $separator" -ForegroundColor Gray

            foreach ($entry in $policies) {
                $displayName = if ($entry.PolicyName.Length -gt 78) {
                    $entry.PolicyName.Substring(0, 75) + "..."
                } else {
                    $entry.PolicyName
                }
                Write-Host (" {0,-80} {1}" -f $displayName, $entry.ConfiguredValue) -ForegroundColor White

                $null = $exportData.Add([PSCustomObject]@{
                    SettingName        = $defName
                    SettingDefinitionId = $defId
                    PolicyName         = $entry.PolicyName
                    PolicyId           = $entry.PolicyId
                    ConfiguredValue    = $entry.ConfiguredValue
                })
            }
            Write-Host " $separator" -ForegroundColor Gray
        }
        else {
            # Only shown when -ShowAll is used
            Write-Host ""
            Write-Host "Policies configuring this setting:" -ForegroundColor Yellow
            Write-Host " (no policies configure this setting)" -ForegroundColor DarkGray
        }
    }

    if ($skippedCount -gt 0) {
        Write-Host ""
        Write-Host "($skippedCount matching definitions not configured in any policy -- use -ShowAll to see them)" -ForegroundColor DarkGray
    }

    # ── Summary ──────────────────────────────────────────────────────────
    $policiesWithSettings = ($settingResults.Keys | Measure-Object).Count
    Write-Host ""
    Write-Host (Get-Separator -Character "=") -ForegroundColor Cyan
    Write-Host " Summary: $($matchedDefinitions.Count) definitions matched, $policiesWithSettings configured in policies" -ForegroundColor Cyan
    Write-Host (Get-Separator -Character "=") -ForegroundColor Cyan

    # ── Export ───────────────────────────────────────────────────────────
    Export-ResultsIfRequested -ExportData $exportData -DefaultFileName "IntuneSettingSearch.csv" -ForceExport:$ExportToCSV -CustomExportPath $ExportPath
}

# ── Helper: extract configured value from a setting instance ─────────
function Get-SettingValue {
    param(
        [Parameter(Mandatory = $true)]
        [object]$SettingInstance
    )

    $type = $SettingInstance.'@odata.type'

    switch -Wildcard ($type) {
        '*choiceSettingInstance' {
            $rawValue = $SettingInstance.choiceSettingValue.value
            if ($rawValue) {
                # Extract last segment for readability (e.g., "..._1" -> "1")
                $segments = $rawValue -split '_'
                $shortValue = $segments[-1]
                return "$shortValue ($rawValue)"
            }
            return "(no value)"
        }
        '*simpleSettingInstance' {
            $val = $SettingInstance.simpleSettingValue.value
            if ($null -ne $val) { return [string]$val }
            return "(no value)"
        }
        '*simpleSettingCollectionInstance' {
            $values = $SettingInstance.simpleSettingCollectionValue
            if ($values -and $values.Count -gt 0) {
                return "Collection ($($values.Count) items)"
            }
            return "Collection (empty)"
        }
        '*groupSettingCollectionInstance' {
            $children = $SettingInstance.groupSettingCollectionValue
            if ($children -and $children.Count -gt 0) {
                return "Group Collection ($($children.Count) items)"
            }
            return "Group Collection (empty)"
        }
        '*groupSettingInstance' {
            return "Group Setting"
        }
        default {
            return "(unknown type: $type)"
        }
    }
}