@cfour-hi/dsh-plugin-devicex 0.0.0-stage → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -2
- package/client.js +754 -0
- package/cordis.patch.yml +3 -0
- package/host/actions-darwin.js +1052 -0
- package/host/actions-win32.js +1299 -0
- package/host/actions.js +45 -0
- package/host/benchmark.js +413 -0
- package/host/cleanup-darwin.js +715 -0
- package/host/cleanup-win32.js +760 -0
- package/host/cleanup.js +62 -0
- package/host/collector-darwin.js +2995 -0
- package/host/collector-win32.js +2057 -0
- package/host/collector.js +71 -0
- package/host/proc.js +232 -0
- package/host/tool.js +254 -0
- package/host/transport.js +294 -0
- package/host/win/hardware.ps1 +501 -0
- package/host/win/metrics.ps1 +535 -0
- package/host/win/network.ps1 +322 -0
- package/host/win/processes.ps1 +169 -0
- package/host/win/security.ps1 +197 -0
- package/host/win/software.ps1 +119 -0
- package/host/win/system.ps1 +279 -0
- package/host/win.js +187 -0
- package/icon.svg +18 -0
- package/index.js +65 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +60 -4
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Device X - Windows network inventory (adapters, routes, Wi-Fi, proxy, ports).
|
|
2
|
+
#
|
|
3
|
+
# Emits exactly one JSON document on stdout (no other stdout text).
|
|
4
|
+
# Read-only: no admin rights, no network calls, no external modules. Wi-Fi data
|
|
5
|
+
# comes from local `netsh wlan show interfaces` output only.
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
|
|
8
|
+
$errors = @()
|
|
9
|
+
|
|
10
|
+
function To-LongOrNull($value) {
|
|
11
|
+
if ($null -eq $value) { return $null }
|
|
12
|
+
try { return [long]$value } catch { return $null }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Invoke-NativeOem([scriptblock]$action) {
|
|
16
|
+
# Native console tools (netsh/tasklist/manage-bde/winget) write OEM/ANSI bytes
|
|
17
|
+
# on a non-English Windows. The bridge sets the console to UTF-8 for the JSON
|
|
18
|
+
# document; temporarily decode native output with the OEM code page so SSIDs,
|
|
19
|
+
# user names and device names are not mojibake, then restore in finally.
|
|
20
|
+
$originalEncoding = [Console]::OutputEncoding
|
|
21
|
+
$oemCodePage = 437
|
|
22
|
+
try {
|
|
23
|
+
$cultureCodePage = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage
|
|
24
|
+
if ($cultureCodePage -gt 0) { $oemCodePage = $cultureCodePage }
|
|
25
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding($oemCodePage)
|
|
26
|
+
return & $action
|
|
27
|
+
} finally {
|
|
28
|
+
[Console]::OutputEncoding = $originalEncoding
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Get-AdapterSpeedBps($adapter) {
|
|
33
|
+
# Get-NetAdapter exposes `Speed` (UInt64 bits per second) and `LinkSpeed` (a
|
|
34
|
+
# formatted string such as "1 Gbps"). Prefer the numeric property and parse
|
|
35
|
+
# the string as a fallback; never report the formatted string as a number.
|
|
36
|
+
$speed = To-LongOrNull $adapter.Speed
|
|
37
|
+
if ($null -ne $speed -and $speed -gt 0) { return $speed }
|
|
38
|
+
$text = [string]$adapter.LinkSpeed
|
|
39
|
+
if ($text -match '^([\d.]+)\s*(bps|Kbps|Mbps|Gbps|Tbps)') {
|
|
40
|
+
try { $value = [double]$matches[1] } catch { return $null }
|
|
41
|
+
$unit = $matches[2]
|
|
42
|
+
if ($unit -eq 'Kbps') { $value = $value * 1000 }
|
|
43
|
+
elseif ($unit -eq 'Mbps') { $value = $value * 1000000 }
|
|
44
|
+
elseif ($unit -eq 'Gbps') { $value = $value * 1000000000 }
|
|
45
|
+
elseif ($unit -eq 'Tbps') { $value = $value * 1000000000000 }
|
|
46
|
+
return [long][math]::Round($value)
|
|
47
|
+
}
|
|
48
|
+
return $null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# --------------------------------------------------------------- adapters --
|
|
52
|
+
$adapters = @()
|
|
53
|
+
try {
|
|
54
|
+
$netAdapters = @(Get-NetAdapter -ErrorAction Stop)
|
|
55
|
+
$loopbackAdapters = @()
|
|
56
|
+
try {
|
|
57
|
+
$hiddenAdapters = @(Get-NetAdapter -IncludeHidden -ErrorAction Stop)
|
|
58
|
+
foreach ($hiddenAdapter in $hiddenAdapters) {
|
|
59
|
+
if ([string]$hiddenAdapter.InterfaceDescription -match 'Loopback') { $loopbackAdapters += $hiddenAdapter }
|
|
60
|
+
}
|
|
61
|
+
} catch { $loopbackAdapters = @() }
|
|
62
|
+
|
|
63
|
+
$ipv4Map = @{}
|
|
64
|
+
$ipv6Map = @{}
|
|
65
|
+
try {
|
|
66
|
+
$addresses = @(Get-NetIPAddress -ErrorAction Stop)
|
|
67
|
+
foreach ($address in $addresses) {
|
|
68
|
+
$addressIndex = 0
|
|
69
|
+
if ($null -ne $address.InterfaceIndex) { $addressIndex = [int]$address.InterfaceIndex }
|
|
70
|
+
if ($addressIndex -le 0) { continue }
|
|
71
|
+
if (-not $ipv4Map.ContainsKey($addressIndex)) { $ipv4Map[$addressIndex] = @() }
|
|
72
|
+
if (-not $ipv6Map.ContainsKey($addressIndex)) { $ipv6Map[$addressIndex] = @() }
|
|
73
|
+
$ip = [string]$address.IPAddress
|
|
74
|
+
if (-not $ip) { continue }
|
|
75
|
+
if ($ip -eq '127.0.0.1' -or $ip -eq '::1') { continue }
|
|
76
|
+
if ($address.AddressFamily -eq 2) {
|
|
77
|
+
$ipv4Map[$addressIndex] += $ip
|
|
78
|
+
} elseif ($address.AddressFamily -eq 23) {
|
|
79
|
+
$ipv6Map[$addressIndex] += $ip
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch { $errors += ('Get-NetIPAddress: ' + $_.Exception.Message) }
|
|
83
|
+
|
|
84
|
+
$seenNames = @{}
|
|
85
|
+
foreach ($adapter in @($netAdapters + $loopbackAdapters)) {
|
|
86
|
+
$adapterName = [string]$adapter.Name
|
|
87
|
+
if (-not $adapterName) { continue }
|
|
88
|
+
if ($seenNames.ContainsKey($adapterName)) { continue }
|
|
89
|
+
$seenNames[$adapterName] = $true
|
|
90
|
+
$adapterIndex = 0
|
|
91
|
+
if ($null -ne $adapter.ifIndex) { $adapterIndex = [int]$adapter.ifIndex }
|
|
92
|
+
$ipv4 = @()
|
|
93
|
+
if ($ipv4Map.ContainsKey($adapterIndex)) { $ipv4 = @($ipv4Map[$adapterIndex]) }
|
|
94
|
+
$ipv6 = @()
|
|
95
|
+
if ($ipv6Map.ContainsKey($adapterIndex)) { $ipv6 = @($ipv6Map[$adapterIndex]) }
|
|
96
|
+
$mtu = $null
|
|
97
|
+
if ($null -ne $adapter.MtuSize) { $mtu = [long]$adapter.MtuSize }
|
|
98
|
+
$medium = $null
|
|
99
|
+
if ($null -ne $adapter.NdisPhysicalMedium) { $medium = [int]$adapter.NdisPhysicalMedium }
|
|
100
|
+
$adapters += [ordered]@{
|
|
101
|
+
name = $adapterName
|
|
102
|
+
description = [string]$adapter.InterfaceDescription
|
|
103
|
+
status = [string]$adapter.Status
|
|
104
|
+
mac = [string]$adapter.MacAddress
|
|
105
|
+
mtu = $mtu
|
|
106
|
+
ifIndex = $adapterIndex
|
|
107
|
+
physical = [bool]$adapter.Physical
|
|
108
|
+
virtual = [bool]$adapter.Virtual
|
|
109
|
+
ndisMedium = $medium
|
|
110
|
+
linkSpeedBps = Get-AdapterSpeedBps $adapter
|
|
111
|
+
ipv4 = $ipv4
|
|
112
|
+
ipv6 = $ipv6
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch { $errors += ('Get-NetAdapter: ' + $_.Exception.Message) }
|
|
116
|
+
|
|
117
|
+
# --------------------------------------------------------- default route --
|
|
118
|
+
$defaultRoute = $null
|
|
119
|
+
try {
|
|
120
|
+
# The effective Windows route metric is RouteMetric + InterfaceMetric; sort
|
|
121
|
+
# by that numeric sum (not a lexicographic two-key sort) so the reported
|
|
122
|
+
# default route is the real egress path when VPN / virtual adapters add
|
|
123
|
+
# extra defaults.
|
|
124
|
+
$routes = @(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop | Sort-Object -Property @{Expression = { [long]$_.RouteMetric + [long]$_.InterfaceMetric }})
|
|
125
|
+
if ($routes.Count -gt 0) {
|
|
126
|
+
$route = $routes[0]
|
|
127
|
+
$metric = $null
|
|
128
|
+
if ($null -ne $route.RouteMetric) { $metric = [long]$route.RouteMetric }
|
|
129
|
+
$routeIndex = $null
|
|
130
|
+
if ($null -ne $route.ifIndex) { $routeIndex = [long]$route.ifIndex }
|
|
131
|
+
$interfaceMetric = $null
|
|
132
|
+
if ($null -ne $route.InterfaceMetric) { $interfaceMetric = [long]$route.InterfaceMetric }
|
|
133
|
+
$defaultRoute = [ordered]@{
|
|
134
|
+
gateway = [string]$route.NextHop
|
|
135
|
+
interface = [string]$route.InterfaceAlias
|
|
136
|
+
ifIndex = $routeIndex
|
|
137
|
+
routeMetric = $metric
|
|
138
|
+
interfaceMetric = $interfaceMetric
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch { $errors += ('Get-NetRoute: ' + $_.Exception.Message) }
|
|
142
|
+
|
|
143
|
+
# -------------------------------------------------------------------- DNS --
|
|
144
|
+
$dnsServers = @()
|
|
145
|
+
try {
|
|
146
|
+
$dnsAddresses = @(Get-DnsClientServerAddress -ErrorAction Stop)
|
|
147
|
+
foreach ($dnsAddress in $dnsAddresses) {
|
|
148
|
+
foreach ($server in @($dnsAddress.ServerAddresses)) {
|
|
149
|
+
$serverText = [string]$server
|
|
150
|
+
if (-not $serverText) { continue }
|
|
151
|
+
if ($serverText -eq '::') { continue }
|
|
152
|
+
if ($serverText -match '^fec0:0:0:ffff::') { continue }
|
|
153
|
+
if ($dnsServers -notcontains $serverText) { $dnsServers += $serverText }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch { $errors += ('Get-DnsClientServerAddress: ' + $_.Exception.Message) }
|
|
157
|
+
|
|
158
|
+
$searchDomains = @()
|
|
159
|
+
try {
|
|
160
|
+
$globalDns = Get-DnsClientGlobalSetting -ErrorAction Stop
|
|
161
|
+
foreach ($suffix in @($globalDns.SuffixSearchList)) {
|
|
162
|
+
$suffixText = [string]$suffix
|
|
163
|
+
if ($suffixText -and $searchDomains -notcontains $suffixText) { $searchDomains += $suffixText }
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
try {
|
|
167
|
+
$dnsClients = @(Get-DnsClient -ErrorAction Stop)
|
|
168
|
+
foreach ($dnsClient in $dnsClients) {
|
|
169
|
+
$suffix = [string]$dnsClient.ConnectionSpecificSuffix
|
|
170
|
+
if ($suffix -and $searchDomains -notcontains $suffix) { $searchDomains += $suffix }
|
|
171
|
+
}
|
|
172
|
+
} catch { }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------ proxy --
|
|
176
|
+
$proxy = $null
|
|
177
|
+
try {
|
|
178
|
+
$internetSettings = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction Stop
|
|
179
|
+
$proxyEnable = $false
|
|
180
|
+
if ($null -ne $internetSettings.ProxyEnable) { $proxyEnable = ([int]$internetSettings.ProxyEnable -eq 1) }
|
|
181
|
+
$proxy = [ordered]@{
|
|
182
|
+
enable = $proxyEnable
|
|
183
|
+
server = [string]$internetSettings.ProxyServer
|
|
184
|
+
autoConfigUrl = [string]$internetSettings.AutoConfigURL
|
|
185
|
+
bypass = [string]$internetSettings.ProxyOverride
|
|
186
|
+
}
|
|
187
|
+
} catch { $errors += ('Internet Settings proxy: ' + $_.Exception.Message) }
|
|
188
|
+
|
|
189
|
+
# ------------------------------------------------------------------ Wi-Fi --
|
|
190
|
+
# netsh labels are localized; parse on the tokens Windows never translates
|
|
191
|
+
# ("SSID", "BSSID", "Mbps", "NN%") plus value-shape fallbacks.
|
|
192
|
+
$wifi = $null
|
|
193
|
+
try {
|
|
194
|
+
$wlanLines = @(Invoke-NativeOem { & netsh wlan show interfaces 2>$null })
|
|
195
|
+
if ($wlanLines.Count -gt 0) {
|
|
196
|
+
$parsedWifi = [ordered]@{}
|
|
197
|
+
$macValues = @()
|
|
198
|
+
$values = @()
|
|
199
|
+
foreach ($line in $wlanLines) {
|
|
200
|
+
$text = [string]$line
|
|
201
|
+
if ($text -match '^\s*([^:]+?)\s*:\s*(.*?)\s*$') {
|
|
202
|
+
$label = $matches[1]
|
|
203
|
+
$value = $matches[2]
|
|
204
|
+
$values += [pscustomobject]@{ label = $label; value = $value }
|
|
205
|
+
if ($value -match '^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$') { $macValues += $value }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if ($values.Count -gt 0 -and -not $parsedWifi.name) { $parsedWifi.name = [string]$values[0].value }
|
|
209
|
+
foreach ($entry in $values) {
|
|
210
|
+
$label = [string]$entry.label
|
|
211
|
+
$value = [string]$entry.value
|
|
212
|
+
if ($label -eq 'SSID') { $parsedWifi.ssid = $value; continue }
|
|
213
|
+
if ($label -eq 'BSSID' -or $label -eq 'AP BSSID') { $parsedWifi.bssid = $value; continue }
|
|
214
|
+
if ($label -eq 'Name') { $parsedWifi.name = $value; continue }
|
|
215
|
+
if ($label -eq 'State') { $parsedWifi.state = $value; continue }
|
|
216
|
+
if ($label -eq 'Authentication') { $parsedWifi.authentication = $value; continue }
|
|
217
|
+
if ($label -eq 'Radio type') { $parsedWifi.radioType = $value; continue }
|
|
218
|
+
if ($value -match '^(\d{1,3})%$') { $parsedWifi.signalPercent = [int]$matches[1]; continue }
|
|
219
|
+
if ($label -match 'Mbps') {
|
|
220
|
+
if (-not $parsedWifi.receiveRateMbps) { $parsedWifi.receiveRateMbps = [double]$value }
|
|
221
|
+
else { $parsedWifi.transmitRateMbps = [double]$value }
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if ($value -match '^\d{1,3}$') {
|
|
225
|
+
$number = [int]$value
|
|
226
|
+
if ($number -ge 1 -and $number -le 233 -and -not $parsedWifi.channel) { $parsedWifi.channel = $number }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (-not $parsedWifi.bssid -and $macValues.Count -gt 1) { $parsedWifi.bssid = [string]$macValues[$macValues.Count - 1] }
|
|
230
|
+
if ($parsedWifi.ssid -and [string]$parsedWifi.ssid -ne '') {
|
|
231
|
+
if (-not $parsedWifi.state) { $parsedWifi.state = 'connected' }
|
|
232
|
+
$wifi = $parsedWifi
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch { $errors += ('netsh wlan show interfaces: ' + $_.Exception.Message) }
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------- listening / sockets --
|
|
238
|
+
$processNames = @{}
|
|
239
|
+
try {
|
|
240
|
+
$runningProcesses = @(Get-Process -ErrorAction SilentlyContinue)
|
|
241
|
+
foreach ($process in $runningProcesses) {
|
|
242
|
+
try { $processNames[[int]$process.Id] = [string]$process.ProcessName } catch { }
|
|
243
|
+
}
|
|
244
|
+
} catch { $processNames = @{} }
|
|
245
|
+
|
|
246
|
+
$tcpListeners = @()
|
|
247
|
+
$establishedCount = 0
|
|
248
|
+
try {
|
|
249
|
+
$connections = @(Get-NetTCPConnection -ErrorAction Stop)
|
|
250
|
+
foreach ($connection in $connections) {
|
|
251
|
+
$state = [string]$connection.State
|
|
252
|
+
if ($state -eq 'Established') { $establishedCount += 1; continue }
|
|
253
|
+
if ($state -ne 'Listen') { continue }
|
|
254
|
+
$localPort = 0
|
|
255
|
+
if ($null -ne $connection.LocalPort) { $localPort = [int]$connection.LocalPort }
|
|
256
|
+
$ownerPid = 0
|
|
257
|
+
if ($null -ne $connection.OwningProcess) { $ownerPid = [int]$connection.OwningProcess }
|
|
258
|
+
$processName = $null
|
|
259
|
+
if ($processNames.ContainsKey($ownerPid)) { $processName = $processNames[$ownerPid] }
|
|
260
|
+
$tcpListeners += [ordered]@{
|
|
261
|
+
address = [string]$connection.LocalAddress
|
|
262
|
+
port = $localPort
|
|
263
|
+
pid = $ownerPid
|
|
264
|
+
process = $processName
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
$errors += ('Get-NetTCPConnection: ' + $_.Exception.Message)
|
|
269
|
+
try {
|
|
270
|
+
$listenConnections = @(Get-NetTCPConnection -State Listen -ErrorAction Stop)
|
|
271
|
+
foreach ($connection in $listenConnections) {
|
|
272
|
+
$localPort = 0
|
|
273
|
+
if ($null -ne $connection.LocalPort) { $localPort = [int]$connection.LocalPort }
|
|
274
|
+
$ownerPid = 0
|
|
275
|
+
if ($null -ne $connection.OwningProcess) { $ownerPid = [int]$connection.OwningProcess }
|
|
276
|
+
$processName = $null
|
|
277
|
+
if ($processNames.ContainsKey($ownerPid)) { $processName = $processNames[$ownerPid] }
|
|
278
|
+
$tcpListeners += [ordered]@{
|
|
279
|
+
address = [string]$connection.LocalAddress
|
|
280
|
+
port = $localPort
|
|
281
|
+
pid = $ownerPid
|
|
282
|
+
process = $processName
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch { }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
$udpListeners = @()
|
|
289
|
+
try {
|
|
290
|
+
$endpoints = @(Get-NetUDPEndpoint -ErrorAction Stop)
|
|
291
|
+
foreach ($endpoint in $endpoints) {
|
|
292
|
+
$localPort = 0
|
|
293
|
+
if ($null -ne $endpoint.LocalPort) { $localPort = [int]$endpoint.LocalPort }
|
|
294
|
+
if ($localPort -le 0) { continue }
|
|
295
|
+
$ownerPid = 0
|
|
296
|
+
if ($null -ne $endpoint.OwningProcess) { $ownerPid = [int]$endpoint.OwningProcess }
|
|
297
|
+
$processName = $null
|
|
298
|
+
if ($processNames.ContainsKey($ownerPid)) { $processName = $processNames[$ownerPid] }
|
|
299
|
+
$udpListeners += [ordered]@{
|
|
300
|
+
address = [string]$endpoint.LocalAddress
|
|
301
|
+
port = $localPort
|
|
302
|
+
pid = $ownerPid
|
|
303
|
+
process = $processName
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch { $errors += ('Get-NetUDPEndpoint: ' + $_.Exception.Message) }
|
|
307
|
+
|
|
308
|
+
# -------------------------------------------------------------------- emit --
|
|
309
|
+
$result = [ordered]@{
|
|
310
|
+
generatedAt = ([DateTimeOffset]::UtcNow).ToUnixTimeMilliseconds()
|
|
311
|
+
adapters = $adapters
|
|
312
|
+
defaultRoute = $defaultRoute
|
|
313
|
+
dns = [ordered]@{ servers = $dnsServers; searchDomains = $searchDomains }
|
|
314
|
+
proxy = $proxy
|
|
315
|
+
wifi = $wifi
|
|
316
|
+
tcpListeners = $tcpListeners
|
|
317
|
+
udpListeners = $udpListeners
|
|
318
|
+
establishedCount = $establishedCount
|
|
319
|
+
errors = $errors
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
$result | ConvertTo-Json -Depth 8 -Compress
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Device X - Windows process inventory (perf counters + CIM + tasklist owners).
|
|
2
|
+
#
|
|
3
|
+
# Emits exactly one JSON document on stdout (no other stdout text).
|
|
4
|
+
# Read-only: no admin rights, no network calls, no external modules.
|
|
5
|
+
# CPU percent is reported in Task-Manager scale (raw perf counter divided by the
|
|
6
|
+
# logical processor count); the Node mapper normalizes and rounds again.
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
|
|
9
|
+
$errors = @()
|
|
10
|
+
|
|
11
|
+
function To-EpochMs($value) {
|
|
12
|
+
if ($null -eq $value) { return $null }
|
|
13
|
+
try { return ([DateTimeOffset]$value).ToUnixTimeMilliseconds() } catch { return $null }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function To-LongOrNull($value) {
|
|
17
|
+
if ($null -eq $value) { return $null }
|
|
18
|
+
try { return [long]$value } catch { return $null }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Invoke-NativeOem([scriptblock]$action) {
|
|
22
|
+
# Native console tools (netsh/tasklist/manage-bde/winget) write OEM/ANSI bytes
|
|
23
|
+
# on a non-English Windows. The bridge sets the console to UTF-8 for the JSON
|
|
24
|
+
# document; temporarily decode native output with the OEM code page so SSIDs,
|
|
25
|
+
# user names and device names are not mojibake, then restore in finally.
|
|
26
|
+
$originalEncoding = [Console]::OutputEncoding
|
|
27
|
+
$oemCodePage = 437
|
|
28
|
+
try {
|
|
29
|
+
$cultureCodePage = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage
|
|
30
|
+
if ($cultureCodePage -gt 0) { $oemCodePage = $cultureCodePage }
|
|
31
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding($oemCodePage)
|
|
32
|
+
return & $action
|
|
33
|
+
} finally {
|
|
34
|
+
[Console]::OutputEncoding = $originalEncoding
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
$logicalCores = 0
|
|
39
|
+
try { $logicalCores = [int]$env:NUMBER_OF_PROCESSORS } catch { $logicalCores = 0 }
|
|
40
|
+
if ($logicalCores -le 0) { $logicalCores = 0 }
|
|
41
|
+
|
|
42
|
+
$totalMemoryBytes = $null
|
|
43
|
+
try {
|
|
44
|
+
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
|
|
45
|
+
if ($null -ne $os.TotalVisibleMemorySize) { $totalMemoryBytes = To-LongOrNull ([long]$os.TotalVisibleMemorySize * 1024) }
|
|
46
|
+
} catch { $errors += ('Win32_OperatingSystem: ' + $_.Exception.Message) }
|
|
47
|
+
|
|
48
|
+
# --- perf counters: instantaneous CPU percent + private working set ----------
|
|
49
|
+
$perfMap = @{}
|
|
50
|
+
try {
|
|
51
|
+
$processPerf = @(Get-CimInstance -ClassName Win32_PerfFormattedData_PerfProc_Process -ErrorAction Stop)
|
|
52
|
+
foreach ($row in $processPerf) {
|
|
53
|
+
$processId = 0
|
|
54
|
+
if ($null -ne $row.IDProcess) { $processId = [int]$row.IDProcess }
|
|
55
|
+
if ($processId -le 0) { continue }
|
|
56
|
+
$rawName = [string]$row.Name
|
|
57
|
+
if ($rawName -eq '_Total' -or $rawName -eq 'Idle') { continue }
|
|
58
|
+
$perfMap[$processId] = [pscustomobject]@{
|
|
59
|
+
name = ($rawName -replace '#\d+$', '')
|
|
60
|
+
percentProcessorTime = To-LongOrNull $row.PercentProcessorTime
|
|
61
|
+
workingSetPrivate = To-LongOrNull $row.WorkingSetPrivate
|
|
62
|
+
workingSet = To-LongOrNull $row.WorkingSet
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch { $errors += ('Win32_PerfFormattedData_PerfProc_Process: ' + $_.Exception.Message) }
|
|
66
|
+
|
|
67
|
+
# --- CIM: identity, parent, command line, creation date ---------------------
|
|
68
|
+
$cimProcesses = @()
|
|
69
|
+
try {
|
|
70
|
+
$cimProcesses = @(Get-CimInstance -ClassName Win32_Process -Property ProcessId,ParentProcessId,Name,CommandLine,CreationDate,WorkingSetSize,ExecutablePath -ErrorAction Stop)
|
|
71
|
+
} catch { $errors += ('Win32_Process: ' + $_.Exception.Message) }
|
|
72
|
+
|
|
73
|
+
# --- tasklist: per-process user names (no elevation required) ---------------
|
|
74
|
+
$userMap = @{}
|
|
75
|
+
try {
|
|
76
|
+
$taskLines = @(Invoke-NativeOem { & tasklist.exe /fo csv /v /nh 2>$null })
|
|
77
|
+
$csvLines = @()
|
|
78
|
+
foreach ($line in $taskLines) {
|
|
79
|
+
if ([string]$line -match '^"') { $csvLines += [string]$line }
|
|
80
|
+
}
|
|
81
|
+
if ($csvLines.Count -gt 0) {
|
|
82
|
+
$taskRows = @($csvLines | ConvertFrom-Csv -Header 'ImageName','ProcessId','SessionName','SessionNumber','MemUsage','Status','UserName','CpuTime','WindowTitle')
|
|
83
|
+
foreach ($taskRow in $taskRows) {
|
|
84
|
+
$taskPid = 0
|
|
85
|
+
try { $taskPid = [int]$taskRow.ProcessId } catch { $taskPid = 0 }
|
|
86
|
+
if ($taskPid -le 0) { continue }
|
|
87
|
+
$userName = [string]$taskRow.UserName
|
|
88
|
+
if ($userName -and $userName -ne 'N/A') { $userMap[$taskPid] = $userName }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch { $errors += ('tasklist: ' + $_.Exception.Message) }
|
|
92
|
+
|
|
93
|
+
# --- Get-Process: responsiveness + total count ------------------------------
|
|
94
|
+
$respondsMap = @{}
|
|
95
|
+
$getProcessCount = 0
|
|
96
|
+
try {
|
|
97
|
+
$allProcesses = @(Get-Process -ErrorAction SilentlyContinue)
|
|
98
|
+
$getProcessCount = $allProcesses.Count
|
|
99
|
+
foreach ($process in $allProcesses) {
|
|
100
|
+
$processId = [int]$process.Id
|
|
101
|
+
try {
|
|
102
|
+
$respondsMap[$processId] = [bool]$process.Responding
|
|
103
|
+
} catch { }
|
|
104
|
+
}
|
|
105
|
+
} catch { $respondsMap = @{} }
|
|
106
|
+
|
|
107
|
+
# --- merge ------------------------------------------------------------------
|
|
108
|
+
$rows = @()
|
|
109
|
+
$nowMs = ([DateTimeOffset]::UtcNow).ToUnixTimeMilliseconds()
|
|
110
|
+
foreach ($process in $cimProcesses) {
|
|
111
|
+
$processId = 0
|
|
112
|
+
if ($null -ne $process.ProcessId) { $processId = [int]$process.ProcessId }
|
|
113
|
+
if ($processId -le 0) { continue }
|
|
114
|
+
$name = [string]$process.Name
|
|
115
|
+
if (-not $name) {
|
|
116
|
+
if ($perfMap.ContainsKey($processId)) { $name = [string]$perfMap[$processId].name }
|
|
117
|
+
}
|
|
118
|
+
if (-not $name) { $name = 'unknown' }
|
|
119
|
+
$cpuPercent = 0.0
|
|
120
|
+
$workingSetBytes = $null
|
|
121
|
+
if ($perfMap.ContainsKey($processId)) {
|
|
122
|
+
$perfRow = $perfMap[$processId]
|
|
123
|
+
if ($null -ne $perfRow.percentProcessorTime) { $cpuPercent = [double]$perfRow.percentProcessorTime }
|
|
124
|
+
if ($null -ne $perfRow.workingSetPrivate) { $workingSetBytes = To-LongOrNull $perfRow.workingSetPrivate }
|
|
125
|
+
if ($null -eq $workingSetBytes -and $null -ne $perfRow.workingSet) { $workingSetBytes = To-LongOrNull $perfRow.workingSet }
|
|
126
|
+
}
|
|
127
|
+
if ($null -eq $workingSetBytes) { $workingSetBytes = To-LongOrNull $process.WorkingSetSize }
|
|
128
|
+
$user = $null
|
|
129
|
+
if ($userMap.ContainsKey($processId)) { $user = $userMap[$processId] }
|
|
130
|
+
$state = 'running'
|
|
131
|
+
if ($respondsMap.ContainsKey($processId) -and $respondsMap[$processId] -eq $false) { $state = 'not responding' }
|
|
132
|
+
$command = [string]$process.CommandLine
|
|
133
|
+
if (-not $command) { $command = [string]$process.ExecutablePath }
|
|
134
|
+
if (-not $command) { $command = $name }
|
|
135
|
+
$creationMs = To-EpochMs $process.CreationDate
|
|
136
|
+
$elapsed = $null
|
|
137
|
+
if ($null -ne $creationMs -and $nowMs -gt $creationMs) {
|
|
138
|
+
$elapsed = [long][math]::Floor(($nowMs - $creationMs) / 1000)
|
|
139
|
+
}
|
|
140
|
+
$parentId = $null
|
|
141
|
+
if ($null -ne $process.ParentProcessId) { $parentId = [int]$process.ParentProcessId }
|
|
142
|
+
$rows += [ordered]@{
|
|
143
|
+
pid = $processId
|
|
144
|
+
ppid = $parentId
|
|
145
|
+
name = $name
|
|
146
|
+
command = $command
|
|
147
|
+
cpuPercent = $cpuPercent
|
|
148
|
+
workingSetBytes = $workingSetBytes
|
|
149
|
+
user = $user
|
|
150
|
+
state = $state
|
|
151
|
+
creationDateMs = $creationMs
|
|
152
|
+
elapsed = $elapsed
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
$total = $getProcessCount
|
|
157
|
+
if ($rows.Count -gt $total) { $total = $rows.Count }
|
|
158
|
+
|
|
159
|
+
# -------------------------------------------------------------------- emit --
|
|
160
|
+
$result = [ordered]@{
|
|
161
|
+
generatedAt = ([DateTimeOffset]::UtcNow).ToUnixTimeMilliseconds()
|
|
162
|
+
totalMemoryBytes = $totalMemoryBytes
|
|
163
|
+
logicalCores = $logicalCores
|
|
164
|
+
total = $total
|
|
165
|
+
processes = $rows
|
|
166
|
+
errors = $errors
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
$result | ConvertTo-Json -Depth 8 -Compress
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Device X - Windows security posture (BitLocker, Secure Boot, Defender, firewall, UAC).
|
|
2
|
+
#
|
|
3
|
+
# Emits exactly one JSON document on stdout (no other stdout text).
|
|
4
|
+
# Read-only. Some sources (BitLocker, Secure Boot) can require elevation; a
|
|
5
|
+
# denied read is reported as null + a non-fatal errors[] entry, never faked.
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
|
|
8
|
+
$errors = @()
|
|
9
|
+
|
|
10
|
+
function Invoke-NativeOem([scriptblock]$action) {
|
|
11
|
+
# Native console tools (netsh/tasklist/manage-bde/winget) write OEM/ANSI bytes
|
|
12
|
+
# on a non-English Windows. The bridge sets the console to UTF-8 for the JSON
|
|
13
|
+
# document; temporarily decode native output with the OEM code page so SSIDs,
|
|
14
|
+
# user names and device names are not mojibake, then restore in finally.
|
|
15
|
+
$originalEncoding = [Console]::OutputEncoding
|
|
16
|
+
$oemCodePage = 437
|
|
17
|
+
try {
|
|
18
|
+
$cultureCodePage = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage
|
|
19
|
+
if ($cultureCodePage -gt 0) { $oemCodePage = $cultureCodePage }
|
|
20
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding($oemCodePage)
|
|
21
|
+
return & $action
|
|
22
|
+
} finally {
|
|
23
|
+
[Console]::OutputEncoding = $originalEncoding
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# --------------------------------------------------------------- BitLocker --
|
|
28
|
+
$fileVault = [ordered]@{ enabled = $null; raw = $null }
|
|
29
|
+
$systemDrive = $env:SystemDrive
|
|
30
|
+
if (-not $systemDrive) { $systemDrive = 'C:' }
|
|
31
|
+
try {
|
|
32
|
+
$bitLockerVolume = Get-BitLockerVolume -MountPoint $systemDrive -ErrorAction Stop
|
|
33
|
+
$protectionEnabled = $null
|
|
34
|
+
if ($null -ne $bitLockerVolume.ProtectionStatus) {
|
|
35
|
+
$protectionValue = [int]$bitLockerVolume.ProtectionStatus
|
|
36
|
+
if ($protectionValue -eq 1) { $protectionEnabled = $true }
|
|
37
|
+
elseif ($protectionValue -eq 0) { $protectionEnabled = $false }
|
|
38
|
+
}
|
|
39
|
+
$volumeStatus = [string]$bitLockerVolume.VolumeStatus
|
|
40
|
+
$rawText = 'ProtectionStatus=' + [string]$bitLockerVolume.ProtectionStatus
|
|
41
|
+
if ($volumeStatus) { $rawText = $rawText + '; VolumeStatus=' + $volumeStatus }
|
|
42
|
+
$fileVault = [ordered]@{ enabled = $protectionEnabled; raw = $rawText; volumeStatus = $volumeStatus }
|
|
43
|
+
} catch {
|
|
44
|
+
$bitLockerError = $_.Exception.Message
|
|
45
|
+
try {
|
|
46
|
+
$manageBdeLines = @(Invoke-NativeOem { & manage-bde.exe -status $systemDrive 2>$null })
|
|
47
|
+
$manageBdeText = ($manageBdeLines -join "`n")
|
|
48
|
+
$manageBdeEnabled = $null
|
|
49
|
+
if ($manageBdeText -match 'Protection On') { $manageBdeEnabled = $true }
|
|
50
|
+
elseif ($manageBdeText -match 'Protection Off') { $manageBdeEnabled = $false }
|
|
51
|
+
$rawLine = $null
|
|
52
|
+
foreach ($line in $manageBdeLines) {
|
|
53
|
+
if ([string]$line -match 'Protection Status') { $rawLine = ([string]$line).Trim(); break }
|
|
54
|
+
}
|
|
55
|
+
if ($rawLine) {
|
|
56
|
+
if ($rawLine -match 'Protection On') { $manageBdeEnabled = $true }
|
|
57
|
+
elseif ($rawLine -match 'Protection Off') { $manageBdeEnabled = $false }
|
|
58
|
+
}
|
|
59
|
+
$fileVault = [ordered]@{ enabled = $manageBdeEnabled; raw = $manageBdeText; volumeStatus = $null }
|
|
60
|
+
$errors += ('Get-BitLockerVolume: ' + $bitLockerError)
|
|
61
|
+
} catch {
|
|
62
|
+
$errors += ('BitLocker: ' + $bitLockerError)
|
|
63
|
+
$fileVault = [ordered]@{ enabled = $null; raw = $bitLockerError; volumeStatus = $null }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# -------------------------------------------------------------- Secure Boot --
|
|
68
|
+
$sip = [ordered]@{ enabled = $null; raw = $null }
|
|
69
|
+
try {
|
|
70
|
+
$secureBootEnabled = Confirm-SecureBootUEFI -ErrorAction Stop
|
|
71
|
+
$sip = [ordered]@{ enabled = [bool]$secureBootEnabled; raw = 'Secure Boot: ' + [string]$secureBootEnabled }
|
|
72
|
+
} catch {
|
|
73
|
+
$sip = [ordered]@{ enabled = $null; raw = $_.Exception.Message }
|
|
74
|
+
$errors += ('Secure Boot: ' + $_.Exception.Message)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ----------------------------------------------------------------- Defender --
|
|
78
|
+
$gatekeeper = [ordered]@{ enabled = $null; raw = $null }
|
|
79
|
+
try {
|
|
80
|
+
$defenderStatus = Get-MpComputerStatus -ErrorAction Stop
|
|
81
|
+
$realTimeEnabled = $null
|
|
82
|
+
if ($null -ne $defenderStatus.RealTimeProtectionEnabled) {
|
|
83
|
+
$realTimeEnabled = [bool]$defenderStatus.RealTimeProtectionEnabled
|
|
84
|
+
}
|
|
85
|
+
$rawText = 'RealTimeProtectionEnabled=' + [string]$defenderStatus.RealTimeProtectionEnabled
|
|
86
|
+
if ($null -ne $defenderStatus.AntivirusEnabled) { $rawText = $rawText + '; AntivirusEnabled=' + [string]$defenderStatus.AntivirusEnabled }
|
|
87
|
+
if ($defenderStatus.AntivirusSignatureVersion) { $rawText = $rawText + '; SignatureVersion=' + [string]$defenderStatus.AntivirusSignatureVersion }
|
|
88
|
+
$gatekeeper = [ordered]@{ enabled = $realTimeEnabled; raw = $rawText }
|
|
89
|
+
} catch {
|
|
90
|
+
$gatekeeper = [ordered]@{ enabled = $null; raw = $_.Exception.Message }
|
|
91
|
+
$errors += ('Get-MpComputerStatus: ' + $_.Exception.Message)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------- Firewall --
|
|
95
|
+
$firewall = [ordered]@{ enabled = $null; raw = $null; stealthMode = $null }
|
|
96
|
+
try {
|
|
97
|
+
$profiles = @(Get-NetFirewallProfile -ErrorAction Stop)
|
|
98
|
+
$allEnabled = $null
|
|
99
|
+
$parts = @()
|
|
100
|
+
if ($profiles.Count -gt 0) {
|
|
101
|
+
$allEnabled = $true
|
|
102
|
+
foreach ($profile in $profiles) {
|
|
103
|
+
$profileEnabled = [bool]$profile.Enabled
|
|
104
|
+
if (-not $profileEnabled) { $allEnabled = $false }
|
|
105
|
+
$parts += ([string]$profile.Name + '=' + [string]$profile.Enabled)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
$firewall = [ordered]@{
|
|
109
|
+
enabled = $allEnabled
|
|
110
|
+
raw = ($parts -join ', ')
|
|
111
|
+
stealthMode = $null
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
$firewall = [ordered]@{ enabled = $null; raw = $_.Exception.Message; stealthMode = $null }
|
|
115
|
+
$errors += ('Get-NetFirewallProfile: ' + $_.Exception.Message)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------- users --
|
|
119
|
+
$userInfo = [ordered]@{ current = $env:USERNAME; admins = @(); total = $null }
|
|
120
|
+
try {
|
|
121
|
+
$localUsers = @(Get-LocalUser -ErrorAction Stop)
|
|
122
|
+
$userInfo.total = $localUsers.Count
|
|
123
|
+
} catch {
|
|
124
|
+
try {
|
|
125
|
+
$fallbackUsers = @(Get-CimInstance -ClassName Win32_UserAccount -Filter 'LocalAccount = True' -ErrorAction Stop)
|
|
126
|
+
$userInfo.total = $fallbackUsers.Count
|
|
127
|
+
} catch { $errors += ('local user count: ' + $_.Exception.Message) }
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
$adminGroup = $null
|
|
131
|
+
try { $adminGroup = Get-LocalGroup -SID 'S-1-5-32-544' -ErrorAction Stop } catch { $adminGroup = $null }
|
|
132
|
+
if ($null -eq $adminGroup) {
|
|
133
|
+
# The group display name is localized; resolve it by the well-known
|
|
134
|
+
# Administrators SID instead of the English name.
|
|
135
|
+
$adminGroup = Get-LocalGroup -ErrorAction Stop | Where-Object { $_.SID.Value -eq 'S-1-5-32-544' } | Select-Object -First 1
|
|
136
|
+
}
|
|
137
|
+
if ($null -eq $adminGroup) { throw 'Could not resolve the local Administrators group (S-1-5-32-544).' }
|
|
138
|
+
$members = @(Get-LocalGroupMember -Group $adminGroup -ErrorAction Stop)
|
|
139
|
+
$admins = @()
|
|
140
|
+
foreach ($member in $members) {
|
|
141
|
+
$memberName = [string]$member.Name
|
|
142
|
+
if ($memberName -match '\\') {
|
|
143
|
+
$parts = $memberName -split '\\'
|
|
144
|
+
$memberName = [string]$parts[$parts.Count - 1]
|
|
145
|
+
}
|
|
146
|
+
if ($memberName) { $admins += $memberName }
|
|
147
|
+
}
|
|
148
|
+
$userInfo.admins = $admins
|
|
149
|
+
} catch { $errors += ('local administrators: ' + $_.Exception.Message) }
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------ remote logon --
|
|
152
|
+
$remoteLogin = $null
|
|
153
|
+
try {
|
|
154
|
+
$terminalServer = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -ErrorAction Stop
|
|
155
|
+
if ($null -ne $terminalServer.fDenyTSConnections) {
|
|
156
|
+
$remoteLogin = ([int]$terminalServer.fDenyTSConnections -eq 0)
|
|
157
|
+
}
|
|
158
|
+
} catch { $errors += ('Remote Desktop setting: ' + $_.Exception.Message) }
|
|
159
|
+
|
|
160
|
+
# -------------------------------------------------------------- auto logon --
|
|
161
|
+
$autoLogin = $null
|
|
162
|
+
try {
|
|
163
|
+
$winlogon = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -ErrorAction Stop
|
|
164
|
+
$autoAdminLogon = [string]$winlogon.AutoAdminLogon
|
|
165
|
+
$defaultUserName = [string]$winlogon.DefaultUserName
|
|
166
|
+
if ($autoAdminLogon -eq '1' -and $defaultUserName) { $autoLogin = $defaultUserName }
|
|
167
|
+
} catch { $errors += ('Winlogon auto logon: ' + $_.Exception.Message) }
|
|
168
|
+
|
|
169
|
+
# UAC (collected for diagnostics; the frozen section shape has no UAC field yet).
|
|
170
|
+
$uac = [ordered]@{ enabled = $null; consentPromptBehaviorAdmin = $null; raw = $null }
|
|
171
|
+
try {
|
|
172
|
+
$policies = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -ErrorAction Stop
|
|
173
|
+
if ($null -ne $policies.EnableLUA) {
|
|
174
|
+
$uac.enabled = ([int]$policies.EnableLUA -eq 1)
|
|
175
|
+
$uac.raw = 'EnableLUA=' + [string]$policies.EnableLUA
|
|
176
|
+
}
|
|
177
|
+
if ($null -ne $policies.ConsentPromptBehaviorAdmin) {
|
|
178
|
+
$uac.consentPromptBehaviorAdmin = [int]$policies.ConsentPromptBehaviorAdmin
|
|
179
|
+
$uac.raw = ([string]$uac.raw + '; ConsentPromptBehaviorAdmin=' + [string]$policies.ConsentPromptBehaviorAdmin)
|
|
180
|
+
}
|
|
181
|
+
} catch { $errors += ('UAC setting: ' + $_.Exception.Message) }
|
|
182
|
+
|
|
183
|
+
# -------------------------------------------------------------------- emit --
|
|
184
|
+
$result = [ordered]@{
|
|
185
|
+
generatedAt = ([DateTimeOffset]::UtcNow).ToUnixTimeMilliseconds()
|
|
186
|
+
fileVault = $fileVault
|
|
187
|
+
sip = $sip
|
|
188
|
+
gatekeeper = $gatekeeper
|
|
189
|
+
firewall = $firewall
|
|
190
|
+
users = $userInfo
|
|
191
|
+
remoteLogin = $remoteLogin
|
|
192
|
+
autoLogin = $autoLogin
|
|
193
|
+
uac = $uac
|
|
194
|
+
errors = $errors
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
$result | ConvertTo-Json -Depth 8 -Compress
|