Nightfall Windows Agent: MSI Deployment

This guide explains multiple ways to deploy the Nightfall Agent (NightfallAgent.msi) with the required API_KEY and COMPANY_ID parameters.

We cover:

Assumptions

  • You have the MSI installer (NightfallAgent.msi) provided by Nightfall.

  • Installation requires two properties:

    • API_KEY="YOUR-API-KEY"

    • COMPANY_ID="YOUR_SECRET_VALUE"

  • Installation is silent (/qn /norestart) and requires administrator rights.

  • Logging is enabled with /l*v for troubleshooting.

PowerShell: Local MSI (already copied to the machine)

Use this if you or your RMM tool place the .msi directly on the machine before running the script.

# Install-NightfallAgent-Local.ps1

$msiPath   = "C:\Temp\NightfallAgent.msi"
$apiKey    = "REPLACE_WITH_API_KEY"
$companyId = "REPLACE_WITH_COMPANY_ID"

$logDir = "C:\Windows\Temp\Nightfall"
$logFile = Join-Path $logDir "NightfallAgent_Install.log"

New-Item -ItemType Directory -Path $logDir -Force | Out-Null

if (Test-Path $msiPath) {
    Write-Output "MSI found at $msiPath. Starting install..."
    $args = "/i `"$msiPath`" API_KEY=`"$apiKey`" COMPANY_ID=`"$companyId`" /qn /norestart /l*v `"$logFile`""
    $proc = Start-Process "msiexec.exe" -ArgumentList $args -Wait -PassThru -NoNewWindow
    if ($proc.ExitCode -eq 0) {
        Write-Output "Nightfall agent installed successfully."
    } else {
        Write-Output "Installer returned exit code $($proc.ExitCode). Check log: $logFile"
        exit $proc.ExitCode
    }
} else {
    Write-Output "MSI not found at $msiPath. Skipping install."
    exit 2
}

PowerShell: Install from a Network Share

Use this if you keep the MSI on a file server. Make sure Domain Computers or the target machines have read access to the share.

⚠️ Use UNC paths (\\server\share\...) — mapped drives won’t work for GPO Startup scripts.

# Install-NightfallAgent-FromShare.ps1

$sourceMsi = "\\fileserver\software\Nightfall\NightfallAgent.msi"
$localMsi  = "C:\Temp\NightfallAgent.msi"
$apiKey    = "YOUR_API_KEY_HERE"
$companyId = "YOUR_SECRET_VALUE"

$logDir = "C:\Windows\Temp\Nightfall"
$logFile = Join-Path $logDir "NightfallAgent_Install.log"

New-Item -ItemType Directory -Path $logDir -Force | Out-Null
New-Item -ItemType Directory -Path (Split-Path $localMsi) -Force | Out-Null

Write-Output "Copying MSI from $sourceMsi to $localMsi..."
Copy-Item -Path $sourceMsi -Destination $localMsi -Force -ErrorAction Stop

if (Test-Path $localMsi) {
    Write-Output "Copy complete. Starting install..."
    $args = "/i `"$localMsi`" API_KEY=`"$apiKey`" COMPANY_ID=`"$companyId`" /qn /norestart /l*v `"$logFile`""
    $proc = Start-Process "msiexec.exe" -ArgumentList $args -Wait -PassThru -NoNewWindow
    if ($proc.ExitCode -eq 0) {
        Write-Output "Nightfall agent installed successfully."
    } else {
        Write-Output "Installer returned exit code $($proc.ExitCode). Check log: $logFile"
        exit $proc.ExitCode
    }
} else {
    Write-Output "MSI copy failed. Check share permissions and path."
    exit 3
}

PowerShell: Download MSI from a URL

Use this if you host the MSI on an internal HTTPS server or CDN.

# Install-NightfallAgent-FromUrl.ps1
# Purpose: Download the Nightfall MSI from a URL, validate it looks like a real MSI, then install silently.
# Notes:
#   - Run elevated (admin). Works as a GPO Startup script.

