-
Notifications
You must be signed in to change notification settings - Fork 323
/
dataProvider_windows.go
336 lines (277 loc) · 14.7 KB
/
dataProvider_windows.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// 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.
// +build windows
package application
import (
"encoding/json"
"fmt"
"os/exec"
"runtime"
"strings"
"github.com/aws/amazon-ssm-agent/agent/context"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/model"
)
const (
PowershellCmd = "powershell"
SysnativePowershellCmd = `C:\Windows\sysnative\WindowsPowerShell\v1.0\powershell.exe `
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
($_.UninstallString -ne $null -and $_.UninstallString -ne '') -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 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 <REGISTRY PATH> | 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
}