Add Capture-MclCache: tree and control files of an MCL cache, SAS signatures blanked

This commit is contained in:
cproudlock
2026-10-18 09:14:51 -04:00
parent 3a8b7cabc7
commit 3b3041173b
2 changed files with 144 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
@echo off
rem Double-click after a Media Creator Lite run finishes.
rem Asks pro or ltsc, then runs Capture-MclCache.ps1 from this folder.
setlocal
set "LABEL=%~1"
if "%LABEL%"=="" set /p "LABEL=Which MCL run just finished? Type pro or ltsc and press Enter: "
if "%LABEL%"=="" set "LABEL=run"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Capture-MclCache.ps1" -Label "%LABEL%"
echo.
pause

View File

@@ -0,0 +1,134 @@
<#
.SYNOPSIS
Captures the layout of a Media Creator Lite cache, plus its small
control files, so the PXE import can be checked against what the
current MCL actually writes.
.DESCRIPTION
Run on the PC where Media Creator Lite just FINISHED a run. Run it once
after the Windows 11 Pro run and once after the Windows 11 LTSC run,
before starting the next MCL run (MCL deletes the cache when it starts).
.\Capture-MclCache.ps1 -Label pro
.\Capture-MclCache.ps1 -Label ltsc
It writes, under the Desktop:
mcl-capture-<label>-<timestamp>\
tree.csv every file: path, size, last write time (UTC)
summary.txt tag files and their contents, file counts, totals
files\ copies of the small text files (*.json, *.xml,
*.tag, *.txt, *.ini, *.log, *.cmd, *.ps1, *.bat)
under 5 MB, same relative paths
mcl-capture-<label>-<timestamp>.zip
Nothing large is copied: no WIM, driver, MSU or installer. Text copies
have any Azure SAS signature (sig=...) blanked, so the zip carries no
storage credential. Read-only on the cache; needs no admin rights.
.PARAMETER Label
Short name for this run, for example pro or ltsc.
.PARAMETER Root
The Media Creator folder. Default C:\ProgramData\GEAerospace\MediaCreator.
.PARAMETER OutDir
Where to write the capture. Default: the current user's Desktop, or
this script's folder when there is no Desktop.
#>
param(
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_-]{1,20}$')][string]$Label,
[string]$Root = "C:\ProgramData\GEAerospace\MediaCreator",
[string]$OutDir = [Environment]::GetFolderPath("Desktop")
)
$ErrorActionPreference = "Stop"
if (-not $OutDir) { $OutDir = $PSScriptRoot } # no Desktop (e.g. run as SYSTEM)
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
Write-Host "Media Creator folder not found: $Root" -ForegroundColor Red
exit 1
}
$Root = (Resolve-Path -LiteralPath $Root).ProviderPath.TrimEnd('\')
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$dest = Join-Path $OutDir "mcl-capture-$Label-$stamp"
$filesDir = Join-Path $dest "files"
New-Item -ItemType Directory -Path $filesDir -Force | Out-Null
$textExt = @(".json", ".xml", ".tag", ".txt", ".ini", ".log", ".cmd", ".ps1", ".bat")
$maxCopy = 5MB
# Azure SAS signature in a URL or connection string.
$sasPattern = '(?i)(sig=)[^&"''\s<]+'
Write-Host "Reading $Root ..."
$all = @(Get-ChildItem -LiteralPath $Root -Recurse -Force -File -ErrorAction SilentlyContinue)
$rows = foreach ($f in $all) {
[pscustomobject]@{
Path = $f.FullName.Substring($Root.Length + 1)
Length = $f.Length
LastWriteUtc = $f.LastWriteTimeUtc.ToString("yyyy-MM-ddTHH:mm:ssZ")
}
}
$rows | Sort-Object Path | Export-Csv -LiteralPath (Join-Path $dest "tree.csv") -NoTypeInformation -Encoding UTF8
$copied = 0
$redacted = 0
$skippedBig = New-Object System.Collections.Generic.List[string]
foreach ($f in $all) {
if ($textExt -notcontains $f.Extension.ToLower()) { continue }
$rel = $f.FullName.Substring($Root.Length + 1)
if ($f.Length -gt $maxCopy) { $skippedBig.Add("$rel ($($f.Length) bytes)"); continue }
$target = Join-Path $filesDir $rel
New-Item -ItemType Directory -Path (Split-Path $target -Parent) -Force | Out-Null
$bytes = [System.IO.File]::ReadAllBytes($f.FullName)
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text -match $sasPattern) {
$text = [regex]::Replace($text, $sasPattern, '${1}REDACTED')
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
$redacted++
}
[System.IO.File]::WriteAllBytes($target, $bytes)
$copied++
}
$tags = @($all | Where-Object { $_.Extension -eq ".tag" })
$sum = New-Object System.Collections.Generic.List[string]
$sum.Add("Media Creator Lite cache capture")
$sum.Add("Label: $Label")
$sum.Add("Captured: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')")
$sum.Add("Computer: $env:COMPUTERNAME")
$sum.Add("Root: $Root")
$sum.Add("PowerShell $($PSVersionTable.PSVersion)")
$sum.Add("")
$sum.Add("Files: $($all.Count), total $([math]::Round((($all | Measure-Object Length -Sum).Sum) / 1GB, 2)) GB")
$sum.Add("Text files copied: $copied (SAS signatures blanked in $redacted)")
$sum.Add("")
$sum.Add("Tag files ($($tags.Count)):")
foreach ($t in $tags) {
$content = (Get-Content -LiteralPath $t.FullName -Raw -ErrorAction SilentlyContinue)
if ($content) { $content = $content.Trim() }
$sum.Add(" $($t.FullName.Substring($Root.Length + 1)) [$content] written $($t.LastWriteTime)")
}
if ($tags.Count -eq 0) { $sum.Add(" NONE - the MCL run did not finish") }
$sum.Add("")
$sum.Add("Top-level folders:")
foreach ($g in ($rows | Group-Object { ($_.Path -split '\\')[0..1] -join '\' } | Sort-Object Name)) {
$gb = [math]::Round((($g.Group | Measure-Object Length -Sum).Sum) / 1GB, 2)
$sum.Add(" $($g.Name): $($g.Count) files, $gb GB")
}
if ($skippedBig.Count) {
$sum.Add("")
$sum.Add("Text files over 5 MB, not copied:")
foreach ($s in $skippedBig) { $sum.Add(" $s") }
}
$sum | Set-Content -LiteralPath (Join-Path $dest "summary.txt") -Encoding UTF8
$zip = "$dest.zip"
Compress-Archive -Path (Join-Path $dest "*") -DestinationPath $zip -Force
Write-Host ""
Write-Host "Captured $($all.Count) files ($copied text files copied)." -ForegroundColor Green
if ($tags.Count -eq 0) {
Write-Host "WARNING: no .tag file. MCL did not finish this run." -ForegroundColor Yellow
}
Write-Host "Send back: $zip"