Files
AutopilotMonitor/rules/schema/analyze-rule.schema.json
Oliver Kieselbach 516cdc71a7 Rules: optional presentation block, {{signal.key}} tokens, one resolution for web and MCP (D-356)
Analyze rules can describe how a finding is shown: a role (outcome, cause, context,
advisory), a one-line summary, the subject it is about and up to six key facts. Like the
explanation, the block is stored raw on the result and resolved where it is rendered.

- Templates: a qualified token {{signal.key}} reads one key of one condition's evidence,
  so counts, durations and correlation fields are reachable. RuleEvidenceKeys (Shared,
  exported to the manifest) lists the keys each condition source records; tests pin it
  against the evaluators.
- Backend: PresentationJson on AnalyzeRules and RuleResults, RuleVersion on results,
  ValidatePresentation on create/update, and results stored without a presentation get
  the current rule's at read time.
- Web + MCP: lib/rulePresentation.ts resolves the block (unrecorded facts left out, summary
  falls back to the explanation). The MCP now runs generated copies of the web modules
  instead of a hand-kept mirror (npm run generate:mcp-logic, freshness tests in both
  suites); the duplicate baseline shrinks by one.
- MCP output: get_session and get_session_summary return the resolved block (issues gain
  ruleId, role, summary, subject, facts); test_analyze_rule previews it; validate_rule
  checks qualified tokens against the key catalog and lints the subject.
- Shared case files: rule-template-interpolation gains the qualified-token cases,
  rule-presentation is new.

No rule carries a presentation yet; renderers behave as before until rules gain one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:48:46 +02:00

