# Backup-NtlarsSettings.ps1 # # Captures this PC's LIVE NTLARS/DNC registry settings and posts them to ShopDB, # which files them against the MACHINE (not this PC) and keeps a revision # history. A tech can then re-download the .reg from the machine's page instead # of hunting for a per-machine file on the share. # # Runs from the SFLD share every GE-Enforce cycle as a Type=PS1 manifest entry # with DetectionMethod=Always. Updating this file on the share changes fleet # behaviour on the next cycle - there is no local copy to heal. # # WHY WOW6432Node IS EXPLICIT: # NTLARS is a 32-bit app, so its settings physically live under # HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC. GE-Enforce runs this # script in 64-bit PowerShell, where HKLM:\SOFTWARE\GE Aircraft Engines\DNC # is a DIFFERENT (usually absent) key. Reading the unredirected path would # find nothing and back up an empty config - silently, every cycle. The path # below is therefore spelled out and never abbreviated. # # Note the asymmetry with what NTLARS itself writes: its Save... button # exports WITHOUT the WOW6432Node segment. ShopDB accepts either dialect and # stores a dialect-neutral projection, so this script does not need to care. # # THROTTLE, LOGGING, CREDENTIALS: # All handled by common\lib\ShopdbBackupClient.psm1, which every backup kind # shares. GE-Enforce fires this every cycle, so the module holds the marker # file, exits early until backups_intervalhours has elapsed, and logs a # no-change outcome ONCE rather than every five minutes. The interval comes # from ShopDB, so cadence is changed centrally rather than by editing this # file on the share. # # Always exits 0 so the GE-Enforce "last run result" stays clean. Failures are # logged, never thrown. param( [string]$BaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb', [int]$TimeoutSec = 30, # Force a post regardless of the throttle. For a tech capturing a # known-good config on demand. [switch]$Force ) $ErrorActionPreference = 'Continue' # The only path this script still owns. Log, marker, state, collector key, # enrollment and base URL all moved to ShopdbBackupClient, which names its files # per KIND so two backup kinds on one bay cannot collide. $DNCKEY = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC' function Convert-RegTypeName { param($Kind) switch ("$Kind") { 'String' { 'REG_SZ' } 'ExpandString' { 'REG_EXPAND_SZ' } 'DWord' { 'REG_DWORD' } 'QWord' { 'REG_QWORD' } 'MultiString' { 'REG_MULTI_SZ' } 'Binary' { 'REG_BINARY' } default { 'REG_SZ' } } } function Get-DncKeys { <# The DNC key and every subkey, root first. Split out from Export-DncToReg so the formatting logic can be exercised against mock keys on a machine with no registry (see Test-RegExport.ps1). #> $keys = @(Get-Item -Path $DNCKEY -EA Stop) $keys += @(Get-ChildItem -Path $DNCKEY -Recurse -EA SilentlyContinue) return $keys } function Export-DncToReg { <# Emits .reg text in the WOW6432Node dialect for the supplied keys. Built by hand rather than shelling out to `reg export` because reg.exe writes UTF-16 to a temp file we would then have to read back, and because this keeps the value types explicit instead of reparsing them. Takes the key list as a parameter so it can be tested with mocks; the escaping and dword formatting here are the part that would corrupt a backup silently and only surface at restore time. #> param([Parameter(Mandatory)]$Keys) $lines = @('Windows Registry Editor Version 5.00', '') $lines += "; NTLARS DNC Registry Backup" $lines += "; Computer: $env:COMPUTERNAME" $lines += "; Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" $lines += '' foreach ($key in $Keys) { # PSPath -> the literal HKEY_LOCAL_MACHINE\... form a .reg file needs. $path = $key.Name -replace '^HKEY_LOCAL_MACHINE', 'HKEY_LOCAL_MACHINE' $lines += "[$path]" foreach ($name in $key.GetValueNames()) { $kind = Convert-RegTypeName $key.GetValueKind($name) $data = $key.GetValue($name) $lhs = if ($name -eq '') { '@' } else { # .NET replacement strings do NOT process backslash escapes, so # the replacement is the literal output: '\\' emits two # backslashes, which is what .reg escaping wants. Writing # '\\\\' here emits FOUR and silently corrupts every path-valued # setting - verified on Windows before this was fixed. '"{0}"' -f ($name -replace '\\', '\\' -replace '"', '\"') } switch ($kind) { 'REG_DWORD' { $lines += ('{0}=dword:{1:x8}' -f $lhs, [uint32]$data) } 'REG_BINARY' { $hex = ($data | ForEach-Object { '{0:x2}' -f $_ }) -join ',' $lines += ('{0}=hex:{1}' -f $lhs, $hex) } 'REG_QWORD' { $bytes = [BitConverter]::GetBytes([uint64]$data) $hex = ($bytes | ForEach-Object { '{0:x2}' -f $_ }) -join ',' $lines += ('{0}=hex(b):{1}' -f $lhs, $hex) } 'REG_MULTI_SZ' { $joined = (($data -join "`0") + "`0`0") $bytes = [Text.Encoding]::Unicode.GetBytes($joined) $hex = ($bytes | ForEach-Object { '{0:x2}' -f $_ }) -join ',' $lines += ('{0}=hex(7):{1}' -f $lhs, $hex) } 'REG_EXPAND_SZ' { $bytes = [Text.Encoding]::Unicode.GetBytes(("$data" + "`0")) $hex = ($bytes | ForEach-Object { '{0:x2}' -f $_ }) -join ',' $lines += ('{0}=hex(2):{1}' -f $lhs, $hex) } default { $escaped = ("$data" -replace '\\', '\\' -replace '"', '\"') $lines += ('{0}="{1}"' -f $lhs, $escaped) } } } $lines += '' } return ($lines -join "`r`n") + "`r`n" } # ============================================================================= # Main # # Guarded so the file can be DOT-SOURCED to get the functions without running a # backup. Test-RegExport.ps1 relies on this to exercise Export-DncToReg against # mock keys on a machine with no registry. # ============================================================================= if ($MyInvocation.InvocationName -eq '.') { return } # The plumbing - key lookup, base URL, interval, throttle, logging, the POST and # its response handling - lives in ShopdbBackupClient. It used to live here, and # being the only implementation meant every defect in it was invisible: the # interval read fell back to a hardcoded 24h for months because nobody had a # second copy to compare against, and the log wrote three lines every five # minutes whatever happened. Seven other Backup-*.ps1 scripts on this share # capture device configs and post none of them; they can now do so without # inheriting a copy of all that. # # What stays here is the part that is actually about NTLARS: where its registry # lives, and how to turn it into .reg text. Import-Module (Join-Path $PSScriptRoot '..\lib\ShopdbBackupClient.psm1') -Force if (-not (Test-Path $DNCKEY)) { # Not an error: plenty of PC types have no NTLARS at all. On those PCs this # is the permanent state, so it is said once rather than 288 times a day. Write-ShopdbQuietState -Kind 'ntlars' -State 'no-dnc-key' -Message ( "No DNC key at $DNCKEY - NTLARS is not installed on this PC. Nothing to do.") exit 0 } # NTLARS keeps its own MachineNo, used only when pc-config.txt has none. ShopDB # compares the two and warns on a mismatch rather than silently trusting one. $ctx = Initialize-ShopdbBackup -Kind 'ntlars' -Force:$Force ` -BaseUrl $(if ($PSBoundParameters.ContainsKey('BaseUrl')) { $BaseUrl } else { '' }) ` -MachineNumberFallback { try { $general = Get-ItemProperty -Path (Join-Path $DNCKEY 'General') -EA Stop if ($general.MachineNo) { return ([string]$general.MachineNo).Trim() } } catch { } return '' } if (-not $ctx.Proceed) { exit 0 } try { $regText = Export-DncToReg -Keys (Get-DncKeys) } catch { Write-ShopdbBackupLog -Kind 'ntlars' -Message "Failed to read the DNC key: $_" exit 0 } # UTF-16LE + BOM, matching what regedit and NTLARS emit. ShopDB sniffs the BOM, # so this is belt-and-braces rather than strictly required. $bytes = [byte[]](0xFF, 0xFE) + [Text.Encoding]::Unicode.GetBytes($regText) [void](Send-ShopdbBackup -Context $ctx -Bytes $bytes ` -SourceFileName ("{0}.reg" -f $ctx.MachineNumber) -TimeoutSec $TimeoutSec) Write-ShopdbBackupLog -Kind 'ntlars' -Message '=== Backup-NtlarsSettings end ===' exit 0