01. 12. 2025 Andrea Mariani NetEye, Unified Monitoring

Running the Icinga Agent as SYSTEM? No thanks.

A safer way to run privileged Windows checks with SystemRunner

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?

  • You throw the least privilege principle out of the window
  • Any serious bug or misconfiguration in the agent can turn into Remote Code Execution (RCE) with full system privileges
  • There is no clear separation between normal monitoring and highly privileged code that can break the server

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.

What SystemRunner is

SystemRunner is a small local framework that:

  • Exposes a single entry point: invoke_as_system.ps1
  • Accepts requests from a low-privilege process (typically the Icinga Agent running as NETWORK SERVICE)
  • Writes each request as a job into a local queue
  • Wakes up a worker that runs as LocalSystem via Task Scheduler
  • Executes the requested file as SYSTEM
  • Returns stdout, stderr and exit codes to the caller, just like a normal plugin

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.

Architecture overview

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 triggered
  • tmp\queue.d\ – JSON files describing queued jobs
  • tmp\ – temporary files for stdout, stderr, exit code and a “done” flag
  • logs\ – per-job text logs plus worker_boot.log

SystemRunner also registers a dedicated Windows Event Log:

  • Log name: System_Runner
  • Source: SR_Invoke

Every 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:

  • Runs as LocalSystem (SID S-1-5-18)
  • Is triggered by events in the System_Runner log from source SR_Invoke with ID 1001
  • Launches C:\ProgramData\SystemRunner\bin\worker.ps1

All privileged execution flows through this one task and this one worker.

Installation and uninstallation

Requirements

  • Windows with PowerShell 5.1
  • PowerShell started with “Run as Administrator”

Installation

  1. Copy the SystemRunner installation script to the server
  2. Open PowerShell as Administrator
  3. Run:
powershell.exe -ExecutionPolicy Bypass -File .\SystemRunner-Install.ps1

The installer will:

  • Create C:\ProgramData\SystemRunner and its subdirectories
  • Create invoke_as_system.ps1 and bin\worker.ps1
  • Create the System_Runner event log and the SR_Invoke source
  • Register a single Scheduled Task that runs as LocalSystem and is triggered by events
  • Apply ACLs so that, for example, NETWORK SERVICE has read/execute on C:\ProgramData\SystemRunner and modify on tmp and logs

This allows the Icinga Agent to queue jobs and read back results, while keeping the framework files themselves protected.

Uninstallation

To uninstall SystemRunner, run:

powershell.exe -ExecutionPolicy Bypass -File .\SystemRunner-Install.ps1 -Uninstall

The uninstall routine:

  • Stops and deletes the Scheduled Task
  • Removes the System_Runner log and the SR_Invoke source using Remove-EventLog
  • Deletes C:\ProgramData\SystemRunner

There 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.

How a job is executed

The check calls the Invoker

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.

What the Invoker does

invoke_as_system.ps1:

  • Generates a GUID for this job
  • Builds file paths for stdout, stderr, exit code, “done” flag and a per-job log
  • Writes a JSON payload to tmp\queue.d\<GUID>.json with all these details
  • Writes an event with ID 1001 into the System_Runner log (source SR_Invoke) containing the GUID
  • Waits until either the done.flag appears or the timeout is reached

When the worker has finished, the Invoker:

  • Reads stdout, stderr and exit code from the temporary files
  • Deletes the temporary files
  • Prints the output (stdout and/or stderr)
  • Exits with the same exit code saved by the worker (0, 1, 2 or 3)

From Icinga’s point of view, SystemRunner behaves like a regular monitoring plugin: it prints a message and returns a numeric status.

The Scheduled Task wakes up the Worker

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.

What the Worker does

worker.ps1:

  • Reads the latest ID 1001 event from the System_Runner log
  • Extracts the GUID from the event message
  • Loads tmp\queue.d\<GUID>.json
  • Applies a series of security checks to the ScriptPath and the execution parameters
  • If all checks are passed, executes the file as SYSTEM and waits up to TimeoutSec seconds
  • Writes the exit code to its designated file and creates the “done” flag
  • Removes the queue file
  • Appends detailed log messages to a text file under logs\
  • Rotates log files and keeps only the last 10 .log files

To 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.

Hardening: checks before running anything as SYSTEM

SystemRunner does not blindly start with whatever path it receives. The Worker enforces a sequence of checks on every single job:

  1. Normalize the path using GetFullPath
  2. Block any file located under the Windows directory ($WINDIR, for example C:\Windows\*)
  3. Block certain executable names such as cmd.exe, powershell.exe, pwsh.exe, cscript.exe, andwscript.exe
  4. Execute the file only if the previous checks have passed
  5. Enforce the timeout, collect the exit code and write logs