475 lines
21 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/OliverKieselbach/Autopilot-Monitor/main/rules/schema/analyze-rule.schema.json",
"title": "Autopilot Monitor Analyze Rule",
"description": "Defines how to analyze collected events to detect issues during Autopilot enrollment.",
"oneOf": [
{ "$ref": "#/$defs/analyzeRule" },
{
"type": "object",
"properties": {
"$schema": { "type": "string" },
"rules": {
"type": "array",
"items": { "$ref": "#/$defs/analyzeRule" }
}
},
"required": ["rules"],
"additionalProperties": false
}
],
"$defs": {
"analyzeRule": {
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema reference for editor IntelliSense."
},
"ruleId": {
"type": "string",
"description": "Unique rule identifier (e.g., \"ANALYZE-APP-001\").",
"pattern": "^ANALYZE-[A-Z]+-\\d{3}$"
},
"title": {
"type": "string",
"description": "Human-readable rule title (e.g., \"Proxy Authentication Required\")."
},
"description": {
"type": "string",
"description": "Detailed description of what this rule detects."
},
"severity": {
"type": "string",
"description": "Severity level of the detected issue.",
"enum": ["info", "warning", "high", "critical"]
},
"category": {
"type": "string",
"description": "Rule category.",
"enum": ["network", "identity", "enrollment", "apps", "esp", "device", "security"]
},
"version": {
"type": "string",
"description": "Semantic version of this rule (e.g., \"1.0.0\").",
"default": "1.0.0"
},
"author": {
"type": "string",
"description": "Author of this rule.",
"default": "Autopilot Monitor"
},
"enabled": {
"type": "boolean",
"description": "Whether this rule is enabled. Set to false to add a rule without activating it immediately.",
"default": true
},
"isCommunity": {
"type": "boolean",
"description": "Whether this is a community-contributed rule. Community rules are stored in the global partition alongside built-in rules.",
"default": false
},
"trigger": {
"type": "string",
"description": "Rule trigger type: \"single\" matches individual events, \"correlation\" combines multiple event types.",
"enum": ["single", "correlation"],
"default": "single"
},
"evaluateOn": {
"type": "array",
"description": "When this rule is evaluated. Absent = [\"enrollment_end\"] (terminal-only, the historical behavior). Interim triggers let the rule fire before the session is terminal: \"whiteglove_sealed\" (first genuine WhiteGlove seal) and \"on_event:<eventType>\" (an ingest batch contained that event type). High-frequency telemetry event types are HARD-BLOCKED as on_event triggers (guardrails blockedInterimTriggerEventTypes): the backend rejects them on save and the runtime ignores them. Interim runs suppress markSessionAsFailed and record no stats; findings notify once per (session, rule). Interim-enabled rules need monotonic conditions — a not_exists precondition on enrollment_complete/enrollment_failed/session_timeout passes trivially mid-run.",
"items": {
"type": "string",
"pattern": "^(enrollment_end|whiteglove_sealed|on_event:[a-z0-9_]{1,128})$"
},
"minItems": 1,
"uniqueItems": true
},
"conditions": {
"type": "array",
"description": "Conditions evaluated against the event stream. All required conditions must match for the rule to fire.",
"items": { "$ref": "#/$defs/ruleCondition" },
"minItems": 1
},
"preconditions": {
"type": "array",
"description": "Optional device-fact gates evaluated BEFORE conditions. ALL preconditions must pass; if any fails the rule is silently skipped — no result, no UI card. Useful for filtering out hardware/OS profiles where the rule does not apply (e.g. virtual machines).",
"items": { "$ref": "#/$defs/rulePrecondition" },
"default": []
},
"baseConfidence": {
"type": "integer",
"description": "Base confidence score (0-100) when required conditions match.",
"minimum": 0,
"maximum": 100,
"default": 50
},
"confidenceFactors": {
"type": "array",
"description": "Additional factors that increase confidence when matched.",
"items": { "$ref": "#/$defs/confidenceFactor" },
"default": []
},
"confidenceThreshold": {
"type": "integer",
"description": "Minimum confidence score (0-100) to create a RuleResult.",
"minimum": 0,
"maximum": 100,
"default": 40
},
"explanation": {
"type": "string",
"description": "Detailed explanation of the detected issue. Supports markdown formatting."
},
"remediation": {
"type": "array",
"description": "Steps to remediate the detected issue.",
"items": { "$ref": "#/$defs/remediationStep" },
"default": []
},
"relatedDocs": {
"type": "array",
"description": "Links to relevant documentation.",
"items": { "$ref": "#/$defs/relatedDoc" },
"default": []
},
"presentation": { "$ref": "#/$defs/rulePresentation" },
"tags": {
"type": "array",
"description": "Tags for filtering and categorization.",
"items": { "type": "string" },
"default": []
},
"markSessionAsFailedDefault": {
"type": "boolean",
"description": "When true, a firing of this rule causes the entire enrollment session to be marked as failed (KO criterion). Tenants can override this via the portal.",
"default": false
},
"templateVariables": {
"type": "array",
"description": "Template variables that must be customized per-tenant. If non-empty, the rule is a template: enabling it creates a tenant custom copy with the user's values substituted.",
"items": { "$ref": "#/$defs/templateVariable" },
"default": []
},
"derivedFromTemplateRuleId": {
"type": "string",
"description": "If this custom rule was created from a template, the original template rule's ID. Used to track lineage and prevent duplicate copies."
}
},
"required": ["ruleId", "title", "severity", "category", "conditions", "explanation"],
"additionalProperties": false
},
"ruleCondition": {
"type": "object",
"description": "A condition evaluated against the event stream.",
"properties": {
"signal": {
"type": "string",
"description": "Descriptive name for this signal (e.g., \"proxy_407_error\")."
},
"source": {
"type": "string",
"description": "Source of the signal. phase_duration and app_install_duration never derive a duration from a span with mixed timestamp provenance (exactly one IME-log endpoint with sourceOffsetOrigin=reader-zone-fallback, i.e. the writer timezone was unknown to the agent for one endpoint only): such a condition evaluates false with evidence timestampProvenance=reader-zone-fallback. Two fallback endpoints share the same error and stay measurable.",
"enum": ["event_type", "event_data", "event_data_array", "phase_duration", "event_count", "app_install_duration", "event_correlation", "clock_skew"]
},
"skewMetric": {
"type": "string",
"description": "For clock_skew only: which device-clock metric to evaluate. clock_jump = persistent step in the device's clock frame mid-session; sustained_offset = whole session ran on a clock off by at least the threshold. value is the threshold in seconds; operator is limited to gt/gte on the magnitude. IME-log-derived events are excluded from the measurement.",
"enum": ["clock_jump", "sustained_offset"]
},
"eventType": {
"type": "string",
"description": "Event type to match on. For event_correlation: the first event type (Event A)."
},
"dataField": {
"type": "string",
"description": "Data field to match on. Uses dot notation for nested fields (e.g., \"data.errorCode\"). For event_data_array: the array field to iterate (e.g., \"artifacts\")."
},
"itemField": {
"type": "string",
"description": "For event_data_array only: the sub-field on each array element to test with operator/value (e.g., \"identity\"). Empty = treat each element as a scalar. The condition matches when ANY element satisfies the operator."
},
"operator": {
"type": "string",
"description": "Comparison operator. exists/not_exists with a dataField test whether ANY event of the type carries a non-empty value at that field: not_exists matches when no event does — including when the event type is absent altogether — so a rule can gate on a missing field (e.g. an ESP failure without an HRESULT). Without a dataField they test only the presence of the event type.",
"enum": ["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"]
},
"value": {
"type": "string",
"description": "Value to compare against."
},
"required": {
"type": "boolean",
"description": "Whether this condition must match for the rule to fire. If false, it only contributes to confidence scoring.",
"default": false
},
"filterField": {
"type": "string",
"description": "Optional same-event value filter for event_count, event_data and event_data_array: only events whose filterField satisfies filterOperator/filterValue are considered. For event_count it applies before counting (both count_gte and count_per_group_gte, e.g. count only performance_snapshot events with memory_used_percent > 90). For event_data/event_data_array the filter and the main dataField/operator test are evaluated on the SAME event instance — the only way to pin two fields of one event together, since separate conditions each scan all events independently (e.g. system_sleep_episode with durationSeconds gte 300 AND backfilled equals false)."
},
"filterOperator": {
"type": "string",
"description": "Operator for the same-event value filter. Same vocabulary as operator.",
"enum": ["equals", "not_equals", "contains", "not_contains", "regex", "not_regex", "gt", "lt", "gte", "lte", "exists", "not_exists", "in", "not_in"]
},
"filterValue": {
"type": "string",
"description": "Value for the same-event value filter. Boolean values are stringified (\"true\"/\"false\")."
},
"correlateEventType": {
"type": "string",
"description": "The second event type to correlate with (Event B). Only used when source = \"event_correlation\"."
},
"joinField": {
"type": "string",
"description": "Data field to join on — must have the same value in both Event A and Event B."
},
"timeWindowSeconds": {
"type": "integer",
"description": "Maximum time in seconds between Event A and Event B. Null or 0 means no time limit.",
"minimum": 0
},
"suppressByEvent": {
"type": "object",
"description": "Optional suppression: if an event of the specified type exists with the same joinField value as the matched event, the match is skipped. Used to prevent rules from firing when a subsequent event resolved the issue (e.g., app_install_completed suppresses app_install_failed for the same appId).",
"properties": {
"eventType": {
"type": "string",
"description": "The event type that resolves/suppresses the matched event (e.g., \"app_install_completed\")."
},
"joinField": {
"type": "string",
"description": "The data field to join on — must have the same value in both the matched event and the suppressing event (e.g., \"appId\")."
}
},
"required": ["eventType", "joinField"],
"additionalProperties": false
},
"eventAFilterField": {
"type": "string",
"description": "Optional filter field on Event A (the first event)."
},
"eventAFilterOperator": {
"type": "string",
"description": "Operator for the Event A filter.",
"enum": ["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"]
},
"eventAFilterValue": {
"type": "string",
"description": "Value for the Event A filter."
}
},
"required": ["signal", "source"],
"additionalProperties": false
},
"rulePrecondition": {
"type": "object",
"description": "A device-fact gate evaluated before conditions. Pure boolean filter — does not contribute to evidence or confidence.",
"properties": {
"source": {
"type": "string",
"description": "Currently only event_data is supported.",
"enum": ["event_data"],
"default": "event_data"
},
"eventType": {
"type": "string",
"description": "Event type carrying the field to test (e.g., \"hardware_spec\", \"os_info\", \"tpm_status\")."
},
"dataField": {
"type": "string",
"description": "Data field to test. Uses dot notation for nested fields."
},
"operator": {
"type": "string",
"description": "Comparison operator. Same vocabulary as conditions.",
"enum": ["equals", "not_equals", "contains", "not_contains", "regex", "not_regex", "gt", "lt", "gte", "lte", "exists", "not_exists", "in", "not_in"]
},
"value": {
"type": "string",
"description": "Value to compare against. Boolean values are stringified (\"true\"/\"false\")."
},
"description": {
"type": "string",
"description": "Human-readable note explaining the intent (e.g., \"skip on virtual machines\")."
}
},
"required": ["source", "operator"],
"additionalProperties": false
},
"confidenceFactor": {
"type": "object",
"description": "A factor that increases confidence when matched.",
"properties": {
"signal": {
"type": "string",
"description": "Descriptive name for this factor."
},
"condition": {
"type": "string",
"description": "Condition expression (e.g., \"count >= 5\", \"exists\", \"duration > 300\")."
},
"weight": {
"type": "integer",
"description": "Confidence weight to add when this factor matches (0-100).",
"minimum": 0,
"maximum": 100
}
},
"required": ["signal", "condition", "weight"],
"additionalProperties": false
},
"remediationStep": {
"type": "object",
"description": "A remediation step with title and sub-steps.",
"properties": {
"title": {
"type": "string",
"description": "Title of the remediation approach."
},
"steps": {
"type": "array",
"description": "Ordered steps to execute.",
"items": { "type": "string" }
}
},
"required": ["title", "steps"],
"additionalProperties": false
},
"relatedDoc": {
"type": "object",
"description": "A link to related documentation.",
"properties": {
"title": {
"type": "string",
"description": "Display title for the link."
},
"url": {
"type": "string",
"description": "URL to the documentation.",
"format": "uri"
}
},
"required": ["title", "url"],
"additionalProperties": false
},
"rulePresentation": {
"type": "object",
"description": "How a finding of this rule is shown: its role, a one-line summary, what it is about and its key facts. Every string is a {{token}} template resolved from the finding's evidence when it is rendered; a fact whose value was not recorded is left out.",
"properties": {
"role": {
"type": "string",
"enum": ["outcome", "cause", "context", "advisory"],
"description": "outcome: restates the session outcome (shown in the verdict, not as a finding row). cause: can explain why a session failed or is stuck. context: a circumstance such as standby, a restart or battery power. advisory: hygiene or security without bearing on the outcome."
},
"summary": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"description": "One line for the finding row, for example \"{{install_retry_loop.appName}} was started {{install_retry_loop.count}} times\". When a token was not recorded, the first sentence of the explanation is shown instead."
},
"subject": { "$ref": "#/$defs/ruleSubject" },
"facts": {
"type": "array",
"description": "Key facts shown as label/value pairs, in this order.",
"maxItems": 6,
"items": { "$ref": "#/$defs/ruleFact" }
}
},
"required": ["role"],
"additionalProperties": false
},
"ruleSubject": {
"type": "object",
"description": "What the finding is about. Findings with the same kind and resolved key are grouped, for example every finding about one app. Use qualified tokens of one signal for key and name, such as {{failed_app.appId}} and {{failed_app.appName}}, so both come from the same event.",
"properties": {
"kind": {
"type": "string",
"enum": ["app", "policy", "account", "script", "device", "network", "esp", "session"],
"description": "The kind of thing the key identifies."
},
"key": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"description": "Template of the grouping key."
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"description": "Template of the display name; the key is shown when it is missing."
}
},
"required": ["kind", "key"],
"additionalProperties": false
},
"ruleFact": {
"type": "object",
"description": "One key fact of a finding.",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 40,
"description": "Short label, for example \"Install starts\"."
},
"value": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"description": "Template of the value, for example {{install_retry_loop.count}}."
},
"format": {
"type": "string",
"enum": ["text", "code", "errorCode", "duration", "number", "timestamp"],
"description": "How the value is shown. errorCode adds the error-code catalog meaning, duration reads seconds, timestamp an ISO 8601 time.",
"default": "text"
}
},
"required": ["label", "value"],
"additionalProperties": false
},
"templateVariable": {
"type": "object",
"description": "A variable in a rule condition that must be customized per-tenant before the rule can be used.",
"properties": {
"name": {
"type": "string",
"description": "Machine name for this variable (e.g., \"cert_subject\")."
},
"label": {
"type": "string",
"description": "Human-readable label shown in the configuration UI."
},
"description": {
"type": "string",
"description": "Help text explaining what value is expected."
},
"conditionIndex": {
"type": "integer",
"description": "Zero-based index into the conditions array where this variable lives.",
"minimum": 0
},
"field": {
"type": "string",
"description": "Which field on the condition to customize.",
"enum": ["value", "eventType", "dataField", "eventAFilterValue"],
"default": "value"
},
"placeholder": {
"type": "string",
"description": "The placeholder value that ships with the template (e.g., \"CN=YOUR-CERTIFICATE-SUBJECT\")."
},
"validation": {
"type": "string",
"description": "Optional regex pattern to validate user input.",
"format": "regex"
}
},
"required": ["name", "label", "conditionIndex", "field", "placeholder"],
"additionalProperties": false
}
}
}