diff --git a/AGENTS.md b/AGENTS.md index 79ec3d0..016ed04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ Two consequences: - **Comments**: about one line. Docstrings one to three. A comment states the constraint, not the obvious. - **Never report success from an exit code alone.** Read the state back and report what it says: the - BitLocker recovery key is read off the volume, the trusted designation with `fsutil devdrv query`, + BitLocker recovery key is read off the volume, the trusted designation off the volume's own flags, writability by writing a file, the deduplication settings with `Get-ReFSDedupSchedule`. - **Never print a value the user did not choose.** A default assigned before a question is asked ends up displayed as a setting somebody made. Leave it unset. @@ -63,6 +63,11 @@ Two consequences: resource, so an English phrase cannot be matched on a localized Windows: report what it said rather than judging it. Values from .NET are safe — enum member names are compiled identifiers — but rendering any value to text uses the machine's regional settings. +- **The Dev Drive designation is the exception, because it is not text.** `FSCTL_QUERY_PERSISTENT_VOLUME_STATE` + answers `PERSISTENT_VOLUME_STATE_DEV_VOLUME` and `PERSISTENT_VOLUME_STATE_TRUSTED_VOLUME`, which + follow `fsutil devdrv trust` and `untrust` exactly and read the same in every language. `fsutil` + output is read only where that call itself fails. Its exit code is worthless: `fsutil devdrv query` + exits 0 for a trusted volume, an untrusted one and plain NTFS alike. - **A ReFS deduplication task is named after its volume.** `Set-ReFSDedupSchedule` registers it under `\Microsoft\Windows\ReFsDedupSvc\` as the volume's `UniqueId` GUID, braced and upper case; the scrub task adds `-Scrub`. Measured on three volumes across separate disks, one of them on an MBR disk. diff --git a/README.md b/README.md index 2d69e63..0c063a4 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Are you ready to proceed with Dev Drive creation? (yes/no): The plan says what will happen on *this* machine, not what usually happens: the BitLocker lines name what this machine can actually carry, and the schedule lines name the times you settled on. -After the drive exists, every setting is read back off the volume and reported - the trusted designation with `fsutil devdrv query`, the name and the deduplication settings from the volume itself - rather than assumed from commands that did not complain. The run ends by triggering a first optimization job and tells you to let it finish; it can take a while, and closing the window early leaves the drive unoptimized. +After the drive exists, every setting is read back off the volume and reported - the trusted designation off the volume's own flags, the name and the deduplication settings from the volume itself - rather than assumed from commands that did not complain. The run ends by triggering a first optimization job and tells you to let it finish; it can take a while, and closing the window early leaves the drive unoptimized. ## Caveats diff --git a/dev_drive.Tests.ps1 b/dev_drive.Tests.ps1 index 68562ed..556302d 100644 --- a/dev_drive.Tests.ps1 +++ b/dev_drive.Tests.ps1 @@ -364,14 +364,40 @@ Describe 'The script itself' { $content | Should -Match '(?ms)fsutil devdrv trust /f "\$devLetterColon" \| Out-Null(?:\s*\r?\n\s*#[^\r\n]*)*\s*\r?\n\s*\$trustExitCode = \$LASTEXITCODE' } - It 'asks the volume for its trust state as well as reading the exit code' { + It 'reads the volume flags after marking it trusted, never before' { $content = Get-Content -Path $script:ScriptPath -Raw - $codeAt = $content.IndexOf('$trustExitCode = $LASTEXITCODE') - $queryAt = $content.IndexOf('$trustQuery = (fsutil devdrv query') + $trustAt = $content.IndexOf('fsutil devdrv trust /f "$devLetterColon"') + $stateAt = $content.IndexOf('$devDriveState = Get-VolumeDevDriveState') $reportAt = $content.IndexOf('$trustReport = Resolve-DevDriveTrustReport') - $codeAt | Should -BeGreaterThan 0 - $queryAt | Should -BeGreaterThan $codeAt - $reportAt | Should -BeGreaterThan $queryAt + $trustAt | Should -BeGreaterThan 0 + $stateAt | Should -BeGreaterThan $trustAt + $reportAt | Should -BeGreaterThan $stateAt + } + + It 'hands the report the volume flags, not only what fsutil printed' { + $call = @($script:Ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.CommandAst] -and + $node.GetCommandName() -eq 'Resolve-DevDriveTrustReport' + }, $true)) + $call.Count | Should -Be 1 + $call[0].Extent.Text | Should -Match '-DevDriveState \$devDriveState' + } + + It 'asks fsutil for text only where the flags could not be read' { + $guards = @($script:Ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.IfStatementAst] -and + $node.Clauses[0].Item1.Extent.Text -match '-not \$devDriveState\.Queried' + }, $true)) + $guards.Count | Should -Be 1 + $guards[0].Clauses[0].Item2.Extent.Text | Should -Match 'fsutil devdrv query' + } + + It 'does not call the run done on a volume that is not a Dev Drive' { + # Two halves of one message disagreeing is the defect this whole path exists to avoid. + $content = Get-Content -Path $script:ScriptPath -Raw + $content | Should -Match "(?ms)if \(\`$trustReport\.Outcome -eq 'NotDevDrive'\) \{.{0,300}?All done\. Dev Drive" } It 'asks the volume what it stored after the daily schedule and before the weekly one' { @@ -394,7 +420,7 @@ Describe 'The script itself' { It 'colours the trust lines by the outcome, so only a real failure is printed as one' { $content = Get-Content -Path $script:ScriptPath -Raw - $content | Should -Match "switch \(\`$trustReport\.Outcome\) \{ 'Trusted' \{ 'Green' \} 'Unconfirmed' \{ 'Gray' \} default \{ 'Yellow' \} \}" + $content | Should -Match "switch \(\`$trustReport\.Outcome\) \{ 'Trusted' \{ 'Green' \} 'Unconfirmed' \{ 'Gray' \} 'NotDevDrive' \{ 'Red' \} default \{ 'Yellow' \} \}" $content | Should -Match '(?ms)foreach \(\$line in \$trustReport\.Lines\) \{\s*\r?\n\s*Write-Host \$line -ForegroundColor \$trustColour' } @@ -3521,6 +3547,32 @@ Describe 'Resolve-BitLockerAutoUnlockReport' { } } +Describe 'Get-VolumeDevDriveState' { + It 'takes the control code and the two flags from winioctl.h, not from a sweep' { + # A sweep once settled on 141, which answers and returns zeros. + Initialize-VolumeStateInterop + [DevDriveInterop.PersistentVolume]::FSCTL_QUERY_PERSISTENT_VOLUME_STATE | Should -Be 0x0009023C + [DevDriveInterop.PersistentVolume]::PERSISTENT_VOLUME_STATE_DEV_VOLUME | Should -Be 0x00002000 + [DevDriveInterop.PersistentVolume]::PERSISTENT_VOLUME_STATE_TRUSTED_VOLUME | Should -Be 0x00004000 + } + + It 'reads a real volume and answers that the system drive is not a Dev Drive' { + # Windows cannot boot from a Dev Drive, so this holds on every machine the suite runs on. + $state = Get-VolumeDevDriveState -DriveLetter 'C' + $state.Queried | Should -BeTrue + $state.IsDevDrive | Should -BeFalse + $state.IsTrusted | Should -BeFalse + $state.Reason | Should -BeNullOrEmpty + } + + It 'answers rather than throws for a volume it cannot open, and says why' { + $state = Get-VolumeDevDriveState -DriveLetter '1' + $state.Queried | Should -BeFalse + $state.IsDevDrive | Should -BeNullOrEmpty + $state.Reason | Should -Match '\(error \d+\)' + } +} + Describe 'Resolve-DevDriveTrustReport' { BeforeAll { $script:TrustedOutput = @' @@ -3531,72 +3583,111 @@ Developer volumes are protected by antivirus filter. Filters currently attached to this developer volume: WdFilter '@ + $script:GermanOutput = 'Dies ist ein vertrauenswuerdiges Entwicklervolume.' + $script:Unread = [PSCustomObject]@{ Queried = $false; IsDevDrive = $null; IsTrusted = $null + Reason = 'Access is denied (error 5)' + } } - It 'confirms trust only when the volume itself says it is trusted' { - $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput $script:TrustedOutput + It 'confirms trust from the volume flags, in one line' { + $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput '' ` + -DevDriveState ([PSCustomObject]@{ Queried = $true; IsDevDrive = $true; IsTrusted = $true; Reason = '' }) $report.Outcome | Should -Be 'Trusted' - ($report.Lines -join "`n") | Should -Match 'X: reports itself trusted' + $report.Lines.Count | Should -Be 1 + ($report.Lines -join "`n") | Should -Match 'X: carries the trusted designation' } - It 'says nothing beyond the one line when the volume is trusted' { - (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput $script:TrustedOutput).Lines.Count | - Should -Be 1 + It 'confirms trust on a machine whose fsutil answers in another language' { + # The whole point of reading flags: this run used to end with "cannot confirm it here". + $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput $script:GermanOutput ` + -DevDriveState ([PSCustomObject]@{ Queried = $true; IsDevDrive = $true; IsTrusted = $true; Reason = '' }) + $report.Outcome | Should -Be 'Trusted' } - It 'calls it a failure only when the command itself failed, whatever the query says' { - $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 1 -QueryOutput $script:TrustedOutput - $report.Outcome | Should -Be 'Failed' + It 'believes the flags over a non-zero exit code' { + # The defect this replaces: exit code 1 was called a failure while the volume said otherwise. + $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 1 -QueryOutput $script:TrustedOutput ` + -DevDriveState ([PSCustomObject]@{ Queried = $true; IsDevDrive = $true; IsTrusted = $true; Reason = '' }) + $report.Outcome | Should -Be 'Trusted' $lines = $report.Lines -join "`n" - $lines | Should -Match 'exited with code 1' - $lines | Should -Match 'will still work' + $lines | Should -Not -Match 'Could not' + $lines | Should -Not -Match 'exit code' + $lines | Should -Not -Match 'Retry' + } + + It 'says a Dev Drive without the designation is untrusted, and how to retry' { + $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput '' ` + -DevDriveState ([PSCustomObject]@{ Queried = $true; IsDevDrive = $true; IsTrusted = $false; Reason = '' }) + $report.Outcome | Should -Be 'Untrusted' + $lines = $report.Lines -join "`n" + $lines | Should -Match 'is a Dev Drive, but' $lines | Should -Match 'Retry by hand with: fsutil devdrv trust /f X:' } - It 'reports an answer it cannot read as unconfirmed, not as a failure' -TestCases @( - @{ Answer = 'Dies ist ein vertrauenswuerdiges Entwicklervolume.' } - @{ Answer = 'This is not a developer volume.' } - @{ Answer = 'This is not a trusted developer volume.' } + It 'claims nothing about the scanner beyond the bit it read' { + # The bit says the designation is absent, not what any particular anti-virus then does. + $lines = (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput '' ` + -DevDriveState ([PSCustomObject]@{ Queried = $true; IsDevDrive = $true; IsTrusted = $false; Reason = '' })).Lines -join "`n" + $lines | Should -Not -Match 'Defender' + } + + It 'says plainly when the volume is not a Dev Drive at all, whatever the trusted bit says' -TestCases @( + @{ Case = 'no trusted bit either'; Trusted = $false } + @{ Case = 'a trusted bit without it'; Trusted = $true } ) { - $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput $Answer + # Nothing before this change could ask that question, so a failed format went unreported. + $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput '' ` + -DevDriveState ([PSCustomObject]@{ Queried = $true; IsDevDrive = $false; IsTrusted = $Trusted; Reason = '' }) + $report.Outcome | Should -Be 'NotDevDrive' + ($report.Lines -join "`n") | Should -Match 'does not carry the Dev Drive designation' + } + + It 'falls back to what fsutil said when the flags could not be read, given ' -TestCases @( + @{ Case = 'no state at all'; Unread = $false } + @{ Case = 'a state that failed to read'; Unread = $true } + ) { + $state = if ($Unread) { $script:Unread } else { $null } + $report = Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput $script:GermanOutput ` + -DevDriveState $state $report.Outcome | Should -Be 'Unconfirmed' $lines = $report.Lines -join "`n" - $lines | Should -Match 'only works in English' - $lines | Should -Match 'everything is as it should be' + $lines | Should -Match 'Could not read the Dev Drive designation off X:' + $lines | Should -Match ([regex]::Escape($script:GermanOutput)) } - It 'raises no alarm on a run where only the language stopped it reading the answer' { - # This is what every successful run looks like on a Windows that is not in English, so it - # must not read as a fault, and must not ask for anything to be done. - $lines = (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput 'Dies ist ein vertrauenswuerdiges Entwicklervolume.').Lines -join "`n" - $lines | Should -Not -Match 'could not mark' - $lines | Should -Not -Match 'will still work' - $lines | Should -Not -Match 'Retry by hand' - $lines | Should -Not -Match 'It should say' + It 'gives what Windows said about the failed read, not only a number' { + $lines = (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput 'anything' ` + -DevDriveState $script:Unread).Lines -join "`n" + $lines | Should -Match 'Access is denied \(error 5\)' } - It 'quotes what the query actually said, so the user judges it themselves' -TestCases @( - @{ Code = 0 } - @{ Code = 1 } - ) { - $lines = (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode $Code -QueryOutput 'This is not a developer volume.').Lines -join "`n" - $lines | Should -Match 'This is not a developer volume\.' + It 'reports a non-zero exit code without turning it into a verdict' { + $lines = (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 1 -QueryOutput 'anything' ` + -DevDriveState $script:Unread).Lines -join "`n" + $lines | Should -Match 'exit code 1' + $lines | Should -Match 'says nothing about the designation' + $lines | Should -Not -Match 'Could not mark' } - It 'says so plainly when the query answered nothing at all' -TestCases @( - @{ Code = 0; Expected = '\(nothing\)' } - @{ Code = 1; Expected = 'said nothing' } - ) { - $lines = (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode $Code -QueryOutput '').Lines -join "`n" - $lines | Should -Match $Expected + It 'leaves a next step on the path where nothing could be established' { + $lines = (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput 'anything' ` + -DevDriveState $script:Unread).Lines -join "`n" + $lines | Should -Match 'Retry by hand with: fsutil devdrv trust /f X:' + } + + It 'does not invite the reader to judge an answer that was never given' { + $lines = (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput '' ` + -DevDriveState $script:Unread).Lines -join "`n" + $lines | Should -Match 'said nothing either' + $lines | Should -Not -Match 'judge it yourself' } It 'returns plain lines rather than an object to unwrap' { - (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput $script:TrustedOutput).Lines | + (Resolve-DevDriveTrustReport -MountPoint 'X:' -TrustExitCode 0 -QueryOutput '' ` + -DevDriveState ([PSCustomObject]@{ Queried = $true; IsDevDrive = $true; IsTrusted = $true; Reason = '' })).Lines | Should -BeOfType [string] } } - Describe 'Test-RecoveryKeyAcknowledged' { It 'accepts ' -TestCases @( @{ Description = 'the word itself'; Answer = 'YES' } diff --git a/dev_drive.ps1 b/dev_drive.ps1 index 1ba38ba..362a04b 100644 --- a/dev_drive.ps1 +++ b/dev_drive.ps1 @@ -134,6 +134,133 @@ namespace DevDriveInterop '@ } +function Initialize-VolumeStateInterop { + <# + FSCTL_QUERY_PERSISTENT_VOLUME_STATE, the one reading of the Dev Drive designation that is not + localized text. Control code, structure and flags come from winioctl.h. + #> + if (([System.Management.Automation.PSTypeName]'DevDriveInterop.PersistentVolume').Type) { + return + } + + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace DevDriveInterop +{ + [StructLayout(LayoutKind.Sequential)] + public struct FILE_FS_PERSISTENT_VOLUME_INFORMATION + { + public UInt32 VolumeFlags; + public UInt32 FlagMask; + public UInt32 Version; + public UInt32 Reserved; + } + + public static class PersistentVolume + { + // CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 143, METHOD_BUFFERED, FILE_ANY_ACCESS). 142 is the write. + public const UInt32 FSCTL_QUERY_PERSISTENT_VOLUME_STATE = 0x0009023C; + public const UInt32 PERSISTENT_VOLUME_STATE_DEV_VOLUME = 0x00002000; + public const UInt32 PERSISTENT_VOLUME_STATE_TRUSTED_VOLUME = 0x00004000; + + private const UInt32 FILE_SHARE_READ_WRITE = 0x00000003; + private const UInt32 OPEN_EXISTING = 3; + private const UInt32 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + private const Int32 ERROR_GEN_FAILURE = 31; + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern SafeFileHandle CreateFileW(string Path, UInt32 Access, UInt32 Share, + IntPtr Security, UInt32 Disposition, UInt32 Flags, IntPtr Template); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool DeviceIoControl(SafeFileHandle Handle, UInt32 Code, + ref FILE_FS_PERSISTENT_VOLUME_INFORMATION Input, UInt32 InputSize, + ref FILE_FS_PERSISTENT_VOLUME_INFORMATION Output, UInt32 OutputSize, + out UInt32 Returned, IntPtr Overlapped); + + // True with the volume's flags in Flags, false with the Win32 error in Error. + public static bool TryQuery(string RootPath, out UInt32 Flags, out Int32 Error) + { + Flags = 0; + Error = 0; + // The root directory, opened for no access: the raw device refuses this control code. + using (SafeFileHandle handle = CreateFileW(RootPath, 0, FILE_SHARE_READ_WRITE, + IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero)) + { + if (handle.IsInvalid) + { + Error = LastError(); + return false; + } + + FILE_FS_PERSISTENT_VOLUME_INFORMATION data = new FILE_FS_PERSISTENT_VOLUME_INFORMATION(); + // Every bit: a file system refuses a mask naming flags it does not know. + data.FlagMask = 0xFFFFFFFF; + data.Version = 1; + UInt32 size = (UInt32)Marshal.SizeOf(typeof(FILE_FS_PERSISTENT_VOLUME_INFORMATION)); + UInt32 returned; + if (!DeviceIoControl(handle, FSCTL_QUERY_PERSISTENT_VOLUME_STATE, + ref data, size, ref data, size, out returned, IntPtr.Zero)) + { + Error = LastError(); + return false; + } + + // A short answer leaves VolumeFlags at what went in, which would read as no flags. + if (returned < size) + { + Error = ERROR_GEN_FAILURE; + return false; + } + + Flags = data.VolumeFlags; + return true; + } + } + + // A failure that left no last error still has to read as a failure. + private static Int32 LastError() + { + Int32 code = Marshal.GetLastWin32Error(); + return code != 0 ? code : ERROR_GEN_FAILURE; + } + } +} +'@ +} + +function Get-VolumeDevDriveState { + <# + Whether a volume is a Dev Drive and whether it is trusted, read as flags rather than as + localized text. Queried is false where the volume could not be asked, and Reason says why. + #> + param([Parameter(Mandatory)][string]$DriveLetter) + + $flags = [uint32]0 + $win32Error = 0 + try { + Initialize-VolumeStateInterop + $answered = [DevDriveInterop.PersistentVolume]::TryQuery("\\?\${DriveLetter}:\", [ref]$flags, [ref]$win32Error) + } + catch { + return [PSCustomObject]@{ Queried = $false; IsDevDrive = $null; IsTrusted = $null; Reason = $_.Exception.Message } + } + + if (-not $answered) { + return [PSCustomObject]@{ Queried = $false; IsDevDrive = $null; IsTrusted = $null; Reason = (Get-Win32ErrorText -Code $win32Error) } + } + + return [PSCustomObject]@{ + Queried = $true + IsDevDrive = ($flags -band [DevDriveInterop.PersistentVolume]::PERSISTENT_VOLUME_STATE_DEV_VOLUME) -ne 0 + IsTrusted = ($flags -band [DevDriveInterop.PersistentVolume]::PERSISTENT_VOLUME_STATE_TRUSTED_VOLUME) -ne 0 + Reason = '' + } +} + function Get-VhdxStorageType { $storageType = New-Object DevDriveInterop.VIRTUAL_STORAGE_TYPE $storageType.DeviceId = [DevDriveInterop.VirtDisk]::VIRTUAL_STORAGE_TYPE_DEVICE_VHDX @@ -866,47 +993,59 @@ function Resolve-BitLockerAutoUnlockReport { function Resolve-DevDriveTrustReport { <# - What to say after marking a volume trusted. Only fsutil's own words answer whether it is: - measured, the exit code is 0 for every real volume, the persistent volume flags read back - as zero, and no CIM property carries it. Those words are localized, so an answer this - script cannot read is shown, never judged. + What to say after marking a volume trusted. The volume's own flags answer it in any language; + fsutil's localized text is read only where those flags could not be. #> param( [Parameter(Mandatory)][string]$MountPoint, [Parameter(Mandatory)][int]$TrustExitCode, - [AllowNull()][AllowEmptyString()][string]$QueryOutput + [AllowNull()][AllowEmptyString()][string]$QueryOutput, + [AllowNull()][PSObject]$DevDriveState ) - if ($TrustExitCode -eq 0 -and $QueryOutput -match '(?im)^\s*This is a trusted developer volume') { + if ($null -ne $DevDriveState -and $DevDriveState.Queried) { + # The Dev Drive bit is asked first: without it the trusted bit describes nothing this run made. + if (-not $DevDriveState.IsDevDrive) { + return [PSCustomObject]@{ + Outcome = 'NotDevDrive' + Lines = @( + "$MountPoint does not carry the Dev Drive designation, although it was formatted as one.", + "Nothing that depends on that designation applies, Defender performance mode included.") + } + } + + if ($DevDriveState.IsTrusted) { + return [PSCustomObject]@{ + Outcome = 'Trusted' + Lines = @("Dev Drive $MountPoint carries the trusted designation, which is the signal for Microsoft Defender to run in performance mode.") + } + } + return [PSCustomObject]@{ - Outcome = 'Trusted' - Lines = @("Dev Drive $MountPoint reports itself trusted, which is the signal for Microsoft Defender to run in performance mode.") + Outcome = 'Untrusted' + Lines = @( + "$MountPoint is a Dev Drive, but the volume does not carry the trusted designation.", + "It still works; anti-virus filters stay attached to it.", + "Retry by hand with: fsutil devdrv trust /f $MountPoint") } } + # Reached only where the volume refused the question, so nothing below is a verdict. $said = @($QueryOutput -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) - - if ($TrustExitCode -eq 0) { - # This is what a successful run looks like on a Windows that is not in English, so it must - # not read as a fault. Nothing else on the machine can be asked instead - see the notes on - # the English phrase above. - $lines = @("Marked $MountPoint as trusted. Reading it back only works in English, and this machine answers in its own language, so the run cannot confirm it here.") - $lines += "fsutil devdrv query $MountPoint said:" - $lines += if ($said.Count -gt 0) { $said | ForEach-Object { " $($_.Trim())" } } else { " (nothing)" } - $lines += "If that says the volume is a trusted developer volume, everything is as it should be." - return [PSCustomObject]@{ Outcome = 'Unconfirmed'; Lines = $lines } + $reason = if ($null -ne $DevDriveState -and $DevDriveState.Reason) { ": $($DevDriveState.Reason)" } else { '.' } + $lines = @("Could not read the Dev Drive designation off $MountPoint$reason") + if ($TrustExitCode -ne 0) { + $lines += "fsutil devdrv trust returned exit code $TrustExitCode, which says nothing about the designation either." } - - $lines = @("Could not mark $MountPoint as trusted (fsutil exited with code $TrustExitCode).") if ($said.Count -gt 0) { $lines += "fsutil devdrv query $MountPoint said:" $lines += $said | ForEach-Object { " $($_.Trim())" } + $lines += "That answer is in this machine's language, so judge it yourself: this run cannot." } else { - $lines += "fsutil devdrv query $MountPoint said nothing." + $lines += "fsutil devdrv query $MountPoint said nothing either." } - $lines += "The Dev Drive will still work, but without the Defender performance mode trust enables." $lines += "Retry by hand with: fsutil devdrv trust /f $MountPoint" - return [PSCustomObject]@{ Outcome = 'Failed'; Lines = $lines } + return [PSCustomObject]@{ Outcome = 'Unconfirmed'; Lines = $lines } } function Test-RecoveryKeyAcknowledged { @@ -2970,14 +3109,20 @@ try { Write-Host "Marking Dev Drive $devLetterColon as trusted for Defender performance" -ForegroundColor Green # /f: the designation lands through a dismount, which fsutil skips on a volume in use. fsutil devdrv trust /f "$devLetterColon" | Out-Null - # fsutil does not throw, so take its exit code before the query overwrites $LASTEXITCODE. + # fsutil does not throw, so take its exit code before anything else overwrites $LASTEXITCODE. $trustExitCode = $LASTEXITCODE - # Cast each record to a string first: on Windows PowerShell a redirected stderr line is an - # ErrorRecord, and Out-String would render it as a whole error display instead of its text. - $trustQuery = (fsutil devdrv query "$devLetterColon" 2>&1 | ForEach-Object { "$_" } | Out-String) - $trustReport = Resolve-DevDriveTrustReport -MountPoint $devLetterColon -TrustExitCode $trustExitCode -QueryOutput $trustQuery - # Grey, not yellow, for an answer that could not be read: on a localized Windows that is every run. - $trustColour = switch ($trustReport.Outcome) { 'Trusted' { 'Green' } 'Unconfirmed' { 'Gray' } default { 'Yellow' } } + # The volume's own flags, which read the same in every language. + $devDriveState = Get-VolumeDevDriveState -DriveLetter $devLetter + $trustQuery = '' + if (-not $devDriveState.Queried) { + # Cast each record to a string first: on Windows PowerShell a redirected stderr line is an + # ErrorRecord, and Out-String would render it as a whole error display instead of its text. + $trustQuery = (fsutil devdrv query "$devLetterColon" 2>&1 | ForEach-Object { "$_" } | Out-String) + } + $trustReport = Resolve-DevDriveTrustReport -MountPoint $devLetterColon -TrustExitCode $trustExitCode ` + -QueryOutput $trustQuery -DevDriveState $devDriveState + # Grey, not yellow, for an answer that could not be read at all. + $trustColour = switch ($trustReport.Outcome) { 'Trusted' { 'Green' } 'Unconfirmed' { 'Gray' } 'NotDevDrive' { 'Red' } default { 'Yellow' } } foreach ($line in $trustReport.Lines) { Write-Host $line -ForegroundColor $trustColour } @@ -3338,7 +3483,11 @@ try { Write-Host "Skipping deduplication as requested." -ForegroundColor Yellow } - Write-Host "All done. Dev Drive $devLetterColon ready." -ForegroundColor Green + if ($trustReport.Outcome -eq 'NotDevDrive') { + Write-Host "Done, but $devLetterColon does not carry the Dev Drive designation. See above." -ForegroundColor Red + } else { + Write-Host "All done. Dev Drive $devLetterColon ready." -ForegroundColor Green + } if (-not $VhdxAtBootGranted -and $VhdxMountAdvice) { Write-Host "" foreach ($line in $VhdxMountAdvice) {