mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
fix warnings and fix v2 agent hash compare
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -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) =>
|
||||
|
||||
+2
-2
@@ -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]
|
||||
|
||||
+10
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user