Commit Graph

80 Commits

Author SHA1 Message Date
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
Oliver Kieselbach dbf6e50e2d feat(agent,web,shared): phase_exit gather trigger — one-shot collection at a phase's end
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>
2026-07-21 15:37:48 +02:00
Oliver Kieselbach a97888ebd5 fix(agent): neutral aggregated 2800 observations; reboot claim gated on observed reboot
Rework after the first real enrollment (session b2e890c1) exposed three
defects in a654c730:

- Shell-Core "RebootCoalescing" match was a false positive by construction:
  the token appears in the ROUTINE SubcategoryProcessing_Started bootstrap
  marker ("Starting subcategory DeviceSetup.RebootCoalescing...") on EVERY
  enrollment. esp_reboot_coalescing removed entirely (branch, constant,
  catalog, tests).
- 2800 reboot-required flags fire at every policy sync; only DeviceSetup-time
  flags cause the coalesced reboot. The session's flags fired during
  AccountSetup - no reboot - yet our Warning events claimed "device will
  restart". Raw event is now a neutral Info observation; the reboot CLAIM
  moved to ANALYZE-ESP-005 with a REQUIRED system_reboot_detected >= 1
  condition (no observed reboot -> advisory silent).
- One event per 2800 record was spam: records are now buffered and debounced
  (10 s) into ONE aggregated event per burst (rebootUris[]/uriCount/
  firstRebootUri/recordCount/unparsedCount). Watermark is persisted at FLUSH,
  not claim - a pre-emit reboot kill loses nothing, the post-restart backfill
  re-emits the burst at its historical time.

Verified 2800 description shape recorded in code, tests, and concept doc:
"The following URI has triggered a reboot: (./Device/...)."; extraction
covered by a real-text regression test. Lesson added to tasks/lessons.md
(observation-level events vs outcome-gated claims; substring tokens appear
in framework bookkeeping records).

Tests: Agent.V2.Core 1867/1867, Functions 3099/3099, MCP 400 passed,
solution build 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:41:47 +02:00
Oliver Kieselbach a654c7309e feat(agent): attribute mid-ESP coalesced reboots to MDM policy URIs (second sign-in detection)
Device-assigned MDM policies whose CSP URIs match the OS reboot-required
catalog force a coalesced reboot at the end of ESP DeviceSetup - the user
sees an unexpected restart and a second sign-in before AccountSetup.
Invisible in the Intune console; admins simply lived with it. Now attributed
and actionable (PMPC research).

- MdmRebootPolicyTracker (+host): watches DeviceManagement-Enterprise-
  Diagnostics-Provider/Admin EventID 2800, emits one mdm_policy_reboot_required
  per reboot-forcing URI (Warning, ImmediateUpload; tolerant EventData-first
  URI extraction with regex fallback; fail-soft OMADM RebootRequired probe).
  Cross-restart RecordId watermark (mdm-reboot-watermark.json) - mandatory,
  the coalesced reboot restarts the agent and the backfill re-reads the log.
- ShellCoreTracker: RebootCoalescing branch on the already-watched 62407
  stream -> esp_reboot_coalescing corroboration event; deliberate deviation:
  emits on backfill too (live emit races the reboot kill).
- ANALYZE-ESP-005 advisory (warning): names the first URI via {{rebootUri}},
  remediation = reassign profiles device -> user groups; +20 confidence on
  coalescing corroboration, +10 on >=2 URIs.
- Config: Collectors.MdmRebootPolicyWatcherEnabled (default ON, internal
  steering only - agent-native collector, NOT an IAgentAnalyzer, no admin UI),
  MdmRebootPolicyBackfillLookbackMinutes=60; ConfigVersion 35.
- Event types deliberately distinct from system_reboot_detected so the
  backend RebootCount (exact-string match, 2 sites) stays uninflated.
- Docs: docs/agent/mdm-reboot-coalescing.md (+index/log). Exact 2800/62407
  message texts are unverified until E2E VM validation (tolerant matching).

Tests: Agent.V2.Core 1868/1868 (21 new), Functions 3099/3099, MCP 400 passed
(event-types drift), solution build 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:05:11 +02:00
Oliver Kieselbach 21dd35f787 feat(backend,shared): gather-rule phase scoping + emit-mode fields, custom-rule toggle wipe fix
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>
2026-07-20 09:50:35 +02:00
Oliver Kieselbach 34edadf0db docs(rules): document observability limits of the ZTD known-issue rules
The agent is normally installed by an IME platform script AFTER MDM
enrollment succeeded, so pre-enrollment failures (enrollment blockers,
TPM attestation) are invisible in the normal flow — no enrollment means
no agent and no authenticable session. Await-enrollment mode does not
help either: the binary only polls for the MDM certificate and cannot
authenticate without it. Only bootstrap-token deployments (one pilot
customer) can report pre-MDM.

- New Observability section in docs/agent/autopilot-ztd-diagnostics.md
  with a per-rule visibility matrix (normal / bootstrap / await) and the
  follow-up option of backfilling Autopilot-channel error records
- ANALYZE-ENRL-003/004 descriptions state the limitation explicitly so
  portal users understand why these rules rarely fire

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 19:14:36 +02:00
Oliver Kieselbach cd870e639d feat(agent+rules): evidence-backed ZTD diagnostics for missing Autopilot profiles
Agent (autopilot_profile_missing evidence package):
- ZtdEvidence collector: one-shot all-level query of the ModernDeployment
  Autopilot event channel (807/809/815/908 errors + 100/153/160/161/163/164
  flow events) -> ztdVerdict field (device_not_registered /
  no_profile_assigned / download_attempted_no_profile_returned / ...);
  the continuous watcher can't supply this (filters out Info)
