windows-csp/apply-mdm-policy.ps1
|
[CmdletBinding()] param ( [Parameter()] [string]$Token, [Parameter()] [string]$PRNumber, [Parameter()] [switch]$RetryAttempt = $false ) begin { # Check for required PowerShell version (7+) if (!($PSVersionTable.PSVersion.Major -ge 7)) { try { # Install PowerShell 7 if missing if (!(Test-Path "$env:SystemDrive\Program Files\PowerShell\7")) { Write-Output '[INFO] Installing PowerShell version 7...' Invoke-Expression "& { $( Invoke-RestMethod https://aka.ms/install-powershell.ps1 ) } -UseMSI -Quiet" } # Refresh PATH $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User') # Restart script in PowerShell 7 pwsh -File "`"$PSCommandPath`"" @PSBoundParameters } catch { Write-Output '[ERROR] PowerShell 7 was not installed. Update PowerShell and try again.' throw $Error } finally { exit $LASTEXITCODE } } else { # $PSStyle.OutputRendering = 'PlainText' } if ($env:token -and $env:token -notlike "null") { $Token = Ninja-Property-Get $env:token } if ($env:prNumber -and $env:prNumber -notlike "null") { $PRNumber = $env:prNumber } if (-not $Token) { Write-LogMessage "Please specify a Token." -LogType Error exit 1 } function Set-PolicyFailure { Ninja-Property-Set latestPolicyFailure (Get-Date -UFormat %s) } function Write-LogMessage { param( [string]$Message, [System.ConsoleColor]$Color = 'White', [string]$LogType = "" ) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $LogType = $LogType.ToUpper() # Add prefix based on color [System.ConsoleColor]$consoleColor = switch ($LogType) { 'ERROR' { 'Red' } 'SUCCESS' { 'Green' } 'WARNING' { 'Yellow' } 'DEBUG' { 'Cyan' } default { 'White' } } $logMessage = "${timestamp}: [$LogType] $Message" # Console output if ($LogType -ne "DEBUG" -or $VerbosePreference -eq 'Continue') { Write-Host $logMessage -ForegroundColor $consoleColor } try { Add-Content -Path $LogFilePath -Value $logMessage -ErrorAction Stop } catch { Write-Host "Error writing to log file: $_" -ForegroundColor Red } } function Remove-XmlComments { param([System.Xml.XmlNode]$Node) foreach ($comment in @($Node.SelectNodes("//comment()"))) { $comment.ParentNode.RemoveChild($comment) | Out-Null } } function Rename-XmlElement { param( [System.Xml.XmlElement]$Element, [string]$NewName ) $doc = $Element.OwnerDocument $newElement = $doc.CreateElement($NewName) # Copy attributes foreach ($attr in $Element.Attributes) { $newAttr = $doc.CreateAttribute($attr.Name) $newAttr.Value = $attr.Value $newElement.Attributes.Append($newAttr) | Out-Null } # Copy child nodes foreach ($child in $Element.ChildNodes) { $imported = $doc.ImportNode($child, $true) $newElement.AppendChild($imported) | Out-Null } # Replace in parent $parent = $Element.ParentNode $parent.ReplaceChild($newElement, $Element) | Out-Null return $newElement } function Get-PolicyFile { $accountname = "jreappstorage" $containername = "it-dept" $file = "policies/definitions.zip" $azureContext = New-AzStorageContext -StorageAccountName $accountname -StorageAccountKey $Token # if prnumber is not null, use it to get the file name if ($PRNumber -and $PRNumber -notlike "null") { $file = "policies/pr-$PRNumber/definitions.zip" } $ExpectedHash = (Get-AzStorageBlob -Container $containername -Blob $file -Context $azureContext).BlobProperties.ContentHash $ExpectedHash = [System.BitConverter]::ToString($ExpectedHash).Replace("-", "") $filePath = Join-Path $tempDir (Split-Path $file -leaf) # Download or verify existing file if ((Test-Path $filePath) -and ((Get-FileHash -Path $filePath -Algorithm "MD5").Hash -eq $ExpectedHash)) { Write-LogMessage "Using existing verified file" -LogType Debug } else { Write-LogMessage "Downloading fresh copy from $accountname/$containername/$file..." -LogType Debug Remove-Item -Path $filePath -Force -ErrorAction SilentlyContinue try { Get-AzStorageBlobContent -Container $containername -Blob $file -Destination $filePath -Context $azureContext -Force | Out-Null $actualHash = (Get-FileHash -Path $filePath -Algorithm "MD5").Hash if ($actualHash -ne $ExpectedHash) { throw "File hash verification failed. Expected: $ExpectedHash, Actual: $actualHash" } Write-LogMessage "File hash verified successfully" -LogType Debug } catch { throw "Failed to download or verify policy file: $_" } } # get the folder path from the file path $folderPath = Split-Path -Path $filePath -Parent $extractedFolder = Join-Path $folderPath ([System.IO.Path]::GetFileNameWithoutExtension($filePath)) Write-LogMessage "Deleting files in: $extractedFolder" -LogType Debug # delete the folder if it exists if (Test-Path -Path $extractedFolder) { Remove-Item -Path $extractedFolder -Recurse -Force } Write-LogMessage "Extracting files in $filePath to: $folderPath" -LogType Debug # unzip the file Expand-Archive -Path $filePath -DestinationPath $folderPath -Force return $extractedFolder } function Test-StatusNodeFailure { param( [string]$DataNode, [string]$CmdRefNode, [string]$Action = "" ) if ($DataNode -eq "200") { return $false } if ($DataNode -eq "418") { return $false } if ($DataNode -eq "500" -and $CmdRefNode -ieq "AllowHibernate") { return $false } if ($DataNode -eq "400" -and $CmdRefNode -ieq "AllNetworks_NetworkLocation") { return $false } if ($DataNode -eq "404" -and $Action -ieq "DELETE") { return $false } return $true } function Install-AzModule { # check if Az.Storage module is installed if (-not (Get-Module -ListAvailable -Name Az.Storage)) { Write-LogMessage "Installing Az.Storage module..." # check to make sure the PSGallery is registered if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { Write-LogMessage "Registering PSGallery repository..." Register-PSRepository -Default } Install-Module -Name Az.Storage -Repository PSGallery -Scope CurrentUser -Force -AllowClobber # Refresh PATH $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User') if (!$RetryAttempt) { # Restart script in PowerShell 7 Write-Host "RESTARTING SCRIPT IN POWERSHELL 7" $NewParams = @{} # Copy all bound parameters except RetryAttempt foreach ($key in $PSBoundParameters.Keys) { if ($key -ne 'RetryAttempt') { $NewParams[$key] = $PSBoundParameters[$key] } } # Add RetryAttempt parameter with value $true $NewParams['RetryAttempt'] = $true pwsh -File $PSCommandPath @NewParams } } else { Write-LogMessage "Az.Storage module is already installed" } # Import the module Import-Module -Name Az.Storage -Force -ErrorAction Stop Write-LogMessage "Az.Storage module imported successfully" -LogType Success } function Invoke-FormatPolicyString { param( [string]$Data, [string]$Format ) if ($Format -eq "chr" -or $Format -ne "xml") { $Data = ($Data -replace '\s+', ' ').Trim() } if ($Data -match '[<>]') { # wrap in CDATA $Data = "<![CDATA[$($Data)]]>" } return $Data } #region -- Deleted Functions -- # function Invoke-DeletePolicy { # param( # [PSObject]$Command # ) # $Uris = @("./User/Vendor/MSFT/Policy/Config", "./Device/Vendor/MSFT/Policy/Config") # $ArgumentList = New-Object System.Collections.Generic.List[string] # $ArgumentList.Add("-OmaUri") # $ArgumentList.Add("./Device/Vendor/MSFT/Policy/ConfigOperations/ADMXInstall") # $xmlContent = [xml] (Start-ProcessEx -Arguments $ArgumentList) # $resultsNodes = $xmlContent.SelectNodes("//Results") # foreach ($result in $resultsNodes) { # $dataNode = $result.SelectSingleNode(".//Data") # # split the Data node value by '/' # $dataParts = $dataNode.InnerText -split '/' # # Log the individual parts # foreach ($part in $dataParts) { # $Uris += "./Device/Vendor/MSFT/Policy/ConfigOperations/ADMXInstall/$part" # } # } # foreach ($uri in $Uris) { # $ArgumentList = New-Object System.Collections.Generic.List[string] # $ArgumentList.Add("-OmaUri") # $ArgumentList.Add($uri) # $ArgumentList.Add("-Command") # $ArgumentList.Add("DELETE") # $xmlContent = [xml] (Start-ProcessEx -Arguments $ArgumentList) # $statusNodes = $xmlContent.SelectNodes("//Status") # if (-not $statusNodes) { # Write-LogMessage "[$($uri)] No Status nodes found in the XML output." -LogType Error # $script:containsErrors = $true # $script:failedPolicies += $uri # } # foreach ($status in $statusNodes) { # $dataNode = $status.SelectSingleNode("Data") # $cmdNode = $status.SelectSingleNode("Cmd") # $cmdRefNode = $status.SelectSingleNode("CmdRef") # if ($cmdNode.InnerText -eq "SyncHdr") { # continue # } # if (Test-StatusNodeFailure -DataNode $dataNode.InnerText -CmdRefNode $cmdRefNode.InnerText -Action "DELETE") { # Write-LogMessage "[$($uri)] Policy deletion error - $($status.OuterXml)" -LogType Error # [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'containsErrors', Justification = 'Used for error tracking')] # $script:containsErrors = $true # $script:failedPolicies += $uri # } # else { # Write-LogMessage "[$($uri)] Successfully deleted policy" -LogType Debug # } # } # } # } # function Start-ProcessEx { # param( # [string[]]$Arguments # ) # $process = New-Object System.Diagnostics.Process # $psi = New-Object System.Diagnostics.ProcessStartInfo # $psi.FileName = $exePath # $psi.RedirectStandardInput = $true # $psi.RedirectStandardOutput = $true # $psi.RedirectStandardError = $true # $psi.UseShellExecute = $false # $psi.CreateNoWindow = $true # foreach ($arg in $Arguments) { # $psi.ArgumentList.Add($arg) # } # $process.StartInfo = $psi # $process.Start() | Out-Null # $output = $process.StandardOutput.ReadToEnd() # $err = $process.StandardError.ReadToEnd() # $process.WaitForExit() # if ($err) { # Write-Error $err # } # return $output # } #endregion function Invoke-ApplyPolicy { param( [string]$Filepath ) $process = New-Object System.Diagnostics.Process $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $exePath $psi.RedirectStandardInput = $true $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $psi.ArgumentList.Add("-SyncMLFile") $psi.ArgumentList.Add($Filepath) $process.StartInfo = $psi $process.Start() | Out-Null $output = $process.StandardOutput.ReadToEnd() $err = $process.StandardError.ReadToEnd() $process.WaitForExit() if ($err) { Write-Error $err } $xmlContent = [xml]$output $statusNodes = $xmlContent.SelectNodes("//Status") if (-not $statusNodes) { Write-LogMessage "[$($Filepath)] No Status nodes found in the XML output." -LogType Error $script:containsErrors = $true # $script:failedPolicies += $Command.Uri.InnerText } foreach ($status in $statusNodes) { $dataNode = $status.SelectSingleNode("Data") $cmdNode = $status.SelectSingleNode("Cmd") $cmdRefNode = $status.SelectSingleNode("CmdRef") if ($cmdNode.InnerText -eq "SyncHdr") { continue } if (Test-StatusNodeFailure -DataNode $dataNode.InnerText -CmdRefNode $cmdRefNode.InnerText -Action $cmdNode.InnerText) { Write-LogMessage "[$($cmdRefNode.InnerText) - $($cmdNode.InnerText.ToUpper())] - $($status.OuterXml)" -LogType Error $script:containsErrors = $true $script:failedPolicies += $cmdRefNode.InnerText } else { Write-LogMessage "[$($cmdRefNode.InnerText) - $($cmdNode.InnerText.ToUpper())]" -LogType Success } } } function Invoke-XmlProcessing { param( [string]$Path ) $xmlContent = Get-Content -Path $Path -Raw # Find all policy nodes (Replace, Add, etc.) inside <SyncBody> $policyPattern = '<(Replace|Add|Delete|Exec)[^>]*>(.*?)</\1>' $policyMatches = [regex]::Matches($xmlContent, $policyPattern, 'Singleline') foreach ($policyMatch in $policyMatches) { $policyXml = $policyMatch.Value $policyType = $policyMatch.Groups[1].Value # Extract CmdID, LocURI, Data, Format, Type as strings $cmdId = [regex]::Match($policyXml, '<CmdID>(.*?)</CmdID>', 'Singleline').Groups[1].Value.Trim() $uri = [regex]::Match($policyXml, '<LocURI>(.*?)</LocURI>', 'Singleline').Groups[1].Value.Trim() $data = [regex]::Match($policyXml, '<Data>(.*?)</Data>', 'Singleline').Groups[1].Value.Trim() $format = [regex]::Match($policyXml, '<Format>(.*?)</Format>', 'Singleline').Groups[1].Value.Trim() $type = [regex]::Match($policyXml, '<Type>(.*?)</Type>', 'Singleline').Groups[1].Value.Trim() if (-not $type) { $type = 'text/plain' } $command = [PSCustomObject]@{ CmdID = $cmdId Uri = $uri Node = $policyXml Command = $policyType Data = $data Format = $format Type = $type } # Handle <Filters> logic $filtersPattern = '<Filters>(.*?)</Filters>' $filtersMatch = [regex]::Match($policyXml, $filtersPattern, 'Singleline') if ($filtersMatch.Success) { $filtersXml = $filtersMatch.Groups[1].Value $minOSMatch = [regex]::Match($filtersXml, '<MinOSVersion>(.*?)</MinOSVersion>', 'Singleline') if ($minOSMatch.Success) { $currentOS = [System.Environment]::OSVersion.Version $minOS = [version]$minOSMatch.Groups[1].Value.Trim() if ([version]$currentOS -lt [version]$minOS) { Write-LogMessage "Skipping policy $($command.Uri) due to MinOSVersion filter ($currentOS < $minOS)" -LogType Debug # remove the policy from xmlContent $xmlContent = $xmlContent.Replace($policyMatch.Value, "") continue } } # --- NEW: Locations filter support (single current location) --- $locationsPattern = '<Locations>(.*?)</Locations>' $locationsMatch = [regex]::Match($filtersXml, $locationsPattern, 'Singleline') if ($locationsMatch.Success) { # Current machine location (single value), e.g. NINJA_LOCATION_NAME=HQ $currentLocation = $null if ($env:NINJA_LOCATION_NAME) { $currentLocation = $env:NINJA_LOCATION_NAME.Trim() } if ($currentLocation) { $locationsXml = $locationsMatch.Groups[1].Value $includeLocations = @() foreach ($m in [regex]::Matches($locationsXml, '<Include>(.*?)</Include>', 'Singleline')) { foreach ($loc in [regex]::Matches($m.Groups[1].Value, '<Location>(.*?)</Location>', 'Singleline')) { $includeLocations += $loc.Groups[1].Value.Trim() } } $excludeLocations = @() foreach ($m in [regex]::Matches($locationsXml, '<Exclude>(.*?)</Exclude>', 'Singleline')) { foreach ($loc in [regex]::Matches($m.Groups[1].Value, '<Location>(.*?)</Location>', 'Singleline')) { $excludeLocations += $loc.Groups[1].Value.Trim() } } $shouldSkipByLocation = $false # If Include list is present, current location must be in it if ($includeLocations.Count -gt 0) { if (-not ($includeLocations -icontains $currentLocation)) { $shouldSkipByLocation = $true } } # Any match in Exclude list will skip the policy if (-not $shouldSkipByLocation -and $excludeLocations.Count -gt 0) { if ($excludeLocations -icontains $currentLocation) { $shouldSkipByLocation = $true } } if ($shouldSkipByLocation) { Write-LogMessage ("Skipping policy {0} due to Locations filter. " + "Current='{1}' Include='{2}' Exclude='{3}'" -f $command.Uri, $currentLocation, ($includeLocations -join ','), ($excludeLocations -join ',') ) -LogType Debug # remove the policy from xmlContent $xmlContent = $xmlContent.Replace($policyMatch.Value, "") continue } } } # --- END Locations filter support --- # Remove <Filters> from policyXml $policyXml = [regex]::Replace($policyXml, $filtersPattern, "", 'Singleline') } # Format policy data if needed (Invoke-FormatPolicy now expects string) $command.Data = Invoke-FormatPolicyString -Data $command.Data -Format $command.Format # Update the policyXml with the (potentially) modified Data value if ($command.Data) { # Replace the <Data>...</Data> content in the policyXml string $policyXml = [regex]::Replace( $policyXml, '<Data>.*?</Data>', "<Data>$($command.Data)</Data>", 'Singleline' ) # Replace the modified policyXml back into the xmlContent string $xmlContent = $xmlContent.Replace($policyMatch.Value, $policyXml) } # (Optional) Call Invoke-ApplyPolicy or other logic as needed # ... } # Save the modified XML string to a temp file and apply all policies at once $tempFile = "$tempDir\$([System.IO.Path]::GetFileName($Path))" Set-Content -Path $tempFile -Value $xmlContent Invoke-ApplyPolicy -Filepath $tempFile } function Invoke-RegProcessing { param( [string]$Path ) $user = (Get-CimInstance Win32_ComputerSystem).UserName if ($user) { $account = New-Object System.Security.Principal.NTAccount($user) $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]).Value (Get-Content $Path -Raw) -ireplace 'HKEY_CURRENT_USER', "HKEY_USERS\$sid" | Set-Content $Path } $process = Start-Process "regedit.exe" -ArgumentList "/s `"$Path`"" -Wait -PassThru if ($process.ExitCode -ne 0) { Write-LogMessage "[$($Path.Replace('C:\Windows\Temp', '')) failed with exit code: $($process.ExitCode)]" -LogType Error $script:containsErrors = $true $script:failedPolicies += $Path } else { Write-LogMessage "[$($Path.Replace('C:\Windows\Temp', ''))]" -LogType Success } } function Limit-LogFile { $LogSeparator = ('#' * 150) Write-LogMessage $LogSeparator -LogType Debug # if the log file is larger than 10MB, delete the first 5MB of the file if ((Test-Path -Path $LogFilePath) -and (Get-Item -Path $LogFilePath).Length -gt 10MB) { Write-LogMessage "Log file is larger than 10MB, trimming the file to 5MB" -LogType Debug # Read content, skip the first half, and write back to the file $content = Get-Content -Path $LogFilePath -Raw $bytesToRemove = (Get-Item -Path $LogFilePath).Length - 5MB if ($bytesToRemove -lt $content.Length) { $newContent = $content.Substring($bytesToRemove) Set-Content -Path $LogFilePath -Value $newContent -Force } } } $tempDir = Join-Path $env:TEMP "JRE-Ninja\policies" $LogFilePath = "$tempDir\MDMPolicies.log" $exePath = $null $script:containsErrors = $false $script:failedPolicies = @() if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null } Write-LogMessage "Using directory: $tempDir" } process { try { Limit-LogFile Install-AzModule $PolicyFolder = Get-PolicyFile # $PolicyFolder = "C:\Users\kzeien\source\repos\windows-policy\definitions" # recursively find all .xml files in the folder (execute all files with 'ingest' in the name first) $PolicyFiles = Get-ChildItem -Path $PolicyFolder -Recurse -Filter "*.xml" | Where-Object { $_.Name -like "*ingest*" -and $_.Name -notlike "*ingest-boilerplate*" } # now add the rest of the xml files $PolicyFiles += Get-ChildItem -Path $PolicyFolder -Recurse -Filter "*.xml" | Where-Object { $_.Name -notlike "*ingest*" } $RegistryFiles = Get-ChildItem -Path $PolicyFolder -Recurse -Filter "*.reg" if (-not $PolicyFiles -and -not $RegistryFiles) { Write-LogMessage "No policy and registry files found in the specified folder." -LogType Error Set-PolicyFailure exit 1 } $exePath = Get-ChildItem -Path $PolicyFolder -Recurse -Filter "*.exe" | Where-Object { $_.Name -like "*SyncMLViewer.Executer*" } if (-not $exePath) { Write-LogMessage "No executable file found in the specified folder." -LogType Error Set-PolicyFailure Write-LogMessage "Policy processing completed." -LogType Debug exit 1 } # Invoke-DeletePolicy foreach ($file in $PolicyFiles) { Write-LogMessage "Processing policy file: $($file.FullName)" -LogType Debug Invoke-XmlProcessing -Path $file.FullName } foreach ($file in $RegistryFiles) { Write-LogMessage "Processing registry file: $($file.FullName)" -LogType Debug Invoke-RegProcessing -Path $file.FullName } if ($script:containsErrors) { Set-PolicyFailure Write-LogMessage "$($script:failedPolicies.Count) failed policies:" -LogType Error Write-LogMessage "Failed policies:`n$($script:failedPolicies -join "`n")" -LogType Error Write-LogMessage "Policy processing completed." -LogType Debug exit 1 } Write-LogMessage "Policy processing completed." -LogType Debug exit 0 } catch { Write-LogMessage "Failed to execute script: $_" -LogType Error Write-LogMessage "Error Line: $($_.InvocationInfo.ScriptLineNumber)" -LogType Error Write-LogMessage "Error Position: $($_.InvocationInfo.OffsetInLine)" -LogType Error Set-PolicyFailure Write-LogMessage "Policy processing completed." -LogType Debug exit 1 } } end { } |