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 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-08-13 17:17:51 +02:00
parent e1b3ec25fd
commit 8277d70915
16 changed files with 89 additions and 33 deletions
+7 -3
View File
@@ -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"
+1 -1
View File
@@ -25,7 +25,7 @@
// az deployment group create \
// --resource-group <rg-name> \
// --template-file infra/mcp-server.bicep \
// --parameters apiUrl=https://autopilotmonitor-api.azurewebsites.net \
// --parameters apiUrl=https://autopilotmonitor-api-eu.azurewebsites.net \
// entraClientSecret=<secret-value>
// This emits `containerAppUrl` as an output (e.g. https://autopilotmonitor-mcp.<env-suffix>.<region>.azurecontainerapps.io).
//
@@ -39,7 +39,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration
/// <summary>
/// Upload interval in seconds
/// </summary>
public int UploadIntervalSeconds { get; set; } = 30;
public int UploadIntervalSeconds { get; set; } = Shared.Constants.DefaultUploadIntervalSeconds;
/// <summary>
/// Maximum batch size for events
@@ -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,
@@ -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;
@@ -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++)
{
@@ -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,
@@ -238,9 +238,12 @@ namespace AutopilotMonitor.Shared
public const int MaxBatchSize = 100;
/// <summary>
/// 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.
/// </summary>
public const int DefaultUploadIntervalSeconds = 30;
public const int DefaultUploadIntervalSeconds = 10;
// -----------------------------------------------------------------------
// API endpoint paths (relative to ApiBaseUrl)
@@ -16,8 +16,10 @@ namespace AutopilotMonitor.Shared.Models
/// <summary>
/// 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.
/// </summary>
public int UploadIntervalSeconds { get; set; } = 30;
public int UploadIntervalSeconds { get; set; } = Constants.DefaultUploadIntervalSeconds;
/// <summary>
/// Whether to self-destruct after enrollment completion (remove Scheduled Task and all files).
@@ -211,7 +211,9 @@ namespace AutopilotMonitor.Shared.Models
public string Signal { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
public string Source { get; set; } = default!;
@@ -240,8 +242,10 @@ namespace AutopilotMonitor.Shared.Models
public string ItemField { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
public string Operator { get; set; } = default!;
@@ -61,6 +61,18 @@ const OPS_EVENT_TYPES: Record<string, string[]> = {
// 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<string, string> = {
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",
@@ -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<string, { bg: string; text: string; border: string; dot: string }> = {
@@ -60,7 +60,6 @@ export interface TenantConfiguration {
homedAppClientId?: string | null;
dataRetentionDays: number;
sessionTimeoutHours: number;
customSettings?: string;
// Agent collector settings
enablePerformanceCollector: boolean;
performanceCollectorIntervalSeconds: number;
+9 -8
View File
@@ -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,
})}`,
},
@@ -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;
@@ -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<string, number> = {
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<string, number> = {
Trace: -1, Debug: 0, Info: 1, Warning: 2, Error: 3, Critical: 4
};
export function generateCsvExport(events: SessionExportEvent[]) {