- Diagnostics\Autopilot registry dump (IsAutopilotDisabled, TenantMatched,
  CloudAssignedTenantDomain/Id) on every autopilot_profile event, flat
  evidence fields on the missing event
- ztd.dds.microsoft.com reachability probe (official endpoint per the
  requirements page - several blogs cite ztd.ssd.*, which is wrong);
  volatile probe fields excluded from the restart-dedup fingerprint;
  Intune diag-upload endpoint deliberately NOT probed (user decision)

Analyze rules (known-issue map from Microsoft Learn):
- ANALYZE-ENRL-003: TPM attestation known error codes (0x800705B4,
  0x801C03EA, 0x81039001/23/24, 0x80070490)
- ANALYZE-ENRL-004: enrollment-blocked known codes (0x80180014, 0x80180018,
  0xC1036501, 0x801C03F3, 0x80070774)
- ANALYZE-ENRL-005: hybrid-join 0x80004005 timeout with per-version KB fix
  builds (gated on isHybridJoin via confidence threshold)
- ANALYZE-DEV-007: NTP clock skew > 2min (documented TPM-attestation/ESP
  timeout cause) using existing ntp_time_check offsetSeconds

Docs: new OKF reference docs/agent/autopilot-ztd-diagnostics.md (event-ID
table, diagnostic registry, endpoints, error-code map, sources + re-check
RSS feed), linked from ZtdEvidence/ModernDeploymentTracker code comments,
indexed in docs/index.md + agent/index.md + log.md.

Tests: 11 ZtdEvidence verdict tests; full V2 suite 1799 green; rule schema
validated (ajv draft2020); backend rule tests 123 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 19:07:05 +02:00
Oliver Kieselbach fa03e290b2 feat(agent): os_build_changed detection + WU channel blind-spot census (session 7443317c)
Session 7443317c proved a WU-tracker blind spot: an OOBE quality update
installed (OsBuild 26200.8037 -> 26200.8655 across the mid-enrollment reboot)
yet the deployed WindowsUpdateTracker emitted ZERO events - the watcher armed
fine (no collector_degradation), the spool is durable, and two 60-min backfills
covered the install window, so the WindowsUpdateClient channel simply carried
none of the targeted EventIDs 19/20/43/44. The tenant has diagnostics upload
disabled, so the fix makes the feature self-evidencing instead of guessing IDs:

- OsBuildChangeHost: persists CurrentBuild.UBR (Registry64 view) to
  AgentState/os-build-state.json and emits os_build_changed (Info, immediate)
  when it differs across a restart - deterministic update corroboration that
  cannot be lost to timing, watermarks, or unexpected servicing paths. First
  run seeds silently; corrupt state re-seeds.
- WindowsUpdateTracker channel census: when the build provably changed AND zero
  targeted WU events were emitted this run, emit a one-shot
  windows_update_channel_census (Debug) with an unfiltered EventID histogram of
  the WindowsUpdateClient AND UpdateOrchestrator channels over the lookback
  window (capped at 5000 records, cap surfaced as censusTruncated). The
  histogram arrives via normal event upload and tells us which IDs to add to
  WindowsUpdateTargetedEventIds via remote config - no redeploy, no diag ZIP.
  Gated by CollectorConfiguration.WindowsUpdateChannelCensusEnabled (default
  on, model-default pattern, no ConfigVersion bump).
- No new hardcoded EventIDs (per the device-validation rule) - census data
  decides what to enable remotely.
- ANALYZE-DEV-006 "OS Build Changed During Enrollment" (info, not KO):
  interpolates {{previousBuild}}/{{currentBuild}}, counts the census as
  corroboration. Catalogs: Constants.cs + MCP resource-catalog.ts + web
  eventTypes.ts (drift-test enforced).

Tests: 6 OsBuildChangeHost (seed/same/changed/unreadable/corrupt/no-dir),
7 census (gating matrix, truncation surfaced, scanner-throw swallowed,
histogram format), 3 ANALYZE-DEV-006. Agent.V2.Core 1754/1754, Backend
2874/2874, MCP 379 (drift green), web tsc + vitest 367/367, sln 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:42:48 +02:00
Oliver Kieselbach 50295e8304 feat(agent): AppX/MSIX failure enrichment in ESP settle window (esp_appx_failure_analysis)
Session 2bc884b6: ESP failed AccountSetup/Apps with 0x80073cf9 while all 8
tracked Win32/WinGet apps succeeded — the failing app was a MSIX/Store app
that installs outside the IME Win32 pipeline and is invisible to
ImeLogTracker. The session ended as a blackbox failure.

When an Apps-subcategory failure arms the 30s settle window, the tracker now
queues a one-shot scan of Microsoft-Windows-AppXDeploymentServer/Operational
(Critical+Error, session window, 200-record/10s cap), scores candidate
packages (exact-HRESULT match > AppX-family error > time proximity, recency
bonus), and emits esp_appx_failure_analysis with flat RuleEngine fields plus
a capped candidates[] detail list — before enrollment_failed lands in the
timeline. Negative results are emitted too (no_appx_candidates points back
at Win32). Fire-once per session; scan runs lock-free on the threadpool;
settle expiry never waits; failures degrade to collector_degraded.

- New: AppxDeploymentFailureScanner (one-shot reader, injectable seam),
  AppxFailureAnalyzer (pure scoring/verdicts, incl. negative-decimal
  HRESULT + PackageFullName extraction from message/properties)
- ProvisioningStatusTracker: optional scanner/dispatcher ctor seams,
  MaybeStartAppxFailureScan at settle-arm time
- Rule ANALYZE-APP-014 (MSIX/Store app failure during ESP) keyed on
  verdict=appx_candidate_identified; dist regenerated
