P0b/P1 of the 2026-08-17 IME code audit (agent release required):
- Tail AppActionProcessor.log (+ rollover glob) in ImeLogTracker and the
--run-ime-matching CLI. IME routes every [Win32App][ActionProcessor]
line there (enforcement decision engine), so IME-NO-ACTION and
IME-NOT-TARGETED-1/2 could never fire before, and GRS skips were
invisible. Live session 0fe2c562 carried 16 no-action and 64
classification lines in that file.
- New patterns (wordings verified in decompiled IME 1.97 AND 1.104):
IME-GRS-SKIP ("is in GRS. The app will not be enforced." -> Postponed;
THE canonical silent non-install reason), IME-GRS-CLEARED (early
subgraph termination, retry next check-in -> Postponed),
IME-TOKEN-FAILURE ("Failed to get AAD token ... errorCode = N").
- New action imeTokenFailure -> ime_token_failure Warning event (new
EventType: Constants + MCP resource-catalog + shared manifests
regenerated). Tracker dedups per distinct errorCode; included in
the historic-replay guard so stale lines from a previous enrollment
cannot emit a misleading warning. Complements IME-TOKEN-SUCCESS:
an auth outage previously left check-ins silently doing nothing.
Tests: adapter emission + unknown-code + tracker dedup; known-actions
catalog extended; all suites green (Functions 4262, Agent 2252, web 762).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A warning does not stop an end user — telemetry-cadence types (performance_snapshot,
agent_metrics_snapshot, download_progress, network_state_change/connectivity_check,
log_entry, agent_trace, stall_probe_check) would turn every ingest batch into an
analyze run. Now enforced in three layers:
- AnalyzeRuleTriggers.BlockedOnEventTypes (code-only enforcement): CRUD write is
rejected with an explicit message, and OnEventTypes() drops blocked types at
runtime so even a persisted row (pre-validation data, direct table write) is
inert for the trigger registry and the engine alike.
- rules/guardrails.json gains blockedInterimTriggerEventTypes as the display/
pre-flight mirror (baked into the MCP RULE_GUARDRAILS via combine.js); a
parity test pins mirror and code set against drift — same pattern as the
gather hard blocks.
- MCP validate_rule: the high-frequency finding is now an ERROR sourced from the
baked mirror, not a hardcoded warning.
Suites: Backend 4155, MCP 548.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex follow-ups to 1ebc2675:
- guardrails.json now mirrors blockedCommandPatterns + maxCommandLength and
carries C:\Windows\System32\config in blockedFilePrefixes; portal
(guardValidation.ts) and MCP (rule-validation.ts) enforce them in agent
order (before the unrestricted-mode early-return), so hard-blocked commands
no longer show "Allowed" in unrestricted tenants. GuardrailsParityTests
pins the JSON mirror to the C# constants.
- gather-rule schema: collectorType enum gains json/xml (agent + portal
supported both; MCP validate_rule falsely rejected them); MCP validates
json/xml targets with file semantics and rejects activePhases +
activeFromPhase set together (backend 400 parity); authoring guide updated.
- Author anti-spoof: tenant + global UpdateRule stamp the caller's JWT
display name before the service call, closing the PUT-upsert hole where a
non-existing ruleId stored the payload author; true updates still preserve
the original. GatherRulesFunctionAuthorEndpointTests drives the real
endpoints (first HttpRequestData-level test) proving body authors never win.
- AgentBootstrap.RetrySelfUpdateAfterEnrollment: internal + UpdateInvokerOverride
test seam (RequestGracefulShutdown idiom), LastVersionCheckSucceeded setter
internal; AgentBootstrapSelfUpdateRetryTests pin the guard, the relaxed
await_enrollment timeouts, and the swallow-everything exception contract
with save/restore of the static state.
Functions 3958, Agent-Core 2200, Web 670, MCP 543 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to gather-rule phase scoping. One-shot collection at a phase START
already existed (phase_change dedups per (rule, phase)) and at enrollment END
(on_event + enrollment_complete); the END of a specific phase had no trigger.
New trigger value "phase_exit": fires once when TriggerPhase is LEFT (empty =
every phase transition). Together with phase_change it forms the two one-shot
bookends of a phase.
Agent (GatherRuleExecutor.OnPhaseChanged):
- Exit rules are evaluated BEFORE _currentPhase advances, so both the
TriggerPhase match and the phase-scope gate see the phase being LEFT. A rule
scoped to DeviceSetup therefore fires on its own exit even though the phase
being entered is out of scope.
- A transition into Failed fires the exit rules of the failing phase — the
snapshot at a failure boundary is the point of the trigger.
- Exit of Unknown never fires; a repeated identical phase signal is not a
transition.
- Dedup key "exit:{ruleId}|{phase}" shares _phaseRulesExecuted with the
phase_change keys but stays in a disjoint key space, so an enter-rule and an
exit-rule on the same phase cannot consume each other's slot.
Portal: trigger labels are now "Phase Start" / "Phase End"; the canonical phase
dropdown and the create/save payloads cover both via PHASE_TRIGGERS; the card
renders "entering X" / "leaving X".
No ConfigVersion bump: no new field is transported. Old agents do not know the
value and simply never fire the rule (silent no-op).
Tests: GatherRuleExecutorPhaseExitTests (11) — enter-vs-exit, no refire on
re-entry, empty-triggerPhase, exit-of-Unknown, same-phase-signal, into-Failed,
Complete, scope-against-left-phase, out-of-scope slot preservation, key-space
disjointness, on_change interplay. Build 0 Errors; V2.Core.Tests 1884/1884;
Functions.Tests 3099/3099; vitest 557/557; tsc silent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 0-2 of the gather-rule anti-spam plan:
- GatherRuleService.UpdateRuleAsync: toggle-style partial PUTs ({enabled,
isBuiltIn, isCommunity}) previously full-replaced the stored custom rule and
wiped Title/Target/Trigger/Parameters. Partials now merge Enabled into the
existing row; full edit payloads preserve the original CreatedAt.
- GatherRule: new opt-in fields ActivePhases (run only during listed phases),
ActiveFromPhase (sticky latch from a phase onwards), EmitMode (always |
on_change). Absent fields = legacy behavior; schema updated accordingly.
- TableStorageService.Rules: Store+Map for the three columns (absent column /
stored-empty -> null); MapToGatherRule internal for roundtrip tests.
- ContentEquivalent covers the new fields, null-tolerant for ActivePhases so
pre-existing rows never diff against seeds (no reseed churn).
- GatherRulesFunction.ValidateScopeAndEmitMode: canonical EnrollmentPhase name
list Start..Complete (Unknown/Failed and numeric tokens rejected), mutual
exclusion of the two scope fields, emitMode whitelist; 400 on violation.
- ConfigVersion 33 -> 34 (33 was taken by MigrateToApiBaseUrl).
Tests: GatherRuleUpdatePartialMergeTests (merge/replace/unknown-rule/built-in
path), GatherRuleScopeFieldsTests (Store<->Map roundtrip incl. legacy rows,
ContentEquivalent matrix, validation accept/reject). Functions.Tests 3029/3029.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Retire ANALYZE-ESP-003 (duplicate of ESP-001) and ANALYZE-CORR-002 (duplicate
of APP-004) via the established sunset path; ESP-001 gains a >60min factor,
APP-004 absorbs the co-occurrence story with working confidence factors
- CORR-003: download evidence (errorPatternId) is now a required condition —
no more firing on proxy + any unrelated app failure
- DEV-002 v2: 'sustained' semantics — new event_count value filter
(filterField/filterOperator/filterValue in schema, Shared model, engine, web
types) requires >=3 snapshots above 90% memory instead of one transient spike
- DEV-003 v2: build-cutoff-only condition (<=22621) now also covers Windows 10;
LTSC caveat + maintenance note (raise to 22631 after 2026-11-10)
- SEC-001 v2: rewritten for the post-June-2026 reality — certificates HAVE
expired, active remediation required (OEM firmware hint added)
- SEC-005/006 v2.1: allow-list extended with OEM factory-preload families
(Intel PPM, AMD Power, Lenovo ThinkPad power/battery, Surface/carrier),
telemetry-curated from 1780 fired sessions; imaging leftovers (USMT.PPKG,
InstallOrder.ini) and generic names deliberately still alert
- APP-001: suppressByEvent on app_install_completed; APP-002: exact MSI exit
codes instead of bare 16xx; APP-006 reframed as 'App Install Retry Loop';
ID-001: title/logic mismatch fixed + GATHER-ID-002 dependency documented
- Fix all 13 dead confidence factors (signal-name mismatches / unsupported
syntax) by rewiring or removal; version bumps everywhere so seeding
propagates the updates
- New RuleEngineEventCountFilterTests + SEC-005 OEM allow-list coverage
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two V2 IME fixes, verified against client logs of sessions fc48c71a / 6b4993e5.
Bug 1 — on an ESP terminal failure of the DeviceSetup Apps subcategory the only
device app was stuck mid-install (RealmJoin stuck Downloading 0%) and IME logged
no per-app error, so app_tracking_summary showed 0 failed and the failure was
visible only via the opaque "Apps (Error)" registry string. New
esp_apps_failure_correlation event names the in-flight (IsActive: InProgress/
Downloading/Installing) device app(s) as the likely cause WITHOUT mutating state
(no fabricated app_install_failed) and only on the Apps subcategory.
Bug 2 — AgentExecutor.exe hosts both platform and proactive-remediation scripts
interleaved in the shared AgentExecutor.log; the policyId-less PS-AGENT-EXITCODE /
PS-AGENT-OUTPUT lines from a remediation invocation bled into the last-started
platform script via the sticky pointer (c3e0124c emitted result=Failed but with
exit 0 + the Teams remediation's "[Compliant] No Classic Teams found" stdout).
New PS-AGENT-INVOCATION pattern (resetPlatformScriptContext) clears the platform
line-capture pointer at every "ExecutorLog AgentExecutor gets invoked" boundary,
so a remediation invocation cannot capture into a platform slot; the platform
result stays keyed by policyId via PS-SCRIPT-RESULT.
Agent-only, V2, inert until redeploy; no ConfigVersion bump. New event type added
to the MCP resource catalog (dual-sync). Green: V2.Core 1522, DecisionCore 400,
Backend BuiltInRules 8, MCP vitest 252.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reworks PPKG detection to one aggregate event the rules parse as an array,
adds a default + customizable allow-list, and makes the event storage-safe.
Rule engine:
- New condition source event_data_array + ItemField: iterates an array field
(any-element match), with dot-path dataField support. (RuleEngine.ConditionEvaluators)
Agent:
- Drop per-package provisioning_package_detected events (noise: a clean Win11 VM
ships ~22 OS-inbox .ppkg). One provisioning_package_scan carries an artifacts[]
array; anyPpkgFound derived from it (consistent, incl. recovery residue).
- Size-based chunking: split artifacts across multiple scan events so each event's
Newtonsoft DataJson stays < 24k (well under the 30k Table-Storage truncation that
would blind the rule); never drops a tail artifact. Per-artifact string fields
(identity/dir/rank/installTime) hard-capped at 256; scanErrors capped.
Rules:
- ANALYZE-SEC-005 (default on) + SEC-006 (template) fire on artifacts.identity
not matching an anchored allow-list (^(?:...)\b) that covers OS-inbox families.
- ANALYZE-SEC-007 alarms when a per-source collection cap was hit (scanTruncated).
Web/MCP/docs: event_data_array in the rule form (types + fields), analyze-rules
docs section + example, event-type catalogs.
Tests: rule-engine array/anchor/dot-path/SEC-007; agent artifacts/chunking/field-cap
/worst-case-size. Build green; V2.Core 1236; backend rule + web tsc green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The post-PR-HS1 single-source design (HS-NEW-RESULT JSON only) had a real timing gap:
IME emits the [HS] new result = JSON line only AFTER its batch send to the Microsoft
service, ~30-90 s after the actual script ran (verified in session c1e714e6: 34-74 s
gap per policy). Short Autopilot enrollments often end inside that window, so health-
script rows were entirely missing from completed sessions even though IME had executed
the scripts.
Two-stage approach (matches original V1 timing for the early signal, while keeping
HS-NEW-RESULT as the canonical truth):
- Re-add HS-COMPLIANCE pattern matching BOTH `pre-remdiation` AND `post-remdiation`
detection compliance lines → HandleHealthScriptDetectionResult emits a script_completed
event immediately. Single regex (`(?<part>pre|post)-remdiation`) covers both phases.
- Re-add HS-RUN-CONTEXT / HS-EXITCODE / HS-STDOUT / HS-STDERR patterns feeding a
single-slot `_pendingHealthScript` accumulator (IME runs health scripts sequentially
within a session — verified). HS-COMPLIANCE merges the slot data onto the early-signal
event so exit / stdout / stderr / runContext are populated from the start, not just
the inferred compliance verdict.
- HS-NEW-RESULT continues to fire later with the full JSON (RemediationStatus,
TargetType, ErrorCode, plus the actual remediation phase's data). UI reducer's
dataCompleteness() scoring keeps the more complete entry — early-signal row gets
enriched in place, no duplicate cards.
Remediation phase itself has no early-signal source in IME logs (only the late JSON
exposes Info.RemediationExitCode and RemediationScriptOutputDetails). A follow-up
based on the agentexecutor invocation line's _RemediationScript.output suffix is
possible once we have a real non-compliant cycle log to validate against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI ajv validation rejected HS-NEW-RESULT.json because the new healthScriptResult
action wasn't in the schema's enum allowlist. Add it next to the other script*
actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional `preconditions` array to analyze rules. Preconditions
evaluate BEFORE conditions and are AND-combined; any failing precondition
silently skips the rule (no RuleResult, no UI card, no tenant-visible
"rule fired"). Distinct from `conditions`, which decide whether the rule
fires given that it applies.
Use case: rules that should only run on certain hardware/OS profiles
(e.g. "skip ANALYZE-SEC-001 on virtual machines because UEFI CA 2023
deployment is host-controlled").
Backend
- New `RulePrecondition` model + `Preconditions` list on `AnalyzeRule`.
- JSON schema gains optional top-level `preconditions` referencing a new
`rulePrecondition` definition (no signal/required/correlation noise —
pure boolean gate).
- `RuleEngine.EvaluateRule` runs preconditions first; on failure returns
null. `EvaluatePrecondition` reuses MatchesEventType / GetDataFieldValue
/ MatchesOperator. Operator semantics:
* not_exists → passes when event type absent OR field empty (handy
for "old agent without the new field" path).
* exists / others → fail closed when event type or field missing.
- `TableStorageService` rule round-trip extended with `PreconditionsJson`
on Store + Map (column-mapped, not blob — same pattern as Conditions).
Web (analyze-rules editor)
- `AnalyzeRule` / `RuleForm` types gain `preconditions` array; form
payload round-trips on POST + PUT; ruleToForm + EMPTY_FORM updated.
- `AnalyzeRuleFormFields`: new amber Preconditions panel above Conditions
with eventType/dataField/operator/value/description rows.
- `AnalyzeRuleCard`: read-only chip section above Conditions with
"skipped silently if any fails" hint.
Tests
- `RuleEnginePreconditionTests` (7 cases): empty preconditions = legacy
behavior, single pass/fail, AND-semantics with two preconditions,
not_exists passes when event missing, equals fails closed when event
missing, baseline.
- 781/781 backend, 507/507 V1 agent, 782/782 V2 agent, 208/208 web vitest.
No agent change yet — useful preconditions can already target existing
event fields (os_info.edition, hardware_spec.cpuName, tpm_status.specVersion,
…). Adding `hardware_spec.isVirtualMachine` is a follow-up PR (agent collector).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Galactic Admin Mode for Audit Logs: banner + cross-tenant endpoint (galactic/audit/logs)
- Audit page pagination (12 entries per page) with Previous/Next controls
- Platform Metrics: moved computation to backend PlatformMetricsService with 5-min cache
- New endpoint GET /api/galactic/metrics/platform replaces N+1 client-side fetches
- IME log tracker enhancements, DO telemetry pattern, and download progress improvements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the missing operator to the enum so ANALYZE-APP-006.json passes
schema validation in the combine-rules workflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Rules as GitHub single source of truth
- Add 75 individual JSON rule files (2 gather, 18 analyze, 55 IME patterns)
under rules/gather/, rules/analyze/, rules/ime-log-patterns/
- Add JSON Schemas for all three rule types with VS Code IntelliSense support
- Add CI combine script (rules/scripts/combine.js) + GitHub Action that
validates schemas with ajv-cli and generates rules/dist/ combined files
- Convert BuiltIn*.cs from hardcoded C# objects to embedded JSON resource loaders
- Add GitHubRuleRepository service to fetch dist/ files from raw.githubusercontent.com
- Add ReseedFromGitHubFunction: POST /api/rules/reseed-from-github?type=all|gather|analyze|ime
## IME Log Patterns
- Extend ImeLogPattern model with Description and IsBuiltIn properties
- Add ImeLogPatterns table + API constants
- Add TableStorageService CRUD methods for IME log patterns
- Add ImeLogPatternService (CRUD, merge, auto-seed from embedded JSON)
- Add ImeLogPatternsFunction: GET/PUT /api/rules/ime-log-patterns (Phase 1: read-only
for tenants, Galactic Admin can enable/disable/edit globally)
- Add IME Log Patterns management page (/ime-log-patterns)
- Wire GetAgentConfigFunction to use ImeLogPatternService instead of static class
## API route consolidation
- All rule endpoints now under /api/rules/* for single certificate exclusion path:
/api/rules/gather, /api/rules/analyze, /api/rules/ime-log-patterns,
/api/rules/reseed-from-github
## Export buttons
- Add per-rule and export-all JSON download buttons to gather-rules,
analyze-rules, and ime-log-patterns pages
## Admin configuration
- Replace old individual reseed buttons with unified "Fetch & Reseed from GitHub"
(calls new GitHub endpoint) plus compact individual-type reseed buttons
- Add CDN cache hint: GitHub raw.githubusercontent.com caches up to 5 minutes
## Preview mode
- ?preview=1 URL param seeds localStorage.previewMode (persists across navigation)
- ?preview=0 clears it; toggle in Navbar settings dropdown (indigo, eye icon)
- IME Log Patterns nav link gated behind galacticAdminMode OR previewMode
- Preview badge shown on nav link when active for normal users
## Tests
- Add xUnit test project (net8.0) with 5 tests for embedded resource loading,
known IME actions validation, and valid categories check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>