The post-PR-HS1 single-source design (HS-NEW-RESULT JSON only) had a real timing gap:
IME emits the [HS] new result = JSON line only AFTER its batch send to the Microsoft
service, ~30-90 s after the actual script ran (verified in session c1e714e6: 34-74 s
gap per policy). Short Autopilot enrollments often end inside that window, so health-
script rows were entirely missing from completed sessions even though IME had executed
the scripts.
Two-stage approach (matches original V1 timing for the early signal, while keeping
HS-NEW-RESULT as the canonical truth):
- Re-add HS-COMPLIANCE pattern matching BOTH `pre-remdiation` AND `post-remdiation`
detection compliance lines → HandleHealthScriptDetectionResult emits a script_completed
event immediately. Single regex (`(?<part>pre|post)-remdiation`) covers both phases.
- Re-add HS-RUN-CONTEXT / HS-EXITCODE / HS-STDOUT / HS-STDERR patterns feeding a
single-slot `_pendingHealthScript` accumulator (IME runs health scripts sequentially
within a session — verified). HS-COMPLIANCE merges the slot data onto the early-signal
event so exit / stdout / stderr / runContext are populated from the start, not just
the inferred compliance verdict.
- HS-NEW-RESULT continues to fire later with the full JSON (RemediationStatus,
TargetType, ErrorCode, plus the actual remediation phase's data). UI reducer's
dataCompleteness() scoring keeps the more complete entry — early-signal row gets
enriched in place, no duplicate cards.
Remediation phase itself has no early-signal source in IME logs (only the late JSON
exposes Info.RemediationExitCode and RemediationScriptOutputDetails). A follow-up
based on the agentexecutor invocation line's _RemediationScript.output suffix is
possible once we have a real non-compliant cycle log to validate against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI ajv validation rejected HS-NEW-RESULT.json because the new healthScriptResult
action wasn't in the schema's enum allowlist. Add it next to the other script*
actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
.* 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>
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
- Galactic Admin Mode for Audit Logs: banner + cross-tenant endpoint (galactic/audit/logs)
- Audit page pagination (12 entries per page) with Previous/Next controls
- Platform Metrics: moved computation to backend PlatformMetricsService with 5-min cache
- New endpoint GET /api/galactic/metrics/platform replaces N+1 client-side fetches
- IME log tracker enhancements, DO telemetry pattern, and download progress improvements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rules/dist/ was excluded by the global dist/ .gitignore pattern.
Added !rules/dist/ exception so the combine-rules workflow can
commit the generated files. Also adds the initial dist files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>