# --- EDIT THESE VALUES ---
$downloadUrl = "https://example.com/NightfallAgent.msi"  # <-- Replace with your direct MSI URL
$localMsi    = "C:\Temp\NightfallAgent.msi"
$apiKey      = "<API_KEY>"        # <-- Replace
$companyId   = "<COMPANY_ID>"     # <-- Replace
# --------------------------

$ErrorActionPreference = "Stop"

# Paths for logging
$logDir  = "C:\Windows\Temp\Nightfall"
$logFile = Join-Path $logDir "NightfallAgent_Install.log"

# Ensure folders exist
New-Item -ItemType Directory -Path (Split-Path $localMsi) -Force | Out-Null
New-Item -ItemType Directory -Path $logDir -Force | Out-Null

# Helper: quick MSI signature + size sanity check
function Test-IsMsi {
    param([string]$Path)
    if (-not (Test-Path $Path)) { return $false }
    $len = (Get-Item $Path).Length
    if ($len -lt 1MB) { return $false } # tiny files are likely HTML/error pages

    # MSI is a CFBF (OLE) container: header D0 CF 11 E0 A1 B1 1A E1
    $fs = [System.IO.File]::Open($Path, 'Open', 'Read', 'ReadWrite')
    try {
        $buf = New-Object byte[] 8
        [void]$fs.Read($buf, 0, 8)
        $hex = ($buf | ForEach-Object { $_.ToString("X2") }) -join " "
        return ($hex -eq "D0 CF 11 E0 A1 B1 1A E1")
    } finally {
        $fs.Close()
    }
}

