Feature Requests
New / Open / Unreviewed

[Feature]: Bulk Windows Installer

What is the installation method of your PatchMon server?

Docker

What is the version of your PatchMon server?

2.0.2

Briefly describe the feature

Windows Auto Enroll/Bulk Install

Detailed description

Would love to see a way to deploy the agent to more than one Windows host at a time via ether some type of Auto Enroll system and generic install script. Or just be able to use the normal install script and not have to define a hostname and let the script pull it from the system, allowing you to use the script on multiple hosts at once.

Why is this useful?

Makes it easier to mass deploy PatchMon via other RRM tools. For example, I would like to use a single script in my TacticalRMM instance that will install the agent, and it will auto-enroll to PatchMon

2 Comments

Posting anonymously

Teejer·1 day ago

Followed this up and got the script above working as a full GPO deployment on a Windows domain - staged to SYSVOL, installed by a GPO scheduled-task preference running as SYSTEM, hosts self-enrol through auto-enrollment so no pre-created host records are needed. Ran it manually on a test client, then deployed it via the GPO task and the host enrolled and reported in cleanly.

Repo with everything (installer, uninstaller, the scheduled-task XML, the GPO staging helper, runbook): https://github.com/Teejer/patchmon-agent-deploy

Things the draft script needed that I'd flag for anyone else picking this up:

  • config.yml must use single-quoted Windows paths. credentials_file: "C:\ProgramData\PatchMon\credentials.yml" is invalid YAML - a double-quoted scalar processes backslash escapes, so \c is an unknown escape. The nasty part is the agent's reaction: it warns once, replaces the file with its own defaults (no patchmon_server) and carries on, so the host silently never reports. PatchMon's own patchmon_install_windows.ps1 writes single-quoted scalars for exactly this reason; this now matches it, and repairs files written the broken way.
  • The original had $autoEnrollSecret+"abcdef" (concatenation instead of assignment), a hardcoded server URL, and $tempPath used before it was assigned with the download block duplicated.
  • PowerShell 5.1 compatibility: New-Service -Description doesn't exist there (used sc.exe description), and -SkipCertificateCheck is PS 6+ ([System.Net.ServicePointManager] callback instead).
  • Idempotence: exits 0 when already healthy, and only takes the "nothing to do" exit when config.yml is parseable and points at the configured server - otherwise it repairs the config, restarts the service and confirms with patchmon-agent ping before considering a reinstall. The service is stopped before its binary is replaced and started rather than recreated when it exists, so the task can run daily without breaking anything.
  • A host appearing in PatchMon with no details right after enrollment is normal - the host record is created at enrollment, the first inventory takes a few minutes.

One deployment caveat worth knowing: the staged script in SYSVOL is readable by every authenticated domain user and contains the auto-enrollment credentials, so treat that token as shareable-to-the-domain and rotate it independently of the admin API credentials.

The installer as deployed (server URL and enrollment credentials are placeholders here):

#Requires -Version 5.1
#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Installs the PatchMon agent on a Windows machine and self-enrolls the host record.

.DESCRIPTION
    What it does, in order:
      1. Exits quietly (code 0) if a healthy PatchMonAgent service is already installed.
      2. Calls the PatchMon auto-enrollment API to create this machine's host record
         and get its permanent api_id / api_key. No pre-populating hosts required.
      3. Downloads the agent binary for this machine's architecture.
      4. Writes config.yml and credentials under C:\ProgramData\PatchMon.
      5. Verifies with `patchmon-agent ping`, then creates and starts the
         PatchMonAgent service (LocalSystem, automatic, auto-restart on failure).

    It is idempotent and cheap on repeat runs, so it is meant to be run every day
    from a GPO Scheduled Task as SYSTEM. New domain members pick up the GPO and
    install themselves.

.PARAMETER ServerURL
    Base URL of the PatchMon server. Defaults to $DefaultServerURL below.

.PARAMETER RegisterScheduledTask
    Also register the daily "PatchMon Agent Install" scheduled task on this machine.
    Use this for the first push (Intune / PDQ / PsExec); GPO-managed machines get the
    task from Group Policy instead and do not need this.

.PARAMETER Force
    Reinstall the agent even if the service already exists.

.EXAMPLE
    .\patchmon-agent-install.ps1
    Install using the settings hard-coded below.

.EXAMPLE
    .\patchmon-agent-install.ps1 -RegisterScheduledTask
    Install and leave a daily self-healing task behind on this box.

.EXAMPLE
    .\patchmon-agent-install.ps1 -ServerURL "http://patchmon.example.com:3000" -SkipSslVerify $true
    Point at a different server without editing the script.

.NOTES
    Deployment walkthrough (auto-enrollment token, signing, GPO): see README.md
    Cleanup: uninstall-patchmon-agent.ps1
