@cfour-hi/dsh-plugin-devicex 0.0.0-stage → 1.7.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.
@@ -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