compat.ps1
|
#Requires -Version 5.0 # Compatibility policy belongs to this PowerShell module, not the reusable service manager. $script:ProtectedServiceNames = @('RemoteAccess', 'RemoteRegistry') function Install-UPFAppxPackage { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)][ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })][string]$Path, [string[]]$DependencyPath, [string]$LicensePath, [switch]$Provisioned, [switch]$SkipLicense, [switch]$ForceUpdateFromAnyVersion, [switch]$DryRun ) $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) $action = if ($Provisioned) { 'Provision' } else { 'Install' } if ($DryRun -or -not $PSCmdlet.ShouldProcess($resolved, "$action UPF AppX/MSIX package")) { return New-PackageLifecycleResult -Target $resolved -Source 'UPFAppxPackage' -Action $action -Status 'Skipped' -SkippedReason 'WhatIf' } $dependencies = @($DependencyPath | ForEach-Object { $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($_) }) $license = if ($LicensePath) { $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LicensePath) } else { $null } [AdNoctem.Substrate.PowerShell.Packages.AppxCompatibility]::Install($resolved, $dependencies, $license, $Provisioned, $SkipLicense, $ForceUpdateFromAnyVersion) } function Update-UPFAppxPackage { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)][ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })][string]$Path, [string[]]$DependencyPath, [switch]$DryRun ) Install-UPFAppxPackage -Path $Path -DependencyPath $DependencyPath -ForceUpdateFromAnyVersion -DryRun:$DryRun -WhatIf:$WhatIfPreference } function Install-UPFAppxPackageSet { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)][string[]]$Path, [string[]]$DependencyPath, [switch]$Provisioned, [switch]$SkipLicense, [switch]$ForceUpdateFromAnyVersion, [switch]$PassThru, [switch]$DryRun ) $results = foreach ($item in $Path) { Install-UPFAppxPackage -Path $item -DependencyPath $DependencyPath -Provisioned:$Provisioned -SkipLicense:$SkipLicense -ForceUpdateFromAnyVersion:$ForceUpdateFromAnyVersion -DryRun:$DryRun -WhatIf:$WhatIfPreference } if ($PassThru) { $results } } function Uninstall-UPFAppxPackageSet { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Delegates confirmation and WhatIf to Uninstall-UPFAppxPackage for each resolved package; verified by the wrapper delegation test.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)][string[]]$Pattern, [switch]$Installed, [switch]$Provisioned, [switch]$AllUsers, [switch]$IncludeProtected, [switch]$Force, [switch]$PassThru, [switch]$DryRun ) $includeInstalled = $Installed -or -not $PSBoundParameters.ContainsKey('Installed') $results = @(foreach ($match in Find-UPFAppxPackage -Pattern $Pattern -Installed:$includeInstalled -Provisioned:$Provisioned -AllUsers:$AllUsers -IncludeProtected:$IncludeProtected) { if (-not $match.Matched) { New-PackageLifecycleResult -Target $match.Pattern -Source 'UPFAppxPackage' -Action 'Resolve' -Status 'Skipped' -SkippedReason 'NoMatch' } else { Uninstall-UPFAppxPackage -InputObject $match.Package -AllUsers:$AllUsers -IncludeProtected:$IncludeProtected -Force:$Force -DryRun:$DryRun -WhatIf:$WhatIfPreference } }) if ($PassThru) { return $results } $removed = @($results | Where-Object Status -EQ 'Removed').Count $skipped = @($results | Where-Object Status -EQ 'Skipped').Count $failed = @($results | Where-Object Status -EQ 'Failed').Count Write-Log -Message "UPF AppX/MSIX package removal complete. Removed: $removed | Skipped: $skipped | Failed: $failed" -Color $(if ($failed -gt 0) { 'Yellow' } else { 'Green' }) } function Merge-ObjectArrays { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Preserves the public v1 name.')] [OutputType([void])] param ([Parameter(Mandatory = $true)][array]$Base, [Parameter(Mandatory = $true)][array]$Overrides) [AdNoctem.Substrate.PowerShell.Core.DataCompatibility]::Merge($Base, $Overrides) } function Get-DefaultApp { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [OutputType([string])] param ([Parameter(Mandatory = $true)][string]$FileExtension) $association = [AdNoctem.Substrate.Windows.ApplicationAssociationManager]::new().Get($FileExtension) if ($null -eq $association) { Write-Error "Could not retrieve default application for '$FileExtension'."; return } $association.Command } function Request-AdministratorPrivilege { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] [OutputType([void])] param ( [string]$ScriptPath = $MyInvocation.PSCommandPath, [System.Collections.IDictionary]$BoundParameters, [object[]]$ArgumentList, [switch]$IsElevatedRelaunch ) if (Test-Elevation) { return } if ($IsElevatedRelaunch) { Write-Error -ErrorAction Continue 'Elevation was attempted but the process is still not elevated. Aborting to avoid a re-launch loop.' exit 1 } if ([string]::IsNullOrWhiteSpace($ScriptPath)) { throw 'Could not determine the script path to re-launch. Pass -ScriptPath explicitly.' } $scriptFile = (Resolve-Path -LiteralPath $ScriptPath -ErrorAction Stop).ProviderPath $hostExe = (Get-Process -Id $PID).Path $workingDirectory = (Get-Location -PSProvider FileSystem).ProviderPath try { $exitCode = [AdNoctem.Substrate.PowerShell.Security.ElevationCompatibility]::Run($hostExe, $scriptFile, $workingDirectory, $BoundParameters, $ArgumentList) exit $exitCode } catch { $native = $_.Exception.GetBaseException() if ($native -is [System.ComponentModel.Win32Exception] -and $native.NativeErrorCode -eq 1223) { Write-Error -ErrorAction Continue 'Elevation was cancelled by the user. Administrator privileges are required to continue.' exit 1223 } throw } } function New-EncryptedCredentialFile { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$KeyPath, [Parameter(Mandatory = $false)][PSCredential]$Credential, [Parameter(Mandatory = $false)][string]$UserName ) if ($null -eq $Credential -and -not $UserName) { throw 'Supply -Credential (programmatic) or -UserName (interactive password prompt).' } $ownedPassword = $null try { if ($null -eq $Credential) { $ownedPassword = Read-Host -Prompt "Password for '$UserName'" -AsSecureString $Credential = [PSCredential]::new($UserName, $ownedPassword) } if (-not $PSCmdlet.ShouldProcess("$Path / $KeyPath", 'Write encrypted credential files')) { if ($WhatIfPreference) { New-OperationResult -Target $Path -Source 'CredentialFile' -Action 'Write' -Status 'DryRun' -Detail 'No files written.' } return } $destination = [AdNoctem.Substrate.IO.FileSystemPath]::Parse($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)) $key = [AdNoctem.Substrate.IO.FileSystemPath]::Parse($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($KeyPath)) [AdNoctem.Substrate.Security.CredentialFileManager]::new().Write($destination, $key, $Credential.UserName, $Credential.Password, $true) New-OperationResult -Target $Path -Source 'CredentialFile' -Action 'Write' -Status 'Completed' -Detail 'Credential stored with a random AES key; key file written separately.' } finally { if ($null -ne $ownedPassword) { $ownedPassword.Dispose() } } } function Get-OfficeDeploymentRecovery { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)][ValidatePattern('^[a-fA-F0-9]{32}$')][string]$RunId, [string]$LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office') ) $outcome = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::ReadRecovery($RunId, $LogRoot) if ($outcome.Failure) { Stop-SubstrateOfficeNativeFailure $outcome.Failure } [PSCustomObject]@{ RunId = $RunId; LogRoot = $LogRoot; Path = Join-Path $LogRoot ($RunId + '.json'); Record = ConvertFrom-Json -InputObject $outcome.Json } } function Get-TransportMessageId { [CmdletBinding()] [OutputType([string])] param ([Parameter(Mandatory = $true)][AllowEmptyString()][string]$HeaderText) [AdNoctem.Substrate.Interop.MailHeaderParser]::GetTransportMessageId($HeaderText) } function Write-SubstrateOfficeJson { [CmdletBinding()] param ([string]$Path, [object]$Value) $json = ConvertTo-Json -InputObject $Value -Depth 30 $failure = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::WriteJournal($Path, $json) if ($failure) { Stop-SubstrateOfficeNativeFailure $failure } } function Test-SubstrateOfficeHost { [CmdletBinding()] [OutputType([bool])] param () [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::SupportedHost() } function Assert-SubstrateOfficeHost { [CmdletBinding()] param () $failure = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::CheckHost() if ($failure) { Stop-SubstrateOfficeOperation $failure.ReasonCode $failure.Detail $failure.Diagnostic } } function Enter-SubstrateOfficeLock { [CmdletBinding()] param () try { [AdNoctem.Substrate.Office.OfficeDeploymentLock]::Acquire() } catch { $errorValue = $_.Exception while ($errorValue.InnerException -and -not $errorValue.Data.Contains('OfficeReason')) { $errorValue = $errorValue.InnerException } if ($errorValue.Data.Contains('OfficeReason')) { Stop-SubstrateOfficeOperation $errorValue.Data['OfficeReason'] $errorValue.Message } throw } } function Invoke-SubstrateOfficeConfiguration { [CmdletBinding()] param ([string]$OdtPath, [xml]$Document, [string]$Directory, [ValidateSet('/download', '/configure', '/customize')][string]$Mode = '/configure', [Security.SecureString]$ProductKey) $outcome = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::InvokeConfiguration($OdtPath, $Document, $Directory, $Mode, $ProductKey) if ($outcome.Failure) { Stop-SubstrateOfficeNativeFailure $outcome.Failure } $outcome.Result } function Stop-SubstrateOfficeNativeFailure { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Raises an operation failure without changing machine state.')] [CmdletBinding()] param ([object]$Failure) try { Stop-SubstrateOfficeOperation $Failure.ReasonCode $Failure.Detail $Failure.Diagnostic } catch { if ($Failure.CleanupError) { $_.Exception.Data['OfficeCleanupError'] = $Failure.CleanupError } if ($null -ne $Failure.ExitCode) { $_.Exception.Data['OfficeExitCode'] = $Failure.ExitCode } throw } } function ConvertFrom-SubstrateOfficeMediaAssessment { [CmdletBinding()] param ([object]$Assessment) $manifest = $null $fingerprint = $null if ($Assessment.Valid) { $manifest = ConvertFrom-Json -InputObject $Assessment.ManifestJson -ErrorAction Stop $fingerprint = Get-SubstrateOfficeFingerprint $manifest } [PSCustomObject]@{ Valid = $Assessment.Valid; Path = $Assessment.Path; Manifest = $manifest; Fingerprint = $fingerprint ReasonCode = $Assessment.ReasonCode; Error = $Assessment.Error; Diagnostic = $Assessment.Diagnostic } } function Save-OfficeDeploymentMedia { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Media names the deployment payload as a collective noun.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ([Parameter(Mandatory = $true)][object]$Configuration, [Parameter(Mandatory = $true)][string]$SourcePath, [Parameter(Mandatory = $true)][string]$OdtPath, [switch]$DryRun) $target = ConvertTo-SubstrateOfficeConfiguration $Configuration Assert-SubstrateOfficePath $SourcePath if (Test-Path -LiteralPath $SourcePath) { $existing = Test-OfficeDeploymentMedia -SourcePath $SourcePath -Configuration $target if (-not $existing.Valid) { Stop-SubstrateOfficeOperation InvalidMedia 'Existing package is incomplete or incompatible; use a new destination.' (Get-SubstrateOfficeMediaDiagnostic $existing) } return $existing } if (-not (Test-OfficeDeploymentTool $OdtPath).Valid) { Stop-SubstrateOfficeOperation UntrustedTool 'ODT verification failed.' } if ($DryRun -or -not $PSCmdlet.ShouldProcess($SourcePath, 'Download pinned Office media and publish verified package')) { return [PSCustomObject]@{ Status = 'Preview'; Path = $SourcePath; Configuration = $target } } $outcome = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::PrepareMedia($target, $SourcePath, $OdtPath) if ($outcome.Failure) { Stop-SubstrateOfficeNativeFailure $outcome.Failure } ConvertFrom-SubstrateOfficeMediaAssessment $outcome.Result } function Test-OfficeDeploymentMedia { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Media names the deployment payload as a collective noun.')] [CmdletBinding()] [OutputType([PSCustomObject])] param ([Parameter(Mandatory = $true)][string]$SourcePath, [object]$Configuration) $stage = 'ProtectedPath' try { Assert-SubstrateOfficeProtectedPath $SourcePath -ObjectKind MediaDirectory $stage = 'Validation' $target = $null if ($Configuration) { $target = ConvertTo-SubstrateOfficeConfiguration $Configuration } $assessment = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::InspectMedia($SourcePath, $target) ConvertFrom-SubstrateOfficeMediaAssessment $assessment } catch { $errorValue = $_.Exception while ($errorValue.InnerException -and -not $errorValue.Data.Contains('OfficeReason')) { $errorValue = $errorValue.InnerException } $reason = $errorValue.Data['OfficeReason'] $message = $errorValue.Message $diagnostic = $errorValue.Data['OfficeDiagnostic'] if (-not $reason) { $reason = 'InvalidMedia'; $message = "Office media validation failed at $stage for '$SourcePath'." } if ($reason -eq 'InvalidContract') { $message = "Office media contract is invalid at '$SourcePath'." } if (-not $diagnostic) { $diagnostic = [PSCustomObject]@{ Stage = $stage; ObjectKind = 'MediaPackage'; Path = $SourcePath; ExceptionType = $errorValue.GetType().FullName Function = 'Test-OfficeDeploymentMedia'; ScriptPath = $PSCommandPath; Line = $_.InvocationInfo.ScriptLineNumber } } [PSCustomObject]@{ Valid = $false; Path = $SourcePath; Manifest = $null; Fingerprint = $null; ReasonCode = $reason; Error = $message; Diagnostic = $diagnostic } } } function Get-SubstrateOfficeMediaFile { [CmdletBinding()] param ([string]$Root) $outcome = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::MediaFiles($Root) if ($outcome.Failure) { Stop-SubstrateOfficeOperation $outcome.Failure.ReasonCode $outcome.Failure.Detail $outcome.Failure.Diagnostic } $outcome.Files } function Invoke-SubstrateOfficeTool { [CmdletBinding()] param ([string]$OdtPath, [ValidateSet('/download', '/configure', '/customize', '/help')][string]$Mode, [string]$ConfigurationPath) $outcome = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::InvokeTool($OdtPath, $Mode, $ConfigurationPath) if ($outcome.Failure) { Stop-SubstrateOfficeOperation $outcome.Failure.ReasonCode $outcome.Failure.Detail $outcome.Failure.Diagnostic } $outcome.Result } function Remove-SubstrateOfficeWorkDirectory { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private cleanup of the operation-owned directory after approval.')] [CmdletBinding()] param ([string]$Path, [string]$Parent) if (-not $Path) { return } $failure = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::RemoveDeploymentDirectory($Path, $Parent) if ($failure) { Stop-SubstrateOfficeOperation $failure.ReasonCode $failure.Detail $failure.Diagnostic } } function Assert-SubstrateOfficePath { [CmdletBinding()] param ([string]$Path) $failure = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::CheckDeploymentPath($Path, $false, 'DeploymentFile') if ($failure) { Stop-SubstrateOfficeOperation $failure.ReasonCode $failure.Detail $failure.Diagnostic } } function Assert-SubstrateOfficeProtectedPath { [CmdletBinding()] param ([string]$Path, [string]$ObjectKind = 'DeploymentFile') $failure = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::CheckDeploymentPath($Path, $true, $ObjectKind) if ($failure) { Stop-SubstrateOfficeOperation $failure.ReasonCode $failure.Detail $failure.Diagnostic } } function New-SubstrateOfficeProtectedDirectory { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private primitive called only after public ShouldProcess approval.')] [CmdletBinding()] param ([string]$Path) $failure = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::CreateDeploymentDirectory($Path) if ($failure) { Stop-SubstrateOfficeOperation $failure.ReasonCode $failure.Detail $failure.Diagnostic } } function Get-OfficeDeploymentPlan { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [ValidateSet('Install', 'Remove', 'Migrate', 'Update', 'SetUpdateConfiguration', 'AddLanguage', 'RemoveLanguage', 'SetApplicationSelection', 'SetApplicationPreference')] [string]$Action, [object]$Configuration, [string]$SourcePath, [string[]]$RemoveProductId = @(), [switch]$RemoveMsi, [string[]]$Language = @(), [object]$Settings = @{}, [object]$Inventory ) if ($null -eq $RemoveProductId) { $RemoveProductId = @() } if ($null -eq $Language) { $Language = @() } if ($Action -notin @('Remove', 'Migrate') -and ($RemoveProductId.Count -or $RemoveMsi)) { Stop-SubstrateOfficeOperation InvalidAuthority 'This action cannot remove products.' } if ($Action -notin @('AddLanguage', 'RemoveLanguage') -and $Language.Count) { Stop-SubstrateOfficeOperation InvalidAuthority 'Language selection belongs only to language operations.' } Assert-SubstrateOfficeSetting $Action $Settings $selection = @(ConvertTo-SubstrateOfficeList $RemoveProductId) $languages = @(ConvertTo-SubstrateOfficeList $Language -Language) $target = $null if ($Configuration) { $target = ConvertTo-SubstrateOfficeConfiguration $Configuration } [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::ValidateRequest($Action, $target, $SourcePath, [string[]]$selection, [bool]$RemoveMsi, [string[]]$languages, $Settings) if (-not $Inventory) { $Inventory = Get-OfficeInventory } $media = $null if ($target -and $SourcePath) { $media = Test-OfficeDeploymentMedia -SourcePath $SourcePath -Configuration $target if ($media.Valid -and -not $target.Version) { $target.Version = $media.Manifest.Version } } [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::CreatePlan($Action, $target, $SourcePath, [string[]]$selection, [bool]$RemoveMsi, [string[]]$languages, $Settings, $Inventory, $media, (Get-SubstrateOfficeFingerprint $Inventory)) } function New-SubstrateOfficeXml { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Creates an in-memory XML document.')] [CmdletBinding()] param ( [ValidateSet('Download', 'Install', 'Remove', 'Migrate', 'Update', 'AddLanguage', 'RemoveLanguage', 'SetApplicationSelection', 'SetUpdateConfiguration', 'SetApplicationPreference')] [string]$Action, [object]$Configuration, [string]$MediaPath, [string[]]$RemoveProductId = @(), [bool]$RemoveMsi = $false, [string[]]$Language = @(), [object]$Settings = @{} ) if ($null -eq $RemoveProductId) { $RemoveProductId = @() } if ($null -eq $Language) { $Language = @() } if (($Action -notin @('Remove', 'Migrate') -and ($RemoveProductId.Count -or $RemoveMsi)) -or ($Action -eq 'Remove' -and $RemoveMsi)) { Stop-SubstrateOfficeOperation InvalidAuthority 'XML operation cannot contain the requested removal.' } Assert-SubstrateOfficeSetting $Action $Settings $target = $null if ($Configuration) { $target = ConvertTo-SubstrateOfficeConfiguration $Configuration } return , [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::CreateXml($Action, $target, $MediaPath, $RemoveProductId, $RemoveMsi, $Language, $Settings) } function Test-OfficeDeployment { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)][object]$Configuration, [object]$Inventory ) $target = ConvertTo-SubstrateOfficeConfiguration $Configuration if (-not $Inventory) { $Inventory = Get-OfficeInventory } [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::Assess($target, $Inventory) } function Install-Win32Program { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] [string]$Path, [string[]]$ArgumentList, [switch]$NoWait, [switch]$PassThru, [switch]$DryRun, [int[]]$SuccessExitCodes = @(0, 1641, 3010), [int[]]$RebootExitCodes = @(1641, 3010), [string]$RunId ) if ($DryRun) { $WhatIfPreference = $true } $target = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) if (-not $PSCmdlet.ShouldProcess($target, 'Install Win32 program')) { return [AdNoctem.Substrate.PowerShell.Packages.ProgramCompatibility]::SkipInstall($target, $RunId, $PSBoundParameters.ContainsKey('RunId')) } [AdNoctem.Substrate.PowerShell.Packages.ProgramCompatibility]::Install($target, $ArgumentList, $NoWait, $PassThru, $SuccessExitCodes, $RebootExitCodes, $RunId, $PSBoundParameters.ContainsKey('RunId')) } function Set-ServiceStartupState { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)][string[]]$Name, [Parameter(Mandatory = $true)][ValidateSet('Automatic', 'Manual', 'Disabled')][string]$StartupType, [string[]]$Filter ) foreach ($selection in [AdNoctem.Substrate.PowerShell.Windows.ServiceCompatibility]::Select($Name, $StartupType, $Filter, $script:ProtectedServiceNames)) { if ($selection.SkippedResult) { $selection.SkippedResult; continue } if (-not $PSCmdlet.ShouldProcess($selection.Name, "Set startup type to $StartupType")) { [AdNoctem.Substrate.PowerShell.Windows.ServiceCompatibility]::Result($selection.Name, 'Skipped', 'WhatIf') continue } [AdNoctem.Substrate.PowerShell.Windows.ServiceCompatibility]::Apply($selection.Name, $StartupType) } } function Import-SecurityEventConfiguration { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param ([string]$Path = (Join-Path $PSScriptRoot 'security.psd1'), [switch]$Force) $cached = try { $script:SecurityEventConfiguration } catch { $null } if ($cached -and -not $Force) { return $cached } $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) $script:SecurityEventConfiguration = [AdNoctem.Substrate.PowerShell.Security.EventConfigurationCompatibility]::Read($resolved) $script:SecurityEventConfiguration } function Get-SecurityEventGroup { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param ([Parameter(Mandatory = $true)][string]$Name, [hashtable]$Configuration = (Import-SecurityEventConfiguration)) [AdNoctem.Substrate.PowerShell.Security.EventConfigurationCompatibility]::Group($Name, $Configuration) } function Get-SecurityEventDefinition { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param ([string]$Group, [string]$LogName, [string]$ProviderName, [int[]]$Id, [string]$Name, [hashtable]$Configuration = (Import-SecurityEventConfiguration)) [AdNoctem.Substrate.PowerShell.Security.EventConfigurationCompatibility]::Definitions($Configuration, $Group, $LogName, $ProviderName, $Id, $Name) } function Resolve-WindowsEventMappedField { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param ([Parameter(Mandatory = $true)][string]$MapName, [Parameter(Mandatory = $true)][object]$Value, [hashtable]$Configuration = (Import-SecurityEventConfiguration)) [AdNoctem.Substrate.PowerShell.Security.EventConfigurationCompatibility]::MappedField($MapName, $Value, $Configuration) } function ConvertFrom-WinEvent { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param ([Parameter(Mandatory = $true, ValueFromPipeline = $true)][object]$Event, [hashtable]$Configuration = (Import-SecurityEventConfiguration)) process { [AdNoctem.Substrate.PowerShell.Security.EventConfigurationCompatibility]::ConvertEvent($Event, $Configuration) } } function Get-WindowsEventByDefinition { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [hashtable[]] $Definition, [Parameter(Mandatory = $false)] [string] $Group, [Parameter(Mandatory = $false)] [int[]] $Id, [Parameter(Mandatory = $false)] [string[]] $LogName, [Parameter(Mandatory = $false)] [datetime] $StartTime = (Get-Date).AddDays(-1), [Parameter(Mandatory = $false)] [datetime] $EndTime = (Get-Date), [Parameter(Mandatory = $false)] [string[]] $ComputerName, [Parameter(Mandatory = $false)] [switch] $SkipMissingChannel, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int] $MaxEvents, [Parameter(Mandatory = $false)] [hashtable] $Configuration = (Import-SecurityEventConfiguration) ) begin { $definitions = [Collections.Generic.List[hashtable]]::new() } process { if ($Definition) { $definitions.AddRange($Definition) } } end { $queries = [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::Prepare($definitions.ToArray(), $Group, $Id, $LogName, $StartTime, $EndTime, $MaxEvents, $Configuration) [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::Read($queries, $ComputerName, $SkipMissingChannel, { param($message) Write-Error $message }) } } function Get-WindowsLogonEvent { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [datetime] $StartTime = (Get-Date).AddDays(-1), [Parameter(Mandatory = $false)] [datetime] $EndTime = (Get-Date), [Parameter(Mandatory = $false)] [int[]] $Id, [Parameter(Mandatory = $false)] [int[]] $LogonType, [Parameter(Mandatory = $false)] [switch] $IncludeSystem, [Parameter(Mandatory = $false)] [string[]] $ComputerName, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int] $MaxEvents, [Parameter(Mandatory = $false)] [hashtable] $Configuration = (Import-SecurityEventConfiguration) ) [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::ReadGroup('Logon', $StartTime, $EndTime, $Id, $ComputerName, $MaxEvents, $Configuration, @{ LogonType = $LogonType; IncludeSystem = [bool]$IncludeSystem }, { param($message) Write-Error $message }) } function Get-WindowsAccountChangeEvent { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [datetime] $StartTime = (Get-Date).AddDays(-1), [Parameter(Mandatory = $false)] [datetime] $EndTime = (Get-Date), [Parameter(Mandatory = $false)] [int[]] $Id, [Parameter(Mandatory = $false)] [string] $TargetUserName, [Parameter(Mandatory = $false)] [string] $SubjectUserName, [Parameter(Mandatory = $false)] [string[]] $ComputerName, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int] $MaxEvents, [Parameter(Mandatory = $false)] [hashtable] $Configuration = (Import-SecurityEventConfiguration) ) [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::ReadGroup('AccountChange', $StartTime, $EndTime, $Id, $ComputerName, $MaxEvents, $Configuration, @{ TargetUserName = $TargetUserName; SubjectUserName = $SubjectUserName }, { param($message) Write-Error $message }) } function Get-WindowsServiceEvent { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [datetime] $StartTime = (Get-Date).AddDays(-1), [Parameter(Mandatory = $false)] [datetime] $EndTime = (Get-Date), [Parameter(Mandatory = $false)] [int[]] $Id, [Parameter(Mandatory = $false)] [string] $ServiceName, [Parameter(Mandatory = $false)] [string[]] $ComputerName, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int] $MaxEvents, [Parameter(Mandatory = $false)] [hashtable] $Configuration = (Import-SecurityEventConfiguration) ) [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::ReadGroup('Service', $StartTime, $EndTime, $Id, $ComputerName, $MaxEvents, $Configuration, @{ ServiceName = $ServiceName }, { param($message) Write-Error $message }) } function Get-WindowsBootEvent { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [datetime] $StartTime = (Get-Date).AddDays(-1), [Parameter(Mandatory = $false)] [datetime] $EndTime = (Get-Date), [Parameter(Mandatory = $false)] [int[]] $Id, [Parameter(Mandatory = $false)] [string[]] $ComputerName, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int] $MaxEvents, [Parameter(Mandatory = $false)] [hashtable] $Configuration = (Import-SecurityEventConfiguration) ) [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::ReadGroup('BootShutdown', $StartTime, $EndTime, $Id, $ComputerName, $MaxEvents, $Configuration, @{}, { param($message) Write-Error $message }) } function Get-WindowsPowerShellEvent { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [datetime] $StartTime = (Get-Date).AddDays(-1), [Parameter(Mandatory = $false)] [datetime] $EndTime = (Get-Date), [Parameter(Mandatory = $false)] [int[]] $Id, [Parameter(Mandatory = $false)] [string] $UserName, [Parameter(Mandatory = $false)] [string] $ScriptBlockText, [Parameter(Mandatory = $false)] [string[]] $ComputerName, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int] $MaxEvents, [Parameter(Mandatory = $false)] [hashtable] $Configuration = (Import-SecurityEventConfiguration) ) [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::ReadGroup('PowerShell', $StartTime, $EndTime, $Id, $ComputerName, $MaxEvents, $Configuration, @{ UserName = $UserName; ScriptBlockText = $ScriptBlockText }, { param($message) Write-Error $message }) } function Get-WindowsScheduledTaskEvent { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [datetime] $StartTime = (Get-Date).AddDays(-1), [Parameter(Mandatory = $false)] [datetime] $EndTime = (Get-Date), [Parameter(Mandatory = $false)] [int[]] $Id, [Parameter(Mandatory = $false)] [string] $TaskName, [Parameter(Mandatory = $false)] [string[]] $ComputerName, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int] $MaxEvents, [Parameter(Mandatory = $false)] [hashtable] $Configuration = (Import-SecurityEventConfiguration) ) [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::ReadGroup('ScheduledTask', $StartTime, $EndTime, $Id, $ComputerName, $MaxEvents, $Configuration, @{ TaskName = $TaskName }, { param($message) Write-Error $message }) } function Get-WindowsSysmonEvent { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [datetime] $StartTime = (Get-Date).AddDays(-1), [Parameter(Mandatory = $false)] [datetime] $EndTime = (Get-Date), [Parameter(Mandatory = $false)] [int[]] $Id, [Parameter(Mandatory = $false)] [string[]] $ComputerName, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int] $MaxEvents, [Parameter(Mandatory = $false)] [hashtable] $Configuration = (Import-SecurityEventConfiguration) ) [AdNoctem.Substrate.PowerShell.Security.EventQueryCompatibility]::ReadGroup('Sysmon', $StartTime, $EndTime, $Id, $ComputerName, $MaxEvents, $Configuration, @{}, { param($message) Write-Error $message }) } function Get-UserInfo { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [OutputType([hashtable])] param () [AdNoctem.Substrate.PowerShell.Windows.IdentityCompatibility]::GetUserInfo() } function Get-UserSID { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [OutputType([string])] param ([Parameter(Mandatory = $true)][string]$UserName) try { [AdNoctem.Substrate.PowerShell.Windows.IdentityCompatibility]::GetSecurityIdentifier($UserName) } catch { Write-Error "Could not find SID for user '$UserName'. $_" return $null } } function Test-LGPOInstalled { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $false)] [string]$Path = (Join-Path -Path $env:ProgramData -ChildPath 'AdNoctem.Substrate.PowerShell\tools\LGPO.exe') ) if ([string]::IsNullOrEmpty($Path)) { return (Test-Path -LiteralPath $Path -PathType Leaf) } $provider = $null $drive = $null try { $filePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path, [ref]$provider, [ref]$drive) } catch { return (Test-Path -LiteralPath $Path -PathType Leaf) } if ($provider.Name -ne 'FileSystem') { return (Test-Path -LiteralPath $Path -PathType Leaf) } [AdNoctem.Substrate.Policies.LgpoTool]::new().IsInstalled([AdNoctem.Substrate.IO.FileSystemPath]::Parse($filePath)) } function Invoke-LGPO { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [ValidateScript({ Test-Path -LiteralPath $_ })] [string]$PolicyPath, [Parameter(Mandatory = $false)] [string]$LgpoExe = (Join-Path -Path $env:ProgramData -ChildPath 'AdNoctem.Substrate.PowerShell\tools\LGPO.exe') ) if (-not (Test-Path -LiteralPath $LgpoExe -PathType Leaf)) { throw "LGPO.exe not found at '$LgpoExe'. Run Install-LGPO first." } $arg = if (Test-Path -LiteralPath $PolicyPath -PathType Container) { '/g' } else { '/t' } if (-not $PSCmdlet.ShouldProcess($PolicyPath, "Apply via LGPO.exe ($arg)")) { return } $executable = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($LgpoExe) $policy = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($PolicyPath) try { [AdNoctem.Substrate.PowerShell.Policies.LgpoCompatibility]::Apply($executable, $policy, $PolicyPath, $PSEdition -eq 'Desktop') } catch [ComponentModel.Win32Exception] { $PSCmdlet.ThrowTerminatingError([AdNoctem.Substrate.PowerShell.Policies.LgpoCompatibility]::LaunchError($_.Exception)) } } function Show-Color { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ArgumentList', Justification = 'Unused extra arguments are intentionally accepted to preserve the original Show-Color API.')] [CmdletBinding()] param ([Parameter(ValueFromRemainingArguments = $true)][object[]]$ArgumentList) foreach ($color in [AdNoctem.Substrate.PowerShell.Diagnostics.ColorCompatibility]::GetColors()) { Write-Host $color -ForegroundColor $color } } # Keep the networking parameter declarations here deliberately simple: adding CmdletBinding # would change how these legacy functions accept unused arguments. function Invoke-SubstrateNetworkCompatibility { [CmdletBinding()] param ( [string]$Operation, [PSCustomObject]$Adapter, [string]$AddressFamily = 'IPv4', [switch]$Required, [string]$Type = 'Any' ) # Load Windows' existing CIM display/type definitions, not its networking implementation. if ($null -eq $Adapter) { Import-Module NetAdapter -ErrorAction Stop } $result = [AdNoctem.Substrate.PowerShell.Networking.NetworkCompatibility]::Invoke($Operation, $Adapter, $AddressFamily, [bool]$Required, $Type) foreach ($message in $result.Messages) { Write-Log -Message $message -Color Red } if ($null -ne $result.Error) { $PSCmdlet.ThrowTerminatingError($result.Error) } if ($null -ne $result.Failure) { throw $result.Failure } $result.Value } function Get-DefaultNetworkAdapter { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> param ([ValidateSet('Any', 'WiFi', 'Ethernet', 'VPN')][string]$Type = 'Any', [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-DefaultNetworkAdapter' @PSBoundParameters } function Get-IPAddress { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> param ([ValidateSet('IPv4', 'IPv6')][string]$AddressFamily = 'IPv4', [PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-IPAddress' @PSBoundParameters } function Get-SubnetMask { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> param ([PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-SubnetMask' @PSBoundParameters } function Get-DefaultGateway { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> param ([ValidateSet('IPv4', 'IPv6')][string]$AddressFamily = 'IPv4', [PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-DefaultGateway' @PSBoundParameters } function Get-DNSServer { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> param ([ValidateSet('IPv4', 'IPv6')][string]$AddressFamily = 'IPv4', [PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-DNSServer' @PSBoundParameters } function Get-MACAddress { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> param ([PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-MACAddress' @PSBoundParameters } function Get-NetworkPrefix { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Alias('Get-Network', 'Get-Prefix')] param ([ValidateSet('IPv4', 'IPv6')][string]$AddressFamily = 'IPv4', [PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-NetworkPrefix' @PSBoundParameters } function Get-NetworkPrefixCIDR { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Alias('Get-NetworkCIDR', 'Get-PrefixCIDR')] param ([ValidateSet('IPv4', 'IPv6')][string]$AddressFamily = 'IPv4', [PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-NetworkPrefixCIDR' @PSBoundParameters } function Get-BroadcastAddress { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> param ([PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-BroadcastAddress' @PSBoundParameters } function Get-MulticastAddress { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> param ([PSCustomObject]$Adapter, [switch]$Required) Invoke-SubstrateNetworkCompatibility -Operation 'Get-MulticastAddress' @PSBoundParameters } function Get-DefenderThreatDetection { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [object] $Date = (Get-Date), [Parameter(Mandatory = $false)] [string] $OutputPath, [Parameter(Mandatory = $false)] [ValidateSet('TXT', 'JSON')] [string] $OutputFormat = 'TXT', [Parameter(Mandatory = $false)] [switch] $IncludeURLs ) $cutoffDate = if ($Date -is [datetime]) { $Date } else { Get-Date $Date } Write-Log -Message "Filtering Defender threat detections since $($cutoffDate.ToString('yyyy-MM-dd HH:mm:ss'))" -Color Yellow $detections = @([AdNoctem.Substrate.PowerShell.Security.InspectionCompatibility]::Detections($cutoffDate, $IncludeURLs.IsPresent)) if ($IncludeURLs -and $detections.Count -gt 0) { Write-Log -Message ' -> ThreatDescriptionURL(s) appended' -Color Gray } Write-Log -Message " -> $($detections.Count) detection(s) found" -Color Gray if ($PSBoundParameters.ContainsKey('OutputPath') -and -not [string]::IsNullOrWhiteSpace($OutputPath)) { $_outPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) switch ($OutputFormat) { 'JSON' { $detections | ConvertTo-Json -Depth 3 | Out-File -FilePath $_outPath -Encoding utf8 } 'TXT' { $detections | Format-List * | Out-String -Width 4096 | Out-File -FilePath $_outPath -Encoding utf8 } } Write-Log -Message " -> Written to: $_outPath" -Color Green } else { $detections | Format-List * } } function Get-DefenderThreat { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string] $OutputPath, [Parameter(Mandatory = $false)] [ValidateSet('TXT', 'JSON')] [string] $OutputFormat = 'TXT', [Parameter(Mandatory = $false)] [switch] $IncludeURLs ) Write-Log -Message 'Retrieving Microsoft Defender threat catalog' -Color Yellow $threats = @([AdNoctem.Substrate.PowerShell.Security.InspectionCompatibility]::Threats($IncludeURLs.IsPresent)) if ($IncludeURLs -and $threats.Count -gt 0) { Write-Log -Message ' -> ThreatDescriptionURL(s) appended' -Color Gray } Write-Log -Message " -> $($threats.Count) threat(s) found" -Color Gray if ($PSBoundParameters.ContainsKey('OutputPath') -and -not [string]::IsNullOrWhiteSpace($OutputPath)) { $_outPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) switch ($OutputFormat) { 'JSON' { $threats | ConvertTo-Json -Depth 3 | Out-File -FilePath $_outPath -Encoding utf8 } 'TXT' { $threats | Format-List * | Out-String -Width 4096 | Out-File -FilePath $_outPath -Encoding utf8 } } Write-Log -Message " -> Written to: $_outPath" -Color Green } else { $threats | Format-List * } } function Find-NewlyWrittenObject { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [object] $Date = (Get-Date), [Parameter(Mandatory = $false)] [ValidateRange(0, 168)] [int] $Before = 2, [Parameter(Mandatory = $false)] [ValidateRange(0, 168)] [int] $After = 1, [Parameter(Mandatory = $false)] [string] $Path = "$env:SystemDrive\", [Parameter(Mandatory = $false)] [string] $OutputPath, [Parameter(Mandatory = $false)] [ValidateSet('TXT', 'JSON')] [string] $OutputFormat = 'TXT' ) $anchorDate = if ($Date -is [datetime]) { $Date } else { Get-Date $Date } $windowStart = $anchorDate.AddHours(-$Before) $windowEnd = $anchorDate.AddHours($After) Write-Log -Message "Searching for files written between $($windowStart.ToString('yyyy-MM-dd HH:mm:ss')) and $($windowEnd.ToString('yyyy-MM-dd HH:mm:ss'))" -Color Yellow Write-Log -Message " Root path: $Path" -Color Gray $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) $items = @([AdNoctem.Substrate.PowerShell.Security.InspectionCompatibility]::Files($resolvedPath, $windowStart, $windowEnd, ($PSVersionTable.PSEdition -eq 'Desktop'), [Action[string]] { param($message) Write-Verbose $message })) Write-Log -Message " -> $($items.Count) file(s) found" -Color Gray if ($items.Count -eq 0) { return } if ($PSBoundParameters.ContainsKey('OutputPath') -and -not [string]::IsNullOrWhiteSpace($OutputPath)) { $_outPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) switch ($OutputFormat) { 'JSON' { $items | ConvertTo-Json -Depth 2 | Out-File -FilePath $_outPath -Encoding utf8 } 'TXT' { $items | Format-Table -AutoSize | Out-String -Width 4096 | Out-File -FilePath $_outPath -Encoding utf8 } } Write-Log -Message " -> Written to: $_outPath" -Color Green } else { $items | Format-Table -AutoSize } } function Invoke-SubstrateOfficeWorkflow { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Uses the public command PSCmdlet for confirmation before native execution.')] [CmdletBinding()] param ([object]$Plan, [string]$ExpectedAction, [System.Management.Automation.PSCmdlet]$Caller, [string]$OdtPath, [string]$LogRoot, [bool]$DryRun, [bool]$ForceCloseApps, [Security.SecureString]$ProductKey) $metadata = Get-SubstrateOfficeExecutionContext | ConvertTo-Json -Depth 30 -Compress $response = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::Deployment(($Plan | ConvertTo-Json -Depth 30 -Compress), $ExpectedAction, $OdtPath, $LogRoot, $ForceCloseApps, $ProductKey, $true, $metadata) if ($response.Failure) { Stop-SubstrateOfficeOperation $response.Failure.ReasonCode $response.Failure.Detail $response.Failure.Diagnostic } $result = $response.Json | ConvertFrom-Json foreach ($warning in $result.Plan.Warnings) { Write-Warning $warning } if ($result.Status -ne 'Preview' -or $DryRun) { return $result } $fresh = $result.Plan $description = "$ExpectedAction; remove [$($fresh.RemoveProductId -join ',')]; ALL supported MSI: $($fresh.RemoveMsi); force-close apps: $ForceCloseApps" if ($fresh.Configuration) { $description += "; target $($fresh.Configuration.TargetProductId) $($fresh.Configuration.Version); languages [$($fresh.Configuration.Language -join ',')]" } if (-not $Caller.ShouldProcess($fresh.MachineId, $description)) { return $result } $response = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::Deployment(($fresh | ConvertTo-Json -Depth 30 -Compress), $ExpectedAction, $OdtPath, $LogRoot, $ForceCloseApps, $ProductKey, $false, $metadata) if ($response.Failure) { Stop-SubstrateOfficeOperation $response.Failure.ReasonCode $response.Failure.Detail $response.Failure.Diagnostic } $response.Json | ConvertFrom-Json } function Invoke-SubstrateOfficeRecovery { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Uses the public command PSCmdlet for confirmation before reopening evidence and executing.')] [CmdletBinding()] param ([object]$Recovery, [string]$OriginalAction, [System.Management.Automation.PSCmdlet]$Caller, [string]$OdtPath, [bool]$DryRun, [bool]$ForceCloseApps, [Security.SecureString]$ProductKey) Assert-SubstrateOfficeField $Recovery @('RunId', 'LogRoot', 'Path', 'Record') @('RunId', 'LogRoot') $metadata = Get-SubstrateOfficeExecutionContext | ConvertTo-Json -Depth 30 -Compress $response = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::Recover($Recovery.RunId, $OriginalAction, $OdtPath, $Recovery.LogRoot, $ForceCloseApps, $ProductKey, $true, $metadata) if ($response.Failure) { Stop-SubstrateOfficeOperation $response.Failure.ReasonCode $response.Failure.Detail $response.Failure.Diagnostic } $result = $response.Json | ConvertFrom-Json foreach ($warning in $result.Plan.Warnings) { Write-Warning $warning } if ($result.Status -ne 'Preview' -or $DryRun) { return $result } $plan = $result.Plan $description = "Resume $OriginalAction; remove [$($plan.RemoveProductId -join ',')]; ALL supported MSI: $($plan.RemoveMsi); force-close apps: $ForceCloseApps; target $($plan.Configuration.TargetProductId) $($plan.Configuration.Version); languages [$($plan.Configuration.Language -join ',')]" if (-not $Caller.ShouldProcess($plan.MachineId, $description)) { return $result } # The native manager reopens and validates the journal; caller-supplied Record is never authority. $response = [AdNoctem.Substrate.PowerShell.Office.OfficeCompatibility]::Recover($Recovery.RunId, $OriginalAction, $OdtPath, $Recovery.LogRoot, $ForceCloseApps, $ProductKey, $false, $metadata) if ($response.Failure) { Stop-SubstrateOfficeOperation $response.Failure.ReasonCode $response.Failure.Detail $response.Failure.Diagnostic } $response.Json | ConvertFrom-Json } function Install-Office { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun, [Security.SecureString] $ProductKey ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'Install' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps ProductKey = $ProductKey } Invoke-SubstrateOfficeWorkflow @parameters } } function Uninstall-Office { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'Remove' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps } Invoke-SubstrateOfficeWorkflow @parameters } } function Switch-OfficeDeployment { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun, [Security.SecureString] $ProductKey ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'Migrate' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps ProductKey = $ProductKey } Invoke-SubstrateOfficeWorkflow @parameters } } function Update-Office { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'Update' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps } Invoke-SubstrateOfficeWorkflow @parameters } } function Set-OfficeUpdateConfiguration { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'SetUpdateConfiguration' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps } Invoke-SubstrateOfficeWorkflow @parameters } } function Add-OfficeLanguage { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'AddLanguage' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps } Invoke-SubstrateOfficeWorkflow @parameters } } function Remove-OfficeLanguage { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'RemoveLanguage' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps } Invoke-SubstrateOfficeWorkflow @parameters } } function Set-OfficeApplicationSelection { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'SetApplicationSelection' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps } Invoke-SubstrateOfficeWorkflow @parameters } } function Set-OfficeApplicationPreference { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared lifecycle calls ShouldProcess on the supplied PSCmdlet before any mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Plan, [Parameter(Mandatory = $true)] [string] $OdtPath, [string] $LogRoot = (Join-Path $env:ProgramData 'PSFoundation-Office'), [switch] $ForceCloseApps, [switch] $DryRun ) process { $parameters = @{ Plan = $Plan ExpectedAction = 'SetApplicationPreference' Caller = $PSCmdlet OdtPath = $OdtPath LogRoot = $LogRoot DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps } Invoke-SubstrateOfficeWorkflow @parameters } } function Stop-SubstrateOfficeOperation { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Throws a structured exception without changing machine state.')] [CmdletBinding()] param ( [string] $Reason, [string] $Message, [object] $Diagnostic ) $exception = New-Object InvalidOperationException($Message) $exception.Data['OfficeReason'] = $Reason if ($Diagnostic) { $exception.Data['OfficeDiagnostic'] = $Diagnostic } throw $exception } function Get-SubstrateOfficeMediaDiagnostic { [CmdletBinding()] param ( [object] $Assessment ) # A valid assessment with a changed fingerprint is not a validation failure. # Older/minimal assessments need not contain the optional diagnostic field. if ($null -ne $Assessment -and -not $Assessment.Valid) { if ($Assessment -is [Collections.IDictionary]) { return $Assessment['Diagnostic'] } $property = $Assessment.PSObject.Properties['Diagnostic'] if ($property) { return $property.Value } } } function Assert-SubstrateOfficeField { [CmdletBinding()] param ( [object] $InputObject, [string[]] $Allowed, [string[]] $Required = @() ) if ($null -eq $InputObject -or $InputObject -is [string]) { Stop-SubstrateOfficeOperation InvalidContract 'An Office contract must be a data object.' } if ($InputObject -is [Collections.IDictionary]) { $names = @($InputObject.Keys) } else { $names = @($InputObject.PSObject.Properties | ForEach-Object { $_.Name }) } foreach ($name in $names) { if ($name -notin $Allowed) { Stop-SubstrateOfficeOperation InvalidContract "Unexpected Office contract field: $name." } } foreach ($name in $Required) { if ($name -notin $names) { Stop-SubstrateOfficeOperation InvalidContract "Missing Office contract field: $name." } } } function ConvertTo-SubstrateOfficeList { [CmdletBinding()] param ( [AllowEmptyCollection()] [string[]] $Value, [switch] $Language ) $seen = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) foreach ($item in $Value) { if ([string]::IsNullOrWhiteSpace($item)) { Stop-SubstrateOfficeOperation InvalidConfiguration 'Empty identifiers are not allowed.' } $normalized = $item.Trim() if ($Language) { $normalized = $normalized.ToLowerInvariant() # Deliberately bounded full-UI language support; no inferred proofing/LIP conversion. if ($normalized -notin @( 'en-us', 'de-de', 'fr-fr', 'es-es', 'it-it', 'nl-nl', 'pt-br', 'pt-pt', 'ja-jp', 'ko-kr', 'zh-cn', 'zh-tw', 'pl-pl', 'cs-cz', 'da-dk', 'fi-fi', 'sv-se', 'nb-no', 'hu-hu', 'tr-tr', 'el-gr', 'ro-ro', 'sk-sk', 'sl-si', 'hr-hr', 'bg-bg', 'et-ee', 'lv-lv', 'lt-lt' )) { Stop-SubstrateOfficeOperation Unsupported "Language '$normalized' is outside the supported full-UI language catalog." } } elseif ($normalized -notmatch '^[A-Za-z0-9]+$') { Stop-SubstrateOfficeOperation InvalidConfiguration 'Product/application identifiers must be alphanumeric.' } if ($seen.Add($normalized)) { $normalized } } } function Get-SubstrateOfficeFingerprint { [CmdletBinding()] param ( [object] $InputObject ) $json = ConvertTo-Json -InputObject $InputObject -Depth 30 -Compress $sha = [Security.Cryptography.SHA256]::Create() try { ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($json)))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } } function ConvertTo-SubstrateOfficeConfiguration { [CmdletBinding()] param ( [object] $Configuration ) $fields = @( 'SchemaVersion', 'TargetProductId', 'Architecture', 'Channel', 'Language', 'PrimaryLanguage', 'Version', 'ExcludeApp', 'RequestedLanguages', 'LocaleSource', 'LocaleEvidence' ) Assert-SubstrateOfficeField $Configuration $fields $fields if ($Configuration.SchemaVersion -ne 1) { Stop-SubstrateOfficeOperation InvalidContract 'Unsupported configuration schema.' } $parameters = @{ TargetProductId = $Configuration.TargetProductId Architecture = $Configuration.Architecture Channel = $Configuration.Channel Language = @($Configuration.Language) ExcludeApp = @($Configuration.ExcludeApp) } if ($Configuration.Version) { $parameters.Version = $Configuration.Version } $normalized = New-OfficeDeploymentConfiguration @parameters if ($Configuration.PrimaryLanguage -ne $normalized.PrimaryLanguage) { Stop-SubstrateOfficeOperation InvalidContract 'PrimaryLanguage must equal the first ordered language.' } if ($Configuration.LocaleSource -notin @('Default', 'Explicit', 'InstalledOffice', 'OperatingSystem', 'Recovery')) { Stop-SubstrateOfficeOperation InvalidContract 'Invalid locale source.' } $normalized.RequestedLanguages = @($Configuration.RequestedLanguages | ForEach-Object { [string]$_ }) $normalized.LocaleSource = [string]$Configuration.LocaleSource $normalized.LocaleEvidence = @($Configuration.LocaleEvidence | ForEach-Object { [string]$_ }) $normalized } function Assert-SubstrateOfficeSetting { [CmdletBinding()] param ( [string] $Action, [object] $Settings ) $allowed = @() switch ($Action) { 'SetUpdateConfiguration' { $allowed = @('Enabled', 'UpdatePath', 'TargetVersion', 'Channel') } 'SetApplicationPreference' { $allowed = @('Preferences') } } Assert-SubstrateOfficeField $Settings $allowed if ($Settings -is [Collections.IDictionary]) { $names = @($Settings.Keys) } else { $names = @($Settings.PSObject.Properties | ForEach-Object { $_.Name }) } if ($Action -eq 'SetUpdateConfiguration') { if (-not $names.Count) { Stop-SubstrateOfficeOperation InvalidConfiguration 'Specify at least one update setting.' } if ('Enabled' -in $names -and $Settings.Enabled -isnot [bool]) { Stop-SubstrateOfficeOperation InvalidConfiguration 'Enabled must be a Boolean.' } if ('UpdatePath' -in $names -and [string]$Settings.UpdatePath -notmatch '^(https://|[A-Za-z]:\\|\\\\)') { Stop-SubstrateOfficeOperation InvalidConfiguration 'UpdatePath must be HTTPS or an absolute local/UNC path.' } if ('TargetVersion' -in $names -and [string]$Settings.TargetVersion -notmatch '^16\.0\.\d+\.\d+$') { Stop-SubstrateOfficeOperation InvalidConfiguration 'TargetVersion must be an exact Office build.' } if ('Channel' -in $names -and $Settings.Channel -notin @( 'Current', 'MonthlyEnterprise', 'SemiAnnual', 'PerpetualVL2019', 'PerpetualVL2021', 'PerpetualVL2024' )) { Stop-SubstrateOfficeOperation InvalidConfiguration 'Unsupported update channel.' } } elseif ($Action -eq 'SetApplicationPreference') { if ('Preferences' -notin $names -or -not @($Settings.Preferences).Count) { Stop-SubstrateOfficeOperation InvalidConfiguration 'At least one application preference is required.' } foreach ($preference in $Settings.Preferences) { Assert-SubstrateOfficeField $preference @('Key', 'Name', 'Value', 'Type', 'App', 'Id') @('Key', 'Name', 'Value', 'Type', 'App', 'Id') if ($preference.Key -notmatch '^software\\microsoft\\office\\16\.0\\(word|excel|powerpoint|outlook|access|onenote)(\\[a-z0-9 _-]+)+$' -or $preference.Type -notin @('REG_DWORD', 'REG_SZ') -or $preference.App -notin @('word16', 'excel16', 'ppt16', 'outlook16', 'access16', 'onenote16') -or $preference.Name -notmatch '^[a-zA-Z0-9 _-]+$' -or $preference.Id -notmatch '^[a-zA-Z0-9_-]+$') { Stop-SubstrateOfficeOperation InvalidConfiguration 'Application preference is outside the supported Office preference schema.' } if ($preference.Value -isnot [string] -and $preference.Value -isnot [int] -and $preference.Value -isnot [long]) { Stop-SubstrateOfficeOperation InvalidConfiguration 'Preference values must be strings or integers.' } if ($preference.Type -eq 'REG_DWORD' -and [string]$preference.Value -notmatch '^\d{1,10}$') { Stop-SubstrateOfficeOperation InvalidConfiguration 'REG_DWORD requires an unsigned decimal value.' } if ($preference.Type -eq 'REG_DWORD' -and [long]$preference.Value -gt [uint32]::MaxValue) { Stop-SubstrateOfficeOperation InvalidConfiguration 'REG_DWORD exceeds the unsigned 32-bit range.' } } } } function Get-SubstrateOfficeExecutionContext { [CmdletBinding()] param () $module = $ExecutionContext.SessionState.Module [PSCustomObject]@{ ModuleVersion = if ($module -and $module.Name -eq 'AdNoctem.Substrate.PowerShell') { [string]$module.Version } else { $null } ModulePath = if ($module -and $module.Name -eq 'AdNoctem.Substrate.PowerShell') { $module.Path } else { $PSCommandPath } PowerShellVersion = [string]$PSVersionTable.PSVersion PowerShellEdition = [string]$PSVersionTable.PSEdition ProcessBitness = [IntPtr]::Size * 8 } } function Resume-OfficeInstallation { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Shared lifecycle calls the supplied PSCmdlet ShouldProcess before mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Recovery, [Parameter(Mandatory = $true)] [string] $OdtPath, [switch] $ForceCloseApps, [Security.SecureString] $ProductKey, [switch] $DryRun ) process { $parameters = @{ Recovery = $Recovery OriginalAction = 'Install' Caller = $PSCmdlet OdtPath = $OdtPath DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps ProductKey = $ProductKey } Invoke-SubstrateOfficeRecovery @parameters } } function Resume-OfficeMigration { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Shared lifecycle calls the supplied PSCmdlet ShouldProcess before mutation.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $Recovery, [Parameter(Mandatory = $true)] [string] $OdtPath, [switch] $ForceCloseApps, [Security.SecureString] $ProductKey, [switch] $DryRun ) process { $parameters = @{ Recovery = $Recovery OriginalAction = 'Migrate' Caller = $PSCmdlet OdtPath = $OdtPath DryRun = [bool]$DryRun ForceCloseApps = [bool]$ForceCloseApps ProductKey = $ProductKey } Invoke-SubstrateOfficeRecovery @parameters } } function Convert-Quote { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Function merges two object arrays; existing public name is intentionally plural.')] [OutputType([void])] param ( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $false)] [ValidateSet("Single", "Double")] [string]$To = "Double" ) $content = Get-Content -Path $Path -Raw $converted = [AdNoctem.Substrate.Core.TextConverter]::ConvertQuotes([string]$content, [AdNoctem.Substrate.Core.QuoteStyle]$To) Set-Content -Path $Path -Value $converted } function New-DriveMapping { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory = $true, Position = 0)] [ValidateLength(1, 1)] [ValidatePattern('[A-Z]')] [ValidateScript({ if (($null -ne (Get-Volume $_ -ErrorAction SilentlyContinue))) { throw 'DriveLetter cannot be a physical volume' } $true })] [string] $DriveLetter, [Parameter(Mandatory = $true, Position = 1)] [ValidateScript({ if ((Test-Path -LiteralPath $_) -and ((Split-Path -Path $_ -Leaf) -ne $_)) { return $true } throw 'Path does not exist or is a root level folder' })] [string] $Path, [Parameter(Mandatory = $false)] [string] $SourceDriveLabel, [Parameter(Mandatory = $false)] [string] $DriveLabel, [Parameter(Mandatory = $false)] [switch] $Restart, [Parameter(Mandatory = $false)] [switch] $Force ) if (-not (Test-Elevation)) { throw 'New-DriveMapping requires an elevated process (administrator token).' } $manager = [AdNoctem.Substrate.Windows.DriveMappingManager]::new() $directory = [AdNoctem.Substrate.IO.FileSystemPath]::Parse([IO.Path]::GetFullPath($Path)) $plan = $manager.Prepare([char]$DriveLetter.ToUpperInvariant(), $directory, $DriveLabel, $SourceDriveLabel, [bool]$Force) if (-not $PSCmdlet.ShouldProcess("$DriveLetter`:", "Map folder '$Path' to drive letter")) { if ($WhatIfPreference) { New-OperationResult -Target "$DriveLetter`:" -Source 'DOS Devices' -Action CreateMapping -Status DryRun -Detail "Would map '$Path' to drive letter '$DriveLetter`:'." } return } $result = $manager.Create($plan.DriveLetter, $plan.Directory, $plan.Label, $plan.SourceLabel, [bool]$Force, [Threading.CancellationToken]::None) if ($Restart) { Restart-Computer -Force } New-OperationResult -Target "$($result.DriveLetter):" -Source 'DOS Devices' -Action CreateMapping -Status Completed -Detail "Mapped '$Path' to drive letter '$($result.DriveLetter):'." -Property @{ Path = $result.Directory.Value; DriveLabel = $result.Label } } function Remove-DriveMapping { <# .EXTERNALHELP AdNoctem.Substrate.PowerShell-help.xml #> [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory = $true, Position = 0)] [ValidateLength(1, 1)] [ValidatePattern('[A-Z]')] [ValidateScript({ $value = Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\DOS Devices' -Name "$_`:" -ErrorAction SilentlyContinue if ($null -eq $value) { throw 'Drive letter not found or does not represent a mapped drive' } $true })] [string] $DriveLetter, [Parameter(Mandatory = $false)] [string] $SourceDriveLabel, [Parameter(Mandatory = $false)] [switch] $Restart, [Parameter(Mandatory = $false)] [switch] $Force ) if (-not (Test-Elevation)) { throw 'Remove-DriveMapping requires an elevated process (administrator token).' } $manager = [AdNoctem.Substrate.Windows.DriveMappingManager]::new() $mapping = $manager.Get([char]$DriveLetter.ToUpperInvariant()) if (-not $mapping) { throw "Drive letter '$DriveLetter`:' is not a mapped drive" } if (-not $PSCmdlet.ShouldProcess("$DriveLetter`:", 'Remove drive letter mapping')) { if ($WhatIfPreference) { New-OperationResult -Target "$DriveLetter`:" -Source 'DOS Devices' -Action RemoveMapping -Status DryRun -Detail "Would remove mapping for drive letter '$DriveLetter`:'." } return } $label = $manager.Remove($mapping.DriveLetter, $SourceDriveLabel, [bool]$Force, [Threading.CancellationToken]::None) if ($Restart) { Restart-Computer -Force } New-OperationResult -Target "$($mapping.DriveLetter):" -Source 'DOS Devices' -Action RemoveMapping -Status Completed -Detail "Removed mapping for drive letter '$($mapping.DriveLetter):'." -Property @{ SourceDriveLabel = $label } } |