fix warnings and fix v2 agent hash compare

This commit is contained in:
Oliver Kieselbach
2026-04-22 20:09:42 +02:00
parent 42ff4284d8
commit 23aa51a3f2
12 changed files with 134 additions and 9 deletions
+1
View File
@@ -132,3 +132,4 @@ tasks/
# Produced by /cleanup-table skill — never commit deletion payloads
db-backups/
plans/REFACTOR_AGENT_V2.md
.claude/scheduled_tasks.lock
@@ -118,7 +118,7 @@ namespace AutopilotMonitor.Agent.Core.Tests.Collectors
"CloudExperienceHost Web App Event 2. Name: 'CommercialOOBE_ESPProgress_WhiteGlove_Success'",
DateTime.UtcNow.AddSeconds(1), "prov", false);
Assert.Single(_captured.ToList().Where(e => e.EventType == "whiteglove_complete"));
Assert.Single(_captured, e => e.EventType == "whiteglove_complete");
Assert.Equal(1, _whiteGloveCompletedCount);
Assert.True(t.IsWhiteGloveDetectedForTest);
// WhiteGlove does NOT trigger FinalizingSetup
@@ -139,7 +139,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Security
var wrongHash = new string('f', 64);
var triggerCount = 0;
string capturedZipHash = null;
string? capturedZipHash = null;
bool capturedDowngrade = false;
var triggerDone = new ManualResetEventSlim(false);
Func<string, bool, Task> trigger = (zip, down) =>
@@ -146,10 +146,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Security
private sealed class FakeApiClient : BackendApiClient
{
private readonly Func<int, RegisterSessionResponse> _respond;
private readonly Action<SessionRegistration> _onRegister;
private readonly Action<SessionRegistration>? _onRegister;
public int CallCount;
public FakeApiClient(Func<int, RegisterSessionResponse> respond, Action<SessionRegistration> onRegister = null)
public FakeApiClient(Func<int, RegisterSessionResponse> respond, Action<SessionRegistration>? onRegister = null)
{
_respond = respond;
_onRegister = onRegister;
@@ -1 +1 @@
113
116
@@ -1 +1 @@
1079
1081
@@ -0,0 +1,33 @@
using AutopilotMonitor.Functions.Functions.Config;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Tests for <see cref="GetAgentConfigFunction.IsV2Client"/> — the pure decision logic that picks
/// the V1 vs V2 hash-oracle fields based on the X-Agent-Version header value.
/// </summary>
public class GetAgentConfigFunctionTests
{
[Theory]
// V2 agents (major >= 2) — must route to the V2 hash oracle.
[InlineData("2.0.114", true)]
[InlineData("2.0.0", true)]
[InlineData("2.1.3", true)]
[InlineData("2.0.114+06bbf13dbeedc", true)] // SemVer build-metadata suffix
[InlineData("3.0.0", true)]
// V1 agents (major < 2) — must stay on the legacy hash oracle.
[InlineData("1.0.1041", false)]
[InlineData("1.5.0", false)]
[InlineData("0.9.0", false)]
// Backward compat — missing/unparsable headers default to V1 so existing agents keep working.
[InlineData(null, false)]
[InlineData("", false)]
[InlineData(" ", false)]
[InlineData("not-a-version", false)]
[InlineData("vNEXT", false)]
public void IsV2Client_MajorVersionGate(string? agentVersion, bool expectedV2)
{
var actual = GetAgentConfigFunction.IsV2Client(agentVersion);
Assert.Equal(expectedV2, actual);
}
}
@@ -94,7 +94,7 @@ public class SlaMetricsServiceTests
Assert.Equal(100, result.CurrentWeek.SuccessRate);
Assert.True(result.CurrentWeek.SuccessRateMet);
Assert.Empty(result.Violators.Where(v => v.ViolationType == "Failed"));
Assert.DoesNotContain(result.Violators, v => v.ViolationType == "Failed");
}
[Fact]
@@ -563,6 +563,11 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
{ "LatestAgentExeSha256", config.LatestAgentExeSha256 ?? string.Empty },
{ "AllowAgentDowngrade", config.AllowAgentDowngrade },
{ "LatestBootstrapScriptVersion", config.LatestBootstrapScriptVersion ?? string.Empty },
// V2 agent binary integrity (separate release line — written by V2 build scripts)
{ "LatestAgentV2Version", config.LatestAgentV2Version ?? string.Empty },
{ "LatestAgentV2Sha256", config.LatestAgentV2Sha256 ?? string.Empty },
{ "LatestAgentV2ExeSha256", config.LatestAgentV2ExeSha256 ?? string.Empty },
{ "LatestBootstrapV2ScriptVersion", config.LatestBootstrapV2ScriptVersion ?? string.Empty },
// Rate limiting per-role settings
{ "UserRateLimitRequestsPerMinute", config.UserRateLimitRequestsPerMinute },
{ "GlobalAdminRateLimitRequestsPerMinute", config.GlobalAdminRateLimitRequestsPerMinute },
@@ -617,6 +622,11 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
LatestAgentExeSha256 = entity.GetString("LatestAgentExeSha256") ?? string.Empty,
AllowAgentDowngrade = entity.GetBoolean("AllowAgentDowngrade") ?? false,
LatestBootstrapScriptVersion = entity.GetString("LatestBootstrapScriptVersion") ?? string.Empty,
// V2 agent binary integrity (separate release line)
LatestAgentV2Version = entity.GetString("LatestAgentV2Version") ?? string.Empty,
LatestAgentV2Sha256 = entity.GetString("LatestAgentV2Sha256") ?? string.Empty,
LatestAgentV2ExeSha256 = entity.GetString("LatestAgentV2ExeSha256") ?? string.Empty,
LatestBootstrapV2ScriptVersion = entity.GetString("LatestBootstrapV2ScriptVersion") ?? string.Empty,
// Rate limiting per-role settings
UserRateLimitRequestsPerMinute = entity.GetInt32("UserRateLimitRequestsPerMinute") ?? 120,
GlobalAdminRateLimitRequestsPerMinute = entity.GetInt32("GlobalAdminRateLimitRequestsPerMinute") ?? 600,
@@ -102,6 +102,44 @@ namespace AutopilotMonitor.Functions.Functions.Config
}
}
/// <summary>
/// Determines whether an X-Agent-Version header advertises the V2 release line.
/// V2 agents use a separate hash-oracle (LatestAgentV2Sha256 / LatestAgentV2ExeSha256)
/// so they are not served V1 hashes and vice versa.
/// </summary>
/// <remarks>
/// Accepts SemVer-ish version strings like "2.0.114" or "2.0.114+abc123".
/// Missing or unparsable header → treat as V1 (backward compat for older agents
/// that do not send the header).
/// </remarks>
internal static bool IsV2Client(string? agentVersion)
{
if (string.IsNullOrWhiteSpace(agentVersion))
return false;
var dot = agentVersion.IndexOf('.');
var majorStr = dot > 0 ? agentVersion.Substring(0, dot) : agentVersion;
return int.TryParse(majorStr, out var major) && major >= 2;
}
/// <summary>
/// Returns the (ZipSha256, ExeSha256) pair appropriate for the calling agent.
/// Reads the X-Agent-Version header and dispatches to the V1 or V2 fields on
/// <see cref="AdminConfiguration"/>.
/// </summary>
internal static (string ZipSha256, string ExeSha256) SelectAgentHashesForClient(
HttpRequestData req,
AutopilotMonitor.Shared.Models.AdminConfiguration adminConfig)
{
var agentVersion = req.Headers.Contains("X-Agent-Version")
? req.Headers.GetValues("X-Agent-Version").FirstOrDefault()
: null;
return IsV2Client(agentVersion)
? (adminConfig.LatestAgentV2Sha256, adminConfig.LatestAgentV2ExeSha256)
: (adminConfig.LatestAgentSha256, adminConfig.LatestAgentExeSha256);
}
/// <summary>
/// Core config logic: fetch tenant + admin config, gather rules, IME patterns.
/// Called by both the cert-auth Run() method and the bootstrap wrapper.
@@ -138,6 +176,11 @@ namespace AutopilotMonitor.Functions.Functions.Config
var tenantDiagPaths = tenantConfig.GetDiagnosticsLogPaths();
var diagLogPaths = globalDiagPaths.Concat(tenantDiagPaths).ToList();
// Select V1 vs V2 hash oracle based on the X-Agent-Version header.
// V1 agents get LatestAgentSha256/ExeSha256; V2 agents get LatestAgentV2Sha256/V2ExeSha256.
// Response field names stay the same so the agent code is unchanged.
var (latestAgentSha256, latestAgentExeSha256) = SelectAgentHashesForClient(req, adminConfig);
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new AgentConfigResponse
{
@@ -168,8 +211,8 @@ namespace AutopilotMonitor.Functions.Functions.Config
EnableSoftwareInventoryAnalyzer = tenantConfig.EnableSoftwareInventoryAnalyzer ?? false,
EnableIntegrityBypassAnalyzer = tenantConfig.EnableIntegrityBypassAnalyzer ?? true
},
LatestAgentSha256 = adminConfig.LatestAgentSha256,
LatestAgentExeSha256 = adminConfig.LatestAgentExeSha256,
LatestAgentSha256 = latestAgentSha256,
LatestAgentExeSha256 = latestAgentExeSha256,
AllowAgentDowngrade = adminConfig.AllowAgentDowngrade,
NtpServer = string.IsNullOrEmpty(tenantConfig.NtpServer) ? "time.windows.com" : tenantConfig.NtpServer,
EnableTimezoneAutoSet = tenantConfig.EnableTimezoneAutoSet ?? false,
@@ -273,6 +273,34 @@ namespace AutopilotMonitor.Shared.Models
/// </summary>
public string LatestBootstrapScriptVersion { get; set; } = default!;
// ===== V2 AGENT HASH ORACLE =====
// Separate release line from V1 so V2 clients are not served V1 hashes (and vice versa).
// Written by scripts/Deployment/V2/build_and_upload_release_agent_build.ps1.
// GetAgentConfigFunction selects V1 vs V2 based on the X-Agent-Version header's major version.
/// <summary>
/// Version string of the latest published V2 agent (e.g. "2.0.114"). See <see cref="LatestAgentVersion"/> for V1.
/// </summary>
public string LatestAgentV2Version { get; set; } = default!;
/// <summary>
/// SHA-256 hash (lowercase hex) of the latest published V2 agent ZIP (AutopilotMonitor-Agent-V2.zip).
/// Mirror of <see cref="LatestAgentSha256"/> for the V2 release line.
/// </summary>
public string LatestAgentV2Sha256 { get; set; } = default!;
/// <summary>
/// SHA-256 hash (lowercase hex) of the latest published V2 agent EXE (AutopilotMonitor.Agent.V2.exe).
/// Mirror of <see cref="LatestAgentExeSha256"/> for the V2 release line.
/// </summary>
public string LatestAgentV2ExeSha256 { get; set; } = default!;
/// <summary>
/// Version string of the latest published V2 bootstrap script (Install-AutopilotMonitor-v2.ps1).
/// Mirror of <see cref="LatestBootstrapScriptVersion"/> for the V2 release line.
/// </summary>
public string LatestBootstrapV2ScriptVersion { get; set; } = default!;
// ===== MODERN DEPLOYMENT NOISE SUPPRESSION =====
/// <summary>
@@ -24,6 +24,16 @@ export interface AdminConfiguration {
opsAlertSlackWebhookUrl?: string;
allowAgentDowngrade?: boolean;
modernDeploymentHarmlessEventIdsJson?: string;
// Agent hash oracle (written by CI/CD / build scripts, surfaced read-only; must round-trip via Save to survive Replace)
latestAgentVersion?: string;
latestAgentSha256?: string;
latestAgentExeSha256?: string;
latestBootstrapScriptVersion?: string;
// V2 agent hash oracle (separate release line — written by scripts/Deployment/V2/*.ps1)
latestAgentV2Version?: string;
latestAgentV2Sha256?: string;
latestAgentV2ExeSha256?: string;
latestBootstrapV2ScriptVersion?: string;
/**
* Feature flag for the V2 Decision Engine index-table dual-write (Plan §M5.d).
* When true, IngestTelemetryFunction enqueues telemetry-index-reconcile envelopes after