From 7cf7ea805146626de95516dd47619bdd0082737a Mon Sep 17 00:00:00 2001
From: Nico77600 <195452170+Nico77600@users.noreply.github.com>
Date: Thu, 15 Oct 2026 22:53:34 +0100
Subject: [PATCH] Reorganize signed PRA package and virtualize high-volume
reports
---
.gitattributes | 40 +
.github/ci.json | 6 +-
CHANGELOG.md | 352 +-
README.md | 67 +-
docs/images/report-warning.png | Bin 217660 -> 0 bytes
docs/images/report.png | Bin 207473 -> 0 bytes
module/PRA.Gui.Directory.ps1 | 37 -
.../Invoke-PraRemoteMailbox.ps1 | 203 +-
package/LICENSE | 21 +
package/PackageIntegrity.psd1 | 218 ++
package/README.md | 77 +
.../config}/PraRemoteMailbox.config.psd1 | 6 +-
.../config}/SharedPermissions.sample.csv | 4 +-
{config => package/config}/Targets.sample.csv | 6 +-
.../docs}/PraRemoteMailbox-Guide.html | 444 ++-
.../docs}/PraRemoteMailbox-Guide.md | 1787 ++++-----
.../docs}/images/console-apply.png | Bin
.../docs}/images/console-preview.png | Bin
{docs => package/docs}/images/gui-fluent.png | Bin
.../docs}/images/readme-banner-dark.png | Bin
.../docs}/images/readme-banner-light.png | Bin
.../docs}/images/readme-flows-dark.png | Bin
.../docs}/images/readme-flows-light.png | Bin
.../docs}/images/readme-how-dark.png | Bin
.../docs}/images/readme-how-light.png | Bin
.../docs}/images/readme-permissions-dark.png | Bin
.../docs}/images/readme-permissions-light.png | Bin
.../docs}/images/readme-principles-dark.png | Bin
.../docs}/images/readme-principles-light.png | Bin
package/docs/images/report-warning.png | Bin 0 -> 226057 bytes
package/docs/images/report.png | Bin 0 -> 236744 bytes
{module => package/module}/PRA.Backup.psm1 | 1727 +++++----
{module => package/module}/PRA.Cloud.psm1 | 2225 ++++++-----
{module => package/module}/PRA.Common.psm1 | 2460 +++++++-----
{module => package/module}/PRA.Directory.psm1 | 3423 +++++++++--------
package/module/PRA.Gui.Directory.ps1 | 234 ++
{module => package/module}/PRA.Gui.psm1 | 2081 +++++-----
{module => package/module}/PRA.Gui.xaml | 384 +-
package/module/Test-PackageIntegrity.ps1 | 328 ++
package/templates/Report.template.html | 480 +++
templates/Report.template.html | 112 -
tests/IntegrityFixture.ps1 | 105 +
tests/Invoke-TestGate.ps1 | 14 +-
tests/PackageIntegrity.Tests.ps1 | 355 ++
tests/PraRemoteMailbox.Gui.Tests.ps1 | 9 +-
tests/PraRemoteMailbox.Tests.ps1 | 264 +-
tests/PraReport.Tests.ps1 | 179 +
tests/ReportFixture.ps1 | 119 +
tools/Build-Documentation.ps1 | 6 +-
tools/New-PraPackage.ps1 | 68 +-
tools/New-ReadmeImages.ps1 | 10 +-
51 files changed, 10802 insertions(+), 7049 deletions(-)
create mode 100644 .gitattributes
delete mode 100644 docs/images/report-warning.png
delete mode 100644 docs/images/report.png
delete mode 100644 module/PRA.Gui.Directory.ps1
rename Invoke-PraRemoteMailbox.ps1 => package/Invoke-PraRemoteMailbox.ps1 (80%)
create mode 100644 package/LICENSE
create mode 100644 package/PackageIntegrity.psd1
create mode 100644 package/README.md
rename {config => package/config}/PraRemoteMailbox.config.psd1 (97%)
rename {config => package/config}/SharedPermissions.sample.csv (97%)
rename {config => package/config}/Targets.sample.csv (94%)
rename {docs => package/docs}/PraRemoteMailbox-Guide.html (56%)
rename {docs => package/docs}/PraRemoteMailbox-Guide.md (91%)
rename {docs => package/docs}/images/console-apply.png (100%)
rename {docs => package/docs}/images/console-preview.png (100%)
rename {docs => package/docs}/images/gui-fluent.png (100%)
rename {docs => package/docs}/images/readme-banner-dark.png (100%)
rename {docs => package/docs}/images/readme-banner-light.png (100%)
rename {docs => package/docs}/images/readme-flows-dark.png (100%)
rename {docs => package/docs}/images/readme-flows-light.png (100%)
rename {docs => package/docs}/images/readme-how-dark.png (100%)
rename {docs => package/docs}/images/readme-how-light.png (100%)
rename {docs => package/docs}/images/readme-permissions-dark.png (100%)
rename {docs => package/docs}/images/readme-permissions-light.png (100%)
rename {docs => package/docs}/images/readme-principles-dark.png (100%)
rename {docs => package/docs}/images/readme-principles-light.png (100%)
create mode 100644 package/docs/images/report-warning.png
create mode 100644 package/docs/images/report.png
rename {module => package/module}/PRA.Backup.psm1 (76%)
rename {module => package/module}/PRA.Cloud.psm1 (84%)
rename {module => package/module}/PRA.Common.psm1 (63%)
rename {module => package/module}/PRA.Directory.psm1 (87%)
create mode 100644 package/module/PRA.Gui.Directory.ps1
rename {module => package/module}/PRA.Gui.psm1 (80%)
rename {module => package/module}/PRA.Gui.xaml (98%)
create mode 100644 package/module/Test-PackageIntegrity.ps1
create mode 100644 package/templates/Report.template.html
delete mode 100644 templates/Report.template.html
create mode 100644 tests/IntegrityFixture.ps1
create mode 100644 tests/PackageIntegrity.Tests.ps1
create mode 100644 tests/PraReport.Tests.ps1
create mode 100644 tests/ReportFixture.ps1
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..237eb06
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,40 @@
+# Signed and hashed package files are byte-exact: no end-of-line conversion, so the Authenticode signature and the manifest SHA256 stay valid.
+# Do not add a global autocrlf rule for these paths.
+package/docs/** linguist-documentation
+
+# Owner-signed files (eight PowerShell files and the manifest); cr-at-eol because their working-tree line endings are CRLF.
+package/Invoke-PraRemoteMailbox.ps1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/module/PRA.Common.psm1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/module/PRA.Directory.psm1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/module/PRA.Backup.psm1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/module/PRA.Cloud.psm1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/module/PRA.Gui.psm1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/module/PRA.Gui.Directory.ps1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/module/Test-PackageIntegrity.ps1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/PackageIntegrity.psd1 -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+
+# Stable hashed files (SHA256 in the signed manifest) and the distribution copy of the changelog.
+package/module/PRA.Gui.xaml -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/templates/Report.template.html -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/config/Targets.sample.csv -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/config/SharedPermissions.sample.csv -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/README.md -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/LICENSE -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/docs/PraRemoteMailbox-Guide.html -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/docs/PraRemoteMailbox-Guide.md -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
+package/docs/images/console-apply.png -text
+package/docs/images/console-preview.png -text
+package/docs/images/gui-fluent.png -text
+package/docs/images/readme-banner-dark.png -text
+package/docs/images/readme-banner-light.png -text
+package/docs/images/readme-flows-dark.png -text
+package/docs/images/readme-flows-light.png -text
+package/docs/images/readme-how-dark.png -text
+package/docs/images/readme-how-light.png -text
+package/docs/images/readme-permissions-dark.png -text
+package/docs/images/readme-permissions-light.png -text
+package/docs/images/readme-principles-dark.png -text
+package/docs/images/readme-principles-light.png -text
+package/docs/images/report-warning.png -text
+package/docs/images/report.png -text
+CHANGELOG.md -text whitespace=blank-at-eol,space-before-tab,blank-at-eof,cr-at-eol
diff --git a/.github/ci.json b/.github/ci.json
index b9f16fe..48bc505 100644
--- a/.github/ci.json
+++ b/.github/ci.json
@@ -1,10 +1,12 @@
{
- "runtimeRoot": ".",
+ "runtimeRoot": "package",
"testMode": "gate",
"testEntry": "tests/Invoke-TestGate.ps1",
"tests": [
"tests/PraRemoteMailbox.Tests.ps1",
- "tests/PraRemoteMailbox.Gui.Tests.ps1"
+ "tests/PraRemoteMailbox.Gui.Tests.ps1",
+ "tests/PackageIntegrity.Tests.ps1",
+ "tests/PraReport.Tests.ps1"
],
"packageBuilder": "tools/New-PraPackage.ps1",
"packageEntry": "Invoke-PraRemoteMailbox.ps1"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca46997..6e01476 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,156 +1,196 @@
-# Changelog
-
-All notable changes of PRA Remote Mailbox. Author: Nicolas Fabert. Versions follow `MAJOR.MINOR.PATCH`:
-a MAJOR version changes the command line or the configuration file, a MINOR version adds a feature,
-a PATCH version fixes a defect. The backup formats have their own version (see the guide, annex D).
-
-## 2.1.0 — 2026-10-09
-
-### Added
-- **Direct target selection for Convert AD/Both:** configuration, OU (DN or read-only directory
- picker) and CSV (Identity column, comma/semicolon). Run-specific `-SearchBase`/`-CsvPath` overrides
- never rewrite the configuration; source/file changes invalidate Preview. Users and shared
- mailboxes continue through the same eligibility, duplicate and backup guards.
-- **WPF window** (`-Gui`) for Convert, Recover, Finalize and Check, with explicit AD + cloud, AD-only
- and cloud-only execution. Native Fluent with PowerShell 7.5+; compatible WPF controls with Windows
- PowerShell 5.1. Every operation still runs the existing Windows PowerShell 5.1 engine.
-- Preview before Apply, typed confirmation, batch selection, progress and activity, result rows,
- log/report access and the next-server handoff instructions. Changing the request or its input
- files invalidates the preview. A failed or incomplete preview never authorizes Apply.
-- Optional structured JSONL events from the common module for the window; ordinary CLI output,
- reports, transcripts and backup formats stay unchanged. GUI module and XAML included in packaging.
-- Offline GUI Pester suite and regressions for event/result coherence and split recovery.
-
-### Changed
-- **Recover -Phase Cloud now always packages final AD restoration**, even when ActiveDirectory is
- installed and `DeferOnPremRestore = $false`. Run Finalize on the AD server using the package printed
- in the result. Phase Both retains its existing inline restoration unless explicitly deferred.
-- `-Gui` can run in either PowerShell edition; actual command-line operations still require Desktop.
- No GUI action runs live on startup, and the window cannot close while its engine is running.
-
-### Fixed
-- Large-batch receipt fingerprints and raw backup validation avoid redundant per-string
- PowerShell binding and regex callbacks. Structural hashes, raw XML bytes, live tamper checks
- and the existing memory/time watchdogs remain unchanged.
-- The offline gate validates structured help metadata instead of matching localized headings.
- French and English help both pass, while a missing synopsis, insufficient examples or a missing
- real `WhatIf` parameter still fail the gate.
-
-This version has offline validation only; no new live-tenant campaign has been performed.
-
-## 2.0.1 — 2026-10-07
-
-### Fixed
-- **Scheduled task / `powershell.exe -File`:** Windows PowerShell 5.1 leaves `$PSScriptRoot` empty in the
- default values of `param()` when a script runs with `-File`. The default `-ConfigPath` of
- `Invoke-PraRemoteMailbox.ps1` (and `-Source` / `-Destination` of `tools\Build-Documentation.ps1`) was built
- there, so the script stopped at once with `Join-Path: Cannot bind argument to parameter 'Path' because it is an
- empty string`. The defaults are now resolved in the script body; an explicit `-ConfigPath` behaves as before.
- Found while building the scenario 2 tool (PRA Cloud Mailbox), whose Collect runs as a scheduled task.
-
-### Tests
-- New block "Scripts start with powershell.exe -File (scheduled task)": no `param()` default of the entry script
- or of the tools may read `$PSScriptRoot`, `$PSCommandPath` or `$MyInvocation`.
-
-## 2.0.0 — 2026-10-01
-
-Rewrite of the operator experience on the model of Purview DLP Report: one entry script driven by the
-configuration file, a readable console, one guide. **The safety engine is unchanged** (backup before
-any write, receipts and SHA-256, AD proofs, stop at the first error); backups written by 1.3.x are read
-by 2.0.0 (tested in the lab: Recover of a 1.3.6 batch).
-
-### Changed — command line and files (breaking)
-
-| 1.3.6 | 2.0.0 |
-|---|---|
-| `PRA-RemoteMailbox.ps1` in `release\` | `Invoke-PraRemoteMailbox.ps1` at the root of the tool folder |
-| `config\DRP-Config.psd1` | `config\PraRemoteMailbox.config.psd1` (English comments, unknown keys refused) |
-| `-Action Convert\|Recover\|CloudCheck` | `-Action Convert\|Recover\|Finalize\|Check` |
-| `-Mode Inventory\|Simulate\|Apply` (default Inventory) | `-Mode Preview\|Apply` (default Preview); `-WhatIf` = Preview |
-| `-Phase Both\|AD\|Cloud\|Finalize\|TagCleanup` | `-Phase Both\|AD\|Cloud`, default `Execution.Phase`; Finalize is an action |
-| `-BackupFile ` + `-StateFile ` | `-Batch ` (the 8 characters printed at the end of the previous step) or a path |
-| `-MailboxScope` | `-Scope` |
-| `-CloudCheckScope`, `-VerifyRetention` | `-Expect Provisioned\|Deprovisioned\|Retained` |
-| `-SyncADConnect` | `EntraConnect.Sync` (default `$true`), local or remote (`EntraConnect.Server`) |
-| `-DeferSharedRestore`, `-KeepCloudShared` | `SharedMailbox.DeferOnPremRestore`, `SharedMailbox.KeepCloudSharedOnRecover` |
-| `-DomainController`, `-BackupDirectory`, `-LogPath`, `-TranscriptPath`, `-ReportDirectory`, `-NoReport`, `-IntervalMinutes`, `-TimeoutMinutes` | configuration only: `DomainController`, `Storage`, `Logging`, `Report`, `Polling` |
-| `-SkipCloudCheck`, `-DeprovisionCloudShared` (obsolete aliases) | removed |
-| `ADConnect.WaitForSync`, `SharedMailbox.VerifyTrusteeInCloud` (ignored by 1.3.x) | removed (the tool always waits; trustees are always checked) |
-| exit code 0 also for Pending | 0 = done, 1 = failed, **2 = done, next step required** (Pending) |
-
-### Added
-
-- **One confirmation per run** after the plan is shown (instead of one prompt per AD write); `-Force` for unattended runs.
-- **Batch IDs**: every Apply prints a short batch ID and the exact **next command**; `-Batch` finds the batch
- folder, its AD proof, and (cloud phase of a Recover) its Convert source by SHA-256.
-- **Remote Entra Connect synchronisation** (`EntraConnect.Server`, PowerShell remoting) and a check of the
- synchronisation **before** the first AD write (1.3.x failed after the AD writes when ADSync was missing).
-- Console in the style of Purview DLP Report: banner, numbered steps, compact before/after view (only the
- attributes that change), summary card with the result, the batch and the next step. Full values stay in the log.
-- HTML report with tiles, status badges and a filter (`templates\Report.template.html`).
-- The plan of a shared mailbox lists **who will get FullAccess, SendAs and SendOnBehalf** (first holders on
- screen, complete lists in the log), so that the permissions can be checked in Preview.
-- **Permission entries of an account no longer in AD** ("Account Unknown": deleted, or another domain) are
- ignored and reported as **warnings** — yellow line in the console, `[WARN]` in the log, CSV column `Warnings`,
- *Warnings* tile and card in the HTML report, yellow summary card. 1.3.x stopped the whole batch on them.
- Warnings never change the exit code. The result object (`-PassThru`) has `WarningCount` and `Warnings`.
-- Preview of a cloud phase: one pass, never waits; missing permissions are shown "to grant" (1.3.x reported an error).
-- `tools\New-PraPackage.ps1` (delivery package with the environment values emptied) and
- `tools\Build-Documentation.ps1` (HTML guide).
-- Single guide `docs\PraRemoteMailbox-Guide.md` / `.html` (replaces the operator guide, the technical
- documentation and the release notes of 1.3.x).
-- `-PassThru`: returns the result object of the run (status, exit code, batch ID, next steps, paths) for
- scripts and orchestrators. Without it the tool writes nothing to the pipeline (1.3.x always did, which
- printed a raw object after the summary in an interactive console).
-
-### Fixed
-
-- **Certificate (app-only) sign-in to Exchange Online always failed**: the session guard compared
- `Get-ConnectionInformation.TenantID` with the tenant GUID, but app-only sessions report the organisation
- domain. The guard now accepts the configured `Cloud.Organization` (and checks the `AppId`). This is the
- "certificate returns UnAuthorized" blocker noted in the 1.3.4 pilot.
-- The AD module no longer creates the `AD:` drive on a default domain controller at import.
-- The PowerShell 5.1 transcript is readable again (one `Write-Host` per line).
-- Before each FullAccess grant, Exchange Online answers "No permissions were found" (the normal answer when
- the right is not there yet). It was read with `-ErrorAction Stop`, so Windows PowerShell 5.1 wrote it in the
- transcript as a `TerminatingError` line that looked like a failure. It is now read through `-ErrorVariable`:
- no line in the transcript, and any other error still stops the run as before (lab T16: 0 such line).
-- **Recover stopped on a normal transient state** (lab T17): right after the sync, while the cloud shared mailbox
- turned into a MailUser, Exchange Online briefly answered "object couldn't be found" (HTTP 404) for the
- recipient. The check took it as an error and stopped (nothing restored on-premises, safe, but the operator had
- to start again). This exact answer now means "not there yet": the check waits and reads again. Any other 404
- or error still stops.
-- **Check (or Convert) after another run in the same console**: Microsoft Graph could not load next to the
- Exchange Online assemblies of the previous run and failed with an obscure `GetTokenAsync ... does not have an
- implementation`. The message now says what to do: run the tool in a new Windows PowerShell window.
-
-### Code review 2026-09-30 — other findings
-
-See the guide, annex F: dead settings, duplicated mode values, per-write prompts, implicit selection of the
-"most recent" backup (removed), messages in two languages, 358-character lines, French/English mix. All
-addressed in 2.0.0. No defect of the backup/proof engine was found.
-
-### Validation
-
-- Test gate on VMLLM (Windows PowerShell 5.1, Pester 6.0.0, PSScriptAnalyzer 1.25.0): **279 / 279 passed**,
- 0 analyzer error (evidence `tests\evidence\gate\v2.0.0-release`). The 61 tests of the removed path options
- of 1.3.6 are replaced by tests of `-Batch` and of the confirmation (1.3.6: 325 tests).
-- Lab campaign 2026-09-30 / 2026-10-01 on `exlab.lab` + tenant M365CPI89259425 (real AD, Entra Connect and
- Exchange Online): 28 runs — Convert, Recover on one and two servers, Finalize, Check, Recover of a 1.3.6
- batch, shared mailbox permissions given to a group (nested group included), SendAs actually granted,
- SendOnBehalf (guide, annex B). 26 PASS; the 5 defects found (approval callback with `-Force` in a
- non-interactive process; certificate session guard; recipient 404 during the Recover check; Graph after
- Exchange Online in the same console; transcript lines) are fixed and covered by the gate.
-
-## 1.3.x — 2026-09-07 to 2026-09-10 (summary)
-
-| Version | Change |
-|---|---|
-| 1.3.6 | Exchange Online pre-check before the AD writes of a Convert (phase Both); bounded verification after one grant; invalid path characters reported precisely. |
-| 1.3.5 | Shared mailboxes provisioned with `msExchRemoteRecipientType` 97 (99 with archive); 100/102 refused for a new Convert. |
-| 1.3.4 | Capture of the declared properties of a native ADUser only. |
-| 1.3.3 | Data-only CLIXML capture written and validated as a stream (backup schema 3), bounded permission cache: the out-of-memory of a 13 shared mailbox batch cannot happen any more. |
-| 1.3.2 | Transcript start on hosts that create the file late. |
-| 1.3.1 | Before / planned / verified values in the console and the log. |
-| 1.3.0 | Backup of the whole batch **before** the first write, AD proofs, stop at the first error (fix of the 1.2.5 incident: the backup was written after the writes). |
-| 1.2.5 | Original script (June 2026). |
+# Changelog
+
+All notable changes of PRA Remote Mailbox. Author: Nicolas Fabert. Versions follow `MAJOR.MINOR.PATCH`:
+a MAJOR version changes the command line or the configuration file, a MINOR version adds a feature,
+a PATCH version fixes a defect. The backup formats have their own version (see the guide, annex D).
+
+## 2.2.0 — Unreleased (source only; no tag, no release)
+
+### Added
+- **Signed-package integrity (Windows PowerShell 5.1).** `module\Test-PackageIntegrity.ps1` and the
+ manifest `PackageIntegrity.psd1` verify, before any code runs, the owner's timestamped Authenticode
+ signature (fixed thumbprint) of the verifier, the manifest and every PowerShell file, and the SHA256 of
+ every stable file (modules, XAML, report template, sample CSV files, README, licence, guide, images).
+ The entry script, each module and the GUI helper run the same bootstrap first. A changed, missing,
+ duplicated, unexpected or reparse-point file fails closed; there is no unsigned mode or bypass.
+ The configuration and the operational CSV lists in `config\` are data, outside the check, as are
+ `Backups\`, `logs\` and `reports\`. A backup, log or report folder inside the package other than its own
+ canonical `Backups`, `logs` or `reports` folder (a sub-folder is fine) is now refused before any file is
+ written, judged on the physical location (a `\\?\` prefix, junction, SUBST drive, 8.3 name or `Documents and Settings` alias cannot hide the package; an unresolvable path is refused; external paths are unchanged): such files would be rejected at the next start.
+ The final manifest is generated from the finalized signed and timestamped runtime bytes.
+- `tests\PackageIntegrity.Tests.ps1`: static inventory/bootstrap checks, a controlled fixture (only the
+ signature call is substituted) for tamper, missing, unexpected, duplicate, manifest-schema and reparse
+ cases, builder checks, and the native signed-package or unsigned fail-closed state of the real package.
+
+- **Report export snapshot.** The filtered CSV export in the report page now iterates a copy of the rows selected
+ when it starts, so changing a filter while it runs cannot change the exported rows or the reported count.
+
+- **High-volume HTML report.** The report page no longer builds a table per row. The rows (22 fields),
+ run errors and warnings are streamed by a Windows PowerShell 5.1 writer into dictionary-indexed,
+ gzip+base64 chunks of at most 20,000 rows; the page decodes all chunks when it opens (eager, not lazy)
+ and shows virtualized *Summary* and *All 22 fields* grids with at most 200 data rows in the page.
+ Status/type/action/flag/text filters (AND, over every row), a search over the 22 fields, keyboard
+ navigation, a details dialog, paged warnings and run errors, and a filtered CSV export of all matching
+ rows (BOM, quoted, formulas neutralised). A current desktop Edge or Chrome is required
+ (`DecompressionStream`); otherwise the page explains it and the CSV holds every row. Compression is not
+ encryption. The server CSV is unchanged.
+- `tests\PraReport.Tests.ps1` with synthetic 50,000 and 100,000 row reports (exact counts, chunk
+ boundaries, last row, escaping, fields) and `tests\ReportFixture.ps1`.
+
+### Changed
+- The runtime moved to the `package\` folder (entry, `module\`, `config\`, `templates\`, `docs\`, runtime
+ README and licence). Inside the package, and in the extracted release, every path is unchanged. Tests,
+ tools and the gate stay outside the distribution. `tools\New-PraPackage.ps1` now builds from
+ `package\`, verifies the signed source first and refuses an unsigned or changed one.
+- Documentation and tools (`Build-Documentation.ps1`, `New-ReadmeImages.ps1`) use the new paths.
+
+## 2.1.0 — 2026-10-09
+
+### Added
+- **Direct target selection for Convert AD/Both:** configuration, OU (DN or read-only directory
+ picker) and CSV (Identity column, comma/semicolon). Run-specific `-SearchBase`/`-CsvPath` overrides
+ never rewrite the configuration; source/file changes invalidate Preview. Users and shared
+ mailboxes continue through the same eligibility, duplicate and backup guards.
+- **WPF window** (`-Gui`) for Convert, Recover, Finalize and Check, with explicit AD + cloud, AD-only
+ and cloud-only execution. Native Fluent with PowerShell 7.5+; compatible WPF controls with Windows
+ PowerShell 5.1. Every operation still runs the existing Windows PowerShell 5.1 engine.
+- Preview before Apply, typed confirmation, batch selection, progress and activity, result rows,
+ log/report access and the next-server handoff instructions. Changing the request or its input
+ files invalidates the preview. A failed or incomplete preview never authorizes Apply.
+- Optional structured JSONL events from the common module for the window; ordinary CLI output,
+ reports, transcripts and backup formats stay unchanged. GUI module and XAML included in packaging.
+- Offline GUI Pester suite and regressions for event/result coherence and split recovery.
+
+### Changed
+- **Recover -Phase Cloud now always packages final AD restoration**, even when ActiveDirectory is
+ installed and `DeferOnPremRestore = $false`. Run Finalize on the AD server using the package printed
+ in the result. Phase Both retains its existing inline restoration unless explicitly deferred.
+- `-Gui` can run in either PowerShell edition; actual command-line operations still require Desktop.
+ No GUI action runs live on startup, and the window cannot close while its engine is running.
+
+### Fixed
+- Large-batch receipt fingerprints and raw backup validation avoid redundant per-string
+ PowerShell binding and regex callbacks. Structural hashes, raw XML bytes, live tamper checks
+ and the existing memory/time watchdogs remain unchanged.
+- The offline gate validates structured help metadata instead of matching localized headings.
+ French and English help both pass, while a missing synopsis, insufficient examples or a missing
+ real `WhatIf` parameter still fail the gate.
+
+This version has offline validation only; no new live-tenant campaign has been performed.
+
+## 2.0.1 — 2026-10-07
+
+### Fixed
+- **Scheduled task / `powershell.exe -File`:** Windows PowerShell 5.1 leaves `$PSScriptRoot` empty in the
+ default values of `param()` when a script runs with `-File`. The default `-ConfigPath` of
+ `Invoke-PraRemoteMailbox.ps1` (and `-Source` / `-Destination` of `tools\Build-Documentation.ps1`) was built
+ there, so the script stopped at once with `Join-Path: Cannot bind argument to parameter 'Path' because it is an
+ empty string`. The defaults are now resolved in the script body; an explicit `-ConfigPath` behaves as before.
+ Found while building the scenario 2 tool (PRA Cloud Mailbox), whose Collect runs as a scheduled task.
+
+### Tests
+- New block "Scripts start with powershell.exe -File (scheduled task)": no `param()` default of the entry script
+ or of the tools may read `$PSScriptRoot`, `$PSCommandPath` or `$MyInvocation`.
+
+## 2.0.0 — 2026-10-01
+
+Rewrite of the operator experience on the model of Purview DLP Report: one entry script driven by the
+configuration file, a readable console, one guide. **The safety engine is unchanged** (backup before
+any write, receipts and SHA-256, AD proofs, stop at the first error); backups written by 1.3.x are read
+by 2.0.0 (tested in the lab: Recover of a 1.3.6 batch).
+
+### Changed — command line and files (breaking)
+
+| 1.3.6 | 2.0.0 |
+|---|---|
+| `PRA-RemoteMailbox.ps1` in `release\` | `Invoke-PraRemoteMailbox.ps1` at the root of the tool folder |
+| `config\DRP-Config.psd1` | `config\PraRemoteMailbox.config.psd1` (English comments, unknown keys refused) |
+| `-Action Convert\|Recover\|CloudCheck` | `-Action Convert\|Recover\|Finalize\|Check` |
+| `-Mode Inventory\|Simulate\|Apply` (default Inventory) | `-Mode Preview\|Apply` (default Preview); `-WhatIf` = Preview |
+| `-Phase Both\|AD\|Cloud\|Finalize\|TagCleanup` | `-Phase Both\|AD\|Cloud`, default `Execution.Phase`; Finalize is an action |
+| `-BackupFile ` + `-StateFile ` | `-Batch ` (the 8 characters printed at the end of the previous step) or a path |
+| `-MailboxScope` | `-Scope` |
+| `-CloudCheckScope`, `-VerifyRetention` | `-Expect Provisioned\|Deprovisioned\|Retained` |
+| `-SyncADConnect` | `EntraConnect.Sync` (default `$true`), local or remote (`EntraConnect.Server`) |
+| `-DeferSharedRestore`, `-KeepCloudShared` | `SharedMailbox.DeferOnPremRestore`, `SharedMailbox.KeepCloudSharedOnRecover` |
+| `-DomainController`, `-BackupDirectory`, `-LogPath`, `-TranscriptPath`, `-ReportDirectory`, `-NoReport`, `-IntervalMinutes`, `-TimeoutMinutes` | configuration only: `DomainController`, `Storage`, `Logging`, `Report`, `Polling` |
+| `-SkipCloudCheck`, `-DeprovisionCloudShared` (obsolete aliases) | removed |
+| `ADConnect.WaitForSync`, `SharedMailbox.VerifyTrusteeInCloud` (ignored by 1.3.x) | removed (the tool always waits; trustees are always checked) |
+| exit code 0 also for Pending | 0 = done, 1 = failed, **2 = done, next step required** (Pending) |
+
+### Added
+
+- **One confirmation per run** after the plan is shown (instead of one prompt per AD write); `-Force` for unattended runs.
+- **Batch IDs**: every Apply prints a short batch ID and the exact **next command**; `-Batch` finds the batch
+ folder, its AD proof, and (cloud phase of a Recover) its Convert source by SHA-256.
+- **Remote Entra Connect synchronisation** (`EntraConnect.Server`, PowerShell remoting) and a check of the
+ synchronisation **before** the first AD write (1.3.x failed after the AD writes when ADSync was missing).
+- Console in the style of Purview DLP Report: banner, numbered steps, compact before/after view (only the
+ attributes that change), summary card with the result, the batch and the next step. Full values stay in the log.
+- HTML report with tiles, status badges and a filter (`templates\Report.template.html`).
+- The plan of a shared mailbox lists **who will get FullAccess, SendAs and SendOnBehalf** (first holders on
+ screen, complete lists in the log), so that the permissions can be checked in Preview.
+- **Permission entries of an account no longer in AD** ("Account Unknown": deleted, or another domain) are
+ ignored and reported as **warnings** — yellow line in the console, `[WARN]` in the log, CSV column `Warnings`,
+ *Warnings* tile and card in the HTML report, yellow summary card. 1.3.x stopped the whole batch on them.
+ Warnings never change the exit code. The result object (`-PassThru`) has `WarningCount` and `Warnings`.
+- Preview of a cloud phase: one pass, never waits; missing permissions are shown "to grant" (1.3.x reported an error).
+- `tools\New-PraPackage.ps1` (delivery package with the environment values emptied) and
+ `tools\Build-Documentation.ps1` (HTML guide).
+- Single guide `docs\PraRemoteMailbox-Guide.md` / `.html` (replaces the operator guide, the technical
+ documentation and the release notes of 1.3.x).
+- `-PassThru`: returns the result object of the run (status, exit code, batch ID, next steps, paths) for
+ scripts and orchestrators. Without it the tool writes nothing to the pipeline (1.3.x always did, which
+ printed a raw object after the summary in an interactive console).
+
+### Fixed
+
+- **Certificate (app-only) sign-in to Exchange Online always failed**: the session guard compared
+ `Get-ConnectionInformation.TenantID` with the tenant GUID, but app-only sessions report the organisation
+ domain. The guard now accepts the configured `Cloud.Organization` (and checks the `AppId`). This is the
+ "certificate returns UnAuthorized" blocker noted in the 1.3.4 pilot.
+- The AD module no longer creates the `AD:` drive on a default domain controller at import.
+- The PowerShell 5.1 transcript is readable again (one `Write-Host` per line).
+- Before each FullAccess grant, Exchange Online answers "No permissions were found" (the normal answer when
+ the right is not there yet). It was read with `-ErrorAction Stop`, so Windows PowerShell 5.1 wrote it in the
+ transcript as a `TerminatingError` line that looked like a failure. It is now read through `-ErrorVariable`:
+ no line in the transcript, and any other error still stops the run as before (lab T16: 0 such line).
+- **Recover stopped on a normal transient state** (lab T17): right after the sync, while the cloud shared mailbox
+ turned into a MailUser, Exchange Online briefly answered "object couldn't be found" (HTTP 404) for the
+ recipient. The check took it as an error and stopped (nothing restored on-premises, safe, but the operator had
+ to start again). This exact answer now means "not there yet": the check waits and reads again. Any other 404
+ or error still stops.
+- **Check (or Convert) after another run in the same console**: Microsoft Graph could not load next to the
+ Exchange Online assemblies of the previous run and failed with an obscure `GetTokenAsync ... does not have an
+ implementation`. The message now says what to do: run the tool in a new Windows PowerShell window.
+
+### Code review 2026-09-30 — other findings
+
+See the guide, annex F: dead settings, duplicated mode values, per-write prompts, implicit selection of the
+"most recent" backup (removed), messages in two languages, 358-character lines, French/English mix. All
+addressed in 2.0.0. No defect of the backup/proof engine was found.
+
+### Validation
+
+- Test gate on VMLLM (Windows PowerShell 5.1, Pester 6.0.0, PSScriptAnalyzer 1.25.0): **279 / 279 passed**,
+ 0 analyzer error (evidence `tests\evidence\gate\v2.0.0-release`). The 61 tests of the removed path options
+ of 1.3.6 are replaced by tests of `-Batch` and of the confirmation (1.3.6: 325 tests).
+- Lab campaign 2026-09-30 / 2026-10-01 on `exlab.lab` + tenant M365CPI89259425 (real AD, Entra Connect and
+ Exchange Online): 28 runs — Convert, Recover on one and two servers, Finalize, Check, Recover of a 1.3.6
+ batch, shared mailbox permissions given to a group (nested group included), SendAs actually granted,
+ SendOnBehalf (guide, annex B). 26 PASS; the 5 defects found (approval callback with `-Force` in a
+ non-interactive process; certificate session guard; recipient 404 during the Recover check; Graph after
+ Exchange Online in the same console; transcript lines) are fixed and covered by the gate.
+
+## 1.3.x — 2026-09-07 to 2026-09-10 (summary)
+
+| Version | Change |
+|---|---|
+| 1.3.6 | Exchange Online pre-check before the AD writes of a Convert (phase Both); bounded verification after one grant; invalid path characters reported precisely. |
+| 1.3.5 | Shared mailboxes provisioned with `msExchRemoteRecipientType` 97 (99 with archive); 100/102 refused for a new Convert. |
+| 1.3.4 | Capture of the declared properties of a native ADUser only. |
+| 1.3.3 | Data-only CLIXML capture written and validated as a stream (backup schema 3), bounded permission cache: the out-of-memory of a 13 shared mailbox batch cannot happen any more. |
+| 1.3.2 | Transcript start on hosts that create the file late. |
+| 1.3.1 | Before / planned / verified values in the console and the log. |
+| 1.3.0 | Backup of the whole batch **before** the first write, AD proofs, stop at the first error (fix of the 1.2.5 incident: the backup was written after the writes). |
+| 1.2.5 | Original script (June 2026). |
diff --git a/README.md b/README.md
index 56c4e7b..eb9be07 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
> [!IMPORTANT]
@@ -32,20 +32,21 @@ In a hybrid organisation, a mailbox hosted on-premises is only a *mail user* for
Done by hand, this means rewriting about ten Exchange attributes per object in Active Directory, for hundreds of objects, under stress — and keeping every original value to come back later: a wrong or lost value cannot be repaired without a backup. This tool does the conversion in a planned, verified and **reversible** way.
-**Author:** Nicolas Fabert · **Version:** 2.1.0 ([changelog](CHANGELOG.md)).
+**Author:** Nicolas Fabert · **Version:** 2.2.0, unreleased source ([changelog](CHANGELOG.md)).
-
-
+
+
## How it works
-
-
+
+
+- **Signed package.** The runtime lives in `package\` (same layout as the extracted release ZIP). Every runtime file authenticates the owner-signed manifest before it runs; a changed, missing or unexpected stable file stops the tool before any import or operation. The configuration and your CSV lists are data, outside that check. There is no unsigned mode. The tool refuses to run from any tree whose files are not signed by the owner: use a signed release package. Backup, log and report folders inside the package are limited to the canonical `Backups`, `logs` and `reports`. See [package/README.md](package/README.md).
- **One script, one configuration file.** The scope is one object (`-Identity`), an OU, the members of an AD group or a CSV list — users only, shared mailboxes only, or both.
- **Preview by default.** Without `-Mode Apply` the tool only reads. With `-Mode Apply` it shows the plan and asks **one** confirmation (`-Force` for unattended runs).
- **One batch ID** links the steps: every Apply prints it with the exact next command, and `-Action Recover -Batch ` rolls the batch back.
@@ -53,8 +54,8 @@ Done by hand, this means rewriting about ten Exchange attributes per object in A
## WPF window
-Open the window with `pwsh -STA -File .\Invoke-PraRemoteMailbox.ps1 -Gui` for native Fluent on
-PowerShell 7.5+, or `powershell.exe -STA -File .\Invoke-PraRemoteMailbox.ps1 -Gui` for compatible
+Open the window with `pwsh -STA -File .\package\Invoke-PraRemoteMailbox.ps1 -Gui` for native Fluent on
+PowerShell 7.5+, or `powershell.exe -STA -File .\package\Invoke-PraRemoteMailbox.ps1 -Gui` for compatible
WPF controls. It starts no operation automatically; each operation runs the existing engine in a
separate **Windows PowerShell 5.1** process.
@@ -72,13 +73,13 @@ and produces a Finalize package if AD restoration remains; **Finalize** runs on
Cloud-only never restores AD, even with RSAT installed. Copy complete batch folders between servers;
the window does not transfer them. See the administrator guide, chapter 8.
-
+
## Convert and Recover
-
-
+
+
What a Convert changes in Active Directory (a Recover puts the saved values back):
@@ -99,8 +100,8 @@ What a Convert changes in Active Directory (a Recover puts the saved values back
## Shared mailbox permissions
-
-
+
+
Exchange on-premises is down, so the tool reads the permissions where Exchange keeps a copy: on the account of the shared mailbox in Active Directory. The preview lists who will get each right; the log has the complete lists. A permission already present in Exchange Online is only verified, never added twice.
@@ -109,12 +110,12 @@ Exchange on-premises is down, so the tool reads the permissions where Exchange k
-
Preview · the plan of each object: only the attributes that change, the holders of each permission; nothing is written
-
Apply · pre-check, backup, AD writes, Entra Connect, Exchange Online; summary card with the batch ID and the next command
+
Preview · the plan of each object: only the attributes that change, the holders of each permission; nothing is written
+
Apply · pre-check, backup, AD writes, Entra Connect, Exchange Online; summary card with the batch ID and the next command
-
HTML report · counters, next step, one row per object with its AD, licence, Exchange Online and permission status
-
Warnings · a permission of a deleted account is ignored and shown in the console, the log, the CSV and the report
+
HTML report · counters, next step, then virtualized, filterable rows (tested to 100,000 rows, at most 200 rows in the page) with a filtered CSV export; needs current desktop Edge or Chrome
+
Warnings · a permission of a deleted account is ignored and shown in the console, the log, the CSV and the report
@@ -136,21 +137,21 @@ Every run also writes a log, a PowerShell transcript and a CSV. Exit codes: 0 =
```powershell
git clone https://github.com/Nico77600/PraRemoteMailbox.git
cd PraRemoteMailbox
-notepad .\config\PraRemoteMailbox.config.psd1 # scope, licence group, routing domain, Entra Connect, tenant
+notepad .\package\config\PraRemoteMailbox.config.psd1 # scope, licence group, routing domain, Entra Connect, tenant
# Optional window; actual operations still run in Windows PowerShell 5.1.
-pwsh -STA -File .\Invoke-PraRemoteMailbox.ps1 -Gui
+pwsh -STA -File .\package\Invoke-PraRemoteMailbox.ps1 -Gui
# Command line: run these commands in Windows PowerShell 5.1.
-.\Invoke-PraRemoteMailbox.ps1 -Action Convert -Identity jdupont@contoso.com # preview for one user: nothing is written
-.\Invoke-PraRemoteMailbox.ps1 -Action Convert # preview of the configured scope (e.g. an OU)
-.\Invoke-PraRemoteMailbox.ps1 -Action Convert -Phase AD -SearchBase "OU=Mailboxes,DC=contoso,DC=com"
-.\Invoke-PraRemoteMailbox.ps1 -Action Convert -Phase Both -CsvPath .\config\Targets.sample.csv
-.\Invoke-PraRemoteMailbox.ps1 -Action Convert -Mode Apply # backup, AD, sync, Exchange Online
-.\Invoke-PraRemoteMailbox.ps1 -Action Convert -Scope SharedOnly -Mode Apply # only the shared mailboxes of the scope
-.\Invoke-PraRemoteMailbox.ps1 -Action Recover -Batch 1d5ac20b # preview of the roll-back of that batch
-.\Invoke-PraRemoteMailbox.ps1 -Action Recover -Batch 1d5ac20b -Mode Apply # roll the batch back
-.\Invoke-PraRemoteMailbox.ps1 -Action Check -Identity jdupont@contoso.com # provisioned or not in Exchange Online
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Convert -Identity jdupont@contoso.com # preview for one user: nothing is written
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Convert # preview of the configured scope (e.g. an OU)
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Convert -Phase AD -SearchBase "OU=Mailboxes,DC=contoso,DC=com"
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Convert -Phase Both -CsvPath .\package\config\Targets.sample.csv
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Convert -Mode Apply # backup, AD, sync, Exchange Online
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Convert -Scope SharedOnly -Mode Apply # only the shared mailboxes of the scope
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Recover -Batch 1d5ac20b # preview of the roll-back of that batch
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Recover -Batch 1d5ac20b -Mode Apply # roll the batch back
+.\package\Invoke-PraRemoteMailbox.ps1 -Action Check -Identity jdupont@contoso.com # provisioned or not in Exchange Online
```
Keep the `Backups` folder on a durable, protected volume: the backups are the only way back. The environment values of the configuration are empty in this repository. The zip of each [release](https://github.com/Nico77600/PraRemoteMailbox/releases) contains only the files needed to run; `.\tools\New-PraPackage.ps1` builds the same package from the repository.
@@ -159,8 +160,8 @@ Keep the `Backups` folder on a durable, protected volume: the backups are the on
The **administrator guide** covers the disaster scenario, what changes in Active Directory, the shared mailbox permissions, installation, configuration, unattended execution with a certificate, ready-to-use recipes (one user, an OU, shared mailboxes only, a list, the roll-back, two servers), the reports, troubleshooting, the internals and the lab test campaign:
-- [docs/PraRemoteMailbox-Guide.md](docs/PraRemoteMailbox-Guide.md)
-- `docs/PraRemoteMailbox-Guide.html` — the same guide as a single HTML file, with a light and a dark theme (download it and open it locally)
+- [package/docs/PraRemoteMailbox-Guide.md](package/docs/PraRemoteMailbox-Guide.md)
+- `package/docs/PraRemoteMailbox-Guide.html` — the same guide as a single HTML file, with a light and a dark theme (download it and open it locally)
## Tests
@@ -173,7 +174,7 @@ The gate runs the real entry script against a synthetic Active Directory, Entra
no write before the complete backup, stop at the first error, no write in Preview, no Apply without
confirmation, memory bounds, proofs and Finalize packages. It also exercises WPF selection,
Preview/Apply guards and OU/CSV targeting. Earlier versions were validated on a real Active Directory,
-Entra Connect and Microsoft 365 tenant (guide, annex B); **2.1.0 has offline validation only, not a new
+Entra Connect and Microsoft 365 tenant (guide, annex B); **2.2.0 (unreleased source) has offline validation only, not a new
live-tenant campaign**. `tools\Build-Documentation.ps1`, which rebuilds the HTML guide on a workstation,
needs PowerShell 7.4+.
diff --git a/docs/images/report-warning.png b/docs/images/report-warning.png
deleted file mode 100644
index 916967925f8fc8b33f106bd95c08c546963b62a7..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 217660
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