Write-Output "Downloading MSI from $downloadUrl ..."
try {
    # Use HttpClient for robust redirects + streaming
    Add-Type -AssemblyName System.Net.Http
    $handler = New-Object System.Net.Http.HttpClientHandler
    $handler.AllowAutoRedirect = $true
    $handler.AutomaticDecompression = [System.Net.DecompressionMethods]::GZip -bor `
                                      [System.Net.DecompressionMethods]::Deflate -bor `
                                      [System.Net.DecompressionMethods]::Brotli
    $client = New-Object System.Net.Http.HttpClient($handler)
    $client.Timeout = [TimeSpan]::FromMinutes(10)
    $client.DefaultRequestHeaders.UserAgent.ParseAdd("Nightfall-Agent-Installer/1.0")

    $response = $client.GetAsync($downloadUrl, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
    if (-not $response.IsSuccessStatusCode) {
        throw "HTTP $([int]$response.StatusCode) $($response.ReasonPhrase)"
    }

    $stream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
    $tmp = "$localMsi.download"
    $fs = [System.IO.File]::Open($tmp, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
    try {
        $buffer = New-Object byte[] (1024*256) # 256 KB chunks
        while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
            $fs.Write($buffer, 0, $read)
        }
    } finally {
        $fs.Dispose()
        $stream.Dispose()
        $client.Dispose()
        $handler.Dispose()
    }

    if (Test-Path $localMsi) { Remove-Item $localMsi -Force }
    Move-Item $tmp $localMsi -Force

} catch {
    Write-Error "Download failed: $($_.Exception.Message)"
    exit 100
}

# Validate the download looks like a real MSI
if (-not (Test-IsMsi -Path $localMsi)) {
    $size = (Get-Item $localMsi).Length
    Write-Error "Downloaded file does not look like a valid MSI (size=$size bytes). The URL may be a landing page or error."
    exit 101
}

# Remove MOTW just in case
try { Unblock-File -Path $localMsi -ErrorAction SilentlyContinue } catch {}

# Install silently with logging
Write-Output "MSI validated. Installing Nightfall Agent..."
$args = "/i `"$localMsi`" API_KEY=`"$apiKey`" COMPANY_ID=`"$companyId`" /qn /norestart /l*v `"$logFile`""
$proc = Start-Process "msiexec.exe" -ArgumentList $args -Wait -PassThru -NoNewWindow

switch ($proc.ExitCode) {
    0     { Write-Output "Nightfall Agent installed successfully."; exit 0 }
    1603  { Write-Error "Fatal error during installation (1603). See log: $logFile"; exit 1603 }
    1618  { Write-Error "Another installation is already in progress (1618)."; exit 1618 }
    1620  { Write-Error "Package could not be opened (1620). File may be invalid. See log: $logFile"; exit 1620 }
    default { Write-Error "Installer returned exit code $($proc.ExitCode). See log: $logFile"; exit $proc.ExitCode }
}

GPO Deployment via Startup Script

Recommended for domain-joined Windows machines. Use a Startup Script because the built-in “Software Installation” GPO cannot pass custom properties like API_KEY.

Steps:

  1. Place the script (e.g., Install-NightfallAgent-FromShare.ps1) in

    \\<domain>\SYSVOL\<domain>\scripts\Nightfall\

  2. Ensure Domain Computers have read access.

  3. In Group Policy Management:

    • Go to Computer Configuration → Policies → Windows Settings → Scripts (Startup/Shutdown).

    • Add a Startup Script.

      • Script name: powershell.exe

      • Script parameters: -ExecutionPolicy Bypass -File "\\SYSVOL<domain>\scripts\Nightfall\Install-NightfallAgent-FromShare.ps1"

  4. Apply the GPO to the desired OU.

  5. Run gpupdate /force or reboot a target machine.

GPO Software Installation with MST (Advanced)

If you have an MST transform that embeds API_KEY and COMPANY_ID, you can deploy the MSI via:

Computer Configuration → Policies → Software Settings → Software installation.

  • Add the MSI via UNC path.

  • Open its Properties → Modifications → Add your .mst.

Without an MST, use GPO via Startup Script instead. One-liner for Testing

One-liner for Testing

Run manually on a single machine (PowerShell elevated):

$msiPath="C:\Temp\NightfallAgent.msi"; Start-Process msiexec.exe -ArgumentList "/i `"$msiPath`" API_KEY=`"YOUR_API_KEY_HERE`" COMPANY_ID=`"YOUR_SECRET_VALUE`" /qn /norestart /l*v `"`"C:\Windows\Temp\Nightfall\NightfallAgent_Install.log`"`"" -Wait

Verification After Install

  • Check for expected services:

Get-Service Nightfall*
  • Confirm presence of the Nightfall AI icon in the system tray (this may take a few seconds).

    • Double click the icon

    • You should see a connected status as seen in the image above.

Uninstalling The Nightfall AI Agent

$ProductName = "NightfallAI Agent"

# Function to retrieve installed products matching product name
function Get-MatchingProducts($name) {
    Write-Host "Searching for products matching: '$name'..."
    Get-WmiObject -Class Win32_Product -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -like "*$name*" }
}

# Function to uninstall a product by ProductCode
function Uninstall-Product($product) {
    $name = $product.Name
    $productCode = $product.IdentifyingNumber

    if ($productCode) {
        Write-Host "Uninstalling '$name' (ProductCode: $productCode)..." -ForegroundColor Green
        Start-Process "msiexec.exe" -ArgumentList "/x $productCode /qn" -Wait -NoNewWindow
        Write-Host "Uninstalled: $name" -ForegroundColor Green
    } else {
        Write-Warning "Skipping ${name}: missing ProductCode."
    }
}

# Try finding the initial product
$products = Get-MatchingProducts -name $ProductName

# If not found, try old NightfallAI Agent name 'Agent'
if (-not $products -or $products.Count -eq 0) {
    Write-Warning "No installed products found matching: '$ProductName'"
    Write-Host "Trying to search for old NightfallAgent name : 'Agent'" -ForegroundColor Yellow
    $products = Get-MatchingProducts -name "Agent"
}

# Final check before uninstall
if (-not $products -or $products.Count -eq 0) {
    Write-Host "No matching products found for either '${ProductName}' or 'Agent'."
    exit 1
}

foreach ($product in $products) {
    Uninstall-Product -product $product
}

Last updated

Was this helpful?