2cb8a2c317
v2-ci / unit-tests (push) Has been cancelled
An upgrade that swaps the install folder dropped the explicit ScriptWorker isolation ACEs on script-worker\, the install folder and a copied-back pki\ (vd03 2026-11-13). Re-applying needed the operator to repeat the arguments. - Install-ScriptWorkerIsolation.ps1 records its effective settings (InstallRoot, HostServiceAccount, PeCacheDir, HostServiceName, ConfigureHostService) beside DenyPaths under ...\Services\OtOpcUaScriptWorker\Parameters (new plan step SettingsRecord) and locks that key to SYSTEM + Administrators (protected DACL; no secrets recorded). - -Reapply: re-apply from the record, no arguments; prints a summary line (changed/unchanged). - -Verify: check only, [ok]/[MISSING] per step, exit 1 when anything is missing. -Uninstall also works from the record. A node isolated by a pre-#901 installer (DenyPaths only) must run the full-argument install once; both modes say so. - Invoke-OtOpcUaInstallSwap.ps1: preflight -> stop -> move live/candidate -> carry appsettings/pki/logs -> -Reapply -> -Verify -> start; a failed -Verify leaves the service stopped. Also the rollback (-NoCarry). - Tests: dry-run harness 101/101 (record + argument rules; record-key SDDL mutation turns 2 red). The Windows test adds swap -> Verify fails (exactly the 3 lost ACEs) -> Reapply (3 changed) -> Verify ok -> Reapply 12/12 unchanged, PE-cache and firewall negative controls, the swap script, and the pre-#901 refusal; 52/52 on windev under 5.1 and 7, with a scratch local host account and with SYSTEM. Evidence: scripts/validation/opcua-windows-service/evidence/2026-11-13-windev-901-reapply-verify. - Docs: ServiceHosting isolation section; vd03 pack (swap step, rollback, post-deploy gate, one-time full-argument run), the 2026-11-13 vd03 record, and the QA pack (QA swap preceded the isolation install; ACLs checked intact over SMB on vq03/vq04 2026-11-13; next-deploy procedure). Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
288 lines
20 KiB
PowerShell
288 lines
20 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Isolates the OtOpcUa ScriptWorker processes on a Windows node (lmxopcua#780, design §6, owner decisions §14 #5/#6):
|
|
the worker's virtual-account identity, the PeCacheDir ACL, an outbound firewall block on the worker executable and
|
|
a deny on the install folder and on every other sensitive folder (-DenyPaths: pki\, data\, logs\, ...). Idempotent; -WhatIf prints every change; -Uninstall rolls it back.
|
|
|
|
.DESCRIPTION
|
|
Only needed when the node runs Scripting:Runner = OutOfProcess or Shadow. It changes, in order:
|
|
|
|
1. AnchorService a DISABLED service '<WorkerServiceName>' whose account is NT SERVICE\<WorkerServiceName>,
|
|
so that virtual account exists and resolves by name. It is never started: the host launches
|
|
every worker itself, under a restricted token whose restricting SIDs are that account +
|
|
Everyone + BUILTIN\Users + RESTRICTED, with every privilege removed
|
|
(Scripting:Worker:Identity; RestrictedWorkerLauncher.cs). Each access check the worker
|
|
makes must pass for the host account AND that set.
|
|
2. PeCacheDir created; ACL replaced: protected, owner Administrators, SYSTEM + Administrators + the host
|
|
account full control. No Users, no worker access (the HMAC key and launch markers live here).
|
|
3. PeEntriesDir <PeCacheDir>\pe, the worker identity gets Modify (workers compile and write HMAC'd entries).
|
|
4. WorkerFolderAcl <InstallRoot>\script-worker, the worker identity gets read + execute.
|
|
5. InstallRootDeny <InstallRoot>, the worker identity is DENIED read and write (inherited): ReadData/List,
|
|
WriteData/AddFile, AppendData/AddSubdirectory, WriteAttributes/EA, Delete, DeleteChild,
|
|
ChangePermissions, TakeOwnership. Traverse/Execute, ReadAttributes and ReadPermissions stay,
|
|
so the loader still reaches script-worker\. The worker cannot read appsettings*.json, the logs
|
|
or the host binaries even where Users can.
|
|
SensitiveDirDeny (one per -DenyPaths folder, lmxopcua#890) the same inherited deny. The worker token's
|
|
restricting SIDs include Everyone and BUILTIN\Users, so ANY folder Users can read is readable
|
|
to the worker unless it is denied here. Name the deployment folder around InstallRoot (which
|
|
covers pki\, data\, logs\, backups and old app.bak.* swaps in one ACE) AND each folder that
|
|
holds keys or secrets explicitly (pki\, data\, the Secrets/LocalDb folder when it lives
|
|
elsewhere), so the deny also holds on a folder whose DACL does not inherit.
|
|
DenyPathRecord the -DenyPaths list, recorded under the anchor service's key
|
|
(HKLM\...\Services\<WorkerServiceName>\Parameters, value DenyPaths). A re-run removes the
|
|
deny from folders dropped from the list; -Uninstall removes it from every recorded folder
|
|
without the operator repeating -DenyPaths.
|
|
SettingsRecord (lmxopcua#901) the other effective arguments (InstallRoot, HostServiceAccount, PeCacheDir,
|
|
HostServiceName, ConfigureHostService) under the same key, whose DACL is set protected,
|
|
SYSTEM + Administrators only. No secrets. -Reapply and -Verify read them back.
|
|
6. FirewallBlockOutbound a Windows Firewall rule blocking all outbound traffic from
|
|
<InstallRoot>\script-worker\ZB.MOM.WW.OtOpcUa.ScriptWorker.exe (every profile). The
|
|
self-contained Windows publish starts the worker through that apphost; a framework-dependent
|
|
worker (dotnet.exe) is NOT covered — the analyzer's System.Net ban is then the only control.
|
|
7. HostServiceEnv (-ConfigureHostService only) Scripting__Worker__Identity and Scripting__Worker__PeCacheDir
|
|
in the OtOpcUaHost service environment. Scripting:Runner itself is NOT set here: the
|
|
runner flip is a separate, per-tier decision (design §12, P2/P3).
|
|
|
|
A deploy that REPLACES the install folder (a rename/swap) drops the explicit ACEs on script-worker\, on the install
|
|
folder and on any denied folder that is recreated or copied in (pki\ on vd03); files copied into an existing folder
|
|
inherit them (lmxopcua#901). After every swap run, with no other arguments:
|
|
|
|
.\Install-ScriptWorkerIsolation.ps1 -Reapply # the recorded settings; prints changed/unchanged per step
|
|
.\Install-ScriptWorkerIsolation.ps1 -Verify # check only; exit 1 and a [MISSING] line per gap
|
|
|
|
Invoke-OtOpcUaInstallSwap.ps1 does both after its swap and does not start the service when -Verify fails. The host
|
|
must be restarted for -ConfigureHostService to take effect.
|
|
|
|
After an apply it walks every denied folder and WARNs about any file or folder whose DACL is protected (does not
|
|
inherit) and has no deny of its own; the inherited deny does not reach those. Without -DenyPaths it WARNs when the
|
|
folder around InstallRoot has a pki, data, logs, backup, config or secrets sibling.
|
|
|
|
.PARAMETER InstallRoot
|
|
The host install folder (holds OtOpcUa.Host.exe and script-worker\).
|
|
|
|
.PARAMETER HostServiceAccount
|
|
The OtOpcUaHost service account: DOMAIN\name, NT SERVICE\name, LocalSystem, or a SID. Gets full control of PeCacheDir.
|
|
|
|
.PARAMETER DenyPaths
|
|
More folders the worker identity is denied (read and write, inherited), lmxopcua#890. Absolute; must exist; not a
|
|
drive or share root; not script-worker\ or the PE cache. A comma- or semicolon-separated string also works (for
|
|
powershell -File). Example for the wonder layout E:\ApiInstall\OtOpcUa\{app,pki,data,logs,backup}:
|
|
-DenyPaths 'E:\ApiInstall\OtOpcUa','E:\ApiInstall\OtOpcUa\pki','E:\ApiInstall\OtOpcUa\data'
|
|
|
|
.PARAMETER PeCacheDir
|
|
The compiled-PE cache (Scripting:Worker:PeCacheDir). Absolute and outside InstallRoot (the host refuses otherwise).
|
|
Default: $env:ProgramData\OtOpcUa\script-pe-cache.
|
|
|
|
.PARAMETER WorkerServiceName
|
|
The worker identity's virtual account name. Default OtOpcUaScriptWorker (Scripting:Worker:Identity =
|
|
'NT SERVICE\OtOpcUaScriptWorker').
|
|
|
|
.PARAMETER ConfigureHostService
|
|
Also write the two Scripting__Worker__* entries into the host service's environment (or remove them with -Uninstall).
|
|
|
|
.PARAMETER Uninstall
|
|
Roll everything back: the firewall rule, every explicit ACE for the worker identity, the anchor service, and (with
|
|
-ConfigureHostService) the env entries. The PE cache is kept unless -RemovePeCache.
|
|
|
|
.PARAMETER Reapply
|
|
lmxopcua#901. Re-apply with the settings the last install recorded (no other argument except -WorkerServiceName,
|
|
-WhatIf). Fails when nothing is recorded, or when the box was installed by an installer older than #901 (that one
|
|
recorded only DenyPaths): run the full-argument install once, which writes the record.
|
|
|
|
.PARAMETER Verify
|
|
lmxopcua#901. Check only: compares every install step with the machine, prints [ok] or [MISSING] per step and exits 1
|
|
when anything is missing (0 when all is in place). Uses the recorded settings, or -InstallRoot/-HostServiceAccount/...
|
|
when given. Protected-DACL coverage gaps are WARNed, as after an apply, and do not fail the check.
|
|
|
|
.PARAMETER PlanOnly
|
|
Print the plan and return it as objects without touching (or even reading) the machine. Runs on any OS; used by the
|
|
dry-run harness scripts/install/tests/Test-ScriptWorkerIsolation.ps1. -HostServiceAccount must then be a SID.
|
|
|
|
.EXAMPLE
|
|
.\Install-ScriptWorkerIsolation.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -HostServiceAccount 'OTOPCUA\svc-otopcua' -WhatIf
|
|
|
|
.EXAMPLE
|
|
.\Install-ScriptWorkerIsolation.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -HostServiceAccount 'OTOPCUA\svc-otopcua' -ConfigureHostService
|
|
|
|
.EXAMPLE
|
|
.\Install-ScriptWorkerIsolation.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -HostServiceAccount 'OTOPCUA\svc-otopcua' -ConfigureHostService -Uninstall
|
|
|
|
.EXAMPLE
|
|
# After an upgrade swapped the install folder (lmxopcua#901): re-apply from the record, then gate on the check.
|
|
.\Install-ScriptWorkerIsolation.ps1 -Reapply
|
|
.\Install-ScriptWorkerIsolation.ps1 -Verify; if ($LASTEXITCODE) { throw 'worker isolation incomplete' }
|
|
|
|
.EXAMPLE
|
|
# The wonder layout: binaries in app\, keys in pki\, secrets.db + LocalDb in data\ (lmxopcua#890).
|
|
.\Install-ScriptWorkerIsolation.ps1 -InstallRoot E:\ApiInstall\OtOpcUa\app -HostServiceAccount LocalSystem -ConfigureHostService `
|
|
-DenyPaths E:\ApiInstall\OtOpcUa, E:\ApiInstall\OtOpcUa\pki, E:\ApiInstall\OtOpcUa\data
|
|
#>
|
|
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
|
|
param(
|
|
# Required unless -Reapply / -Verify / -Uninstall take them from the record (lmxopcua#901).
|
|
[string]$InstallRoot,
|
|
[string]$HostServiceAccount,
|
|
[string]$PeCacheDir,
|
|
[ValidatePattern('^[A-Za-z0-9_.\-]{1,80}$')] [string]$WorkerServiceName = 'OtOpcUaScriptWorker',
|
|
[string]$HostServiceName = 'OtOpcUaHost',
|
|
[string[]]$DenyPaths = @(),
|
|
[switch]$ConfigureHostService,
|
|
[switch]$Uninstall,
|
|
[switch]$RemovePeCache,
|
|
[switch]$Reapply,
|
|
[switch]$Verify,
|
|
[switch]$PlanOnly
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
Import-Module (Join-Path $PSScriptRoot 'ScriptWorkerIsolation.psm1') -Force
|
|
|
|
# lmxopcua#901: -Reapply / -Verify / -Uninstall without arguments run from what the last install recorded.
|
|
if (@($Reapply, $Verify, $Uninstall, $PlanOnly | Where-Object { $_ }).Count -gt 1) { throw '-Reapply, -Verify, -Uninstall and -PlanOnly are exclusive' }
|
|
if ($Reapply) {
|
|
$given = @('InstallRoot', 'HostServiceAccount', 'PeCacheDir', 'HostServiceName', 'DenyPaths', 'ConfigureHostService', 'RemovePeCache' |
|
|
Where-Object { $PSBoundParameters.ContainsKey($_) })
|
|
if ($given.Count) { throw "-Reapply takes its settings from the record; do not pass -$($given -join ', -') (run a normal install to change them)" }
|
|
}
|
|
$fromRecord = $false
|
|
if (($Reapply -or $Verify -or $Uninstall) -and -not $InstallRoot -and -not $HostServiceAccount) {
|
|
# The record key is SYSTEM + Administrators only, so a non-elevated read would look like "nothing recorded".
|
|
$me = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
|
if (-not $me.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'run elevated (the recorded settings are readable by SYSTEM and Administrators only)' }
|
|
$rec = Get-OtOpcUaIsolationSettings -WorkerServiceName $WorkerServiceName
|
|
if (-not $rec) {
|
|
throw ("no recorded ScriptWorker isolation settings under HKLM\SYSTEM\CurrentControlSet\Services\$WorkerServiceName\Parameters. " +
|
|
'Either isolation is not installed, or it was installed by an installer older than lmxopcua#901 (which recorded only DenyPaths): ' +
|
|
'run the install once with its full arguments (-InstallRoot, -HostServiceAccount, -DenyPaths, [-ConfigureHostService]); that writes the record.')
|
|
}
|
|
$InstallRoot = $rec.InstallRoot
|
|
$HostServiceAccount = $rec.HostServiceAccount
|
|
if (-not $PSBoundParameters.ContainsKey('PeCacheDir') -and $rec.PeCacheDir) { $PeCacheDir = $rec.PeCacheDir }
|
|
if (-not $PSBoundParameters.ContainsKey('HostServiceName')) { $HostServiceName = $rec.HostServiceName }
|
|
if (-not $PSBoundParameters.ContainsKey('DenyPaths')) { $DenyPaths = @($rec.DenyPaths) }
|
|
if (-not $PSBoundParameters.ContainsKey('ConfigureHostService')) { $ConfigureHostService = [switch]$rec.ConfigureHostService }
|
|
$fromRecord = $true
|
|
}
|
|
if (-not $InstallRoot -or -not $HostServiceAccount) { throw '-InstallRoot and -HostServiceAccount are required (or -Reapply / -Verify / -Uninstall with a recorded install)' }
|
|
|
|
if (-not $PeCacheDir) {
|
|
$programData = if ($env:ProgramData) { $env:ProgramData } else { 'C:\ProgramData' }
|
|
$PeCacheDir = "$($programData.TrimEnd('\'))\OtOpcUa\script-pe-cache"
|
|
}
|
|
|
|
function Resolve-HostSid([string]$Account) {
|
|
if ($Account -match '^[Ss]-1-\d+(-\d+)+$') { return $Account.ToUpperInvariant() }
|
|
if ($PlanOnly) { throw "-PlanOnly needs -HostServiceAccount as a SID (no account lookup without the OS)" }
|
|
if ($Account -in 'LocalSystem', '.\LocalSystem', 'NT AUTHORITY\SYSTEM') { return 'S-1-5-18' }
|
|
if ($Account -match '^NT SERVICE\\(.+)$') { return (Get-OtOpcUaServiceSid -Name $Matches[1]) }
|
|
$name = $Account -replace '^\.\\', "$env:COMPUTERNAME\"
|
|
([System.Security.Principal.NTAccount]::new($name)).Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
}
|
|
|
|
$hostSid = Resolve-HostSid $HostServiceAccount
|
|
# lmxopcua#890: what an earlier run denied, so a re-run drops stale denies and -Uninstall removes all of them. -PlanOnly
|
|
# reads nothing from the machine, so there it is empty.
|
|
$recorded = if ($PlanOnly) { @() } else { @(Get-OtOpcUaRecordedDenyPaths -WorkerServiceName $WorkerServiceName) }
|
|
$plan = Get-ScriptWorkerIsolationPlan -InstallRoot $InstallRoot -HostSid $hostSid -PeCacheDir $PeCacheDir -HostServiceAccount $HostServiceAccount `
|
|
-WorkerServiceName $WorkerServiceName -HostServiceName $HostServiceName -DenyPaths $DenyPaths -RecordedDenyPaths $recorded `
|
|
-ConfigureHostService:$ConfigureHostService -Uninstall:$Uninstall -RemovePeCache:$RemovePeCache
|
|
$denied = @($plan | Where-Object { $_.Id -eq 'DenyPathRecord' -and $_.Data.ContainsKey('Paths') } | ForEach-Object { $_.Data.Paths } | Where-Object { $_ })
|
|
|
|
$workerSid = Get-OtOpcUaServiceSid -Name $WorkerServiceName
|
|
Write-Host ("ScriptWorker isolation {0}: worker identity NT SERVICE\{1} ({2}), host account {3} ({4}){5}" -f `
|
|
$(if ($Uninstall) { 'ROLLBACK' } elseif ($Verify) { 'VERIFY' } elseif ($Reapply) { 'REAPPLY' } else { 'install' }), $WorkerServiceName,
|
|
$workerSid, $HostServiceAccount, $hostSid, $(if ($fromRecord) { ', settings from the record' } else { '' }))
|
|
if ($fromRecord) {
|
|
Write-Host (" InstallRoot {0}; PeCacheDir {1}; DenyPaths {2}; ConfigureHostService {3}" -f $InstallRoot, $PeCacheDir,
|
|
$(if (@($DenyPaths).Count) { $DenyPaths -join ', ' } else { 'none' }), [bool]$ConfigureHostService)
|
|
}
|
|
|
|
if ($PlanOnly) {
|
|
foreach ($step in $plan) { Write-Host (" [plan] {0,-22} {1}" -f $step.Id, $step.Change) }
|
|
return $plan
|
|
}
|
|
|
|
if ($PSVersionTable.PSEdition -eq 'Core' -and -not $IsWindows) { throw 'Install-ScriptWorkerIsolation.ps1 applies changes on Windows only (use -PlanOnly elsewhere)' }
|
|
|
|
if ($Verify) {
|
|
# lmxopcua#901: check only, nothing changes. Exit 1 when any step is not in place.
|
|
$checks = foreach ($step in $plan) {
|
|
$r = try { Test-ScriptWorkerIsolationStep -Step $step } catch { [pscustomobject]@{ Ok = $false; Detail = "check failed: $($_.Exception.Message)" } }
|
|
Write-Host (" [{0,-7}] {1,-22} {2} - {3}" -f $(if ($r.Ok) { 'ok' } else { 'MISSING' }), $step.Id, $step.Target, $r.Detail)
|
|
[pscustomobject]@{ Id = $step.Id; Target = $step.Target; Ok = [bool]$r.Ok; Detail = $r.Detail }
|
|
}
|
|
$workerFolder = Join-Path $InstallRoot.TrimEnd('\') 'script-worker'
|
|
$gaps = @(foreach ($p in @($InstallRoot.TrimEnd('\')) + $denied) { Find-OtOpcUaDenyCoverageGap -Path $p -WorkerSid $workerSid -Skip @($workerFolder, $PeCacheDir.TrimEnd('\')) })
|
|
foreach ($g in @($gaps | Sort-Object -Unique)) { Write-Warning "$g does not inherit the worker deny (protected DACL, no deny of its own); check it grants nothing to Everyone, BUILTIN\Users or RESTRICTED." }
|
|
$missing = @($checks | Where-Object { -not $_.Ok })
|
|
Write-Host ''
|
|
if ($missing.Count) {
|
|
Write-Host ("VERIFY FAILED: {0} of {1} step(s) not in place: {2}. Run -Reapply (as administrator) and verify again." -f `
|
|
$missing.Count, @($checks).Count, (($missing | ForEach-Object { "$($_.Id) $($_.Target)" }) -join '; '))
|
|
exit 1
|
|
}
|
|
Write-Host ("VERIFY OK: {0}/{0} step(s) in place." -f @($checks).Count)
|
|
exit 0
|
|
}
|
|
if (-not $Uninstall -and -not (Test-Path -LiteralPath (Join-Path $InstallRoot 'script-worker'))) {
|
|
throw "$InstallRoot\script-worker not found - publish the Host (it carries script-worker\) before isolating it"
|
|
}
|
|
if (-not $Uninstall) {
|
|
# Checked up front so a typo stops the run before any step changes the machine.
|
|
$missing = @($denied | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Container) })
|
|
if ($missing.Count) { throw "DenyPaths folder(s) not found: $($missing -join '; ')" }
|
|
if ($denied.Count -eq 0) {
|
|
$parent = Split-Path -Parent $InstallRoot.TrimEnd('\')
|
|
if ($parent -and $parent -notmatch '^[A-Za-z]:\\?$') {
|
|
$siblings = @(Get-ChildItem -LiteralPath $parent -Directory -Force -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -match '^(pki|data|logs?|backups?|config|secrets?)$' } | ForEach-Object FullName)
|
|
foreach ($s in $siblings) {
|
|
Write-Warning "$s sits beside the install folder and is NOT denied to the worker (BUILTIN\Users is one of its restricting SIDs). Pass -DenyPaths '$parent' and the folders holding keys or secrets (lmxopcua#890)."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
|
if (-not $WhatIfPreference -and -not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
throw 'run elevated (services, firewall rules and folder ACLs need an administrator)'
|
|
}
|
|
|
|
$results = @(foreach ($step in $plan) {
|
|
if ($PSCmdlet.ShouldProcess($step.Target, "$($step.Id): $($step.Change)")) {
|
|
$outcome = Invoke-ScriptWorkerIsolationStep -Step $step
|
|
Write-Host (" [{0,-9}] {1,-22} {2}" -f $outcome, $step.Id, $step.Target)
|
|
[pscustomobject]@{ Id = $step.Id; Target = $step.Target; Outcome = $outcome }
|
|
}
|
|
})
|
|
if (-not $WhatIfPreference) {
|
|
$nChanged = @($results | Where-Object Outcome -eq 'changed').Count
|
|
Write-Host (" summary: {0} step(s), {1} changed, {2} unchanged" -f $results.Count, $nChanged, ($results.Count - $nChanged))
|
|
}
|
|
|
|
if (-not $WhatIfPreference -and -not $Uninstall) {
|
|
# The inherited deny stops at a protected DACL; list every such item that has no deny of its own (lmxopcua#890).
|
|
$workerFolder = Join-Path $InstallRoot.TrimEnd('\') 'script-worker'
|
|
$gaps = @(foreach ($p in @($InstallRoot.TrimEnd('\')) + $denied) { Find-OtOpcUaDenyCoverageGap -Path $p -WorkerSid $workerSid -Skip @($workerFolder, $PeCacheDir.TrimEnd('\')) })
|
|
$gaps = @($gaps | Sort-Object -Unique)
|
|
foreach ($g in $gaps) {
|
|
Write-Warning "$g does not inherit the worker deny (protected DACL, no deny of its own). Check its ACL grants nothing to Everyone, BUILTIN\Users or RESTRICTED, or add it to -DenyPaths."
|
|
}
|
|
Write-Host (" coverage: {0} denied folder(s); {1} protected item(s) the deny does not reach" -f (1 + $denied.Count), $gaps.Count)
|
|
}
|
|
|
|
if (-not $WhatIfPreference) {
|
|
Write-Host ''
|
|
if ($Uninstall) {
|
|
Write-Host 'Rolled back. If the host still sets Scripting:Worker:Identity (appsettings or env), unset it and restart the host.'
|
|
}
|
|
else {
|
|
Write-Host 'Done. Host configuration for this node:'
|
|
Write-Host " Scripting:Worker:Identity = NT SERVICE\$WorkerServiceName"
|
|
Write-Host " Scripting:Worker:PeCacheDir = $PeCacheDir"
|
|
if (-not $ConfigureHostService) { Write-Host ' (not written: re-run with -ConfigureHostService, or set them in appsettings)' }
|
|
Write-Host ' Restart OtOpcUaHost for a configuration change to take effect.'
|
|
}
|
|
}
|
|
$results
|