mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
315 lines
12 KiB
PowerShell
315 lines
12 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Bootstrap script to deploy and start the Autopilot Monitor agent.
|
|
|
|
.DESCRIPTION
|
|
This script is designed to be deployed via Intune as a PowerShell Script during Autopilot.
|
|
It runs VERY EARLY in the enrollment process (first Intune action) and:
|
|
1. Creates a unique session ID for this enrollment
|
|
2. Downloads the monitoring agent binaries
|
|
3. Configures the agent (filesystem only - NO registry)
|
|
4. Registers agent as Scheduled Task (runs on computer startup)
|
|
5. Agent self-destructs when enrollment completes
|
|
|
|
.PARAMETER ApiBaseUrl
|
|
The base URL of the backend API (default: http://localhost:7071 for local testing)
|
|
Production example: https://autopilot-api.azurewebsites.net
|
|
|
|
.PARAMETER AgentDownloadUrl
|
|
URL to download the agent binaries from (ZIP file)
|
|
|
|
.EXAMPLE
|
|
.\Install-AutopilotMonitor.ps1
|
|
(Uses default local API for testing)
|
|
|
|
.EXAMPLE
|
|
.\Install-AutopilotMonitor.ps1 -ApiBaseUrl "https://autopilot-api.azurewebsites.net"
|
|
|
|
.NOTES
|
|
- Agent is temporary and auto-removes after enrollment
|
|
- Everything in C:\ProgramData\AutopilotMonitor (easy cleanup)
|
|
- No Registry entries (clean machine after removal)
|
|
- Scheduled Task survives reboots during enrollment
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
#[string]$ApiBaseUrl = "http://localhost:7071",
|
|
[string]$ApiBaseUrl = "https://autopilotmonitor-func.azurewebsites.net",
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$AgentDownloadUrl = "https://autopilotmonitor.blob.core.windows.net/agent/AutopilotMonitor-Agent.zip",
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[int]$MaxOsAgeHours = 5
|
|
)
|
|
|
|
# Configuration - Everything in ProgramData for easy cleanup
|
|
$AgentBasePath = "$env:ProgramData\AutopilotMonitor"
|
|
$AgentBinPath = "$AgentBasePath\Agent"
|
|
$AgentConfigPath = "$AgentBasePath\Config"
|
|
$AgentSpoolPath = "$AgentBasePath\Spool"
|
|
$AgentLogsPath = "$AgentBasePath\Logs"
|
|
$TaskName = "AutopilotMonitor-Agent"
|
|
$LogFile = "$AgentLogsPath\Bootstrap.log"
|
|
|
|
# Ensure directories exist
|
|
New-Item -Path $AgentBasePath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
|
New-Item -Path $AgentBinPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
|
New-Item -Path $AgentConfigPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
|
New-Item -Path $AgentSpoolPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
|
New-Item -Path $AgentLogsPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
|
|
|
function Write-Log {
|
|
param([string]$Message)
|
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
$logMessage = "[$timestamp] $Message"
|
|
Write-Output $logMessage
|
|
Add-Content -Path $LogFile -Value $logMessage -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Get-FileMd5Base64 {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path
|
|
)
|
|
|
|
$md5 = [System.Security.Cryptography.MD5]::Create()
|
|
try {
|
|
$stream = [System.IO.File]::OpenRead($Path)
|
|
try {
|
|
$hashBytes = $md5.ComputeHash($stream)
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
}
|
|
|
|
return [Convert]::ToBase64String($hashBytes)
|
|
}
|
|
finally {
|
|
$md5.Dispose()
|
|
}
|
|
}
|
|
|
|
try {
|
|
Write-Log "===== Autopilot Monitor Bootstrap Started ====="
|
|
Write-Log "API Base URL: $ApiBaseUrl"
|
|
|
|
# ── Pre-flight: Skip installation on already-enrolled devices ──
|
|
|
|
# Check 1: OS install date must be within threshold (fresh device)
|
|
$osInstallDate = (Get-CimInstance Win32_OperatingSystem).InstallDate
|
|
$osAge = (Get-Date) - $osInstallDate
|
|
$osAgeHoursRounded = "{0:0.0}" -f $osAge.TotalHours
|
|
Write-Log "OS install date: $osInstallDate (age: ${osAgeHoursRounded}h)"
|
|
|
|
if ($osAge.TotalHours -gt $MaxOsAgeHours) {
|
|
Write-Log "SKIP: OS was installed ${osAgeHoursRounded}h ago (threshold: ${MaxOsAgeHours}h). Device is not freshly provisioned."
|
|
exit 0
|
|
}
|
|
|
|
# Check 2: MDM enrollment must not be completed yet
|
|
$enrollmentPath = "HKLM:\SOFTWARE\Microsoft\Enrollments"
|
|
$mdmEnrolled = $false
|
|
|
|
if (Test-Path $enrollmentPath) {
|
|
$enrollmentEntries = Get-ChildItem -Path $enrollmentPath -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.PSChildName -match '^[0-9A-Fa-f\-]{36}$' }
|
|
|
|
foreach ($entry in $enrollmentEntries) {
|
|
$providerID = (Get-ItemProperty -Path $entry.PSPath -Name "ProviderID" -ErrorAction SilentlyContinue).ProviderID
|
|
if ($providerID) {
|
|
$mdmEnrolled = $true
|
|
Write-Log "Found MDM enrollment: ProviderID=$providerID (EnrollmentID=$($entry.PSChildName))"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($mdmEnrolled) {
|
|
# Double-check: Is enrollment status tracking showing completed?
|
|
$espPath = "HKLM:\SOFTWARE\Microsoft\Windows\Autopilot\EnrollmentStatusTracking\Device\Setup"
|
|
$hasCompleted = (Get-ItemProperty -Path $espPath -Name "HasProvisioningCompleted" -ErrorAction SilentlyContinue).HasProvisioningCompleted
|
|
|
|
if ($hasCompleted -eq 1) {
|
|
Write-Log "SKIP: Autopilot enrollment already completed (HasProvisioningCompleted=1). Device is already enrolled."
|
|
exit 0
|
|
}
|
|
}
|
|
|
|
# Check 3: Is the agent already installed? (leftover from previous run)
|
|
if (Test-Path $AgentBinPath) {
|
|
$existingAgent = Get-ChildItem -Path $AgentBinPath -Filter "AutopilotMonitor.Agent.exe" -ErrorAction SilentlyContinue
|
|
if ($existingAgent) {
|
|
Write-Log "SKIP: Agent already installed at $($existingAgent.FullName). Previous enrollment may have been in progress. Please check if enrollment completed or clean up C:\ProgramData\AutopilotMonitor before re-running."
|
|
exit 0
|
|
}
|
|
}
|
|
|
|
Write-Log "Pre-flight checks passed - device is freshly provisioned and enrollment in progress"
|
|
|
|
# Create agent configuration (JSON - NO REGISTRY!)
|
|
Write-Log "Creating agent configuration..."
|
|
$agentConfig = @{
|
|
apiBaseUrl = $ApiBaseUrl
|
|
uploadIntervalSeconds = 30
|
|
cleanupOnExit = $false
|
|
selfDestructOnComplete = $false
|
|
keepLogFile = $true
|
|
imeMatchLogPath = "C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log"
|
|
}
|
|
|
|
$agentConfigJson = $agentConfig | ConvertTo-Json -Depth 10
|
|
$agentConfigFile = Join-Path $AgentConfigPath "agent-config.json"
|
|
Set-Content -Path $agentConfigFile -Value $agentConfigJson -Force
|
|
Write-Log "Configuration saved to $agentConfigFile"
|
|
|
|
# Download and extract agent binaries
|
|
$agentExePath = Join-Path $AgentBinPath "AutopilotMonitor.Agent.exe"
|
|
|
|
if (Test-Path $agentExePath) {
|
|
Write-Log "Agent already installed at $agentExePath"
|
|
}
|
|
else {
|
|
Write-Log "Downloading agent from $AgentDownloadUrl..."
|
|
|
|
try {
|
|
# Download agent ZIP
|
|
$zipPath = Join-Path $env:TEMP "AutopilotMonitor-Agent.zip"
|
|
$maxDownloadAttempts = 3
|
|
$downloadAttempt = 0
|
|
$downloadResponse = $null
|
|
|
|
do {
|
|
$downloadAttempt++
|
|
try {
|
|
Write-Log "Download attempt ${downloadAttempt}/${maxDownloadAttempts}"
|
|
$downloadResponse = Invoke-WebRequest `
|
|
-Uri $AgentDownloadUrl `
|
|
-OutFile $zipPath `
|
|
-UseBasicParsing `
|
|
-TimeoutSec 30 `
|
|
-ErrorAction Stop `
|
|
-PassThru
|
|
Write-Log "Downloaded agent to $zipPath"
|
|
break
|
|
}
|
|
catch {
|
|
if ($downloadAttempt -ge $maxDownloadAttempts) {
|
|
throw
|
|
}
|
|
|
|
$retryDelaysInSeconds = @(2, 4, 8)
|
|
$retryDelaySeconds = $retryDelaysInSeconds[$downloadAttempt - 1]
|
|
Write-Log "Download failed (attempt $downloadAttempt): $($_.Exception.Message). Retrying in ${retryDelaySeconds}s..."
|
|
Start-Sleep -Seconds $retryDelaySeconds
|
|
}
|
|
} while ($downloadAttempt -lt $maxDownloadAttempts)
|
|
|
|
# Integrity check: compare HTTP Content-MD5 with local ZIP hash
|
|
$expectedMd5Header = $downloadResponse.Headers["Content-MD5"]
|
|
$expectedMd5 = if ($expectedMd5Header -is [System.Array]) { "$($expectedMd5Header[0])".Trim() } else { "$expectedMd5Header".Trim() }
|
|
if ($expectedMd5 -notmatch '\S') {
|
|
Write-Log "WARNING: Response has no Content-MD5 header - skipping MD5 integrity validation"
|
|
}
|
|
else {
|
|
$actualMd5 = Get-FileMd5Base64 -Path $zipPath
|
|
Write-Log "Validating Content-MD5 header against downloaded ZIP"
|
|
if ($actualMd5 -ne $expectedMd5) {
|
|
throw "MD5 integrity check failed. Expected (Content-MD5)='$expectedMd5', Actual='$actualMd5'"
|
|
}
|
|
Write-Log "MD5 integrity check passed"
|
|
}
|
|
|
|
# Extract to agent bin path
|
|
Expand-Archive -Path $zipPath -DestinationPath $AgentBinPath -Force
|
|
Write-Log "Extracted agent to $AgentBinPath"
|
|
|
|
# Cleanup
|
|
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
|
Write-Log "Cleaned up temporary files"
|
|
|
|
# Verify extraction
|
|
if (-not (Test-Path $agentExePath)) {
|
|
throw "Agent executable not found after extraction at $agentExePath"
|
|
}
|
|
|
|
Write-Log "Agent installation completed successfully"
|
|
}
|
|
catch {
|
|
Write-Log "ERROR downloading/extracting agent: $($_.Exception.Message)"
|
|
throw
|
|
}
|
|
}
|
|
|
|
# Register as Scheduled Task (survives reboots, easy to remove)
|
|
Write-Log "Registering Scheduled Task..."
|
|
|
|
# Check if task already exists
|
|
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
|
|
if ($null -ne $existingTask) {
|
|
Write-Log "Scheduled Task already exists - removing old task"
|
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
|
}
|
|
|
|
# Create action - run agent executable
|
|
$action = New-ScheduledTaskAction -Execute $agentExePath -WorkingDirectory $AgentBinPath
|
|
|
|
# Create trigger - run at computer startup
|
|
$trigger = New-ScheduledTaskTrigger -AtStartup
|
|
|
|
# Create settings - run whether user is logged on or not
|
|
$settings = New-ScheduledTaskSettingsSet `
|
|
-AllowStartIfOnBatteries `
|
|
-DontStopIfGoingOnBatteries `
|
|
-StartWhenAvailable `
|
|
-RunOnlyIfNetworkAvailable `
|
|
-ExecutionTimeLimit (New-TimeSpan -Hours 0) # No time limit
|
|
|
|
# Create principal - run as SYSTEM
|
|
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
|
|
|
# Register the task
|
|
Register-ScheduledTask `
|
|
-TaskName $TaskName `
|
|
-Action $action `
|
|
-Trigger $trigger `
|
|
-Settings $settings `
|
|
-Principal $principal `
|
|
-Description "Autopilot Monitor Agent - Temporary enrollment monitoring" `
|
|
-Force
|
|
|
|
Write-Log "Scheduled Task registered successfully"
|
|
|
|
# Start the task immediately (don't wait for next reboot)
|
|
Write-Log "Starting agent task..."
|
|
Start-ScheduledTask -TaskName $TaskName
|
|
Write-Log "Agent task started"
|
|
|
|
Write-Log "===== Bootstrap Completed Successfully ====="
|
|
Write-Log "Agent Path: $AgentBasePath"
|
|
Write-Log "Scheduled Task: $TaskName"
|
|
Write-Log "API Base URL: $ApiBaseUrl"
|
|
Write-Log "Agent will monitor enrollment and auto-remove when complete"
|
|
Write-Log "Bootstrap log: $LogFile"
|
|
Write-Log ""
|
|
Write-Log "IMPORTANT: Agent runs as Scheduled Task and will:"
|
|
Write-Log " - Generate unique session ID on startup"
|
|
Write-Log " - Survive reboots during enrollment"
|
|
Write-Log " - Monitor enrollment phases in real-time"
|
|
Write-Log " - Upload events to $ApiBaseUrl"
|
|
Write-Log " - Self-destruct when enrollment completes"
|
|
Write-Log " - Clean removal: delete C:\ProgramData\AutopilotMonitor folder"
|
|
|
|
exit 0
|
|
}
|
|
catch {
|
|
Write-Log "===== Bootstrap FAILED ====="
|
|
Write-Log "ERROR: $($_.Exception.Message)"
|
|
Write-Log "Stack trace: $($_.ScriptStackTrace)"
|
|
Write-Log "Please check log file: $LogFile"
|
|
exit 1
|
|
}
|