diff --git a/scripts/Bootstrap/Install-AutopilotMonitor.ps1 b/scripts/Bootstrap/Install-AutopilotMonitor.ps1 index f369ac30..273a64b3 100644 --- a/scripts/Bootstrap/Install-AutopilotMonitor.ps1 +++ b/scripts/Bootstrap/Install-AutopilotMonitor.ps1 @@ -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" diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Configuration/RemoteConfigService.cs b/src/Agent/AutopilotMonitor.Agent.Core/Configuration/RemoteConfigService.cs index 1d495be3..6d9cd4c0 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Configuration/RemoteConfigService.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Configuration/RemoteConfigService.cs @@ -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(), + ImeLogPatterns = new System.Collections.Generic.List() }; } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs index 9d141ba5..6ba99e6c 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs @@ -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)"}"); + } + /// /// Starts optional collectors based on remote configuration /// diff --git a/src/Agent/AutopilotMonitor.Agent/Program.cs b/src/Agent/AutopilotMonitor.Agent/Program.cs index 33e86565..24ab0048 100644 --- a/src/Agent/AutopilotMonitor.Agent/Program.cs +++ b/src/Agent/AutopilotMonitor.Agent/Program.cs @@ -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>(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; + } + /// /// Checks if another AutopilotMonitor.Agent.exe process is already running. /// diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 7ed6ff82..ec635144 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -5 +9 diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs index ef13caee..c4643b8d 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/GetAgentConfigFunction.cs @@ -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; } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/AgentConfigResponse.cs b/src/Shared/AutopilotMonitor.Shared/Models/AgentConfigResponse.cs index 475400b1..0d6a6454 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/AgentConfigResponse.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/AgentConfigResponse.cs @@ -8,8 +8,37 @@ namespace AutopilotMonitor.Shared.Models /// public class AgentConfigResponse { - public bool Success { get; set; } - public string Message { get; set; } + /// + /// Semantic config version from backend. + /// Used for debugging and future schema evolution. + /// + public int ConfigVersion { get; set; } + + /// + /// Event upload debounce interval in seconds. + /// + public int UploadIntervalSeconds { get; set; } = 30; + + /// + /// Whether to cleanup files on exit. + /// + public bool CleanupOnExit { get; set; } = false; + + /// + /// Whether to self-destruct after enrollment completion. + /// + public bool SelfDestructOnComplete { get; set; } = false; + + /// + /// Preserve logs during cleanup/self-destruct. + /// + public bool KeepLogFile { get; set; } = true; + + /// + /// Optional path to IME pattern match log file. + /// + public string ImeMatchLogPath { get; set; } = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log"; + public CollectorConfiguration Collectors { get; set; } /// @@ -22,14 +51,6 @@ namespace AutopilotMonitor.Shared.Models /// Delivered from backend so patterns can be updated without agent rebuild. /// public List ImeLogPatterns { get; set; } = new List(); - - public int ConfigVersion { get; set; } - - /// - /// How often the agent should re-fetch config (seconds) - /// Default: 300 (5 minutes) - /// - public int RefreshIntervalSeconds { get; set; } = 300; } ///