Commit Graph

129 Commits

Author SHA1 Message Date
Oliver Kieselbach 08455cc5f9 OOBE quality update: install events from the System log, servicing steps, a bounded engine phase in Shadow mode (D-310)
Report 3a2207978e4c: a session was failed 30 min after the ESP exit while Windows installed its
OOBE quality update. The update watcher had been blind since July and the engine had no notion
of the phase.

- Windows Update client: 19/20/43/44 come from the System log (provider-filtered XPath), as the
  provider manifest routes them; 25/26/31/41 from Operational via their own config list.
  Updates are classified os/defender/store/other: only Windows/.NET updates become rows, the
  rest is counted into windows_update_activity_summary. Per-channel watermarks persist a record
  only after its row and engine signal were handed to the ingress.
- Servicing: CBS steps from the Setup log (windows_update_servicing), client UpdateAgentLCU.
- Engine: OsUpdateActivity and OsUpdateFacts (event-time ordered) anchor an OOBE update phase
  after the ESP exit. Each state has its own bound: the update runs at most 3 h (then Failed
  oobe_update_not_finished), after it the user has 60 min to sign in (then the engine stops
  waiting without a verdict), after the sign-in one window of grace.
- The phase acts only in OobeUpdatePhaseMode=Active. Shadow, the default, reports what it would
  decide as oobe_update_phase_shadow right before today's verdict. The agent stamps the mode on
  EnrollmentFactsObserved at every start; a later stamp can take Active away, never grant it.
- Agent: a stop without a verdict ends the session like the max-lifetime watchdog
  (agent_shutting_down reason=stopped_waiting); the backend classifies it honestly (verdict
  origin stopwait), never Failed.
- Measurement: oobe_update_page (the update page and its ExpeditedUpdate_* names; a 62407 value
  only as boolean, integer or HRESULT) and oobe_update_state (NDUP and ESP-policy registry
  values), both Debug and bounded per run.
- ANALYZE-DEV-004 1.2.0 counts install and download failures only, never scan failures.
- Event types in Constants, MCP catalog and web; shared manifests regenerated; privacy page
  names Windows updates installed during the enrollment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:50 +02:00
Oliver Kieselbach 0d6c03d64b Public texts no longer name a customer domain or IME decompiles
- Two code comments of the gather-rule timeline feed named a customer's domain next to the
  session id; the session id stays as the regression anchor.
- The get_ime_pattern_health description and the IME Pattern Health admin text no longer send
  the reader to "the IME decompile"; the IME-TOKEN-FAILURE and IME-DO-TEL pattern descriptions
  say "verified against IME x" / "absent from the x builds" (rules/dist regenerated).
- Code comments that cited decompiled IME builds cite the IME version instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 22:00:52 +02:00
Oliver Kieselbach d8ab70b166 chore(rules): override fast-json-patch to 3.1.1 (Dependabot #103)
ajv-cli 5.0.0 is the latest release and still declares fast-json-patch ^2.0.0,
so neither Dependabot nor npm audit fix can move past CVE-2021-4279. An npm
override forces 3.1.1 into the lockfile; the 3.x API is backward compatible and
the validate command used in combine-rules.yml never touches the package.
Verified: npm ci from the lockfile plus all four ajv validate steps pass locally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 08:29:58 +02:00
Oliver Kieselbach 1b27babd17 ci: zizmor gate for the workflow files plus a clean baseline (D-268)
zizmor (static analysis for GitHub Actions) now runs twice: locally before a
workflow commit (hard rule in .claude/CLAUDE.md) and as the `zizmor` job next to
the pin gate in action-pins.yml (zizmorcore/zizmor-action pinned by SHA, zizmor
1.30.1 pinned, SARIF into Code scanning, severity low and above). Like the pin
gate it reports and does not block. zizmor.yml holds the single reasoned ignore.

Baseline fixes so the gate starts green:
- persist-credentials: false on every checkout except combine-rules.yml, the one
  job that pushes with the checkout credentials (ignored in zizmor.yml, with reason)
- publish-scripts.yml: id-token: write moves from the workflow to the publish job;
  indexnow-notify.yml: permissions: {}
- deploy-mcp.yml: env values referenced as shell variables inside bash steps
  instead of template expansion
- pester.yml / publish-scripts.yml: reusable workflow via the self-repository form
  $/ (GitHub changelog 2026-07-30); the pin gate treats $/ like ./
- combine-rules.yml: ajv-cli installed with npm ci from rules/scripts/package-lock.json
  (no ts-node peer, no lifecycle scripts) instead of an ad-hoc global install;
  Dependabot watches /rules/scripts
- azure/login pin comment corrected to the tag the SHA actually is (v3.0.2; v3
  moved on to v3.1.0), no runtime change
- Dependabot: 7-day cooldown on every ecosystem (security updates are exempt)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 12:56:03 +02:00
Oliver Kieselbach 020248fbda feat(agent): collapse recurring health-script runs into one summary per policy
IME re-runs remediation policies on its own schedule for as long as the
device is up, so an enrollment that waits for its user reported every run
of every policy. The tracker now emits a policy's first run and every run
whose result differs from the last reported one (compliance, exit code,
stderr, remediation status, error code); repeats are counted and surface
as one script_recurrence_summary per policy when the tracker stops.

The start signal of a known policy is held until its result is known,
because the running indicators pair a start with a later result; a start
without any result is still emitted after a grace. The counters live in
the tracker state file next to the file positions, so a hard kill rolls
both back together. Platform scripts are untouched. The summary does not
count as enrollment activity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 00:38:00 +02:00
Oliver Kieselbach 106480ef52 fix(agent,rules): health-script lines never feed a platform-script slot — PS-SCRIPT-EXITCODE deleted, HS-INVOCATION opens an unowned invocation in the IME log, the no-marker fallback stays within files platform runs open in (D-248)
Test-VM session 24dc69d1 (2.0.1459) had the overwrite geometry the result hold (D-247)
was built for, yet the hold never fired: "Powershell execution is done, exitCode = 0" —
written by IME's health-script worker (AgentCommon ScriptWorker) to both
IntuneManagementExtension.log and HealthScripts.log — was read as PS-SCRIPT-EXITCODE and
attributed to the pending platform script c81b8053 1.2 s before its executor even started
(IME-log marker: the last PS-SCRIPT-GENERATED; HealthScripts.log: the no-marker fallback to
the last platform start). The completion went out at the result line with the foreign exit
code, and the run's own end block, recovered 150 ms later by the rewind, was dropped as
already emitted. Both codes were 0 by luck; a failing health script would have turned a
succeeded platform script into script_failed. The worker's launch line flipped the pending
script's context the same way (338d9434, twice in one run).

- rules: PS-SCRIPT-EXITCODE deleted — the line is health-script-only (3 hits = 3 cycles,
  platform scripts never produce it), HS-EXITCODE already carries the value, and a line that
  lands inside a parallel platform run's window cannot be attributed by position.
  HS-INVOCATION (new): the worker's command line `"…\agentexecutor.exe"  -remediationScript`
  (identical from IME 1.104.102 to 1.105.152) as agentInvocationArgument of a non-powershell
  kind — the launch line it writes next stays with that invocation. Both take effect on the
  next reseed for every agent from 2.0.1458 on.
- tracker: a file without markers resolves to the platform run in flight only when a
  platform run has ever opened in that file (IntuneManagementExtension.log after a rollover,
  AgentExecutor.log); HealthScripts.log and the app logs never qualify. Test seams:
  ProcessLogMessageForTest takes the source file, ClearInvocationMarkersForTest.
- tests: ImeLogTrackerPlatformScriptContaminationTests +5 with the session's real lines —
  the worker's exit line pre-fills nothing, its launch line flips no context, HealthScripts.log
  lines resolve to no platform script, the context line after an IME-log rollover still finds
  the run in flight, and no shipped pattern reads the worker's exit line.
- rules/dist regenerated (node rules/scripts/combine.js); analyze-rules.json catches up with
  ANALYZE-OFFICE-001 1.1.1 (6657c6b9 was committed without its dist).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 02:52:43 +02:00
Oliver Kieselbach 6657c6b9c0 fix(rules): ANALYZE-OFFICE-001 explains what the detector actually reports
office_install_failed is emitted only for an error code in the C2R INSTALL scenario; a window without on-disk proof ends silently. The old text described a StreamingFinished-based failure and attributed the early "done" of the built-in app type to IME, which never sees Office CSP installs. Text only, version 1.1.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 02:28:12 +02:00
Oliver Kieselbach 197de1d718 fix(agent): the IME log tracker verifies and rewinds when a concurrent IME process overwrote bytes behind its bookmark; executor lines belong to the invocation at their file position (D-246)
IME's trace listener opens every log once per process with FileMode.Append and FileShare.ReadWrite
and writes at its own stream position afterwards. AgentExecutor.exe (two processes per user-context
script, one per detection script) has such a listener on AgentExecutor.log and on
IntuneManagementExtension.log, so a process that resumes logging after another one appended
overwrites those bytes: the final file holds the last writer, a byte-position tailer has read the
first version. Session 46749560 lost 6 of 16 platform-script results and 4 of 16 executor end
blocks with every tracker health counter at zero — results fell back to the exit code (one Success
where IME said Failed), one finished script stayed Incomplete. The agentexecutor_fallback results
in the fleet are this race, not late IME logging.

The tracker now keeps a fingerprint ledger of every entry it processed from the two multi-writer
files (FNV-1a 64 over the decoded bytes, one CmTraceEntryAssembler shared by the read loop and the
verification so entry boundaries are identical by construction), verifies it on every script end
signal, on a byte-budgeted cadence while an invocation is in flight (256 KB/s, at least 1 s
apart), in a 4 KB guard zone on every growth pass and on a fragment at the bookmark, and rewinds
to the first changed entry; entries whose (offset, hash) still read unchanged are skipped,
everything else is processed as backlog (never fresh, never a calibration anchor). The
policyId-less executor lines are owned by the nearest preceding invocation marker in their file
(PS-AGENT-ARG opens, PS-AGENT-SCRIPT-START upgrades to the platform script, PS-AGENT-INVOCATION and
PS-AGENT-COMPLETED close) instead of a last-started pointer, so an end block a rewind uncovers
reaches its own script. Restart seeds the ledger from the 64 KB behind the persisted bookmark;
a rollover clears it. Four restart-safe counters (ime_overwrite_rewinds,
ime_overwrite_bytes_reprocessed, ime_verify_passes, ime_verified_bytes) ride agent_metrics_snapshot
and ime_pattern_hits — recovered data, not skipped work, no new event type.

Tests: ImeLogTrackerOverwriteRewindTests (15) drive the positional-overwrite write pattern against
both files, including the two field scenarios with the shipped patterns; agent suite 2921 green,
backend known-actions list extended for the two new patterns.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 23:40:20 +02:00
Oliver Kieselbach 61892592e4 feat(rules): community rule submissions — submit custom rules from the portal, review via GA page or MCP, publish by repo commit + reseed (D-236)
Tenant admins submit references to their own gather/analyze rules; the server freezes the rule, records pre-flight findings and fire stats, and stores it under a 12-character id (RuleSubmissions, SessionReports layout). Attribution is the submitter's frozen choice (anonymous by default) and becomes the published author. Review (approve with reserved id / decline with comment) through /admin/reports/rule-submissions or the MCP tools list_rule_submissions, get_rule_submission, review_rule_submission, reseed_rules_from_github; published is derived from the global catalog, never stored. Analyze rules now get the same server-side author stamp as gather rules; ANALYZE-ID-001 carries author Community. Privacy page names the credit; bumps internal and .claude/commands gitlinks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 22:53:32 +02:00
Oliver Kieselbach 8224c8f1a8 fix(rules,engine,agent): ESP-004 gates on the esp_apps_timeout verdict; not_exists absence gate; Shell-Core failure inherits settle detail (D-234, D-235)
ANALYZE-ESP-004 fired on every ESP terminal failure of a Continue-anyway
profile and rendered the configured FirstSync limit as elapsed time — the
mayHaveContinuedAnyway flag only says the profile has the button. The rule
now requires the agent's own verdict (app_install_failed with
failureType=esp_apps_timeout), names that app via a same-event filter, reads
failedSubcategory from esp_failure_settle_started and words the limit as a
limit. Verified with test_analyze_rule: cb7036a6 fires, 683f1eff (0x80070652
after 19 min) stays silent.