- MCP resource-catalog + web HRESULT badge + gather-rules eventTypes
- Tests: 16 analyzer + 7 trigger; settle-window fixture now injects a
  fake scanner + sync dispatcher (no real event log in tests)

Inert until agent redeploy (rule needs backend redeploy). Post-deploy
checks in tasks/todo.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:49:27 +02:00
Oliver Kieselbach 33618e183d feat(wu-oobe): detect & surface Windows Update during OOBE + self-maintaining rule sunset
Windows Update activity during Autopilot OOBE/ESP is a blind spot no other tool
(Intune console included) surfaces well — a cumulative update installing mid-OOBE
can break enrollment (r/Intune KB5095189). This makes it visible with secured
evidence.

Agent (net48):
- WindowsUpdateTracker + WindowsUpdateWatcherHost: watch
  Microsoft-Windows-WindowsUpdateClient/Operational (EventIDs 19/20/43/44 =
  success/failure/install-started/download-started), startup backfill (OOBE updates
  run before the agent starts), RecordId watermark dedup (restart boundary vs
  intra-run seen-set so a high-id live event can't hide older backfill events),
  HRESULT->symbol decode. Emits windows_update_succeeded/_failed/_started with the
  real event Timestamp. Default-on config knobs; uncertain EventIDs configurable
  without redeploy.
- RegistryCollector: emitOnlyIfExists (suppress positive-assertion events when the
  key is absent) + Registry64 base view by default (bitness-independent, matches
  TenantIdResolver) with optional registryView override.

Rules:
- ANALYZE-DEV-004 (high: WU failed during enrollment, high-insight not KO) +
  ANALYZE-DEV-005 (info: WU installed). reboot_pending corroboration matches
  event_data exists==true.
- GATHER-DEVICE-005 -> windows_update_reboot_pending (emitOnlyIfExists),
  GATHER-DEVICE-004 -> windows_update_history (Get-HotFix, opt-in).
- 5 EventTypes synced across Constants + MCP resource-catalog + web eventTypes.

Self-maintaining rule lifecycle (analyze + gather parity):
- Gather rules gain the sunset/orphan-GC that only analyze had
  (ProcessSunsetGatherRuleAsync + runtime filter + seed/reseed/github paths).
- RuleProvenance field (embedded/github, null=embedded back-compat): the embedded
  catalog only sunsets its own rules; GitHub-reseeded rules ahead of the deployed
  binary (new ids AND content updates to existing ids) are exempt and reclaimed to
  embedded once the binary catches up. No orphans, no operator knowledge required.

Verified: full solution builds; backend suite 2770 pass, agent suite 1638 pass;
rules AJV-valid + combined.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:22:41 +02:00
Oliver Kieselbach a70785fb42 feat(rules): built-in analyze-rule hygiene pass ahead of public docs catalog
- 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>
2026-07-06 12:32:32 +02:00
Oliver Kieselbach 44203423ec feat(rules): ANALYZE-ESP-004 soft-failure for ESP-timeout + Continue-anyway
Devices that hit the ESP terminal timeout in DeviceSetup on a profile that
allows "Continue anyway" are recorded as Failed even though the user most
likely dismissed the error and reached the desktop. The existing advisory-
defang only covers AccountSetup failures, so DeviceSetup timeouts (e.g. a
slow blocking app missing the 30-min wall) show up as hard failures.

Add a built-in, enabled-by-default analyze rule (severity: warning) keyed on
enrollment_failed.mayHaveContinuedAnyway==true. It surfaces the soft failure
and points at the root cause (slow blocking app / ESP timeout / detection
mismatch / masked certificate failure) without changing session lifetime or
the terminal status. No agent change required; ships via EmbeddedResource or
ReseedFromGitHub.

Tests: RuleEngineEspSoftFailureTests (fires on the continue-anyway shape,
stays silent on hard failures and when the flag is absent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 00:47:11 +02:00
Oliver Kieselbach 474af7f78c fix(agent): ESP-Apps failure correlation + platform-script stdout/exit contamination
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>
2026-06-22 20:37:44 +02:00
Oliver Kieselbach ecd8036dc0 updated guardrails 2026-06-18 17:40:07 +02:00
Oliver Kieselbach cf8fadfc17 fix(rules): drop AutoLogon false-positive on normal Autopilot enrollments
ANALYZE-SEC-002 fired Warning (confidence 90) on every normal user-driven
Entra-join enrollment. During ESP, Windows writes AutoAdminLogon=1 +
DefaultUserName + AutoLogonSID + no DefaultPassword for the enrolling user —
the exact fingerprint the sysinternals_autologon_suspected heuristic targeted
(enabled && user && !password). AutoLogon-enabled is registry-indistinguishable
from a Sysinternals/kiosk setup during Autopilot, so the heuristic and SEC-002
could never tell a benign enrollment from a real one.

Declare AutoLogon only when a plaintext password is actually present:
- delete ANALYZE-SEC-002 (broad AutoLogon-active warning)
- remove the non-discriminating sysinternals heuristic from AutoLogonAnalyzer
  (computation + finding label + checks.sysinternals_autologon_suspected)
- strip the sysinternals condition+factor from SEC-004 (kiosk allow-list, still
  a disabled opt-in template) and refresh its now-stale SEC-002 guidance
- ANALYZE-SEC-003 (plaintext DefaultPassword -> high) is now the sole default-on
  AutoLogon grade; the autologon_analysis event still carries all raw facts at Info

Tests: assert SEC-002 absent + no rule references the sysinternals signal; add
analyzer regression guard for enabled+user+no-password. V2-only, no ConfigVersion
bump. Build 0 err, AutoLogon 19/19, BuiltInRulesTests 8/8, web tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:06:53 +02:00
Oliver Kieselbach dbfdc9b239 feat(agent): event-driven Office (M365 Apps C2R) install detector + DO telemetry
Surfaces the real Microsoft 365 Apps Click-to-Run install lifecycle that the
Intune "integrated" app hides (IME reports done in 1-2 min while C2R keeps
streaming in the background). V2-only.

Event-driven, no idle polling:
- OfficeProcessWatcher: WMI Win32_ProcessStartTrace push on OfficeC2RClient.exe
  (+ startup probe) for start; Process.Exited for stop, debounced by a settle
  window (OfficeInstallSettleSeconds, default 10s) so short worker gaps in one
  install don't trigger a premature office_install_failed.
- RegistryChangeWatcher: RegNotifyChangeKeyValue push on ...\Office\ClickToRun
  (Configuration value + Scenario subkey churn); bootstrap fallback watches the
  nearest existing ancestor non-recursive + name-only until ClickToRun appears.
- OfficeInstallDetector: pure state machine (Idle->Active->Terminal), emits
  office_install_started/progress/completed/failed (progress on real change only,
  no heartbeat). Error code only from non-zero numeric/hex Scenario values.

Office Delivery Optimization:
- DeliveryOptimizationCollector extended with a 2nd wake source (Office active),
  classifies non-IME Office-CDN jobs and folds aggregated DO bytes/peers/real
  download-% into the office_install_* events (not emitted as download_progress
  to avoid a phantom app in the backend AppInstallSummary).

Plumbing: office_install_* event-type constants, EnableOfficeInstallDetector +
OfficeInstallSettleSeconds config (ConfigVersion 28->29), MCP + web event-type
catalogs, ANALYZE-OFFICE-001 (office_install_failed -> Warning). 22 detector unit
tests; full V2 suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:17:53 +02:00
Oliver Kieselbach 425502c419 feat(ppkg): array-aware analyze rules + allow-list + size-chunked scan event
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>
2026-06-07 15:14:51 +02:00
Oliver Kieselbach 01fac29bcb feat(agent): AutoLogon detector + analyze rules (V2)
V2-only AutoLogonAnalyzer that reads the Winlogon AutoLogon configuration and
emits an autologon_analysis event with raw facts at Info severity. AutoLogon can
be a legitimate kiosk/shared-device setup or a way to automate enrollment/OOBE;
the agent reports facts only and never reads the DefaultPassword value (presence
detected via value-name enumeration). Backend analyze-rules grade the facts.

- AutoLogonAnalyzer (always-on, no tenant toggle): startup no-op, runs at
  DeviceSetup-phase completion (new AutoLogonDeviceSetupTrigger on
  EspAndHelloHost.DeviceSetupProvisioningComplete) and at final shutdown.
- Sysinternals Autologon heuristic (AutoLogon enabled + default user + no
  registry password = LSA-secret credential); never reads the LSA secret.
- Analyze rules: ANALYZE-SEC-002 (AutoLogon enabled -> warning, default-on),
  ANALYZE-SEC-003 (plaintext DefaultPassword -> high), ANALYZE-SEC-004 (kiosk
  allow-list template). Sysinternals graded via a real confidence condition
  inside SEC-002/004 so the allow-list still suppresses approved kiosks.
- autologon_analysis added to all event-type catalogs; rule-builder picker also
  gains the previously-missing integrity_bypass_analysis + local_admin_analysis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:09:52 +02:00
Oliver Kieselbach 2900b35256 feat(agent): provisioning-package (PPKG) detector + analyze rules (V2)
V2 PPKG scanner that runs once at DeviceSetup-phase start and reports raw facts
only — .ppkg file presence, Provisioning\Packages registry metadata,
Recovery\Customizations residue, and best-effort per-package content indicators.
PPKGs can come from legitimate bulk enrollment or be a pre/in-enrollment
manipulation vector; intent is judged by backend analyze-rules, not the agent.

- ProvisioningPackageCollector + ProvisioningPackageHost, wired as a kernel host
  in DefaultComponentFactory (one-shot at DeviceSetup start, not at agent start).
- Events: provisioning_package_scan (aggregate) + provisioning_package_detected
  (one scalar event per artifact so analyze rules can match per package).
- Analyze rules ANALYZE-SEC-005/006 incl. a per-tenant allow-list template.
- Event-type catalogs kept in sync (Constants.EventTypes, MCP resource-catalog,
  rule-builder eventTypes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:06:08 +02:00
Oliver Kieselbach 9855860f1c feat(mcp): interpolate {{token}} rule-template placeholders in analysis output
MCP-side mirror of the web interpolateRuleTemplate: resolve
{{reason}}/{{appName}}/{{errorCode}}/... from each rule result's
matchedConditions before get_session_details and get_session_summary
ship the explanation + remediation text, so MCP tool output never leaks
raw {{...}} placeholders. Resolution order is kept in lock-step with the
web copy (byField -> whitelisted auto-field -> signal name).

Also rewords the ANALYZE-ENRL-001 footnote to explain that a remaining
literal {{...}} means the field simply was not recorded for this session
(version 1.1.0 -> 1.1.1; compiled dist and the rule-version assertion in
RuleEngineDetectionFailureTests updated to match).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:40:09 +02:00
Oliver Kieselbach 3318467857 fix(sessions): emit terminal session_timeout event so 5h-timeout sessions analyze
The maintenance sweep graduated a stalled session to terminal Failed via the repo
(MaintenanceService Stage 2, SessionTimeoutHours=5h default) but wrote no event into
the session stream and never enqueued analyze. The analyze pipeline fires on stream
events, so a server-timed-out session analyzed to totalIssues:0 — unlike the agent
max-lifetime path, which emits a real enrollment_failed and is correctly analyzed.

- New server-authored event type Constants.EventTypes.SessionTimeout = "session_timeout"
  (+ MCP termination_events catalog; drift test green).
- MaintenanceService Stage 2 now captures the UpdateSessionStatusAsync result and, only
  on the real first transition (idempotent — the sweep never re-collects a Failed
  session), emits a session_timeout event (new pure static BuildSessionTimeoutEvent:
  Sequence = max(session events)+1 so it sorts last, Phase=Unknown, Source=System.
  Maintenance, Severity=Error) and enqueues auto-analyze with ReasonEnrollmentFailed.
  Best-effort try/catch — never blocks the timeout transition. Producer injected.
- RuleEngine: EvaluatePrecondition gains a pure event-type-presence gate (no-dataField
  exists/not_exists) — additive (no rule used dataField-less preconditions), enables a
  session-level suppression with no shared join field.
- New rule ANALYZE-ENRL-002 "Session Timed Out": fires on session_timeout, suppressed
  when enrollment_complete OR enrollment_failed exists (5h<6h late-completion race +
  no double-report with ANALYZE-ENRL-001). dist regenerated via combine.js.
- Web: session_timeout treated as terminal for stale-status refetch (the sweep sends
  no SignalR delta).

Tests: +3 RuleEngine pure-type precondition, SessionTimeoutRuleTests (real shipped rule
fire/suppress), MaintenanceServiceSessionTimeoutEventTests (shape/sequence). Backend
2168 pass, web 434 pass, MCP 177 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:09:39 +02:00
Oliver Kieselbach dff5ba65f4 fix(rules): sunset = delete, with cross-tenant orphan RuleState GC
Session 080edee9 follow-up — the customer screenshots showed ANALYZE-CORR-001
("Slow Network") and ANALYZE-APP-009 ("App Download Stalled") still firing
on a clean re-analyze, despite my previous fix shipping them as
enabled:false in the JSON. Investigation via direct DB query:

- AnalyzeRules/global rows for both: Enabled=false ✓ (version-bump landed)
- RuleStates/{tenant} rows for both: Enabled=true ✗ (orphan overrides
  from the user toggling them in the portal UI on 2026-03-09)

[AnalyzeRuleService.cs:60](src/Backend/AutopilotMonitor.Functions/Services/AnalyzeRuleService.cs#L60)
overlays per-tenant RuleState on top of the global Enabled, so the orphans
kept resurrecting the rules. No reseed path was cleaning them up.

The right semantic is "sunset = remove from catalog", not "ship as
enabled:false and hope nobody overrides". Three coordinated changes:

1. Both sunset rules deleted from the rules/analyze/ source tree (they
   never had a working condition shape — CORR-001 didn't actually check
   download speeds, APP-009 matched the V2 termination handler's
   post-mortem-synthetic download_progress:failed event).

2. New IRuleRepository.DeleteRuleStatesForRuleIdAcrossTenantsAsync
   cross-partition cleanup primitive — scans RuleStates by RowKey, deletes
   matching rows across every tenant partition.

3. Both reseed paths now do orphan-state GC:
   - AnalyzeRuleService.EnsureBuiltInRulesSeededAsync (on-startup):
     detects rules that exist in DB as built-in but aren't in
     BuiltInAnalyzeRules.GetAll() anymore → deletes the global row +
     fans out the per-tenant RuleState GC.
   - AnalyzeRuleService.ReseedBuiltInRulesAsync (delete+reimport): same
     diff-based detection on the pre-delete snapshot.
   - ReseedFromGitHubFunction.ReseedAnalyzeAsync (admin "Reseed from
     GitHub" button — the path the user actually clicks): mirrored
     identical logic so every reseed flavor leaves the same DB invariant.

4. ReseedBuiltInRulesAsync return shape extended to
   (deleted, written, orphanStatesGcd) so the admin UI can surface the
   cleanup count to operators.

Coverage:
- RuleEngineDetectionFailureTests: the two version-bump assertions for
  CORR-001/APP-009 are replaced with `DoesNotContain` against the
  built-in catalog (sunset semantic).
- AnalyzeRuleServiceSunsetTests (new, 3 tests): pins EnsureSeed orphan-GC
  invariant, asserts live-rule states are NOT touched, and pins the
  ReseedBuiltIn (deleted, written, orphanStatesGcd) tuple shape.

After deploy + a single GetAllRulesForTenantAsync call (any analyze run
triggers it), the orphan RuleStates for sunset rules are automatically
cleaned. The user's tenant 5ca2b350-… will see CORR-001 + APP-009
disappear from the Rules UI on the next analyze cycle.

Backend 2073/2073.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:31:32 +02:00
Oliver Kieselbach 8e26eb15e6 fix(rules): make detection-failure result actionable for end users
Session 080edee9 follow-up — the analyze cards the user saw on re-analysis
were neither accurate (Slow Network / App Download Stalled both fired as
false positives) nor specific (Enrollment Failed never named the offending
app; ANALYZE-APP-013 never fired). Five coordinated changes, all
version-bumped so EnsureBuiltInRulesSeededAsync re-imports them on the next
backend boot — the title/severity/version-check filter does not pick up
condition-only diffs:

ANALYZE-APP-013 v1.1.0 — fire on legacy + new agent shape:
  Previous rule keyed on `failureType == esp_apps_detection_failure`,
  which is only emitted by agents that shipped AFTER 4dd050e0. Historical
  sessions (and any agent on the old build) won't match.
  Replace with a precondition + condition pair that works for BOTH:
    * precondition: esp_failure_settle_started.errorCode == 0x87d1041c
      (V2 ProvisioningStatusTracker emits this on every Apps subcategory
      failure with that HRESULT, regardless of agent version)
    * required: app_install_failed.detectionResult == NotDetected
      (ImeLogTracker sets this from the IME's DetectionActionHandler line
      across all V2 agents)
    * optional: esp_failure_settle_started.errorCode (for {{errorCode}}
      interpolation; appName comes via auto-field from the required match)

ANALYZE-ENRL-001 v1.1.0 — restore appName + cross-link to APP-013:
  My previous trim dropped the failed_app condition on the theory that the
  auto-field path would carry it. That was wrong — auto-fields come from
  the FIRST matched condition's evidence, which here is `enrollment_failed`
  (no appName). Add back the optional failed_app condition with
  dataField=appName so byField resolves {{appName}}. Explanation now leads
  with the failed app, lists reason/subcategory/errorCode, and cross-links
  to the actionable ANALYZE-APP-013 instead of "see other rule results".

ANALYZE-CORR-001 v1.1.0 ("Slow Network") — version bump for DB sync:
  Was already enabled:false in source but the DB still had it enabled (the
  seed mechanism only resyncs on title/severity/trigger/version diff, so
  the source `enabled` flip never propagated). Bump version → next deploy
  resyncs. Also adds a suppressByEvent on the app_failed condition so a
  later app_install_completed for the same appId suppresses the match (a
  retry that succeeded shouldn't count as failure).

ANALYZE-APP-009 v1.1.0 ("App Download Stalled") — same version bump for
DB sync + documents why a status-aware suppression isn't expressible yet:
  The V2 EnrollmentTerminationHandler synthesizes
  `download_progress: failed` events for promoted-stuck apps AFTER the
  real `download_progress: 100%` event already landed. The engine's
  suppressByEvent can't filter on a third event's status field, so
  status-aware suppression has to wait for either an engine extension or
  a dedicated `download_stalled` event type.

RuleEngineDetectionFailureTests — 6 new tests:
  * APP-013 fires on the historical 080edee9 event shape
  * APP-013 does NOT fire for non-Apps subcategory HRESULTs
  * APP-013 does NOT fire without esp_failure_settle_started (precondition)
  * ENRL-001 MatchedConditions carries appName + errorCode + subcategory
    + reason from three distinct event types (interpolator material)
  * CORR-001 + APP-009 shipped as disabled

Coverage: 2064/2064 backend, 413/413 web.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:53:02 +02:00
Oliver Kieselbach 6007b9ecc5 fix(rules,web): pin rule template interpolation to the required-condition event
Codex review (P2): ANALYZE-APP-013's `failed_app_name` + `detection_hresult`
optional conditions scanned all app_install_failed events independently of
the required condition. In a session with multiple app failures the
template could interpolate {{appName}} / {{errorCode}} from a different
event than the one that matched `failureType == esp_apps_detection_failure`.

Two coordinated changes:

1. `interpolateRuleTemplate` gains an "auto-field" resolution tier between
   the explicit byField path and the bySignal fallback. The backend's
   `RuleEngine.AddDataFieldsToEvidence` already injects a whitelist of
   identifying fields (appId, appName, errorPatternId, errorCode, exitCode,
   status) onto every matched-condition evidence dict — the interpolator
   now reads those, pinning to the FIRST matched condition (= the required
   condition) so a rule with a single required match resolves all four
   tokens against the same source event.

2. ANALYZE-APP-013 is trimmed to a single required condition; the redundant
   optional conditions for appName / errorCode are removed because the
   auto-field tier already exposes them from the required match. Same
   pattern applied to ANALYZE-ENRL-001's `failed_app` condition.

Coverage: 4 new vitest cases (multi-failure correct interpolation, first-
condition-wins shadowing, explicit-field precedence, internal-field leakage
guard). 413/413 web tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:10:03 +02:00
Oliver Kieselbach db7ca6ed87 fix(rules): surface detection-failure root cause in analyze pipeline
Session 080edee9 fired only ANALYZE-ENRL-001 ("Enrollment Failed") with a
generic explanation, even though we already had all the data to point at
the specific failed app (Microsoft Apps for Enterprise) and the specific
HRESULT (0x87D1041C - Intune detection rule mismatch). Three coordinated
rule changes close that enrichment gap:

ANALYZE-APP-001 (Win32 App Detection Script Failure):
  Switch from a regex on the `message` field (which never matched the
  agent's "Install status unconfirmed - ESP timed out" wording) to a
  direct check on `dataField: detectionResult, operator: equals,
  value: NotDetected`. The rule engine already supports nested DataJson
  fields on `event_type` conditions, so this is the right shape and the
  rule now fires whenever the IME reported a detection-rule mismatch.

ANALYZE-APP-013 (App Detection Failure During ESP - 0x87D1041C, new):
  Matches the canonical `failureType: esp_apps_detection_failure` tag
  emitted by the V2 agent's new classifier. Severity=critical, baseConf=90.
  Pulls failed appName + HRESULT into the explanation via the existing
  `{{token}}` interpolator, and ships a three-step remediation plan:
  (1) validate the detection rule against an installed reference device,
  (2) investigate post-install async tasks (the install may complete only
  after ESP's detection probe runs), (3) cross-check the per-app event's
  detectionResult + bytesDownloaded + hresultFromWin32.

ANALYZE-ENRL-001 (Enrollment Failed):
  Add optional conditions that capture {reason, failedSubcategory, errorCode,
  failedApp.appName} into MatchedConditions so the explanation can resolve
  `{{reason}}` / `{{failedSubcategory}}` / `{{errorCode}}` / `{{appName}}`
  via the existing `interpolateRuleTemplate` engine. Remediation text now
  cross-references ANALYZE-APP-* rules for the actionable root cause.

dist regenerated via `node rules/scripts/combine.js` (25 -> 26 rules).
Backend embeds dist/analyze-rules.json as a resource; next deploy picks up
the new content automatically.

Coverage: 2058/2058 backend tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 13:41:48 +02:00
Oliver Kieselbach 2cf35eca5b chore(rules,agent): allowlist RealmJoin registry prefixes + bump build counters (Phase E)
- rules/guardrails.json: add new "RealmJoin" category covering
  SYSTEM\CurrentControlSet\Services\realmjoin and SOFTWARE\RealmJoin. Hive-agnostic
  match covers HKLM\SYSTEM\..., HKLM\SOFTWARE\RealmJoin, and HKU\<sid>\SOFTWARE\RealmJoin
  so operator-authored Gather-rules can probe the RJ keys (the RealmJoinWatcher
  itself bypasses GatherRuleGuards and watches the registry directly).
- src/Web/autopilot-monitor-web/lib/guardrails.generated.ts: regenerated via
  node rules/scripts/combine.js so the Web admin UI surface matches the new
  registry allowlist.
- buildcounter.txt (V2 + V1): auto-incremented by the Phase A-D builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:39:02 +02:00
Oliver Kieselbach 76284cfcb2 fix(agent/v2,rules): two-stage health-script emission to close short-session timing gap
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>
2026-05-12 00:58:21 +02:00
Oliver Kieselbach 527e36b957 fix(rules/schema): allow healthScriptResult action in ime-log-pattern enum
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>
2026-05-11 22:41:19 +02:00
Oliver Kieselbach 594b3906ce feat(agent/v2,web): capture full health-script lifecycle with phase-aware semantics + live indicator
Replaces the line-by-line HS-COMPLIANCE / HS-EXITCODE / HS-STDOUT / HS-STDERR /
HS-RUN-CONTEXT pattern set (only ever captured the pre-detection phase, reset state
mid-cycle) with a single atomic source: HS-NEW-RESULT matches IME's "[HS] new result =
{JSON}" line and emits up to three script_completed events per policy run (pre-detection
/ remediation / post-detection). HS-SCRIPT-START now also fires a live script_started
event so the UI shows a "running" indicator immediately, before the consolidated final
result lands ~30s-3min later.

Phase-aware failure semantics: detection / post-detection scripts use exit code as a
compliance verdict (not a crash signal), so a non-compliant detection that gets
remediated no longer counts as a "failed" event in metrics — the row renders with
amber styling via isNonCompliantReport(). Only the remediation phase + platform
scripts route non-zero exit to script_failed.

Defensive against wire-format drift: regex uses [\s\S]* with end-anchor, JSON parsed
with try/catch + counter, frontend toNumber() accepts both numeric and string fields
(backend serializes ints as strings via Dictionary<string,string>). Reducer dedupes
with timestamp included so repeated runs of the same policy in long sessions don't
collapse into one row.

V2 only — V1 (Agent.Core) phasing out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:36:49 +02:00
Oliver Kieselbach e88c825b56 feat(agent,rules): VM detection on hardware_spec + ANALYZE-SEC-001 skip on VMs
Adds a coarse virtual-machine flag to every hardware_spec event so the rule
engine's preconditions feature can filter device-specific rules. Wires the
new flag into ANALYZE-SEC-001 (Secure Boot UEFI CA 2023): the rule no longer
fires on Hyper-V / VMware / VirtualBox / Xen / QEMU / Parallels / KVM
guests, where the firmware DB is host-controlled and the inside-the-VM
UEFICA2023Status registry value is not actionable.

Agent (V1 + V2)
- Centralized VM detection in DeviceInfoProvider.IsVirtualMachine(mfr, model)
  alongside the existing GetManufacturer / GetModel helpers (which already
  handle Lenovo's Win32_ComputerSystemProduct.Version quirk). Reused, not
  duplicated — same source of truth as SessionRegistration / DiagnosticsPackage
  / GatherRulesMode.
- New CollectSystemInfo step in DeviceInfoCollector.HardwareSpec emits
  systemManufacturer, systemModel, isVirtualMachine onto the hardware_spec
  event. No new WMI query — calls into DeviceInfoProvider.
- Conservative bias: Microsoft Corporation manufacturer is shared between
  Hyper-V VMs and physical Surface devices, disambiguated via model
  ("Virtual Machine" → VM, "Surface Laptop 5" → physical).

Rule
- ANALYZE-SEC-001 gains a precondition (equals isVirtualMachine=false) and
  bumps to v1.1.0 so EnsureBuiltInRulesSeededAsync re-saves it for tenants
  on next backend cold start.
- Old agents (no isVirtualMachine field on hardware_spec) → fail-closed →
  rule is silently skipped on those devices until the agent updates. Safe
  default for a security rule.

Tests
- DeviceInfoProviderVmDetectionTests (V1 + V2 mirror, 18 cases each):
  Hyper-V / Cloud PC / VMware / VirtualBox / Xen / QEMU / Parallels / KVM
  positive + Surface Laptop/Pro/Book + Lenovo / Dell / HP negative + null/empty
  edge cases. Pinned at the central DeviceInfoProvider so all consumers benefit.
- 781/781 backend, 525/525 V1 agent, 800/800 V2 agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:35:00 +02:00
Oliver Kieselbach 5abdcb6eee feat(rules): preconditions gate — skip rules silently on non-matching device facts
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>
2026-04-28 23:18:40 +02:00
Oliver Kieselbach bba5761d14 refactor(v2): app_tracking_summary back to flat V1 schema
V2 had drifted to a renamed bucket schema (installedApps/failedApps/...)
plus nested perApp/byPhase. The nested objects were dead — no Web
component or rule consumed them — and the renamed counters needed a
fallback chain in every reader. Single flat schema is simpler:

- Builder rewritten to emit only the flat V1 keys: installed/failed/
  skipped/postponed (final), downloading/installing/pending (live),
  errorCount/deviceErrors/userErrors/hasErrors/isAllCompleted/
  ignoredCount (aggregate helpers), plus name lists per bucket
  (installedNames/failedNames/skippedNames/postponedNames/pendingNames).
  perApp + byPhase removed. The includePerAppDetail flag is gone — all
  emits ship the same shape (live snapshots stay cheap because the
  names lists are short strings, not per-app timing objects).

- ignoredCount needed a separate accessor: it lives on the live
  AppPackageStateList, not on the deduped phase-snapshot+live union
  used by the termination accessor. New ImeIgnoredCount property on
  DefaultComponentFactory + ignoredCountAccessor parameter on
  EnrollmentTerminationHandler.ctor (default () => 0 keeps existing
  test rigs unaffected).

- ANALYZE-APP-007.dataField back to errorCount (flat), dist regenerated.

- Web hooks (useSessionDerivedData, useProgressDerivedData) drop the
  V2 fallback chain — single-schema reads only.

- Tests rewritten for V1 schema: counter validation, device/user
  error split, hasErrors/isAllCompleted bool helpers, name-list
  population per bucket. EnrollmentTerminationHandlerTests collapsed
  the three V2-perApp tests into one V1-counts+split-errors test;
  per-app timing detail now lives only in FinalStatusBuilder /
  final-status.json, not in the event payload.

Per-transition emit cadence and ImmediateUpload=false on live
snapshots are unchanged from the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:39:09 +02:00
Oliver Kieselbach d295eea390 fix(rules): align ANALYZE-APP-007 + SEC-001 with V2 emit + flat data lookup
ANALYZE-APP-007: dataField errorCount -> failedApps. V1 emitted errorCount
in app_tracking_summary; V2 emits failedApps. Pulling the rule to V2-naming
removes the V1-compat alias debt from the agent.

ANALYZE-SEC-001: drop the data. prefix from dataField. RuleEngine's
GetDataFieldValue does flat dictionary lookup; the data. prefix matched
nothing under V1 or V2. Both V1 and V2 emit uefiCA2023Status /
uefiSecureBootEnabled at the root of Data, so the rule was a long-standing
no-match.

Regenerated rules/dist/analyze-rules.json from the source files via
rules/scripts/combine.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:55:05 +02:00
Oliver Kieselbach 4da1540f8c added hardware hash command to guardrails 2026-04-15 01:46:32 +02:00
Oliver Kieselbach 45edbab0e1 feat: enhanced analyze rule engine (in not_in support) MarkSessionAsFailed support, new ESP rule for cert errors 2026-04-14 21:46:20 +02:00
okieselbach 32bc9da60c chore: regenerate rules/dist from individual files 2026-04-10 16:13:38 +00:00
Oliver Kieselbach 625809423f Better app failure detection and reporting 2026-04-10 18:12:30 +02:00
Oliver Kieselbach 3e7f66e8ea added app dashboard 2026-04-07 15:57:45 +02:00
Oliver Kieselbach 3a5bf83b7c new seed file 2026-04-07 09:47:59 +02:00
Oliver Kieselbach 6a8a916d3c extended schema with new values 2026-04-06 17:56:11 +02:00
Oliver Kieselbach 860d00c017 added better error code handling and mapping 2026-04-06 17:53:55 +02:00
Oliver Kieselbach e6390db5be better diagnosics package collection 2026-04-05 17:19:22 +02:00
Oliver Kieselbach 0976a6e45e new template analyze rule fix 2026-04-03 20:05:19 +02:00
Oliver Kieselbach ed0e19fa36 added template vars to analyze rules for better user journey 2026-04-03 19:52:58 +02:00
Oliver Kieselbach e19852cd9a add structured error code extraction from IME log patterns
The backend already reads errorCode from event data for FailureCode and
failure reason formatting, but the agent never populated it. This change
extracts error codes (exit codes, HRESULT) from named regex capture groups
in IME log error patterns and sends them as structured errorCode fields.

Also fixes a pre-existing bug where ErrorPatternId/ErrorDetail were not
persisted across agent restarts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:56:00 +02:00
Oliver Kieselbach eaba031b82 fix multiline script output matching in PS-AGENT-OUTPUT/ERROR patterns
.* doesn't match newlines without Singleline flag — script stdout/stderr
from AgentExecutor.log spans multiple lines and was never captured.
Changed to [\s\S]*? to match across newlines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:25:53 +02:00
Oliver Kieselbach 7763563c76 improved matching with exclude pattern 2026-04-02 23:13:21 +02:00
Oliver Kieselbach 743d7b3372 fixex for vulnerability matching (respect platform, wildcard matches with low confidance flag now, mismatched secure client, updated seed file) 2026-04-02 21:36:57 +02:00
Claude e3a99e519a Fix CVE version filtering to exclude already-fixed vulnerabilities
The FilterByVersion wildcard logic was treating CVEs with specific
versions in their CPE URI as affecting ALL versions. Now extracts the
version from the CPE 2.3 URI and compares it against the installed
version, so only truly relevant CVEs are shown.

Also adds missing CPE seed mappings for common Intune/Autopilot
managed software: Intune Management Extension, Company Portal,
ODBC/OLE DB drivers, Remote Desktop, Azure CLI.

https://claude.ai/code/session_01Kv2aAEVqbeFSsyPv7gU7pF
2026-03-28 18:03:29 +00:00
Oliver Kieselbach f6ed4660d1 agent: secureboot certs incl. analyze rule, timezone auto set, NTP offset check 2026-03-26 09:23:12 +01:00