If you’ve been monitoring Windows for a while, you’ve probably seen this pattern: some checks must run as LocalSystem (S-1-5-18), and the “quick fix” is to run the Icinga Agent itself as SYSTEM. It works. It’s also a really bad idea.
Why?
SystemRunner takes a different approach: instead of running the whole agent as SYSTEM, it provides a narrow, controlled and well-logged channel to run only what really needs SYSTEM privileges.
SystemRunner is a small local framework that:
invoke_as_system.ps1NETWORK SERVICE)Key point: there is exactly one Scheduled Task and one SYSTEM worker that serve all checks. You don’t create a scheduled task per script.
During installation, SystemRunner creates a base directory:
C:\ProgramData\SystemRunner
Inside it you’ll find:
invoke_as_system.ps1 – the Invoker called by checks (for example from Icinga)bin\worker.ps1 – the worker that runs as LocalSystem when triggeredtmp\queue.d\ – JSON files describing queued jobstmp\ – temporary files for stdout, stderr, exit code and a “done” flaglogs\ – per-job text logs plus worker_boot.logSystemRunner also registers a dedicated Windows Event Log:
System_RunnerSR_InvokeEvery new job causes the Invoker to write an event with ID 1001 into this log. The Scheduled Task is configured with an EventTrigger that reacts to those events.
The installer then registers a single Scheduled Task that:
System_Runner log from source SR_Invoke with ID 1001C:\ProgramData\SystemRunner\bin\worker.ps1All privileged execution flows through this one task and this one worker.
powershell.exe -ExecutionPolicy Bypass -File .\SystemRunner-Install.ps1
The installer will:
C:\ProgramData\SystemRunner and its subdirectoriesinvoke_as_system.ps1 and bin\worker.ps1System_Runner event log and the SR_Invoke sourceNETWORK SERVICE has read/execute on C:\ProgramData\SystemRunner and modify on tmp and logsThis allows the Icinga Agent to queue jobs and read back results, while keeping the framework files themselves protected.
To uninstall SystemRunner, run:
powershell.exe -ExecutionPolicy Bypass -File .\SystemRunner-Install.ps1 -Uninstall
The uninstall routine:
System_Runner log and the SR_Invoke source using Remove-EventLogC:\ProgramData\SystemRunnerThere is one detail to be aware of here: when Remove-EventLog is called, the System_Runner log is marked for deletion, but it may still appear in the Windows Event Viewer until either the Windows Event Log service is restarted, or the server is rebooted. So it’s normal to still see System_Runner in the list right after uninstall; it will disappear after the next service restart or reboot.
From Icinga (or any other process on the host) you typically call:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe `
-NoProfile -ExecutionPolicy Bypass `
-File "C:\ProgramData\SystemRunner\invoke_as_system.ps1" `
-ScriptPath "C:\ProgramData\icinga2\scripts\Check-Something.ps1" `
-Arguments "-Param1 foo -Param2 bar" `
-TimeoutSec 180
ScriptPath is the file you want to run as SYSTEM, Arguments are its parameters, and TimeoutSec is the job timeout in seconds.
invoke_as_system.ps1:
tmp\queue.d\<GUID>.json with all these detailsSystem_Runner log (source SR_Invoke) containing the GUIDdone.flag appears or the timeout is reachedWhen the worker has finished, the Invoker:
From Icinga’s point of view, SystemRunner behaves like a regular monitoring plugin: it prints a message and returns a numeric status.
The EventTrigger on the Scheduled Task detects the new ID 1001 event and starts:
powershell.exe -File C:\ProgramData\SystemRunner\bin\worker.ps1
running as LocalSystem.
worker.ps1:
System_Runner logtmp\queue.d\<GUID>.jsonTimeoutSec secondslogs\.log filesTo actually execute a job, two things must both exist and match: a valid queue file tmp\queue.d\<GUID>.json, and an event with ID 1001 containing the same GUID in the System_Runner log. The Invoker is the component that creates them both.
SystemRunner does not blindly start with whatever path it receives. The Worker enforces a sequence of checks on every single job:
GetFullPath$WINDIR, for example C:\Windows\*)cmd.exe, powershell.exe, pwsh.exe, cscript.exe, andwscript.exeThe worker first computes an absolute path:
$fullFilePath = [System.IO.Path]::GetFullPath($FilePath)
This removes tricks like ..\..\something.ps1, ensures that all following checks operate on a clean absolute path, and rejects malformed paths. If GetFullPath fails, the job is marked as fatal, a clear error is logged, and exit code 3 is returned.
The Worker reads the Windows directory, normalizes it, and compares:
$windir = [System.Environment]::GetEnvironmentVariable("WINDIR")
$windirFull = [System.IO.Path]::GetFullPath($windir)
if ($fullFilePath.StartsWith($windirFull, [System.StringComparison]::OrdinalIgnoreCase)) {
# BLOCKED
}
If the file resides anywhere under C:\Windows\..., the job is blocked, an explanatory message is written to the logs, an error is written to stderr, and exit code 3 is returned. This prevents direct use of system binaries or scripts dropped into system directories as ScriptPath.
Even outside C:\Windows, SystemRunner should not become “run this shell as SYSTEM”. For this reason the Worker blocks specific executable names:
$blockedExeNames = @(
"cmd.exe",
"powershell.exe",
"pwsh.exe",
"cscript.exe",
"wscript.exe"
)
$exeName = [System.IO.Path]::GetFileName($fullFilePath).ToLowerInvariant()
if ($blockedExeNames -contains $exeName) {
# BLOCKED
}
If the filename is one of these, the job is refused, the reason is logged and exit code 3 is returned, regardless of the directory in which the file lives.
If the ScriptPath:
C:\Windows\*then the worker proceeds with execution.
SystemRunner can execute anything that Start-Process can launch as a target:
.ps1).exe).bat, .cmd).vbs)Everything remains subject to the path restriction and the filename blacklist.
If the file ends with .ps1, the worker builds a PowerShell command that runs the script, propagates $LASTEXITCODE and sets exit code 3 on exceptions, then calls powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "<cmdBase>".
For all other types (.exe, .bat, .cmd, .vbs, etc.), the worker calls Start-Process -FilePath <full path> and passes arguments parsed with a regex, so that spaces and quotes are handled correctly.
Note that the restriction applies to the file passed as ScriptPath. For example, using a batch file (.bat) or a .vbs script is allowed, even if Windows internally uses cmd.exe or wscript.exe due to file associations. From the SystemRunner point of view, the ScriptPath is your file, not the interpreter executable itself.
After starting the process:
TimeoutSec.log file under logs\ records the command, arguments, PID, exit code, and any reason why the job was blocked or failed.log files and deletes older onesTo abuse SystemRunner and get a malicious file executed as SYSTEM, an attacker needs several things at the same time:
ScriptPath (for example, by changing Icinga configuration such as commands, services or variables like vars.systemrunner_script, or by gaining the ability to call the Invoker directly under the same account as the agent)tmp\queue.d\<GUID>.json and a corresponding event with ID 1001 in the System_Runner log with the same GUIDAll of this typically requires administrative-level access on the host. A normal unprivileged user:
SR_Invoke into the System_Runner logIf you make sure that:
then SystemRunner does not open a new easy path for low-privilege users. It simply concentrates privileged execution into one well-defined and auditable component.
If someone already has the ability to modify monitoring scripts and change Icinga configuration, that person is already in a position to do serious damage, with or without SystemRunner.
.ps1) outside C:\Windows\*.exe) outside C:\Windows\*.bat, .cmd).vbs)Start-ProcessC:\Windows\*cmd.exe, powershell.exe, pwsh.exe, cscript.exe or wscript.exe directly as ScriptPathobject CheckCommand "c-systemrunner-powershell" {
import "plugin-check-command"
command = [ "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" ]
arguments = {
"-NoProfile" = { }
"-ExecutionPolicy" = "Bypass"
"-File" = {
value = "C:\\ProgramData\\SystemRunner\\invoke_as_system.ps1"
order = 0
}
"-ScriptPath" = {
value = "$systemrunner_script$"
required = true
order = 1
}
"-Arguments" = {
value = "$systemrunner_arguments$"
order = 2
skip_key = true
}
"-TimeoutSec" = {
value = "$systemrunner_timeout$"
order = 3
}
}
vars.systemrunner_timeout = 300
}
template Service "st-systemrunner-powershell" {
max_check_attempts = 3
check_interval = 1m
retry_interval = 30s
check_command = "c-systemrunner-powershell"
vars.systemrunner_timeout = 180
}
apply Service "win-vss-system" {
import "st-systemrunner-powershell"
vars.systemrunner_script = "C:\\ProgramData\\icinga2\\scripts\\Check-VSS.ps1"
vars.systemrunner_arguments = "-InstanceID 1"
assign where host.vars.os == "Windows"
}
In this example, Check-VSS.ps1 is executed as SYSTEM through SystemRunner, while the Icinga Agent itself can keep running as NETWORK SERVICE. The script’s output and exit code become the output and state of the Icinga service.
Multiple services can use SystemRunner with different scripts and arguments. They all share the same Invoker, Worker and Scheduled Task.
If the Icinga Agent runs as SYSTEM:
With SystemRunner:
NETWORK SERVICE)C:\Windows\*, no direct cmd.exe / powershell.exe / cscript.exe / wscript.exe as ScriptPath)It doesn’t magically solve every security problem, but it does move you from “the whole agent always runs as SYSTEM” to “only specific scripts go through a controlled SYSTEM channel with clear rules and logs”.
It’s worth saying this explicitly: perfect security doesn’t exist.
SystemRunner:
But if someone compromises the server, gains administrative privileges, or controls both the scripts on disk and the Icinga configuration, they can still do a lot of damage, with or without SystemRunner.
The goal of this framework is not to “secure everything forever”, but to:
C:\Windows\*, no direct consoles, path checks, logging)We’ll never get absolute security, but we can at least stop running the agent as SYSTEM “because it’s easier” and start treating system-level privileges with the respect they deserve. SystemRunner is one step in that direction.
If you’re curious and want to try SystemRunner, you can copy the full script directly from here.
Review it carefully and test it in a lab environment before using it on production systems.
#Requires -Version 5.1
<#
.SYNOPSIS
SystemRunner
.DESCRIPTION
Framework per eseguire script come LocalSystem in modo controllato.
.AUTHOR
Andrea Mariani
Email: andrea.mariani@wuerth-it.com
.VERSION
1.0
.NOTES
Use at your own risk. Review the code and test it in a lab environment before
deploying to production systems.
#>
[CmdletBinding()]
param(
[string]$BaseDir = "C:\ProgramData\SystemRunner",
[string]$TaskName = "RunAsSystem_Task",
[string]$EventSource = "SR_Invoke",
[string]$EventLogName = "System_Runner",
[switch]$Uninstall
)
$ErrorActionPreference = "Stop"
#───────────────────────────────────────────────────────────────────────────────
# DISINSTALLAZIONE
#───────────────────────────────────────────────────────────────────────────────
if ($Uninstall.IsPresent) {
Write-Host "[*] Disinstallazione SystemRunner..." -ForegroundColor Yellow
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
Write-Host " - Task rimosso."
}
if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) {
try { Remove-EventLog -Source $EventSource } catch {}
}
if ([System.Diagnostics.EventLog]::Exists($EventLogName)) {
try { Remove-EventLog -LogName $EventLogName } catch {}
}
if (Test-Path $BaseDir) {
Remove-Item -Recurse -Force $BaseDir
}
Write-Host "[+] Disinstallazione completata." -ForegroundColor Green
exit
}
#───────────────────────────────────────────────────────────────────────────────
# INSTALLAZIONE
#───────────────────────────────────────────────────────────────────────────────
Write-Host "[*] Installando SystemRunner..." -ForegroundColor Cyan
$Bin = Join-Path $BaseDir "bin"
$Tmp = Join-Path $BaseDir "tmp"
$Logs = Join-Path $BaseDir "logs"
$Queue = Join-Path $Tmp "queue.d"
foreach ($d in @($Bin, $Tmp, $Logs, $Queue)) {
if (-not (Test-Path $d)) {
New-Item -ItemType Directory -Path $d -Force | Out-Null
}
}
Write-Host " - Cartelle verificate/create."
# Log eventi
if (-not ([System.Diagnostics.EventLog]::Exists($EventLogName))) {
New-EventLog -LogName $EventLogName -Source $EventSource
} elseif (-not ([System.Diagnostics.EventLog]::SourceExists($EventSource))) {
New-EventLog -LogName $EventLogName -Source $EventSource
}
Write-Host " - Log eventi verificato."
#───────────────────────────────────────────────────────────────────────────────
# INVOKER
#───────────────────────────────────────────────────────────────────────────────
$InvokerPath = Join-Path $BaseDir "invoke_as_system.ps1"
$invoker = @"
#Requires -Version 5.1
param(
[Parameter(Mandatory=`$true)][string]`$ScriptPath,
[string]`$Arguments = "",
[int]`$TimeoutSec = 300
)
`$BaseDir = "C:\ProgramData\SystemRunner"
`$QueueDir = Join-Path `$BaseDir "tmp\\queue.d"
`$EventSource = "SR_Invoke"
`$EventId = 1001
`$LogsDir = Join-Path `$BaseDir "logs"
`$ErrorActionPreference = 'Stop'
`$g = [guid]::NewGuid().ToString()
`$TmpDir = Join-Path `$BaseDir "tmp"
`$stdoutPath = Join-Path `$TmpDir "`$g.stdout.txt"
`$stderrPath = Join-Path `$TmpDir "`$g.stderr.txt"
`$exitCodePath = Join-Path `$TmpDir "`$g.exitcode.txt"
`$doneFlag = Join-Path `$TmpDir "`$g.done.flag"
`$logPath = Join-Path `$LogsDir "`$g.log"
`$queueFile = Join-Path `$QueueDir "`$g.json"
try {
`$payload = @{
ID = `$g
FilePath = `$ScriptPath
Arguments = `$Arguments
TimeoutSec = `$TimeoutSec
StdOutPath = `$stdoutPath
StdErrPath = `$stderrPath
ExitCodePath = `$exitCodePath
DoneFlagPath = `$doneFlag
LogPath = `$logPath
}
`$payload | ConvertTo-Json -Compress | Set-Content -Encoding UTF8 -Path `$queueFile -ErrorAction Stop
} catch {
Write-Output "CRITICAL - impossibile scrivere nella queue: $(`$_.Exception.Message)"
exit 3
}
try {
if (-not [System.Diagnostics.EventLog]::SourceExists(`$EventSource)) {
New-EventLog -LogName "System_Runner" -Source `$EventSource
}
[System.Diagnostics.EventLog]::WriteEntry(`$EventSource, "Job `$g", [System.Diagnostics.EventLogEntryType]::Information, `$EventId)
} catch {
Write-Output "CRITICAL - impossibile scrivere nel registro eventi: $(`$_.Exception.Message)"
exit 3
}
`$deadline = (Get-Date).AddSeconds(`$TimeoutSec + 90)
while ((Get-Date) -lt `$deadline) {
if (Test-Path `$doneFlag) { break }
Start-Sleep -Milliseconds 250
}
if (-not (Test-Path `$doneFlag)) {
Write-Output "UNKNOWN - Timeout dopo `$TimeoutSec secondi (worker non completato)."
if (Test-Path `$logPath) {
Write-Output ("`n--- LOG ---`n" + (Get-Content -Raw `$logPath))
}
exit 3
}
`$stdout = ""
`$stderr = ""
`$exit = 3
try {
if (Test-Path `$stdoutPath) {
`$stdout = Get-Content -Raw `$stdoutPath -ErrorAction Stop
}
} catch {}
try {
if (Test-Path `$stderrPath) {
`$stderr = Get-Content -Raw `$stderrPath -ErrorAction Stop
}
} catch {}
try {
if (Test-Path `$exitCodePath) {
`$exitStr = (Get-Content -Raw `$exitCodePath).Trim()
if (`$exitStr -match '^\d+$') {
`$exit = [int]`$exitStr
} else {
`$exit = 3
}
}
} catch {
`$exit = 3
}
try {
Remove-Item `$stdoutPath, `$stderrPath, `$exitCodePath, `$doneFlag -Force -ErrorAction SilentlyContinue
} catch {}
`$stdout = [string]`$stdout
`$stderr = [string]`$stderr
if ([string]::IsNullOrWhiteSpace(`$stderr)) {
Write-Output (`$stdout.TrimEnd())
}
elseif ([string]::IsNullOrWhiteSpace(`$stdout)) {
Write-Output (`$stderr.TrimEnd())
}
else {
Write-Output ((`$stdout.TrimEnd()) + "`n" + (`$stderr.TrimEnd()))
}
exit `$exit
"@
Set-Content -Path $InvokerPath -Encoding UTF8 -Value $invoker -Force
#───────────────────────────────────────────────────────────────────────────────
# WORKER – blocco console + blocco ScriptPath sotto WINDIR
#───────────────────────────────────────────────────────────────────────────────
$WorkerPath = Join-Path $Bin "worker.ps1"
$worker = @'
#Requires -Version 5.1
param()
$BaseDir = "C:\ProgramData\SystemRunner"
$QueueDir = Join-Path $BaseDir "tmp\queue.d"
$LogsDir = Join-Path $BaseDir "logs"
$CrashLog = Join-Path $LogsDir "worker_boot.log"
function Write-Log([string]$Message, [string]$Path) {
if (-not $Path) { return }
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
try { Add-Content -Path $Path -Value "[$ts] $Message" } catch {}
}
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
Add-Content -Path $CrashLog -Value "[$ts] Worker avviato."
try {
$ev = Get-WinEvent -LogName "System_Runner" -MaxEvents 1 |
Where-Object { $_.Id -eq 1001 } |
Select-Object -First 1
if (-not $ev) {
Add-Content -Path $CrashLog -Value "Nessun evento trovato."
exit
}
$jobID = ($ev.Message -split "Job ")[1].Trim()
if (-not $jobID) {
Add-Content -Path $CrashLog -Value "GUID non trovato."
exit
}
$queueFile = Join-Path $QueueDir ("$jobID.json")
if (-not (Test-Path $queueFile)) {
Add-Content -Path $CrashLog -Value "File di coda mancante: $queueFile"
exit
}
$PayloadAsJson = Get-Content -Raw -Path $queueFile
Add-Content -Path $CrashLog -Value "Caricato payload da queue: $queueFile"
$job = $PayloadAsJson | ConvertFrom-Json
$LogPath = $job.LogPath
Write-Log "Worker parsed JSON OK." $LogPath
$FilePath = $job.FilePath
$ArgumentsString = $job.Arguments
$TimeoutSec = $job.TimeoutSec
$StdOutPath = $job.StdOutPath
$StdErrPath = $job.StdErrPath
$ExitCodePath = $job.ExitCodePath
$DoneFlagPath = $job.DoneFlagPath
$exitCode = 3
#──────────────────────────────────────────────────────────────────────
# NORMALIZZAZIONE PERCORSO E BLOCCO DIRECTORY DI SISTEMA (WINDIR)
#──────────────────────────────────────────────────────────────────────
try {
$fullFilePath = [System.IO.Path]::GetFullPath($FilePath)
} catch {
$errorMessage = "FATAL: percorso ScriptPath non valido: $FilePath"
Write-Log $errorMessage $LogPath
Set-Content -Path $StdErrPath -Value $errorMessage
Set-Content -Path $ExitCodePath -Value $exitCode
New-Item -ItemType File -Path $DoneFlagPath -Force | Out-Null
try { Remove-Item -Path $queueFile -Force } catch {}
exit $exitCode
}
$windir = [System.Environment]::GetEnvironmentVariable("WINDIR")
try {
$windirFull = [System.IO.Path]::GetFullPath($windir)
} catch {
$windirFull = $windir
}
# Se il percorso completo inizia con WINDIR (es. C:\Windows\...), blocca
if ($fullFilePath.StartsWith($windirFull, [System.StringComparison]::OrdinalIgnoreCase)) {
$errorMessage = "BLOCKED: ScriptPath sotto la directory di sistema non consentito: $fullFilePath"
Write-Log $errorMessage $LogPath
Set-Content -Path $StdErrPath -Value $errorMessage
Set-Content -Path $ExitCodePath -Value $exitCode
New-Item -ItemType File -Path $DoneFlagPath -Force | Out-Null
try { Remove-Item -Path $queueFile -Force } catch {}
# Rotazione log (ultimi 10 .log)
try {
$logFiles = Get-ChildItem -Path $LogsDir -Filter "*.log" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending
if ($logFiles.Count -gt 10) {
$logFiles | Select-Object -Skip 10 |
Remove-Item -Force -ErrorAction SilentlyContinue
}
} catch {
Add-Content -Path $CrashLog -Value "Errore nella rotazione log (blocco WINDIR): $($_.Exception.Message)"
}
exit $exitCode
}
#──────────────────────────────────────────────────────────────────────
# BLOCCO ESEGUIBILI "CONSOLE" / INTERPRETI DIRETTI
#──────────────────────────────────────────────────────────────────────
$blockedExeNames = @(
"cmd.exe",
"powershell.exe",
"pwsh.exe",
"cscript.exe",
"wscript.exe"
)
$exeName = [System.IO.Path]::GetFileName($fullFilePath).ToLowerInvariant()
if ($blockedExeNames -contains $exeName) {
$errorMessage = "BLOCKED: eseguibile non consentito come ScriptPath: $exeName"
Write-Log $errorMessage $LogPath
Set-Content -Path $StdErrPath -Value $errorMessage
Set-Content -Path $ExitCodePath -Value $exitCode
New-Item -ItemType File -Path $DoneFlagPath -Force | Out-Null
try { Remove-Item -Path $queueFile -Force } catch {}
# Rotazione log (ultimi 10 .log)
try {
$logFiles = Get-ChildItem -Path $LogsDir -Filter "*.log" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending
if ($logFiles.Count -gt 10) {
$logFiles | Select-Object -Skip 10 |
Remove-Item -Force -ErrorAction SilentlyContinue
}
} catch {
Add-Content -Path $CrashLog -Value "Errore nella rotazione log (blocco exe): $($_.Exception.Message)"
}
exit $exitCode
}
#──────────────────────────────────────────────────────────────────────
# ESECUZIONE SCRIPT / FILE
#──────────────────────────────────────────────────────────────────────
try {
if ($fullFilePath.EndsWith('.ps1')) {
$cmdBase = "try { & `"$fullFilePath`""
if (-not [string]::IsNullOrWhiteSpace($ArgumentsString)) {
$cmdBase += " $ArgumentsString"
}
$cmdBase += "; exit `$LASTEXITCODE } catch { Write-Error `$_.Exception.Message; exit 3 }"
Write-Log "Eseguo PowerShell: $cmdBase" $LogPath
$process = Start-Process -FilePath "powershell.exe" `
-ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-Command",$cmdBase `
-PassThru `
-RedirectStandardOutput $StdOutPath `
-RedirectStandardError $StdErrPath `
-Wait `
-NoNewWindow
}
else {
$ArgumentsArray = @()
if (-not [string]::IsNullOrWhiteSpace($ArgumentsString)) {
$ArgumentsArray = [regex]::Matches($ArgumentsString, '("[^"]*"|''[^'']*''|\S+)') |
ForEach-Object { $_.Value }
}
Write-Log "Eseguo file: $fullFilePath Args: $($ArgumentsArray -join ' ')" $LogPath
if ($ArgumentsArray.Count -gt 0) {
$process = Start-Process -FilePath $fullFilePath `
-ArgumentList $ArgumentsArray `
-PassThru `
-RedirectStandardOutput $StdOutPath `
-RedirectStandardError $StdErrPath `
-Wait `
-NoNewWindow
}
else {
$process = Start-Process -FilePath $fullFilePath `
-PassThru `
-RedirectStandardOutput $StdOutPath `
-RedirectStandardError $StdErrPath `
-Wait `
-NoNewWindow
}
}
Write-Log "PID: $($process.Id)" $LogPath
if ($process.WaitForExit($TimeoutSec * 1000)) {
$exitCode = $process.ExitCode
Write-Log "ExitCode $exitCode" $LogPath
}
else {
Write-Log "TIMEOUT dopo ${TimeoutSec}s" $LogPath
try { Stop-Process -Id $process.Id -Force } catch {}
Add-Content -Path $StdErrPath -Value "`nERROR: Timeout"
$exitCode = 3
}
}
catch {
$errorMessage = "FATAL: $($_.Exception.Message)"
Write-Log "Errore durante l'esecuzione: $errorMessage" $LogPath
Set-Content -Path $StdErrPath -Value $errorMessage
$exitCode = 3
}
finally {
Write-Log "Scrivo exit code ($exitCode) e flag." $LogPath
Set-Content -Path $ExitCodePath -Value $exitCode
New-Item -ItemType File -Path $DoneFlagPath -Force | Out-Null
}
try { Remove-Item -Path $queueFile -Force } catch {}
Add-Content -Path $CrashLog -Value "File $queueFile rimosso."
# ROTAZIONE LOG – conserva solo gli ultimi 10 file .log
try {
$logFiles = Get-ChildItem -Path $LogsDir -Filter "*.log" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending
if ($logFiles.Count -gt 10) {
$logFiles | Select-Object -Skip 10 |
Remove-Item -Force -ErrorAction SilentlyContinue
}
}
catch {
Add-Content -Path $CrashLog -Value "Errore nella rotazione log: $($_.Exception.Message)"
}
}
catch {
Add-Content -Path $CrashLog -Value "Errore fatale worker: $($_.Exception.Message)"
}
'@
Set-Content -Path $WorkerPath -Encoding UTF8 -Value $worker -Force
#───────────────────────────────────────────────────────────────────────────────
# TASK SCHEDULER
#───────────────────────────────────────────────────────────────────────────────
if (-not (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue)) {
$PS64 = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe"
$Argument = "-NoProfile -ExecutionPolicy Bypass -File `"$WorkerPath`""
$taskXml = @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>SystemRunner Worker</Description>
</RegistrationInfo>
<Triggers>
<EventTrigger>
<Enabled>true</Enabled>
<Subscription><![CDATA[
<QueryList>
<Query Id="0" Path="${EventLogName}">
<Select Path="${EventLogName}">*[System[Provider[@Name='${EventSource}'] and EventID=1001]]</Select>
</Query>
</QueryList>
]]></Subscription>
</EventTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>S-1-5-18</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>Queue</MultipleInstancesPolicy>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
</Settings>
<Actions Context="Author">
<Exec>
<Command>$PS64</Command>
<Arguments>$Argument</Arguments>
<WorkingDirectory>$BaseDir</WorkingDirectory>
</Exec>
</Actions>
</Task>
"@
Register-ScheduledTask -TaskName $TaskName -Xml $taskXml -Force | Out-Null
Write-Host " - Task '$TaskName' creato."
}
else {
Write-Host " - Task '$TaskName' già presente."
}
#───────────────────────────────────────────────────────────────────────────────
# ACL
#───────────────────────────────────────────────────────────────────────────────
icacls $BaseDir /grant "NT AUTHORITY\NETWORK SERVICE:(OI)(CI)(RX)" /T | Out-Null
icacls $Tmp /grant "NT AUTHORITY\NETWORK SERVICE:(OI)(CI)(M)" /T | Out-Null
icacls $Logs /grant "NT AUTHORITY\NETWORK SERVICE:(OI)(CI)(M)" /T | Out-Null
Write-Host " - ACL applicate."
Write-Host "[+] Installazione completata – SystemRunner pronto." -ForegroundColor Green