Rule engine: not_exists with a dataField on event_type/event_data
conditions matches when no event of the type carries a non-empty value —
including an absent type — instead of being dead behind the fieldValue null
short-circuit. Schema and MCP authoring guide say so.

Agent: a Shell-Core 62407 failure that lands inside the provisioning
tracker's 30-s settle window (a third of all ESP terminal failures) now
inherits the pending errorCode / failedSubcategory / category /
likelyCulpritApps, so enrollment_failed, Sessions.FailureReason and the
termination-time app classification see the HRESULT the registry had
already named. failureType stays the Shell-Core value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:20:24 +02:00
Oliver Kieselbach 12e66299e6 feat(rules): allow-list folders under the signed-in user's profile (D-228, D-229)
Gather rules and admin-configured diagnostics paths could never reach a
folder below a user profile. The %LOGGED_ON_USER_PROFILE% token lifts the
C:\Users hard block for AppData\Local and AppData\Roaming, but lifting is
not admitting: the path then fell into filePrefixes, which is matched
literally against the already-expanded path, and the profile name is not
known when that list is authored. The exception only ever worked in
unrestricted mode, and no agent test passed a non-null userProfilePath.

D-228 introduces userProfileFilePrefixes, holding entries relative to the
profile root (AppData\Local\RealmJoin). Both guards resolve them against
the detected profile and match segment-bounded; entries that are rooted,
contain "..", or sit outside AppData\Local / AppData\Roaming are dropped at
load, so configuration can only narrow the hard block's exception, never
widen it. The allowlist is anchored to the detected profile, so a rule
cannot reach another user's AppData. The AppData subdirectory list existed
in both guard classes with the GatherRuleGuards copy unused; it is now one
list and DiagnosticsPathGuards delegates. filePrefixes additionally gains
C:\ProgramData\RealmJoin\choco\logs.

D-229 fixes the pre-flight validators, which must only ever reject what the
agent rejects:
- the portal returned "allowed" on the lifted hard block without consulting
  the allowlist, so a rule the agent refuses passed the enable gate and then
  failed silently on the device with a security_warning;
- the MCP validator offered logparser the diagnosticsPathPrefixes list that
  no collector consults (four paths reported allowed that the agent blocks);
- the MCP validator did not expand environment variables at all, reporting
  %ProgramData% targets as blocked although the device reads them.

Measured before changing the portal: no rule in any tenant used the token,
so nothing that was working gets disabled.

Verified: solution build 0 warnings, agent 2897 tests, web 1257 + tsc +
eslint 0, MCP 779, guardrails.json validates against its schema, rules/dist
regenerates without drift. The six new agent tests were green against the
unchanged guard first, which is what proves the exception was inert.

