Skip to content

Commit f06dc4d

Browse files
authored
✨ Detect Azure Edition and hotpatching for Windows (#6342)
Signed-off-by: Christian Zunker <christian@mondoo.com>
1 parent 3c7fa23 commit f06dc4d

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed

providers/os/detector/detector_platform_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,20 @@ func TestWindows2025Detector(t *testing.T) {
734734
assert.Equal(t, []string{"windows", "os"}, di.Family)
735735
}
736736

737+
func TestAzureWindows2025Detector(t *testing.T) {
738+
di, err := detectPlatformFromMock("./testdata/detect-azure-windows2025.toml")
739+
assert.Nil(t, err, "was able to create the provider")
740+
741+
assert.Equal(t, "windows", di.Name, "os name should be identified")
742+
assert.Equal(t, "Windows Server 2025 Datacenter Azure Edition", di.Title, "os title should be identified")
743+
assert.Equal(t, "26311", di.Version, "os version should be identified")
744+
assert.Equal(t, "5000", di.Build, "os build version should be identified")
745+
assert.Equal(t, "AMD64", di.Arch, "os arch should be identified")
746+
assert.Equal(t, []string{"windows", "os"}, di.Family)
747+
assert.Equal(t, "ServerTurbine", di.Labels["windows.mondoo.com/edition-id"])
748+
assert.Equal(t, "false", di.Labels["windows.mondoo.com/hotpatch"])
749+
}
750+
737751
func TestPhoton1Detector(t *testing.T) {
738752
di, err := detectPlatformFromMock("./testdata/detect-photon1.toml")
739753
assert.Nil(t, err, "was able to create the provider")

providers/os/detector/detector_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func TestWindowsPlatform(t *testing.T) {
108108
Labels: map[string]string{
109109
"windows.mondoo.com/product-type": "1",
110110
"windows.mondoo.com/display-version": "21H2",
111+
"windows.mondoo.com/edition-id": "Professional",
111112
},
112113
},
113114
},
@@ -133,6 +134,7 @@ func TestWindowsPlatform(t *testing.T) {
133134
Labels: map[string]string{
134135
"windows.mondoo.com/product-type": "1",
135136
"windows.mondoo.com/display-version": "23H2",
137+
"windows.mondoo.com/edition-id": "Professional",
136138
},
137139
},
138140
},
@@ -158,6 +160,7 @@ func TestWindowsPlatform(t *testing.T) {
158160
Labels: map[string]string{
159161
"windows.mondoo.com/product-type": "3",
160162
"windows.mondoo.com/display-version": "21H2",
163+
"windows.mondoo.com/edition-id": "ServerStandard",
161164
},
162165
},
163166
},

