A self-hosted web UI for running PowerShell and Batch scripts on a Windows host, with support for running a script under a different Windows account (optionally elevated), and role-based access control (Admin / User with per-script Execute/Write grants).
Read SECURITY.md before deploying this anywhere near production - it covers the two secrets you must configure, how the elevation/run-as mechanism works, and what to test on a real Windows box before trusting it (this project was built in a Linux sandbox with no Windows host available, so the Win32 interop has not been run for real - see the "Testing the elevation path" section).
PowerShell/Batch execution, real UAC-style elevation, and "run this script as a
different Windows account" are all Win32 operating-system features - there's no
elevation, no LogonUser, no Windows accounts to run as outside a Windows host. This
app runs directly on Windows, either via dotnet run for development or, for
production, installed as a Windows Service via the MSI installer (see "Installing as a
Windows Service" below).
src/
ScriptRunner.Core - domain models, execution engine (script types, Win32 process
launcher for elevation/run-as), credential encryption, auth
primitives. No ASP.NET or EF dependency - it's plain .NET.
ScriptRunner.Data - EF Core (SQLite) DbContext and startup seeding.
ScriptRunner.Api - ASP.NET Core Web API + SignalR hub (live script output) +
controllers + JWT auth + authorization policies + the
first-run setup wizard's backend (IDataDirectoryProvider,
SetupController, SetupGateMiddleware).
web/ - React + TypeScript (Vite) single-page app.
installer/ - MSI installer (WiX) that packages the published app as a
Windows Service - see "Installing as a Windows Service" below.
Everything needed to add a new script type later (e.g. Python) is isolated behind
IScriptExecutor in ScriptRunner.Core/Services/Execution - implement the interface,
register it in Program.cs, and it shows up in the UI's type picker automatically.
- Windows 10/11 or Windows Server, with:
- .NET 8 SDK
- Node.js 20+ (for the frontend)
- This project was built and its frontend was verified in a Linux sandbox that could
not install the .NET SDK (no network access to nuget/dotnet in that environment), so
the backend has not been compiled here - run
dotnet buildyourself as the first step and fix anything a real compiler flags. The React frontend, in contrast, has been installed, built, and linted successfully as part of building this.
Local development (dotnet run) skips the setup wizard described below entirely -
appsettings.Development.json points the database straight at a local
scriptrunner.db file, which is treated as "setup already complete" (see
IDataDirectoryProvider.IsConnectionStringOverridden). An installed Windows Service
(the MSI, or Install-Service.ps1) instead shows a first-run web setup wizard - see
"First run for an installed service" further down.
-
Generate the two required secrets (see SECURITY.md for details) and set them as environment variables:
$env:SCRIPTRUNNER_MASTER_KEY = [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) $env:SCRIPTRUNNER_JWT_SIGNING_KEY = [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
(Don't use
Get-Randomfor this - it's a fast, statistically-random PRNG, not a cryptographically secure one, and both of these values are encryption/signing keys where that distinction matters.RandomNumberGenerator.GetBytesis the CSPRNG. If you're on Windows PowerShell 5.1 rather than PowerShell 7, use[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)instead - see SECURITY.md.)(Set these permanently via System Properties > Environment Variables, or in your Windows Service configuration -
setxonly affects future processes, not already-open shells.) -
Restore and run the API:
cd src/ScriptRunner.Api dotnet restore dotnet runOn first startup, if no admin account exists yet, one is created automatically as
admin. If you setSCRIPTRUNNER_ADMIN_USERNAME/SCRIPTRUNNER_ADMIN_PASSWORDfirst, those are used; otherwise the account is created with a fixed default password (ScriptRunner- seeDbSeeder.DefaultAdminPassword). This is a known, public credential, not a randomly-generated one - anyone with the source knows it, so treat it as a placeholder to change immediately after your first login, not as a real secret. A warning is printed to the console/log, and also written to afirst-run-admin-password.txtfile next to the database (handy for a headless Windows Service, where console output isn't visible) - delete that file once you've logged in and changed the password. SetSCRIPTRUNNER_ADMIN_PASSWORDbefore first run for anything beyond local testing so the default is never used at all.The SQLite database itself defaults to
%ProgramData%\ScriptRunner\scriptrunner.dbin every environment except local development (dotnet run, which usesscriptrunner.dbright insrc/ScriptRunner.Api/for convenience - seeappsettings.Development.json). Override either withConnectionStrings:Defaultin config or the environment. -
In a second terminal, run the frontend dev server:
cd web npm install npm run dev
Open the URL Vite prints (default
http://localhost:5173). It proxies/apiand/hubsto the backend (seeweb/vite.config.ts- adjust the target port if yourdotnet runprinted a different one). -
Log in as the admin, then:
- Admin > Credentials: add any Windows accounts you want scripts to be able to run as (and/or to authenticate to a network script share - see below).
- Admin > Manage Scripts: create a script (PowerShell or Batch), either pasted directly into ScriptRunner ("Stored") or as a live reference to a UNC path ("Network reference" - see "Network-referenced scripts" below), declare its parameters, decide whether it needs elevation, and pick which stored credentials it's allowed to run as. Grant specific users Execute and/or Write access from the same screen.
- Admin > Users: create accounts for your team.
A script can either be Stored (its content lives in ScriptRunner's own database, as
it always has) or a Network reference - a UNC path (e.g.
\\fileserver\scripts\deploy.ps1) that ScriptRunner reads live at the start of every
run, instead of copying the content into the database. This is for teams that already
keep a script repository elsewhere (a file share, a synced folder from source control,
etc.) and want that share to stay the single source of truth - ScriptRunner always
executes whatever's on the share right now, not a stale copy.
Pick "Network reference" in the script form (Admin > Manage Scripts), enter the UNC
path, and optionally pick a stored credential to authenticate to that share with (reuses
the same encrypted-at-rest credential store as run-as/elevation - see
SECURITY.md). Use the Test connection button to verify the path is
reachable and readable before saving; if it comes back "access denied," the form lets
you pick a different credential (or links to Admin > Credentials to add one) right there.
No credential is needed if the ScriptRunner service account itself already has read
access to the share.
Under the hood this uses a one-shot Windows impersonation (LogonUser with
LOGON32_LOGON_NEW_CREDENTIALS + WindowsIdentity) scoped to just that one file read,
not a persistent mapped-drive-style session - see the comment on
NetworkScriptContentReader for why that specifically avoids Windows's
one-credential-per-server-per-session limitation for SMB/UNC connections, which would
otherwise bite you the moment two scripts on the same server need different credentials.
Recommended: build and run the MSI installer. .\build-installer.ps1 produces
installer\bin\Release\ScriptRunnerSetup.msi, which installs the app to
C:\Program Files\ScriptRunner, registers and starts the ScriptRunner service,
generates SCRIPTRUNNER_MASTER_KEY/SCRIPTRUNNER_JWT_SIGNING_KEY and stores them
scoped to that service's own registry key (not machine-wide), opens a firewall rule for
the port, and sets %ProgramData%\ScriptRunner as the default data directory (you
choose the real one from the web setup wizard - see "First run for an installed service"
below). See installer/README.md for prerequisites (the WiX CLI),
what exactly gets installed, upgrading, and uninstalling - including a note on what to
budget for testing on your specific target machine.
Unlike local development, an installed service has no admin account and (until you
choose one) no finalized data directory - browsing to http://<machine>:8080 after
install lands you on a first-run setup wizard instead of a login page:
- Welcome - a reminder that the master key/JWT signing key were already generated automatically at install time (see above); this wizard only handles the data directory and admin account.
- Data directory - a folder-tree browser (backed by a small server-side API, since
a browser can't open a native OS folder dialog against a remote machine's
filesystem) for picking where the SQLite database lives. The default -
%ProgramData%\ScriptRunner- works for most installs; browse to a different drive or folder if you'd rather, or create a new folder right from the browser. - Admin account - choose the first admin's username and password. There's no fixed default password in this path (unlike the env-var/local-dev fallback described above) - you set a real one here.
Finishing the wizard signs you in as that admin account immediately - no separate login
step. From then on GET /api/setup/status reports setup complete and every other API
endpoint (previously blocked by SetupGateMiddleware with a 503 setup_required
response) becomes available.
Skipping the wizard for scripted/unattended deployments: if
SCRIPTRUNNER_ADMIN_USERNAME/SCRIPTRUNNER_ADMIN_PASSWORD are already set as
environment variables before the service's first startup (the same variables used by
local development, above), ScriptRunner auto-completes setup at the default data
directory and creates that admin account immediately - the wizard never appears. This is
the same "set env vars to skip the default-password path" behavior local development has
always had; it now also skips the wizard entirely.
If you'd rather not use the MSI, Program.cs already calls
builder.Host.UseWindowsService(), so a plain publish also runs as a normal Windows
Service - installer\scripts\Install-Service.ps1 does this by hand (idempotent,
handles the registry env vars and firewall rule for you):
dotnet publish src\ScriptRunner.Api -c Release -o C:\ScriptRunner
.\installer\scripts\Install-Service.ps1 -InstallDir C:\ScriptRunner -DataDir "$env:ProgramData\ScriptRunner"or, doing it fully by hand with raw sc.exe (what the script above automates):
dotnet publish src/ScriptRunner.Api -c Release -o C:\ScriptRunner
sc.exe create ScriptRunner binPath= "C:\ScriptRunner\ScriptRunner.Api.exe" start= auto
sc.exe failure ScriptRunner reset= 86400 actions= restart/5000Either way, set SCRIPTRUNNER_MASTER_KEY/SCRIPTRUNNER_JWT_SIGNING_KEY somewhere the
service process will see them - not a user-level env var from your own login
session, which a service running under a different account won't inherit. The MSI and
Install-Service.ps1 both use the service's own registry Environment key
(HKLM:\SYSTEM\CurrentControlSet\Services\ScriptRunner\Environment) so the secrets stay
scoped to this one service rather than polluting every process on the machine; if you're
setting them by hand instead, System Properties > Environment Variables (machine-level)
also works.
Which account should the service run as? See SECURITY.md's "Choosing the service
account" section - short version: LocalSystem is the simplest choice that reliably
supports LogonUser/CreateProcessAsUser/elevation for other accounts, but consider a
dedicated service account with just the privileges it needs if your security policy
requires avoiding LocalSystem.
- Implement
IScriptExecutorinScriptRunner.Core/Services/Execution(seePowerShellExecutor/BatchExecutorfor the pattern - you mostly need to build the right command line for your interpreter). - Register it:
builder.Services.AddSingleton<IScriptExecutor, YourExecutor>();inProgram.cs. - Add its key to
ScriptType.KnownTypesinScriptRunner.Core/Models/ScriptType.csso it shows up in the admin "New Script" type picker.
No other code needs to change - the execution pipeline, permission model, credential
handling, and UI all key off IScriptExecutor.ScriptTypeKey/FileExtension.
- EF Core Migrations (the app uses
EnsureCreated()for simplicity). Switch todotnet ef migrationsonce the schema stabilizes for your deployment. Practically: this means an existingscriptrunner.dbfrom before a schema change (e.g. theSourceMode/NetworkPath/NetworkCredentialIdcolumns added for network-referenced scripts) won't gain the new columns on its own - delete the old database (and anybootstrap.jsonnext to it) and let ScriptRunner recreate it, or migrate the data by hand, after pulling a build with schema changes. - Audit-log export, script versioning/history, scheduled (cron-style) runs, and multi-node/HA deployment (the SQLite file and in-memory run-cancellation registry are single-instance).
- Fine-grained rate limiting/lockout on login attempts.