Takes effect on devices only after an agent release: guardrails.json is an
EmbeddedResource in the agent assembly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 11:54:36 +02:00
Oliver Kieselbach e33d85c01d Agent Host changes for agents/gather-rule-exitcode-info-update (#151) 2026-09-08 14:57:30 +02:00
Oliver Kieselbach 66452621c6 exit-code class per app + reboot rule: IME-EXITCODE-CLASS, ANALYZE-APP-018, exitCode on every terminal app event (D-214)
- IME-EXITCODE-CLASS captures the return-code class of the installer exit code ("lpExitCode is defined as <Success|SoftReboot|HardReboot|Retry|Failed>", verified against the IME string literal) as exitCodeClass on AppPackageState; new action captureExitCodeClass, persisted across the agent restart a reboot causes, last value wins across a Retry loop. IME-EXITCODE-MAPPED-FAILED keeps the error path; both match the same line.
- Adapter: exitCodeClass whenever known, exitCode on every terminal state (Installed/Postponed/Skipped/Error) instead of Error only.
- ANALYZE-APP-018 (warning, apps): app_install_completed.exitCodeClass in SoftReboot,HardReboot; +25 HardReboot, +15 observed system_reboot_detected; never fails the session.
- Web: completed-app exit badge amber (failed stays red), "mapped as SoftReboot/HardReboot/Retry" on timeline and Install Progress.
- Tests: pack end to end (phase, current app, class lines, negatives, restart round trip), adapter payloads, linearity samples, backend rule engine + known actions.
- internal gitlink: plan, D-214, backlog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 18:08:49 +02:00
Oliver Kieselbach 0dcf0eb503 Observation coverage in get_session_summary, absence audit with coverage lint, diagnostics packaging counters (D-213)
MCP: coverage block (window, IME tracker, collectors, upload, diagnostics, gaps) built from the
health events the summary already fetches (session-coverage.ts, data.<key> slices); validate_rule
warns on a required not_exists of an IME-log-derived event without the ime_tracker_degraded
precondition; authoring guide 'absence'; instructions and prompts point at coverage.gaps.
Rules: guardrails.json imeLogDerivedEventTypes (schema + combine). Backend: catalog invariant test —
absence rules carry the coverage gate or a documented exception (ANALYZE-ID-004).
Agent: diagnostics_uploaded/_upload_failed carry packaging counters (truncated, includedFiles,
skippedFiles, skippedByReason, problemsByKind) — counts only, never paths.
Also: shared-manifests.json regenerated (was stale at HEAD; McpAccessPolicy doc string) + codegen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 17:35:52 +02:00
Oliver Kieselbach d2197c6a57 error-code catalog v2: generated from primary sources, symbols and families, MCP lookup_error_code (D-211/D-212)
Shared: error-codes.json schemaVersion 2 (607 entries, 50 IME enforcement states) generated
by scripts/error-codes/generate.mjs from MS Learn tables and the IME enums of one build;
ErrorCodeEntry gains symbol/category/imeRetriesDuringEsp, ErrorCodeCatalog gains
TryLookupDetailed (HRESULT_FROM_WIN32 derivation as data), TryLookupBySymbol,
TryLookupEnforcementState. Backend enricher siblings carry symbol/category/derivedFromWin32
plus enforcementStateInfo. Web: validating, minifying sync; formatErrorCodeSource for every
tooltip; windows_update_failed badge; script error codes decoded; bundle budget test.
Agent: WindowsUpdateTracker reads the catalog symbol (WU_E_UNKNOWN sentinel, load failure
never costs an event). MCP: error-code-catalog.ts (lexical-only), lookup_error_code for every
role, search_knowledge merges catalog hits, get_session_summary key events carry
errorCode/errorText, Dockerfile COPY + ERROR_CODES_PATH. ANALYZE-APP-014 1.2.1 corrects the
0x80073cf0 gloss. Gitlinks: internal (D-211/D-212, plan review, backlog), .claude/commands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 16:24:35 +02:00
Oliver Kieselbach 34ec44e3d4 backend: duration evaluators refuse mixed timestamp provenance only
The first cut refused every reader-zone-fallback endpoint. The DeviceSetup line is
written before the agent starts and is backlog on every device, so that silenced
ANALYZE-ESP-001 for all agents without an era anchor (control sessions 2708e6b4 and
18d80f9c came back "not measurable" after the deploy). Two fallback endpoints share the
writer-zone error and cancel it; only a span mixing a fallback endpoint with an anchored
one (or with "now" for an open phase) is unmeasurable — the a7140f98 shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 21:34:15 +02:00
Oliver Kieselbach f607e21c13 agent+rules: Hybrid Join user affinity from the IME user token; IME backlog eras anchored by the bootstrap record
hybrid_login_pending now measures the absence of a real-user desktop after a reboot
(cancelled by the DesktopArrivalDetector or a real JoinInfo user), is never armed on
WhiteGlove devices (resume marker, Part-2 flag, .part1- archive) and reports the
JoinInfo placeholder as a fact only — on a Hybrid join the AD sign-in never replaces it.
New EntraUserAffinityDetector: armed at the real-user desktop on Hybrid devices, the
first IME-TOKEN-SUCCESS line after the desktop posts ime_user_token_acquired; ten
minutes without one post entra_user_affinity_pending with the IME token-failure codes.
ImeLogTracker exposes raw per-line token callbacks past the historic-replay gate;
HelloTracker surfaces User Device Registration 304/305 as device_registration_event.
ANALYZE-ID-004 v2.0.0 keys on those events (event_type not_exists gained a branch),
GATHER-DEVICE-006 follows, GATHER-DEVICE-007/008 collect the Entra operational log.
Constants + MCP catalog gain the five types (D-072), the guard test learns the
multi-line OnTraceEvent shape, gather_* outputs pass the MCP filter gate by prefix.

CMTrace: ImeLogEraPreScan/CmTraceEraTable anchor the backlog era that holds the
bootstrap execution record against the Deployed install marker (0.13-0.36 s in the
field), whole era or nothing, sibling files by local time; origin era-anchored.
Backend phase_duration/app_install_duration refuse a reader-zone-fallback endpoint.

Decisions D-190, D-191. Session a7140f98.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 21:12:56 +02:00
Oliver Kieselbach f0a3f04398 fix(agent,rules): AccountSetup activity means observed progress; same-event filter for event_data conditions (q8n)
Agent: ProvisioningStatusTracker.HasAccountSetupActivity now requires at least one
AccountSetup subcategory past notStarted. Windows writes AccountSetupCategory.Status with
every subcategory notStarted BEFORE the Device-ESP page exits, so the presence check
confirmed the Device->Account handoff (Shell-Core 62407) as the final ESP exit; the
user-apps-settled synthesis then completed AccountSetup and released the User-ESP
keep-awake minutes before the real exit (fleet: 77 % of keep-awake sessions, median 3 min;
on battery that is enough for Modern Standby). D-183.

Rule engine: filterField/filterOperator/filterValue now apply to event_data and
event_data_array as a same-event filter (shared ApplyValueFilter with event_count).
ANALYZE-DEV-012 v1.1.0 excludes backfilled pre-session episodes (backfilled equals false);
a second independent condition could not, since each condition scans all events on its
own. D-184. Schema, Shared model, MCP authoring guide and generated types follow.

Tests: ProvisioningStatusTrackerAccountSetupActivityTests (the verbatim all-notStarted
registry JSON), SleepEpisodeRuleTests (+3 backfill cases), RuleEngineEventDataFilterTests.
Solution build 0 warnings; agent suite 2820 and backend suite 5791 green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 13:11:28 +02:00
Oliver Kieselbach db020789e1 chore: private submodule docs/ becomes internal/ (knowledge + decisions + work files + private instructions)
- .gitmodules: docs -> internal, repo renamed to Autopilot-Monitor-internal; the OKF bundle now lives at internal/docs/
- .claude/CLAUDE.md rewritten: where knowledge lives, working-file rules, hard rules; imports the private half from internal/CLAUDE.md (silently absent without submodule access)
- .claude/architecture-principles.md moved into the internal bundle (reconciled against the code)
- tasks/ (gitignored) retired; working files live in internal/work/
- doc path references docs/<bundle> -> internal/docs/<bundle> in code comments, skills gitlink, codeql paths, build script
- rule descriptions no longer cite an internal doc path (customer-visible text); rules/dist regenerated
- public tree no longer names private operator tooling
- .claude/commands gitlink: skill docs point at the new paths
2026-09-04 02:19:00 +02:00
Oliver Kieselbach f4a5eac539 chore(rules): IME pattern-pack hygiene against the IME decompiles (1.97.107.0 / 1.104.102.0 / 1.105.103.0) — no wording drift in any shipped pattern; the 11 disabled legacy patterns whose literals last existed in IME 1.50 (fleet minimum 1.97) are deleted instead of left as re-enable traps (IME-DETECTED-OLD, IME-SET-CURRENT-1/2/3, IME-SKIP-APPLICABILITY-NOTMET, IME-SKIP-DEPENDENCY-DETECT, IME-SKIP-DETECTED, IME-SKIP-UNINSTALL-NOTDETECTED-2, IME-UPDATE-NAME-1/2/3); IME-DO-TIMEOUT-1/2 match the exact source literal "DO downloading is not finished within timeout, jobId = …, fileId = …" instead of two lookaheads; IME-DO-TEL documented as emitter-less from 1.104 on and kept only for the 1.101/1.103 fleet share
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:23:14 +02:00
Oliver Kieselbach 09492e908c fix(security): IME log-pattern pack is anchored and linear (all 90 patterns start with ^, IME-APP-VERSION spells the report-delta JSON out instead of a chained [^}]*? scan) and the ImeLogTracker read loop is bounded — BoundedLineReader (32 MB per line, evidence: real 5.6 MB "Get policies" lines; exact byte bookmarks, no decoder read-ahead loss on cancel), 2 s per-line matching budget across all patterns, held-back unterminated EOF tails (≤1 MB, 1 s settle) instead of dropping them and raw-matching fragments, a complete entry after a dropped one is no longer swallowed as its close, one Warning per pass (oversizedLines/regexTimeouts/lineBudgetBreaks) instead of silent Debug timeouts; unanchored PS-SCRIPT-CONTEXT false positives on [Win32App] SideCarScript… lines (which overwrote the pending platform script's RunContext) are gone; --run-ime-matching assembles multiline entries like the tracker
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:03:50 +02:00
Oliver Kieselbach 84d06e1cf6 fix(security): LocalAdminAnalyzer never exempts a logged-in local SAM account from the shutdown scan (CWE-807) and matches built-ins by well-known RID so localized Guest/Administrator are not flagged
The shutdown run allowed every logged-in user (explorer.exe owner) dynamically, so a backdoor account created after the startup scan and signed in before enrollment completed was scored 0. The WMI inventory is now read first and a logged-in name that is a local account is rejected for the account and the profile check (reported as logged_in_local_accounts); only Entra/domain identities — the only ones seen in real sessions — remain dynamically allowed for their profile folder.

Built-ins are recognised by RID 500/501/503/504 on the machine SID in addition to their English names: since disabled accounts are inventoried (88dfd331), localized Gast/Invité/Administrateur would otherwise flag every non-English device. kioskUser0 (Assigned Access autologon) joins the built-in list; the profile check skips junctions and $<32 hex> folders (cannot be an account name, pure payload noise). ANALYZE-ID-002 1.3.0 explanation mentions the new field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 01:42:51 +02:00
Oliver Kieselbach 88dfd331dd fix(security): LocalAdminAnalyzer inventories disabled accounts and reads Administrators-group membership from the SAM (NetLocalGroupGetMembers on S-1-5-32-544); a dormant /active:no admin backdoor now surfaces as unexpected_admin_members (+40, Error), built-in Administrator enabled-state reported for evidence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 01:00:29 +02:00
Oliver Kieselbach 0515523e49 fix(security): guardrails.json values are emitted into generated TS through a complete encoder (JSON.stringify + U+2028/29) and rejected up-front if they contain control or line-terminator characters; new guardrails schema validated in combine-rules before any bot commit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 00:19:52 +02:00
Oliver Kieselbach 5055db2de1 feat(rules): detect installer-mutex app failures (0x80070652) and SWG-blocked diagnostics uploads
Two analyze rules born from the pilatus-aircraft diagnosis (2026-08-27):

- ANALYZE-APP-017 "App Install Blocked by Another Installer (0x80070652)":
  fires on app_install_failed with hresultFromWin32 -2147023278
  (ERROR_INSTALL_ALREADY_RUNNING); confidence boosts for MSI exit code
  1618 and for an ESP failure settling with the same code. The 08-26
  burst failed 16 sessions deterministically at ~21 min (IME's fixed
  msiexec-busy retry budget) right after a preceding installer left the
  Windows Installer mutex held - previously only generic
  "Enrollment Failed" fired, naming no cause.
- ANALYZE-NET-001 "Diagnostics Upload Blocked by Web Security Gateway":
  fires on diagnostics_upload_failed answered by an HTML block page
  carrying a recognizable security-product marker (McAfee/Skyhigh,
  Zscaler, Netskope, ...). OnFailure diagnostics are lost exactly on the
  sessions that need them; plain HTML errors without a vendor marker
  (stale/stopped endpoint) stay excluded on purpose.

Both validated via MCP validate_rule (0 errors) and dry-run with
test_analyze_rule: fire on the burst session (confidence 100 / 90),
silent on a timeout-shaped failure and on a succeeded session that
carried unrelated app_install_failed events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 19:40:17 +02:00
Oliver Kieselbach c91d8d4501 fix(agent): complete enrollments with zero user-targeted apps instead of failing them
IME never writes its "Completed user session" line when the user has no
assigned Win32 apps - its zero-app branch returns before that statement
(ApplicationPoller.cs:713-721, identical in 1.80/1.83/1.97/1.104). So
ImeUserSessionCompletedUtc stayed null, every arm of
ShouldTransitionToAwaitingHello was unsatisfiable, and the 30-min
advisory_completion backstop resolved healthy enrollments as
esp_exit_without_completion_evidence. One tenant: 10 of 30 sessions in a week,
all false positives - DAD-validated desktop, Hello provisioned, RealmJoin still
installing. Every Succeeded session there carries ime_user_session_completed;
not one of the failed ones does.

IME-USER-SESSION-ZERO-APPS reads the line IME does write there and routes it
through the same OnUserSessionCompleted callback. That is IME's own verdict
rather than a weakened gate: the zero-app branch locks the ESP in with an empty
application list, and the sibling emit site returns ProvisioningComplete right
after logging it. Guarded to the AccountSetup phase so it cannot burn the
adapter fire-once flag on a pre-sign-in timestamp, and replayed on the phase
transition when the observation predates the phase marker. AreUserEspAppsSettled
accepts an empty app list only with that explicit observation, never on
emptiness alone.

Second, independent defect: advisory_completion (30 min) overran the RealmJoin
completion gate, which deliberately holds the session and carries its own
bounded resolution (60 min from detection, activity-extended, 4 h cap). One
session installed 25 RealmJoin packages inside the window, the last 2 min before
the failure, while completion_waiting still read "waiting on:
realmjoin_resolution". The handler now re-arms while that gate is closed and its
own deadline is still armed; convergence comes from the gate itself, which
always ends with an Outcome set.

ime_user_session_completed gains an evidence field (zero_user_apps_enumerated /
user_session_completed) - no new event type, so the Constants ->
resource-catalog -> shared-manifests triple stays untouched. The pattern ships
via the config API and reaches existing agents, where an unknown action is a
single Debug line; the behaviour needs the agent release.

Build 0 warnings. DecisionCore 558/558, V2.Core 2530/2530, Backend 4634/4634,
MCP event-types-drift 6/6.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 12:27:42 +02:00
Oliver Kieselbach 06cf1c4c12 feat(agent): system timeline watcher — clock steps + sleep episodes as ground truth
Two enrollment blind spots become timeline ground truth from the System event
log via one EventLogWatcher (WindowsUpdateTracker pattern, 24h backfill with
RecordId watermark recovering pre-agent records):

- system_clock_changed (Kernel-General 1): oldTime/newTime/signed timeDeltaMs,
  reason and the setting process. 2s floor suppresses w32time 1ms micro-slews;
  steps >= 5min are Warning + immediate upload. Payload instants are the
  authoritative source (event timestamps can be clamped server-side).
- system_sleep_episode (Power-Troubleshooter 1 classic sleep/hibernate,
  Kernel-Power 507 Modern Standby; 506 deliberately unconsumed): kind,
  enteredAt/exitedAt, durationSeconds, wake source / AC state. 60s real-sleep
  floor filters Modern Standby screen-off scenarios.

ConfigVersion 39 (Collectors.EnableSystemTimelineWatcher +
SystemEventBackfillLookbackMinutes). Both types are non-activity on agent and
backend (idle/stall clocks unaffected; a sleeping device stays Stalled).

Consumers: TimeJumpBadge gains the ground-truth cause "clock-set"
(classifyTimeJump matches backward display steps against recorded clock sets);
EventTimeline badge blocks for both events plus the previously bare
power_state_change; SessionInfoCard keeps the wall-clock duration and adds a
standby note; TimeAttributionCalculator builds SleepSpans (RebootSpan clone,
AttributionVersion 3) persisted as SleepSeconds/SleepSpansJson and rendered in
TimeAttributionLane; ANALYZE-DEV-012 (info, single) fires once per session for
episodes >= 5min - deliberately the only new rule, clock findings stay with
DEV-008 to avoid duplicate-finding flood.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 19:41:56 +02:00
Oliver Kieselbach 8f24876682 feat(agent): live power-state watcher + battery analyze rules DEV-009/010
Event-driven AC/battery observation during enrollment (no polling): a WMI
Win32_PowerManagementEvent (EventCode 10) push subscription re-probes
GetSystemPowerStatus and diffs snapshots through a pure tracker into
power_state_change events - transition ac_to_battery (Warning, immediate) /
battery_to_ac (Info) / threshold_crossed at 50/30/15% (per-level lifetime
latch, lowest-crossed-only on jumps, arm-time baseline covers sessions
starting low, 5s trailing-edge debounce, emission cap 20). Never armed on
devices without a battery; arm failure surfaces as collector_degraded.
Kill-switch Collectors.EnablePowerStateWatcher (ConfigVersion 38).

power_state_change is classified non-activity on both sides
(SignalActivityClassifier + IsPeriodicOrStallEvent) so battery drain can
neither keep idle collectors alive nor heal a Stalled session.

New analyze rules, both with on_event:power_state_change interim triggers
and deliberately single-required-condition (event_data conditions scan
events independently - no same-instance join):
- ANALYZE-DEV-009 (high): thresholdPercent equals 15
- ANALYZE-DEV-010 (warning): transition equals ac_to_battery

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:24:41 +02:00
Oliver Kieselbach 81c14bb3ed fix(rules): close the rule-ID namespace gaps around the shared global keyspace
Custom rules are tenant-partitioned and functionally collision-free, but three
surfaces still keyed on the bare non-unique ruleId string:

- RuleStats global aggregate (global_{ruleId}) summed same-ID custom rules of
  unrelated tenants (title/severity last-writer-wins). The global row is now
  catalog-only: analyze gates on IsBuiltIn/IsCommunity, gather gates on the
  reserved-namespace pattern (agent events carry no flag; custom rules can
  never occupy the reserved pattern).
- The 'never ship a built-in in the CUSTOM namespace' commitment from
  RuleIdPolicy was documented but unenforced: combine.js now fails on
  duplicate or non-reserved built-in IDs, BuiltInRuleCatalogPolicyTests pins
  the same invariants on the embedded catalog, and the GitHub reseed rejects
  non-reserved IDs before writing to the global partition.
- DeleteRuleStatesForRuleIdAcrossTenantsAsync (cross-partition delete by bare
  RowKey) now refuses IDs outside the reserved namespace with failed=-1 so
  callers abort the follow-up global catalog delete.

Verified: prod global partitions (45 analyze + 11 gather) are fully inside the
reserved namespace, so the guards touch no existing data. New OKF doc
docs/rules/rule-id-namespace.md captures the identity/collision model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 14:13:46 +02:00
Oliver Kieselbach ab61839685 feat(backend): P6 split - CMTrace skew tripwire + customer clock_skew rule
Operator side: CmTraceSkewTripwire piggybacks on the terminal counter
reconcile's partition scan (zero extra reads) and compares median
ReceivedAt-OccurredUtc deltas of IME-derived vs other events. A clean
15-min-grid divergence (residual < 2 min, >=20 samples / >=3 distinct
batches per side) emits the CmTraceTimeSkewRegression ops event
(Agent/Warning, goal: never fires). Bias-dominated sessions are
suppressed; measuredWriterOffsetMinutes is never consulted (sticky
after era flip-backs). Kill switch: CmTraceSkewTripwireDisabled.

Customer side: new clock_skew analyze condition source (skewMetric
clock_jump | sustained_offset, threshold seconds, gt/gte magnitude)
behind built-in ANALYZE-DEV-008 (300 s = the Kerberos/Entra 5-min skew
boundary). Batch-median frames with spool-spread filter, plateau step
detection (a gradual backlog-drain ramp cannot fake a jump) and
end-state persistence; IME-derived events are excluded so a CMTrace
anchoring regression can never fire the customer rule. Evidence carries
field=skewSummary/value for {{skewSummary}} interpolation.

Full mirroring chain: schema enum + skewMetric property, shared
manifest + combine.js regeneration, MCP authoring guide + semantic
lint, web SOURCES/form/card, ops-event dual-register, docs. P5 is
subsumed by the tripwire. PercentileMath is now the single nearest-rank
implementation (Apps/PlatformMetrics delegate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 22:30:10 +02:00
Oliver Kieselbach e1b03de5b3 fix(agent): recover ESP completion after a forced mid-ESP reboot
Seven sits-d Cloud PCs enrolled the same day with identical configuration
(~138 required apps at the user-ESP gate, DeviceGuard/DmaGuard device-assigned).
The two that finished AccountSetup before the coalesced reboot landed succeeded;
the five the reboot killed in Stage=EspAccountSetup never derived a completion
again and ended Incomplete on healthy devices. Two defects behind that:

ShellCoreTracker.BackfillRecentEspExitEvents() has existed since session
772fe502 but no caller ever invoked it, so the Shell-Core watcher only saw
records written after Start(). Every agent restart silently dropped an ESP exit
(62407) or Hello-wizard start (62404) from its downtime. EspAndHelloHost.Start()
now calls it, and the fixed 5-minute window is sized to the actual downtime when
the previous run died to a reboot (LastBootUtc, clamped to 360 min).

The user-apps-settled synthesis ran only on the EspExited edge. When the ESP page
exits with apps still in flight the single attempt misses and nothing re-checks —
8110e262 and a89aac2d both reached 138/138 apps, 0 failed, and still never
completed. The exit edge is now recorded and the synthesis re-evaluated from
ImeLogTracker.OnAppStateChanged. No new completion path: the gate conditions are
unchanged, this only gives the existing conservative check a second look. The
fire-once claim moves to Interlocked (edge and re-check run on different threads)
and the re-check does not re-emit app_install_starved.

Rules: ANALYZE-ESP-005 gains on_event:session_stalled so a hung session produces
a finding at the 60-minute stall instead of staying finding-less for hours
(verified against 8110e262 — fired, confidence 95). New ANALYZE-APP-016 turns the
already-emitted app_install_starved events into a customer finding and, unlike
ANALYZE-APP-015, fires even on a succeeded session: 3b9291aa was recorded
Succeeded with 99 required apps that never started installing.

ANALYZE-APP-015 and ANALYZE-ESP-001 were considered for the stall trigger and
deliberately left alone — APP-015 is the documented worked example of the
terminal-precondition trap, and ESP-001's phase_duration gate on a still-open
phase is unverified mid-run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 17:00:59 +02:00
Oliver Kieselbach 2cf7216be5 Token-failure grace window + registry_app_baseline summary
Live findings from the first two 2.0.1402 sessions (Oliver, 2026-08-18):

1. "Failed to get AAD token" is logged routinely during the device phase
   (no user token exists yet) - both test sessions raised the Warning
   although nothing was wrong. The failure is now held pending: any
   IME-TOKEN-SUCCESS match clears it (staleness-gated so a replayed
   success from a previous enrollment cannot mask a current outage), and
   ime_token_failure fires only when no success follows for the 30-min
   grace window, then deduped per errorCode. Genuine auth outages run
   long enough to hit the window; routine device-phase failures never do.

2. New one-shot registry_app_baseline event (Info, only when non-empty):
   summary of the Win32Apps state that predates the agent run - counts
   by enforcement class + capped 50-app typed payload incl.
   LastUpdatedTimeUtc. On WDP the DPP-policy apps install in Batch 1
   before the bootstrap-script agent exists; their outcomes live only in
   this baseline, closing the pre-agent app gap without log backfill.
   The diff rail stays silent on baseline state (anti-replay unchanged).

Event type dual-registered (Constants + MCP catalog + manifests regen).
Tests: grace-window cycle (resolved / unresolved / re-arm dedup),
baseline summary (counts, one-shot, empty). All suites green
(Agent 2274, Functions 4262, MCP 561, web 762), 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 03:16:58 +02:00
Oliver Kieselbach 68eb2da283 IME tracker: tail AppActionProcessor.log; GRS + token-failure signals
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>
2026-08-18 01:01:02 +02:00
Oliver Kieselbach ccf4f2e054 IME patterns: quote-flexible report deltas for IME 1.104 serialization
Live validation against session 0fe2c562 (IME 1.104.102, first session on
the repaired pattern set) showed all report-delta patterns at zero: IME
1.104 logs the delta JSON with single quotes ({"EnforcementState":...)
where older versions logged doubled quotes ({""EnforcementState"":...).

Every literal quote in the seven delta patterns (DETECTED-INSTALLED-1,
ENFORCEMENT-SUCCESS, ERROR-DOWNLOAD, ERROR-REPORT, NOT-APPLICABLE-2/3/4)
is now matched as "{1,2} to cover both serializations.

Verified against the session's AppWorkload.log via validate-ime-patterns:
ENFORCEMENT-SUCCESS 12 (was 0), ERROR-REPORT 14 (was 0); no CRITICALs;
the 2026-08-17 repair set confirmed live (ESP-TRACK-STATUS 24,
ESP-APP-REGISTERED 12, CACHE-MISS 11 in the agent's own match log).

Also observed in 1.104, tracked separately (no pattern change possible):
[DO TEL] line removed entirely - the DO download-duration display feeds
on it; needs an agent-side replacement (DO collector enrichment).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:43:32 +02:00
Oliver Kieselbach a3089753d3 IME patterns: repair drift found by decompiled-source audit (IME 1.97 Dec-25)
Audit vs decompiled IME 1.97.107: ~55/79 patterns verified verbatim, but
several were broken or dead against the current V3 pipeline.

Fixes:
- IME-ESP-TRACK-STATUS: drop trailing anchor (line grew ' with name: <n>.')
- HS-COMPLIANCE: tolerate both 'remdiation' typo and corrected wording;
  new HS-COMPLIANCE-POST for the reworded post-detection line
- IME-DETECTION-RESULT: capture DetectedDifferentVersion (ordered first)
- IME-ENFORCEMENT-SUCCESS: unpin OldValue (Error/PendingReboot/null paths)
- IME-ERROR-REPORT: allow unquoted null OldValue on first write
- Disable 11 dead patterns (emitters removed with IME's old V1/V2 pipeline;
  IME-DETECTED-OLD additionally collided with IME-SKIP-INSTALL-DETECTED)

New signals (existing actions, tailed files only):
- IME-ESP-APP-REGISTERED + IME-CACHE-MISS as app-name sources (replacing
  dead IME-UPDATE-NAME-1/2/3)
- IME-WINGET-ERROR (WinGet failures were invisible)
- IME-EXITCODE-MAPPED-FAILED (normal mapped-exit-code failure path)
- IME-STOP-SIGNAL (service stop != OS shutdown)
- IME-RESTART-STOP-PROCESSING (reboot-gated processing abort)

Backend seed diff now also compares Enabled + Parameters - enable/disable
flips and parameter changes previously never propagated to the table.

All exact wordings verified against decompiled source; regex smoke-tested
via validate-ime-patterns.py (engine=regex, 75/75 compiled, all new
patterns fire on synthetic lines with the exact IME format strings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 21:33:37 +02:00
Oliver Kieselbach 65651dd599 ANALYZE-ID-004 v1.2.0: pre-logon VPN + Hello-as-symptom self-service guidance
From the 3b7ba31f (succeeded) vs gardner/esa (parked) hybrid session comparison:
explanation now covers the cached-credentials/pre-logon-VPN variant and why the
Hello wizard never appearing is downstream of the missing PRT; new remediation
block for VPN machine tunnels, cloud Kerberos trust, explicit Hello disable,
update deferral and pre-provisioning. Links the new docs article.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:36:10 +02:00
Oliver Kieselbach daa6df6bab rules: evaluate 30 device-phase built-ins at the WhiteGlove seal, 6 also on_event
Every built-in whose evidence exists by the end of pre-provisioning Part 1 now
carries evaluateOn [enrollment_end, whiteglove_sealed] — app failures, low disk,
memory pressure, unsupported build, WU failures, clock skew, ESP device
subcategories, mid-ESP reboots, retry-recovery, cert store, local admins,
critical vulnerabilities, firmware CA2023, Secure Boot, AutoLogon, PPKG scans,
Office install and hybrid/TPM/MDM known-error codes. Six rules additionally
evaluate when their (rare, acute) trigger event arrives:
on_event:app_install_failed → APP-002/004/007 + CORR-003,
on_event:windows_update_failed → DEV-004,
on_event:modern_deployment_error → ENRL-003.

Deliberately enrollment-end only: APP-010/ESP-004/ENRL-001 (require
enrollment_complete/failed), ENRL-002 (session_timeout), DEV-001 (Hello =
user phase), APP-015 (user phase, terminal-precondition design), ID-004
(keeps its own hybrid_login_pending trigger). Minor version bump on all 36
changed rules (seed contract); rule JSONs normalized to 2-space formatting;
semantic equality vs HEAD verified (only evaluateOn/version differ).

Backend 4157 tests green (APP-013 version pin updated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:29:45 +02:00
Oliver Kieselbach 98fd173fb8 rules+backend+mcp: rule-verify sweep fixes (dead factor, {{token}} evidence, stats cap, list stringify)
Full verify pass over all 54 built-in rules (validate_rule sweep, 30d/7d
get_rule_stats, test_analyze_rule spot checks). Fixes:

- ANALYZE-APP-007 v1.1.0: declare enrollment_failed as a non-required
  condition — the exists-factor referenced an undeclared signal and could
  never apply.
- ANALYZE-APP-014 v1.1.0: add non-required event_data conditions for
  espCategory/espErrorCode/topCandidatePackage/candidateCount/confidence
  so all explanation {{tokens}} resolve instead of rendering literally.
- ANALYZE-ENRL-002 v1.2.0: capture session_timeout.timeoutHours so
  {{timeoutHours}} interpolates (verified via dry-run: renders "5").
- RuleEngine TryFlatLookup/ResolveDotPath: flatten IEnumerable data
  values to a comma-joined string — List fields (e.g. app_tracking_summary
  .pendingNames) stringified to "System.Collections.Generic.List`1[…]",
  breaking contains-matching and {{pendingNames}} interpolation.
- GetRuleStatsAsync: raise the 500-row cap to 10000 and log when hit.
  The scan walks dates ascending, so the old cap silently dropped the
  NEWEST dates of a 30d window — newly added rules (e.g. ANALYZE-SEC-008)
  were entirely missing from global stats.
- MCP validate_rule lint: accept event_data_array itemField as a {{token}}
  resolver — {{identity}} in ANALYZE-SEC-005/006 was a false positive.

Backend 4157 tests green, MCP 548 green + tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:50:29 +02:00
Oliver Kieselbach 06626a4095 web: evaluateOn in the analyze-rule editor + interim-trigger badge
The evaluateOn feature was API/MCP-only — portal users could not see or set
evaluation triggers. The rule editor gains an Evaluation Triggers section
(enrollment end / WhiteGlove seal checkboxes + on_event type list) with
client-side grammar + hard-block validation sourced from the baked guardrails
mirror (combine.js now emits BLOCKED_INTERIM_TRIGGER_EVENT_TYPES for the web);
rule cards show an 'Interim: …' badge for interim-enabled rules (incl. the
built-in ANALYZE-ID-004). Create/edit payloads round-trip evaluateOn; the
template-copy path already deep-clones it server-side.

Suites: web 762 + tsc + lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:34:56 +02:00
Oliver Kieselbach 22a8c7a51d rules: hard-block high-frequency event types as evaluateOn on_event triggers
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>
2026-08-14 16:07:39 +02:00
Oliver Kieselbach 72136f0273 backend+mcp+web: evaluateOn interim triggers for analyze rules
Implements docs/rules/analyze-rule-triggers.md (spec approved today):

- AnalyzeRule.EvaluateOn (absent = [enrollment_end], today's behavior bit-for-bit)
  with the shared AnalyzeRuleTriggers grammar/matching helpers; grammar enforced
  in tenant rule CRUD and the analyze-rule JSON schema.
- RuleEngine: AnalyzeRunContext (Terminal/Interim/Reanalyze), interim rule filter,
  KO suppression on interim runs, and RuleResult update-semantics replacing the
  permanent dedupe freeze — interim rows refresh, the terminal pass finalizes or
  resolves them (ResolvedAt, kept for audit), Reanalyze preserves lifecycle markers.
- Notification dedupe moves onto RuleResult.NotifiedAt (one send per session+rule,
  stamped after a successful send; survives the manual reanalyze rebuild).
- Handler: whiteglove_sealed / interim_trigger reasons — notify + SignalR yes,
  IssuesDetected + rule stats terminal-only (Rule Regression Radar baselines clean).
- Ingest: InterimTriggerRegistry (per-tenant 5-min TTL cache, fail-soft) gates the
  interim_trigger enqueue (batch eventType intersection) and the whiteglove_sealed
  enqueue on the first genuine WhiteGlove seal.
- Storage: EvaluateOnJson on AnalyzeRules; FirstDetectedAt/LastEvaluatedAt/
  IsInterim/ResolvedAt/NotifiedAt on RuleResults (nullable — legacy rows read as
  final).
- MCP: three authoring lints (terminal-precondition trap, unknown/high-frequency
  on_event types, KO-on-interim), authoring guide + baked schema updated;
  get_session_summary marks interim and excludes resolved findings.
- Web: preliminary badge on interim findings, resolved findings behind a toggle,
  issue counts exclude resolved (analysis section + diagnosis flow).
- ANALYZE-ID-004 v1.1.0: first interim-enabled rule (on_event:hybrid_login_pending),
  threshold raised to 75 so only repeated overdue-login signals fire mid-run.

Suites: Backend 4152, MCP 548, Web 762 + tsc + lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 15:53:02 +02:00
Oliver Kieselbach 6c6014940e rules: hybrid user-affinity + never-settled-app detection, dsregcmd on_event snapshot, evidence interpolation fixes
ANALYZE-ID-004 (hybrid sign-in never establishes Entra user affinity),
ANALYZE-APP-015 (required app pending forever, ESP completion deferred),
ANALYZE-ENRL-002 v1.1 (desktop-reached + apps-done evidence), GATHER-DEVICE-006
(dsregcmd /status triggered by hybrid_login_pending). Engine: list evidence
values now comma-joined for UI/{{token}} interpolation; timeoutHours added to
the evidence whitelist so ANALYZE-ENRL-002's explanation interpolates.

Motivated by esa-logistics sessions 3ac8cb0e/95f71149 (user-phase stuck,
GlobalProtect never settling, placeholder identity persisting across reboots).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:39:16 +02:00
Oliver Kieselbach 2f8ad82b0a gather-rules: mirror hard blocks in portal/MCP, stamp author on PUT-upsert; agent: bootstrap retry tests
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>
2026-08-11 12:29:05 +02:00
Oliver Kieselbach 14acfd5c59 rules: renumber Secure Boot rule to ANALYZE-SEC-008 — SEC-002 is a retired ID
ANALYZE-SEC-002 was the sunset AutoLogon rule; BuiltInRulesTests pins
that the ID never returns (the CI failure on main). Reusing a retired
ID would also inherit any historical tenant state keyed to it. Rule
content is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 13:51:16 +02:00
Oliver Kieselbach 9fce499f4c rules: ANALYZE-SEC-002 — warn when Secure Boot is disabled
Fires on secureboot_status.uefiSecureBootEnabled=false; legacy BIOS
devices (registry key absent, agent sets note field) are skipped via
precondition. +20 confidence on physical devices. Validated with MCP
validate_rule (0 findings) and test_analyze_rule dry-runs: fired at 90
on a physical SecureBoot:False session, silent on a SecureBoot:True
session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 13:27:19 +02:00
Oliver Kieselbach 3c1229a301 MCP rule authoring: deterministic context, validate_rule, and session dry-run (#124)
* feat(backend): dry-run endpoint for draft analyze rules (rule authoring)

POST /api/rules/analyze/dryrun evaluates a DRAFT analyze rule against one
session's events and returns a full diagnostic trace: per-condition
matched/evidence (incl. per-eventType occurrence counts), per-factor
confidence breakdown, verdict, and the would-be MatchedConditions map for
{{token}} interpolation preview. Persists nothing: no RuleResult row, no
session-status change, no notifications.

- RuleEngine.DryRun.cs: DryRunRuleAsync / DryRunEvaluate share the exact
  evaluators (EvaluatePrecondition/EvaluateCondition/EvaluateConfidenceFactor)
  with the production path but evaluate ALL conditions instead of breaking on
  the first failed required one; parity with EvaluateRule is pinned by tests.
- DryRunAnalyzeRuleFunction: TenantAdminOrGlobalReader (read-in-effect POST),
  GA/GlobalReader cross-tenant via session tenant resolution, 1 MB body cap,
  structural draft validation (unknown source/operator, correlation without
  joinField, invalid regex, non-evaluable confidenceFactor DSL) with
  actionable error messages.
- EndpointAccessPolicyCatalog: register the new route.
- RuleEngineDryRunTests: trace semantics, production parity matrix,
  side-effect freedom (VerifyNoOtherCalls), draft validation matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(mcp): rule-authoring resources — guide, schemas, guardrails

Deterministic authoring context for customer AIs, replacing chunked
search_docs retrieval for rule creation: get_resource (and the MCP-protocol
resources) now serve

- rule_authoring_guide: hand-written, complete authoring knowledge (collector
  types, triggers, condition sources, operators, confidence model incl. the
  exact factor-condition DSL shapes, {{token}} interpolation order, guardrail
  matching semantics, pitfalls, validate->dry-run workflow)
- rule_schemas: the JSON Schemas (2020-12) for gather + analyze rules
- rule_guardrails: the on-device collection allowlists

Schemas + guardrails are generated into rule-authoring.generated.ts by a new
combine.js emitter (same pattern as the web guardrails mirror; rules/ stays
the single source of truth and CI guardrails-in-sync guards the file).
rule-authoring-drift.test.ts additionally pins generated == repo source,
guide enums == schema enums, and the get_resource size cap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(mcp): validate_rule + test_analyze_rule tools and author-rule prompt

Closes the authoring loop for customer AIs: draft -> validate -> dry-run
against a real session -> iterate -> create in the portal.

- validate_rule (MCP-local, no backend call): ajv 2020-12 validation against
  the baked schemas, guardrail pre-flight mirroring the agent matching
  semantics (hive-stripped segment-bounded registry/file prefixes,
  whitespace-bounded WMI, exact commands, /-bounded event log channels, hard
  blocks), and semantic lint for schema-valid-but-dead rules (unreachable
  threshold, non-evaluable factor DSL, unknown eventTypes, unanchored
  not_regex allowlists, unresolvable {{tokens}}, trigger cross-fields,
  outputEventType conventions/collisions).
- test_analyze_rule: calls the new POST /api/rules/analyze/dryrun and adds
  the {{token}}-interpolated explanation/remediation preview via the
  existing interpolate-rule-template.
- author-rule MCP prompt guiding scenario -> resources -> draft -> validate
  -> dry-run.
- Both tools are visible to every role (backend enforces
  TenantAdminOrGlobalReader on the dryrun route); catalog snapshot updated.
- deps: ajv + ajv-formats promoted to direct dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(okf): rule authoring surface concept doc

docs/rules/rule-authoring-surface.md: the AI-assisted authoring stack
(deterministic MCP resources via combine.js baking, MCP-local validate_rule
layers, the dry-run trace twin with its parity + side-effect invariants, and
what is deliberately absent); registered in index.md, logged 2026-08-05.

Customer docs (rules/ai-assisted-rule-authoring.md + SUMMARY + FAQ entry) are
committed in the autopilotmonitor-docs repository separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 17:24:28 +02:00
Oliver Kieselbach e7ea431ccf feat(esp): recovery story for terminally reported ESP failures (session 4910a5a5)
DeviceSetup/Apps failed, the user pressed "Try again" ~23 min later, apps
recovered to 10/10 and DeviceSetup resolved - yet the advisory_completion
window un-defanged the stale failure and failed the live enrollment,
because the advisory variant was exempt from all re-arm/rebase guards.

Engine fix (DecisionCore):
- New facts EspAdvisoryFailureResolvedUtc + EspAdvisoryFailureCategory
  (state-borne; deadline payload would be lost on re-arm/rebase).
- TryResolveAdvisoryOnCategoryRecovery in both ProvisioningComplete
  handlers: category match sets the fact + emits
  esp_failure_advisory_resolved.
- Guards now (!hasAdvisoryAnchor || advisoryResolved): enforcement-progress
  re-arm, hello-never-observed promote and the reboot rebase apply again
  once the failure demonstrably un-happened; unresolved advisories keep
  the legacy un-defang exactly.
- Truthful terminal reason esp_recovered_without_completion_evidence
  (+ failureRecovered=true, LastFailureTrigger=DeadlineFired so the
  likely-stuck app promotion stays off).

Story events (agent tracker):
- esp_failure_retry_detected: failed subcategory leaves "failed" after the
  terminal fire - consistent with the user pressing "Try again".
- esp_failure_recovered: previously failed category reaches success.
- EspFailureDetectedEventArgs.LikelyCulpritApps (never-started first,
  cap 3) rides the EspTerminalFailure payload so esp_failure_advisory /
  enrollment_failed name the culprit app directly.

Surfacing: MCP resource-catalog, web timeline badges (Recovered /
User retry pills, culprit chip on advisory), gather-rule autocomplete,
ANALYZE-ESP-006 "ESP Failure Recovered After User Retry".

Tests: AdvisoryRecoveryTests (13, incl. the 527/611/635/671 field-ordinal
replay that no longer fails the session), EspFailureRecoveryStoryTests (5),
snapshot-builder guards 18 -> 20. All suites green (DecisionCore 493,
V2.Core 2018, Backend 3201, web vitest 589, MCP 449 incl. drift).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:03:44 +02:00
Oliver Kieselbach 9e24b92fbf feat(agent): ESP policy-provider stall detector + Autopilot registry guardrail
Customer field case: pre-provisioned devices with the ConfigMgr client as a
blocking app occasionally park the user ESP at "Apps (Identifying)" for days.
Root cause (verified against the EnrollmentStatusTracking CSP doc): only a
"ConfigMgr" provider is registered under Device\Setup\Apps\PolicyProviders
(no "Sidecar" = IME) and TrackingPoliciesCreated never becomes 1 - a wait the
CSP defines WITHOUT any timeout. Renaming the key to Sidecar unblocks
immediately. Microsoft documents that ESP providers are not aware of each
other and that pre-provisioning is unsupported with co-management.

New always-on esp_policy_provider_stalled tripwire, generalized to any
provider in both kinds (Setup/Apps TrackingPoliciesCreated != 1,
DevicePreparation InstallationState not in {2,3}):

- EspPolicyProviderProbe: fail-soft registry read of the
  EnrollmentStatusTracking mirror (device + user-SID scopes), defensive
  value parsing (missing/unparseable = incomplete), ScopedOverride test seam.
  Research and CSP contract quotes captured in the XML doc header.
- EspPolicyProviderStallDetector: pure EvaluateStallTransition on a 15-min
  continuous-incompleteness dwell (resets on completion/disappearance;
  absent key = normal early enrollment), one-shot per provider,
  StartupEventGate fingerprint over the sorted stalled key set for
  cross-restart dedup. Warning + ImmediateUpload via InformationalEventPost;
  purely observational, no decision-engine coupling, no config toggle,
  no ConfigVersion bump (disk_space_low precedent).
- EspPolicyProviderStallHost: 60-s wall-clock tick, deliberately outside the
  StallProbe idle gate (idle clock resets on session activity) and the
  idle-stopped periodic collectors.
- Registered in Constants.EventTypes, MCP resource-catalog (health_events),
  web gather-rule autocomplete (stall category).

Guardrail companion: SOFTWARE\Microsoft\Windows\Autopilot joins the
gather-rule registry allowlist ("Autopilot / OOBE / Setup") so tenants can
build custom rules on the subtree (e.g. not_exists on the Sidecar provider
key); generated web mirror regenerated via combine.js; positive + sibling-
spoof guard tests added.

Docs: OKF concept doc agent/esp-policy-provider-stall.md (+ index/log).

Tests: 22 new detector tests (pure transition matrix + emission surface incl.
the ConfigMgr-only customer scenario and gate-across-restart dedup); agent
suite 2013/2013, web vitest 589/589 + tsc clean, MCP 449 passed incl.
event-types-drift.

Both the detector and the embedded allowlist take effect with the next agent
release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:20:47 +02:00
Oliver Kieselbach 90012d9a66 feat(agent): verify Secure Boot CA-2023 state against firmware, not just servicing registry
Customer report: ANALYZE-SEC-001 warned on devices whose firmware already
carried the Windows UEFI CA 2023 certificate — the agent only read the
SecureBoot\Servicing registry value, which mirrors the Windows Update
rollout and lags behind reality (OEM factory image, WinCS, WU not run yet).

Agent: new UefiSecureBootCertReader reads the UEFI db/KEK variables
natively (GetFirmwareEnvironmentVariableExW; SeSystemEnvironmentPrivilege
enabled on the process token and restored — first privilege-adjustment
code in the repo). Fail-soft, never throws; legacy BIOS maps to not_uefi.
secureboot_status gains per-certificate booleans, uefiFirmwareReadStatus,
and the one-sided marker uefiCa2023FirmwareConfirmed (emitted only on
positive confirmation, never as false).

Rule v3.0.0: not_exists precondition on the marker suppresses the rule
when the firmware confirms the certificate; the marker's absence keeps
old agents on exact v2 behavior, so the rule is safe to seed before the
agent rollout. Firmware-verified-missing raises confidence (+20 db,
+10 KEK 2K CA 2023).

Verified: full solution build; backend 3201, agent 1988, web 582 tests
green; tsc/ajv clean; elevated cross-check on real firmware matches
Get-SecureBootUEFI on all seven certificate probes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:07:01 +02:00
Oliver Kieselbach 3f9576d4a9 fix(agent): enforce the gather-rule allowlist in every collector
LogParserCollector and EventLogCollector called no guard at all, while
File/Json/Xml did — and rules/gather-rules.md already promised customers
that log-parser paths were allowlisted. A tenant admin could author a
logparser rule against C:\Users via POST /api/rules/gather and have up to
1000 lines uploaded as telemetry, bypassing the hard C:\Users block the
same page guarantees; eventlog could read any channel, Security included.
The backend validates no collectorType, target, or parameters, so the
agent is the only place this can be enforced.

Both now guard. logparser cannot check rule.Target directly (GetFullPath
throws on a wildcard), so it checks the directory for wildcard targets and
re-checks each resolved path. Event log channels get an allowlist matched
on a "/" boundary. Hard-blocked channels live in a C# constant rather than
guardrails.json: a parse error drops the allowlists to empty, which is
fail-closed, but would have lifted a block sourced from the same file.

Also fixes an inert timeout in CommandCollector: ReadToEnd() blocks until
the pipe closes, so WaitForExit(30000) was only reached once the process
had already exited and a hung command pinned its worker indefinitely.

Adds the first real CI gate. No workflow ran tests on a pull request — the
deploy workflows build without testing and build-agent.yml covered only
DecisionCore.Tests on manual dispatch — while trust/security-faq.md
already told customers automated tests run in CI. ci.yml now runs all six
suites on every PR and merge, plus a job that fails on generated-guardrail
drift, alongside CodeQL and Dependabot. OfficeBinaryWatcherTests is marked
SerialThreading; its bounded re-probe races the shared ThreadPool and
failed once under full-suite load.

Verified: 3101 backend, 1911 agent V2, 474 decision core, 44 dialog, 572
web, 447 MCP. The timeout fix is verified by runtime, not assertion — a
Start-Sleep 120 command now returns after ~30s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 00:28:04 +02:00