From 8277d709154c3dc24cf8ca00bbfac16a1eb65547 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Thu, 13 Aug 2026 17:17:51 +0200 Subject: [PATCH] fix: close live catalog/type drift across web, backend and agent (audit P1) - adminConfig.ts: drop retired V1 latestAgent*/customSettings ghost fields, add the 8 backend fields the interface was missing - ops alerts: register the Sla category (BreachNotification/ConsecutiveFailures/ EvaluationCompleted) + OrphanEventsCleaned so operators can route them - analyze rules: expose app_install_duration in the builder SOURCES; fix stale XML docs (event_data_array, count_per_group_gte/in/not_in) - UploadIntervalSeconds: collapse 5 diverged defaults (10 vs 30) onto Constants.DefaultUploadIntervalSeconds = 10 (the served value) - combine-rules workflow: commit ALL three combine.js outputs and include rules/guardrails.json in the path filters - api.ts apps sessions builders: move trailing optional strings into an options object (adjacent optional strings swallowed a forgotten tenantId) - SummaryDialog: canonical phase display names ("Apps (Device)") - mcp-server.bicep: fix pre-EU-cutover hostname in deploy docs Co-Authored-By: Claude Fable 5 --- .github/workflows/combine-rules.yml | 10 ++++--- infra/mcp-server.bicep | 2 +- .../Configuration/AgentConfiguration.cs | 2 +- .../Configuration/RemoteConfigService.cs | 2 +- .../Runtime/AgentRuntimeHost.cs | 2 +- .../MainWindow.xaml.cs | 19 +++++++++++++- .../Config/GetAgentConfigFunction.cs | 2 +- .../AutopilotMonitor.Shared/Constants.cs | 7 +++-- .../Models/Config/AgentConfigResponse.cs | 4 ++- .../Models/Rules/AnalyzeRule.cs | 8 ++++-- .../admin/components/OpsAlertRulesSection.tsx | 13 ++++++++++ .../app/analyze-rules/types.ts | 2 +- .../app/settings/types.ts | 1 - src/Web/autopilot-monitor-web/lib/api.ts | 17 ++++++------ .../types/adminConfig.ts | 26 ++++++++++++++----- .../utils/sessionExportUtils.ts | 5 ++-- 16 files changed, 89 insertions(+), 33 deletions(-) diff --git a/.github/workflows/combine-rules.yml b/.github/workflows/combine-rules.yml index 4888892b..d1733071 100644 --- a/.github/workflows/combine-rules.yml +++ b/.github/workflows/combine-rules.yml @@ -8,6 +8,7 @@ on: - 'rules/ime-log-patterns/**' - 'rules/schema/**' - 'rules/scripts/**' + - 'rules/guardrails.json' pull_request: paths: - 'rules/gather/**' @@ -15,6 +16,7 @@ on: - 'rules/ime-log-patterns/**' - 'rules/schema/**' - 'rules/scripts/**' + - 'rules/guardrails.json' jobs: validate-and-combine: @@ -55,9 +57,11 @@ jobs: - name: Combine rules into dist/ run: node rules/scripts/combine.js - - name: Commit dist/ changes + - name: Commit generated outputs if: github.ref == 'refs/heads/main' && github.event_name == 'push' uses: stefanzweifel/git-auto-commit-action@v7 with: - commit_message: "chore: regenerate rules/dist from individual files" - file_pattern: "rules/dist/*" + commit_message: "chore: regenerate rules/dist + generated TS from individual files" + # combine.js writes THREE outputs; committing only dist/ would silently discard + # the two generated TS modules and leave the guardrails-in-sync CI check red. + file_pattern: "rules/dist/* src/Web/autopilot-monitor-web/utils/guardrails.generated.ts src/McpServer/autopilot-monitor-mcp/src/rule-authoring.generated.ts" diff --git a/infra/mcp-server.bicep b/infra/mcp-server.bicep index 0be9838d..80fbe698 100644 --- a/infra/mcp-server.bicep +++ b/infra/mcp-server.bicep @@ -25,7 +25,7 @@ // az deployment group create \ // --resource-group \ // --template-file infra/mcp-server.bicep \ -// --parameters apiUrl=https://autopilotmonitor-api.azurewebsites.net \ +// --parameters apiUrl=https://autopilotmonitor-api-eu.azurewebsites.net \ // entraClientSecret= // This emits `containerAppUrl` as an output (e.g. https://autopilotmonitor-mcp...azurecontainerapps.io). // diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/AgentConfiguration.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/AgentConfiguration.cs index 55de8610..7218da48 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/AgentConfiguration.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/AgentConfiguration.cs @@ -39,7 +39,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration /// /// Upload interval in seconds /// - public int UploadIntervalSeconds { get; set; } = 30; + public int UploadIntervalSeconds { get; set; } = Shared.Constants.DefaultUploadIntervalSeconds; /// /// Maximum batch size for events diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/RemoteConfigService.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/RemoteConfigService.cs index c21b8d08..00fc1334 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/RemoteConfigService.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/RemoteConfigService.cs @@ -322,7 +322,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration return new AgentConfigResponse { ConfigVersion = 0, - UploadIntervalSeconds = 30, + UploadIntervalSeconds = Shared.Constants.DefaultUploadIntervalSeconds, SelfDestructOnComplete = true, KeepLogFile = false, EnableGeoLocation = true, diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentRuntimeHost.cs b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentRuntimeHost.cs index 582a7278..f8a79100 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentRuntimeHost.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentRuntimeHost.cs @@ -40,7 +40,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime // or hour-long gaps that hide live UI from operators). private const int MinUploadIntervalSeconds = 5; private const int MaxUploadIntervalSeconds = 300; - private const int DefaultUploadIntervalSeconds = 30; + private const int DefaultUploadIntervalSeconds = Shared.Constants.DefaultUploadIntervalSeconds; private const int MinUploadBatchSize = 1; private const int MaxUploadBatchSize = 500; private const int DefaultUploadBatchSize = 100; diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/MainWindow.xaml.cs b/src/Agent/AutopilotMonitor.SummaryDialog/MainWindow.xaml.cs index 70d07674..a305a752 100644 --- a/src/Agent/AutopilotMonitor.SummaryDialog/MainWindow.xaml.cs +++ b/src/Agent/AutopilotMonitor.SummaryDialog/MainWindow.xaml.cs @@ -869,9 +869,26 @@ namespace AutopilotMonitor.SummaryDialog private static string FormatPhaseName(string phase) { - // Convert "DeviceSetup" -> "Device Setup", "AccountSetup" -> "Account Setup" if (string.IsNullOrEmpty(phase)) return phase; + // Canonical display names, mirroring EnrollmentEvent.GetPhaseName in Shared. + // This dialog is deliberately dependency-free (standalone EXE), so the mapping is + // restated here instead of referencing Shared — the camel-case splitter below is + // only the fallback for unknown tokens and cannot produce "Apps (Device)". + switch (phase) + { + case "Start": return "Start"; + case "DevicePreparation": return "Device Preparation"; + case "DeviceSetup": return "Device Setup"; + case "AppsDevice": return "Apps (Device)"; + case "AccountSetup": return "Account Setup"; + case "AppsUser": return "Apps (User)"; + case "FinalizingSetup": return "Finalizing Setup"; + case "Complete": return "Complete"; + case "Failed": return "Failed"; + } + + // Fallback: "SomeNewPhase" -> "Some New Phase" var result = new System.Text.StringBuilder(); for (int i = 0; i < phase.Length; i++) { diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs index 3b13d850..55c952b1 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs @@ -275,7 +275,7 @@ namespace AutopilotMonitor.Functions.Functions.Config await response.WriteAsJsonAsync(new AgentConfigResponse { ConfigVersion = 37, // EnableEspContinueAnywayObservation (Continue-Anyway observation mode) - UploadIntervalSeconds = 10, + UploadIntervalSeconds = Shared.Constants.DefaultUploadIntervalSeconds, SelfDestructOnComplete = tenantConfig.SelfDestructOnComplete ?? true, KeepLogFile = tenantConfig.KeepLogFile ?? false, EnableGeoLocation = tenantConfig.EnableGeoLocation ?? true, diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index 22bcafd4..a9c922bd 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -238,9 +238,12 @@ namespace AutopilotMonitor.Shared public const int MaxBatchSize = 100; /// - /// Default upload interval in seconds (fallback timer; normal path uses FileSystemWatcher) + /// Default upload interval in seconds (fallback timer; normal path uses FileSystemWatcher). + /// Single source for this default: GetAgentConfigFunction serves it, AgentConfigResponse + /// and AgentConfiguration initialize from it, RemoteConfigService's offline fallback and + /// AgentRuntimeHost's clamp fallback reuse it — keep them referencing this constant. /// - public const int DefaultUploadIntervalSeconds = 30; + public const int DefaultUploadIntervalSeconds = 10; // ----------------------------------------------------------------------- // API endpoint paths (relative to ApiBaseUrl) diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs index 003e1fa7..e33e1527 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs @@ -16,8 +16,10 @@ namespace AutopilotMonitor.Shared.Models /// /// Event upload debounce interval in seconds. + /// Sourced from the shared constant so the served value, the offline fallback and + /// this deserialization default can never diverge. /// - public int UploadIntervalSeconds { get; set; } = 30; + public int UploadIntervalSeconds { get; set; } = Constants.DefaultUploadIntervalSeconds; /// /// Whether to self-destruct after enrollment completion (remove Scheduled Task and all files). diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Rules/AnalyzeRule.cs b/src/Shared/AutopilotMonitor.Shared/Models/Rules/AnalyzeRule.cs index 4ed5e19b..1873de00 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Rules/AnalyzeRule.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Rules/AnalyzeRule.cs @@ -211,7 +211,9 @@ namespace AutopilotMonitor.Shared.Models public string Signal { get; set; } = default!; /// - /// Source of the signal: "event_type", "event_data", "phase_duration", "event_count", "app_install_duration", "event_correlation" + /// Source of the signal: "event_type", "event_data", "event_data_array", "phase_duration", + /// "event_count", "app_install_duration", "event_correlation". + /// Must stay in sync with DryRunAnalyzeRuleFunction.KnownSources and the RuleEngine switch. /// public string Source { get; set; } = default!; @@ -240,8 +242,10 @@ namespace AutopilotMonitor.Shared.Models public string ItemField { get; set; } = default!; /// - /// Comparison operator: "equals", "not_equals", "contains", "not_contains", "regex", "not_regex", "gt", "lt", "gte", "lte", "exists", "not_exists", "count_gte" + /// Comparison operator: "equals", "not_equals", "contains", "not_contains", "regex", "not_regex", + /// "gt", "lt", "gte", "lte", "exists", "not_exists", "count_gte", "count_per_group_gte", "in", "not_in". /// For "event_correlation": operator for the Event B filter (applied to DataField). + /// Must stay in sync with DryRunAnalyzeRuleFunction.KnownOperators. /// public string Operator { get; set; } = default!; diff --git a/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx index 8f15a6bc..3fbb1557 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx @@ -61,6 +61,18 @@ const OPS_EVENT_TYPES: Record = { // PR2: GA restored a single row from a backup. Warning severity so operators // can wire a Telegram rule and see the audit in near-real-time. "BackupRowRestored", + // Orphan cleanup sweep (Warning): events whose parent session no longer exists were + // deleted during maintenance aggregation. Backend helper RecordOrphanEventsCleanedAsync. + "OrphanEventsCleaned", + ], + // SLA breach evaluation (SlaBreachEvaluationService). Dual-register per memory + // feedback_ops_event_types_dual_register. BreachNotification = Warning per tenant+type + // (cooldown-gated), ConsecutiveFailures = Error (threshold crossed), EvaluationCompleted = + // Info run summary. Backend helpers RecordSla{BreachNotification,ConsecutiveFailures,EvaluationCompleted}Async. + Sla: [ + "SlaBreachNotification", + "SlaConsecutiveFailures", + "SlaEvaluationCompleted", ], Security: [ "DeviceBlocked", @@ -145,6 +157,7 @@ const SEVERITIES = ["Info", "Warning", "Error", "Critical"]; const CATEGORY_COLORS: Record = { Consent: "text-indigo-700 dark:text-indigo-300", Maintenance: "text-purple-700 dark:text-purple-300", + Sla: "text-rose-700 dark:text-rose-300", Security: "text-orange-700 dark:text-orange-300", Tenant: "text-teal-700 dark:text-teal-300", Agent: "text-cyan-700 dark:text-cyan-300", diff --git a/src/Web/autopilot-monitor-web/app/analyze-rules/types.ts b/src/Web/autopilot-monitor-web/app/analyze-rules/types.ts index eb754c0e..6121865f 100644 --- a/src/Web/autopilot-monitor-web/app/analyze-rules/types.ts +++ b/src/Web/autopilot-monitor-web/app/analyze-rules/types.ts @@ -116,7 +116,7 @@ export const CATEGORIES = ["network", "identity", "apps", "device", "esp", "enro export const SEVERITIES = ["info", "warning", "high", "critical"] as const; export const TRIGGERS = ["single", "correlation"] as const; export const OPERATORS = ["equals", "not_equals", "contains", "not_contains", "regex", "not_regex", "gt", "lt", "gte", "lte", "exists", "not_exists", "count_gte", "count_per_group_gte", "in", "not_in"] as const; -export const SOURCES = ["event_type", "event_data", "event_data_array", "phase_duration", "event_count", "event_correlation"] as const; +export const SOURCES = ["event_type", "event_data", "event_data_array", "phase_duration", "app_install_duration", "event_count", "event_correlation"] as const; export const PRECONDITION_OPERATORS = ["equals", "not_equals", "contains", "not_contains", "regex", "not_regex", "gt", "lt", "gte", "lte", "exists", "not_exists", "in", "not_in"] as const; export const SEVERITY_COLORS: Record = { diff --git a/src/Web/autopilot-monitor-web/app/settings/types.ts b/src/Web/autopilot-monitor-web/app/settings/types.ts index dc93b041..678fcd4c 100644 --- a/src/Web/autopilot-monitor-web/app/settings/types.ts +++ b/src/Web/autopilot-monitor-web/app/settings/types.ts @@ -60,7 +60,6 @@ export interface TenantConfiguration { homedAppClientId?: string | null; dataRetentionDays: number; sessionTimeoutHours: number; - customSettings?: string; // Agent collector settings enablePerformanceCollector: boolean; performanceCollectorIntervalSeconds: number; diff --git a/src/Web/autopilot-monitor-web/lib/api.ts b/src/Web/autopilot-monitor-web/lib/api.ts index d273987a..0f6bf075 100644 --- a/src/Web/autopilot-monitor-web/lib/api.ts +++ b/src/Web/autopilot-monitor-web/lib/api.ts @@ -309,6 +309,9 @@ export const api = { `${API_BASE_URL}/api/apps/list${qs({ tenantId, days: String(days) })}`, analytics: (tenantId: string, appName: string, days: number) => `${API_BASE_URL}/api/apps/${encodeURIComponent(appName)}/analytics${qs({ tenantId, days: String(days) })}`, + // model/version live in an options object: as trailing positional strings they sat + // adjacent to other optional strings, so a forgotten argument silently shifted the + // next one into its place (compiles fine, queries the wrong thing). sessions: ( tenantId: string, appName: string, @@ -316,8 +319,7 @@ export const api = { status: "all" | "failed" | "succeeded" = "all", offset = 0, limit = 50, - model?: string, - version?: string + opts?: { model?: string; version?: string } ) => `${API_BASE_URL}/api/apps/${encodeURIComponent(appName)}/sessions${qs({ tenantId, @@ -325,8 +327,8 @@ export const api = { status, offset: String(offset), limit: String(limit), - model, - version, + model: opts?.model, + version: opts?.version, })}`, // Global Admin variants — tenantId is optional: @@ -343,8 +345,7 @@ export const api = { offset = 0, limit = 50, tenantId?: string, - model?: string, - version?: string + opts?: { model?: string; version?: string } ) => `${API_BASE_URL}/api/global/apps/${encodeURIComponent(appName)}/sessions${qs({ days: String(days), @@ -352,8 +353,8 @@ export const api = { offset: String(offset), limit: String(limit), tenantId, - model, - version, + model: opts?.model, + version: opts?.version, })}`, }, diff --git a/src/Web/autopilot-monitor-web/types/adminConfig.ts b/src/Web/autopilot-monitor-web/types/adminConfig.ts index 9627f4e4..8e7acbc1 100644 --- a/src/Web/autopilot-monitor-web/types/adminConfig.ts +++ b/src/Web/autopilot-monitor-web/types/adminConfig.ts @@ -38,7 +38,22 @@ export interface AdminConfiguration { opsEventRetentionDays?: number; slaNotificationCooldownHours?: number; diagnosticsGlobalLogPathsJson?: string; - customSettings?: string; + /** Max diagnostics download size in MB; blobs above are rejected (413). 0 = no limit. Default 500. */ + maxDiagnosticsDownloadSizeMB?: number; + /** Timeout in seconds for the whole diagnostics download+stream. 0 = no timeout. Default 120. */ + diagnosticsDownloadTimeoutSeconds?: number; + /** JSON tier name → rate limits/features, e.g. {"pro":{"apiRateLimit":300}}. */ + planTierDefinitionsJson?: string; + /** MCP server access: "Disabled" | "WhitelistOnly" (default) | "AllMembers". */ + mcpAccessPolicy?: string; + /** Global kill-switch for the in-app feedback prompt. Default true. */ + feedbackEnabled?: boolean; + /** Minimum tenant age in days before the feedback prompt shows. Default 14. */ + feedbackMinTenantAgeDays?: number; + /** Cooldown in days before a user is re-prompted for feedback. 0 = single wave. Default 60. */ + feedbackCooldownDays?: number; + /** JSON list of ImeLogPattern IDs re-emitted as WhiteGloveSealingPatternDetected. Empty = off. */ + whiteGloveSealingPatternIdsJson?: string; nvdApiKey?: string; vulnerabilityCorrelationEnabled?: boolean; vulnerabilityDataLastSyncUtc?: string; @@ -52,12 +67,9 @@ 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) + // V2 agent hash oracle (written by CI/CD / build scripts, surfaced read-only; must + // round-trip via Save to survive Replace). The V1-era latestAgent*/latestBootstrapScriptVersion + // fields were retired with the legacy agent line and no longer exist on the C# model. latestAgentV2Version?: string; latestAgentV2Sha256?: string; latestAgentV2ExeSha256?: string; diff --git a/src/Web/autopilot-monitor-web/utils/sessionExportUtils.ts b/src/Web/autopilot-monitor-web/utils/sessionExportUtils.ts index 9510c393..1a1a8c7a 100644 --- a/src/Web/autopilot-monitor-web/utils/sessionExportUtils.ts +++ b/src/Web/autopilot-monitor-web/utils/sessionExportUtils.ts @@ -35,8 +35,9 @@ export const EXPORT_V1_PHASE_ORDER = ["Start", "Device Preparation", "Device Set export const EXPORT_V2_PHASE_ORDER = ["Start", "Device Preparation", "App Installation", "Finalizing Setup", "Complete", "Failed"]; -const SEVERITY_INT: Record = { - Debug: 0, Info: 1, Warning: 2, Error: 3, Critical: 4 +// Exported for the shared-manifest parity test — must match the C# EventSeverity enum. +export const SEVERITY_INT: Record = { + Trace: -1, Debug: 0, Info: 1, Warning: 2, Error: 3, Critical: 4 }; export function generateCsvExport(events: SessionExportEvent[]) {