BXUP Track progress & test yourself — free
IT Support / Service Desk / Sysadmin · Hands-on lab · ~5 min read
IT SUPPORT / SERVICE DESK / SYSADMIN · HANDS-ON LAB

Lab 07 · Bulk Ops with Dry-Run Discipline

Execute a CSV-driven group restructure with the full bulk-operations protocol — input validation, reviewed dry run, generated rollback file, console-truth verification — and catch the two malformed rows and the one entry that would strand a user's access before anything runs.

Rohan administers Entra ID for a Bengaluru fintech that is renaming its department groups after a reorg. The change request looks trivial: move ~20 users from old GG-* groups to new SEC-* groups, per a CSV that HR's coordinator exported. Rohan has watched a colleague run exactly this kind of "trivial" bulk change and lock the finance team out of their own share for half a day. The CSV in this lab has three problems in it — two rows that are structurally broken and one that is structurally fine but operationally wrong. The malformed rows will announce themselves to any validator. The stranding row will not: only a human asking "what does this user end up with?" catches it.

You will run this against a sandbox — a free Entra test tenant, an AD lab VM, or, fully offline, a mock module provided below that mimics the cmdlets. The discipline is identical in all three.

Time & tier

~60 minutes hands-on · Core tier (maps to module M4). $0 — PowerShell 7 plus any AI chat. Sandbox options: a free Microsoft 365 Developer/Entra trial tenant with Graph PowerShell, a local AD VM, or the offline mock (no tenant needed).

Setup

Save the change request as restructure.csv:

action,upn,source_group,target_group
move,c.rakesh@fincorp.test,GG-Ops,SEC-Ops
move,a.priya@fincorp.test,GG-Ops,SEC-Ops
move,d.kumar@fincorp.test,GG-Finance,SEC-Finance
move,s.leela@fincorp.test,GG-Finance,SEC-Finance
move,v.arun@fincorp.test,GG-Finance,SEC-Finance
move,m.divya@fincorp.test,GG-HR,SEC-HR
move,j.thomas@fincorp.test,GG-HR,SEC-HR
move,r.iyer@fincorp.test,GG-Sales,SEC-Sales
move,k.bhat@fincorp.test,GG-Sales,SEC-Sales
move,n.sharma@fincorp.test,GG-Sales,SEC-Sales
move,t.george,GG-Sales,SEC-Sales
remove,p.sen@fincorp.test,GG-Finance,
move,b.reddy@fincorp.test,GG-Ops,SEC-Ops
move,h.mehta@fincorp.test,GG-Ops,SEC-Ops
move,s.kaur@fincorp.test,GG-HR,SEC-HR
move,l.singh@fincorp.test,GG-HR,
move,g.pillai@fincorp.test,GG-Sales,SEC-Sales
move,v.gupta@fincorp.test,GG-Ops,SEC-Ops
move,e.dsouza@fincorp.test,GG-Exec,SEC-Exec
move,a.naik@fincorp.test,GG-Finance,SEC-Finance

Context you have from the change ticket: GG-Finance grants the Finance share and the AP invoice system; its replacement is SEC-Finance. P.Sen is a current, active Finance employee — the coordinator's note says "cleanup rows included." All SEC-* target groups already exist. If you are working offline, save this mock as mock-ad.ps1 and dot-source it; it fakes the three cmdlets and records every call:

$script:Log = [System.Collections.ArrayList]@()
function Add-GroupMember { param($Group,$Upn,[switch]$WhatIf)
  if ($WhatIf) { "WhatIf: add $Upn -> $Group" }
  else { [void]$Log.Add("ADD,$Upn,$Group") ; "added $Upn -> $Group" } }
function Remove-GroupMember { param($Group,$Upn,[switch]$WhatIf)
  if ($WhatIf) { "WhatIf: remove $Upn from $Group" }
  else { [void]$Log.Add("REMOVE,$Upn,$Group") ; "removed $Upn from $Group" } }
function Get-ChangeLog { $script:Log }

