Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
env:
WIN_TEST_PACKAGES: ${{ steps.find-packages.outputs.packages }}
run: |
$packages = ($env:WIN_TEST_PACKAGES -split " ") | Where-Object { $_ -ne '' }
[string[]]$packages = ($env:WIN_TEST_PACKAGES -split " ") | Where-Object { $_ -ne '' }
go test -count=1 -v @packages 2>&1 | Tee-Object -FilePath windows-test-output.txt
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

Expand Down
27 changes: 27 additions & 0 deletions providers/os/detector/detector_win.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func runtimeWindowsDetector(pf *inventory.Platform, conn shared.Connection) (boo
}

pf.Labels["windows.mondoo.com/hotpatch"] = strconv.FormatBool(hotpatchEnabled)

detectIntuneDeviceID(pf, conn)
return true, nil
}

Expand All @@ -58,6 +60,8 @@ func runtimeWindowsDetector(pf *inventory.Platform, conn shared.Connection) (boo
pf.Labels["windows.mondoo.com/product-type"] = data.ProductType

correctForWindows11(pf)

detectIntuneDeviceID(pf, conn)
return true, nil
}

Expand Down Expand Up @@ -172,6 +176,29 @@ func staticWindowsDetector(pf *inventory.Platform, conn shared.Connection) (bool
return true, nil
}

// detectIntuneDeviceID detects the Intune device ID for Windows client systems.
// This includes workstations (product-type "1") and Windows 11 Enterprise Multi-Session
// systems which report as product-type "3" but are manageable via Intune.
func detectIntuneDeviceID(pf *inventory.Platform, conn shared.Connection) {
isWorkstation := pf.Labels["windows.mondoo.com/product-type"] == "1"
isWindows11MultiSession := pf.Labels["windows.mondoo.com/product-type"] == "3" &&
strings.Contains(pf.Title, "Windows 11") &&
strings.Contains(pf.Title, "Multi-Session")
if !isWorkstation && !isWindows11MultiSession {
return
}

intuneDeviceID, err := win.GetIntuneDeviceID(conn)
if err != nil {
log.Debug().Err(err).Msg("could not get Intune device ID")
return
}

if intuneDeviceID != "" {
pf.Labels["windows.mondoo.com/intune-device-id"] = intuneDeviceID
}
}

// correctForWindows11 replaces the windows 10 title with windows 11 if the build number is greater than 22000
// See https://techcommunity.microsoft.com/discussions/windows-management/windows-10-21h2-and-windows-11-21h2-both-show-up-as-2009-release/2994441
func correctForWindows11(pf *inventory.Platform) {
Expand Down
142 changes: 142 additions & 0 deletions providers/os/detector/detector_win_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package detector

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
"go.mondoo.com/mql/v13/providers/os/connection/mock"
)

func TestDetectIntuneDeviceID(t *testing.T) {
// Hash of the encoded Intune PowerShell command
const intuneCommandHash = "ee17502eeba7988928f8b668d05e2cb9e35bdc4cf9ebcde632470d87aa6a6905"

intuneEnrolledMock := &mock.TomlData{
Commands: map[string]*mock.Command{
intuneCommandHash: {
Stdout: `{"EnrollmentGUID":"12345678-1234-1234-1234-123456789012","EntDMID":"abcdef12-3456-7890-abcd-ef1234567890"}`,
},
},
}

intuneNotEnrolledMock := &mock.TomlData{
Commands: map[string]*mock.Command{
intuneCommandHash: {
Stdout: "",
},
},
}

t.Run("workstation should detect Intune device ID", func(t *testing.T) {
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(intuneEnrolledMock))
require.NoError(t, err)

pf := &inventory.Platform{
Title: "Windows 10 Enterprise",
Labels: map[string]string{
"windows.mondoo.com/product-type": "1",
},
}

detectIntuneDeviceID(pf, conn)
assert.Equal(t, "abcdef12-3456-7890-abcd-ef1234567890", pf.Labels["windows.mondoo.com/intune-device-id"])
})

t.Run("workstation not enrolled should not set label", func(t *testing.T) {
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(intuneNotEnrolledMock))
require.NoError(t, err)

pf := &inventory.Platform{
Title: "Windows 10 Enterprise",
Labels: map[string]string{
"windows.mondoo.com/product-type": "1",
},
}

detectIntuneDeviceID(pf, conn)
_, exists := pf.Labels["windows.mondoo.com/intune-device-id"]
assert.False(t, exists)
})

