Files
2025-05-14 13:25:48 -04:00

613 lines
23 KiB
PowerShell

<#
.Description
Script to remove SCCM agent from PCs
Completly based on James Chambers and Chad Simmons powershell scripts to remove the SCCM agent.
Updated with other scripts and testing.
$ccmpath is path to SCCM Agent's own uninstall routine.
.Notes
Script created or based on the following:
Source: https://github.com/robertomoir/remove-sccm/blob/master/remove-sccmagent.ps1
Source: https://www.optimizationcore.com/deployment/sccm-client-complete-remove-uninstall-powershell-script/
Source: https://jamesachambers.com/remove-microsoft-sccm-by-force/
Source: https://github.com/ChadSimmons/Scripts/blob/default/ConfigMgr/Troubleshooting/Remove-ConfigMgrClient.ps1
#>
#region Functions
function Test-IsAdmin {
<#
.SYNOPSIS
Checks if the current user has administrative privileges.
.DESCRIPTION
Function determines whether the current user has administrative privileges by attempting to create a new WindowsPrincipal object and checking the IsInRole method for the "Administrator" role.
If the check fails, it throws an exception indicating the lack of administrative privileges.
.EXAMPLE
Test-IsAdmin
If the current user has administrative privileges, the function completes without any output. If not, it throws an exception.
.NOTES
This function should be called at the beginning of scripts that require administrative privileges to ensure proper execution.
#>
try {
# Create a new WindowsPrincipal object for the current user
$currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
# Check if the current user is in the "Administrators" role
if (-not $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "Script needs to run with Administrative privileges."
}
} catch {
throw "Must be run with Administrative priviliges."
}
}
function Stop-WinService {
<#
.SYNOPSIS
Stops a specified Windows service if it exists and is running.
.DESCRIPTION
Function checks if a specified Windows service exists and retrieves its status. If the service is running,
it attempts to stop it. Includes error handling to catch and throw any issues encountered, with specific messages
for services that do not exist.
.PARAMETER ServiceName
The name of the Windows service to stop.
.EXAMPLE
Stop-WinService -ServiceName "wuauserv"
Attempts to stop the Windows Update service if it exists and is running.
.NOTES
This function requires administrative privileges to stop Windows services.
#>
param (
[Parameter(Mandatory = $true)]
[string]$ServiceName
)
try {
# Check if the service exists
$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($null -eq $service) {
throw "Service '$ServiceName' does not exist."
}
# Check if the service is running
if ($service.Status -eq 'Running') {
# Attempt to stop the service
Write-Host "Stopping service '$ServiceName'..."
Stop-Service -Name $ServiceName -Force -ErrorAction Stop
Write-Host "Service '$ServiceName' stopped successfully."
} else {
Write-Host "Service '$ServiceName' is not running."
}
} catch {
throw "$_"
}
}
function Remove-RegKey {
<#
.SYNOPSIS
Deletes a specified registry key and its subkeys.
.DESCRIPTION
This function removes a specified registry key from the Windows Registry, including all its subkeys and values.
It includes error handling to catch and throw any issues encountered during the operation.
.PARAMETER RegKeyPath
The path of the registry key to delete.
.EXAMPLE
Remove-RegKey -RegKeyPath "HKLM:\SOFTWARE\MyApp"
Deletes the "MyApp" key and all its subkeys and values from the HKEY_LOCAL_MACHINE\SOFTWARE path.
.NOTES
This function requires administrative privileges to modify the Windows Registry.
#>
param (
[Parameter(Mandatory = $true)]
[string]$RegKeyPath
)
try {
# Check if the registry key exists
if (Test-Path -Path $RegKeyPath) {
# Attempt to remove the registry key
Write-Host "Removing registry key '$RegKeyPath'..."
Remove-Item -Path $RegKeyPath -Recurse -Force -Confirm:$false -ErrorAction Stop
Write-Host "Registry key '$RegKeyPath' removed successfully."
} else {
Write-Host "Registry key '$RegKeyPath' does not exist."
}
} catch {
throw "Error removing registry key '$RegKeyPath'"
}
}
function Clear-Files {
<#
.SYNOPSIS
Deletes specified files or folders, including subdirectories, and takes ownership if necessary.
.DESCRIPTION
This function iterates through an array of file paths, taking ownership of each file or directory and then deleting it.
It ensures both files and subdirectories are removed, handling any errors encountered during the process.
.PARAMETER FilePaths
An array of file paths to delete. These can be files or directories.
.EXAMPLE
$filesToDelete = @("C:\Temp\File1.txt", "C:\Temp\Folder1")
Clear-Files -FilePaths $filesToDelete
.NOTES
This function requires administrative privileges to take ownership and delete files or directories.
#>
param (
[string[]]$FilePaths
)
foreach ($FilePath in $FilePaths) {
try {
# Take ownership of the file or folder
$null = takeown.exe /F "$FilePath" /R /A /D Y 2>&1
# Delete the file or folder, including subdirectories
Remove-Item -Path $FilePath -Force -Recurse -ErrorAction Stop
Write-Host "Successfully deleted: $FilePath"
} catch {
Write-Host "Error deleting $($FilePath)"
}
}
}
function Remove-WmiNamespace {
<#
.SYNOPSIS
Removes a specified WMI namespace.
.DESCRIPTION
This function checks if a specified WMI namespace exists and removes it if found. It uses CIM (Common Information Model) cmdlets
to query and delete the WMI namespace. Errors are handled silently to ensure smooth execution.
.PARAMETER WmiName
The name of the WMI namespace to be removed.
.PARAMETER WmiNameSpace
The parent namespace where the specified WMI namespace resides.
.EXAMPLE
Remove-WmiNamespace -WmiName "ccm" -WmiNameSpace "root\ccm"
.NOTES
Ensure the script runs with administrative privileges to modify WMI namespaces.
.SOURCE
References:
- https://learn.microsoft.com/en-us/powershell/scripting/overview?view=powershell-7.1
- https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-powershell-cim-cmdlets?view=powershell-7.1
#>
param (
[string]$WmiName,
[string]$WmiNameSpace
)
try {
# Query for the specified WMI namespace
$WmiRepository = Get-CimInstance -query "SELECT * FROM __Namespace WHERE Name='$WmiName'" -Namespace "$WmiNameSpace" -ErrorAction SilentlyContinue
# Check if the namespace exists
if ($null -ne $WmiRepository) {
Write-Host "Found WMI Repository $WmiName, removing..."
# Remove the WMI namespace
Get-CimInstance -query "SELECT * FROM __Namespace WHERE Name='$WmiName'" -Namespace "$WmiNameSpace" | Remove-CimInstance -Confirm:$false -ErrorAction SilentlyContinue
}
else {
Write-Host "WMI Repository $WmiName not found"
}
}
catch {
throw "Error udpating WMI namespace."
}
}
function Verify-SccmClientDelete {
<#
.SYNOPSIS
Verifies the deletion of the SCCM client by checking for the absence of specific services and files.
.DESCRIPTION
Checks if the SCCM (System Center Configuration Manager) client has been successfully deleted from the system.
It does this by verifying the absence of the SCCM client service (`CcmExec`) and the SCCM setup file (`ccmsetup.exe`).
If neither the service nor the setup file is found, the deletion is considered successful.
If either the service or the setup file still exists, appropriate warnings are issued, and the function sets an exit code indicating failure.
.PARAMETER None
.EXAMPLE
$exitCode = Verify-SccmClientDelete
Write-Host "Exit Code: $exitCode"
.NOTES
This function requires administrative privileges to check the existence of services and files.
Ensure that the script is run with appropriate permissions to avoid errors.
#>
# Variables to store the SCCM service name and file path
$SccmService = "CcmExec"
$SccmFilePath = "$Env:WinDir\ccmsetup\ccmsetup.exe"
$ExitCode = 0
try {
# Attempt to retrieve the SCCM service
$CCMexecService = Get-Service -Name $SccmService -ErrorAction SilentlyContinue
# Attempt to retrieve the SCCM setup file
$CCMexecSetupFile = Get-Item -Path $SccmFilePath -ErrorAction SilentlyContinue
# Check if both the service and the setup file do not exist
if (($null -eq $CCMexecService) -and ($null -eq $CCMexecSetupFile)) {
# SCCM Client deletion confirmed.
Write-Host "Confirmation. SCCM client service does not exist!"
}
else {
# Check if the SCCM service still exists
if ($null -ne $CCMexecService) {
# Set exit code for existing service
$ExitCode = 90 # 0x431 ERROR_SERVICE_EXISTS / The specified service already exists.
Write-Warning "Service $CCMexecService still exists, completing with failure $ExitCode"
}
# Check if the SCCM setup file still exists
if ($null -ne $CCMexecSetupFile) {
# Set exit code for existing file
$ExitCode = 91 # The specified file still exists.
Write-Warning "File $CCMexecSetupFile still exists, completing with failure $ExitCode"
}
}
}
catch {
# Handle any errors that occur during the check
throw "Error verifying SCCM client deletion."
}
# Return the exit code
return $ExitCode
}
function Start-CompleteIntuneSync {
<#
.SYNOPSIS
Initiates an Intune sync session and verifies its completion through Event Viewer logs.
.DESCRIPTION
This function performs an Intune sync by creating and starting an MDM session using Windows.Management.MdmSessionManager.
It waits for 60 seconds to allow the sync process to initiate. It then checks for specific events in the Event Viewer
to confirm the sync's start and completion: Looks for events 208 and 209 in the "Applications and Services Logs > Microsoft > Windows > DeviceManagement-Enterprise-Diagnostics-Provider > Admin".
The function returns the time these events were logged, or "Not found" if the events are not present.
The Journey:
Initial approach used `intunemanagementextension://syncapp` protocol as suggested by Jannik Reinhard's blog (https://jannikreinhard.com/2022/07/31/summary-of-the-intune-management-extension/). However, this method did not yield consistent results across different devices
Focus then shifted to leveraging the `Windows.Management.MdmSessionManager` class, known for managing Mobile Device Management (MDM) sessions. The use of `[Windows.Management.MdmSessionManager,Windows.Management,ContentType=WindowsRuntime]` to create and start an MDM session was adopted based on documentation and community blogs:
- https://oofhours.com/2024/03/30/when-does-a-windows-client-sync-with-intune/
Note: There was an initial attempt to use `Add-Type -AssemblyName "Windows.Management"` which resulted in an error indicating the assembly could not be found. This led to the realization that direct referencing and instantiation of the Windows Runtime type was necessary.
.REFERENCES
- "Intune Management Extension" by Jannik Reinhard: https://jannikreinhard.com/2022/07/31/summary-of-the-intune-management-extension/
- "When Does a Windows Client Sync with Intune?" by Michael Niehaus: https://oofhours.com/2024/03/30/when-does-a-windows-client-sync-with-intune/
.PARAMETER None
.EXAMPLE
Start-CompleteIntuneSync
.NOTES
This function requires administrative privileges to access Event Viewer logs.
Make sure to run this script with appropriate permissions.
#>
# Initialize variables for event checking
$eventLog = "Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin"
$syncStartEventID = 208
$syncCompleteEventID = 209
$syncStartTime = Get-Date
# Log the start of the sync attempt
Write-Host "Starting Intune sync at $syncStartTime"
try {
# Create and start the MDM session using Windows.Management.MdmSessionManager
[Windows.Management.MdmSessionManager,Windows.Management,ContentType=WindowsRuntime] > $null
$session = [Windows.Management.MdmSessionManager]::TryCreateSession()
$session.StartAsync() | Out-Null
# Wait for 60 seconds to allow the sync to initiate
Start-Sleep -Seconds 60
# Check for the sync start event in Event Viewer
$syncStartEvent = Get-WinEvent -LogName $eventLog | Where-Object { $_.Id -eq $syncStartEventID -and $_.TimeCreated -ge $syncStartTime }
if ($syncStartEvent) {
Write-Host "Sync start event (ID $syncStartEventID) found."
$syncStartEventTime = $syncStartEvent.TimeCreated
} else {
Write-Host "Sync start event (ID $syncStartEventID) not found."
$syncStartEventTime = "Not found"
}
# Check for the sync complete event in Event Viewer
$syncCompleteEvent = Get-WinEvent -LogName $eventLog | Where-Object { $_.Id -eq $syncCompleteEventID -and $_.TimeCreated -ge $syncStartTime }
if ($syncCompleteEvent) {
Write-Host "Sync complete event (ID $syncCompleteEventID) found."
$syncCompleteEventTime = $syncCompleteEvent.TimeCreated
} else {
Write-Host "Sync complete event (ID $syncCompleteEventID) not found."
$syncCompleteEventTime = "Not found"
}
# Return details of the sync process
return @{
SyncStartEvent = $syncStartEventTime
SyncCompleteEvent = $syncCompleteEventTime
SyncStartTime = $syncStartTime
}
} catch {
throw "Error during Intune sync process. "
}
}
function WriteAndExitWithSummary {
<#
.SYNOPSIS
Writes a summary of the script's execution to the console and then exits the script with a specified status code.
.DESCRIPTION
The function takes a status code and a summary string as parameters. It writes the summary along with the current date and time to the console using Write-Host.
After writing the summary, it exits the script with the given status code. If the given status code is below 0 (negative) it changes exit status code to 0.
.PARAMETER StatusCode
The exit status code to be used when exiting the script.
0: OK
1: FAIL
Other: WARNING
.PARAMETER Summary
The summary string that describes the script's execution status. This will be written to the console.
.EXAMPLE
WriteAndExitWithSummary -StatusCode 0 -Summary "All operations completed successfully."
.EXAMPLE
WriteAndExitWithSummary -StatusCode 1 -Summary "Error: SCCM client removal failed."
.NOTES
Last Modified: August 27, 2023
Author: Manuel Nieto
#>
param (
[int]$StatusCode,
[string]$Summary
)
# Combine the summary with the current date and time.
$finalSummary = "$([datetime]::Now) = $Summary"
# Determine the prefix based on the status code.
$prefix = switch ($StatusCode) {
0 { "OK" }
1 { "FAIL" }
default { "WARNING" }
}
# Easier to read in log file
Write-Host "`n`n"
# Write the final summary to the console.
Write-Host "$prefix $finalSummary"
# Easier to read in log file
Write-Host "`n`n"
# Exit the script with the given status code.
if ($StatusCode -lt 0) {$StatusCode = 0}
Exit $StatusCode
}
#endregion
#region Main
# Initialize
$Error.Clear() # Clear any previous errors.
$t = Get-Date # Get current date and time.
$CCMpath = "$Env:WinDir\ccmsetup\ccmsetup.exe" # Path to SCCM setup executable.
$verifyBeginResult # Variable to store beginning SCCM verification result.
$verifyEndResult # Variable to store ending SCCM verification result.
$summary = "" # Initialize summary string.
$StatusCode = 0 # Initialize status code to zero.
# New lines, easier to read Agentexecutor Log file.
Write-Host "`n`n"
#Log start time.
Write-Host "SCCM Agent cleanup start time: $t"
try {
#Test Admin rights
Test-IsAdmin
# Confirm if SCCM client is present.
$verifyBeginResult = Verify-SccmClientDelete
# Only execute if we have confirmation that SCCM client exists.
if ($verifyBeginResult -gt 0) {
# Stopping SCCM services.
try {
#Stop SCCM services.
Stop-WinService CcmExec
Stop-WinService ccmsetup
Stop-WinService smstsmgr
Stop-WinService CmRcService
$summary += "SCCM services stopped. "
} catch {
$summary += "Error stopping SCCM services: $_ "
$StatusCode = -2
}
# Remove SCCM client.
try {
# Remove SCCM client.
if (Test-Path $CCMpath) {
Write-Host "Found $CCMpath, Uninstalling SCCM agent. `n"
#Start Uninstall, Included -WorkingDirectory to Start-Process cmdlet as Workaround to error when working directory has characters "[" "]"
Start-Process -WorkingDirectory $Env:WinDir -FilePath $CCMpath -ArgumentList "/uninstall" -Wait -NoNewWindow
# wait for exit
$CCMProcess = Get-Process ccmsetup -ErrorAction SilentlyContinue
try {
$CCMProcess.WaitForExit()
} catch {}
$summary += "SCCM client removed. "
}
else {
$summary += "SCCM client not found. "
}
} catch {
$summary += "Error removing SCCM client. "
$StatusCode = -2
}
# Removing services from registry
try {
# Remove Services from Registry.
$CurrentPath = "HKLM:\SYSTEM\CurrentControlSet\Services"
Remove-RegKey "$CurrentPath\CcmExec"
Remove-RegKey "$CurrentPath\CCMSetup"
Remove-RegKey "$CurrentPath\smstsmgr"
Remove-RegKey "$CurrentPath\CmRcService"
$summary += "SCCM services removed from registry. "
} catch {
$summary += "Error removing SCCM services from registry: $_. "
$StatusCode = -2
}
try {
# Remove SCCM Client from Registry
$CurrentPath = "HKLM:\SOFTWARE\Microsoft"
Remove-RegKey "$CurrentPath\CCM"
Remove-RegKey "$CurrentPath\CCMSetup"
Remove-RegKey "$CurrentPath\SMS"
$CurrentPath = "HKLM:\SOFTWARE\Wow6432Node\Microsoft"
Remove-RegKey "$CurrentPath\CCM"
Remove-RegKey "$CurrentPath\CCMSetup"
Remove-RegKey "$CurrentPath\SMS"
$summary += "SCCM client registry keys removed. "
} catch {
$summary += "Error removing SCCM client registry keys: $_. "
$StatusCode = -2
}
try {
# Remove WMI Namespaces
Remove-WmiNamespace "ccm" "root"
Remove-WmiNamespace "sms" "root\cimv2"
$summary += "SCCM WMI namespaces removed. "
} catch {
$summary += "Error removing SCCM WMI namespaces: $_. "
$StatusCode = -2
}
try {
# Reset MDM Authority
Write-Host "MDM Authority, reviewing and deleting registry key if necessary"
$CurrentPath = "HKLM:\SOFTWARE\Microsoft"
Remove-RegKey "$CurrentPath\DeviceManageabilityCSP"
$summary += "MDM authority reset. "
} catch {
$summary += "Error resetting MDM authority. "
$StatusCode = -2
}
try {
# Remove Folders and Files
$CurrentPath = "$Env:WinDir"
Clear-Files "$CurrentPath\CCM"
Clear-Files "$CurrentPath\ccmsetup"
Clear-Files "$CurrentPath\ccmcache"
Clear-Files "$CurrentPath\SMSCFG.ini"
Clear-Files "$CurrentPath\SMS*.mif"
$summary += "SCCM related files and folders removed. "
} catch {
$summary += "Error removing SCCM files and folders: $_. "
$StatusCode = -2
}
try {
# Remove SCCM certificates
$CurrentPath = "HKLM:\SOFTWARE\Microsoft\SystemCertificates\SMS\Certificates"
Remove-RegKey "$CurrentPath\*"
$summary += "SCCM certificates removed. "
} catch {
$summary += "Error removing SCCM certificates: $_. "
$StatusCode = -2
}
try {
# Confirm if SCCM client was removed.
$verifyEndResult = Verify-SccmClientDelete
if ($verifyEndResult -eq 0) {
$summary += "SCCM client removal verified. "
} else {
$StatusCode = $verifyEndResult
$summary += "SCCM client removal failed with code $verifyEndResult. "
}
} catch {
$summary += "Error verifying SCCM client removal: $_. "
$StatusCode = -2
}
}
}
catch {
# Log error and set status code to failure
$summary += "Execution Error: $_ "
$StatusCode = 1
}
# Perform Intune sync and log the result. Only if no errors.
if ($StatusCode -le 0) {
try {
$syncDetails = Start-CompleteIntuneSync
$summary += "Intune sync request: $($syncDetails.SyncStartTime), Start: $($syncDetails.SyncStartEvent), Completed: $($syncDetails.SyncCompleteEvent). "
} catch {
$summary += "Error during Intune sync. "
}
}
# Write the summary and exit with the appropriate status code
WriteAndExitWithSummary -StatusCode $StatusCode -Summary $summary
#Finished!
#endregion