Drift auto-remediation
What it does
When the drift page detects that a customer's CA policy has drifted from the tenant's comparison baseline, Policytab can generate a draft change request that reverts supported drifted fields back to baseline. The change is not auto-applied - it still goes through the standard dry-run + approval workflow. There is no background worker; an operator chooses Revert to baseline from the drift UI.
The feature targets the four drift categories where Policytab can express the revert through an existing change kind: assignment, condition, grant, and application. Pure state reverts (enabled ↔ disabled) are also covered. Anything else (display name edits, session controls, deleted policies) is out of scope and still requires a manual change.
When to use it
- Customers with high-churn IT teams where direct-portal edits happen often and you want a fast revert path.
- Tenants where the baseline IS the source of truth - any drift is by definition a regression.
- Standard-tier MSPs running 50+ tenants who can't manually triage every drift event.
When not to use it:
- Tenants whose baseline is heavily customized at the tenant layer. The auto-remediator reverts to baseline, not to the customer-specific intent - you'd get noisy change requests for every drift that is actually intentional.
- During heavy migration periods where temporary drift is expected.
How to run it
- Sign in as Admin or Owner.
- Open the tenant Drift page.
- For each supported drift item, choose Revert to baseline (or use bulk actions where offered).
There is no per-tenant toggle - remediation is always operator-initiated.
What drift is detected
Drift is computed by compareTenantSnapshotToBaseline (see lib/baseline/compare-tenant.ts). Each policy in the live snapshot is matched to a baseline policy by code or display name, then field-by-field compared. A drift entry carries:
kind:missing,extra, ormodified(onlymodifiedis auto-remediable today)severity:critical,warning,infochangedFields: the set of normalized-intent fields that differ
The auto-remediator only acts on modified drift. missing would require creating a brand-new policy (the policy.create change kind exists but auto-create is intentionally out of scope - we don't want a runaway loop creating policies if the snapshot has a transient gap). extra is by definition BYO and not in the baseline at all.
How "Revert to baseline" works for non-state drift
The interesting case is non-state drift, where the change-management toolkit doesn't have a "set everything to baseline at once" operation. Auto-remediation handles this by emitting one change request per drifted category, all linked by a shared drift_remediation_group_id recorded in the audit log:
| Category | Emits | Reverts what |
|---|---|---|
state |
policy.state_change |
enabled / disabled / report-only |
assignment |
policy.assignment_change |
exclude-group add/remove deltas only when both sides are concrete Entra GUIDs |
condition |
policy.condition_change |
location add/remove deltas only when both sides are concrete Entra GUIDs; set platforms/clientAppTypes lists |
grant |
policy.grant_change |
set builtInControls list, operator, authentication strength id |
application |
policy.application_change |
include / exclude app id deltas |
A single drifted policy where assignment AND grant both differ produces two change requests, both stamped with the same drift_remediation_group_id. Audit reads can correlate the pair as a single remediation event.
The classifier short-circuits: if a category's changedFields flag is set but the field-level deep-equal says nothing actually differs (e.g. baseline omits the field), no change request is emitted for that category.
Reviewing and applying
For each auto-generated change request:
- Tenant detail → Drift → choose Revert to baseline for a supported policy/category, then open the created draft from Changes.
- Click into the change. The payload is fully populated; the diff is rendered as if you'd typed it.
- Dry-run. The standard warnings apply (
critical_policy_disable,grant_remove_mfa, etc.). If the auto-revert would itself trip a critical warning, you'll know. - Approve and apply (or Reject if the drift turns out to be intentional).
If require_approval is on (see Approval workflow), the standard four-eyes rule applies.
Troubleshooting
- I expected a change request and none appeared. Possible causes, in order:
- The drift category isn't in the supported list (e.g. session controls drifted but no other category did → no change request emitted).
- The drift's
kindismissingorextra(auto-remediator only acts onmodified). - The drift was a no-op at field level (the baseline omits a field that the live policy carries set to an empty array - these are equivalent, no change needed).
- Auto-remediation created a change with a critical warning. That's working as intended - the dry-run is the gate. The change still requires your explicit approval before apply. Review and decide.
- Multiple linked changes - can I approve them as a group? Not yet. Each change is its own row, with its own dry-run, its own approval. The shared
drift_remediation_group_idis for audit correlation, not bulk approval. - Auto-remediated change failed to apply. Same path as any apply failure - see Troubleshooting → apply fails. The auto-remediation didn't do anything special on apply; it only built the payload.
Audit log shape
Every auto-generated change emits a drift.auto_remediation.created audit row with:
resource = change_request:<id>payload.drifted_policy_id- the baseline policy codepayload.graph_policy_id- the live Entra object idpayload.category- one of the five categories abovepayload.drift_remediation_group_id- shared across linked changes,nullfor single-category remediations
The subsequent dry-run, approve, and apply events emit their own rows linked via resource = change_request:<id>. A fully audited auto-remediation cycle has four rows minimum.