t.Run("Windows 11 multi-session should detect Intune device ID", func(t *testing.T) {
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(intuneEnrolledMock))
require.NoError(t, err)

pf := &inventory.Platform{
Title: "Windows 11 Enterprise Multi-Session",
Labels: map[string]string{
"windows.mondoo.com/product-type": "3",
},
}

detectIntuneDeviceID(pf, conn)
assert.Equal(t, "abcdef12-3456-7890-abcd-ef1234567890", pf.Labels["windows.mondoo.com/intune-device-id"])
})

t.Run("Windows Server should skip Intune detection", func(t *testing.T) {
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(intuneEnrolledMock))
require.NoError(t, err)

pf := &inventory.Platform{
Title: "Windows Server 2022 Datacenter",
Labels: map[string]string{
"windows.mondoo.com/product-type": "3",
},
}

detectIntuneDeviceID(pf, conn)
_, exists := pf.Labels["windows.mondoo.com/intune-device-id"]
assert.False(t, exists)
})

t.Run("Windows Server 2025 should skip Intune detection", func(t *testing.T) {
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(intuneEnrolledMock))
require.NoError(t, err)

pf := &inventory.Platform{
Title: "Windows Server 2025 Datacenter",
Labels: map[string]string{
"windows.mondoo.com/product-type": "3",
},
}

detectIntuneDeviceID(pf, conn)
_, exists := pf.Labels["windows.mondoo.com/intune-device-id"]
assert.False(t, exists)
})

t.Run("domain controller should skip Intune detection", func(t *testing.T) {
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(intuneEnrolledMock))
require.NoError(t, err)

pf := &inventory.Platform{
Title: "Windows Server 2022 Datacenter",
Labels: map[string]string{
"windows.mondoo.com/product-type": "2",
},
}

detectIntuneDeviceID(pf, conn)
_, exists := pf.Labels["windows.mondoo.com/intune-device-id"]
assert.False(t, exists)
})

t.Run("empty product-type should skip Intune detection", func(t *testing.T) {
conn, err := mock.New(0, &inventory.Asset{}, mock.WithData(intuneEnrolledMock))
require.NoError(t, err)

pf := &inventory.Platform{
Title: "Windows",
Labels: map[string]string{},
}

detectIntuneDeviceID(pf, conn)
_, exists := pf.Labels["windows.mondoo.com/intune-device-id"]
assert.False(t, exists)
})
}
52 changes: 52 additions & 0 deletions providers/os/detector/windows/intune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package windows

import (
"encoding/json"
"io"

"github.com/rs/zerolog/log"
"go.mondoo.com/mql/v13/providers/os/connection/shared"
"go.mondoo.com/mql/v13/providers/os/resources/powershell"
)

type IntuneDeviceInfo struct {
EnrollmentGUID string `json:"EnrollmentGUID"`
EntDMID string `json:"EntDMID"`
}

func ParseIntuneDeviceID(r io.Reader) (string, error) {
data, err := io.ReadAll(r)
if err != nil {
return "", err
}

// If the output is empty, the device is not enrolled in Intune
if len(data) == 0 {
return "", nil
}

var info IntuneDeviceInfo
err = json.Unmarshal(data, &info)
if err != nil {
return "", err
}
log.Debug().Str("EntDMID", info.EntDMID).Msg("parsed Intune device information")

return info.EntDMID, nil
}

