# Nightfall Windows Agent Deployment: MSI

We cover:

* PowerShell scripts ([local](#powershell-local-msi-already-copied-to-the-machine), [network share](#powershell-install-from-a-network-share), [download from URL](#powershell-download-msi-from-a-url))
* [Deployment through Group Policy (GPO)](#gpo-deployment-via-startup-script)
* [One-liner script for testing](#one-liner-for-testing)
* [Uninstall with or without the .msi present.](#uninstalling-the-nightfall-ai-agent)

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

```powershell
# 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.<br>

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

```powershell
# 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.

```powershell
# 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`&#x20;
     * 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:&#x20;

**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

<figure><img src="https://3764378997-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FZeqNSdo8J8cLJPU3Gs5M%2Fuploads%2FZYYdTA1M0SkuWHkMnyLC%2FScreenshot%202025-09-05%20at%208.03.06%E2%80%AFPM.png?alt=media&#x26;token=bb9e026e-ac0c-4a18-bc13-64da4e23ddb9" alt="" width="269"><figcaption></figcaption></figure>

* Check for expected services:

{% code fullWidth="false" %}

```powershell
Get-Service Nightfall*
```

{% endcode %}

* 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

```powershell
$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
}

```
