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: 2 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ draid
dsse
eas
Ecmp
efi
eip
ekm
ekus
Expand Down Expand Up @@ -228,6 +229,7 @@ Sas
sbom
scim
scm
secureboot
SECRETID
secretmanager
SECRETVALUE
Expand Down
10 changes: 10 additions & 0 deletions providers/os/resources/os.lr
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@ machine.cpu @defaults("manufacturer model processorCount coreCount") {
coreCount int
}

// Secure Boot status (Linux only, reads from EFI variables)
machine.secureboot @defaults("enabled") {
// Whether the system is booted in EFI/UEFI mode
efi() bool
// Whether Secure Boot is enabled
enabled() bool
// Whether Secure Boot is in setup mode (keys can be modified without authentication)
setupMode() bool
}

// Operating system information
os {
// Pretty hostname on macOS/Linux or device name on Windows
Expand Down
95 changes: 95 additions & 0 deletions providers/os/resources/os.lr.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions providers/os/resources/os.lr.versions
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,10 @@ machine.cpu.coreCount 13.2.3
machine.cpu.manufacturer 13.2.3
machine.cpu.model 13.2.3
machine.cpu.processorCount 13.2.3
machine.secureboot 13.6.2
machine.secureboot.efi 13.6.2
machine.secureboot.enabled 13.6.2
machine.secureboot.setupMode 13.6.2
machine.system 9.0.0
machine.system.family 9.0.0
machine.system.manufacturer 9.0.0
Expand Down
84 changes: 84 additions & 0 deletions providers/os/resources/secureboot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright Mondoo, Inc. 2024, 2026
// SPDX-License-Identifier: BUSL-1.1

package resources

import (
"sync"

"github.com/spf13/afero"
"go.mondoo.com/mql/v13/providers/os/connection/shared"
)

// EFI variable GUID for global Secure Boot variables.
const efiGlobalVariable = "8be4df61-93ca-11d2-aa0d-00e098032b8c"

type mqlMachineSecurebootInternal struct {
once sync.Once
cacheEfi bool
cacheEnabled bool
cacheSetupMode bool
fetchErr error
}

func (s *mqlMachineSecureboot) id() (string, error) {
return "machine.secureboot", nil
}

// fetchStatus reads the EFI firmware variables once and caches the result.
func (s *mqlMachineSecureboot) fetchStatus() error {
s.once.Do(func() {
conn := s.MqlRuntime.Connection.(shared.Connection)
fs := conn.FileSystem()

// Check if the system is booted in EFI mode by looking for /sys/firmware/efi.
_, err := fs.Stat("/sys/firmware/efi")
if err != nil {
// No EFI directory means legacy BIOS boot — no Secure Boot possible.
return
}
s.cacheEfi = true

s.cacheEnabled = readEfiVarBool(fs, "SecureBoot-"+efiGlobalVariable)
s.cacheSetupMode = readEfiVarBool(fs, "SetupMode-"+efiGlobalVariable)
})
return s.fetchErr
}

// readEfiVarBool reads an EFI variable from /sys/firmware/efi/efivars/ and
// returns true if its data byte is 1. EFI variable files contain a 4-byte
// attribute header followed by the variable data.
func readEfiVarBool(fs afero.Fs, name string) bool {
data, err := afero.ReadFile(fs, "/sys/firmware/efi/efivars/"+name)
if err != nil {
return false
}
// Must have at least 4 bytes of attributes + 1 byte of data.
if len(data) < 5 {
return false
}
// The data portion starts after the 4-byte EFI variable attributes header.
// For SecureBoot/SetupMode the data is a single uint8: 1 = on, 0 = off.
return data[4] == 1
}

func (s *mqlMachineSecureboot) efi() (bool, error) {
if err := s.fetchStatus(); err != nil {
return false, err
}
return s.cacheEfi, nil
}

func (s *mqlMachineSecureboot) enabled() (bool, error) {
if err := s.fetchStatus(); err != nil {
return false, err
}
return s.cacheEnabled, nil
}

func (s *mqlMachineSecureboot) setupMode() (bool, error) {
if err := s.fetchStatus(); err != nil {
return false, err
}
return s.cacheSetupMode, nil
}
48 changes: 48 additions & 0 deletions providers/os/resources/secureboot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright Mondoo, Inc. 2024, 2026
// SPDX-License-Identifier: BUSL-1.1

package resources

import (
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)

func TestReadEfiVarBool(t *testing.T) {
t.Run("enabled variable", func(t *testing.T) {
fs := afero.NewMemMapFs()
// 4-byte attribute header + 1-byte data (0x01 = enabled)
err := afero.WriteFile(fs, "/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c",
[]byte{0x06, 0x00, 0x00, 0x00, 0x01}, 0o444)
assert.NoError(t, err)

assert.True(t, readEfiVarBool(fs, "SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"))
})

t.Run("disabled variable", func(t *testing.T) {
fs := afero.NewMemMapFs()
// 4-byte attribute header + 1-byte data (0x00 = disabled)
err := afero.WriteFile(fs, "/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c",
[]byte{0x06, 0x00, 0x00, 0x00, 0x00}, 0o444)
assert.NoError(t, err)

assert.False(t, readEfiVarBool(fs, "SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"))
})

t.Run("missing variable", func(t *testing.T) {
fs := afero.NewMemMapFs()
assert.False(t, readEfiVarBool(fs, "SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"))
})

t.Run("truncated file", func(t *testing.T) {
fs := afero.NewMemMapFs()
// Only 3 bytes — too short to contain attributes + data
err := afero.WriteFile(fs, "/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c",
[]byte{0x06, 0x00, 0x00}, 0o444)
assert.NoError(t, err)

assert.False(t, readEfiVarBool(fs, "SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"))
})
}
Loading