// powershellGetIntuneDeviceID runs a powershell script to retrieve the Intune device ID from enrolled Windows clients.
func powershellGetIntuneDeviceID(conn shared.Connection) (string, error) {
pscommand := `Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\Enrollments\' | ForEach-Object { $dmClient = Join-Path $_.PSPath 'DMClient\MS DM Server'; if (Test-Path $dmClient) { $props = Get-ItemProperty $dmClient -ErrorAction SilentlyContinue; if ($props.EntDMID) { [PSCustomObject]@{ EnrollmentGUID = $_.PSChildName; EntDMID = $props.EntDMID } } } } | Select-Object -First 1 | ConvertTo-Json -Compress`

log.Debug().Msg("checking Intune device ID")
cmd, err := conn.RunCommand(powershell.Encode(pscommand))
if err != nil {
log.Debug().Err(err).Msg("could not run powershell command to get Intune device ID")
return "", nil
}
return ParseIntuneDeviceID(cmd.Stdout)
}
36 changes: 36 additions & 0 deletions providers/os/detector/windows/intune_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package windows

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseIntuneDeviceID(t *testing.T) {
t.Run("parse Intune device ID", func(t *testing.T) {
data := `{"EnrollmentGUID":"12345678-1234-1234-1234-123456789012","EntDMID":"abcdef12-3456-7890-abcd-ef1234567890"}`

id, err := ParseIntuneDeviceID(strings.NewReader(data))
require.NoError(t, err)
assert.Equal(t, "abcdef12-3456-7890-abcd-ef1234567890", id)
})

t.Run("parse empty output", func(t *testing.T) {
id, err := ParseIntuneDeviceID(strings.NewReader(""))
require.NoError(t, err)
assert.Equal(t, "", id)
})

t.Run("parse missing EntDMID", func(t *testing.T) {
data := `{"EnrollmentGUID":"12345678-1234-1234-1234-123456789012","EntDMID":""}`

id, err := ParseIntuneDeviceID(strings.NewReader(data))
require.NoError(t, err)
assert.Equal(t, "", id)
})
}
13 changes: 13 additions & 0 deletions providers/os/detector/windows/intune_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

//go:build linux || darwin || netbsd || openbsd || freebsd
// +build linux darwin netbsd openbsd freebsd

package windows

import "go.mondoo.com/mql/v13/providers/os/connection/shared"

func GetIntuneDeviceID(conn shared.Connection) (string, error) {
return powershellGetIntuneDeviceID(conn)
}
64 changes: 64 additions & 0 deletions providers/os/detector/windows/intune_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

//go:build windows
// +build windows

package windows

import (
"runtime"

"github.com/rs/zerolog/log"
"go.mondoo.com/mql/v13/providers/os/connection/shared"
"golang.org/x/sys/windows/registry"
)

func GetIntuneDeviceID(conn shared.Connection) (string, error) {
log.Debug().Msg("checking Intune device ID")

// if we are running locally on windows, we want to avoid using powershell to be faster
if conn.Type() == shared.Type_Local && runtime.GOOS == "windows" {
return getIntuneDeviceIDFromRegistry()
}

// for all non-local checks use powershell
return powershellGetIntuneDeviceID(conn)
}

// getIntuneDeviceIDFromRegistry reads the Intune device ID directly from the Windows registry.
func getIntuneDeviceIDFromRegistry() (string, error) {
enrollmentsKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Enrollments`, registry.ENUMERATE_SUB_KEYS)
if err != nil {
log.Debug().Err(err).Msg("could not open Enrollments registry key")
return "", nil
}
defer enrollmentsKey.Close()

subkeys, err := enrollmentsKey.ReadSubKeyNames(-1)
if err != nil {
log.Debug().Err(err).Msg("could not read Enrollments subkeys")
return "", nil
}

for _, subkey := range subkeys {
dmClientPath := `SOFTWARE\Microsoft\Enrollments\` + subkey + `\DMClient\MS DM Server`
dmClientKey, err := registry.OpenKey(registry.LOCAL_MACHINE, dmClientPath, registry.QUERY_VALUE)
if err != nil {
continue
}

entDMID, _, err := dmClientKey.GetStringValue("EntDMID")
dmClientKey.Close()
if err != nil {
continue
}

if entDMID != "" {
log.Debug().Str("EntDMID", entDMID).Msg("found Intune device ID")
return entDMID, nil
}
}

return "", nil
}
23 changes: 23 additions & 0 deletions providers/os/detector/windows/intune_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

//go:build windows
// +build windows

package windows

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetIntuneDeviceIDFromRegistry(t *testing.T) {
// This test runs on Windows and reads the real registry.
// On machines without Intune enrollment, it should return an empty string without error.
// On Intune-enrolled machines, it should return the device ID.
id, err := getIntuneDeviceIDFromRegistry()
require.NoError(t, err)
assert.IsType(t, "", id)
}
Loading