This commit is contained in:
1 parent
0274e7b398
commit
b9a3308214
9 files changed
+93
-6
No files matched your search
@@ -4,6 +4,7 @@ Set-StrictMode -Version Latest;$ErrorActionPreference='Stop';Import-Module (Join
|
||||
$Category=@($Category|ForEach-Object{$_.Split(',')}|ForEach-Object{$_.Trim()}|Where-Object{$_}|Select-Object -Unique);$valid=@('Temp','CrashReports','RecentItems','ThumbnailCache','PowerShellHistory','BrowserCache','WindReports');if(@($Category|Where-Object{$_-notin$valid}).Count){throw 'Unknown cleanup category.'}
|
||||
$plan=if($Mode-in@('Plan','Run')){Get-WindCleanupPlan -AgeDays $AgeDays -Category $Category}else{$null}
|
||||
if($Mode-eq'Plan'){if($Json){$plan|ConvertTo-Json -Depth 7}else{$plan|Select-Object FileCount,Bytes,AgeDays,Exclusions|Format-List;$plan.Files|Select-Object Path,Length,LastWriteUtc|Format-Table -Wrap};return}
|
||||
if($Mode-eq'Run'-and-not$PSCmdlet.ShouldProcess('Selected cleanup files and optional current clipboard','Delete planned files and clear clipboard when requested')){return}
|
||||
$root=Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\cleanup';[IO.Directory]::CreateDirectory($root)|Out-Null
|
||||
if($Mode-eq'Run'){$report=Join-Path $root ((Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')+'.json');Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false|Format-List;if($ClearClipboard){Clear-WindCurrentClipboard;Write-Output 'Current clipboard cleared.'};Write-Output "Cleanup report: $report";return}
|
||||
Assert-WindMutationAllowed
|
||||
|
||||
@@ -179,12 +179,14 @@ Read the [complete tool breakdown](docs/TOOL-BREAKDOWN.md), [current roadmap](do
|
||||
|
||||
## Development and release status
|
||||
|
||||
The current source includes unreleased cleanup preview, stale-file and dependency-matching fixes after v0.0.5. Windows PowerShell 5.1 passes the expanded 128 checks. The new cleanup test suite encountered an antivirus block in PowerShell 7; see [validation status](docs/VALIDATION.md). The downloadable v0.0.5 executable does not contain these unreleased changes.
|
||||
|
||||
```powershell
|
||||
.\tests\Run-All.ps1
|
||||
.\Build-Executable.ps1
|
||||
.\Build-Release.ps1
|
||||
```
|
||||
|
||||
Tests use synthetic registry/app/network/service/account backends, read-only security inventory and temporary workspace files. CI runs 120 PowerShell checks on Windows PowerShell 5.1 and PowerShell 7, plus ten native launcher checks, PE inspection and the one-file publish. The source is ready for review as a **release candidate**; full live uninstall/sign-out/reboot/update/network/device validation on the supported OS matrix remains required before advertising a production-validated release. See [validation](docs/VALIDATION.md) and [contributing](CONTRIBUTING.md).
|
||||
Tests use synthetic registry/app/network/service/account backends, read-only security inventory and temporary workspace files. CI is configured to run 128 PowerShell checks on Windows PowerShell 5.1 and PowerShell 7, plus ten native launcher checks, PE inspection and the one-file publish. Full live uninstall/sign-out/reboot/update/network/device validation on the supported OS matrix remains required before advertising a production-validated release. See [validation](docs/VALIDATION.md) and [contributing](CONTRIBUTING.md).
|
||||
|
||||
`Build-Executable.ps1` creates the self-contained x64 executable and checksum. `Build-Release.ps1` writes a source ZIP and checksum, and `Build-AmePlaybook.ps1` creates the separate AME wrapper when 7-Zip is installed. Builders exclude local reports, refuse to overwrite an existing artifact and do not sign or publish it.
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
# Delivery status — 2026-09-24
|
||||
# Delivery status — 2026-09-26
|
||||
|
||||
Unreleased changes after v0.0.5: cleanup previews now return before report creation or clipboard clearing; cleanup preserves files whose recorded size or modification time changed after planning; operation dependencies use the same case-insensitive matching during validation and execution. Seven disposable-file/clipboard-mock regressions and one dependency regression were added. Windows PowerShell 5.1 passed all component suites (128 checks). PowerShell 7 blocked the new cleanup test's replacement catalog with an antivirus response; no matching recent Defender detection was available. This compatibility failure remains open and no replacement executable is published for these changes.
|
||||
|
||||
The update architecture and implementation pass was completed on 2026-09-20. The product is named ms-private. Local OS inventory, offline bundle verification, the graphical and terminal wizards, app/debloat flows, Microsoft app-account purge, network enforcement and capture, reversible privacy-service shutdown, Recall, exact sensors, privacy-artifact cleanup, lock handling, security status and AME packaging are implemented or prepared as described below. No real package download, app removal, account purge, policy write, service mutation, firewall change, device disable, cleanup or Recall deletion was performed.
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## Completed checks
|
||||
|
||||
Unreleased audit, 2026-09-26: seven new cleanup regressions pass in Windows PowerShell 5.1, covering a side-effect-free preview, unchanged-file deletion, timestamp and length changes, locked files, invalid timestamps and the real cleanup entry script with a mocked clipboard. The operation runner also tests differently capitalized dependency names. The Windows PowerShell 5.1 component suites total 128 passing checks. PowerShell 7 reported an antivirus block in the replacement cleanup catalog; that new suite is not validated in that engine. No security exclusions or bypasses were added. Changed-file detection uses size and modification time; it does not detect identical metadata or eliminate the filesystem race between checking and deleting a file.
|
||||
|
||||
All 48 feature tests also pass in PowerShell 7. The previous v0.0.5 Gitea job ended as `Canceled after 0s`; it provides no remote test evidence. The local validation results above remain distinct from server CI status.
|
||||
|
||||
Release-candidate update, 2026-09-24: 120 tests pass in each of Windows PowerShell 5.1 and PowerShell 7 (25 policy/transaction, 29 app/menu, 19 offline-bundle, 47 feature/package/security/service/account/preset/operation-runner). The policy tests include current-user registry scope, Home direct-control selection and managed-edition exclusion, and Enterprise-only selection. The app tests include aggressive all-removable-app and explicit Store selection, safe skipping of ambiguous desktop registrations, protection of security/dependency applications, completeness of FreshInstall/Medium/Strong/Maximum and the non-destructive EstablishedSafe preset, the reported JSON catalog failure and journal callback scope failure, browser-before-removal sequencing, installer errors, stale inventory, partial failure, bundle deduplication, group selection, keyboard navigation, filtering and terminal layout limits. Feature tests cover real populated and empty operation-runner reports, captured GUI child-script invocation and deferred atomic JSON writers across nested module boundaries, cleanup report finalization, invoker path-traversal refusal, fresh/established/unknown install-age decisions, continued execution of independent groups after a failure, dependency skipping, preset schema refusal, shared Recall apply/skip/conflict decisions, atomic JSON replacement, malformed or duplicate policy/service/account/network catalogs, protected update-endpoint refusal, untrusted registry-hive refusal, network journal recovery, cleanup-root and protected-log refusal, recovery-journal exclusion, browser-cache scope, protected-service exclusion, service disable/restore, forged service-plan rejection, exact account-package reset, credential allowlisting, report redaction, VeraCrypt volume validation, versioned secret-free security audit behavior and AME manifest structure. Native arrow/Space/Enter/Esc browser selection was also exercised in a real Windows PowerShell console. Live host inventory, service planning and account planning were read-only. No real uninstall, account purge, policy or service write, reboot, firewall change, capture, device disable, Recall deletion or update installation was performed.
|
||||
|
||||
The .NET launcher has ten passing tests for valid archive acceptance, installed payload and manifest verification, repair after modified files, rejection and repair of undeclared installed files, manifest path traversal refusal, bad-hash refusal and undeclared archive-entry refusal. `Build-Executable.ps1` must also produce exactly one x64 self-contained executable plus its checksum. Its managed PE inspection verifies AMD64 and GUI headers, the embedded `requireAdministrator` manifest, version, checksum and signature state. Before publishing, launch the GUI in a clean VM, alter and add runtime files to confirm repair, and test a Program Files access failure.
|
||||
|
||||
@@ -35,7 +35,7 @@ function Invoke-MsPrivateGroupPlan {
|
||||
$entry=@($entries | Where-Object { $_.Id -ceq [string]$group.Id })[0]
|
||||
$blocked=@($entry.DependsOn | Where-Object {
|
||||
$dependency=[string]$_
|
||||
$dependencyEntry=@($entries | Where-Object { $_.Id -ceq $dependency })
|
||||
$dependencyEntry=@($entries | Where-Object { $_.Id -eq $dependency })
|
||||
$dependencyEntry.Count -ne 1 -or $dependencyEntry[0].Status -ne 'Succeeded'
|
||||
})
|
||||
if($blocked.Count){$entry.Status='SkippedDependency';$entry.Error='Required group did not succeed: '+($blocked-join', ');if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8};continue}
|
||||
|
||||
@@ -88,14 +88,17 @@ function Invoke-WindCleanup {
|
||||
if($Plan.Schema-ne 2-or$Plan.AgeDays-lt 0){throw 'Invalid cleanup plan.'};$valid=@('Temp','CrashReports','RecentItems','ThumbnailCache','PowerShellHistory','BrowserCache','WindReports');if(@($Plan.Categories|Where-Object{$_-notin$valid}).Count){throw 'Cleanup plan contains an unknown category.'}
|
||||
$catalog=@(Get-WindPrivacyCleanupCatalog|Where-Object{$Plan.Categories-contains$_.Category});$cutoff=[datetime]::Parse($Plan.CutoffUtc).ToUniversalTime()
|
||||
$report=[pscustomobject]@{Schema=2;Kind='WindCleanup';CreatedUtc=[DateTime]::UtcNow.ToString('o');Categories=@($Plan.Categories);Status='Running';Deleted=0;Skipped=0;FreedBytes=[long]0;Entries=@()}
|
||||
if(-not$PSCmdlet.ShouldProcess('Selected cleanup files','Delete planned privacy artifacts and write cleanup report')){$report.Status='Preview';return $report}
|
||||
$save=New-MsPrivateJsonWriter -Path $ReportPath -Depth 7;&$save $report
|
||||
foreach($entry in @($Plan.Files)){
|
||||
$path=[IO.Path]::GetFullPath([string]$entry.Path);$root=[IO.Path]::GetFullPath([string]$entry.Root).TrimEnd('\');$matches=@($catalog|Where-Object{[IO.Path]::GetFullPath([string]$_.Root).TrimEnd('\')-eq$root-and$_.Category-eq$entry.Category-and(Test-WindCleanupName ([IO.Path]::GetFileName($path)) @($_.Patterns))})
|
||||
$out=[pscustomobject]@{Category=$entry.Category;Path=$path;Status='Checking';Reason=$null};$report.Entries+=@($out);&$save $report
|
||||
try{
|
||||
if($matches.Count-ne 1-or-not(Test-WindCleanupPathSafe -Path $path -Root $root)){throw 'Path escaped an allowlisted category target or crosses a reparse point.'}
|
||||
$item=Get-Item -LiteralPath $path -Force -ErrorAction Stop;if($item.PSIsContainer-or($item.Attributes-band[IO.FileAttributes]::ReparsePoint)-or$item.LastWriteTimeUtc-ge$cutoff){$out.Status='Skipped';$out.Reason='Changed since plan or not a regular old file.';$report.Skipped++;continue}
|
||||
if($PSCmdlet.ShouldProcess($path,'Delete selected privacy artifact')){$length=[long]$item.Length;Remove-Item -LiteralPath $path -Force -ErrorAction Stop;$out.Status='Deleted';$report.Deleted++;$report.FreedBytes+=$length}else{$out.Status='Preview'}
|
||||
$item=Get-Item -LiteralPath $path -Force -ErrorAction Stop
|
||||
$plannedWrite=[datetime]::Parse([string]$entry.LastWriteUtc,[Globalization.CultureInfo]::InvariantCulture,[Globalization.DateTimeStyles]::RoundtripKind).ToUniversalTime()
|
||||
if($item.PSIsContainer-or($item.Attributes-band[IO.FileAttributes]::ReparsePoint)-or$item.LastWriteTimeUtc-ge$cutoff-or$item.LastWriteTimeUtc-ne$plannedWrite-or$item.Length-ne[long]$entry.Length){$out.Status='Skipped';$out.Reason='Changed since plan or not a regular old file.';$report.Skipped++;continue}
|
||||
$length=[long]$item.Length;Remove-Item -LiteralPath $path -Force -ErrorAction Stop;$out.Status='Deleted';$report.Deleted++;$report.FreedBytes+=$length
|
||||
}catch{$out.Status='Skipped';$out.Reason=$_.Exception.Message;$report.Skipped++}finally{&$save $report}
|
||||
}
|
||||
$report.Status='Completed';&$save $report;$report
|
||||
|
||||
+1
-1
@@ -8,5 +8,5 @@ foreach ($file in $files) {
|
||||
[System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$errors) | Out-Null
|
||||
if ($errors.Count) {throw ($errors | Out-String)}
|
||||
}
|
||||
foreach ($suite in @('Run-Tests.ps1','Run-AppTests.ps1','Run-OfflineUpdateTests.ps1','Run-FeatureTests.ps1')) {& (Join-Path $PSScriptRoot $suite)}
|
||||
foreach ($suite in @('Run-Tests.ps1','Run-AppTests.ps1','Run-OfflineUpdateTests.ps1','Run-FeatureTests.ps1','Run-CleanupTests.ps1')) {& (Join-Path $PSScriptRoot $suite)}
|
||||
Write-Output 'All checks passed. No live policy or application mutations were performed.'
|
||||
@@ -0,0 +1,70 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference='Stop'
|
||||
$root=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$scratch=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-cleanup-test-'+[guid]::NewGuid().ToString('N'))
|
||||
$module=Import-Module (Join-Path $root 'src/Wind.Cleanup.psm1') -Force -PassThru
|
||||
$script:passed=0
|
||||
function Assert-Cleanup([bool]$Condition,[string]$Message){if(-not$Condition){throw $Message};$script:passed++}
|
||||
try{
|
||||
$files=Join-Path $scratch 'files';[IO.Directory]::CreateDirectory($files)|Out-Null
|
||||
# Replace only the catalog in this module instance: all deletions stay in this test's directory.
|
||||
& $module {
|
||||
param($TestRoot)
|
||||
$script:cleanupTestRoot=$TestRoot
|
||||
function script:Get-WindPrivacyCleanupCatalog {
|
||||
[pscustomobject]@{Category='Temp';Root=$script:cleanupTestRoot;Patterns=@('*');Recurse=$true}
|
||||
}
|
||||
} $files
|
||||
$file=Join-Path $files 'sample.txt';$report=Join-Path $scratch 'report.json'
|
||||
function New-TestCleanupPlan {
|
||||
[IO.File]::WriteAllText($file,'original')
|
||||
[IO.File]::SetLastWriteTimeUtc($file,[DateTime]::UtcNow.AddDays(-30))
|
||||
Get-WindCleanupPlan -AgeDays 7 -Category Temp
|
||||
}
|
||||
|
||||
$plan=New-TestCleanupPlan
|
||||
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -WhatIf
|
||||
Assert-Cleanup ($result.Status-eq'Preview'-and(Test-Path -LiteralPath $file)-and-not(Test-Path -LiteralPath $report)) 'WhatIf changed files or wrote a cleanup report.'
|
||||
|
||||
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false
|
||||
Assert-Cleanup ($result.Deleted-eq1-and$result.FreedBytes-eq8-and-not(Test-Path -LiteralPath $file)) 'Unchanged planned file was not deleted and accounted for.'
|
||||
|
||||
$plan=New-TestCleanupPlan
|
||||
[IO.File]::SetLastWriteTimeUtc($file,[DateTime]::UtcNow.AddDays(-20))
|
||||
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false
|
||||
Assert-Cleanup ($result.Skipped-eq1-and$result.Deleted-eq0-and(Test-Path -LiteralPath $file)) 'File changed after planning was deleted because it was still older than the cutoff.'
|
||||
|
||||
$plan=New-TestCleanupPlan;$originalWrite=[IO.File]::GetLastWriteTimeUtc($file)
|
||||
[IO.File]::WriteAllText($file,'different size');[IO.File]::SetLastWriteTimeUtc($file,$originalWrite)
|
||||
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false
|
||||
Assert-Cleanup ($result.Skipped-eq1-and(Test-Path -LiteralPath $file)) 'Changed file size was ignored when the timestamp was preserved.'
|
||||
|
||||
$plan=New-TestCleanupPlan;$handle=[IO.File]::Open($file,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None)
|
||||
try{$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false}finally{$handle.Dispose()}
|
||||
Assert-Cleanup ($result.Skipped-eq1-and$result.Deleted-eq0-and(Test-Path -LiteralPath $file)) 'Locked file was not recorded as skipped.'
|
||||
|
||||
$plan=New-TestCleanupPlan;$plan.Files[0].LastWriteUtc='invalid'
|
||||
$result=Invoke-WindCleanup -Plan $plan -ReportPath $report -Confirm:$false
|
||||
Assert-Cleanup ($result.Skipped-eq1-and(Test-Path -LiteralPath $file)) 'Invalid planned timestamp did not preserve the file.'
|
||||
|
||||
# Exercise the real entry script with mock dependencies; never read or clear the real clipboard.
|
||||
$wrapper=Join-Path $scratch 'wrapper';$src=Join-Path $wrapper 'src';[IO.Directory]::CreateDirectory($src)|Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $root 'Cleanup.ps1') -Destination $wrapper
|
||||
[IO.File]::WriteAllText((Join-Path $src 'Wind.Core.psm1'),'function Assert-WindMutationAllowed {}')
|
||||
$mock=@'
|
||||
function Get-WindCleanupPlan { [pscustomobject]@{Files=@()} }
|
||||
function Invoke-WindCleanup { throw 'Unexpected cleanup mutation during WhatIf.' }
|
||||
function Clear-WindCurrentClipboard { throw 'Unexpected clipboard mutation during WhatIf.' }
|
||||
'@
|
||||
[IO.File]::WriteAllText((Join-Path $src 'Wind.Cleanup.psm1'),$mock)
|
||||
& (Join-Path $wrapper 'Cleanup.ps1') -Mode Run -ClearClipboard -WhatIf
|
||||
Assert-Cleanup $true 'Cleanup entry script WhatIf did not return before mutations.'
|
||||
}finally{
|
||||
Remove-Module $module -ErrorAction SilentlyContinue
|
||||
$resolved=[IO.Path]::GetFullPath($scratch)
|
||||
$temp=[IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\')+'\'
|
||||
if(-not$resolved.StartsWith($temp,[StringComparison]::OrdinalIgnoreCase)){throw 'Unsafe test cleanup directory.'}
|
||||
if([IO.Directory]::Exists($resolved)){[IO.Directory]::Delete($resolved,$true)}
|
||||
}
|
||||
Write-Output "$script:passed cleanup tests passed. Only disposable files and a mocked clipboard were used."
|
||||
@@ -87,6 +87,11 @@ $run=Invoke-MsPrivateGroupPlan -Groups $groups
|
||||
Assert-True ($run.Status-eq'PartialFailure'-and($run.Entries | Where-Object { $_.Id -eq 'First' }).Status-eq'Failed') 'Operation runner did not record a failed group.'
|
||||
Assert-True (($run.Entries | Where-Object { $_.Id -eq 'Dependent' }).Status-eq'SkippedDependency'-and$events-notcontains'dependent') 'Operation runner executed a blocked dependency.'
|
||||
Assert-True (($run.Entries | Where-Object { $_.Id -eq 'Independent' }).Status-eq'Succeeded'-and$events-contains'independent') 'Operation runner did not continue with an independent group.'
|
||||
$caseRun=Invoke-MsPrivateGroupPlan -Groups @(
|
||||
[pscustomobject]@{Id='First';Label='First';DependsOn=@();Action={}},
|
||||
[pscustomobject]@{Id='Second';Label='Second';DependsOn=@('first');Action={}}
|
||||
)
|
||||
Assert-True ($caseRun.Status-eq'Completed'-and$caseRun.Entries[1].Status-eq'Succeeded') 'A dependency accepted during validation was incorrectly skipped due to capitalization.'
|
||||
$runReportRoot=Join-Path ([IO.Path]::GetTempPath()) ('ms-private-run-report-'+[guid]::NewGuid().ToString('N'))
|
||||
try{
|
||||
$runReportPath=Join-Path $runReportRoot 'run.json'
|
||||
|
||||
Reference in new issue
Block a user