#>
[CmdletBinding()]
param(
    # ------------------------------------------------------------------ #
    #  SITE SETTINGS - these three are the ones you edit.                #
    # ------------------------------------------------------------------ #
    # Put your real PatchMon URL here. HTTPS is strongly preferred: the agent sends
    # its API credentials on every report. If your server is still plain HTTP on a
    # port, uncomment the line below, change the one above, and switch back to HTTPS
    # as soon as the server has a certificate.
    # [string]$ServerURL

Posting anonymously

Teejer·5 months ago

I used something like this to make the computer record and install the agent without having to prepopulate the record. I also needed mine to be digitally signed in my case, having a static file that copied the bootstrap token made that possible to sign easily.

I may make changes, this is my first draft of it. It is working though.

You'll need to create an autoenrollment token and put it in the $autoEnroll* variables.

I'm distributing this through GPO, creates a scheduled task that runs every day. This way as new computers get added to the domain they also get the task and install the agent.

#https://patchmon.net/docs/patchmon-operator-guide#installing-the-patchmon-agent
#a lot of this was taken from the installer script you download from the patchmon server that embeds the bootstrap token, but I needed to have a signed version, so I  did some of it differently.
$ServerURL     = "https://patchmon.host.edu"
$InstallPath   = "C:\Program Files\PatchMon"
$ConfigPath    = "C:\ProgramData\PatchMon"
$SkipSslVerify = $false
$serviceName = "PatchMonAgent"
$serviceDisplayName = "PatchMon Agent"
$serviceDescription = "PatchMon Agent - Monitors system packages and sends updates to PatchMon server"
$autoEnrollKey="patchmon_ae_abcde"
$autoEnrollSecret+"abcdef"

function log( $message){
    try{
        
        $message = "<$(get-date -Format "MM/dd/yy hh:mm")" + $message
        $logFile = $("C:\patchmon_" + $(get-date -uformat "%Y_%m") + ".log")
        #write-host $message |out-file $logFile -append
        add-content $logFile $($message) |Out-Null
        #write-host $thisScript
        

    } catch [Exception]{
        Write-host $("Log Error: " + $_.Exception.Message);
    }
}

$apiId = $null  
$apiKey = $null


# Check if service already exists
$existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue

if ($existingService) {
    Write-Host "Service already exists, no need to install. I think?" -ForegroundColor Yellow
    exit 0
}


#Make new host in patchmon
#get initial api_id andn key
try{
    $enroll=Invoke-WebRequest -Uri "$ServerURL/api/v1/auto-enrollment/enroll" -Headers @{"X-Auto-Enrollment-Key"=$($autoEnrollKey); "X-Auto-Enrollment-Secret"=$($autoEnrollSecret)} -Method POST -Body "{`"friendly_name`":`"$($env:COMPUTERNAME)`"}" -ContentType "application/json" -UseBasicParsing
    if($enroll.StatusCode -eq 201){
        $e=convertfrom-json $enroll
        $apiId = $e.host.api_id  
        $apiKey = $e.host.api_key
        log "Enrollment Successful"
    }else{
        log "Failed enrollment code:$($enroll.StatusCode)"
    }
}catch [Exception]{
   log "Failed enrollment code:$($enroll.StatusCode)"
    
}

#get install ps1 - will only be taking booststrap api key out of it - don't want to bother with singing this at install time - too much work and don't want to have a signing cert on all servers
try{
    $r = Invoke-WebRequest -Uri "https://patchmon.sys.utahtech.edu/api/v1/hosts/install?os=windows" -Headers @{"X-API-ID"="$apiId"; "X-API-KEY"="$apiKey"} -UseBasicParsing; $r.Content | Set-Content "$env:TEMP\patchmon-install.ps1" -Encoding UTF8;

    $BootstrapToken = (((get-content $env:TEMP\patchmon-install.ps1|select-string 'env:PATCHMON_BOOTSTRAP_TOKEN =') -split " = ")[1] -replace '"', '')
}catch [Exception]{
   log "Failed to download installer code:$($enroll.StatusCode)"
    
}

  
  Write-Host "Fetching credentials from PatchMon server..." -ForegroundColor Cyan
    try {
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        $body = @{ token = $BootstrapToken } | ConvertTo-Json
        $response = Invoke-RestMethod -Uri "$ServerURL/api/v1/hosts/bootstrap/exchange" -Method Post -Body $body -ContentType "application/json" -UseBasicParsing
        #permenant api id and key
        $APIID = $apiId
        $APIKey = $apiKey
           if (-not $APIID -or -not $APIKey) {
            log  "Failed to fetch credentials. Bootstrap token may have expired. Please request a new installation script."
                Write-Error "Failed to fetch credentials. Bootstrap token may have expired. Please request a new installation script."
                exit 1
            }
        Write-Host "Credentials received successfully." -ForegroundColor Green
    }catch {
        log "Failed to fetch credentials: $($_.Exception.Message). Bootstrap token may have expired. Please request a new installation script."
        Write-Error "Failed to fetch credentials: $($_.Exception.Message). Bootstrap token may have expired. Please request a new installation script."
        exit 1
    }


        # Download the binary from the server
        $downloadURL = "$ServerURL/api/v1/hosts/agent/download?arch=amd64&os=windows"
Write-Host "Downloading PatchMon agent..." -ForegroundColor Yellow
try {
    $headers = @{}
    if ($APIID -and $APIKey) {
        $headers["X-API-ID"] = $APIID
        $headers["X-API-KEY"] = $APIKey
    }
    Invoke-WebRequest -Uri $downloadURL -

Posting anonymously