Private/Get-DeploymentTypeInfo.ps1

<#
.Synopsis
Created on: 28/10/2023
Update on: 23/03/2024
Created by: Ben Whitmore
Filename: Get-DeploymentTypeInfo.ps1
 
.Description
Function to get deployment type information from ConfigMgr
 
.PARAMETER LogId
The component (script name) passed as LogID to the 'Write-Log' function.
This parameter is built from the line number of the call from the function up the
 
.PARAMETER ApplicationId
The CI_ID of the application to get deployment type information for
#>

function Get-DeploymentTypeInfo {
    param (
        [Parameter(Mandatory = $false, ValuefromPipeline = $false, HelpMessage = "The component (script name) passed as LogID to the 'Write-Log' function")]
        [string]$LogId = $($MyInvocation.MyCommand).Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $false, Position = 0, HelpMessage = 'The id of the application(s) to get information for')]
        [string]$ApplicationId
    )
    begin {

        # Create an empty array to store the deployment type information
        $deploymentTypes = @()

        # Characters that are not allowed in Windows folder names
        $invalidChars = '[<>:"/\\|\?\*]'

        # Get UTF-8 encoding without BOM
        $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $false 
        
    }
    process {

        try {

            # Grab the SDMPackgeXML which contains the application and deployment type details
            Write-Log -Message ("Invoking Get-CMApplication where Id equals '{0}'" -f $ApplicationId) -LogId $LogId
            Write-Host ("Invoking Get-CMApplication where Id equals '{0}'" -f $ApplicationId) -ForegroundColor Cyan
            $xmlPackage = Get-CMApplication -Id $ApplicationId | Where-Object { $null -ne $_.SDMPackageXML } | Select-Object -ExpandProperty SDMPackageXML
        
            # Prepare xml from SDMPackageXML
            $xmlContent = [xml]($xmlPackage)

            # Get the total number of deployment types for the application
            $totalDeploymentTypes = ($xmlContent.AppMgmtDigest.Application.DeploymentTypes.DeploymentType | Measure-Object | Select-Object -ExpandProperty Count)
            Write-Log -Message ("The total number of deployment types for '{0}' is '{1}')" -f $xmlContent.AppMgmtDigest.Application.title.'#text', $totalDeploymentTypes) -LogId $LogId
            Write-Host ("The total number of deployment types for '{0}' is '{1}')" -f $xmlContent.AppMgmtDigest.Application.title.'#text', $totalDeploymentTypes) -ForegroundColor Cyan

            if ($totalDeploymentTypes -ge 0) {

                # If there are deployment types, iterate through each deployment type and collect the details
                foreach ($object in $xmlContent.AppMgmtDigest.DeploymentType) {

                    # Handle multiple objects if content is an array
                    if ($object.Installer.Contents.Content.Location.Count -gt 1) {
                        $installLocation = $object.Installer.Contents.Content.Location[0]
                        $uninstallLocation = $object.Installer.Contents.Content.Location[1]
                    }
                    else {
                        $installLocation = $object.Installer.Contents.Content.Location
                        $uninstallLocation = $object.Installer.Contents.Content.Location
                    }

                    # Sanitize the folder names
                    $applicationNameSanitized = ($xmlContent.AppMgmtDigest.Application.title.'#text' -replace $invalidChars, '_').TrimEnd('.', ' ')
                    $deploymentTypeNameSanitized = ($object.Title.InnerText -replace $invalidChars, '_').TrimEnd('.', ' ')

                    # Detection Methods child folder path
                    $detectionMethodsFolderPath = ("{0}\{1}" -f $applicationNameSanitized, $deploymentTypeNameSanitized)

                    # Build final folder name strings
                    $detectionMethodsFolder = Join-Path -Path "$workingFolder_Root\DetectionMethods" -ChildPath $detectionMethodsFolderPath

                    # Create the Detection Methods folder
                    New-FolderToCreate -Root "$workingFolder_Root\DetectionMethods" -FolderNames $detectionMethodsFolderPath

                    # Remove existing files from the Detection Methods folder to avoid ambiguity on import
                    $existingFiles = [System.IO.Directory]::GetFiles($detectionMethodsFolder)
                    
                    foreach ($file in $existingFiles) {
                        Write-Log -Message ("Removing existing file '{0}'" -f $file) -LogId $LogId -Severity 2
                        [System.IO.File]::Delete($file) 
                    }

                    # Create a new custom hashtable to store Deployment type details
                    $deploymentObject = [PSCustomObject]@{}

                    # Add deployment type details to the PSCustomObject
                    $deploymentObject | Add-Member NoteProperty -Name Application_Id -Value $ApplicationId
                    $deploymentObject | Add-Member NoteProperty -Name ApplicationName -Value $xmlContent.AppMgmtDigest.Application.title.'#text'
                    $deploymentObject | Add-Member NoteProperty -Name Application_LogicalName -Value $xmlContent.AppMgmtDigest.Application.LogicalName
                    $deploymentObject | Add-Member NoteProperty -Name LogicalName -Value $object.LogicalName
                    $deploymentObject | Add-Member NoteProperty -Name Name -Value $object.Title.InnerText
                    $deploymentObject | Add-Member NoteProperty -Name Technology -Value $object.Installer.Technology
                    $deploymentObject | Add-Member NoteProperty -Name ExecutionContext -Value $object.Installer.ExecutionContext
                    $deploymentObject | Add-Member NoteProperty -Name InstallContent -Value $installLocation.TrimEnd('\') 
                    $deploymentObject | Add-Member NoteProperty -Name InstallCommandLine -Value $object.Installer.CustomData.InstallCommandLine
                    $deploymentObject | Add-Member NoteProperty -Name UnInstallSetting -Value $object.Installer.CustomData.UnInstallSetting
                    $deploymentObject | Add-Member NoteProperty -Name UninstallContent -Value $uninstallLocation.TrimEnd('\') 
                    $deploymentObject | Add-Member NoteProperty -Name UninstallCommandLine -Value $object.Installer.CustomData.UninstallCommandLine
                    $deploymentObject | Add-Member NoteProperty -Name ExecuteTime -Value $object.Installer.CustomData.ExecuteTime
                    $deploymentObject | Add-Member NoteProperty -Name MaxExecuteTime -Value $object.Installer.CustomData.MaxExecuteTime
                    $deploymentObject | Add-Member NoteProperty -Name DetectionProvider -Value $object.Installer.DetectAction.Provider

                    # Switch on the detection method and save to file
                    Switch ($object.Installer.DetectAction.Provider) {

                        # If Detection Method is a 'Script'
                        'Script' {
                            $detectionTypeScriptBody = $object.Installer.DetectAction.Args.Arg.Where({ $_.Name -eq 'ScriptBody' }).InnerText
                            $detectionTypeScriptRunAs32Bit = $object.Installer.DetectAction.Args.Arg.Where({ $_.Name -eq 'RunAs32Bit' }).InnerText
                            $detectionTypeExecutionContext = $object.Installer.DetectAction.Args.Arg.Where({ $_.Name -eq 'ExecutionContext' }).InnerText
                            $detectionTypeScriptType = $object.Installer.DetectAction.Args.Arg.Where({ $_.Name -eq 'ScriptType' }).InnerText
        
                            # ScriptType
                            # 0 = PowerShell
                            # 1 = VBScript
                            # 2 = JavaScript
        
                            Switch ($detectionTypeScriptType) {
                                '0' {
                                    $detectionTypeScriptFileExtension = '.ps1'
                                }
                                '1' {
                                    $detectionTypeScriptFileExtension = '.vbs'
                                }
                                '2' {
                                    $detectionTypeScriptFileExtension = '.js'
                                }
                            }
        
                            # Extract only the encoded base64 script from the script body
                            $pattern = '# ENCODEDSCRIPT # Begin Configuration Manager encoded script block #\s*(.*?)\s*# ENCODEDSCRIPT# End Configuration Manager encoded script block'
                            $matchSigned = [regex]::Match($detectionTypeScriptBody, $pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
        
                            if ($matchSigned.Success) {
                                $extractedContent = $matchSigned.Groups[1].Value

                                # Decode the Base64 string
                                #$Encoding = [System.Text.Encoding]::GetEncoding('windows-1252')
                                #$scriptContent = $Encoding.GetString([System.Convert]::FromBase64String($extractedContent))
                                $scriptContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($extractedContent))

                                # We need to trim the script to honor digital signatures
                                # Split the original string into lines
                                $lines = $scriptContent -split "`n"

                                # Remove the last line because the xml adds padding before the encoded string
                                $lines = $lines[0..($lines.Count - 2)]

                                # Join the lines back into a single string
                                $finalScriptContent = $lines -join "`n"
                            }
                            else {
                                
                                # No need to deal with encoding, just pass the script body to the final object for saving
                                $finalScriptContent = $detectionTypeScriptBody
                            }
  
                            # Write the detection method to a file
                            $detectionMethodFile = Join-Path -Path $detectionMethodsFolder -ChildPath "DetectionScript$detectionTypeScriptFileExtension"
   
                            try {

                                # Specifying Out-File with Encoding UTF8 is not honored so use .NET method instead to write to file
                                [System.IO.File]::WriteAllText($detectionMethodFile, $finalScriptContent, $utf8NoBomEncoding)
                                Write-Log -Message ("Detection method script saved to file '{0}'" -f $detectionMethodFile) -LogId $LogId
                                Write-Host ("Detection method script saved to file '{0}'" -f $detectionMethodFile) -ForegroundColor Green
                            }
                            catch {
                                Write-Log -Message ("Could not write detection method to file '{0}'" -f $detectionMethodFile) -LogId $LogId -Severity 3
                                Write-Host ("Could not write detection method to file '{0}'" -f $detectionMethodFile) -ForegroundColor Red
                            }
                        }

                        # If Detection Method is File/Reg/MSICode
                        'Local' {
                            $detectionTypeExecutionContext = $object.Installer.DetectAction.Args.Arg.Where({ $_.Name -eq 'ExecutionContext' }).InnerText
                            $detectionTypeMethodBody = $object.Installer.DetectAction.Args.Arg.Where({ $_.Name -eq 'MethodBody' }).InnerText
                            
                            # Define the detection method Xml file path
                            $detectionMethodXmlFile = Join-Path -Path $detectionMethodsFolder -ChildPath 'MethodBody.xml'
                        
                            # Write the detection method to an Xml file

                            try {
                                # Specifying Out-File with Encoding UTF8 is not honored so use .NET method instead
                                [System.IO.File]::WriteAllText($detectionMethodXmlFile, $detectionTypeMethodBody, $utf8NoBomEncoding)
                                Write-Log -Message ("Detection method XML saved to file '{0}'" -f $detectionMethodXmlFile) -LogId $LogId
                                Write-Host ("`Detection method XML saved to file '{0}'" -f $detectionMethodXmlFile) -ForegroundColor Cyan
                            }
                            catch {
                                Write-Log -Message ("Could not write detection method to file '{0}'" -f $detectionMethodXmlFile) -LogId $LogId -Severity 3
                                Write-Host ("Could not write detection method to file '{0}'" -f $detectionMethodXmlFile) -ForegroundColor Red
                            }

                            # Attempt to extract the local detection methods from the XML. We will ignore any 'or' operators as these are not supported in Intune
                            if (Test-Path -Path $detectionMethodXmlFile) {
                                $localDetectionMethods = Get-DetectionMethod -LogId $LogId -XMLObject $detectionTypeMethodBody
                                
                                if ($localDetectionMethods.Count -gt 0) {

                                    Write-Log -Message 'Local Detection Methods extracted from XML' -LogId $LogId
                                    Write-Host "`nLocal Detection Methods extracted from XML" -ForegroundColor Cyan

                                    foreach ($method in $localDetectionMethods) {
                                        Write-Log -Message ("{0}" -f $method) -LogId $LogId
                                        Write-Host ("{0}" -f $method) -ForegroundColor Green
                                    }
                                    
                                    # Export the detection method to a Json file
                                    # Define the detection method Json file path
                                    $detectionMethodJsonFile = Join-Path -Path $detectionMethodsFolder -ChildPath 'DetectionMethod.json'

                                    # Create a detection method json file
                                    $detectionJson = New-IntuneDetectionMethod -LocalSettings $localDetectionMethods

                                    try {
                                        # Specifying Out-File with Encoding UTF8 is not honored so use .NET method instead
                                        [System.IO.File]::WriteAllText($detectionMethodJsonFile, $detectionJson, $utf8NoBomEncoding)
                                        Write-Log -Message ("Intune detection method Json saved to file '{0}'" -f $detectionMethodJsonFile) -LogId $LogId
                                        Write-Host ("`Intune detection method Json saved to file '{0}'" -f $detectionMethodJsonFile) -ForegroundColor Cyan
                                    }
                                    catch {
                                        Write-Log -Message ("Could not create Intune detection method Json file '{0}'" -f $detectionMethodJsonFile) -LogId $LogId -Severity 3
                                        Write-Host ("Could not create Intune detection method Json file '{0}'" -f $detectionMethodJsonFile) -ForegroundColor Red
                                    }
                                }
                                else {
                                    Write-Log -Message ("There was an error getting the local detection methods for deployment type '{0}' from the XML" -f $detectionMethodXmlFile) -LogId $LogId -Severity 3
                                    Write-Host ("There was an error getting the local detection methods for deployment type '{0}' from the XML" -f $detectionMethodXmlFile) -ForegroundColor Red
                                }
                            }
                            else {
                                Write-Log -Message ("There was an error getting the local detection methods for deployment type '{0}' from the XML" -f $detectionMethodXmlFile) -LogId $LogId -Severity 3
                                Write-Host ("There was an error getting the local detection methods for deployment type '{0}' from the XML" -f $detectionMethodXmlFile) -ForegroundColor Red
                            }
                        }
                    }
                    
                    # Add detection method details to the PSCustomObject
                    $deploymentObject | Add-Member NoteProperty -Name DetectionTypeScriptRunAs32Bit -Value $detectionTypeScriptRunAs32Bit
                    $deploymentObject | Add-Member NoteProperty -Name DetectionTypeScriptType -Value $detectionTypeScriptType
                    $deploymentObject | Add-Member NoteProperty -Name DetectionTypeExecutionContext -Value $detectionTypeExecutionContext
                    $deploymentObject | Add-Member NoteProperty -Name DetectionMethodXmlFile -Value $detectionMethodXmlFile
                    $deploymentObject | Add-Member NoteProperty -Name DetectionMethodJsonFile -Value $detectionMethodJsonFile

                    Write-Log -Message ("Application_Id = '{0}', Application_Name = '{1}', Application_LogicalName = '{2}', LogicalName = '{3}', Name = '{4}', `
                    Technology = '{5}', ExecutionContext = '{6}', InstallContext = '{7}', InstallCommandLine = '{8}', UninstallSetting = '{9}', UninstallContent = '{10}', `
                    UninstallCommandLine = '{11}', ExecuteTime = '{12}', MaxExecuteTime = '{13}', DetectionProvider = '{14}', DetectionTypeScriptRunAs32Bit = '{15}', `
                    DetectionTypeScriptType = '{16}', DetectionTypeExecutionContext = '{17}', DetectionMethodXmlFile = '{18}', DetectionMethodJsonFile = '{19}'"
 -f `
                            $ApplicationId, `
                            $xmlContent.AppMgmtDigest.Application.title.'#text', `
                            $xmlContent.AppMgmtDigest.Application.LogicalName, `
                            $object.LogicalName, `
                            $object.Title.InnerText, `
                            $object.Installer.Technology, `
                            $object.Installer.ExecutionContext, `
                            $installLocation, `
                            $object.Installer.CustomData.InstallCommandLine, `
                            $object.Installer.CustomData.UnInstallSetting, `
                            $uninstallLocation, `
                            $object.Installer.CustomData.UninstallCommandLine, `
                            $object.Installer.CustomData.ExecuteTime, `
                            $object.Installer.CustomData.MaxExecuteTime, `
                            $object.Installer.DetectAction.Provider, `
                            $detectionTypeScriptRunAs32Bit, `
                            $detectionTypeScriptType, `
                            $detectionTypeExecutionContext, `
                            $detectionMethodXmlFile, `
                            $detectionMethodJsonFile) -LogId $LogId

                    # Output the deployment type object
                    Write-Host "`nDeplopymentType Details extracted" -ForegroundColor Cyan
                    Write-Host "$deploymentObject`n" -ForegroundColor Green

                    # Add the deployment type object to the array
                    $deploymentTypes += $deploymentObject          
                }
            }
            else {
                Write-Log -Message ("Warning: No DeploymentTypes found for '{0}'" -f $xmlContent.AppMgmtDigest.Application.LogicalName) -LogId $LogId -Severity 2
                Write-Host ("Warning: No DeploymentTypes found for '{0}'" -f $xmlContent.AppMgmtDigest.Application.LogicalName) -ForegroundColor Yellow
            }
        
            return $deploymentTypes

        }
        catch {
            Write-Log -Message ("Could not get deployment type information for application Id '{0}'" -f $ApplicationId) -LogId $LogId -Severity 3
            Write-Warning -Message ("Could not get deployment type information for application id '{0}'" -f $ApplicationId)
            Get-ScriptEnd -LogId $LogId -ErrorMessage $_.Exception.Message
        }
    }
}