Guided steps

  1. Validate before anything else. Ask your AI: "Write a PowerShell validation pass for this CSV that checks: action is 'move' or 'remove'; upn matches name@domain format; source_group and target_group are non-empty where the action requires them; and flags — without fixing — every failing row with a reason." Review the generated code with the lab-06 six-gate mindset (it is read-only, but it still gets a read), then run it. Expected output: exactly two structurally invalid rows flagged. Record them in bulk-change-log.md.
  2. Now do the review a validator cannot. Go row by row and ask the operational question: after this row runs, what access does this user have? Structural validity is not safety. At least one structurally perfect row leaves an active employee with less access than their job requires. Write down which row, what the user loses, and what question you would send back to the requester before proceeding. Do not silently "fix" it — you do not own the intent; the requester does.
  3. Quarantine, don't patch. Produce restructure-clean.csv containing only rows that passed both gates, and restructure-held.csv with the three held rows and a one-line reason each. The held file goes back to the coordinator; the clean file proceeds. This split — proceed with the safe majority, hold the exceptions — is the bulk-ops move that keeps reorgs on schedule without gambling.
  4. Generate the executor with rollback-first ordering. Prompt: "Write the script that processes the clean CSV: for each row, first append the user's current source-group membership to rollback.csv, then remove from source_group and add to target_group. SupportsShouldProcess throughout; stop-on-error per user; transcript log." Review it: the non-negotiable detail is that the rollback line is written before any change executes — an AI draft that logs rollback after the change has it backwards, because a crash mid-run then leaves changes with no undo trail.
  5. Dry-run and actually read it. Run with -WhatIf and read every line of the output against the clean CSV. Expected output: one remove + one add per row, seventeen rows, no line touching p.sen, l.singh or t.george. The dry run is not a ritual — you are looking for the row you misjudged. Paste the WhatIf output into your log with a sentence: "Reviewed, matches intent, proceeding."
  6. Execute in the sandbox. Run for real (mock or tenant). Expected output: 17 users moved, rollback.csv populated with 17 pre-change membership lines.
  7. Verify against console truth. Do not trust the script's own success messages. In the tenant, spot-check five users' group memberships in the portal; with the mock, dump Get-ChangeLog and diff it against the clean CSV. Expected output: zero discrepancies — and a written note of how you verified, because "the script said Done" is the anti-pattern this step exists to kill.
  8. Prove the rollback works. Pick one user, run the rollback path for just that user (-WhatIf is fine), and confirm it would restore the exact prior membership. A rollback file nobody has tested is a rollback hope, not a rollback plan. Close bulk-change-log.md with the change report: rows received, rows held and why, rows executed, verification method, rollback status.
Warning

The dangerous row in this CSV is not one of the malformed ones — those are caught by any validator, including the AI's. The dangerous row is well-formed, plausible, and wrong about a human being's access. No validation script asks "should we?"; that question is the operator's, and skipping it is how bulk ops become incident tickets.

One more habit worth naming before you verify: sequence removes and adds defensively. If your executor adds the user to the target group before removing them from the source, a mid-run failure leaves someone with extra access — annoying but safe. The reverse ordering leaves someone with no access — a stranded user and an SLA clock. Most AI drafts pick an ordering arbitrarily; a reviewed script picks it deliberately and comments why. For an access-granting restructure like this one, add-then-remove is the defensible default; for a privilege-reduction change, the calculus flips, and the fact that you thought about it belongs in the change report.

Verify

  • bulk-change-log.md records two malformed rows with structural reasons and one held-for-intent row with an operational reason.
  • The stranding row's entry states specifically what access the user would lose and the clarifying question sent back to the requester.
  • The executor writes rollback entries before changes; you can point to the lines proving it.
  • The pasted -WhatIf output covers exactly the 17 clean rows and was signed off in writing.
  • Console-truth verification is documented with method and result, independent of script output.
  • The rollback test on one user is recorded and would restore prior state.

Self-grading rubric (pass = 7/10): both malformed rows caught (0–2) · stranding row caught with impact stated (0–3) · rollback-before-change ordering enforced (0–2) · dry run reviewed and signed, not just run (0–1) · console-truth verification + rollback test documented (0–2).

▸Answer key — open only after your change log is completeoptional deep dive — click to expand

The two malformed rows:

  • Row 11 — t.george,GG-Sales,SEC-Sales: the UPN t.george has no domain suffix; a Graph or AD lookup fails or, worse, resolves ambiguously. Held for correction.
  • Row 16 — l.singh@fincorp.test,GG-HR, (empty target): action is move but there is no target group. Executing naively removes her from GG-HR and adds her to nothing — a stranding, but a structurally detectable one. Held.

The engineered stranding row — row 12: remove,p.sen@fincorp.test,GG-Finance, is structurally valid — remove legitimately has no target — and passes every format check. But P.Sen is an active Finance employee, GG-Finance is what grants the Finance share and the AP system, and no other row adds her to SEC-Finance. Run it and she is locked out of her own department's tooling on Monday. Correct handling: hold the row and ask the requester — "Row 12 removes P.Sen from GG-Finance with no replacement; she is active in Finance. Is this an offboarding, an error, or should she move to SEC-Finance?" Silently converting it to a move is also wrong: you would be inventing intent, and if she was meant to be removed (transfer, disciplinary hold), your "fix" grants access she should not have.

Rollback ordering: the correct sequence per user is record → remove → add. AI drafts frequently emit remove → add → record, which means a crash between remove and record leaves an undocumented change. If you caught and reordered this, credit yourself; if your draft happened to be correct, verify you checked — the point is the check.

Console truth: with the mock, Get-ChangeLog should show 34 lines (17 removes, 17 adds), none referencing p.sen, l.singh or t.george. Any extra or missing line is a defect in your executor worth chasing.

Scoring: t.george caught = 1 · l.singh caught = 1 · p.sen held with impact stated = 2 · question-back written instead of silent fix = 1 · rollback-first verified = 2 · WhatIf reviewed in writing = 1 · independent verification = 1 · rollback tested = 1. 8+/10 is bulk-ops discipline.

Key Takeaway:

Validators catch malformed rows; only an operator asking "what does this user end up with?" catches the well-formed row that strands a person — which is why bulk-ops discipline is validate, hold, dry-run, rollback-first, and verify against the console, in that order, every time.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in