1. Path normalization

The 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.

2. Blocking files under the system directory

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.

3. Blocking console and interpreter executables

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.

4. What SystemRunner is allowed to execute

If the ScriptPath:

  • Is a valid, normalized path
  • Is not under C:\Windows\*
  • Does not match any of the blocked executable names

then the worker proceeds with execution.

SystemRunner can execute anything that Start-Process can launch as a target:

  • PowerShell scripts (.ps1)
  • Executables (.exe)
  • Batch scripts (.bat, .cmd)
  • VBScript files (.vbs)
  • And other executable file types

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.

5. Timeout, exit code and logging

After starting the process:

  • The worker waits for the specified TimeoutSec
  • If the process does not finish in time, it logs a timeout message, tries to terminate the process, appends an error to stderr and forces exit code 3
  • In all cases, the exit code is written to a dedicated file which the Invoker will read and return to Icinga
  • A per-job .log file under logs\ records the command, arguments, PID, exit code, and any reason why the job was blocked or failed
  • After each run, the Worker keeps only the last 10 .log files and deletes older ones

Security model: What it would take to abuse SystemRunner

To abuse SystemRunner and get a malicious file executed as SYSTEM, an attacker needs several things at the same time:

  • The ability to influence which file path is used as 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)
  • The ability to write or modify files in locations that are used as ScriptPath targets and are readable by SYSTEM
  • And, in practice, enough privileges to create both parts of a job: a valid JSON entry in tmp\queue.d\<GUID>.json and a corresponding event with ID 1001 in the System_Runner log with the same GUID

All of this typically requires administrative-level access on the host. A normal unprivileged user:

  • Cannot change Icinga configuration
  • Cannot write into the directories where “real” monitoring scripts live
  • Cannot write events as SR_Invoke into the System_Runner log

If you make sure that:

  • Directories used for SystemRunner scripts are writable only by Administrators and SYSTEM
  • Icinga configuration is managed only by trusted admins
  • ScriptPath is never taken from untrusted input

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.

What SystemRunner can and cannot do

It can

  • Run as SYSTEM:
    • PowerShell scripts (.ps1) outside C:\Windows\*
    • Executables (.exe) outside C:\Windows\*
    • Batch scripts (.bat, .cmd)
    • VBScript files (.vbs)
    • Other file types supported by Start-Process
  • Serve many different checks: one Invoker, one Worker and one Scheduled Task handle all jobs
  • Return plugin-style output and Nagios-compatible exit codes (0, 1, 2, 3)
  • Keep a clear audit trail of what was executed, with which arguments, with which result, and why something was blocked

It cannot

  • Execute files located under C:\Windows\*
  • Use cmd.exe, powershell.exe, pwsh.exe, cscript.exe or wscript.exe directly as ScriptPath
  • Invent new scripts or binaries: it only executes files that already exist on disk and that you explicitly reference

Example integration with Icinga

CheckCommand

object 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
}

Service template

template Service "st-systemrunner-powershell" {
  max_check_attempts = 3
  check_interval     = 1m
  retry_interval     = 30s

  check_command = "c-systemrunner-powershell"

  vars.systemrunner_timeout = 180
}

Example service

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.

Why I say “Running the Icinga Agent as SYSTEM? No thanks.”

If the Icinga Agent runs as SYSTEM:

  • Any serious vulnerability or injection in the agent can become Remote Code Execution with full system privileges
  • Every plugin automatically inherits SYSTEM, even when it doesn’t really need it
  • There’s no clean separation between ordinary monitoring logic and highly privileged actions

With SystemRunner:

  • The Icinga Agent runs with reduced privileges (for example as NETWORK SERVICE)
  • There is a single Worker running as SYSTEM, and invoked only when needed
  • Privileged execution is constrained by clear rules (no C:\Windows\*, no direct cmd.exe / powershell.exe / cscript.exe / wscript.exe as ScriptPath)
  • All privileged jobs are logged, time-limited and rotated

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”.

An honest note about security

It’s worth saying this explicitly: perfect security doesn’t exist.

SystemRunner:

  • Reduces the attack surface compared to running the Icinga Agent as SYSTEM
  • Makes it harder to abuse privileged execution
  • Makes privileged actions more visible and auditable

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:

  • Move privileged execution into a narrower and more controlled channel
  • Add sensible guardrails (no C:\Windows\*, no direct consoles, path checks, logging)
  • Make it obvious what is being executed as SYSTEM

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.

SystemRunner-Install.ps1

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

Andrea Mariani

Andrea Mariani

Author

Andrea Mariani

Leave a Reply

Your email address will not be published. Required fields are marked *

Archive