logical_disk: read BitLocker status through fveapi.dll (#2533)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-07 23:29:16 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 92483242ba
commit 607601b7d1
8 changed files with 408 additions and 401 deletions
+24 -3
View File
@@ -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
+55 -189
View File
@@ -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}
}
}
}
@@ -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))
})
}
}
+195
View File
@@ -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
}
}
+85
View File
@@ -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)
}
-55
View File
@@ -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
}
-103
View File
@@ -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)),
)
}
-51
View File
@@ -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
}