// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not // use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing // permissions and limitations under the License. // Package application contains application gatherer. //go:build windows // +build windows package application import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/aws/amazon-ssm-agent/agent/appconfig" "github.com/aws/amazon-ssm-agent/agent/context" "github.com/aws/amazon-ssm-agent/agent/plugins/inventory/model" ) const ( ArgsForDetectingOSArch = `get-wmiobject -class win32_processor | select-object addresswidth` KeywordFor64BitArchitectureReportedByPowershell = "64" KeywordFor32BitArchitectureReportedByPowershell = "32" Architecture64BitReportedByGoRuntime = "amd64" ConvertGuidToCompressedGuidCmd = `function Convert-GuidToCompressedGuid { [CmdletBinding()] [OutputType('System.String')] param ( [Parameter(ValueFromPipeline="", ValueFromPipelineByPropertyName="", Mandatory=$true)] [string]$Guid ) begin { $Guid = $Guid.Replace('-', '').Replace('{', '').Replace('}', '') } process { try { $Groups = @( $Guid.Substring(0, 8).ToCharArray(), $Guid.Substring(8, 4).ToCharArray(), $Guid.Substring(12, 4).ToCharArray(), $Guid.Substring(16, 16).ToCharArray() ) $Groups[0..2] | foreach { [array]::Reverse($_) } $CompressedGuid = ($Groups[0..2] | foreach { $_ -join '' }) -join '' $chararr = $Groups[3] for ($i = 0; $i -lt $chararr.count; $i++) { if (($i % 2) -eq 0) { $CompressedGuid += ($chararr[$i+1] + $chararr[$i]) -join '' } } $CompressedGuid } catch { Write-Error $_.Exception.Message } } } function Clean-Quotes-Backslash { param ([string]$str) if($str.length -ge 2 -and $str.Substring(0,1) -eq '"' -and $str.Substring($str.length - 1) -eq '"'){ $str = $str.Substring(1, $str.length - 2) } $str = $str.Replace('\', '\\') $str = $str.Replace('"', '\"') return $str } ` ArgsToReadRegistryFromProducts = `$products = Get-ItemProperty HKLM:\Software\Classes\Installer\Products\* | Select-Object @{n="PSChildName";e={$_."PSChildName"}} | Select -expand PSChildName ` RegistryPathCurrentVersionUninstall = `HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*` RegistryPathWow6432NodeCurrentVersionUninstall = `HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*` ArgsToReadRegistryApplications = ` [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 Get-ItemProperty %v | Where-Object {($_.DisplayName -ne $null -and $_DisplayName -ne '' -and $_.DisplayName -notmatch '^KB[000000-999999]') -and ($_.SystemComponent -eq $null -or ($_.SystemComponent -ne $null -and $_.SystemComponent -eq '0')) -and ($_.ParentKeyName -eq $null) -and ($_.WindowsInstaller -eq $null -or ($_.WindowsInstaller -eq '0') -or ($_.WindowsInstaller -eq 1 -and $products -contains (Convert-GuidToCompressedGuid $_.PSChildName))) -and ($_.ReleaseType -eq $null -or ($_.ReleaseType -ne $null -and $_.ReleaseType -ne 'Security Update' -and $_.ReleaseType -ne 'Update Rollup' -and $_.ReleaseType -ne 'Hotfix')) } | Select-Object @{n="Name";e={$_."DisplayName"}}, @{n="PackageId";e={$_."PSChildName"}}, @{n="Version";e={$_."DisplayVersion"}}, Publisher, @{n="InstalledTime";e={[datetime]::ParseExact($_."InstallDate","yyyyMMdd",$null).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")}} | %% { [Console]::WriteLine(@" {"Name":"$(Clean-Quotes-Backslash $_.Name)","PackageId":"$($_.PackageId)","Version":"$(Clean-Quotes-Backslash $_.Version)","Publisher":"$(Clean-Quotes-Backslash $_.Publisher)","InstalledTime":"$($_.InstalledTime)"}, "@)} ` ) var PowershellCmd = appconfig.PowerShellPluginCommandName var SysnativePowershellCmd = filepath.Join(os.Getenv("SystemRoot"), "sysnative", "WindowsPowerShell", "v1.0", "powershell.exe") var ArgsToReadRegistryFromWindowsCurrentVersionUninstall = fmt.Sprintf(ArgsToReadRegistryApplications, RegistryPathCurrentVersionUninstall) var ArgsToReadRegistryFromWow6432Node = fmt.Sprintf(ArgsToReadRegistryApplications, RegistryPathWow6432NodeCurrentVersionUninstall) // decoupling exec.Command for easy testability var cmdExecutor = executeCommand func executeCommand(command string, args ...string) ([]byte, error) { return exec.Command(command, args...).CombinedOutput() } // collectPlatformDependentApplicationData collects application data for windows platform func collectPlatformDependentApplicationData(context context.T) []model.ApplicationData { /* Note: We get list of installed apps by using powershell to query registry from 2 locations: Path-1 => HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* Path-2 => HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* Path-2 is used to get a list of 32 bit apps running on a 64 bit OS (when 64bit agent is running on 64bit OS) For all other scenarios we use Path-1 to get the list of installed apps. Reference: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724072(v=vs.85).aspx Powershell command format: Get-ItemProperty | Where-Object {$_.DisplayName -ne $null} | Select-Object @{Name="Name";Expression={$_."DisplayName"}} | ConvertTo-Json We use calculated property of Select-Object to format the data accordingly. Reference: https://technet.microsoft.com/en-us/library/ff730948.aspx For determining the OS architecture we use the following command: get-wmiobject -class win32_processor | select-object addresswidth addresswidth - On a 32-bit operating system, the value is 32 and on a 64-bit operating system it is 64. Reference: https://msdn.microsoft.com/en-us/library/aa394373%28v=vs.85%29.aspx We use following rules to detect applications from Registry. This is done to ensure AWS:Application’s behavior is similar to Add/Remove programs in Windows: Records that meets all rules are added in the result set: 1. ‘DisplayName’ must be present with a valid value, as this is reflected as Display Name in AWS:Application inventory type. Also, its value must not start with ‘KB’ followed by 6 numbers - as that indicates a Windows update. 2. ‘UninstallString’ must be present, because it stores the command line that gets executed by Add/Remove programs, when user tries to uninstall a program. 3. ‘SystemComponent’ must be either absent or present with value set to 0, because this value is usually set on programs that have been installed via a Windows Installer Package (MSI). 4. ‘ParentKeyName’ must NOT be present, because that indicates its an update to the parent program. 5. ‘ReleaseType’ must either be absent or if present must not have value set to ‘Security Update’, ‘Update Rollup’, ‘Hotfix’, because that indicates its an update to an existing program. 6. ‘WindowsInstaller’ must be either absent or present with value 0. If the value is set to 1, then the application is included in the list if and only if the corresponding compressed guid (explained below) is also present in HKLM:\Software\Classes\Installer\Products\ Calculation of compressed guid: Each Guid has 5 parts separated by '-'. For the first three each one will be totally reversed, and for the remaining two each one will be reversed by every other character. Then the final compressed Guid will be constructed by concatinating all the reversed parts without '-'. -Example- Input : 2BE0FA87-5B36-43CF-95C8-C68D6673FB94 Reversed : 78AF0EB2-63B5-FC34-598C-6CD86637BF49 Final Compressed Guid: 78AF0EB263B5FC34598C6CD86637BF49 Reference: https://community.spiceworks.com/how_to/2238-how-add-remove-programs-works */ //it will enable us to run other complicated queries too. var data, apps []model.ApplicationData var cmd string log := context.Log() //detecting process architecture exeArch := runtime.GOARCH log.Infof("Exe architecture as detected by golang runtime - %v", exeArch) //detecting OS architecture osArch := detectOSArch(context, PowershellCmd, ArgsForDetectingOSArch) log.Infof("Detected OS architecture as - %v", osArch) if strings.Contains(osArch, KeywordFor32BitArchitectureReportedByPowershell) { //os architecture is 32 bit if exeArch != Architecture64BitReportedByGoRuntime { //exe architecture is also 32 bit //since both exe & os are 32 bit - we need to detect only 32 bit apps cmd = ConvertGuidToCompressedGuidCmd + ArgsToReadRegistryFromProducts + ArgsToReadRegistryFromWindowsCurrentVersionUninstall apps = executePowershellCommands(context, PowershellCmd, cmd, model.Arch32Bit) data = append(data, apps...) } else { log.Error("Detected an unsupported scenario of 64 bit amazon ssm agent running on 32 bit windows OS - nothing to report") } } else if strings.Contains(osArch, KeywordFor64BitArchitectureReportedByPowershell) { //os architecture is 64 bit if exeArch == Architecture64BitReportedByGoRuntime { //both exe & os architecture is 64 bit //detecting 32 bit apps by querying Wow6432Node path in registry cmd = ConvertGuidToCompressedGuidCmd + ArgsToReadRegistryFromProducts + ArgsToReadRegistryFromWow6432Node apps = executePowershellCommands(context, PowershellCmd, cmd, model.Arch32Bit) data = append(data, apps...) //detecting 64 bit apps by querying normal registry path cmd = ConvertGuidToCompressedGuidCmd + ArgsToReadRegistryFromProducts + ArgsToReadRegistryFromWindowsCurrentVersionUninstall apps = executePowershellCommands(context, PowershellCmd, cmd, model.Arch64Bit) data = append(data, apps...) } else { //exe architecture is 32 bit - all queries to registry path will be redirected to wow6432 so need to use sysnative //reference: https://blogs.msdn.microsoft.com/david.wang/2006/03/27/howto-detect-process-bitness/ //detecting 32 bit apps by querying Wow632 registry node cmd = ConvertGuidToCompressedGuidCmd + ArgsToReadRegistryFromProducts + ArgsToReadRegistryFromWow6432Node apps = executePowershellCommands(context, PowershellCmd, cmd, model.Arch32Bit) data = append(data, apps...) //detecting 64 bit apps by using sysnative for reading registry to avoid path redirection cmd = ConvertGuidToCompressedGuidCmd + ArgsToReadRegistryFromProducts + ArgsToReadRegistryFromWindowsCurrentVersionUninstall apps = executePowershellCommands(context, SysnativePowershellCmd, cmd, model.Arch64Bit) data = append(data, apps...) } } else { log.Error("Can't find application data because unable to detect OS architecture - nothing to report") } return data } // detectOSArch detects OS architecture; decouple for unit test var detectOSArch = detectOSArchFun func detectOSArchFun(context context.T, command, args string) (osArch string) { var output []byte var err error log := context.Log() log.Infof("Getting OS architecture") log.Infof("Executing command: %v %v", command, args) if output, err = cmdExecutor(command, args); err != nil { log.Debugf("Failed to execute command : %v %v with error - %v", command, args, err.Error()) log.Debugf("Command Stderr: %v", string(output)) err = fmt.Errorf("Command failed with error: %v", string(output)) log.Error(err.Error()) log.Infof("Unable to detect OS architecture") } else { cmdOutput := string(output) log.Debugf("Command output: %v", cmdOutput) osArch = strings.TrimSpace(cmdOutput) } return } // executePowershellCommands executes commands in powershell to get all windows applications installed. func executePowershellCommands(context context.T, command, args, arch string) (data []model.ApplicationData) { var output []byte var err error log := context.Log() log.Infof("Getting all %v windows applications", arch) log.Infof("Executing command: %v %v", command, args) if output, err = cmdExecutor(command, args); err != nil { log.Debugf("Failed to execute command : %v %v with error - %v", command, args, err.Error()) log.Debugf("Command Stderr: %v", string(output)) err = fmt.Errorf("Command failed with error: %v", string(output)) log.Error(err.Error()) log.Infof("No application data to return") } else { // Clean all Ctrl code from UTF-8 string cmdOutput := stripCtlFromUTF8(string(output)) log.Debugf("Command output: %v", cmdOutput) if data, err = convertToApplicationData(cmdOutput, arch); err != nil { err = fmt.Errorf("Unable to convert query output to ApplicationData - %v", err.Error()) log.Error(err.Error()) log.Infof("No application data to return") } else { log.Infof("Number of %v applications detected by %v - %v", arch, GathererName, len(data)) str, _ := json.Marshal(data) log.Debugf("Gathered applications: %v", string(str)) } } return } // convertToApplicationData converts powershell command output to an array of model.ApplicationData func convertToApplicationData(cmdOutput, architecture string) (data []model.ApplicationData, err error) { //This implementation is closely tied to the kind of powershell command we run in windows. A change in command //MUST be accompanied with a change in json conversion logic as well. /* The powershell command that we run in windows to get applications information will generate data in the following format: { "Name": "EC2ConfigService", "Version": "3.17.1032.0" }, { "Name": "aws-cfn-bootstrap", "Version": "1.4.10" }, { "Name": "AWS PV Drivers", "Version": "7.3.2" }, We do the following operations: - convert the string to a json array string - unmarshal the string - add architecture details as given input */ str := convertEntriesToJsonArray(cmdOutput) // remove newlines because powershell 2.0 sometimes inserts newlines every 80 characters or so str = cleanupNewLines(str) //unmarshall json string & add architecture information if err = json.Unmarshal([]byte(str), &data); err == nil { //iterate over all entries and add default value of architecture as given input for i, item := range data { //set architecture to given input item.Architecture = architecture item.CompType = componentType(item.Name) data[i] = item } } return }