From 607601b7d1c7bff1a7a434207fb2192bb63602a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Wed, 7 Oct 2026 23:29:16 +0200 Subject: [PATCH] logical_disk: read BitLocker status through fveapi.dll (#2533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Opus 5.5 Signed-off-by: Jan-Otto Kröpke --- docs/collector.logical_disk.md | 27 +- .../collector/logical_disk/logical_disk.go | 244 ++++-------------- .../logical_disk_internal_test.go | 49 ++++ internal/headers/fveapi/fveapi.go | 195 ++++++++++++++ internal/headers/fveapi/fveapi_test.go | 85 ++++++ internal/headers/propsys/propsys.go | 55 ---- internal/headers/shell32/shell32.go | 103 -------- internal/headers/shell32/types.go | 51 ---- 8 files changed, 408 insertions(+), 401 deletions(-) create mode 100644 internal/collector/logical_disk/logical_disk_internal_test.go create mode 100644 internal/headers/fveapi/fveapi.go create mode 100644 internal/headers/fveapi/fveapi_test.go delete mode 100644 internal/headers/propsys/propsys.go delete mode 100644 internal/headers/shell32/shell32.go delete mode 100644 internal/headers/shell32/types.go diff --git a/docs/collector.logical_disk.md b/docs/collector.logical_disk.md index 1922083f..462583af 100644 --- a/docs/collector.logical_disk.md +++ b/docs/collector.logical_disk.md @@ -13,11 +13,11 @@ The logical_disk collector exposes metrics about logical disks (in contrast to p ### `--collector.logical_disk.volume-include` -If given, a disk needs to match the include regexp in order for the corresponding disk metrics to be reported +If given, a disk needs to match the include regular expression in order for the corresponding disk metrics to be reported ### `--collector.logical_disk.volume-exclude` -If given, a disk needs to *not* match the exclude regexp in order for the corresponding disk metrics to be reported +If given, a disk needs to *not* match the exclude regular expression in order for the corresponding disk metrics to be reported ### `--collector.logical_disk.enabled` @@ -46,7 +46,28 @@ Comma-separated list of collectors to use. Available collectors: metrics, bitloc ### Warning about size metrics The `free_bytes` and `size_bytes` metrics are not updated in real time and might have a delay of 10-15min. -This is the same behavior as the windows performance counters. +This is the same behavior as the Windows performance counters. + +### BitLocker status +The `bitlocker_status` sub-collector reads the BitLocker status through `fveapi.dll`, the BitLocker API of Windows. +It works when windows_exporter runs as a service (`LocalSystem`) and as a non-elevated user. +The API is undocumented, so the status is derived from reverse-engineered flags. + +The `status` label has one of these values: + +| Status | Meaning | +|--------------------------|---------------------------------------------------------------------------| +| `on` | The volume is fully encrypted and protection is active | +| `off` | The volume is not encrypted | +| `encrypting` | Encryption is in progress or paused | +| `decrypting` | Decryption is in progress or paused | +| `suspended` | The volume is fully encrypted, but protection is suspended | +| `locked` | The volume is locked | +| `waiting_for_activation` | The volume is encrypted with a clear key, but no key protector exists yet | +| `unknown` | The status flags do not match a known state | +| `disabled` | Not reported anymore, always 0. Kept for compatibility | + +A fully encrypted volume is only reported as `on` if protection is active. ### Example metric Query the rate of write operations to a disk diff --git a/internal/collector/logical_disk/logical_disk.go b/internal/collector/logical_disk/logical_disk.go index 29e84b7d..cfc5a00d 100644 --- a/internal/collector/logical_disk/logical_disk.go +++ b/internal/collector/logical_disk/logical_disk.go @@ -24,17 +24,13 @@ import ( "fmt" "log/slog" "regexp" - "runtime" - "runtime/debug" "slices" "strconv" "strings" "time" "github.com/alecthomas/kingpin/v2" - "github.com/go-ole/go-ole" - "github.com/prometheus-community/windows_exporter/internal/headers/propsys" - "github.com/prometheus-community/windows_exporter/internal/headers/shell32" + "github.com/prometheus-community/windows_exporter/internal/headers/fveapi" "github.com/prometheus-community/windows_exporter/internal/mi" "github.com/prometheus-community/windows_exporter/internal/pdh" "github.com/prometheus-community/windows_exporter/internal/types" @@ -46,10 +42,29 @@ const ( Name = "logical_disk" subCollectorMetrics = "metrics" subCollectorBitlocker = "bitlocker_status" - - bitlockerStatusLocked = 6 ) +// bitlockerStatusLabels lists the values of the status label of the bitlocker_status metric. +// The order and the values originate from the Shell property System.Volume.BitLockerProtection, +// which was used before fveapi.dll. The labels are kept to not break existing queries. +// "disabled" is never reported by fveapi.dll and always 0. +// +//nolint:gochecknoglobals +var bitlockerStatusLabels = []struct { + state fveapi.State + label string +}{ + {-1, "disabled"}, + {fveapi.StateOn, "on"}, + {fveapi.StateOff, "off"}, + {fveapi.StateEncrypting, "encrypting"}, + {fveapi.StateDecrypting, "decrypting"}, + {fveapi.StateSuspended, "suspended"}, + {fveapi.StateLocked, "locked"}, + {fveapi.StateUnknown, "unknown"}, + {fveapi.StateWaitingForActivation, "waiting_for_activation"}, +} + type Config struct { CollectorsEnabled []string `yaml:"enabled"` VolumeInclude *regexp.Regexp `yaml:"volume-include"` @@ -73,14 +88,6 @@ type Collector struct { perfDataCollector *pdh.Collector perfDataObject []perfDataCounterValues - bitlockerReqCh chan string - bitlockerResCh chan struct { - err error - status int - } - - ctxCancelFunc context.CancelFunc - avgReadQueue *prometheus.Desc avgWriteQueue *prometheus.Desc freeSpace *prometheus.Desc @@ -184,10 +191,6 @@ func (c *Collector) GetName() string { } func (c *Collector) Close() error { - if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) { - c.ctxCancelFunc() - } - return nil } @@ -340,25 +343,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { return fmt.Errorf("failed to create LogicalDisk collector: %w", err) } - if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) { - initErrCh := make(chan error) - c.bitlockerReqCh = make(chan string, 1) - c.bitlockerResCh = make(chan struct { - err error - status int - }, 1) - - ctx, cancel := context.WithCancel(context.Background()) - - c.ctxCancelFunc = cancel - - go c.workerBitlocker(ctx, initErrCh) - - if err = <-initErrCh; err != nil { - return fmt.Errorf("failed to initialize BitLocker worker: %w", err) - } - } - return nil } @@ -516,7 +500,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error } if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) { - c.collectBitlocker(ch, data.Name, false) + c.collectBitlocker(ch, volumes, data.Name, false) } } @@ -540,7 +524,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error continue } - c.collectBitlocker(ch, mountPoint, true) + c.collectBitlocker(ch, volumes, mountPoint, true) } } @@ -549,12 +533,9 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error // collectBitlocker emits the BitLocker status of a volume. With lockedOnly, // volumes in any other state are skipped. -func (c *Collector) collectBitlocker(ch chan<- prometheus.Metric, volume string, lockedOnly bool) { - c.bitlockerReqCh <- volume - - bitlockerStatus := <-c.bitlockerResCh - - if bitlockerStatus.err != nil { +func (c *Collector) collectBitlocker(ch chan<- prometheus.Metric, volumes map[string]string, volume string, lockedOnly bool) { + status, err := fveapi.GetStatus(getFveVolumeName(volumes, volume)) + if err != nil { level := slog.LevelWarn if lockedOnly { // Drive letters without performance data include empty media drives. @@ -562,27 +543,27 @@ func (c *Collector) collectBitlocker(ch chan<- prometheus.Metric, volume string, } c.logger.Log(context.Background(), level, "failed to get BitLocker status for "+volume, - slog.Any("err", bitlockerStatus.err), + slog.Any("err", err), ) return } - if bitlockerStatus.status == -1 { + state := status.State() + + if lockedOnly && state != fveapi.StateLocked { + return + } + + if state == fveapi.StateUnknown { c.logger.Debug("BitLocker status for "+volume+" is unknown", - slog.Int("status", bitlockerStatus.status), + slog.String("flags", fmt.Sprintf("0x%08X", status.Flags)), ) - - return } - if lockedOnly && bitlockerStatus.status != bitlockerStatusLocked { - return - } - - for i, status := range []string{"disabled", "on", "off", "encrypting", "decrypting", "suspended", "locked", "unknown", "waiting_for_activation"} { + for _, statusLabel := range bitlockerStatusLabels { val := 0.0 - if bitlockerStatus.status == i { + if state == statusLabel.state { val = 1.0 } @@ -591,11 +572,27 @@ func (c *Collector) collectBitlocker(ch chan<- prometheus.Metric, volume string, prometheus.GaugeValue, val, volume, - status, + statusLabel.label, ) } } +// getFveVolumeName returns the name of a volume in the Win32 device namespace, +// as required by fveapi.dll. +func getFveVolumeName(volumes map[string]string, volume string) string { + // The volume GUID path (\\?\Volume{GUID}) is known for every mounted volume, including mount points. + if volumeGUID, ok := volumes[volume]; ok { + return `\\.\` + strings.TrimPrefix(volumeGUID, `\\?\`) + } + + // Volumes without a mount point are reported by their device name, e.g. HarddiskVolume1. + if strings.HasPrefix(volume, "HarddiskVolume") { + return `\\.\GLOBALROOT\Device\` + volume + } + + return `\\.\` + volume +} + func getDriveType(driveType uint32) string { switch driveType { case windows.DRIVE_UNKNOWN: @@ -769,134 +766,3 @@ func getAllMountedVolumes() (map[string]string, error) { volumes[strings.TrimSuffix(mountPoint, `\`)] = strings.TrimSuffix(windows.UTF16ToString(guidBuf), `\`) } } - -/* -++ References - -| System.Volume. | Control Panel | manage-bde conversion | manage-bde | Get-BitlockerVolume | Get-BitlockerVolume | -| BitLockerProtection | | | protection | VolumeStatus | ProtectionStatus | -| ------------------- | -------------------------------- | ------------------------- | -------------- | ---------------------------- | ------------------- | -| 1 | BitLocker on | Used Space Only Encrypted | Protection On | FullyEncrypted | On | -| 1 | BitLocker on | Fully Encrypted | Protection On | FullyEncrypted | On | -| 1 | BitLocker on | Fully Encrypted | Protection On | FullyEncryptedWipeInProgress | On | -| 2 | BitLocker off | Fully Decrypted | Protection Off | FullyDecrypted | Off | -| 3 | BitLocker Encrypting | Encryption In Progress | Protection Off | EncryptionInProgress | Off | -| 3 | BitLocker Encryption Paused | Encryption Paused | Protection Off | EncryptionSuspended | Off | -| 4 | BitLocker Decrypting | Decryption in progress | Protection Off | DecyptionInProgress | Off | -| 4 | BitLocker Decryption Paused | Decryption Paused | Protection Off | DecryptionSuspended | Off | -| 5 | BitLocker suspended | Used Space Only Encrypted | Protection Off | FullyEncrypted | Off | -| 5 | BitLocker suspended | Fully Encrypted | Protection Off | FullyEncrypted | Off | -| 6 | BitLocker on (Locked) | Unknown | Unknown | $null | Unknown | -| 7 | | | | | | -| 8 | BitLocker waiting for activation | Used Space Only Encrypted | Protection Off | FullyEncrypted | Off | - --- -*/ -func (c *Collector) workerBitlocker(ctx context.Context, initErrCh chan<- error) { - defer func() { - if r := recover(); r != nil { - c.logger.ErrorContext(ctx, "workerBitlocker panic", - slog.Any("panic", r), - slog.String("stack", string(debug.Stack())), - ) - - // Restart the workerBitlocker - initErrCh := make(chan error) - - go c.workerBitlocker(ctx, initErrCh) - - if err := <-initErrCh; err != nil { - c.logger.ErrorContext(ctx, "workerBitlocker restart failed", - slog.Any("err", err), - ) - } - } - }() - - // The only way to run WMI queries in parallel while being thread-safe is to - // ensure the CoInitialize[Ex]() call is bound to its current OS thread. - // Otherwise, attempting to initialize and run parallel queries across - // goroutines will result in protected memory errors. - runtime.LockOSThread() - - defer runtime.UnlockOSThread() - - if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE); err != nil { - var oleCode *ole.OleError - if errors.As(err, &oleCode) && oleCode.Code() != ole.S_OK && oleCode.Code() != 0x00000001 { - initErrCh <- fmt.Errorf("CoInitializeEx: %w", err) - - return - } - } - - defer ole.CoUninitialize() - - var pkey propsys.PROPERTYKEY - - // The ideal solution to check the disk encryption (BitLocker) status is to - // use the WMI APIs (Win32_EncryptableVolume). However, only programs running - // with elevated priledges can access those APIs. - // - // Our alternative solution is based on the value of the undocumented (shell) - // property: "System.Volume.BitLockerProtection". That property is essentially - // an enum containing the current BitLocker status for a given volume. This - // approached was suggested here: - // https://stackoverflow.com/questions/41308245/detect-bitlocker-programmatically-from-c-sharp-without-admin/41310139 - // - // Note that the link above doesn't give any explanation / meaning for the - // enum values, it simply says that 1, 3 or 5 means the disk is encrypted. - // - // I directly tested and validated this strategy on a Windows 10 machine. - // The values given in the BitLockerStatus enum contain the relevant values - // for the shell property. I also directly validated them. - if err := propsys.PSGetPropertyKeyFromName("System.Volume.BitLockerProtection", &pkey); err != nil { - initErrCh <- fmt.Errorf("PSGetPropertyKeyFromName failed: %w", err) - - return - } - - close(initErrCh) - - for { - select { - case <-ctx.Done(): - return - case path, ok := <-c.bitlockerReqCh: - if !ok { - return - } - - if !strings.Contains(path, `:`) { - c.bitlockerResCh <- struct { - err error - status int - }{err: nil, status: -1} - - continue - } - - status, err := func(path string) (int, error) { - item, err := shell32.SHCreateItemFromParsingName(path) - if err != nil { - return -1, fmt.Errorf("SHCreateItemFromParsingName failed: %w", err) - } - - defer item.Release() - - var v ole.VARIANT - - if err := item.GetProperty(&pkey, &v); err != nil { - return -1, fmt.Errorf("GetProperty failed: %w", err) - } - - return int(v.Val), v.Clear() - }(path) - - c.bitlockerResCh <- struct { - err error - status int - }{err: err, status: status} - } - } -} diff --git a/internal/collector/logical_disk/logical_disk_internal_test.go b/internal/collector/logical_disk/logical_disk_internal_test.go new file mode 100644 index 00000000..21799303 --- /dev/null +++ b/internal/collector/logical_disk/logical_disk_internal_test.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package logical_disk + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetFveVolumeName(t *testing.T) { + t.Parallel() + + volumes := map[string]string{ + "C:": `\\?\Volume{eeb47cb6-f390-4b7b-a107-3d70152440a4}`, + `C:\MOUNT1`: `\\?\Volume{219d1d11-da09-4d2c-86eb-7ac13f10f127}`, + } + + for _, tc := range []struct { + volume string + want string + }{ + {volume: "C:", want: `\\.\Volume{eeb47cb6-f390-4b7b-a107-3d70152440a4}`}, + {volume: `C:\MOUNT1`, want: `\\.\Volume{219d1d11-da09-4d2c-86eb-7ac13f10f127}`}, + {volume: "HarddiskVolume4", want: `\\.\GLOBALROOT\Device\HarddiskVolume4`}, + {volume: "Z:", want: `\\.\Z:`}, + } { + t.Run(tc.volume, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tc.want, getFveVolumeName(volumes, tc.volume)) + }) + } +} diff --git a/internal/headers/fveapi/fveapi.go b/internal/headers/fveapi/fveapi.go new file mode 100644 index 00000000..4c67fa22 --- /dev/null +++ b/internal/headers/fveapi/fveapi.go @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +// Package fveapi reads the BitLocker status of a volume through fveapi.dll. +// +// fveapi.dll is the private user-mode BitLocker (Full Volume Encryption) API. +// It is undocumented and Microsoft gives no ABI guarantee, so this package +// only uses the oldest and smallest surface: FveGetStatusW with the +// version 1 FVE_STATUS structure. The DLL is loaded lazily, and a missing +// DLL or export results in an error instead of a crash. +// +// Why not the Shell property System.Volume.BitLockerProtection: the Shell +// returns VT_EMPTY for built-in service accounts (LocalSystem, virtual +// service accounts), so it does not work for windows_exporter running as a +// service (https://github.com/prometheus-community/windows_exporter/issues/2290). +// FveGetStatusW returns the same result for LocalSystem and for a +// non-elevated user. +// +// Volume names must use the Win32 device namespace, for example +// `\\.\C:`, `\\.\Volume{GUID}` or `\\.\GLOBALROOT\Device\HarddiskVolume1`. +// A bare `C:` fails with E_ACCESSDENIED. +// +// The flag values come from the reverse-engineered header of KNSoft.NDK (MIT): +// https://github.com/KNSoft/KNSoft.NDK/blob/main/Source/Include/KNSoft/NDK/Win32/FVE/FveApi.h +// +// Verified on Windows 11 (build 26300), as LocalSystem and as a non-elevated user, +// against Win32_EncryptableVolume GetProtectionStatus/GetConversionStatus/GetLockStatus +// and manage-bde -status. Flags per state: +// +// State Flags WMI Protection/Conversion/Lock +// fully decrypted 0x00000004 0/0/0 +// encryption paused 0x000401A1 0/4/0 +// encryption running 0x00042121 0/2/0 +// protection on 0x00041109 1/1/0 (OS volume with TPM: 0x00045309) +// protection suspended 0x00040509 0/1/0 +// locked 0x00041909 2/-/1 +// decryption paused 0x00040191 0/5/0 +// decryption running 0x00042111 0/3/0 +// EFI and recovery 0x00000004 - +// non-existent volume HRESULT 0x80070002 +// +// A suspended volume stays FULLY_ENCRYPTED; only PROTECTION_ACTIVE is cleared. +// A locked volume still reports FULLY_ENCRYPTED and PROTECTION_ACTIVE, so LOCKED +// has to be checked first. 0x80 marks a paused conversion and 0x2000 a running one; +// both are not needed for the state. During decryption, ConvertedPercent is the +// decrypted share of the volume. +// +// Not verified: waiting for activation (clear key without key protectors). +package fveapi + +import ( + "errors" + "fmt" + "unsafe" + + "golang.org/x/sys/windows" +) + +//nolint:gochecknoglobals +var ( + modFveapi = windows.NewLazySystemDLL("fveapi.dll") + procFveGetStatusW = modFveapi.NewProc("FveGetStatusW") +) + +// FVE_STATUS_FLAG_* values of the Flags field. +const ( + StatusFlagInitialized uint32 = 0x00000001 + StatusFlagFullyDecrypted uint32 = 0x00000004 + StatusFlagFullyEncrypted uint32 = 0x00000008 + StatusFlagDecryptionInProgress uint32 = 0x00000010 + StatusFlagEncryptionInProgress uint32 = 0x00000020 + StatusFlagConversionPausedMask uint32 = 0x000000C0 + StatusFlagNonTPMProtector uint32 = 0x00000100 + StatusFlagTPMProtector uint32 = 0x00000200 + StatusFlagClearKey uint32 = 0x00000400 + StatusFlagLocked uint32 = 0x00000800 + StatusFlagProtectionActive uint32 = 0x00001000 + StatusFlagOSVolume uint32 = 0x00004000 +) + +// statusV1 is the version 1 FVE_STATUS structure (32 bytes). +// The padding fields keep the layout independent of the platform alignment of float64. +type statusV1 struct { + StructureSize uint32 + StructureVersion uint32 + Flags uint32 + _ uint32 + ConvertedPercent float64 + LastConvertStatus int32 + _ uint32 +} + +// Status is the BitLocker status of a volume as reported by FveGetStatusW. +type Status struct { + Flags uint32 + ConvertedPercent float64 +} + +// State is the BitLocker state derived from Status.Flags. +type State int + +const ( + StateUnknown State = iota + StateOff + StateOn + StateEncrypting + StateDecrypting + StateSuspended + StateLocked + StateWaitingForActivation +) + +var ErrUnavailable = errors.New("fveapi.dll or FveGetStatusW is not available") + +// GetStatus returns the BitLocker status of volumeName, e.g. `\\.\C:`. +func GetStatus(volumeName string) (Status, error) { + if err := procFveGetStatusW.Find(); err != nil { + return Status{}, fmt.Errorf("%w: %w", ErrUnavailable, err) + } + + volumeNamePtr, err := windows.UTF16PtrFromString(volumeName) + if err != nil { + return Status{}, fmt.Errorf("invalid volume name %q: %w", volumeName, err) + } + + status := statusV1{ + StructureSize: uint32(unsafe.Sizeof(statusV1{})), + StructureVersion: 1, + } + + hr, _, _ := procFveGetStatusW.Call( + uintptr(unsafe.Pointer(volumeNamePtr)), + uintptr(unsafe.Pointer(&status)), + ) + + // FveGetStatusW returns an HRESULT. Only the sign bit indicates failure. + if int32(hr) < 0 { + // HRESULT_FROM_WIN32 (facility 7) wraps a Win32 error code, e.g. 0x80070002. + if uint32(hr)&0xFFFF0000 == 0x80070000 { + return Status{}, fmt.Errorf("FveGetStatusW(%s) failed: %w", volumeName, windows.Errno(uint32(hr)&0xFFFF)) + } + + return Status{}, fmt.Errorf("FveGetStatusW(%s) failed: HRESULT 0x%08X", volumeName, uint32(hr)) + } + + return Status{ + Flags: status.Flags, + ConvertedPercent: status.ConvertedPercent, + }, nil +} + +// State maps the status flags to a BitLocker state. +// A fully encrypted volume is only reported as on if protection is active, +// because a suspended volume stays fully encrypted with protection off. +// Flag combinations that do not match a known state return StateUnknown +// instead of being reported as off. +func (s Status) State() State { + flags := s.Flags + + switch { + case flags&StatusFlagLocked != 0: + return StateLocked + case flags&StatusFlagFullyDecrypted != 0: + return StateOff + case flags&StatusFlagInitialized == 0: + return StateUnknown + case flags&StatusFlagEncryptionInProgress != 0: + return StateEncrypting + case flags&StatusFlagDecryptionInProgress != 0: + return StateDecrypting + case flags&StatusFlagFullyEncrypted == 0: + return StateUnknown + case flags&StatusFlagProtectionActive != 0: + return StateOn + case flags&StatusFlagClearKey != 0 && flags&(StatusFlagTPMProtector|StatusFlagNonTPMProtector) == 0: + // Encrypted with a clear key, before any key protector was added. + return StateWaitingForActivation + default: + return StateSuspended + } +} diff --git a/internal/headers/fveapi/fveapi_test.go b/internal/headers/fveapi/fveapi_test.go new file mode 100644 index 00000000..caae651a --- /dev/null +++ b/internal/headers/fveapi/fveapi_test.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package fveapi + +import ( + "os" + "testing" + "unsafe" + + "github.com/stretchr/testify/require" +) + +func TestStatusV1Layout(t *testing.T) { + t.Parallel() + + // FVE_STATUS version 1 is 32 bytes with Flags at offset 8 and ConvertedPercent at offset 16. + require.Equal(t, uintptr(32), unsafe.Sizeof(statusV1{})) + require.Equal(t, uintptr(8), unsafe.Offsetof(statusV1{}.Flags)) + require.Equal(t, uintptr(16), unsafe.Offsetof(statusV1{}.ConvertedPercent)) + require.Equal(t, uintptr(24), unsafe.Offsetof(statusV1{}.LastConvertStatus)) +} + +func TestStatusState(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + flags uint32 + want State + }{ + // Flags observed on Windows 11 and compared with Win32_EncryptableVolume and manage-bde. + {name: "fully decrypted", flags: 0x00000004, want: StateOff}, + {name: "encryption paused", flags: 0x000401A1, want: StateEncrypting}, + {name: "encryption running", flags: 0x00042121, want: StateEncrypting}, + {name: "protection on", flags: 0x00041109, want: StateOn}, + {name: "protection on, OS volume with TPM", flags: 0x00045309, want: StateOn}, + {name: "protection suspended", flags: 0x00040509, want: StateSuspended}, + {name: "locked", flags: 0x00041909, want: StateLocked}, + {name: "decryption paused", flags: 0x00040191, want: StateDecrypting}, + {name: "decryption running", flags: 0x00042111, want: StateDecrypting}, + + // Derived from the flag names, not observed yet. + {name: "waiting for activation", flags: 0x00000409, want: StateWaitingForActivation}, + + // Unexpected combinations must not be reported as off. + {name: "no flags", flags: 0, want: StateUnknown}, + {name: "initialized only", flags: StatusFlagInitialized, want: StateUnknown}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tc.want, Status{Flags: tc.flags}.State()) + }) + } +} + +func TestGetStatus(t *testing.T) { + t.Parallel() + + systemDrive := os.Getenv("SystemDrive") + require.NotEmpty(t, systemDrive) + + status, err := GetStatus(`\\.\` + systemDrive) + require.NoError(t, err) + require.NotEqual(t, StateUnknown, status.State(), "unexpected flags 0x%08X", status.Flags) + + _, err = GetStatus(`\\.\Volume{00000000-0000-0000-0000-000000000000}`) + require.Error(t, err) + require.NotErrorIs(t, err, ErrUnavailable) +} diff --git a/internal/headers/propsys/propsys.go b/internal/headers/propsys/propsys.go deleted file mode 100644 index 11be3609..00000000 --- a/internal/headers/propsys/propsys.go +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build windows - -package propsys - -import ( - "fmt" - "unsafe" - - "github.com/go-ole/go-ole" - "golang.org/x/sys/windows" -) - -//nolint:gochecknoglobals -var ( - modPropsys = windows.NewLazySystemDLL("propsys.dll") - procPSGetPropertyKeyFromName = modPropsys.NewProc("PSGetPropertyKeyFromName") -) - -type PROPERTYKEY struct { - Fmtid ole.GUID - Pid uint32 -} - -func PSGetPropertyKeyFromName(name string, key *PROPERTYKEY) error { - namePtr, err := windows.UTF16PtrFromString(name) - if err != nil { - return fmt.Errorf("failed to convert name to UTF16: %w", err) - } - - hr, _, err := procPSGetPropertyKeyFromName.Call( - uintptr(unsafe.Pointer(namePtr)), - uintptr(unsafe.Pointer(key)), - ) - - if hr != 0 { - return fmt.Errorf("PSGetPropertyKeyFromName failed: %w", err) - } - - return nil -} diff --git a/internal/headers/shell32/shell32.go b/internal/headers/shell32/shell32.go deleted file mode 100644 index 17c14769..00000000 --- a/internal/headers/shell32/shell32.go +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build windows - -package shell32 - -import ( - "errors" - "fmt" - "syscall" - "unsafe" - - "github.com/go-ole/go-ole" - "github.com/prometheus-community/windows_exporter/internal/headers/propsys" - "golang.org/x/sys/windows" -) - -//nolint:gochecknoglobals -var ( - modShell32 = windows.NewLazySystemDLL("shell32.dll") - procSHCreateItemFromParsingName = modShell32.NewProc("SHCreateItemFromParsingName") - - iidIShellItem2 = ole.NewGUID("{7E9FB0D3-919F-4307-AB2E-9B1860310C93}") -) - -// SHCreateItemFromParsingName creates and initializes a Shell item object from a parsing name. -// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-shcreateitemfromparsingname -func SHCreateItemFromParsingName(path string) (*IShellItem2, error) { - ptrPath, err := windows.UTF16PtrFromString(path) - if err != nil { - return nil, fmt.Errorf("failed to convert path to UTF16: %w", err) - } - - var result *IShellItem2 - - hr, _, _ := procSHCreateItemFromParsingName.Call( - uintptr(unsafe.Pointer(ptrPath)), - 0, - uintptr(unsafe.Pointer(iidIShellItem2)), - uintptr(unsafe.Pointer(&result)), - ) - - // procSHCreateItemFromParsingName.Call returns an HRESULT as its first value. - // Only the sign bit indicates failure (the FAILED macro); success codes such - // as S_OK (0) and S_FALSE (1) must not be treated as errors. The HRESULT, not - // GetLastError, carries the COM error information. - if int32(hr) < 0 { - return nil, fmt.Errorf("SHCreateItemFromParsingName failed: %w", ole.NewError(hr)) - } - - if result == nil { - return nil, errors.New("SHCreateItemFromParsingName returned nil") - } - - return result, nil -} - -func (item *IShellItem2) GetProperty(key *propsys.PROPERTYKEY, v *ole.VARIANT) error { - hr, _, _ := syscall.SyscallN( - item.lpVtbl.GetProperty, - uintptr(unsafe.Pointer(item)), - uintptr(unsafe.Pointer(key)), - uintptr(unsafe.Pointer(v)), - ) - - // GetProperty is a COM method whose return value is an HRESULT. Only the sign - // bit indicates failure (the FAILED macro); success codes such as S_OK (0) - // and S_FALSE (1) must not be treated as errors. The third return value of - // SyscallN is GetLastError, which COM methods do not set, so it would report - // stale, unrelated errors (e.g. "Too many posts were made to a semaphore"). - if int32(hr) < 0 { - return fmt.Errorf("GetProperty failed: %w", ole.NewError(hr)) - } - - // A successful property lookup may still return VT_EMPTY when the Shell - // property is unavailable. Keep that distinct from a real numeric zero, - // which the BitLocker collector maps to the "disabled" state. - if v.VT == ole.VT_EMPTY { - v.Val = -1 - } - - return nil -} - -func (item *IShellItem2) Release() { - _, _, _ = syscall.SyscallN( - item.lpVtbl.Release, - uintptr(unsafe.Pointer(item)), - ) -} diff --git a/internal/headers/shell32/types.go b/internal/headers/shell32/types.go deleted file mode 100644 index 1ef005a1..00000000 --- a/internal/headers/shell32/types.go +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build windows - -package shell32 - -type IShellItem2 struct { - lpVtbl *IShellItem2Vtbl -} - -type IShellItem2Vtbl struct { - // IUnknown - QueryInterface uintptr - AddRef uintptr - Release uintptr - - // IShellItem - BindToHandler uintptr - GetParent uintptr - GetDisplayName uintptr - GetAttributes uintptr - Compare uintptr - - // IShellItem2 - GetPropertyStore uintptr - GetPropertyStoreWithCreateObject uintptr - GetPropertyStoreForKeys uintptr - GetPropertyDescriptionList uintptr - Update uintptr - GetProperty uintptr - GetCLSID uintptr - GetFileTime uintptr - GetInt32 uintptr - GetString uintptr - GetUInt32 uintptr - GetUInt64 uintptr - GetBool uintptr -}