<#

.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
