MSIX.Accelerator.ps1
|
# ============================================================================= # MSIX Accelerator support # ----------------------------------------------------------------------------- # Implements a thin parser for the Accelerator YAML schema documented at # https://learn.microsoft.com/windows/msix/toolkit/accelerators. # # An accelerator captures the conversion recipe for a specific Win32 product: # its eligibility, the sequence of fixes, and (for FixType=PSF) a YAML-encoded # config.json. Sample accelerators: # https://github.com/microsoft/MSIX-Labs/tree/master/DeveloperLabs/SampleAccelerators # # We support PSF FixType natively. Other FixTypes (Capability, Dependency, # Services, etc.) are surfaced as findings for human review. # ============================================================================= function ConvertFrom-MsixYamlAccelerator { <# .SYNOPSIS Parses an accelerator YAML file using an intentionally-restricted scalar parser. .DESCRIPTION Reads an accelerator YAML file from -Path and returns a hashtable of the top-level keys. Only flat scalar (key: value) and inline list (key: [a, b, c]) forms are recognised. Quoting with single or double quotes around scalar values is honoured (stripped); everything else is treated as a literal string. Nested mappings, anchors/aliases, tags (e.g. !!python/object/apply, !!binary, !!set), multi-document streams, flow mappings, and any other YAML feature that could instantiate a .NET object are NOT supported. By design, hostile constructs degrade to literal text rather than causing object instantiation. .PARAMETER Path Path to the accelerator .yaml / .yml file. .OUTPUTS [hashtable] with one entry per recognised top-level key. Inline-list values are returned as string arrays; everything else is a string. .EXAMPLE $raw = ConvertFrom-MsixYamlAccelerator -Path .\line.yaml $raw.PackageName .NOTES SECURITY: Accelerator YAML is parsed by an intentionally-restricted scalar parser. Only flat key:value and key:[value1,value2] forms are supported. Tags, references, multi-document streams, and any YAML feature that could instantiate .NET objects are NOT supported -- by design. Do not switch to a full third-party YAML library for this input: accelerator files are user-supplied and a full YAML parser would be a code-execution vector on untrusted accelerator authors. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [string]$Path ) if (-not (Test-Path -LiteralPath $Path)) { throw "Accelerator not found: $Path" } # Thin file wrapper over the string parser (ConvertFrom-MsixAcceleratorYaml), # which now understands nested maps + block lists in addition to the original # flat scalars / inline lists (issue #18). The same safe, value-only parsing # guarantees apply: no type tags, anchors, or object instantiation. $text = Get-Content -LiteralPath $Path -Raw return ConvertFrom-MsixAcceleratorYaml -Yaml $text } function _MsixConvertAcceleratorScalar { <# Converts a single YAML scalar value (the right-hand side of "key: value", or a "- item" list element) into a [string] or [string[]]. Inline lists [a, b, c] become string arrays; quotes are stripped. NEVER resolves YAML tags, anchors, or aliases — hostile constructs (e.g. "!!python/object/...") are returned verbatim as inert strings. Value-only by design. #> param([string]$Value) $v = $Value # Strip a trailing comment that is clearly outside a quoted string. if ($v -notmatch '^["''].*["'']$' -and $v -match '^(.*?)\s+#') { $v = $matches[1] } $v = $v.Trim() if ($v -match '^\[(.*)\]\s*$') { # Inline list: [a, b, c] $items = [System.Collections.Generic.List[string]]::new() foreach ($item in ($matches[1] -split ',')) { $t = $item.Trim() if ($t -match '^"(.*)"$' -or $t -match "^'(.*)'$") { $t = $matches[1] } if ($t -ne '') { $items.Add($t) } } return [string[]]$items.ToArray() } if ($v -match '^"(.*)"$' -or $v -match "^'(.*)'$") { return [string]$matches[1] } # Everything else — including hostile YAML tag syntax — is kept literal. return [string]$v } function _MsixParseAcceleratorMap { <# Recursive-descent map parser. $Lines is an [object[]] of @($indent,$content) tuples (blank/comment/doc-marker lines already stripped). $Cursor is a @{ Index = <int> } hashtable threaded through the recursion (a plain int would not mutate across calls, and [ref] double-wraps awkwardly in PS 5.1). Consumes consecutive "key: value" lines at exactly $MapIndent and returns a [hashtable]. A key with an empty value followed by deeper-indented lines recurses into a nested map or block list. #> param( [object[]]$Lines, [hashtable]$Cursor, [int]$MapIndent ) $map = @{} while ($Cursor.Index -lt $Lines.Count) { $ln = $Lines[$Cursor.Index] if ($ln[0] -ne $MapIndent) { break } if ($ln[1].StartsWith('- ')) { break } $m = [regex]::Match($ln[1], '^([A-Za-z0-9_\-]+):\s*(.*)$') if (-not $m.Success) { break } $key = $m.Groups[1].Value $val = $m.Groups[2].Value $Cursor.Index++ if ($val -ne '') { $map[$key] = _MsixConvertAcceleratorScalar -Value $val } elseif ($Cursor.Index -lt $Lines.Count -and $Lines[$Cursor.Index][0] -gt $MapIndent) { $childIndent = $Lines[$Cursor.Index][0] if ($Lines[$Cursor.Index][1].StartsWith('- ')) { $map[$key] = _MsixParseAcceleratorList -Lines $Lines -Cursor $Cursor -ListIndent $childIndent } else { $map[$key] = _MsixParseAcceleratorMap -Lines $Lines -Cursor $Cursor -MapIndent $childIndent } } else { $map[$key] = '' } } return $map } function _MsixParseAcceleratorList { <# Recursive-descent block-list parser. Consumes consecutive "- ..." lines at exactly $ListIndent and returns an [object[]]. A "- key: value" element starts a nested map (the "- " is rewritten to spaces so the element's keys align at ListIndent+2); a plain "- scalar" element becomes a scalar value. #> param( [object[]]$Lines, [hashtable]$Cursor, [int]$ListIndent ) $items = [System.Collections.Generic.List[object]]::new() while ($Cursor.Index -lt $Lines.Count) { $ln = $Lines[$Cursor.Index] if ($ln[0] -ne $ListIndent) { break } if (-not $ln[1].StartsWith('- ')) { break } $after = $ln[1].Substring(2) if ([regex]::IsMatch($after, '^([A-Za-z0-9_\-]+):\s*(.*)$')) { # Map element: rewrite this line so its first key sits at ListIndent+2, # then let the map parser consume it plus its indented continuation. $itemIndent = $ListIndent + 2 $Lines[$Cursor.Index] = @($itemIndent, $after) $items.Add((_MsixParseAcceleratorMap -Lines $Lines -Cursor $Cursor -MapIndent $itemIndent)) } else { $Cursor.Index++ $items.Add((_MsixConvertAcceleratorScalar -Value $after)) } } return ,$items.ToArray() } function ConvertFrom-MsixAcceleratorYaml { <# .SYNOPSIS Parses accelerator YAML text into nested hashtables / arrays using a safe, value-only recursive-descent parser. .DESCRIPTION Supports the YAML subset accelerators actually use: scalars, inline lists ([a, b, c]), indentation-based nested maps, and block lists (including block-lists-of-maps for RemediationApproach trees). SECURITY: this is NOT a general YAML parser and never will be. Every leaf is a [string] or [string[]]; containers are [hashtable] / [object[]]. It NEVER instantiates .NET/CLR types, so YAML type tags (e.g. !!python/object/apply, !!net/object), anchors/aliases (&/*), and multi-document/directive markers (---, ..., %) are treated as inert literal text or skipped — closing the deserialisation code-execution vector that full YAML libraries expose on untrusted accelerator files. Tabs in indentation are rejected (YAML forbids them; mixing tabs/spaces silently corrupts structure). .PARAMETER Yaml The accelerator YAML document as a string. .OUTPUTS [hashtable] for a mapping document, or [object[]] for a top-level list. .EXAMPLE ConvertFrom-MsixAcceleratorYaml -Yaml (Get-Content acc.yaml -Raw) #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Yaml ) # Tokenize: drop blank/comment/doc-marker lines, capture (indent, content). $tokens = [System.Collections.Generic.List[object]]::new() foreach ($raw in ($Yaml -split "`r?`n")) { if ($raw -match '^[ ]*\t') { throw "Accelerator YAML uses a tab character in indentation, which is not allowed. Use spaces only." } $trimmedEnd = $raw -replace '\s+$', '' if ($trimmedEnd -eq '') { continue } $content = $trimmedEnd.TrimStart(' ') if ($content.StartsWith('#')) { continue } # Multi-document / directive markers: single-document parser ignores them. if ($content -eq '---' -or $content -eq '...' -or $content.StartsWith('%')) { continue } $indent = $trimmedEnd.Length - $content.Length # Store the [indent, content] pair as one element. List[object].Add does # not enumerate its argument, so add the 2-element array directly — a # leading unary comma would double-wrap it (@(@(indent,content))) and # make $ln[0] an array instead of the indent int. $tokens.Add([object[]]@($indent, $content)) } if ($tokens.Count -eq 0) { return @{} } $lines = [object[]]$tokens.ToArray() $cursor = @{ Index = 0 } if ($lines[0][1].StartsWith('- ')) { return _MsixParseAcceleratorList -Lines $lines -Cursor $cursor -ListIndent $lines[0][0] } return _MsixParseAcceleratorMap -Lines $lines -Cursor $cursor -MapIndent $lines[0][0] } function Import-MsixAccelerator { <# .SYNOPSIS Loads an accelerator YAML file and returns an object describing the recipe and any PSF fixups it contains. .DESCRIPTION Parses an Accelerator YAML file via ConvertFrom-MsixYamlAccelerator, then translates the recipe into the module's PSF builder shapes: FileRedirectionFixup, RegLegacyFixups, EnvVarFixup configs, plus any per-app arguments / working directory. The resulting object feeds Invoke-MsixAccelerator (or can be inspected and applied manually). Non-PSF fix types (Capability, Dependency, anything unrecognised) are surfaced under ManualNotes so the operator can action them. The underlying parser is intentionally value-only (see ConvertFrom-MsixYamlAccelerator .NOTES): it supports flat scalars, inline lists, nested maps, and block lists of maps such as structured RemediationApproach trees, while leaving YAML type tags, anchors, and aliases inert. .PARAMETER Path Path to the accelerator .yaml / .yml file. .OUTPUTS [pscustomobject] with Source, PackageName, PackageVersion, Publisher, Eligible, Status, Architecture, FixSteps[], SuggestedFixups[] (PSF hashtables ready for Add-MsixPsfV2), AppOptions[] (workingDirectory / arguments), Capabilities[], Dependencies[], ManualNotes[]. .EXAMPLE $accel = Import-MsixAccelerator -Path .\line.yaml $accel.SuggestedFixups #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path ) $raw = ConvertFrom-MsixYamlAccelerator -Path $Path $report = [pscustomobject]@{ Source = (Resolve-Path -LiteralPath $Path).Path PackageName = $raw.PackageName PackageVersion = $raw.PackageVersion Publisher = $raw.PublisherName Eligible = $raw.EligibleForConversion Status = $raw.ConversionStatus Architecture = $raw.Architecture FixSteps = @() SuggestedFixups = @() AppOptions = @() Capabilities = @() Dependencies = @() ManualNotes = @() } foreach ($step in @($raw.RemediationApproach)) { if (-not $step) { continue } $fix = $step.Fix if (-not $fix) { continue } $report.FixSteps += [pscustomobject]@{ Sequence = $step.SequenceNumber Issue = $step.Issue.Description FixType = $fix.FixType Reference = $fix.Reference } switch ($fix.FixType) { 'PSF' { $cfg = $fix.FixDetails.PSFConfig if ($cfg) { foreach ($app in @($cfg.applications)) { if ($app.workingDirectory -or $app.arguments) { $report.AppOptions += New-MsixPsfArgument ` -AppId $app.id ` -Arguments $app.arguments ` -WorkingDirectory $app.workingDirectory } } foreach ($proc in @($cfg.processes)) { foreach ($f in @($proc.fixups)) { $dll = ($f.dll -replace '\d+\.dll$', '.dll') if ($dll -eq 'FileRedirectionFixup.dll' -and $f.config.redirectedPaths.packageRelative) { foreach ($pr in @($f.config.redirectedPaths.packageRelative)) { $report.SuggestedFixups += New-MsixPsfFileRedirectionConfig ` -Base $pr.base -Patterns @($pr.patterns) } } elseif ($dll -eq 'RegLegacyFixups.dll' -and $f.config.remediation) { foreach ($rem in @($f.config.remediation)) { $report.SuggestedFixups += New-MsixPsfRegLegacyConfig ` -Hive $rem.hive -Access $rem.access -Patterns @($rem.patterns) } } elseif ($dll -eq 'EnvVarFixup.dll' -and $f.config.envVars) { $h = @{} foreach ($k in $f.config.envVars.Keys) { $h[$k] = $f.config.envVars[$k] } $report.SuggestedFixups += New-MsixPsfEnvVarConfig -Variables $h } } } } } 'Capability' { $report.Capabilities += @($fix.FixDetails.Capabilities) } 'Dependency' { $report.Dependencies += @($fix.FixDetails.Dependencies) } default { $report.ManualNotes += [pscustomobject]@{ FixType = $fix.FixType Issue = $step.Issue.Description Detail = ($fix.FixDetails | ConvertTo-Json -Depth 5 -Compress) } } } } return $report } function Invoke-MsixAccelerator { <# .SYNOPSIS Applies an accelerator recipe to an .msix file: runs Add-MsixPsfV2 with the synthesised fixups, AppOptions, and signs the result. .DESCRIPTION Non-PSF fix steps (Capability, Dependency, Services, EntryPoint, etc.) cannot be applied automatically and are returned in the output as ManualSteps for the operator to action. .PARAMETER PackagePath Existing .msix to which the accelerator's PSF block will be applied. .PARAMETER AcceleratorPath Path to the accelerator YAML. .PARAMETER Pfx Path to the signing PFX. Forwarded to Add-MsixPsfV2. .PARAMETER PfxPassword SecureString password for -Pfx. .OUTPUTS [pscustomobject] the same report shape produced by Import-MsixAccelerator. When the accelerator contained nothing applicable, the report is returned without modifying the package. .EXAMPLE Invoke-MsixAccelerator -PackagePath .\line.msix ` -AcceleratorPath .\line.yaml ` -Pfx .\cert.pfx -PfxPassword (Read-Host -AsSecureString) .EXAMPLE # Dry-run via -WhatIf to see what would be applied Invoke-MsixAccelerator -PackagePath .\line.msix ` -AcceleratorPath .\line.yaml -WhatIf #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$PackagePath, [Parameter(Mandatory)] [string]$AcceleratorPath, [string]$Pfx, [SecureString]$PfxPassword ) $accel = Import-MsixAccelerator -Path $AcceleratorPath if ($accel.Status -in 'Failed','Not Eligible') { Write-MsixLog -Level Warning -Message "Accelerator declares ConversionStatus '$($accel.Status)'. Proceeding anyway, but review FixSteps first." } if ($accel.SuggestedFixups.Count -eq 0 -and $accel.AppOptions.Count -eq 0) { Write-MsixLog -Level Warning -Message 'Accelerator contains no PSF fixups; nothing to inject. Returning report only.' return $accel } if ($PSCmdlet.ShouldProcess($PackagePath, "Apply accelerator $($accel.Source)")) { Add-MsixPsfV2 -PackagePath $PackagePath ` -Fixups ([hashtable[]]$accel.SuggestedFixups) ` -AppOptions ([hashtable[]]$accel.AppOptions) ` -Pfx $Pfx ` -PfxPassword $PfxPassword } return $accel } |