categories/Services.ps1

# Services category: curated, risk-rated optional-service catalog (research.md - originally 36
# entries cross-referenced against multiple independent public sources; 35 remain Service-typed
# below since 008 moved DoSvc to a Registry-typed action - see that note further down). Not
# already covered by telemetry/oem-suites - FR-037.
#
# 008: DoSvc (Delivery Optimization) removed from the generic Service-typed catalog above -
# confirmed during 006's VM validation that Set-Service is rejected by DoSvc's ACL even under a
# full admin token (Microsoft intentionally steers DoSvc configuration through Group Policy, not
# the service control API). Replaced below with a Registry-typed action targeting the documented
# DODownloadMode Group Policy key instead - see Get-OctaServicesActions/Set-OctaServicesAction.

$script:OctaServicesCatalog = @(
    # Safe - target Disabled, no working-functionality loss if genuinely unused
    @{ Name = 'MapsBroker'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'WpcMonSvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'PhoneSvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'RetailDemo'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'wisvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'icssvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'XboxGipSvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'XblAuthManager'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'XblGameSave'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'XboxNetApiSvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'Fax'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'WalletService'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'TabletInputService'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'lfsvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'WerSvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'SCardSvr'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'ScDeviceEnum'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'SCPolicySvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'BTAGService'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'bthserv'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'SensorService'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'WbioSrvc'; Target = 'Disabled'; RiskLevel = 'Safe' }
    @{ Name = 'DevicePickerUserSvc'; Target = 'Disabled'; RiskLevel = 'Safe'; IsUserTemplate = $true }

    # Moderate - target Manual, real functionality trade-off if actually used
    @{ Name = 'Spooler'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'DPS'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'WdiServiceHost'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'WdiSystemHost'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'DiagsSvc'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'SessionEnv'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'TermService'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'UmRdpService'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'WorkFolders'; Target = 'Manual'; RiskLevel = 'Moderate' }
    @{ Name = 'Netlogon'; Target = 'Manual'; RiskLevel = 'Moderate' }

    # Risky - real, narrow chance of a serious problem
    @{ Name = 'BDESVC'; Target = 'Disabled'; RiskLevel = 'Risky' }
    @{ Name = 'AssignedAccessManagerSvc'; Target = 'Disabled'; RiskLevel = 'Risky' }
)

function Get-OctaServicesCategory {
    [CmdletBinding()]
    param()
    return [pscustomobject]@{
        Id                          = 'services'
        DisplayName                 = 'Services'
        Description                 = 'Curated, risk-rated optional Windows services'
        RequiresElevation           = $true
        ContainsIrreversibleActions = $false
        GetActionsFunction          = 'Get-OctaServicesActions'
        ApplyActionFunction         = 'Set-OctaServicesAction'
    }
}

function Get-OctaServicesActions {
    [CmdletBinding()]
    param()
    $actions = @()

    # ponytail: one bulk Get-CimInstance, matched against the catalog in memory - not one
    # -Filter query per catalog entry. Measured directly: 35 separate `-Filter "Name='X'"`
    # calls took ~10.1s total (~290ms each, WQL query overhead per call), while fetching every
    # service in a single unfiltered call and grouping by name took 0.47s - about 20x faster for
    # the exact same result. This tripped the project's own "GetActionsFunction completes
    # quickly" perf test at the 10-second threshold, the same root cause as the earlier
    # Get-OctaCategory startup-delay fix (many small expensive calls instead of one cheap one).
    $allServices = @(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue)
    $servicesByName = @{}
    foreach ($svc in $allServices) { $servicesByName[$svc.Name] = $svc }

    foreach ($entry in $script:OctaServicesCatalog) {
        # ponytail: named $matchedServices, not $matches - PowerShell variable names are
        # case-insensitive, so a bare "$matches" is the SAME variable as the automatic $Matches
        # the -match operator populates on the line right below it. It happened to work only
        # because nothing downstream read $Matches's regex capture groups after this point - a
        # future edit that did (a common PowerShell idiom right after a -match) would have
        # silently gotten this services array instead. Found via static analysis
        # (PSAvoidAssignmentToAutomaticVariable), not a live failure.
        if ($entry.IsUserTemplate) {
            $matchedServices = @($allServices | Where-Object { $_.Name -match "^$($entry.Name)_" })
        }
        elseif ($servicesByName.ContainsKey($entry.Name)) {
            $matchedServices = @($servicesByName[$entry.Name])
        }
        else {
            $matchedServices = @()
        }

        foreach ($svc in $matchedServices) {
            if ($svc.StartMode -eq $entry.Target) { continue }
            $actions += New-OctaAction -TargetType Service -TargetIdentifier $svc.Name `
                -CurrentValue $svc.StartMode -PlannedValue $entry.Target -Reversible $true -RiskLevel $entry.RiskLevel
        }
    }

    # Delivery Optimization via Group Policy (008) - see file header comment for why this isn't
    # a Set-Service call like everything else above.
    $doKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization'
    $doMode = (Get-ItemProperty -Path $doKey -Name 'DODownloadMode' -ErrorAction SilentlyContinue).DODownloadMode
    if ($doMode -ne 0) {
        $displayCurrent = if ($null -eq $doMode) { '(not set)' } else { $doMode }
        $actions += New-OctaAction -TargetType Registry -TargetIdentifier "$doKey|DODownloadMode" `
            -CurrentValue $displayCurrent -PlannedValue 0 -Reversible $true -RiskLevel Moderate
    }

    return $actions
}

function Set-OctaServicesAction {
    [CmdletBinding()]
    param([Parameter(Mandatory)]$Action)

    if ($Action.TargetType -eq 'Registry') {
        $keyPath, $valueName = $Action.TargetIdentifier -split '\|', 2
        if (-not (Test-Path $keyPath)) {
            New-Item -Path $keyPath -Force | Out-Null
        }
        Set-ItemProperty -Path $keyPath -Name $valueName -Value $Action.PlannedValue -Type DWord
        return
    }

    Set-Service -Name $Action.TargetIdentifier -StartupType $Action.PlannedValue
}