Agent: use remote-config exclusivly

Function: deliver new default config
This commit is contained in:
Oliver Kieselbach
2026-02-13 17:03:14 +01:00
parent 6fb501607b
commit 731894bc04
7 changed files with 116 additions and 115 deletions
+8 -39
View File
@@ -7,23 +7,16 @@
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)
3. Installs agent binaries (agent uses built-in backend URL or CLI override)
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"
(Uses built-in backend URL from the agent)
.NOTES
- Agent is temporary and auto-removes after enrollment
@@ -34,21 +27,16 @@
[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
[int]$MaxOsAgeMinutes = 60
)
# 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"
@@ -57,7 +45,6 @@ $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
@@ -94,18 +81,17 @@ function Get-FileMd5Base64 {
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)"
$osAgeMinutesRounded = [int][Math]::Round($osAge.TotalMinutes)
Write-Log "OS install date: $osInstallDate (age: ${osAgeMinutesRounded}m)"
if ($osAge.TotalHours -gt $MaxOsAgeHours) {
Write-Log "SKIP: OS was installed ${osAgeHoursRounded}h ago (threshold: ${MaxOsAgeHours}h). Device is not freshly provisioned."
if ($osAge.TotalMinutes -gt $MaxOsAgeMinutes) {
Write-Log "SKIP: OS was installed ${osAgeMinutesRounded}m ago (threshold: ${MaxOsAgeMinutes}m). Device is not freshly provisioned."
exit 0
}
@@ -149,22 +135,6 @@ try {
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"
@@ -291,7 +261,6 @@ try {
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 ""
@@ -299,7 +268,7 @@ try {
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 " - Upload events to built-in backend URL (or CLI override /--backend-api)"
Write-Log " - Self-destruct when enrollment completes"
Write-Log " - Clean removal: delete C:\ProgramData\AutopilotMonitor folder"
@@ -58,11 +58,10 @@ namespace AutopilotMonitor.Agent.Core.Configuration
_logger.Info("Fetching remote configuration from backend...");
var config = await _apiClient.GetAgentConfigAsync(_tenantId);
if (config != null && config.Success)
if (config != null)
{
_logger.Info($"Remote config fetched: ConfigVersion={config.ConfigVersion}, " +
$"GatherRules={config.GatherRules?.Count ?? 0}, " +
$"RefreshInterval={config.RefreshIntervalSeconds}s");
$"GatherRules={config.GatherRules?.Count ?? 0}");
LogCollectorSettings(config.Collectors);
SetConfig(config);
@@ -70,7 +69,7 @@ namespace AutopilotMonitor.Agent.Core.Configuration
return config;
}
_logger.Warning($"Remote config fetch returned unsuccessful: {config?.Message}");
_logger.Warning("Remote config fetch returned empty response");
}
catch (Exception ex)
{
@@ -148,11 +147,15 @@ namespace AutopilotMonitor.Agent.Core.Configuration
{
return new AgentConfigResponse
{
Success = true,
Message = "Default configuration (offline fallback)",
Collectors = CollectorConfiguration.CreateDefault(),
ConfigVersion = 0,
RefreshIntervalSeconds = 300
UploadIntervalSeconds = 30,
CleanupOnExit = false,
SelfDestructOnComplete = false,
KeepLogFile = true,
ImeMatchLogPath = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log",
Collectors = CollectorConfiguration.CreateDefault(),
GatherRules = new System.Collections.Generic.List<GatherRule>(),
ImeLogPatterns = new System.Collections.Generic.List<ImeLogPattern>()
};
}
@@ -215,6 +215,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
{
_remoteConfigService = new RemoteConfigService(_apiClient, _configuration.TenantId, _logger);
_remoteConfigService.FetchConfigAsync().Wait(TimeSpan.FromSeconds(15));
ApplyRuntimeSettingsFromRemoteConfig();
}
catch (Exception ex)
{
@@ -222,6 +223,26 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
}
private void ApplyRuntimeSettingsFromRemoteConfig()
{
var config = _remoteConfigService?.CurrentConfig;
if (config == null) return;
_configuration.UploadIntervalSeconds = config.UploadIntervalSeconds;
_configuration.CleanupOnExit = config.CleanupOnExit;
_configuration.SelfDestructOnComplete = config.SelfDestructOnComplete;
_configuration.KeepLogFile = config.KeepLogFile;
if (!string.IsNullOrWhiteSpace(config.ImeMatchLogPath))
{
_configuration.ImeMatchLogPath = config.ImeMatchLogPath;
}
_logger.Info("Applied runtime settings from remote config");
_logger.Info($" uploadIntervalSeconds={_configuration.UploadIntervalSeconds}, cleanupOnExit={_configuration.CleanupOnExit}, selfDestructOnComplete={_configuration.SelfDestructOnComplete}, keepLogFile={_configuration.KeepLogFile}");
_logger.Info($" imeMatchLogPath={_configuration.ImeMatchLogPath ?? "(none)"}");
}
/// <summary>
/// Starts optional collectors based on remote configuration
/// </summary>
+33 -46
View File
@@ -8,7 +8,6 @@ using AutopilotMonitor.Agent.Core.Configuration;
using AutopilotMonitor.Agent.Core.Logging;
using AutopilotMonitor.Agent.Core.Monitoring.Core;
using Microsoft.Win32;
using Newtonsoft.Json;
namespace AutopilotMonitor.Agent
{
@@ -289,6 +288,7 @@ namespace AutopilotMonitor.Agent
string certThumbprint = null;
string tenantIdOverride = null;
string apiBaseUrlOverride = null;
string imeLogPathOverride = null;
string imeMatchLogPath = null;
string simulationLogDir = null;
@@ -304,6 +304,13 @@ namespace AutopilotMonitor.Agent
if (tenantIdIndex >= 0 && tenantIdIndex + 1 < args.Length)
tenantIdOverride = args[tenantIdIndex + 1];
// Local backend URL override for debugging/testing.
// Supported aliases:
// --ApiUrl https://...
// --api-url https://...
// --backend-api https://...
apiBaseUrlOverride = GetArgValue(args, "--ApiUrl", "--api-url", "--backend-api");
var imeLogPathIndex = Array.IndexOf(args, "--ime-log-path");
if (imeLogPathIndex >= 0 && imeLogPathIndex + 1 < args.Length)
imeLogPathOverride = args[imeLogPathIndex + 1];
@@ -325,56 +332,17 @@ namespace AutopilotMonitor.Agent
// Defaults
string apiBaseUrl = "https://autopilotmonitor-func.azurewebsites.net";
int uploadIntervalSeconds = 30;
bool cleanupOnExit = true;
bool selfDestructOnComplete = true;
bool cleanupOnExit = false;
bool selfDestructOnComplete = false;
bool rebootOnCompleteConfig = false;
bool enableGeoLocationConfig = true;
bool useClientCertAuthConfig = true;
// Load configuration JSON written by PowerShell bootstrap script
var configFilePath = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Config\agent-config.json");
if (File.Exists(configFilePath))
{
try
{
var configJson = File.ReadAllText(configFilePath);
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(configJson);
if (configDict != null)
{
if (configDict.ContainsKey("apiBaseUrl") && configDict["apiBaseUrl"] != null)
apiBaseUrl = configDict["apiBaseUrl"].ToString();
if (configDict.ContainsKey("uploadIntervalSeconds") && configDict["uploadIntervalSeconds"] != null)
uploadIntervalSeconds = Convert.ToInt32(configDict["uploadIntervalSeconds"]);
if (configDict.ContainsKey("cleanupOnExit") && configDict["cleanupOnExit"] != null)
cleanupOnExit = Convert.ToBoolean(configDict["cleanupOnExit"]);
if (configDict.ContainsKey("selfDestructOnComplete") && configDict["selfDestructOnComplete"] != null)
selfDestructOnComplete = Convert.ToBoolean(configDict["selfDestructOnComplete"]);
if (configDict.ContainsKey("rebootOnComplete") && configDict["rebootOnComplete"] != null)
rebootOnCompleteConfig = Convert.ToBoolean(configDict["rebootOnComplete"]);
if (configDict.ContainsKey("enableGeoLocation") && configDict["enableGeoLocation"] != null)
enableGeoLocationConfig = Convert.ToBoolean(configDict["enableGeoLocation"]);
if (configDict.ContainsKey("useClientCertAuth") && configDict["useClientCertAuth"] != null)
useClientCertAuthConfig = Convert.ToBoolean(configDict["useClientCertAuth"]);
if (configDict.ContainsKey("keepLogFile") && configDict["keepLogFile"] != null)
keepLogFile = keepLogFile || Convert.ToBoolean(configDict["keepLogFile"]);
if (configDict.ContainsKey("imeMatchLogPath") && configDict["imeMatchLogPath"] != null)
imeMatchLogPath = configDict["imeMatchLogPath"].ToString();
}
}
catch { /* use defaults */ }
}
// Environment variable overrides config file
// Environment variable overrides built-in default
apiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? apiBaseUrl;
// CLI override wins over environment variable/default
if (!string.IsNullOrWhiteSpace(apiBaseUrlOverride))
apiBaseUrl = apiBaseUrlOverride;
if (noCleanup)
{
@@ -429,6 +397,25 @@ namespace AutopilotMonitor.Agent
};
}
private static string GetArgValue(string[] args, params string[] names)
{
if (args == null || names == null || names.Length == 0)
return null;
for (int i = 0; i < args.Length - 1; i++)
{
foreach (var name in names)
{
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
}
}
return null;
}
/// <summary>
/// Checks if another AutopilotMonitor.Agent.exe process is already running.
/// </summary>
@@ -1 +1 @@
5
9
@@ -45,10 +45,9 @@ namespace AutopilotMonitor.Functions.Functions
if (string.IsNullOrEmpty(tenantId))
{
var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequest.WriteAsJsonAsync(new AgentConfigResponse
await badRequest.WriteAsJsonAsync(new
{
Success = false,
Message = "tenantId query parameter is required"
error = "tenantId query parameter is required"
});
return badRequest;
}
@@ -87,13 +86,15 @@ namespace AutopilotMonitor.Functions.Functions
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new AgentConfigResponse
{
Success = true,
Message = "Configuration retrieved successfully",
ConfigVersion = 2,
UploadIntervalSeconds = 30,
CleanupOnExit = false,
SelfDestructOnComplete = false,
KeepLogFile = true,
ImeMatchLogPath = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log",
Collectors = collectors,
GatherRules = gatherRules,
ImeLogPatterns = imeLogPatterns,
ConfigVersion = 1,
RefreshIntervalSeconds = 300
ImeLogPatterns = imeLogPatterns
});
return response;
@@ -102,10 +103,9 @@ namespace AutopilotMonitor.Functions.Functions
{
_logger.LogError(ex, "Error getting agent config");
var errorResp = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResp.WriteAsJsonAsync(new AgentConfigResponse
await errorResp.WriteAsJsonAsync(new
{
Success = false,
Message = "Internal server error"
error = "Internal server error"
});
return errorResp;
}
@@ -8,8 +8,37 @@ namespace AutopilotMonitor.Shared.Models
/// </summary>
public class AgentConfigResponse
{
public bool Success { get; set; }
public string Message { get; set; }
/// <summary>
/// Semantic config version from backend.
/// Used for debugging and future schema evolution.
/// </summary>
public int ConfigVersion { get; set; }
/// <summary>
/// Event upload debounce interval in seconds.
/// </summary>
public int UploadIntervalSeconds { get; set; } = 30;
/// <summary>
/// Whether to cleanup files on exit.
/// </summary>
public bool CleanupOnExit { get; set; } = false;
/// <summary>
/// Whether to self-destruct after enrollment completion.
/// </summary>
public bool SelfDestructOnComplete { get; set; } = false;
/// <summary>
/// Preserve logs during cleanup/self-destruct.
/// </summary>
public bool KeepLogFile { get; set; } = true;
/// <summary>
/// Optional path to IME pattern match log file.
/// </summary>
public string ImeMatchLogPath { get; set; } = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log";
public CollectorConfiguration Collectors { get; set; }
/// <summary>
@@ -22,14 +51,6 @@ namespace AutopilotMonitor.Shared.Models
/// Delivered from backend so patterns can be updated without agent rebuild.
/// </summary>
public List<ImeLogPattern> ImeLogPatterns { get; set; } = new List<ImeLogPattern>();
public int ConfigVersion { get; set; }
/// <summary>
/// How often the agent should re-fetch config (seconds)
/// Default: 300 (5 minutes)
/// </summary>
public int RefreshIntervalSeconds { get; set; } = 300;
}
/// <summary>