providers/os/detector/detector_win.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ func runtimeWindowsDetector(pf *inventory.Platform, conn shared.Connection) (boo
2828
current, err := win.GetWindowsOSBuild(conn)
2929
if err == nil && current.UBR > 0 {
3030
platformFromWinCurrentVersion(pf, current)
31+
32+
hotpatchEnabled, err := win.GetWindowsHotpatch(conn, pf)
33+
if err != nil {
34+
// Don't return an error here, as it is expected that this key may not exist
35+
log.Debug().Err(err).Msg("could not get windows hotpatch status")
36+
}
37+
38+
pf.Labels["windows.mondoo.com/hotpatch"] = strconv.FormatBool(hotpatchEnabled)
3139
return true, nil
3240
}
3341

@@ -76,6 +84,7 @@ func platformFromWinCurrentVersion(pf *inventory.Platform, current *win.WindowsC
7684

7785
pf.Labels["windows.mondoo.com/product-type"] = productType
7886
pf.Labels["windows.mondoo.com/display-version"] = current.DisplayVersion
87+
pf.Labels["windows.mondoo.com/edition-id"] = current.EditionID
7988

8089
correctForWindows11(pf)
8190
}
@@ -106,6 +115,18 @@ func staticWindowsDetector(pf *inventory.Platform, conn shared.Connection) (bool
106115
pf.Title = productName.Value.String
107116
}
108117

118+
editionID, err := rh.GetRegistryItemValue(registry.Software, "Microsoft\\Windows NT\\CurrentVersion", "EditionID")
119+
if err == nil {
120+
log.Debug().Str("editionID", editionID.Value.String).Msg("found editionID")
121+
pf.Labels["windows.mondoo.com/edition-id"] = editionID.Value.String
122+
}
123+
124+
arch, err := rh.GetRegistryItemValue(registry.Software, "Microsoft\\Windows NT\\CurrentVersion", "Architecture")
125+
if err == nil && arch.Value.String != "" {
126+
log.Debug().Str("architecture", arch.Value.String).Msg("found architecture")
127+
pf.Arch = arch.Value.String
128+
}
129+
109130
ubr, err := rh.GetRegistryItemValue(registry.Software, "Microsoft\\Windows NT\\CurrentVersion", "UBR")
110131
if err == nil && ubr.Value.String != "" {
111132
log.Debug().Str("ubr", ubr.Value.String).Msg("found ubr")
@@ -124,6 +145,28 @@ func staticWindowsDetector(pf *inventory.Platform, conn shared.Connection) (bool
124145
}
125146
}
126147

148+
platformArch := "amd64"
149+
if pf.Arch != "" {
150+
platformArch = strings.ToLower(pf.Arch)
151+
}
152+
hotpatchPackage, err := rh.GetRegistryItemValue(registry.Software, "Microsoft\\Windows NT\\CurrentVersion\\Update\\TargetingInfo\\DynamicInstalled\\Hotpatch."+platformArch, "Name")
153+
if err == nil && hotpatchPackage.Value.String != "" {
154+
log.Debug().Str("hotpatchPackage", hotpatchPackage.Value.String).Msg("found hotpatchPackage")
155+
}
156+
157+
enableVirtualizationBasedSecurity, err := rh.GetRegistryItemValue(registry.System, "CurrentControlSet\\Control\\DeviceGuard", "EnableVirtualizationBasedSecurity")
158+
if err == nil && enableVirtualizationBasedSecurity.Value.String != "" {
159+
log.Debug().Str("enableVirtualizationBasedSecurity", enableVirtualizationBasedSecurity.Value.String).Msg("found enableVirtualizationBasedSecurity")
160+
}
161+
162+
hotPatchTableSize, err := rh.GetRegistryItemValue(registry.System, "CurrentControlSet\\Control\\Session Manager\\Memory Management", "HotPatchTableSize")
163+
if err == nil && enableVirtualizationBasedSecurity.Value.String != "" {
164+
log.Debug().Str("hotPatchTableSize", hotPatchTableSize.Value.String).Msg("found hotPatchTableSize")
165+
}
166+
167+
hotpatchEnabled := hotpatchPackage.Value.String == win.HotpatchPackage && enableVirtualizationBasedSecurity.Value.String == "1" && hotPatchTableSize.Value.String != "0"
168+
pf.Labels["windows.mondoo.com/hotpatch"] = strconv.FormatBool(hotpatchEnabled)
169+
127170
correctForWindows11(pf)
128171

129172
return true, nil
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[commands."55dbc0e9b838caa11145eed07f6e73644bda27bf65a0c58a52291f9a18384481"]
2+
stdout="""
3+
{
4+
"CurrentBuild": "26311",
5+
"UBR": 5000,
6+
"InstallationType": "Server",
7+
"EditionID": "ServerTurbine",
8+
"ProductName": "Windows Server 2025 Datacenter Azure Edition",
9+
"DisplayVersion": "24H2",
10+
"Architecture": "AMD64",
11+
"ProductType": "ServerNT"
12+
}
13+
"""
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package windows
5+
6+
import (
7+
"encoding/json"
8+
"io"
9+
"strings"
10+
11+
"github.com/rs/zerolog/log"
12+
"go.mondoo.com/cnquery/v12/providers/os/connection/shared"
13+
"go.mondoo.com/cnquery/v12/providers/os/resources/powershell"
14+
)
15+
16+
const (
17+
HotpatchPackage = "Hotpatch Enrollment Package"
18+
)
19+
20+
type WindowsHotpatch struct {
21+
Name string `json:"Name"`
22+
HotPatchTableSize string `json:"HotPatchTableSize"`
23+
EnableVirtualizationBasedSecurity string `json:"EnableVirtualizationBasedSecurity"`
24+
}
25+
26+
func ParseWinRegistryHotpatch(r io.Reader) (bool, error) {
27+
data, err := io.ReadAll(r)
28+
if err != nil {
29+
return false, err
30+
}
31+
32+
var hotpatch WindowsHotpatch
33+
err = json.Unmarshal(data, &hotpatch)
34+
if err != nil {
35+
return false, err
36+
}
37+
log.Debug().Interface("Hotpatch", hotpatch).Msg("Parsed hotpatch information")
38+
39+
return hotpatch.Name == HotpatchPackage && hotpatch.EnableVirtualizationBasedSecurity == "1" && hotpatch.HotPatchTableSize != "0", nil
40+
}
41+
42+
// https://learn.microsoft.com/en-us/windows-server/get-started/hotpatch
43+
// https://learn.microsoft.com/en-us/windows-server/get-started/enable-hotpatch-azure-edition
44+
45+
// powershellGetWindowsHotpatch runs a powershell script to determine whether hotpatching is enabled on the system.
46+
func powershellGetWindowsHotpatch(conn shared.Connection, arch string) (bool, error) {
47+
// FIXME: for windows 2025 this might be arm64
48+
pscommand := `
49+
$info = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Update\TargetingInfo\DynamicInstalled\Hotpatch.` + strings.ToLower(arch) + `' -Name Name
50+
$sysInfo = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard' -Name EnableVirtualizationBasedSecurity
51+
$hotpatch = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management' -Name HotPatchTableSize
52+
$sysInfo | Add-Member -MemberType NoteProperty -Name Name -Value $info.Name
53+
$hotpatch | Add-Member -MemberType NoteProperty -Name HotPatchTableSize -Value $hotpatch.HotPatchTableSize
54+
$sysInfo | Select-Object Name, EnableVirtualizationBasedSecurity, HotPatchTableSize | ConvertTo-Json
55+
`
56+
57+
log.Debug().Msg("checking Windows hotpatch runtime")
58+
cmd, err := conn.RunCommand(powershell.Encode(pscommand))
59+
if err != nil {
60+
log.Debug().Err(err).Msg("could not run powershell command to get hotpatch information")
61+
// Don't return an error here, as it is expected that this key may not exist
62+
return false, nil
63+
}
64+
return ParseWinRegistryHotpatch(cmd.Stdout)
65+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package windows
5+
6+
import (
7+
"strings"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestParseWinRegistryHotpatch(t *testing.T) {
14+
15+
t.Run("parse hptpatching settings correctly", func(t *testing.T) {
16+
data := `{
17+
"Name": "Hotpatch Enrollment Package",
18+
"HotPatchTableSize": "4096",
19+
"EnableVirtualizationBasedSecurity": "1"
20+
}`
21+
22+
m, err := ParseWinRegistryHotpatch(strings.NewReader(data))
23+
assert.Nil(t, err)
24+
assert.True(t, m)
25+
})
26+
27+
t.Run("parse missing table size", func(t *testing.T) {
28+
data := `{
29+
"Name": "Hotpatch Enrollment Package",
30+
"HotPatchTableSize": "0",
31+
"EnableVirtualizationBasedSecurity": "1"
32+
}`
33+
34+
m, err := ParseWinRegistryHotpatch(strings.NewReader(data))
35+
assert.Nil(t, err)
36+
assert.False(t, m)
37+
})
38+
39+
t.Run("parse missing name", func(t *testing.T) {
40+
data := `{
41+
"Name": "",
42+
"HotPatchTableSize": "4096",
43+
"EnableVirtualizationBasedSecurity": "1"
44+
}`
45+
46+
m, err := ParseWinRegistryHotpatch(strings.NewReader(data))
47+
assert.Nil(t, err)
48+
assert.False(t, m)
49+
})
50+
51+
t.Run("parse missing VBS", func(t *testing.T) {
52+
data := `{
53+
"Name": "Hotpatch Enrollment Package",
54+
"HotPatchTableSize": "1",
55+
"EnableVirtualizationBasedSecurity": "0"
56+
}`
57+
58+
m, err := ParseWinRegistryHotpatch(strings.NewReader(data))
59+
assert.Nil(t, err)
60+
assert.False(t, m)
61+
})
62+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
//go:build linux || darwin || netbsd || openbsd || freebsd
5+
// +build linux darwin netbsd openbsd freebsd
6+
7+
package windows
8+
9+
import (
10+
"strconv"
11+
12+
"github.com/rs/zerolog/log"
13+
"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
14+
"go.mondoo.com/cnquery/v12/providers/os/connection/shared"
15+
)
16+
17+
func GetWindowsHotpatch(conn shared.Connection, pf *inventory.Platform) (bool, error) {
18+
buildNumber, err := strconv.Atoi(pf.Version)
19+
if err != nil {
20+
log.Error().Err(err).Msg("could not parse windows build number")
21+
}
22+
log.Debug().Int("buildNumber", buildNumber).Msg("parsed windows build number")
23+
if buildNumber < 20348 {
24+
return false, nil
25+
}
26+
27+
// In case of Windows Server 2022+, check for hotpatching
28+
// This can be activated for on-prem or Azure Editions
29+
return powershellGetWindowsHotpatch(conn, pf.Arch)
30+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
//go:build windows
5+
// +build windows
6+
7+
package windows
8+
9+
import (
10+
"runtime"
11+
"strconv"
12+
"strings"
13+
14+
"github.com/rs/zerolog/log"
15+
"go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
16+
"go.mondoo.com/cnquery/v12/providers/os/connection/shared"
17+
"golang.org/x/sys/windows/registry"
18+
)
19+
20+
func GetWindowsHotpatch(conn shared.Connection, pf *inventory.Platform) (bool, error) {
21+
log.Debug().Msg("checking windows hotpatch")
22+
23+
buildNumber, err := strconv.Atoi(pf.Version)
24+
if err != nil {
25+
log.Error().Err(err).Msg("could not parse windows build number")
26+
}
27+
log.Debug().Int("buildNumber", buildNumber).Msg("parsed windows build number")
28+
if buildNumber < 20348 {
29+
return false, nil
30+
}
31+
// In case of Windows Server 2022+, check for hotpatching
32+
// This can be activated for on-prem or Azure Editions
33+
34+
// if we are running locally on windows, we want to avoid using powershell to be faster
35+
if conn.Type() == shared.Type_Local && runtime.GOOS == "windows" {
36+
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Update\TargetingInfo\DynamicInstalled\Hotpatch.`+strings.ToLower(pf.Arch), registry.QUERY_VALUE)
37+
if err != nil {
38+
log.Debug().Err(err).Msg("could not open registry key DynamicInstalled")
39+
// Don't return an error here, as it is expected that this key may not exist
40+
return false, nil
41+
}
42+
defer k.Close()
43+
44+
hotpatchName, _, err := k.GetStringValue("Name")
45+
if err != nil && err != registry.ErrNotExist {
46+
return false, err
47+
}
48+
49+
systemKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\DeviceGuard`, registry.QUERY_VALUE)
50+
if err != nil {
51+
log.Debug().Err(err).Msg("could not open registry key DeviceGuard")
52+
// Don't return an error here, as it is expected that this key may not exist
53+
return false, nil
54+
}
55+
defer systemKey.Close()
56+
57+
enableVirtualizationBasedSecurity, _, err := systemKey.GetIntegerValue("EnableVirtualizationBasedSecurity")
58+
if err != nil && err != registry.ErrNotExist {
59+
log.Debug().Err(err).Msg("could not get EnableVirtualizationBasedSecurity value")
60+
return false, err
61+
}
62+
63+
memoryKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management`, registry.QUERY_VALUE)
64+
if err != nil {
65+
log.Debug().Err(err).Msg("could not open registry key Memory Management")
66+
// Don't return an error here, as it is expected that this key may not exist
67+
return false, nil
68+
}
69+
defer memoryKey.Close()
70+
71+
hotPatchTableSize, _, err := memoryKey.GetIntegerValue("HotPatchTableSize")
72+
if err != nil && err != registry.ErrNotExist {
73+
log.Debug().Err(err).Msg("could not get HotPatchTableSize value")
74+
return false, err
75+
}
76+
77+
log.Debug().Str("hotpatchName", hotpatchName).Int("enableVirtualizationBasedSecurity", int(enableVirtualizationBasedSecurity)).Int("hotPatchTableSize", int(hotPatchTableSize)).Msg("parsed windows hotpatch settings")
78+
79+
return hotpatchName == HotpatchPackage && enableVirtualizationBasedSecurity == 1 && hotPatchTableSize > 0, nil
80+
}
81+
82+
// for all non-local checks use powershell
83+
return powershellGetWindowsHotpatch(conn, pf.Arch)
84+
}

0 commit comments

Comments
 (0)