From 8fa57dcb6623bf01b4d8d965f97b5d83e440318d Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 19:55:04 -0500 Subject: [PATCH 01/12] Launch RecallManager v1 engine --- README.md | 166 ++++++++++++------ RecallManager.ps1 | 146 ++++++++------- config/profiles.json | 47 +++++ .../Private/Get-RecallEffectiveState.ps1 | 36 ++++ .../Private/Get-RecallFeatureState.ps1 | 25 +++ .../Private/Get-RecallPolicyState.ps1 | 35 ++++ .../Private/Get-RecallProfileDefinition.ps1 | 10 ++ .../Private/Get-RecallWindowsInfo.ps1 | 30 ++++ .../Save-RecallConfigurationBackup.ps1 | 32 ++++ .../Private/Set-RecallFeatureState.ps1 | 22 +++ .../Private/Set-RecallPolicyValue.ps1 | 22 +++ .../Private/Test-RecallAdministrator.ps1 | 9 + .../Public/Export-RecallReport.ps1 | 9 + src/RecallManager/Public/Get-RecallAudit.ps1 | 35 ++++ src/RecallManager/Public/Get-RecallPlan.ps1 | 31 ++++ src/RecallManager/Public/Get-RecallStatus.ps1 | 22 +++ .../Public/Restore-RecallConfiguration.ps1 | 27 +++ .../Public/Set-RecallProfile.ps1 | 32 ++++ src/RecallManager/RecallManager.psd1 | 28 +++ src/RecallManager/RecallManager.psm1 | 20 +++ 20 files changed, 660 insertions(+), 124 deletions(-) create mode 100644 config/profiles.json create mode 100644 src/RecallManager/Private/Get-RecallEffectiveState.ps1 create mode 100644 src/RecallManager/Private/Get-RecallFeatureState.ps1 create mode 100644 src/RecallManager/Private/Get-RecallPolicyState.ps1 create mode 100644 src/RecallManager/Private/Get-RecallProfileDefinition.ps1 create mode 100644 src/RecallManager/Private/Get-RecallWindowsInfo.ps1 create mode 100644 src/RecallManager/Private/Save-RecallConfigurationBackup.ps1 create mode 100644 src/RecallManager/Private/Set-RecallFeatureState.ps1 create mode 100644 src/RecallManager/Private/Set-RecallPolicyValue.ps1 create mode 100644 src/RecallManager/Private/Test-RecallAdministrator.ps1 create mode 100644 src/RecallManager/Public/Export-RecallReport.ps1 create mode 100644 src/RecallManager/Public/Get-RecallAudit.ps1 create mode 100644 src/RecallManager/Public/Get-RecallPlan.ps1 create mode 100644 src/RecallManager/Public/Get-RecallStatus.ps1 create mode 100644 src/RecallManager/Public/Restore-RecallConfiguration.ps1 create mode 100644 src/RecallManager/Public/Set-RecallProfile.ps1 create mode 100644 src/RecallManager/RecallManager.psd1 create mode 100644 src/RecallManager/RecallManager.psm1 diff --git a/README.md b/README.md index 1e796fc..5dd96df 100644 --- a/README.md +++ b/README.md @@ -3,94 +3,148 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Platform](https://img.shields.io/badge/Platform-Windows%2011-0078D6.svg?logo=windows)](https://www.microsoft.com/windows) [![PowerShell](https://img.shields.io/badge/PowerShell-5.1%2B-5391FE.svg?logo=powershell)](https://learn.microsoft.com/powershell/) +[![Launch](https://img.shields.io/badge/Launch-v1.0%20Beta-orange.svg)](CHANGELOG.md) -A simple PowerShell tool to manage the Windows **Recall** feature using DISM. It checks whether Recall is currently enabled, then lets you disable or enable it interactively — with clear success/failure reporting and a heads-up when a restart is required. +**RecallManager audits, explains, configures, verifies, and rolls back the Windows Recall privacy state.** -## Features +This v1 beta turns the original one-question DISM toggle into a state-aware management tool for individual users, support engineers, endpoint administrators, and automation workflows. -- **Check Recall status** — shows whether the Recall feature is currently enabled or disabled. -- **Enable/Disable Recall** — prompts you to toggle the feature based on its current state. -- **Reliable result reporting** — uses DISM exit codes to report success, failure, or a pending restart. -- **Admin privileges check** — warns and exits if the script isn't run elevated (required by DISM). +> [!IMPORTANT] +> This branch is a pre-release launch candidate. Test it on a supported non-production Windows 11 device before considering a merge or public release. -## How It Works +## Why RecallManager exists -The script uses Deployment Image Servicing and Management (DISM) commands to query and change the Recall optional feature: +Recall can be affected by multiple layers: Windows eligibility, optional-feature state, device policy, user policy, user opt-in, and pending restarts. A feature flag alone does not explain the effective state. -- Check Recall status: +RecallManager builds a normalized view and answers: - ```powershell - dism /Online /Get-FeatureInfo /FeatureName:Recall - ``` +- Is the optional component present, enabled, disabled, removed, or unavailable? +- Are machine or current-user policies blocking availability or snapshot saving? +- Which setting wins when states disagree? +- What exact changes would a privacy profile make? +- Can the prior configuration be restored? -- Disable Recall: +```mermaid +flowchart LR + A[Inspect Windows] --> B[Read Recall feature] + A --> C[Read machine policies] + A --> D[Read current-user policies] + B --> E[Calculate effective state] + C --> E + D --> E + E --> F[Explain findings] + F --> G[Plan desired profile] + G --> H[Back up] + H --> I[Apply] + I --> J[Re-audit and verify] + J --> K[Restore if needed] +``` + +## Screenshot preview - ```powershell - dism /Online /Disable-Feature /FeatureName:Recall /NoRestart - ``` +The launch documentation already contains replaceable screenshot frames. Capture the real UI after testing and overwrite the matching files. -- Enable Recall: +![RecallManager status screenshot placeholder](docs/images/status-placeholder.svg) - ```powershell - dism /Online /Enable-Feature /FeatureName:Recall /NoRestart - ``` +See [Screenshot Capture Guide](docs/screenshots.md) for the required shots and redaction checklist. -Changes to the feature may require a restart to take effect — the script tells you when that's the case (DISM exit code `3010`). +## Profiles -## Installation +| Profile | Purpose | Component change | Snapshot policy | +|---|---|---:|---:| +| `AuditOnly` | Inspect without changes | None | None | +| `UserControlled` | Remove RecallManager policy overrides | None | Return to Windows/user control | +| `SnapshotsOff` | Keep the component but block snapshots | None | Blocked at machine and current-user scope | +| `PrivacyHardened` | Maximum supported local hardening | Disable and remove | Availability and snapshots blocked | -1. Download or clone the repository: +> [!WARNING] +> Microsoft documents that disabling Recall availability or turning off snapshot saving can delete previously saved snapshots. `SnapshotsOff` and `PrivacyHardened` are therefore marked destructive and require confirmation unless explicitly automated. - ```powershell - git clone https://github.com/antonflor/RecallManager.git - ``` +## Quick start -2. Open an **elevated** PowerShell terminal (Run as Administrator — required by DISM). +Open **PowerShell as Administrator** for changes. Status and audit commands can run without elevation. -3. Run the script: +```powershell +git clone https://github.com/antonflor/RecallManager.git +cd RecallManager - ```powershell - .\RecallManager.ps1 - ``` +# Human-readable status +.\RecallManager.ps1 -Command Status - > If script execution is blocked by your execution policy, you can run it for the current session only with: - > `powershell -ExecutionPolicy Bypass -File .\RecallManager.ps1` +# Full privacy audit +.\RecallManager.ps1 -Command Audit -## Usage +# Preview every intended change +.\RecallManager.ps1 -Command Plan -Profile PrivacyHardened -1. Run the script as Administrator. If it isn't elevated, it warns you and exits. -2. The script displays whether Recall is currently **enabled** or **disabled**. -3. Answer the prompt (`Y/N`) to toggle the feature. -4. The script reports the result. If Windows needs a restart to complete the change, it says so. +# PowerShell's native WhatIf preview +.\RecallManager.ps1 -Command Apply -Profile PrivacyHardened -Preview -## Example Output +# Apply after review +.\RecallManager.ps1 -Command Apply -Profile PrivacyHardened +# Restore the most recent RecallManager backup +.\RecallManager.ps1 -Command Restore ``` -Recall is currently ENABLED. -Recall is enabled. Do you want to disable it? (Y/N): Y -Disabling Recall... -[==========================100.0%==========================] -Recall has been disabled. A RESTART is required to complete the change. + +## Automation examples + +```powershell +# JSON for RMM, Intune, or another collector +.\RecallManager.ps1 -Command Audit -Format Json + +# Write an audit artifact +.\RecallManager.ps1 -Command Export -OutputPath C:\Temp\recall-audit.json + +# Noninteractive enforcement after testing +.\RecallManager.ps1 -Command Apply -Profile SnapshotsOff -Yes ``` -## Requirements +## Effective-state model -- **Windows 11** (a build/edition where the Recall optional feature is available) -- **PowerShell 5.1 or newer** -- **Administrator privileges** (required to modify system features with DISM) +```mermaid +stateDiagram-v2 + [*] --> UserControlled: feature available / no blocking policy + UserControlled --> SnapshotsBlocked: DisableAIDataAnalysis = 1 + SnapshotsBlocked --> Disabled: AllowRecallEnablement = 0 + UserControlled --> Disabled: feature disabled or unavailable + Disabled --> UserControlled: restore policy and component +``` -## Disclaimer +RecallManager intentionally does **not** attempt to read Recall snapshot contents. Microsoft protects snapshot access with Windows Hello and user-scoped encryption; this tool manages supported feature and policy surfaces instead. -This script modifies system-level settings. **Run at your own risk.** The author is not responsible for any damage or unintended effects resulting from its use. Make sure you have backups of important data and understand the script before running it. +## Repository map -## License +```text +RecallManager.ps1 CLI and interactive entry point +config/profiles.json Desired-state profile definitions +src/RecallManager/ PowerShell module + Public/ Supported commands + Private/ Detection, policy, feature, and backup logic +tests/ Pester tests +docs/ Architecture, profiles, screenshots, launch plan +.github/workflows/test.yml Windows validation pipeline +``` -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +## Documentation -## Contributing +- [Architecture](docs/architecture.md) +- [Profiles and safety behavior](docs/profiles.md) +- [Screenshot Capture Guide](docs/screenshots.md) +- [Testing and launch checklist](docs/testing.md) +- [Windows policy reference](docs/windows-policy-reference.md) +- [Website and product launch brief](docs/website-launch.md) +- [Contributing](CONTRIBUTING.md) +- [Security policy](SECURITY.md) -Feel free to fork this repository and submit pull requests for improvements or additional features! +## Current v1 beta boundaries -## Author +- Windows 11 only; Recall itself requires eligible Copilot+ hardware and supported builds. +- Local machine and current-user policy inspection only. +- Enterprise/MDM reporting is informational; RecallManager does not replace Intune or Group Policy management. +- Snapshot contents and encrypted databases are never opened. +- No graphical desktop application yet. The PowerShell module is designed to become the shared engine for a future GUI. + +## License -Created by [Tony Flores](https://github.com/antonflor) +MIT. See [LICENSE](LICENSE). diff --git a/RecallManager.ps1 b/RecallManager.ps1 index c94c024..1ab60ac 100644 --- a/RecallManager.ps1 +++ b/RecallManager.ps1 @@ -1,88 +1,98 @@ <# .SYNOPSIS - Check, enable, or disable the Windows Recall feature using DISM. + Command-line entry point for RecallManager. .DESCRIPTION - Queries the current state of the "Recall" optional feature and prompts the - user to enable or disable it. Requires Administrator privileges (DISM). - A restart may be required for changes to take effect. - -.NOTES - Author : Tony Flores (https://github.com/antonflor) - License: MIT + Audits, explains, plans, applies, verifies, and rolls back supported + Windows Recall configuration. Run elevated for changes; audit commands can + run without elevation. #> +[CmdletBinding()] +param( + [ValidateSet('Interactive', 'Status', 'Audit', 'Plan', 'Apply', 'Restore', 'Export')] + [string]$Command = 'Interactive', -# Query the current state of the Recall feature. -# Returns $true (enabled), $false (disabled), or $null (unknown/unavailable). -function Get-RecallStatus { - $featureInfo = dism /Online /Get-FeatureInfo /FeatureName:Recall /English - if ($LASTEXITCODE -ne 0) { - Write-Host "Unable to query the Recall feature (DISM exit code $LASTEXITCODE)." - Write-Host "Recall may not be available on this edition or build of Windows." - return $null - } + [ValidateSet('AuditOnly', 'UserControlled', 'SnapshotsOff', 'PrivacyHardened')] + [string]$Profile = 'AuditOnly', - if ($featureInfo -match 'State : Enable') { - Write-Host "Recall is currently ENABLED." - return $true - } elseif ($featureInfo -match 'State : Disable') { - Write-Host "Recall is currently DISABLED." - return $false - } + [ValidateSet('Text', 'Json')] + [string]$Format = 'Text', - Write-Host "Unable to determine the status of Recall." - return $null -} + [string]$OutputPath, + [string]$BackupPath, + [switch]$Yes, + [switch]$Preview +) -# Report the result of a DISM enable/disable operation based on its exit code. -function Show-DismResult { - param([string]$Action) +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' - switch ($LASTEXITCODE) { - 0 { Write-Host "Recall has been successfully $Action." } - 3010 { Write-Host "Recall has been $Action. A RESTART is required to complete the change." } - default { Write-Host "Failed to $($Action -replace 'd$','') Recall (DISM exit code $LASTEXITCODE)." } - } -} +$modulePath = Join-Path $PSScriptRoot 'src\RecallManager\RecallManager.psd1' +Import-Module $modulePath -Force -function Disable-Recall { - Write-Host "Disabling Recall..." - dism /Online /Disable-Feature /FeatureName:Recall /NoRestart - Show-DismResult -Action 'disabled' -} +function Write-RecallObject { + param([Parameter(Mandatory = $true)]$InputObject) -function Enable-Recall { - Write-Host "Enabling Recall..." - dism /Online /Enable-Feature /FeatureName:Recall /NoRestart - Show-DismResult -Action 'enabled' + if ($Format -eq 'Json') { + $InputObject | ConvertTo-Json -Depth 8 + } + else { + $InputObject | Format-List + } } -# --- Main --- +function Invoke-InteractiveMenu { + Write-Host '' + Write-Host 'RecallManager' -ForegroundColor Cyan + Write-Host 'Audit, explain, and control Windows Recall.' + Write-Host '' + Write-Host '[1] Show status' + Write-Host '[2] Run privacy audit' + Write-Host '[3] Preview SnapshotsOff plan' + Write-Host '[4] Preview PrivacyHardened plan' + Write-Host '[5] Apply SnapshotsOff profile' + Write-Host '[6] Apply PrivacyHardened profile' + Write-Host '[7] Restore latest backup' + Write-Host '[Q] Quit' -# DISM requires an elevated session. -$identity = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() -if (-not $identity.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Warning "This script requires running as Administrator!" - Pause - exit 1 + $selection = Read-Host 'Choose an action' + switch ($selection.ToUpperInvariant()) { + '1' { Write-RecallObject (Get-RecallStatus) } + '2' { Write-RecallObject (Get-RecallAudit) } + '3' { Write-RecallObject (Get-RecallPlan -Profile SnapshotsOff) } + '4' { Write-RecallObject (Get-RecallPlan -Profile PrivacyHardened) } + '5' { Set-RecallProfile -Profile SnapshotsOff } + '6' { Set-RecallProfile -Profile PrivacyHardened } + '7' { Restore-RecallConfiguration -BackupPath $BackupPath } + 'Q' { return } + default { Write-Warning 'Unknown selection.' } + } } -$status = Get-RecallStatus - -if ($status -eq $true) { - $response = Read-Host "Recall is enabled. Do you want to disable it? (Y/N)" - if ($response -eq 'Y') { - Disable-Recall - } else { - Write-Host "No changes made." - } -} elseif ($status -eq $false) { - $response = Read-Host "Recall is disabled. Do you want to enable it? (Y/N)" - if ($response -eq 'Y') { - Enable-Recall - } else { - Write-Host "No changes made." +try { + switch ($Command) { + 'Interactive' { Invoke-InteractiveMenu } + 'Status' { Write-RecallObject (Get-RecallStatus) } + 'Audit' { Write-RecallObject (Get-RecallAudit) } + 'Plan' { Write-RecallObject (Get-RecallPlan -Profile $Profile) } + 'Apply' { + $confirmPreference = -not $Yes.IsPresent + Set-RecallProfile -Profile $Profile -Confirm:$confirmPreference -WhatIf:$Preview + } + 'Restore' { + $confirmPreference = -not $Yes.IsPresent + Restore-RecallConfiguration -BackupPath $BackupPath -Confirm:$confirmPreference -WhatIf:$Preview + } + 'Export' { + if ([string]::IsNullOrWhiteSpace($OutputPath)) { + $OutputPath = Join-Path (Get-Location) ('RecallManager-Audit-{0}.json' -f (Get-Date -Format 'yyyyMMdd-HHmmss')) + } + Export-RecallReport -Path $OutputPath + Write-Host "Audit report written to $OutputPath" + } } -} else { +} +catch { + Write-Error $_ exit 1 } diff --git a/config/profiles.json b/config/profiles.json new file mode 100644 index 0000000..305a856 --- /dev/null +++ b/config/profiles.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": 1, + "profiles": { + "AuditOnly": { + "description": "Inspect and report the effective Recall state without changing the computer.", + "featureAction": "None", + "machinePolicies": {}, + "userPolicies": {}, + "destructive": false + }, + "UserControlled": { + "description": "Remove RecallManager-created policy overrides and return supported choices to Windows and the signed-in user.", + "featureAction": "None", + "machinePolicies": { + "AllowRecallEnablement": null, + "DisableAIDataAnalysis": null + }, + "userPolicies": { + "DisableAIDataAnalysis": null + }, + "destructive": false + }, + "SnapshotsOff": { + "description": "Keep the Recall component available while policy-blocking snapshot saving for the device and current user.", + "featureAction": "None", + "machinePolicies": { + "DisableAIDataAnalysis": 1 + }, + "userPolicies": { + "DisableAIDataAnalysis": 1 + }, + "destructive": true + }, + "PrivacyHardened": { + "description": "Block Recall availability, block snapshot saving, and disable/remove the optional component.", + "featureAction": "DisableRemove", + "machinePolicies": { + "AllowRecallEnablement": 0, + "DisableAIDataAnalysis": 1 + }, + "userPolicies": { + "DisableAIDataAnalysis": 1 + }, + "destructive": true + } + } +} diff --git a/src/RecallManager/Private/Get-RecallEffectiveState.ps1 b/src/RecallManager/Private/Get-RecallEffectiveState.ps1 new file mode 100644 index 0000000..4d8c4cd --- /dev/null +++ b/src/RecallManager/Private/Get-RecallEffectiveState.ps1 @@ -0,0 +1,36 @@ +function Get-RecallEffectiveState { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)]$Feature, + [Parameter(Mandatory = $true)][object[]]$Policies + ) + + $allow = $Policies | Where-Object { $_.Scope -eq 'Machine' -and $_.Name -eq 'AllowRecallEnablement' } | Select-Object -First 1 + $machineSnapshots = $Policies | Where-Object { $_.Scope -eq 'Machine' -and $_.Name -eq 'DisableAIDataAnalysis' } | Select-Object -First 1 + $userSnapshots = $Policies | Where-Object { $_.Scope -eq 'CurrentUser' -and $_.Name -eq 'DisableAIDataAnalysis' } | Select-Object -First 1 + + $availabilityBlocked = $false + if ($null -ne $allow -and $allow.Configured) { $availabilityBlocked = [int]$allow.Value -eq 0 } + $machineBlocked = $false + if ($null -ne $machineSnapshots -and $machineSnapshots.Configured) { $machineBlocked = [int]$machineSnapshots.Value -eq 1 } + $userBlocked = $false + if ($null -ne $userSnapshots -and $userSnapshots.Configured) { $userBlocked = [int]$userSnapshots.Value -eq 1 } + + $snapshotBlocked = $machineBlocked -or $userBlocked + $featureUnavailable = $Feature.State -in @('Disabled','DisablePending','DisabledWithPayloadRemoved','Unavailable','UnsupportedPlatform') + + if ($availabilityBlocked -or $featureUnavailable) { + $state = 'Disabled' + $reason = if ($availabilityBlocked) { 'Recall availability is blocked by machine policy.' } else { "The Recall optional feature state is $($Feature.State)." } + } + elseif ($snapshotBlocked) { + $state = 'SnapshotsBlocked' + $reason = 'Recall may be installed, but saving snapshots is blocked by policy.' + } + else { + $state = 'UserControlled' + $reason = 'No blocking policy was detected; the signed-in user remains responsible for opt-in choices.' + } + + [pscustomobject]@{ State = $state; Reason = $reason; AvailabilityBlocked = $availabilityBlocked; SnapshotSavingBlocked = $snapshotBlocked } +} diff --git a/src/RecallManager/Private/Get-RecallFeatureState.ps1 b/src/RecallManager/Private/Get-RecallFeatureState.ps1 new file mode 100644 index 0000000..7492b85 --- /dev/null +++ b/src/RecallManager/Private/Get-RecallFeatureState.ps1 @@ -0,0 +1,25 @@ +function Get-RecallFeatureState { + [CmdletBinding()] + param() + + if ($env:OS -ne 'Windows_NT') { + return [pscustomobject]@{ + Name = 'Recall'; State = 'UnsupportedPlatform'; RestartNeeded = $false + Source = 'PlatformCheck'; Error = $null + } + } + + try { + $feature = Get-WindowsOptionalFeature -Online -FeatureName 'Recall' -ErrorAction Stop + return [pscustomobject]@{ + Name = 'Recall'; State = [string]$feature.State; RestartNeeded = [bool]$feature.RestartNeeded + Source = 'Get-WindowsOptionalFeature'; Error = $null + } + } + catch { + return [pscustomobject]@{ + Name = 'Recall'; State = 'Unavailable'; RestartNeeded = $false + Source = 'Get-WindowsOptionalFeature'; Error = $_.Exception.Message + } + } +} diff --git a/src/RecallManager/Private/Get-RecallPolicyState.ps1 b/src/RecallManager/Private/Get-RecallPolicyState.ps1 new file mode 100644 index 0000000..b334ac6 --- /dev/null +++ b/src/RecallManager/Private/Get-RecallPolicyState.ps1 @@ -0,0 +1,35 @@ +function Get-RecallPolicyEntry { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][string]$Scope + ) + + $value = $null + $configured = $false + if (Test-Path $Path) { + $item = Get-ItemProperty -Path $Path -ErrorAction SilentlyContinue + if ($null -ne $item -and $null -ne $item.PSObject.Properties[$Name]) { + $value = $item.$Name + $configured = $true + } + } + + [pscustomobject]@{ Scope = $Scope; Name = $Name; Configured = $configured; Value = $value; Path = $Path } +} + +function Get-RecallPolicyState { + [CmdletBinding()] + param() + if ($env:OS -ne 'Windows_NT') { return @() } + + $entries = @() + foreach ($name in @('AllowRecallEnablement','DisableAIDataAnalysis','SetMaximumStorageSpaceForRecallSnapshots','SetMaximumStorageDurationForRecallSnapshots','SetDenyUriListForRecall','SetDenyAppListForRecall','SetDataLossPreventionProvider','AllowRecallExport')) { + $entries += Get-RecallPolicyEntry -Path $script:PolicyMachinePath -Name $name -Scope 'Machine' + } + foreach ($name in @('DisableAIDataAnalysis','SetMaximumStorageSpaceForRecallSnapshots','SetMaximumStorageDurationForRecallSnapshots','SetDenyUriListForRecall','SetDenyAppListForRecall')) { + $entries += Get-RecallPolicyEntry -Path $script:PolicyUserPath -Name $name -Scope 'CurrentUser' + } + return $entries +} diff --git a/src/RecallManager/Private/Get-RecallProfileDefinition.ps1 b/src/RecallManager/Private/Get-RecallProfileDefinition.ps1 new file mode 100644 index 0000000..8dd3942 --- /dev/null +++ b/src/RecallManager/Private/Get-RecallProfileDefinition.ps1 @@ -0,0 +1,10 @@ +function Get-RecallProfileDefinition { + [CmdletBinding()] + param([Parameter(Mandatory = $true)][string]$Profile) + + if (-not (Test-Path $script:ProfilePath)) { throw "Profile file not found: $script:ProfilePath" } + $configuration = Get-Content -Path $script:ProfilePath -Raw | ConvertFrom-Json + $definition = $configuration.profiles.PSObject.Properties[$Profile] + if ($null -eq $definition) { throw "Unknown profile '$Profile'." } + return $definition.Value +} diff --git a/src/RecallManager/Private/Get-RecallWindowsInfo.ps1 b/src/RecallManager/Private/Get-RecallWindowsInfo.ps1 new file mode 100644 index 0000000..bbd41d8 --- /dev/null +++ b/src/RecallManager/Private/Get-RecallWindowsInfo.ps1 @@ -0,0 +1,30 @@ +function Get-RecallWindowsInfo { + [CmdletBinding()] + param() + + $isWindows = $env:OS -eq 'Windows_NT' + if (-not $isWindows) { + return [pscustomobject]@{ + IsWindows = $false + ProductName = $null + EditionId = $null + DisplayVersion = $null + CurrentBuild = $null + UBR = $null + Architecture = if ([Environment]::Is64BitOperatingSystem) { '64-bit' } else { '32-bit' } + ComputerName = $env:COMPUTERNAME + } + } + + $cv = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' + [pscustomobject]@{ + IsWindows = $true + ProductName = $cv.ProductName + EditionId = $cv.EditionID + DisplayVersion = $cv.DisplayVersion + CurrentBuild = [int]$cv.CurrentBuildNumber + UBR = [int]$cv.UBR + Architecture = if ([Environment]::Is64BitOperatingSystem) { '64-bit' } else { '32-bit' } + ComputerName = $env:COMPUTERNAME + } +} diff --git a/src/RecallManager/Private/Save-RecallConfigurationBackup.ps1 b/src/RecallManager/Private/Save-RecallConfigurationBackup.ps1 new file mode 100644 index 0000000..d6e0e0b --- /dev/null +++ b/src/RecallManager/Private/Save-RecallConfigurationBackup.ps1 @@ -0,0 +1,32 @@ +function Get-RecallBackupDirectory { + [CmdletBinding()] + param() + $base = if ($env:ProgramData) { $env:ProgramData } else { $env:TEMP } + return Join-Path $base 'RecallManager\Backups' +} + +function Save-RecallConfigurationBackup { + [CmdletBinding()] + param([Parameter(Mandatory = $true)]$Status) + + $directory = Get-RecallBackupDirectory + New-Item -Path $directory -ItemType Directory -Force | Out-Null + $path = Join-Path $directory ('recall-backup-{0}.json' -f (Get-Date -Format 'yyyyMMdd-HHmmss')) + $backup = [ordered]@{ + SchemaVersion = 1 + CreatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') + ComputerName = $env:COMPUTERNAME + Feature = $Status.Feature + Policies = $Status.Policies + } + $backup | ConvertTo-Json -Depth 8 | Set-Content -Path $path -Encoding UTF8 + return $path +} + +function Get-LatestRecallBackup { + [CmdletBinding()] + param() + $directory = Get-RecallBackupDirectory + if (-not (Test-Path $directory)) { return $null } + return Get-ChildItem -Path $directory -Filter 'recall-backup-*.json' | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1 +} diff --git a/src/RecallManager/Private/Set-RecallFeatureState.ps1 b/src/RecallManager/Private/Set-RecallFeatureState.ps1 new file mode 100644 index 0000000..fd9ccf8 --- /dev/null +++ b/src/RecallManager/Private/Set-RecallFeatureState.ps1 @@ -0,0 +1,22 @@ +function Set-RecallFeatureState { + [CmdletBinding(SupportsShouldProcess = $true)] + param([Parameter(Mandatory = $true)][ValidateSet('None', 'Enable', 'Disable', 'DisableRemove')][string]$Action) + + if ($Action -eq 'None') { return [pscustomobject]@{ Action = 'None'; RestartNeeded = $false } } + if ($Action -eq 'Enable') { + if ($PSCmdlet.ShouldProcess('Windows optional feature Recall', 'Enable')) { + $result = Enable-WindowsOptionalFeature -Online -FeatureName 'Recall' -All -NoRestart -ErrorAction Stop + return [pscustomobject]@{ Action = 'Enable'; RestartNeeded = [bool]$result.RestartNeeded } + } + } + elseif ($Action -in @('Disable', 'DisableRemove')) { + $remove = $Action -eq 'DisableRemove' + if ($PSCmdlet.ShouldProcess('Windows optional feature Recall', $Action)) { + $parameters = @{ Online = $true; FeatureName = 'Recall'; NoRestart = $true; ErrorAction = 'Stop' } + if ($remove) { $parameters.Remove = $true } + $result = Disable-WindowsOptionalFeature @parameters + return [pscustomobject]@{ Action = $Action; RestartNeeded = [bool]$result.RestartNeeded } + } + } + return [pscustomobject]@{ Action = $Action; RestartNeeded = $false; Preview = $true } +} diff --git a/src/RecallManager/Private/Set-RecallPolicyValue.ps1 b/src/RecallManager/Private/Set-RecallPolicyValue.ps1 new file mode 100644 index 0000000..ad05fd9 --- /dev/null +++ b/src/RecallManager/Private/Set-RecallPolicyValue.ps1 @@ -0,0 +1,22 @@ +function Set-RecallPolicyValue { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)][ValidateSet('Machine', 'CurrentUser')][string]$Scope, + [Parameter(Mandatory = $true)][string]$Name, + [AllowNull()]$Value + ) + + $path = if ($Scope -eq 'Machine') { $script:PolicyMachinePath } else { $script:PolicyUserPath } + if ($null -eq $Value) { + if ((Test-Path $path) -and $PSCmdlet.ShouldProcess("$Scope policy $Name", 'Remove policy override')) { + Remove-ItemProperty -Path $path -Name $Name -ErrorAction SilentlyContinue + } + return + } + + if ($PSCmdlet.ShouldProcess("$Scope policy $Name", "Set value to $Value")) { + New-Item -Path $path -Force | Out-Null + $propertyType = if ($Value -is [string]) { 'String' } else { 'DWord' } + New-ItemProperty -Path $path -Name $Name -Value $Value -PropertyType $propertyType -Force | Out-Null + } +} diff --git a/src/RecallManager/Private/Test-RecallAdministrator.ps1 b/src/RecallManager/Private/Test-RecallAdministrator.ps1 new file mode 100644 index 0000000..ee7060e --- /dev/null +++ b/src/RecallManager/Private/Test-RecallAdministrator.ps1 @@ -0,0 +1,9 @@ +function Test-RecallAdministrator { + [CmdletBinding()] + param() + + if ($env:OS -ne 'Windows_NT') { return $false } + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($identity) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} diff --git a/src/RecallManager/Public/Export-RecallReport.ps1 b/src/RecallManager/Public/Export-RecallReport.ps1 new file mode 100644 index 0000000..52fbb85 --- /dev/null +++ b/src/RecallManager/Public/Export-RecallReport.ps1 @@ -0,0 +1,9 @@ +function Export-RecallReport { + [CmdletBinding()] + param([Parameter(Mandatory = $true)][string]$Path) + + $parent = Split-Path $Path -Parent + if ($parent -and -not (Test-Path $parent)) { New-Item -Path $parent -ItemType Directory -Force | Out-Null } + Get-RecallAudit | ConvertTo-Json -Depth 10 | Set-Content -Path $Path -Encoding UTF8 + return Get-Item $Path +} diff --git a/src/RecallManager/Public/Get-RecallAudit.ps1 b/src/RecallManager/Public/Get-RecallAudit.ps1 new file mode 100644 index 0000000..7e335e8 --- /dev/null +++ b/src/RecallManager/Public/Get-RecallAudit.ps1 @@ -0,0 +1,35 @@ +function Get-RecallAudit { + [CmdletBinding()] + param() + + $status = Get-RecallStatus + $findings = New-Object System.Collections.Generic.List[object] + if (-not $status.Windows.IsWindows) { $findings.Add([pscustomobject]@{ Severity = 'Error'; Code = 'UnsupportedPlatform'; Message = 'RecallManager changes require Windows.' }) } + if ($status.Windows.IsWindows -and $status.Windows.CurrentBuild -lt 26100) { $findings.Add([pscustomobject]@{ Severity = 'Info'; Code = 'OlderBuild'; Message = 'This Windows build predates the supported Windows 11 24H2 Recall policy baseline.' }) } + if ($status.Feature.State -eq 'Unavailable') { $findings.Add([pscustomobject]@{ Severity = 'Info'; Code = 'FeatureUnavailable'; Message = 'The Recall optional feature was not found or could not be queried on this device.' }) } + if ($status.EffectiveState -eq 'UserControlled') { $findings.Add([pscustomobject]@{ Severity = 'Warning'; Code = 'UserControlled'; Message = 'No blocking Recall policy was detected. Snapshot saving remains dependent on device eligibility and user opt-in.' }) } + if ($status.EffectiveState -eq 'SnapshotsBlocked') { $findings.Add([pscustomobject]@{ Severity = 'Pass'; Code = 'SnapshotsBlocked'; Message = 'Snapshot saving is blocked by policy while the component may remain installed.' }) } + if ($status.EffectiveState -eq 'Disabled') { $findings.Add([pscustomobject]@{ Severity = 'Pass'; Code = 'RecallDisabled'; Message = 'Recall availability is blocked or the optional component is disabled/unavailable.' }) } + + $configuredPolicies = @($status.Policies | Where-Object { $_.Configured }) + $score = 100 + if ($status.EffectiveState -eq 'UserControlled') { $score = 50 } + elseif ($status.EffectiveState -eq 'SnapshotsBlocked') { $score = 85 } + + [pscustomobject]@{ + TimestampUtc = $status.TimestampUtc + ComputerName = $status.ComputerName + EffectiveState = $status.EffectiveState + PrivacyScore = $score + Explanation = $status.Explanation + Feature = $status.Feature + ConfiguredPolicies = $configuredPolicies + Findings = $findings.ToArray() + Recommendations = @( + 'Run Plan before Apply to review every intended change.', + 'Use SnapshotsOff when you want policy enforcement without removing the component.', + 'Use PrivacyHardened only when you accept component removal and possible deletion of existing snapshots by Windows policy behavior.', + 'Capture screenshots for the documentation only after validating the workflow on a supported test device.' + ) + } +} diff --git a/src/RecallManager/Public/Get-RecallPlan.ps1 b/src/RecallManager/Public/Get-RecallPlan.ps1 new file mode 100644 index 0000000..868bbbe --- /dev/null +++ b/src/RecallManager/Public/Get-RecallPlan.ps1 @@ -0,0 +1,31 @@ +function Get-RecallPlan { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateSet('AuditOnly', 'UserControlled', 'SnapshotsOff', 'PrivacyHardened')] + [string]$Profile + ) + + $definition = Get-RecallProfileDefinition -Profile $Profile + $steps = New-Object System.Collections.Generic.List[object] + $order = 1 + foreach ($property in $definition.machinePolicies.PSObject.Properties) { + $action = if ($null -eq $property.Value) { 'Remove policy override' } else { "Set to $($property.Value)" } + $steps.Add([pscustomobject]@{ Order = $order; Scope = 'Machine'; Target = $property.Name; Action = $action }); $order++ + } + foreach ($property in $definition.userPolicies.PSObject.Properties) { + $action = if ($null -eq $property.Value) { 'Remove policy override' } else { "Set to $($property.Value)" } + $steps.Add([pscustomobject]@{ Order = $order; Scope = 'CurrentUser'; Target = $property.Name; Action = $action }); $order++ + } + if ($definition.featureAction -ne 'None') { $steps.Add([pscustomobject]@{ Order = $order; Scope = 'Windows'; Target = 'Recall optional feature'; Action = $definition.featureAction }); $order++ } + if ($Profile -ne 'AuditOnly') { + $steps.Insert(0, [pscustomobject]@{ Order = 0; Scope = 'RecallManager'; Target = 'Current configuration'; Action = 'Back up before changes' }) + $steps.Add([pscustomobject]@{ Order = $order; Scope = 'RecallManager'; Target = 'Effective state'; Action = 'Re-audit and verify' }) + } + + [pscustomobject]@{ + Profile = $Profile; Description = $definition.description; Destructive = [bool]$definition.destructive + RequiresAdmin = $Profile -ne 'AuditOnly'; MayDeleteData = $Profile -in @('SnapshotsOff', 'PrivacyHardened') + RestartMayBeNeeded = $definition.featureAction -ne 'None'; Steps = $steps.ToArray() + } +} diff --git a/src/RecallManager/Public/Get-RecallStatus.ps1 b/src/RecallManager/Public/Get-RecallStatus.ps1 new file mode 100644 index 0000000..78e16d3 --- /dev/null +++ b/src/RecallManager/Public/Get-RecallStatus.ps1 @@ -0,0 +1,22 @@ +function Get-RecallStatus { + [CmdletBinding()] + param() + + $windows = Get-RecallWindowsInfo + $feature = Get-RecallFeatureState + $policies = @(Get-RecallPolicyState) + $effective = Get-RecallEffectiveState -Feature $feature -Policies $policies + + [pscustomobject]@{ + TimestampUtc = (Get-Date).ToUniversalTime().ToString('o') + ComputerName = $windows.ComputerName + Windows = $windows + IsAdministrator = Test-RecallAdministrator + Feature = $feature + Policies = $policies + EffectiveState = $effective.State + Explanation = $effective.Reason + SnapshotDataAccess = 'Not inspected: Recall snapshot content is user-scoped, encrypted, and Windows Hello protected.' + RestartNeeded = [bool]$feature.RestartNeeded + } +} diff --git a/src/RecallManager/Public/Restore-RecallConfiguration.ps1 b/src/RecallManager/Public/Restore-RecallConfiguration.ps1 new file mode 100644 index 0000000..f734656 --- /dev/null +++ b/src/RecallManager/Public/Restore-RecallConfiguration.ps1 @@ -0,0 +1,27 @@ +function Restore-RecallConfiguration { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param([string]$BackupPath) + + if (-not (Test-RecallAdministrator)) { throw 'Restoring Recall configuration requires an elevated PowerShell session.' } + if ([string]::IsNullOrWhiteSpace($BackupPath)) { + $latest = Get-LatestRecallBackup + if ($null -eq $latest) { throw 'No RecallManager backup was found.' } + $BackupPath = $latest.FullName + } + if (-not (Test-Path $BackupPath)) { throw "Backup not found: $BackupPath" } + + $backup = Get-Content -Path $BackupPath -Raw | ConvertFrom-Json + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Restore Recall configuration from $BackupPath")) { + foreach ($entry in $backup.Policies) { + $value = if ($entry.Configured) { $entry.Value } else { $null } + Set-RecallPolicyValue -Scope $entry.Scope -Name $entry.Name -Value $value -Confirm:$false + } + $featureAction = 'None' + if ($backup.Feature.State -eq 'Enabled') { $featureAction = 'Enable' } + elseif ($backup.Feature.State -eq 'Disabled') { $featureAction = 'Disable' } + elseif ($backup.Feature.State -eq 'DisabledWithPayloadRemoved') { $featureAction = 'DisableRemove' } + $featureResult = Set-RecallFeatureState -Action $featureAction -Confirm:$false + $after = Get-RecallStatus + return [pscustomobject]@{ BackupPath = $BackupPath; Restored = $true; RestartNeeded = [bool]($after.RestartNeeded -or $featureResult.RestartNeeded); Status = $after } + } +} diff --git a/src/RecallManager/Public/Set-RecallProfile.ps1 b/src/RecallManager/Public/Set-RecallProfile.ps1 new file mode 100644 index 0000000..600f304 --- /dev/null +++ b/src/RecallManager/Public/Set-RecallProfile.ps1 @@ -0,0 +1,32 @@ +function Set-RecallProfile { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory = $true)] + [ValidateSet('AuditOnly', 'UserControlled', 'SnapshotsOff', 'PrivacyHardened')] + [string]$Profile + ) + + if ($Profile -eq 'AuditOnly') { return Get-RecallAudit } + if (-not (Test-RecallAdministrator)) { throw 'Applying Recall profiles requires an elevated PowerShell session.' } + + $definition = Get-RecallProfileDefinition -Profile $Profile + $before = Get-RecallStatus + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Apply Recall profile '$Profile'")) { + $backupPath = Save-RecallConfigurationBackup -Status $before + foreach ($property in $definition.machinePolicies.PSObject.Properties) { Set-RecallPolicyValue -Scope Machine -Name $property.Name -Value $property.Value -Confirm:$false } + foreach ($property in $definition.userPolicies.PSObject.Properties) { Set-RecallPolicyValue -Scope CurrentUser -Name $property.Name -Value $property.Value -Confirm:$false } + $featureResult = Set-RecallFeatureState -Action $definition.featureAction -Confirm:$false + $after = Get-RecallStatus + $verified = switch ($Profile) { + 'SnapshotsOff' { $after.EffectiveState -in @('SnapshotsBlocked', 'Disabled') } + 'PrivacyHardened' { $after.EffectiveState -eq 'Disabled' } + 'UserControlled' { $true } + default { $false } + } + return [pscustomobject]@{ + Profile = $Profile; BackupPath = $backupPath; BeforeState = $before.EffectiveState; AfterState = $after.EffectiveState + Verified = $verified; RestartNeeded = [bool]($after.RestartNeeded -or $featureResult.RestartNeeded); Status = $after + } + } + return Get-RecallPlan -Profile $Profile +} diff --git a/src/RecallManager/RecallManager.psd1 b/src/RecallManager/RecallManager.psd1 new file mode 100644 index 0000000..3d5b249 --- /dev/null +++ b/src/RecallManager/RecallManager.psd1 @@ -0,0 +1,28 @@ +@{ + RootModule = 'RecallManager.psm1' + ModuleVersion = '1.0.0' + GUID = '6a4293ad-25f9-49b4-b9dc-851c320794f5' + Author = 'Tony Flores' + CompanyName = 'Community' + Copyright = '(c) 2024-2026 Tony Flores. MIT License.' + Description = 'Audit, explain, configure, verify, and restore Windows Recall privacy state.' + PowerShellVersion = '5.1' + FunctionsToExport = @( + 'Get-RecallStatus', + 'Get-RecallAudit', + 'Get-RecallPlan', + 'Set-RecallProfile', + 'Restore-RecallConfiguration', + 'Export-RecallReport' + ) + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + PrivateData = @{ + PSData = @{ + Tags = @('Windows11', 'Recall', 'Privacy', 'Security', 'PowerShell') + LicenseUri = 'https://github.com/antonflor/RecallManager/blob/main/LICENSE' + ProjectUri = 'https://github.com/antonflor/RecallManager' + } + } +} diff --git a/src/RecallManager/RecallManager.psm1 b/src/RecallManager/RecallManager.psm1 new file mode 100644 index 0000000..b5f0073 --- /dev/null +++ b/src/RecallManager/RecallManager.psm1 @@ -0,0 +1,20 @@ +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +$script:ModuleRoot = $PSScriptRoot +$script:RepositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +$script:ProfilePath = Join-Path $script:RepositoryRoot 'config\profiles.json' +$script:PolicyMachinePath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI' +$script:PolicyUserPath = 'HKCU:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI' + +Get-ChildItem (Join-Path $PSScriptRoot 'Private\*.ps1') | Sort-Object Name | ForEach-Object { . $_.FullName } +Get-ChildItem (Join-Path $PSScriptRoot 'Public\*.ps1') | Sort-Object Name | ForEach-Object { . $_.FullName } + +Export-ModuleMember -Function @( + 'Get-RecallStatus', + 'Get-RecallAudit', + 'Get-RecallPlan', + 'Set-RecallProfile', + 'Restore-RecallConfiguration', + 'Export-RecallReport' +) From e4f6dcb1fb85f0ddca36383756b0e98ac79a65cd Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 19:56:42 -0500 Subject: [PATCH 02/12] Add launch documentation and visual placeholders --- .gitignore | 9 ++ CHANGELOG.md | 26 ++++++ CONTRIBUTING.md | 21 +++++ SECURITY.md | 18 ++++ docs/architecture.md | 117 ++++++++++++++++++++++++ docs/images/audit-placeholder.svg | 9 ++ docs/images/plan-placeholder.svg | 9 ++ docs/images/social-card-placeholder.svg | 9 ++ docs/images/status-placeholder.svg | 9 ++ docs/profiles.md | 61 ++++++++++++ docs/screenshots.md | 74 +++++++++++++++ docs/testing.md | 59 ++++++++++++ docs/website-launch.md | 81 ++++++++++++++++ docs/windows-policy-reference.md | 30 ++++++ 14 files changed, 532 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md create mode 100644 docs/architecture.md create mode 100644 docs/images/audit-placeholder.svg create mode 100644 docs/images/plan-placeholder.svg create mode 100644 docs/images/social-card-placeholder.svg create mode 100644 docs/images/status-placeholder.svg create mode 100644 docs/profiles.md create mode 100644 docs/screenshots.md create mode 100644 docs/testing.md create mode 100644 docs/website-launch.md create mode 100644 docs/windows-policy-reference.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0a000f --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.log +*.tmp +*.bak +TestResults/ +coverage/ +.vscode/ +.idea/ +RecallManager-Audit-*.json +recall-backup-*.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..282c9c7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to RecallManager are documented here. + +## [1.0.0-beta.1] - 2026-07-18 + +### Added + +- State-aware Recall audit covering Windows build, optional-feature state, machine policy, and current-user policy. +- Effective-state explanation instead of a raw enabled/disabled flag. +- Four desired-state profiles: AuditOnly, UserControlled, SnapshotsOff, and PrivacyHardened. +- Change planning, native `WhatIf`, confirmation gates, pre-change JSON backup, post-change verification, and restore. +- Human-readable and JSON output for support, RMM, and endpoint workflows. +- PowerShell module architecture with public/private separation. +- Pester and PSScriptAnalyzer workflow for Windows. +- Product documentation, Mermaid diagrams, screenshot placeholders, and a website launch brief. + +### Changed + +- Replaced the original interactive-only DISM toggle with a command-oriented CLI while retaining an interactive menu. + +### Safety + +- Snapshot contents are never read. +- Destructive profiles are clearly identified and require confirmation by default. +- Main is not targeted by this launch work until testing is complete. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3b55aa6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contributing + +RecallManager changes can affect Windows privacy settings and optional features. Contributions should favor documented Windows interfaces, idempotent behavior, explicit confirmation, and reversible changes. + +## Development flow + +1. Create a branch from `main`. +2. Keep public commands in `src/RecallManager/Public` and internal helpers in `Private`. +3. Add or update Pester tests. +4. Run PSScriptAnalyzer and Pester on Windows PowerShell 5.1 and PowerShell 7 where practical. +5. Document any destructive or restart-requiring behavior. +6. Open a pull request; do not commit feature development directly to `main`. + +## Design rules + +- Prefer official PowerShell, DISM, Group Policy, or Policy CSP surfaces. +- Never open, copy, decrypt, or inspect Recall snapshot contents. +- Build a plan before changing state. +- Back up every setting RecallManager changes. +- Re-query Windows after changes; do not treat command success as verification. +- Keep profiles declarative in `config/profiles.json`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..522cbf7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Security Policy + +## Reporting + +Please report security issues privately through GitHub's security advisory feature rather than opening a public issue. + +## Scope + +Security-sensitive areas include: + +- privilege elevation and administrator checks; +- policy or registry writes; +- optional-feature servicing; +- backup-file permissions and contents; +- command injection or unsafe path handling; +- accidental exposure of Recall data. + +RecallManager must never read, decrypt, upload, or export Recall snapshot contents. Audit reports should contain configuration metadata only. Before sharing reports publicly, review computer names, policy values, application names, and filtered URLs for sensitive information. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..3c6cb0f --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,117 @@ +# Architecture + +RecallManager separates inspection, reasoning, planning, mutation, verification, and presentation. The PowerShell module is the product engine; the current CLI and any future desktop interface should call the same exported commands. + +## Component model + +```mermaid +flowchart TB + subgraph Interfaces + CLI[RecallManager.ps1 CLI] + MENU[Interactive menu] + FUTURE[Future desktop GUI] + RMM[RMM / Intune / automation] + end + + subgraph PublicModule[Public module commands] + STATUS[Get-RecallStatus] + AUDIT[Get-RecallAudit] + PLAN[Get-RecallPlan] + APPLY[Set-RecallProfile] + RESTORE[Restore-RecallConfiguration] + EXPORT[Export-RecallReport] + end + + subgraph Detection[Detection layer] + WIN[Windows build and edition] + FEATURE[Optional feature state] + MPOL[Machine policy] + UPOL[Current-user policy] + end + + subgraph Mutation[Controlled mutation layer] + BACKUP[JSON backup] + POLICY[Policy writer] + DISM[Windows optional-feature cmdlets] + VERIFY[Post-change reinspection] + end + + CLI --> PublicModule + MENU --> PublicModule + FUTURE --> PublicModule + RMM --> PublicModule + STATUS --> Detection + AUDIT --> STATUS + PLAN --> Detection + APPLY --> BACKUP --> POLICY + APPLY --> DISM + POLICY --> VERIFY + DISM --> VERIFY + RESTORE --> BACKUP + EXPORT --> AUDIT +``` + +## Effective-state evaluation + +The optional feature and policy layers can disagree. RecallManager normalizes them into one of three operator-facing states. + +```mermaid +flowchart TD + A[Read optional feature] --> D{Availability policy = 0?} + B[Read machine policy] --> D + C[Read current-user policy] --> D + D -->|Yes| X[Disabled] + D -->|No| E{Feature disabled, removed, or unavailable?} + E -->|Yes| X + E -->|No| F{DisableAIDataAnalysis = 1 at either scope?} + F -->|Yes| Y[SnapshotsBlocked] + F -->|No| Z[UserControlled] +``` + +`UserControlled` does not mean snapshots are actively being saved. Microsoft requires user opt-in; it means RecallManager did not detect a blocking policy or unavailable component. + +## Change transaction + +```mermaid +sequenceDiagram + actor Operator + participant CLI + participant Plan + participant Backup + participant Windows + participant Verify + + Operator->>CLI: Apply profile + CLI->>Plan: Resolve desired state + Plan-->>Operator: Confirmation / WhatIf output + Operator->>CLI: Confirm + CLI->>Backup: Save current feature and policies + Backup-->>CLI: Backup path + CLI->>Windows: Apply policy values + CLI->>Windows: Change optional feature if required + CLI->>Verify: Re-run full status inspection + Verify-->>Operator: Before, after, restart requirement +``` + +## Data boundaries + +RecallManager reads configuration metadata only: + +- Windows version and edition; +- Recall optional-feature state; +- documented Windows AI policy values; +- RecallManager-created backup metadata. + +It does not read or decrypt Recall snapshots, search indexes, databases, exported snapshot packages, or Windows Hello-protected content. + +## Extension points + +The module can later support: + +- a WinUI or WPF frontend; +- fleet-oriented compliance output; +- signed PowerShell Gallery releases; +- richer edition/build compatibility rules; +- policy conflict provenance from domain policy or MDM; +- approved app and URI exclusion management; +- event log integration. diff --git a/docs/images/audit-placeholder.svg b/docs/images/audit-placeholder.svg new file mode 100644 index 0000000..1c27722 --- /dev/null +++ b/docs/images/audit-placeholder.svg @@ -0,0 +1,9 @@ + + AUDIT SCREENSHOTScreenshot placeholder for RecallManager documentation. + + + AUDIT SCREENSHOT + Replace with: audit.png + Capture: .\RecallManager.ps1 -Command Audit + Redact usernames, device names, domains, URLs, and paths before publishing. + diff --git a/docs/images/plan-placeholder.svg b/docs/images/plan-placeholder.svg new file mode 100644 index 0000000..3920251 --- /dev/null +++ b/docs/images/plan-placeholder.svg @@ -0,0 +1,9 @@ + + PLAN SCREENSHOTScreenshot placeholder for RecallManager documentation. + + + PLAN SCREENSHOT + Replace with: plan.png + Capture: .\RecallManager.ps1 -Command Plan -Profile PrivacyHardened + Redact usernames, device names, domains, URLs, and paths before publishing. + diff --git a/docs/images/social-card-placeholder.svg b/docs/images/social-card-placeholder.svg new file mode 100644 index 0000000..4c539e0 --- /dev/null +++ b/docs/images/social-card-placeholder.svg @@ -0,0 +1,9 @@ + + RecallManager social card placeholderA launch asset placeholder for RecallManager. + + + RecallManager + Know what Windows Recall is actually doing. + Audit • Explain • Harden • Verify • Restore + v1.0 beta launch asset — replace before publishing + diff --git a/docs/images/status-placeholder.svg b/docs/images/status-placeholder.svg new file mode 100644 index 0000000..3b7538b --- /dev/null +++ b/docs/images/status-placeholder.svg @@ -0,0 +1,9 @@ + + STATUS SCREENSHOTScreenshot placeholder for RecallManager documentation. + + + STATUS SCREENSHOT + Replace with: status.png + Capture: .\RecallManager.ps1 -Command Status + Redact usernames, device names, domains, URLs, and paths before publishing. + diff --git a/docs/profiles.md b/docs/profiles.md new file mode 100644 index 0000000..399ce40 --- /dev/null +++ b/docs/profiles.md @@ -0,0 +1,61 @@ +# Profiles and safety behavior + +Profiles are declarative desired states stored in `config/profiles.json`. The module turns the profile into a plan, backs up the current configuration, applies only the requested differences, and reinspects Windows. + +```mermaid +flowchart LR + A[AuditOnly] -->|No writes| B[Report] + C[UserControlled] -->|Remove overrides| D[Windows / user choice] + E[SnapshotsOff] -->|Block snapshot saving| F[Component retained] + G[PrivacyHardened] -->|Block + remove| H[Component disabled] +``` + +## AuditOnly + +Use for inventory, support intake, troubleshooting, and compliance collection. It never writes settings and does not require administrator rights. + +## UserControlled + +Removes policy values managed by RecallManager: + +- machine `AllowRecallEnablement`; +- machine `DisableAIDataAnalysis`; +- current-user `DisableAIDataAnalysis`. + +It does not automatically enable the optional feature. This profile is intentionally conservative: it returns the policy surface to Windows/user control rather than forcing Recall on. + +## SnapshotsOff + +Sets `DisableAIDataAnalysis` to `1` at machine and current-user scope while leaving the optional feature unchanged. + +Microsoft documents that enabling the policy named **Turn off saving snapshots for Recall** prevents saving snapshots and can delete snapshots that were previously saved. Treat this as a destructive profile even though it does not remove the component. + +## PrivacyHardened + +Sets: + +- machine `AllowRecallEnablement` to `0`; +- machine and current-user `DisableAIDataAnalysis` to `1`; +- Recall optional feature to disabled with payload removal. + +This profile can require a restart. Microsoft documents that disabling Recall availability removes the component and deletes previously saved snapshots. + +## Confirmation model + +| Invocation | Behavior | +|---|---| +| `Plan` | Displays intended changes only | +| `Apply -Preview` | Uses PowerShell `WhatIf`; no changes | +| `Apply` | Prompts before applying | +| `Apply -Yes` | Noninteractive; intended only after testing | +| `Restore` | Prompts before restoring latest backup | + +## Backups + +Backups are written under: + +```text +%ProgramData%\RecallManager\Backups +``` + +They contain feature state and policy metadata, not Recall snapshot contents. A backup can restore policy values and attempt to restore the optional-feature state. If feature payload was removed, Windows may need Windows Update or another servicing source to enable it again. diff --git a/docs/screenshots.md b/docs/screenshots.md new file mode 100644 index 0000000..7865129 --- /dev/null +++ b/docs/screenshots.md @@ -0,0 +1,74 @@ +# Screenshot Capture Guide + +The repository includes SVG placeholders so the documentation has stable image paths before real screenshots are available. Replace each placeholder with a PNG using the filename below, then update the Markdown extension from `.svg` to `.png` where referenced. + +## Required launch screenshots + +### 1. Status overview + +Placeholder: `docs/images/status-placeholder.svg` +Final asset: `docs/images/status.png` + +Capture: + +```powershell +.\RecallManager.ps1 -Command Status +``` + +Include the effective state, explanation, optional-feature state, and administrator status. + +![Status screenshot placeholder](images/status-placeholder.svg) + +### 2. Privacy audit + +Placeholder: `docs/images/audit-placeholder.svg` +Final asset: `docs/images/audit.png` + +Capture: + +```powershell +.\RecallManager.ps1 -Command Audit +``` + +Show the privacy score, configured policies, findings, and recommendations. + +![Audit screenshot placeholder](images/audit-placeholder.svg) + +### 3. Change plan + +Placeholder: `docs/images/plan-placeholder.svg` +Final asset: `docs/images/plan.png` + +Capture: + +```powershell +.\RecallManager.ps1 -Command Plan -Profile PrivacyHardened +``` + +Show the destructive warning, backup step, policy changes, optional-feature action, and verification step. + +![Plan screenshot placeholder](images/plan-placeholder.svg) + +### 4. WhatIf preview + +Final asset: `docs/images/whatif.png` + +```powershell +.\RecallManager.ps1 -Command Apply -Profile PrivacyHardened -Preview +``` + +### 5. Successful application and restart notice + +Final asset: `docs/images/applied.png` + +Use a supported test machine. Show the before state, after state, backup path, verification result, and restart requirement. + +## Capture standards + +- Use Windows Terminal with a clean PowerShell profile. +- Use a 16:9 or 3:2 crop, at least 1600 pixels wide. +- Keep the same terminal dimensions and font across all screenshots. +- Use Windows dark mode or a neutral terminal theme. +- Redact computer names, usernames, domains, filtered URLs, application names, and backup paths when sensitive. +- Do not show Recall snapshot content. +- Prefer PNG for terminal screenshots and SVG for diagrams or product illustrations. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..674fecf --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,59 @@ +# Testing and launch checklist + +This branch should remain separate from `main` until the following checks pass on real Windows hardware. + +## Automated checks + +```powershell +Install-Module Pester -Force -Scope CurrentUser +Install-Module PSScriptAnalyzer -Force -Scope CurrentUser + +Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error +Invoke-Pester -Path .\tests -Output Detailed +``` + +The GitHub Actions workflow runs the same classes of checks on `windows-latest`. + +## Manual test matrix + +| Scenario | Expected result | +|---|---| +| Non-elevated `Status` | Completes successfully | +| Non-elevated `Audit` | Completes successfully | +| Non-elevated `Apply` | Fails with a clear elevation message | +| Unsupported/non-Copilot+ device | Reports feature unavailable without crashing | +| `Plan -Profile SnapshotsOff` | No writes; lists backup, policies, and verification | +| `Apply -Preview` | Native WhatIf output; no registry or feature changes | +| `SnapshotsOff` | Policies written, component unchanged, audit says SnapshotsBlocked or Disabled | +| `PrivacyHardened` | Policies written, feature disabled/removed, restart surfaced | +| `Restore` | Restores policy values from the latest backup | +| JSON output | Valid JSON with no terminal formatting artifacts | + +## Release gate + +```mermaid +flowchart TD + A[Automated checks pass] --> B[Test on unsupported Windows device] + B --> C[Test on eligible Copilot+ PC] + C --> D[Validate all four profiles] + D --> E[Validate restore after each change profile] + E --> F[Capture and redact screenshots] + F --> G[Review README and website copy] + G --> H[Tag beta release] + H --> I[Merge only after approval] +``` + +## High-risk checks + +- Confirm `SnapshotsOff` behavior against current Microsoft documentation and the installed Windows build. +- Confirm whether existing snapshots are deleted by policy on the test device. +- Confirm feature removal and restoration with and without network access. +- Confirm domain or MDM policy does not immediately overwrite local values. +- Inspect backup permissions under `%ProgramData%\RecallManager\Backups`. +- Confirm a pending restart is clearly reported. + +## Suggested beta label + +`v1.0.0-beta.1` + +Do not publish a stable `v1.0.0` tag until supported-device testing and restore testing are complete. diff --git a/docs/website-launch.md b/docs/website-launch.md new file mode 100644 index 0000000..01af127 --- /dev/null +++ b/docs/website-launch.md @@ -0,0 +1,81 @@ +# Website and product launch brief + +RecallManager can support a small focused product site without becoming a commercial security suite. The site should explain the effective-state problem, show the audit/plan workflow, document safety boundaries, and direct users to GitHub releases. + +## Positioning + +**Headline:** Know what Windows Recall is actually doing. +**Subheadline:** Audit, explain, harden, verify, and restore Windows Recall configuration with an open-source PowerShell tool. + +Primary audience: + +- privacy-conscious Windows users; +- desktop support and MSP engineers; +- endpoint and infrastructure engineers; +- security teams evaluating Copilot+ PCs. + +## Site map + +```mermaid +flowchart TD + HOME[Home] --> DOWNLOAD[Download] + HOME --> HOW[How it works] + HOME --> PROFILES[Privacy profiles] + HOME --> DOCS[Documentation] + HOME --> FAQ[FAQ] + HOME --> GITHUB[GitHub] + DOWNLOAD --> RELEASES[Signed releases and checksums] + DOCS --> CLI[CLI reference] + DOCS --> ENTERPRISE[Automation / Intune examples] + DOCS --> SAFETY[Safety and rollback] +``` + +## Home page sections + +1. **Hero** — one-line promise, GitHub/download button, status screenshot. +2. **The problem** — feature, policy, user choice, and restart state can disagree. +3. **The workflow** — Audit → Explain → Plan → Back up → Apply → Verify → Restore. +4. **Profiles** — cards for AuditOnly, UserControlled, SnapshotsOff, PrivacyHardened. +5. **Transparency** — no snapshot inspection, no telemetry, open source, reversible changes. +6. **Screenshots** — status, audit, plan, WhatIf, successful apply. +7. **For IT** — JSON output, predictable commands, GitHub Actions validation. +8. **FAQ** — eligibility, snapshots, admin rights, restore behavior, managed devices. +9. **Final CTA** — view source, inspect the plan, test the beta. + +## Visual direction + +- Windows-native but independent: dark navy, neutral gray, cyan accents, warning amber. +- Monospace details paired with a clean sans-serif interface font. +- Use state chips: `User Controlled`, `Snapshots Blocked`, `Disabled`, `Restart Required`. +- Avoid fear-based imagery. Show transparent state, plans, and verification. +- Use Mermaid diagrams in docs and purpose-built SVG illustrations on the marketing site. + +## Launch assets needed + +- square project icon; +- horizontal wordmark; +- social preview card (1200×630); +- five tested/redacted screenshots; +- 30-second terminal demo GIF or MP4; +- beta release notes; +- checksums and, later, code-signing information. + +## Suggested domains and naming + +Keep the product name `RecallManager`. Suitable site patterns include: + +- `recallmanager.dev` +- `recallmanager.app` +- a project page under `tonyflor.me` +- GitHub Pages from this repository + +Domain availability must be checked before purchase. + +## Launch sequence + +1. Test this branch on unsupported and supported Windows hardware. +2. Capture screenshots using `docs/screenshots.md`. +3. Publish `v1.0.0-beta.1` as a GitHub prerelease. +4. Launch a one-page site linked to the prerelease. +5. Collect issues and compatibility reports. +6. Add signing and packaged releases before stable v1. diff --git a/docs/windows-policy-reference.md b/docs/windows-policy-reference.md new file mode 100644 index 0000000..8170b6d --- /dev/null +++ b/docs/windows-policy-reference.md @@ -0,0 +1,30 @@ +# Windows Recall policy reference + +RecallManager v1 uses documented Windows AI policy names and the Windows optional-feature cmdlets. + +| Purpose | Policy value | Scope used by RecallManager | +|---|---|---| +| Allow or block Recall availability | `AllowRecallEnablement` | Machine | +| Turn off snapshot saving | `DisableAIDataAnalysis` | Machine and current user | +| Maximum snapshot storage | `SetMaximumStorageSpaceForRecallSnapshots` | Audit only in v1 | +| Maximum retention | `SetMaximumStorageDurationForRecallSnapshots` | Audit only in v1 | +| URI exclusions | `SetDenyUriListForRecall` | Audit only in v1 | +| App exclusions | `SetDenyAppListForRecall` | Audit only in v1 | +| DLP provider | `SetDataLossPreventionProvider` | Audit only in v1 | +| EEA export control | `AllowRecallExport` | Audit only in v1 | + +Registry policy path: + +```text +SOFTWARE\Policies\Microsoft\Windows\WindowsAI +``` + +Official references: + +- https://learn.microsoft.com/windows/client-management/manage-recall +- https://learn.microsoft.com/windows/client-management/mdm/policy-csp-windowsai +- https://learn.microsoft.com/powershell/module/dism/get-windowsoptionalfeature +- https://learn.microsoft.com/powershell/module/dism/enable-windowsoptionalfeature +- https://learn.microsoft.com/powershell/module/dism/disable-windowsoptionalfeature + +Policy and feature behavior can change with Windows servicing. Revalidate destructive behavior before each stable release. From aa534757955f14c4da30d696d0029bc763538cbe Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 19:57:34 -0500 Subject: [PATCH 03/12] Add validation and beta release automation --- .github/PULL_REQUEST_TEMPLATE.md | 25 ++++++++++ .github/workflows/release.yml | 52 +++++++++++++++++++++ .github/workflows/test.yml | 57 +++++++++++++++++++++++ build/Package-Release.ps1 | 29 ++++++++++++ tests/RecallManager.Tests.ps1 | 78 ++++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 build/Package-Release.ps1 create mode 100644 tests/RecallManager.Tests.ps1 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d0f1d02 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +## What changed + + + +## Safety impact + +- [ ] Audit-only change +- [ ] Changes Windows policy +- [ ] Changes an optional feature +- [ ] May delete existing Recall snapshots +- [ ] May require restart + +## Validation + +- [ ] PSScriptAnalyzer +- [ ] Pester on Windows PowerShell 5.1 +- [ ] Pester on PowerShell 7 +- [ ] Tested on a device where Recall is unavailable +- [ ] Tested on an eligible Copilot+ PC +- [ ] Restore tested +- [ ] Screenshots redacted + +## Documentation + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e7a8306 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Package RecallManager release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: Version without the v prefix + required: true + default: 1.0.0-beta.1 + +permissions: + contents: write + +jobs: + package: + runs-on: windows-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Resolve version + id: version + shell: pwsh + run: | + if ('${{ github.event_name }}' -eq 'workflow_dispatch') { + $version = '${{ inputs.version }}' + } + else { + $version = '${{ github.ref_name }}'.TrimStart('v') + } + "version=$version" >> $env:GITHUB_OUTPUT + + - name: Build ZIP and checksum + shell: powershell + run: .\build\Package-Release.ps1 -Version '${{ steps.version.outputs.version }}' + + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: RecallManager-${{ steps.version.outputs.version }} + path: dist/* + + - name: Attach files to GitHub release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} + files: dist/* + generate_release_notes: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..17e312f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: Validate RecallManager + +on: + pull_request: + push: + branches: + - main + - 'agent/**' + +permissions: + contents: read + +jobs: + test: + runs-on: windows-latest + strategy: + matrix: + shell: + - powershell + - pwsh + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install validation modules + shell: ${{ matrix.shell }} + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module Pester -MinimumVersion 5.5.0 -Force -Scope CurrentUser + Install-Module PSScriptAnalyzer -Force -Scope CurrentUser + + - name: Run PSScriptAnalyzer + shell: ${{ matrix.shell }} + run: | + $results = Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error + $results | Format-Table -AutoSize + if ($results) { throw 'PSScriptAnalyzer reported findings.' } + + - name: Run Pester + shell: ${{ matrix.shell }} + run: | + $config = New-PesterConfiguration + $config.Run.Path = '.\tests' + $config.Output.Verbosity = 'Detailed' + $config.TestResult.Enabled = $true + $config.TestResult.OutputPath = 'TestResults.xml' + $config.TestResult.OutputFormat = 'NUnitXml' + Invoke-Pester -Configuration $config + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: pester-${{ matrix.shell }} + path: TestResults.xml + if-no-files-found: ignore diff --git a/build/Package-Release.ps1 b/build/Package-Release.ps1 new file mode 100644 index 0000000..67d9969 --- /dev/null +++ b/build/Package-Release.ps1 @@ -0,0 +1,29 @@ +[CmdletBinding()] +param( + [string]$Version = '1.0.0-beta.1', + [string]$OutputDirectory = (Join-Path $PSScriptRoot '..\dist') +) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +$repositoryRoot = Split-Path $PSScriptRoot -Parent +$stage = Join-Path $env:TEMP ('RecallManager-{0}' -f $Version) +$archive = Join-Path $OutputDirectory ('RecallManager-{0}.zip' -f $Version) + +Remove-Item -Path $stage -Recurse -Force -ErrorAction SilentlyContinue +New-Item -Path $stage -ItemType Directory -Force | Out-Null +New-Item -Path $OutputDirectory -ItemType Directory -Force | Out-Null + +$include = @('RecallManager.ps1','README.md','CHANGELOG.md','LICENSE','SECURITY.md','config','src') +foreach ($item in $include) { + Copy-Item -Path (Join-Path $repositoryRoot $item) -Destination $stage -Recurse -Force +} + +Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $archive -Force +$hash = Get-FileHash -Path $archive -Algorithm SHA256 +$hashLine = '{0} {1}' -f $hash.Hash.ToLowerInvariant(), (Split-Path $archive -Leaf) +$hashLine | Set-Content -Path ($archive + '.sha256') -Encoding ASCII + +Remove-Item -Path $stage -Recurse -Force +Get-Item $archive, ($archive + '.sha256') diff --git a/tests/RecallManager.Tests.ps1 b/tests/RecallManager.Tests.ps1 new file mode 100644 index 0000000..71bdc9a --- /dev/null +++ b/tests/RecallManager.Tests.ps1 @@ -0,0 +1,78 @@ +BeforeAll { + $modulePath = Join-Path $PSScriptRoot '..\src\RecallManager\RecallManager.psd1' + Import-Module $modulePath -Force +} + +Describe 'RecallManager module surface' { + It 'exports the expected public commands' { + $expected = @('Get-RecallStatus','Get-RecallAudit','Get-RecallPlan','Set-RecallProfile','Restore-RecallConfiguration','Export-RecallReport') + $actual = (Get-Command -Module RecallManager).Name + foreach ($name in $expected) { $actual | Should -Contain $name } + } + + It 'loads every declared profile' { + foreach ($profile in @('AuditOnly', 'UserControlled', 'SnapshotsOff', 'PrivacyHardened')) { + { Get-RecallPlan -Profile $profile } | Should -Not -Throw + } + } + + It 'marks privacy profiles that can delete snapshots as destructive' { + (Get-RecallPlan -Profile SnapshotsOff).Destructive | Should -BeTrue + (Get-RecallPlan -Profile PrivacyHardened).Destructive | Should -BeTrue + } + + It 'does not mark AuditOnly as destructive' { + (Get-RecallPlan -Profile AuditOnly).Destructive | Should -BeFalse + } +} + +Describe 'Effective state reasoning' { + InModuleScope RecallManager { + BeforeEach { + Mock Get-RecallWindowsInfo { + [pscustomobject]@{ + IsWindows = $true; ProductName = 'Windows 11 Pro'; EditionId = 'Professional' + DisplayVersion = '24H2'; CurrentBuild = 26100; UBR = 5000 + Architecture = '64-bit'; ComputerName = 'TESTPC' + } + } + Mock Test-RecallAdministrator { $true } + } + + It 'reports Disabled when the availability policy blocks Recall' { + Mock Get-RecallFeatureState { [pscustomobject]@{ State = 'Enabled'; RestartNeeded = $false } } + Mock Get-RecallPolicyState { + @( + [pscustomobject]@{ Scope = 'Machine'; Name = 'AllowRecallEnablement'; Configured = $true; Value = 0 }, + [pscustomobject]@{ Scope = 'Machine'; Name = 'DisableAIDataAnalysis'; Configured = $false; Value = $null }, + [pscustomobject]@{ Scope = 'CurrentUser'; Name = 'DisableAIDataAnalysis'; Configured = $false; Value = $null } + ) + } + (Get-RecallStatus).EffectiveState | Should -Be 'Disabled' + } + + It 'reports SnapshotsBlocked when snapshot saving is blocked' { + Mock Get-RecallFeatureState { [pscustomobject]@{ State = 'Enabled'; RestartNeeded = $false } } + Mock Get-RecallPolicyState { + @( + [pscustomobject]@{ Scope = 'Machine'; Name = 'AllowRecallEnablement'; Configured = $false; Value = $null }, + [pscustomobject]@{ Scope = 'Machine'; Name = 'DisableAIDataAnalysis'; Configured = $true; Value = 1 }, + [pscustomobject]@{ Scope = 'CurrentUser'; Name = 'DisableAIDataAnalysis'; Configured = $false; Value = $null } + ) + } + (Get-RecallStatus).EffectiveState | Should -Be 'SnapshotsBlocked' + } + + It 'reports UserControlled when no blocking state is detected' { + Mock Get-RecallFeatureState { [pscustomobject]@{ State = 'Enabled'; RestartNeeded = $false } } + Mock Get-RecallPolicyState { + @( + [pscustomobject]@{ Scope = 'Machine'; Name = 'AllowRecallEnablement'; Configured = $false; Value = $null }, + [pscustomobject]@{ Scope = 'Machine'; Name = 'DisableAIDataAnalysis'; Configured = $false; Value = $null }, + [pscustomobject]@{ Scope = 'CurrentUser'; Name = 'DisableAIDataAnalysis'; Configured = $false; Value = $null } + ) + } + (Get-RecallStatus).EffectiveState | Should -Be 'UserControlled' + } + } +} From 688e572c31f11b6668c11b42963a09ca9c04ac3e Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 19:59:21 -0500 Subject: [PATCH 04/12] Tighten module analyzer compatibility --- src/RecallManager/RecallManager.psm1 | 1 - 1 file changed, 1 deletion(-) diff --git a/src/RecallManager/RecallManager.psm1 b/src/RecallManager/RecallManager.psm1 index b5f0073..bfaaa07 100644 --- a/src/RecallManager/RecallManager.psm1 +++ b/src/RecallManager/RecallManager.psm1 @@ -1,7 +1,6 @@ Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' -$script:ModuleRoot = $PSScriptRoot $script:RepositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $script:ProfilePath = Join-Path $script:RepositoryRoot 'config\profiles.json' $script:PolicyMachinePath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI' From fbe3846fc7745d3a97a6fda532af003b22db457f Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 19:59:36 -0500 Subject: [PATCH 05/12] Clarify intentional terminal output in analyzer --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 17e312f..5421af8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,8 @@ jobs: - name: Run PSScriptAnalyzer shell: ${{ matrix.shell }} run: | - $results = Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error + # RecallManager intentionally uses Write-Host for its interactive terminal UI. + $results = Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error -ExcludeRule PSAvoidUsingWriteHost $results | Format-Table -AutoSize if ($results) { throw 'PSScriptAnalyzer reported findings.' } From 81d5b78e6aaaf12388c6d28606b3fec93e88b536 Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 20:14:18 -0500 Subject: [PATCH 06/12] Document future recall-manager.net website handoff --- docs/future-website-handoff.md | 256 +++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 docs/future-website-handoff.md diff --git a/docs/future-website-handoff.md b/docs/future-website-handoff.md new file mode 100644 index 0000000..c410cff --- /dev/null +++ b/docs/future-website-handoff.md @@ -0,0 +1,256 @@ +# Future website handoff: recall-manager.net + +This document preserves the decisions and launch context for the future RecallManager website. It is intentionally a planning handoff only; website code does not belong in this repository. + +## Decisions already made + +- **Domain:** `recall-manager.net` +- **Product:** RecallManager +- **Software model:** free, open source, community-first, and ad-free +- **Website model:** public informational and documentation site +- **Website repository:** separate repository, kept private until launch readiness +- **Monetization:** not part of the product or website plan; advertising and AdSense work are parked indefinitely +- **Application repository:** remains the canonical source for the PowerShell module, tests, releases, checksums, technical behavior, and security policy + +The website should strengthen trust in the project, make the tool easier to understand, and help Windows users safely evaluate Recall configuration. It should not turn the application into a commercial product or introduce advertising into the application. + +## Product positioning + +**Primary headline:** Know what Windows Recall is actually doing. + +**Primary description:** RecallManager is a free, open-source Windows utility that audits, explains, configures, verifies, and restores Microsoft Recall settings. + +Core trust statements: + +- free and open source; +- no account required; +- no advertising in the application; +- no telemetry by default; +- no Recall snapshot inspection; +- every planned change is visible before application; +- configuration is backed up before changes; +- supported changes can be restored; +- source, releases, checksums, and known limitations are public. + +Avoid fear-based privacy marketing. The project should explain what Windows reports, show how settings interact, and let users make an informed choice. + +## Repository boundary + +```mermaid +flowchart LR + USERS[Windows users and IT teams] --> SITE[recall-manager.net] + SITE --> RELEASES[GitHub Releases] + SITE --> APPDOCS[Canonical technical docs] + SITE --> ISSUES[GitHub Issues] + + subgraph PUBLICAPP[Public RecallManager repository] + ENGINE[PowerShell module] + TESTS[Tests and validation] + APPDOCS[Architecture, policy, safety docs] + RELEASES[Release ZIPs and checksums] + ISSUES[Issues and compatibility reports] + end + + subgraph PRIVATESITE[Private website repository until launch] + SITE + WEBUI[Site components and styling] + CONTENT[Website-specific copy and pages] + DEPLOY[Hosting and deployment config] + end +``` + +### RecallManager application repository owns + +- PowerShell source and module manifest; +- profile definitions; +- tests and analysis configuration; +- release packaging and checksums; +- architecture and policy behavior; +- security policy and disclosures; +- compatibility information derived from tested releases; +- GitHub issues and community contributions. + +### Future website repository owns + +- website framework and components; +- branding implementation and visual assets; +- landing-page and website-specific copy; +- navigation, accessibility, responsive behavior, and metadata; +- deployment configuration; +- domain configuration documentation; +- public documentation presentation or generated documentation shell; +- website-only analytics, if the project ever deliberately adopts privacy-respecting analytics. + +The website repository must not become a second source of truth for application behavior. Technical statements should link to, generate from, or be reviewed against the public application repository. + +## Proposed site map + +```mermaid +flowchart TD + HOME[Home] --> DOWNLOAD[Download] + HOME --> HOW[How it works] + HOME --> DOCS[Documentation] + HOME --> SECURITY[Security and privacy] + HOME --> COMPAT[Compatibility] + HOME --> FAQ[FAQ] + HOME --> CHANGELOG[Changelog] + HOME --> GITHUB[GitHub] + + DOWNLOAD --> RELEASES[GitHub Releases] + DOWNLOAD --> CHECKSUMS[Checksums and verification] + DOCS --> QUICKSTART[Quick start] + DOCS --> COMMANDS[Command reference] + DOCS --> PROFILES[Profiles] + DOCS --> RESTORE[Backup and restore] + DOCS --> AUTOMATION[Automation and IT usage] +``` + +Recommended initial pages: + +1. `/` — product explanation, trust boundaries, screenshots, and GitHub/download calls to action; +2. `/download` — current stable or beta release, checksum verification, requirements, and release notes; +3. `/docs` — documentation index; +4. `/docs/getting-started` — installation and first audit; +5. `/docs/profiles` — profile behavior and destructive-change warnings; +6. `/docs/backup-and-restore` — backup location, restore behavior, and limitations; +7. `/security` — threat boundaries, reporting process, and snapshot-content non-access statement; +8. `/compatibility` — tested Windows builds and hardware outcomes; +9. `/faq` — common user and administrator questions; +10. `/changelog` — release history sourced from the application repository; +11. `/about` — project purpose and maintainer information; +12. `/privacy` — website privacy statement, even when no analytics are enabled. + +## Download and release rules + +The website should direct downloads to canonical GitHub Releases rather than maintain an independent binary mirror. This prevents the site and source repository from serving different builds. + +Every displayed release should include: + +- version and release channel; +- publication date; +- supported Windows requirements; +- link to release notes; +- direct GitHub Release asset link; +- SHA-256 checksum; +- signing status when code signing is introduced; +- clear beta or prerelease labeling. + +```mermaid +sequenceDiagram + participant Maintainer + participant AppRepo as RecallManager repository + participant Actions as GitHub Actions + participant Releases as GitHub Releases + participant Site as recall-manager.net + participant User + + Maintainer->>AppRepo: Tag tested release + AppRepo->>Actions: Validate and package + Actions->>Releases: Publish ZIP and checksum + Site->>Releases: Reference canonical release metadata + User->>Site: Review requirements and safety notes + User->>Releases: Download canonical artifact +``` + +## Visual direction + +- Windows-native but clearly independent from Microsoft; +- dark navy, neutral gray, restrained cyan, and warning amber; +- clean sans-serif typography with monospace for commands and state values; +- state chips such as `User Controlled`, `Snapshots Blocked`, `Disabled`, and `Restart Required`; +- terminal output, diagrams, and verified state transitions instead of generic shield imagery; +- accessible contrast, visible focus states, reduced-motion support, and mobile-first layouts; +- no fake warnings, countdowns, scare copy, or oversized download prompts. + +## Existing launch assets in this repository + +These files are temporary references for the future website repository: + +- `docs/images/status-placeholder.svg` +- `docs/images/audit-placeholder.svg` +- `docs/images/plan-placeholder.svg` +- `docs/images/social-card-placeholder.svg` +- `docs/screenshots.md` + +Real captures should use the stable filenames defined in `docs/screenshots.md`. Screenshots must be taken from tested builds and reviewed for usernames, hostnames, paths, serial numbers, and other personal information. + +## Private website repository bootstrap + +When website work begins: + +1. Create a new private repository for the site. +2. Copy this document into that repository as the initial product brief. +3. Copy only website-specific placeholder assets that are still useful. +4. Add a link back to the public RecallManager repository. +5. Keep deployment secrets out of source control and provide only a sanitized `.env.example` when needed. +6. Use preview deployments while the repository remains private. +7. Add branch protection, dependency updates, accessibility checks, and link validation before launch. +8. Do not publish the repository or point production DNS at it until the launch checklist is complete. + +No website framework, hosting provider, analytics platform, or DNS layout is selected by this document. Those decisions should be made when implementation begins rather than prematurely locking the project to a stack. + +## Public launch gate + +```mermaid +flowchart TD + A[Application beta tested] --> B[Stable release candidate] + B --> C[Real screenshots captured and redacted] + C --> D[Private website implementation complete] + D --> E[Accessibility and mobile review] + E --> F[Download and checksum links verified] + F --> G[Security, privacy, and disclaimer review] + G --> H[Point recall-manager.net to production] + H --> I[Make website repository public when ready] +``` + +Minimum launch conditions: + +- a tested RecallManager release exists; +- the website links to the correct release and checksum; +- all placeholder screenshots have been replaced; +- compatibility claims are based on recorded testing; +- the security and privacy pages match actual behavior; +- the site includes a clear statement that RecallManager is an independent open-source project and is not affiliated with or endorsed by Microsoft; +- all navigation, download, and external links have been tested; +- no secrets, private preview URLs, internal notes, or personal machine data are present. + +## Cleanup in this repository after website launch + +Once the separate website repository is established, review this application repository and remove planning-only duplication. + +| Item | Expected action after website launch | +|---|---| +| This handoff document | Remove or reduce to a short link to the website repository | +| Social-card placeholder | Move final source asset to the website repository | +| Website-only visual planning | Move to the website repository | +| Technical screenshots used by README/docs | Keep when they help application users | +| Screenshot capture requirements | Keep if releases still depend on them | +| Architecture, profiles, policy, testing, and security docs | Keep as canonical application documentation | +| Release workflows and checksums | Keep in the application repository | + +The cleanup should happen only after the website repository has retained the needed history and assets. + +## Explicitly out of scope + +- advertising or AdSense integration; +- monetization requirements; +- paid application tiers; +- accounts, subscriptions, license keys, or feature gating; +- advertisements, sponsor prompts, or donation nags inside RecallManager; +- collecting or uploading Recall snapshot contents; +- silently installing software from the website; +- maintaining a separate release build outside GitHub Releases. + +## Future implementation kickoff + +When website development begins, use this document together with: + +- `README.md` for current positioning and commands; +- `docs/architecture.md` for the effective-state model; +- `docs/profiles.md` for profile safety behavior; +- `docs/screenshots.md` for required captures; +- `docs/testing.md` for release readiness; +- `SECURITY.md` for security boundaries; +- `CHANGELOG.md` for release history. + +The first website milestone should be a private, deployable documentation and download site for `recall-manager.net`—not a redesign of the RecallManager application. \ No newline at end of file From 4a34d73c1b4cc4d5b2d7b8a252e69a7adb42748b Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 20:14:49 -0500 Subject: [PATCH 07/12] Reference future recall-manager.net website handoff --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5dd96df..cc8439c 100644 --- a/README.md +++ b/README.md @@ -122,10 +122,16 @@ src/RecallManager/ PowerShell module Public/ Supported commands Private/ Detection, policy, feature, and backup logic tests/ Pester tests -docs/ Architecture, profiles, screenshots, launch plan +docs/ Architecture, profiles, screenshots, testing, and future-site handoff .github/workflows/test.yml Windows validation pipeline ``` +## Future public website + +The public project website will eventually live at **`recall-manager.net`** in a separate repository that remains private until launch readiness. RecallManager itself remains free, open source, community-first, and ad-free. Website implementation and monetization are not part of this application repository. + +See [Future website handoff](docs/future-website-handoff.md) for the preserved product direction, repository boundary, sitemap, launch gate, and later cleanup plan. + ## Documentation - [Architecture](docs/architecture.md) @@ -133,7 +139,7 @@ docs/ Architecture, profiles, screenshots, launch p - [Screenshot Capture Guide](docs/screenshots.md) - [Testing and launch checklist](docs/testing.md) - [Windows policy reference](docs/windows-policy-reference.md) -- [Website and product launch brief](docs/website-launch.md) +- [Future recall-manager.net website handoff](docs/future-website-handoff.md) - [Contributing](CONTRIBUTING.md) - [Security policy](SECURITY.md) @@ -147,4 +153,4 @@ docs/ Architecture, profiles, screenshots, launch p ## License -MIT. See [LICENSE](LICENSE). +MIT. See [LICENSE](LICENSE). \ No newline at end of file From a4535e790edd2139aef1ac00d534a90223266c9a Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 20:14:58 -0500 Subject: [PATCH 08/12] Remove superseded website launch brief --- docs/website-launch.md | 81 ------------------------------------------ 1 file changed, 81 deletions(-) delete mode 100644 docs/website-launch.md diff --git a/docs/website-launch.md b/docs/website-launch.md deleted file mode 100644 index 01af127..0000000 --- a/docs/website-launch.md +++ /dev/null @@ -1,81 +0,0 @@ -# Website and product launch brief - -RecallManager can support a small focused product site without becoming a commercial security suite. The site should explain the effective-state problem, show the audit/plan workflow, document safety boundaries, and direct users to GitHub releases. - -## Positioning - -**Headline:** Know what Windows Recall is actually doing. -**Subheadline:** Audit, explain, harden, verify, and restore Windows Recall configuration with an open-source PowerShell tool. - -Primary audience: - -- privacy-conscious Windows users; -- desktop support and MSP engineers; -- endpoint and infrastructure engineers; -- security teams evaluating Copilot+ PCs. - -## Site map - -```mermaid -flowchart TD - HOME[Home] --> DOWNLOAD[Download] - HOME --> HOW[How it works] - HOME --> PROFILES[Privacy profiles] - HOME --> DOCS[Documentation] - HOME --> FAQ[FAQ] - HOME --> GITHUB[GitHub] - DOWNLOAD --> RELEASES[Signed releases and checksums] - DOCS --> CLI[CLI reference] - DOCS --> ENTERPRISE[Automation / Intune examples] - DOCS --> SAFETY[Safety and rollback] -``` - -## Home page sections - -1. **Hero** — one-line promise, GitHub/download button, status screenshot. -2. **The problem** — feature, policy, user choice, and restart state can disagree. -3. **The workflow** — Audit → Explain → Plan → Back up → Apply → Verify → Restore. -4. **Profiles** — cards for AuditOnly, UserControlled, SnapshotsOff, PrivacyHardened. -5. **Transparency** — no snapshot inspection, no telemetry, open source, reversible changes. -6. **Screenshots** — status, audit, plan, WhatIf, successful apply. -7. **For IT** — JSON output, predictable commands, GitHub Actions validation. -8. **FAQ** — eligibility, snapshots, admin rights, restore behavior, managed devices. -9. **Final CTA** — view source, inspect the plan, test the beta. - -## Visual direction - -- Windows-native but independent: dark navy, neutral gray, cyan accents, warning amber. -- Monospace details paired with a clean sans-serif interface font. -- Use state chips: `User Controlled`, `Snapshots Blocked`, `Disabled`, `Restart Required`. -- Avoid fear-based imagery. Show transparent state, plans, and verification. -- Use Mermaid diagrams in docs and purpose-built SVG illustrations on the marketing site. - -## Launch assets needed - -- square project icon; -- horizontal wordmark; -- social preview card (1200×630); -- five tested/redacted screenshots; -- 30-second terminal demo GIF or MP4; -- beta release notes; -- checksums and, later, code-signing information. - -## Suggested domains and naming - -Keep the product name `RecallManager`. Suitable site patterns include: - -- `recallmanager.dev` -- `recallmanager.app` -- a project page under `tonyflor.me` -- GitHub Pages from this repository - -Domain availability must be checked before purchase. - -## Launch sequence - -1. Test this branch on unsupported and supported Windows hardware. -2. Capture screenshots using `docs/screenshots.md`. -3. Publish `v1.0.0-beta.1` as a GitHub prerelease. -4. Launch a one-page site linked to the prerelease. -5. Collect issues and compatibility reports. -6. Add signing and packaged releases before stable v1. From 1ec95da27266e4ac2bc536c89423cb193d28eace Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 20:15:42 -0500 Subject: [PATCH 09/12] Clarify website work is deferred --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cc8439c..c156631 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,9 @@ docs/ Architecture, profiles, screenshots, testing, ## Future public website -The public project website will eventually live at **`recall-manager.net`** in a separate repository that remains private until launch readiness. RecallManager itself remains free, open source, community-first, and ad-free. Website implementation and monetization are not part of this application repository. +A public project website is planned for **`recall-manager.net`**, but website implementation is deliberately deferred until the application has been tested and is ready for launch. Its code will live in a separate repository that remains private during development. + +RecallManager remains free, open source, community-first, and ad-free. Advertising, monetization, paid tiers, accounts, and website implementation are not part of this application repository or the current v1 beta. See [Future website handoff](docs/future-website-handoff.md) for the preserved product direction, repository boundary, sitemap, launch gate, and later cleanup plan. From f08900902c6fce4cf884d17ac0ebfc4f4bbc3c1b Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 20:16:53 -0500 Subject: [PATCH 10/12] No-op verify website handoff reference From 7e43427164a2856d1f033c4f444a101fc379eea9 Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 20:17:25 -0500 Subject: [PATCH 11/12] Keep community website direction documented From 2c7230c2073202ed671fbb43fc83f22169c45420 Mon Sep 17 00:00:00 2001 From: Anton Flor Date: Sat, 18 Jul 2026 20:17:51 -0500 Subject: [PATCH 12/12] Preserve future website handoff