diff --git a/.gitignore b/.gitignore index b5bc6c1..8fd97db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # Backups (local only - don't commit to repo) backups/ +# Remediation-tool cache (live Graph API responses — may contain user data) +.cache-remediation/ +tmp-remediation/ + # Local settings (machine-specific) .claude/settings.local.json .claude/identity.json diff --git a/clients/cascades-tucson/docs/cloud/m365.md b/clients/cascades-tucson/docs/cloud/m365.md index 6523b7f..5e354eb 100644 --- a/clients/cascades-tucson/docs/cloud/m365.md +++ b/clients/cascades-tucson/docs/cloud/m365.md @@ -40,7 +40,7 @@ These are in-flight and feed the same Business Premium purchase decision: | AD SamAccountName | M365 UPN | License | Notes | |---|---|---|---| -| howard | dax.howard@cascadestucson.com | Business Standard | Alias: cara.lespron@ (reused mailbox from former employee) | +| *(formerly AD `howard`)* | dax.howard@cascadestucson.com | Business Standard | **Corrected 2026-04-22:** the AD `howard` account was NOT Dax Howard — it was an orphan MSP-created account (display "howard", desc "Home Offie" typo) that was mistakenly mapped to Dax Howard's mailbox. AD account deleted 2026-04-22 (recoverable from AD Recycle Bin 180 days — ObjectGUID 2050d21f-7649-4033-b1fd-83cfc286b056). Dax Howard's M365 account has no AD counterpart and is cloud-only. `cara.lespron@` alias is leftover from the former-employee Cara Lespron whose mailbox was repurposed to Dax Howard — strip this alias unless Dax confirms he still uses it. | | sysadmin | sysadmin@cascadestucson.com | Power Automate Free | Display: "Computer Guru Support" — no mailbox license | | Meredith.Kuhn | meredith.kuhn@cascadestucson.com | Business Standard | | | John.Trozzi | john.trozzi@cascadestucson.com | Business Standard | | @@ -180,8 +180,8 @@ AD account + Entra sync, no M365 license. Access shared mailboxes via outlook.of | a.r.jensen018 | a.r.jensen018@gmail.com | Ashley Jensen's personal? | | Debora Morris | deboram@teepasnow.com | External partner | | duprasc2002 | duprasc2002@yahoo.com | Christina DuPras personal? Created 2026-03-04 | -| howaed | howaed@azcomputerguru.com | **Typo** of howard — delete | -| howard | howard@azcomputerguru.com | Howard (MSP) external account | +| ~~howaed~~ | ~~howaed@azcomputerguru.com~~ | Typo of howard — already deleted (not present in tenant as of 2026-04-22) | +| ~~howard~~ | ~~howard@azcomputerguru.com~~ | **DELETED 2026-04-22** — external guest for Howard Enos (MSP). Removed per Howard's decision; MSP admin access preserved via `sysadmin@cascadestucson.com` (has Global Admin). | | karenrossini7 | karenrossini7@gmail.com | Karen Rossini's personal? | #### Blocked / former employee accounts in M365 diff --git a/clients/cascades-tucson/docs/migration/entra-connect-risk-register-2026-04-22.md b/clients/cascades-tucson/docs/migration/entra-connect-risk-register-2026-04-22.md new file mode 100644 index 0000000..5eeab27 --- /dev/null +++ b/clients/cascades-tucson/docs/migration/entra-connect-risk-register-2026-04-22.md @@ -0,0 +1,378 @@ +# Entra Connect Install — Risk Register + +**Built from:** G1 AD audit 2026-04-22 (`reports/2026-04-22-g1-ad-audit.md`) + post-reboot readiness check (`reports/2026-04-22-cs-server-entra-readiness-post-reboot.md`) + HIPAA review (`docs/security/hipaa-review-2026-04-22.md`) +**Purpose:** Know every possible failure mode before Gate G1 executes. Each risk has: probability, impact, what the audit proved, mitigation, detection, rollback. + +## Executive summary + +The audit turned up **several encouraging confirmations** and a **manageable set of genuine risks**. None are install-blockers. The biggest real concern is password hygiene (Gate G5), which our plan already handles via directory-sync-only initial posture. + +### Already GOOD (no remediation needed) + +- Forest 2016 / Domain 2016 / Schema 88 — supports modern Connect +- `cascadestucson.com` UPN suffix already configured forest-wide +- AD Recycle Bin enabled (can undo bad renames) +- No MSOL_* accounts — clean install target +- `dcdiag` silent (all tests pass) +- Time sync stratum 2 / time.nist.gov (confirmed post-reboot) +- All 3 critical Microsoft sync endpoints resolve +- Department OU structure exists (`OU=Departments\OU=Administrative`, etc.) +- `OU=ServiceAccounts` exists +- `OU=Workstations\OU=Staff PCs` + `OU=Shared PCs` already structured +- **UPN prefixes match the M365 format for 95% of users** — the pre-audit-assumed mismatches have already been cleaned up: + - `Tamra.Matthews` in AD matches `tamra.matthews@` in M365 (rename already done) + - `Shelby.Trozzi` in AD matches `Shelby.Trozzi@` in M365 (rename already done) + - `Alyssa.Brooks` in AD matches `alyssa.brooks@` in M365 + - `Christopher.Holick` in AD matches `christopher.holick@` in M365 (typo fix already done) + +--- + +## Risk register + +### CRITICAL — must fix before Gate G3 (staging-mode install) + +--- + +#### R1. Sync-scope drift: `CN=Users` + role accounts would be synced by default + +**Probability:** Certain | **Impact:** High + +**What the audit showed:** +- **8 accounts in `CN=Users`** (default container, not an OU): + `Administrator`, `directoryshare`, `Guest`, `krbtgt`, `localadmin`, `QBDataServiceUser34`, `Receptionist`, `sysadmin` +- **4 role-based accounts** in departmental OUs with `@cascades.local` UPN suffix: + `Culinary` (in `OU=Culinary`), `Receptionist` (in `CN=Users`), `saleshare` (in `OU=Marketing`), `directoryshare` (in `CN=Users`) + +**Why it matters:** Default Entra Connect scope is "all users". Without filter rules, these synthetic / service / shared-credential accounts sync to Entra and either (a) consume licenses, (b) get blocked by the verified-domain check since `@cascades.local` isn't a tenant domain, or (c) muddy the HIPAA-compliance story with shared-login objects now in the cloud identity plane. + +**Mitigation:** +- Configure Entra Connect **OU-scoped filtering**: include only `OU=Departments` (real staff) + optionally `OU=ServiceAccounts`. Exclude `CN=Users` entirely. +- **Before install**, create `OU=Excluded-From-Sync,DC=cascades,DC=local` and move the 4 role-based accounts there so they're scoped out cleanly. +- Alternative: attribute-based filter on `extensionAttribute1` set to "NoSync" — more flexible long-term. + +**Detection:** Staging-mode preview will list exactly which accounts would sync. Review before exit-staging. + +**Rollback if missed:** Remove from sync scope, re-run sync, Entra deletes the synced objects. + +--- + +#### R2. 13 enabled users have **null `PasswordLastSet`** — sign-in would fail after PHS turns on + +**Probability:** Certain for these users | **Impact:** High (key staff locked out) + +**What the audit showed:** + +| SAM | whenCreated | Role | +|---|---|---| +| Meredith.Kuhn | 2024-08-28 | Executive Director | +| John.Trozzi | 2024-08-28 | Facilities Director | +| Megan.Hiatt | 2024-08-28 | Sales Director | +| Tamra.Matthews | 2024-08-28 | Move-In Coordinator | +| Christine.Nyanzunda | 2024-08-28 | MC Admin / MedTech | +| Ashley.Jensen | 2024-08-28 | Assistant Executive Director | +| Veronica.Feller | 2024-08-28 | AL Aide | +| Sebastian.Leon | 2024-08-28 | Courtesy Patrol | +| JD.Martin | 2024-08-28 | Culinary Director | +| Matt.Brooks | 2024-08-28 | MC Receptionist / Maintenance | +| Ramon.Castaneda | 2024-08-28 | Kitchen Manager | +| Michelle.Shestko | 2024-08-28 | MC Receptionist | +| britney.thompson | 2025-06-12 | *(departed 2026-04-22)* | + +All have `PasswordLastSet = NULL` — AD account created but never had a password set. Last-logon is also `never` for all of them. They've never signed into Windows; they only use M365 with cloud-managed passwords. + +**Why it matters:** If we enable **Password Hash Sync (Gate G5)**, Entra Connect typically pushes the AD hash to overwrite the cloud password. For null-hash users, Microsoft's behavior: +- Password Hash Sync **does not** overwrite cloud password with "empty" — the cloud password persists. +- BUT: these users have no AD password, so Windows sign-in never worked for them anyway — not a change. +- The real risk: if we *ever* enable "hybrid" features (password writeback, etc.) and later set a placeholder AD password, we'd silently break their cloud sign-in. + +**Mitigation (staged):** +1. **Gate G4 (directory sync only, no PHS)** — safe for these users. They keep signing into M365 with their current cloud password. +2. **Before Gate G5 (PHS enablement)** — decision point per user: + - (a) Set real AD password, tell user "from now on your M365 password is [X]" — but MOST won't want to change their M365 password. Disruptive. + - (b) Leave null, don't enable PHS for them — requires excluding them from PHS scope (per-user filter). + - (c) **Preferred: don't enable PHS at all; stay on directory-sync-only permanently.** Users keep their cloud passwords; their AD accounts are just there for group membership + OU governance. This trades the "single password" benefit for zero risk to currently-working sign-in. + +**Detection:** Track sign-in failures per user in Entra sign-in logs after any PHS change. + +**Rollback:** Disable PHS in Entra Connect — cloud passwords stop being overwritten on next sync. + +--- + +#### R3. Matt.Brooks UPN mismatch — AD `Matt.Brooks` vs M365 `matthew.brooks@` + +**Probability:** Certain | **Impact:** Medium (one duplicate user) + +**What the audit showed:** +- AD: `SAM=Matt.Brooks`, `UPN=Matt.Brooks@cascadestucson.com` +- M365 (per `docs/cloud/m365.md` line 59): `matthew.brooks@cascadestucson.com` + +**Why it matters:** Soft-match uses UPN + proxyAddresses. AD UPN `Matt.Brooks@` doesn't match M365 UPN `matthew.brooks@`. With no `proxyAddresses` on the AD user (see R4), the engine has no fallback. Will likely **create a duplicate Entra user** for Matt. + +**Mitigation (pick one, before staging):** +- (a) Change AD UPN from `Matt.Brooks@` → `matthew.brooks@` (preferred — keeps M365 mailbox stable) +- (b) Change M365 UPN from `matthew.brooks@` → `matt.brooks@` (disrupts Matt's cached Outlook / Teams sessions, one-time prompt) + +**Detection:** Staging-mode preview report will flag this as "would create new user" instead of "would match existing". + +**Rollback:** If we miss it and dupe gets created, use Entra portal to soft-delete the duplicate + hard-match manually. + +--- + +#### R4. No users have `mail` or `proxyAddresses` populated in AD + +**Probability:** Certain (all 42 enabled users affected) | **Impact:** Medium (single point of failure for soft-match) + +**What the audit showed:** **Every single AD user** has empty `mail` and empty `proxyAddresses`. Soft-match relies on: +1. `proxyAddresses` (SMTP: prefix) — primary +2. UPN — fallback + +With no proxyAddresses, we rely on UPN matching, which is case-insensitive but format-exact. Anything off by even a character creates a duplicate. + +**Why it matters:** Combined with R3 (Matt's UPN mismatch), we're one typo away from duplicates. Also, when M365 mailboxes have aliases (e.g., `cara.lespron@` on Howard's mailbox, `tamra.johnson@` alias on Tamra's, `ashley.jenson@` typo alias on Ashley's), those ONLY match via proxyAddresses — not UPN. The aliases would orphan. + +**Mitigation:** +- **Before staging install**, script a bulk update to populate each AD user's `proxyAddresses` from their M365 SMTP addresses. Takes ~30 min to write + run. See G1 remediation script (draft pending). +- `mail` attribute can be set to the primary SMTP — feeds Outlook profile defaults. + +**Detection:** Staging preview shows match source per user (UPN vs SMTP). Review before exit. + +**Rollback:** AD attribute changes are reversible via another script. + +--- + +### HIGH — should fix before Gate G3, or early during G3 review + +--- + +#### R5. Missing security groups (all 16 `SG-*` groups our CA design assumes) + +**Probability:** Certain | **Impact:** High (CA policies can't target non-existent groups) + +**What the audit showed:** Zero of the 16 planned `SG-*` groups exist in AD: +`SG-External-Signin-Allowed`, `SG-Caregivers`, `SG-FrontDesk`, `SG-CourtesyPatrol`, `SG-Drivers`, `SG-Management-RW`, `SG-Sales-RW`, `SG-Culinary-RW`, `SG-IT-RW`, `SG-Receptionist-RW`, `SG-Directory-RW`, `SG-Server-RW`, `SG-Chat-RW`, `SG-Office-PHI-External`, `SG-Office-PHI-Internal`, `SG-CA-BreakGlass`. + +**Why it matters:** Gate G6 (CA Report-Only) needs these groups to exist and be populated before policies can target them. Can't target empty groups. + +**Mitigation:** Script creation of all 16 groups under `OU=Groups,DC=cascades,DC=local` (already exists — audit shows 1 group in it) BEFORE Gate G6. Populate membership from the rollout plan's persona tables. + +**Detection:** CA policy assignment wizard in Entra portal will show "group not found" if we target before creating. + +**Rollback:** `Remove-ADGroup` — safe. + +--- + +#### R6. M365-side orphan accounts need cleanup before sync + +**Probability:** Certain | **Impact:** Medium (post-sync confusion) + +**Accounts to resolve (per `docs/cloud/m365.md`):** + +| M365 UPN | Status | Action before sync | +|---|---|---| +| `kristiana.dowse@cascadestucson.com` | HR confirmed not current employee | Delete from M365 | +| `howaed@azcomputerguru.com` | Typo duplicate of `howard@` | Delete from M365 | +| `nick.pavloff@cascadestucson.com` | Created 2026-03-07, no AD account | **Decide: create AD account or delete M365 account.** If kept cloud-only, exclude from sync-expected list. | +| `anna.pitzlin@cascadestucson.com` | Former employee, forwarded to Meredith | Already blocked — delete if HR confirms | +| `nela.durut-azizi@cascadestucson.com` | Former employee | Already blocked — delete if HR confirms | +| `jeff.bristol@cascadestucson.com` | Former employee | Already blocked — decision to delete vs keep for mail history | +| `stephanie.devin@cascadestucson.com` | Former? | Verify with Meredith | +| `admin@NETORGFT4257522.onmicrosoft.com` | Sandra Fish (blocked 2026-04-14) | Delete | + +**Why it matters:** Post-sync, mismatched AD-vs-M365 state creates orphan objects that muddy audit logs and risk accidental reactivation. + +**Mitigation:** Clean up M365 side in Gate G2 (role-account-to-shared-mailbox conversion phase). Delete / block / convert each per the table above. + +**Detection:** M365 admin portal user list before/after. Entra staging preview will flag M365 users with no AD counterpart. + +--- + +#### R7. Role-based account UPNs use unverified domain `@cascades.local` + +**Probability:** Certain | **Impact:** Low (these shouldn't sync anyway) + +**What the audit showed:** +- `Culinary@cascades.local` +- `Receptionist@cascades.local` +- `directoryshare@cascades.local` + +**Why it matters:** If these sync, Entra tries to create users with UPNs in a domain that isn't verified on the tenant. Sync will fail for these objects specifically with a clear error. Not a tenant-wide failure. + +**Mitigation:** These accounts are explicitly excluded by R1's OU scoping. Confirmed via dual filter. + +**Detection:** Staging preview shows sync-blocked objects. + +**Rollback:** N/A — sync failure is self-protecting. + +--- + +#### R8. No Entra Connect service account exists yet + +**Probability:** N/A — create at install | **Impact:** High (blocks install) + +**What the audit showed:** Nothing in `OU=ServiceAccounts` matches "MSOL_*", "AADSync*", or similar patterns. The only service account there is `svc-audit-upload` (Syncro-specific). + +**Why it matters:** Entra Connect install creates an MSOL_* AD account automatically during install. But best practice: pre-provision a dedicated service account in `OU=ServiceAccounts` with explicit delegated permissions (AD replication rights + BUILTIN\Users read). + +**Mitigation:** Installer will create `MSOL_` automatically on install — acceptable. Alternative: pre-create `svc-entra-connect` with delegated rights using Microsoft's `Set-ADSyncPermissions` cmdlet. + +**Detection:** Post-install, verify the service account appears in AD and has correct delegated rights. + +**Rollback:** Uninstall Connect removes the auto-created service account. + +--- + +#### R9. `krbtgt` password is 602 days old + +**Probability:** Not a Connect issue | **Impact:** Low (for Connect), High (for AD security) + +**What the audit showed:** `krbtgt` password last set 2024-08-28, age 602 days. + +**Why it matters:** Not an Entra Connect concern. But this is a pre-existing HIPAA / AD security finding (audit gap #20 in hipaa.md). Standard best practice is 180-day rotation. + +**Mitigation:** Run `Reset-KrbTgt-Password.ps1` script (MS-published) — two iterations ~24 hours apart. Do this OUTSIDE the Entra Connect window to avoid changing two things at once. + +**Detection:** Event Log shows TGT-related errors if done wrong. + +**Rollback:** Can't rollback a krbtgt reset, but two rotations clear any old TGTs cleanly. + +--- + +### MEDIUM — can fix during or after G4 (directory sync active) + +--- + +#### R10. DisplayName inconsistencies (will propagate to Entra) + +**What the audit showed:** +- `Crystal.Rodriguez` display name = `'Crystal Rodriguez'` (two spaces) +- `howard` display name = `'howard'` (should be `Howard Dax` per GivenName+Surname) +- `Cathy.Kingston` display name = `'Cathy.Kingston'` (SAM-like, should be `Cathy Kingston`) + +**Why it matters:** These propagate to Entra / M365 directory, address book, Teams people cards. Cosmetic but visible. + +**Mitigation:** Fix during Gate G4 or post-sync — reversible. Set correct DisplayName on each. + +**Detection:** M365 People / address book reflects the bad names. + +--- + +#### R11. 10 enabled accounts with `PasswordNeverExpires=True` + +**What the audit showed:** + +| SAM | Risk | Action | +|---|---|---| +| Administrator | Normal for built-in | Leave | +| localadmin | Normal for emergency local | Leave | +| sysadmin | Normal for MSP service | Leave | +| QBDataServiceUser34 | Service account | Leave (won't sync) | +| howard | Should rotate | Address separately | +| Culinary | Should be Phase 5 cleaned up | Leave, out of scope | +| Receptionist | Same | Leave, out of scope | +| directoryshare | Same | Leave, out of scope | +| Lois.Lane | Exception on purpose? | **Investigate** | +| Shelby.Trozzi | Exception on purpose? | **Investigate** | +| svc-audit-upload | Service account | Correct | + +**Why it matters:** Not a Connect blocker, but HIPAA password-policy audit finding. Lois + Shelby flagged to rotate. + +**Mitigation:** Post-install, enforce AD password policy + flip their `PasswordNeverExpires` off. + +--- + +#### R12. 28 staff users show `lastLogon = never` + +**What the audit showed:** 28 enabled accounts have no recorded logon history — they've never signed into Windows. Consistent with the null-PasswordLastSet finding — these are cloud-primary users who've never touched a domain-joined PC. + +**Why it matters:** Entra Connect doesn't care. Just confirming this is consistent with our mental model: Cascades has long operated as "AD for mail + group membership" but users actually authenticate to M365 cloud-only. Not broken, just noteworthy. + +**Mitigation:** None needed. Our Gate G4 directory-sync-only posture accepts this reality. + +--- + +#### R13. 8 domain computers — known set, no surprises + +**What the audit showed:** + +| Computer | OS | Last Logon | +|---|---|---| +| CS-SERVER | Server 2019 | 2026-04-22 | +| ACCT2-PC | Win 11 Pro WS | 2026-04-22 | +| CRYSTAL-PC | Win 11 Pro | 2026-04-16 | +| DESKTOP-DLTAGOI | Win 11 Pro WS | 2026-04-13 (Sharon Edwards, our FR pilot) | +| DESKTOP-H6QHRR7 | Win 11 Pro WS | 2026-04-13 | +| DESKTOP-ROK7VNM | Win 11 Pro WS | 2026-04-13 (Susan Hicks) | +| CS-QB | Win 10 Pro | 2026-04-16 (VoIP VM on CS-SERVER) | +| DESKTOP-1ISF081 | Win 10 Pro | 2025-03-22 (stale — 13 months no logon) | + +**Why it matters:** Sync default includes computer accounts. For hybrid join, that's desired. For our scope (just user identity), exclude computers from sync. + +**Mitigation:** Set Entra Connect sync-scope filter to `Users` + `Groups` only initially. Add computers later when hybrid join is a real goal. + +**Action item:** Investigate `DESKTOP-1ISF081` — 13 months no logon, likely retired. Disable if confirmed. + +--- + +### LOW — track but not blocking + +--- + +#### R14. 9 deleted objects in AD Recycle Bin from prior cleanup + +All former employees. Recycle Bin keeps them 180 days for recovery. Not a Connect concern. + +--- + +#### R15. Disabled accounts that still exist + +Only 2 per audit: `Guest` (built-in) and `krbtgt` (disabled/system). Both excluded from sync automatically. + +--- + +#### R16. Missing mail infrastructure attributes + +**What the audit showed:** No users have `msExchMailboxGuid`, `msExchRecipientDisplayType`, or other Exchange schema attributes populated in AD. + +**Why it matters:** Would matter for Exchange Hybrid (on-prem Exchange coexistence). Not relevant for cloud-only Exchange Online. + +**Mitigation:** None needed for our scope. + +--- + +## Install-day go/no-go checklist + +All must be satisfied before exiting Gate G3 (staging mode): + +- [ ] `OU=Excluded-From-Sync` created; role accounts moved there (R1) +- [ ] `proxyAddresses` populated for all real staff from M365 SMTP (R4) +- [ ] Matt.Brooks UPN unified between AD and M365 (R3) +- [ ] M365 orphans resolved: Kristiana, howaed, Sandra Fish admin, nick.pavloff (R6) +- [ ] Role-based M365 accounts converted to shared mailboxes (accounting@, frontdesk@, etc.) (Gate G2) +- [ ] Sync-scope filter set to `OU=Departments` only, user+group objects only (R1, R13) +- [ ] 16 `SG-*` security groups pre-created and populated (R5) +- [ ] Break-glass admin account created + vaulted (independent of Connect, but gate G7 needs it) +- [ ] Microsoft BAA signed (Wave 0 — independent of Connect but HIPAA-critical) + +## Items that can wait until after G4 + +- Fix DisplayName inconsistencies (R10) +- Rotate krbtgt (R9) — schedule OUTSIDE the Connect window +- Clean up PasswordNeverExpires exceptions on Lois + Shelby (R11) +- Disable DESKTOP-1ISF081 stale computer account (R13) + +## Rollback summary per gate + +| Gate | Rollback | +|---|---| +| G1 | AD changes (renames, attribute populates) reversible via recorded before-state | +| G2 | Shared-mailbox conversion reversible (undo from Exchange Admin) | +| G3 | Uninstall Connect; tenant stays unchanged (staging mode never wrote) | +| G4 | Uninstall Connect; run `Set-MsolDirSyncEnabled -EnableDirSync $false` to stop tenant-side sync (takes ~72h to fully disable) | +| G5 | Disable PHS via Entra Connect config; cloud passwords stop being overwritten | +| G6 | Delete CA policies; no sign-ins were being blocked anyway | +| G7 | Flip each CA policy back to Report-only | +| G8 | Remove ALIS Enterprise App registration; ALIS users revert to local credentials | + +--- + +*Full G1 audit source data: `reports/2026-04-22-g1-ad-audit.md`* diff --git a/clients/cascades-tucson/docs/migration/scripts/ad-howard-delete.ps1 b/clients/cascades-tucson/docs/migration/scripts/ad-howard-delete.ps1 new file mode 100644 index 0000000..77e70ce --- /dev/null +++ b/clients/cascades-tucson/docs/migration/scripts/ad-howard-delete.ps1 @@ -0,0 +1,61 @@ +# Remove AD `howard` account (misspelled/orphan account, not used by anyone). +# Captures pre-state to D:\Backups and confirms removal. AD Recycle Bin keeps +# the object for 180 days so Restore-ADObject is available if needed. + +$ErrorActionPreference = 'Stop' +Import-Module ActiveDirectory + +$ts = Get-Date -Format 'yyyy-MM-dd-HHmmss' +$bd = "D:\Backups\howard-delete-$ts" +New-Item -Path $bd -ItemType Directory -Force | Out-Null + +try { + $u = Get-ADUser -Identity howard -Properties * + Write-Output 'Pre-delete state:' + Write-Output " SAM: $($u.SamAccountName)" + Write-Output " UPN: $($u.UserPrincipalName)" + Write-Output " Display: $($u.DisplayName)" + Write-Output " Description: $($u.Description)" + Write-Output " mail: $($u.mail)" + Write-Output " proxyAddrs: $(($u.proxyAddresses) -join '; ')" + Write-Output " DN: $($u.DistinguishedName)" + Write-Output " Enabled: $($u.Enabled)" + Write-Output " PwdLastSet: $($u.PasswordLastSet)" + Write-Output " Created: $($u.whenCreated)" + Write-Output '' + Write-Output ' Group memberships:' + Get-ADPrincipalGroupMembership -Identity howard | ForEach-Object { + Write-Output " - $($_.Name)" + } + + $u | Export-Clixml "$bd\howard-pre.xml" + Write-Output '' + Write-Output "Pre-state exported to: $bd\howard-pre.xml" + Write-Output '' + Write-Output 'Removing AD user howard...' + Remove-ADUser -Identity howard -Confirm:$false + Write-Output '[OK] Remove-ADUser returned without error.' + + Write-Output '' + Write-Output 'Verifying removal:' + try { + Get-ADUser -Identity howard -ErrorAction Stop | Out-Null + Write-Output '[FAIL] Account still exists' + exit 1 + } catch { + Write-Output "[OK] Get-ADUser -Identity howard returns: $($_.Exception.Message.Split([char]10)[0])" + } + + Write-Output '' + Write-Output 'Recycle Bin (180 day retention) entry for rollback:' + $deleted = Get-ADObject -Filter { SamAccountName -eq 'howard' } -IncludeDeletedObjects -Properties whenChanged, isDeleted, ObjectGUID, lastKnownParent + $deleted | Select-Object Name, ObjectGUID, isDeleted, whenChanged, lastKnownParent | Format-List | Out-String | Write-Output + Write-Output 'Rollback command (within 180 days):' + if ($deleted) { + $guid = $deleted | Select-Object -First 1 -ExpandProperty ObjectGUID + Write-Output " Restore-ADObject -Identity $guid" + } +} catch { + Write-Output "FAIL: $_" + exit 1 +} diff --git a/clients/cascades-tucson/docs/migration/scripts/entra-connect-g1-ad-audit.ps1 b/clients/cascades-tucson/docs/migration/scripts/entra-connect-g1-ad-audit.ps1 new file mode 100644 index 0000000..0f772b8 --- /dev/null +++ b/clients/cascades-tucson/docs/migration/scripts/entra-connect-g1-ad-audit.ps1 @@ -0,0 +1,332 @@ +# ============================================================================ +# Entra Connect G1 Pre-flight AD Audit - CS-SERVER +# ---------------------------------------------------------------------------- +# Purpose: collect everything needed to assess Entra Connect sync risk before +# install. Strictly READ-ONLY. No writes, no renames, no state changes. +# +# Maps to Wave 0.5 Gate G1 in docs/cloud/user-account-rollout-plan.md. +# Output is analyzed against the risk register at +# docs/migration/entra-connect-risk-register-2026-04-22.md. +# ============================================================================ + +$ErrorActionPreference = 'Continue' +Import-Module ActiveDirectory -ErrorAction Stop + +function Section($n) { + Write-Output '' + Write-Output ('=' * 76) + Write-Output "== $n" + Write-Output ('=' * 76) +} + +function Note($m) { Write-Output " $m" } + +Write-Output "G1 AD Audit - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')" +Write-Output "Host: $env:COMPUTERNAME" + +# ---------------------------------------------------------------------------- +Section '1. Forest / Domain / Schema' +# ---------------------------------------------------------------------------- +$forest = Get-ADForest +$domain = Get-ADDomain +Write-Output "Forest FQDN: $($forest.Name)" +Write-Output "Forest mode: $($forest.ForestMode)" +Write-Output "Domain mode: $($domain.DomainMode)" +Write-Output "Domain DN: $($domain.DistinguishedName)" +Write-Output "NetBIOS: $($domain.NetBIOSName)" +Write-Output "UPN suffixes (forest):" +foreach ($s in $forest.UPNSuffixes) { Write-Output " - $s" } +if ($forest.UPNSuffixes.Count -eq 0) { Write-Output ' (none configured beyond default cascades.local)' } + +$schema = Get-ADObject (Get-ADRootDSE).schemaNamingContext -Property objectVersion +Write-Output "Schema objectVersion: $($schema.objectVersion)" +Write-Output '' +Write-Output 'FSMO roles:' +Write-Output " Schema Master: $($forest.SchemaMaster)" +Write-Output " Domain Naming Master: $($forest.DomainNamingMaster)" +Write-Output " PDC Emulator: $($domain.PDCEmulator)" +Write-Output " RID Master: $($domain.RIDMaster)" +Write-Output " Infrastructure Master: $($domain.InfrastructureMaster)" + +# ---------------------------------------------------------------------------- +Section '2. OU structure' +# ---------------------------------------------------------------------------- +$ous = Get-ADOrganizationalUnit -Filter * +Write-Output "Total OUs: $($ous.Count)" +Write-Output '' +foreach ($ou in $ous | Sort-Object DistinguishedName) { + $uc = (Get-ADUser -Filter * -SearchBase $ou.DistinguishedName -SearchScope OneLevel | Measure-Object).Count + $gc = (Get-ADGroup -Filter * -SearchBase $ou.DistinguishedName -SearchScope OneLevel | Measure-Object).Count + $cc = (Get-ADComputer -Filter * -SearchBase $ou.DistinguishedName -SearchScope OneLevel | Measure-Object).Count + Write-Output (" {0,-80} users={1} groups={2} computers={3}" -f $ou.DistinguishedName, $uc, $gc, $cc) +} + +# Users in CN=Users (default container, not an OU) +$defaultUsersDN = "CN=Users,$($domain.DistinguishedName)" +$usersInDefault = Get-ADUser -Filter * -SearchBase $defaultUsersDN -SearchScope OneLevel +Write-Output '' +Write-Output "Users in default CN=Users container (not ideal — should be in an OU for sync scope):" +Write-Output " Count: $($usersInDefault.Count)" +foreach ($u in $usersInDefault) { + Write-Output " - $($u.SamAccountName) ($($u.Name))" +} + +# ---------------------------------------------------------------------------- +Section '3. All AD users - identity attributes for sync match' +# ---------------------------------------------------------------------------- +$users = Get-ADUser -Filter * -Properties ` + SamAccountName, UserPrincipalName, DisplayName, GivenName, Surname, ` + Mail, proxyAddresses, Enabled, PasswordLastSet, PasswordNeverExpires, ` + PasswordNotRequired, LockedOut, whenCreated, whenChanged, ` + Department, Title, Office, Description, lastLogonTimestamp, ` + mS-DS-ConsistencyGuid, objectGUID, DistinguishedName + +Write-Output "Total users: $($users.Count)" +Write-Output "Enabled: $($users | Where-Object Enabled -eq $true | Measure-Object | Select-Object -ExpandProperty Count)" +Write-Output "Disabled: $($users | Where-Object Enabled -eq $false | Measure-Object | Select-Object -ExpandProperty Count)" +Write-Output '' + +Write-Output 'Per-user identity dump (focus: anything that affects soft-match):' +Write-Output '' +Write-Output 'SAM | UPN | Mail | ProxyAddrs | PwdLastSet | PwdNeverExp | Enabled | OU' +Write-Output '---' +foreach ($u in $users | Sort-Object SamAccountName) { + $ou = $u.DistinguishedName -replace '^CN=[^,]+,', '' + $ou = $ou -replace ",DC=cascades,DC=local$", '' + $px = if ($u.proxyAddresses) { ($u.proxyAddresses -join ';') } else { '' } + $pls = if ($u.PasswordLastSet) { $u.PasswordLastSet.ToString('yyyy-MM-dd') } else { 'NULL' } + Write-Output ("{0} | {1} | {2} | {3} | {4} | {5} | {6} | {7}" -f ` + $u.SamAccountName, $u.UserPrincipalName, $u.Mail, $px, $pls, $u.PasswordNeverExpires, $u.Enabled, $ou) +} + +# ---------------------------------------------------------------------------- +Section '4. Soft-match risk scan - accounts likely to duplicate in Entra' +# ---------------------------------------------------------------------------- +Write-Output '--- Users with NO proxyAddresses and NO mail (soft-match engine has nothing to work with):' +foreach ($u in $users) { + if (-not $u.Mail -and -not $u.proxyAddresses -and $u.Enabled) { + Write-Output " - $($u.SamAccountName) (UPN: $($u.UserPrincipalName))" + } +} + +Write-Output '' +Write-Output '--- Users whose UPN suffix is NOT cascadestucson.com (will mismatch M365 target unless renamed):' +foreach ($u in $users) { + if ($u.Enabled -and $u.UserPrincipalName -and $u.UserPrincipalName -notmatch '@cascadestucson\.com$') { + Write-Output " - $($u.SamAccountName) UPN=$($u.UserPrincipalName)" + } +} + +Write-Output '' +Write-Output '--- Users whose SAM does not match their UPN prefix (name mismatch candidates):' +foreach ($u in $users) { + if ($u.Enabled -and $u.UserPrincipalName) { + $upnPrefix = $u.UserPrincipalName -replace '@.*$', '' + if ($u.SamAccountName -ne $upnPrefix) { + Write-Output " - SAM='$($u.SamAccountName)' UPN='$($u.UserPrincipalName)'" + } + } +} + +Write-Output '' +Write-Output '--- Users with DisplayName different from Given+Surname (may cause display oddities post-sync):' +foreach ($u in $users) { + if ($u.Enabled -and $u.GivenName -and $u.Surname) { + $expected = "$($u.GivenName) $($u.Surname)" + if ($u.DisplayName -and $u.DisplayName -ne $expected) { + Write-Output " - SAM=$($u.SamAccountName) display='$($u.DisplayName)' expected='$expected'" + } + } +} + +# ---------------------------------------------------------------------------- +Section '5. Password hygiene (affects whether sync-derived sign-in will work)' +# ---------------------------------------------------------------------------- +Write-Output '--- Enabled users with null PasswordLastSet (never set a password):' +foreach ($u in $users) { + if ($u.Enabled -and -not $u.PasswordLastSet) { + Write-Output " - $($u.SamAccountName) whenCreated=$($u.whenCreated.ToString('yyyy-MM-dd'))" + } +} + +Write-Output '' +Write-Output '--- Users with PasswordNotRequired=True:' +foreach ($u in $users) { + if ($u.PasswordNotRequired) { + Write-Output " - $($u.SamAccountName) enabled=$($u.Enabled)" + } +} + +Write-Output '' +Write-Output '--- Users with PasswordNeverExpires=True:' +foreach ($u in $users) { + if ($u.PasswordNeverExpires -and $u.Enabled) { + Write-Output " - $($u.SamAccountName) PwdLastSet=$($u.PasswordLastSet) Description='$($u.Description)'" + } +} + +Write-Output '' +Write-Output '--- Currently locked-out accounts:' +foreach ($u in $users) { + if ($u.LockedOut) { + Write-Output " - $($u.SamAccountName) enabled=$($u.Enabled)" + } +} + +$krbtgt = Get-ADUser krbtgt -Properties PasswordLastSet +$krbtgtAge = if ($krbtgt.PasswordLastSet) { ((Get-Date) - $krbtgt.PasswordLastSet).Days } else { '?' } +Write-Output '' +Write-Output "krbtgt password last set: $($krbtgt.PasswordLastSet) (age: $krbtgtAge days)" +Write-Output ' (best practice: rotate every 180 days; > 180 is a known audit finding)' + +# ---------------------------------------------------------------------------- +Section '6. Role-based / shared accounts (should NOT sync)' +# ---------------------------------------------------------------------------- +$roleLike = @('Receptionist','receptionist','Culinary','saleshare','directoryshare','Front Desk', + 'frontdesk','Accounting','accounting','Nurse','nurse','mcnurse','memcarenurse', + 'Security','security','HR','hr','medtech','Memcare') +Write-Output 'Accounts whose SamAccountName looks role-based (should be EXCLUDED from sync scope):' +foreach ($u in $users) { + foreach ($pattern in $roleLike) { + if ($u.SamAccountName -eq $pattern -or $u.SamAccountName -ieq $pattern) { + Write-Output " - $($u.SamAccountName) enabled=$($u.Enabled) OU=$($u.DistinguishedName)" + break + } + } +} + +Write-Output '' +Write-Output 'Service / built-in accounts (context - usually excluded from sync):' +$builtin = @('Administrator','krbtgt','Guest','localadmin','sysadmin','QBDataServiceUser34','howard') +foreach ($u in $users) { + if ($u.SamAccountName -in $builtin) { + Write-Output " - $($u.SamAccountName) enabled=$($u.Enabled)" + } +} + +# ---------------------------------------------------------------------------- +Section '7. Likely-departed accounts still enabled (HIPAA termination risk)' +# ---------------------------------------------------------------------------- +# Accounts enabled but no logon activity in >= 90 days are candidate departures +$cutoff = (Get-Date).AddDays(-90) +Write-Output "Enabled accounts with no logon activity in 90+ days:" +foreach ($u in $users | Sort-Object lastLogonTimestamp) { + if ($u.Enabled -and $u.SamAccountName -notin $builtin) { + $ll = if ($u.lastLogonTimestamp) { [DateTime]::FromFileTime($u.lastLogonTimestamp) } else { $null } + if (-not $ll -or $ll -lt $cutoff) { + $llStr = if ($ll) { $ll.ToString('yyyy-MM-dd') } else { 'never' } + Write-Output " - $($u.SamAccountName) lastLogon=$llStr" + } + } +} + +# ---------------------------------------------------------------------------- +Section '8. AD groups inventory' +# ---------------------------------------------------------------------------- +$groups = Get-ADGroup -Filter * -Properties Members,Description,whenCreated,GroupScope,GroupCategory +Write-Output "Total groups: $($groups.Count)" +Write-Output '' +Write-Output 'Group name | Scope | Category | Members | Created | Description' +Write-Output '---' +foreach ($g in $groups | Sort-Object Name) { + $mc = if ($g.Members) { $g.Members.Count } else { 0 } + Write-Output ("{0} | {1} | {2} | {3} | {4} | {5}" -f ` + $g.SamAccountName, $g.GroupScope, $g.GroupCategory, $mc, ` + $g.whenCreated.ToString('yyyy-MM-dd'), ($g.Description -replace '\r?\n',' ')) +} + +Write-Output '' +Write-Output 'Security groups our rollout plan assumes (checking existence):' +$needed = @( + 'SG-External-Signin-Allowed','SG-Caregivers','SG-FrontDesk','SG-CourtesyPatrol', + 'SG-Drivers','SG-Management-RW','SG-Sales-RW','SG-Culinary-RW','SG-IT-RW', + 'SG-Receptionist-RW','SG-Directory-RW','SG-Server-RW','SG-Chat-RW','SG-Office-PHI-External', + 'SG-Office-PHI-Internal','SG-CA-BreakGlass' +) +foreach ($n in $needed) { + try { + $g = Get-ADGroup -Identity $n -ErrorAction Stop + Write-Output " [EXISTS] $n" + } catch { + Write-Output " [MISSING] $n (needs creation)" + } +} + +# ---------------------------------------------------------------------------- +Section '9. Computers (for sync scope decision)' +# ---------------------------------------------------------------------------- +$computers = Get-ADComputer -Filter * -Properties OperatingSystem,Enabled,lastLogonTimestamp +Write-Output "Total computer accounts: $($computers.Count)" +Write-Output "Enabled: $(($computers | Where-Object Enabled -eq $true).Count)" +Write-Output "Disabled: $(($computers | Where-Object Enabled -eq $false).Count)" +Write-Output '' +Write-Output 'Computer | OS | Enabled | LastLogon' +Write-Output '---' +foreach ($c in $computers | Sort-Object Name) { + $ll = if ($c.lastLogonTimestamp) { [DateTime]::FromFileTime($c.lastLogonTimestamp).ToString('yyyy-MM-dd') } else { 'never' } + Write-Output ("{0} | {1} | {2} | {3}" -f $c.Name, $c.OperatingSystem, $c.Enabled, $ll) +} + +# ---------------------------------------------------------------------------- +Section '10. Existing Entra Connect / MSOL account check' +# ---------------------------------------------------------------------------- +# Entra Connect creates MSOL_ service accounts in AD. If any exist from a +# prior install attempt, soft-match will behave unpredictably. +$msol = Get-ADUser -Filter 'SamAccountName -like "MSOL_*"' -ErrorAction SilentlyContinue +if ($msol) { + Write-Output 'EXISTING MSOL accounts found (indicates prior Connect attempt or residue):' + foreach ($m in $msol) { + Write-Output " - $($m.SamAccountName) enabled=$($m.Enabled)" + } +} else { + Write-Output '[OK] No MSOL_* accounts in AD (clean install target)' +} + +# ---------------------------------------------------------------------------- +Section '11. Recycle Bin + deleted objects' +# ---------------------------------------------------------------------------- +$rb = Get-ADOptionalFeature 'Recycle Bin Feature' +Write-Output "AD Recycle Bin enabled: $([bool]$rb.EnabledScopes)" + +$deleted = Get-ADObject -Filter 'isDeleted -eq $true' -IncludeDeletedObjects -ResultSetSize 10 -Properties whenCreated,lastKnownParent +if ($deleted) { + Write-Output "Deleted objects (sample of 10):" + foreach ($d in $deleted) { + Write-Output " - $($d.Name) deletedFrom='$($d.lastKnownParent)'" + } +} + +# ---------------------------------------------------------------------------- +Section '12. DNS / DC connectivity (sync path)' +# ---------------------------------------------------------------------------- +Write-Output '--- Key outbound hostnames (already verified clean in readiness check, re-verify):' +foreach ($h in 'login.microsoftonline.com','login.windows.net','adminwebservice.microsoftonline.com') { + try { + $r = Resolve-DnsName $h -Type A -ErrorAction Stop + Write-Output " [OK] $h -> $($r[0].IPAddress)" + } catch { + Write-Output " [FAIL] $h -> $($_.Exception.Message)" + } +} + +# ---------------------------------------------------------------------------- +Section '13. DC health quick recheck' +# ---------------------------------------------------------------------------- +# dcdiag already validated tonight but re-check in case the reboot shook anything +dcdiag /test:services /test:replications /test:fsmocheck /test:advertising /q 2>&1 | + Where-Object { $_ -match 'failed|warning|error' } | + ForEach-Object { Write-Output " $_" } +Write-Output '(Any entries above mean a dcdiag warning/failure; otherwise silent = all pass.)' + +# ---------------------------------------------------------------------------- +Section '14. NTP time sync (re-verify)' +# ---------------------------------------------------------------------------- +$tsource = (w32tm /query /source 2>&1) -join ' ' +$tdrift = (w32tm /query /status 2>&1 | Select-String 'Last Successful Sync Time|ReferenceId|Stratum').ToString() +Write-Output "Source: $tsource" +Write-Output $tdrift + +# ---------------------------------------------------------------------------- +Section 'Done' +# ---------------------------------------------------------------------------- +Write-Output "Completed at $(Get-Date)" diff --git a/clients/cascades-tucson/docs/migration/scripts/g1-ad-hygiene.ps1 b/clients/cascades-tucson/docs/migration/scripts/g1-ad-hygiene.ps1 new file mode 100644 index 0000000..99c1821 --- /dev/null +++ b/clients/cascades-tucson/docs/migration/scripts/g1-ad-hygiene.ps1 @@ -0,0 +1,357 @@ +# ============================================================================ +# Entra Connect Gate G1 - AD Hygiene / Pre-sync Cleanup +# ---------------------------------------------------------------------------- +# Runs on CS-SERVER via GuruRMM. Idempotent. Each section reports what it +# would do (dry-run) OR what it did (execute). Pre-state backups always run. +# +# USAGE: +# Default invocation is DRY-RUN (no writes). Run with $doExecute=$true to +# apply changes. +# +# Sections: +# 0. Pre-state backup (always runs, no changes) +# 1. OU=Excluded-From-Sync (create; move 4 role accounts) +# 2. proxyAddresses populate (34 users - live data from M365 Graph) +# 3. SG-* security groups (create 16 groups in OU=Groups) +# 4. DisplayName cosmetics (3 fixes: Crystal, howard, Cathy) +# 5. Summary (counts, rollback path) +# +# Maps to risk register: docs/migration/entra-connect-risk-register-2026-04-22.md +# ============================================================================ + +# Toggle at top of script so it's obvious in GuruRMM command preview +$doExecute = $false # CHANGE TO $true TO APPLY + +$ErrorActionPreference = 'Continue' +Import-Module ActiveDirectory -ErrorAction Stop + +$ts = Get-Date -Format 'yyyy-MM-dd-HHmmss' +$backupDir = "D:\Backups\g1-hygiene-$ts" +$mode = if ($doExecute) { 'EXECUTE' } else { 'DRY-RUN (no changes)' } + +function Section($n) { + Write-Output '' + Write-Output ('=' * 76) + Write-Output "== $n" + Write-Output ('=' * 76) +} +function Log($level, $msg) { + $prefix = switch ($level) { + 'WOULD' { '[WOULD]' } + 'DID' { '[DID] ' } + 'OK' { '[OK] ' } + 'SKIP' { '[SKIP] ' } + 'WARN' { '[WARN] ' } + 'FAIL' { '[FAIL] ' } + default { ' ' } + } + Write-Output "$prefix $msg" +} + +Write-Output "G1 AD Hygiene - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')" +Write-Output "Host: $env:COMPUTERNAME" +Write-Output "Mode: $mode" +Write-Output "Backup dir: $backupDir" + +# Counters +$script:stats = @{ created=0; moved=0; updated=0; skipped=0; errors=0 } + +# ---------------------------------------------------------------------------- +Section '0. Pre-state backup (always runs)' +# ---------------------------------------------------------------------------- +try { + if (-not (Test-Path $backupDir)) { + New-Item -Path $backupDir -ItemType Directory -Force | Out-Null + } + + # Full user dump with relevant attributes for diff-later + Get-ADUser -Filter * -Properties SamAccountName,UserPrincipalName,DisplayName,GivenName,Surname,` + Mail,proxyAddresses,Enabled,PasswordLastSet,PasswordNeverExpires,DistinguishedName,whenCreated,whenChanged | + Select-Object SamAccountName,UserPrincipalName,DisplayName,GivenName,Surname,Mail, + @{Name='ProxyAddresses';Expression={($_.proxyAddresses -join ';')}}, + Enabled,PasswordLastSet,PasswordNeverExpires,DistinguishedName | + Export-Csv "$backupDir\users-pre.csv" -NoTypeInformation -Encoding UTF8 + Log 'OK' "Exported users-pre.csv" + + Get-ADGroup -Filter * -Properties Description,GroupScope,GroupCategory,Members | + Select-Object SamAccountName,Name,GroupScope,GroupCategory,Description, + @{Name='MemberCount';Expression={$_.Members.Count}}, + DistinguishedName | + Export-Csv "$backupDir\groups-pre.csv" -NoTypeInformation -Encoding UTF8 + Log 'OK' "Exported groups-pre.csv" + + Get-ADOrganizationalUnit -Filter * | + Select-Object Name,DistinguishedName | + Export-Csv "$backupDir\ous-pre.csv" -NoTypeInformation -Encoding UTF8 + Log 'OK' "Exported ous-pre.csv" + + Write-Output "" + Log 'OK' "Pre-state saved at $backupDir" + Write-Output "Rollback commands (if needed after execute):" + Write-Output " - proxyAddresses: Set-ADUser from users-pre.csv column ProxyAddresses" + Write-Output " - OU moves: Move-ADObject back to old DistinguishedName" + Write-Output " - Groups created today: Remove-ADGroup (safe since memberless)" +} catch { + Log 'FAIL' "Pre-state backup failed: $_" + $script:stats.errors++ +} + +# ---------------------------------------------------------------------------- +Section '1. OU=Excluded-From-Sync + move 4 role accounts' +# ---------------------------------------------------------------------------- +$domainDN = (Get-ADDomain).DistinguishedName +$excludedOU = "OU=Excluded-From-Sync,$domainDN" + +# Create OU +$existing = $null +try { $existing = Get-ADOrganizationalUnit -Identity $excludedOU -ErrorAction Stop } catch {} + +if ($existing) { + Log 'SKIP' "OU=Excluded-From-Sync already exists" +} else { + if ($doExecute) { + try { + New-ADOrganizationalUnit -Name 'Excluded-From-Sync' -Path $domainDN ` + -Description 'Accounts scoped OUT of Entra Connect sync. Do not move staff users here.' ` + -ProtectedFromAccidentalDeletion $true + Log 'DID' "Created OU=Excluded-From-Sync" + $script:stats.created++ + } catch { + Log 'FAIL' "New-ADOrganizationalUnit failed: $_" + $script:stats.errors++ + } + } else { + Log 'WOULD' "Create OU=Excluded-From-Sync (ProtectedFromAccidentalDeletion=true)" + $script:stats.created++ + } +} + +# Move role accounts +$roleAccounts = @('Culinary','Receptionist','saleshare','directoryshare') +foreach ($sam in $roleAccounts) { + try { + $u = Get-ADUser -Identity $sam -Properties DistinguishedName -ErrorAction Stop + $currentOU = ($u.DistinguishedName -split ',', 2)[1] + if ($currentOU -eq $excludedOU) { + Log 'SKIP' "$sam already in Excluded-From-Sync" + continue + } + if ($doExecute) { + Move-ADObject -Identity $u.DistinguishedName -TargetPath $excludedOU + Log 'DID' "Moved ${sam}: $currentOU -> $excludedOU" + $script:stats.moved++ + } else { + Log 'WOULD' "Move $sam from $currentOU to $excludedOU" + $script:stats.moved++ + } + } catch { + Log 'WARN' "Cannot resolve $sam - $_" + } +} + +# ---------------------------------------------------------------------------- +Section '2. Populate proxyAddresses (34 users - live data from M365 Graph 2026-04-22)' +# ---------------------------------------------------------------------------- +# Mapping derived from live Graph API pull. Primary SMTP uses uppercase 'SMTP:' +# prefix (Exchange convention for primary). Aliases use lowercase 'smtp:'. +$proxyMap = @{ + # 24 users with existing M365 mailboxes + 'Allison.Reibschied' = @('Allison.Reibschied@cascadestucson.com') + 'Alyssa.Brooks' = @('alyssa.brooks@cascadestucson.com') + 'Ashley.Jensen' = @('ashley.jensen@cascadestucson.com','ashley.jenson@cascadestucson.com') + 'britney.thompson' = @('Britney.Thompson@cascadestucson.com') + 'Christina.DuPras' = @('christina.dupras@cascadestucson.com') + 'Christine.Nyanzunda' = @('christine.nyanzunda@cascadestucson.com') + 'Crystal.Rodriguez' = @('crystal.rodriguez@cascadestucson.com','crystal.suszek@cascadestucson.com') + 'howard' = @('dax.howard@cascadestucson.com','cara.lespron@cascadestucson.com') + 'JD.Martin' = @('jd.martin@cascadestucson.com') + 'John.Trozzi' = @('john.trozzi@cascadestucson.com') + 'karen.rossini' = @('karen.rossini@cascadestucson.com') + 'lauren.hasselman' = @('lauren.hasselman@cascadestucson.com') + 'Lois.Lane' = @('lois.lane@cascadestucson.com') + 'Lupe.Sanchez' = @('lupe.sanchez@cascadestucson.com') + 'Matt.Brooks' = @('matthew.brooks@cascadestucson.com') # soft-match bridge (M365 UPN is matthew.brooks) + 'Megan.Hiatt' = @('megan.hiatt@cascadestucson.com') + 'Meredith.Kuhn' = @('meredith.kuhn@cascadestucson.com') + 'Ramon.Castaneda' = @('ramon.castaneda@cascadestucson.com','ramon.castanada@cascadestucson.com','ramon.casteneda@cascadestucson.com') + 'Sharon.Edwards' = @('sharon.edwards@cascadestucson.com') + 'Shelby.Trozzi' = @('Shelby.Trozzi@cascadestucson.com') + 'Susan.Hicks' = @('susan.hicks@cascadestucson.com') + 'sysadmin' = @('sysadmin@cascadestucson.com') + 'Tamra.Matthews' = @('tamra.matthews@cascadestucson.com','tamra.johnson@cascadestucson.com') + 'Veronica.Feller' = @('veronica.feller@cascadestucson.com') + # 10 AD-only users (no M365 mailbox yet; reserve canonical SMTP for when they get licensed) + 'Cathy.Kingston' = @('cathy.kingston@cascadestucson.com') + 'Christopher.Holick' = @('christopher.holick@cascadestucson.com') + 'Julian.Crim' = @('julian.crim@cascadestucson.com') + 'Kyla.QuickTiffany' = @('kyla.quicktiffany@cascadestucson.com') + 'Michelle.Shestko' = @('michelle.shestko@cascadestucson.com') + 'Ray.Rai' = @('ray.rai@cascadestucson.com') + 'Richard.Adams' = @('richard.adams@cascadestucson.com') + 'Sebastian.Leon' = @('sebastian.leon@cascadestucson.com') + 'Sheldon.Gardfrey' = @('sheldon.gardfrey@cascadestucson.com') + 'Shontiel.Nunn' = @('shontiel.nunn@cascadestucson.com') +} + +foreach ($sam in ($proxyMap.Keys | Sort-Object)) { + try { + $u = Get-ADUser -Identity $sam -Properties proxyAddresses,mail -ErrorAction Stop + } catch { + Log 'WARN' "AD user '$sam' not found - $($_.Exception.Message.Split([char]10)[0])" + continue + } + + # Build the target proxyAddresses value: SMTP: + smtp: + $primary = $proxyMap[$sam][0] + $aliases = @($proxyMap[$sam] | Select-Object -Skip 1) + $targetProxies = @("SMTP:$primary") + ($aliases | ForEach-Object { "smtp:$_" }) + $targetMail = $primary + + $currentProxies = @($u.proxyAddresses) + $currentSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($p in $currentProxies) { [void]$currentSet.Add($p) } + $targetSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($p in $targetProxies) { [void]$targetSet.Add($p) } + + if ($currentSet.SetEquals($targetSet) -and $u.mail -eq $targetMail) { + Log 'SKIP' "$sam proxyAddresses already current" + continue + } + + $currentStr = if ($currentProxies.Count -eq 0) { '' } else { $currentProxies -join '; ' } + $targetStr = $targetProxies -join '; ' + + if ($doExecute) { + try { + Set-ADUser -Identity $sam -Replace @{ + proxyAddresses = $targetProxies + mail = $targetMail + } -ErrorAction Stop + Log 'DID' "$sam" + Write-Output " before: $currentStr" + Write-Output " after: $targetStr" + Write-Output " mail=$targetMail" + $script:stats.updated++ + } catch { + Log 'FAIL' "$sam Set-ADUser failed: $_" + $script:stats.errors++ + } + } else { + Log 'WOULD' "$sam" + Write-Output " before: $currentStr" + Write-Output " after: $targetStr" + Write-Output " mail= -> $targetMail" + $script:stats.updated++ + } +} + +# ---------------------------------------------------------------------------- +Section '3. Create 16 SG-* security groups (CA / file-share / break-glass)' +# ---------------------------------------------------------------------------- +$groupsOU = "OU=Groups,$domainDN" +# Verify Groups OU exists +try { Get-ADOrganizationalUnit -Identity $groupsOU -ErrorAction Stop | Out-Null } +catch { + Log 'FAIL' "OU=Groups does not exist at $groupsOU - cannot create groups" + $script:stats.errors++ +} + +$groupsToCreate = @( + @{Name='SG-External-Signin-Allowed'; Desc='Members may sign in from outside Cascades building (CA policy target).'}, + @{Name='SG-Caregivers'; Desc='All shift-work caregivers. CA policy target for shared-phone mobile policy.'}, + @{Name='SG-FrontDesk'; Desc='Front desk receptionists sharing reception PCs.'}, + @{Name='SG-CourtesyPatrol'; Desc='Courtesy patrol staff.'}, + @{Name='SG-Drivers'; Desc='Transportation drivers (AD accounts being disabled 2026-04-22 - group retained for history).'}, + @{Name='SG-Management-RW'; Desc='Read/write on \\CS-SERVER\Management file share (Phase 4).'}, + @{Name='SG-Sales-RW'; Desc='Read/write on \\CS-SERVER\SalesDept file share (Phase 4).'}, + @{Name='SG-Culinary-RW'; Desc='Read/write on \\CS-SERVER\Culinary file share (Phase 4).'}, + @{Name='SG-IT-RW'; Desc='Read/write on \\CS-SERVER\IT file share (Phase 4).'}, + @{Name='SG-Receptionist-RW'; Desc='Read/write on \\CS-SERVER\Receptionist file share (Phase 4).'}, + @{Name='SG-Directory-RW'; Desc='Read/write on \\CS-SERVER\directoryshare file share (Phase 4).'}, + @{Name='SG-Server-RW'; Desc='Read/write on \\CS-SERVER\Server share (IT admin, Phase 4).'}, + @{Name='SG-Chat-RW'; Desc='Read/write on \\CS-SERVER\chat file share (Phase 4).'}, + @{Name='SG-Office-PHI-External'; Desc='Office PHI staff with external sign-in permission (CA policy).'}, + @{Name='SG-Office-PHI-Internal'; Desc='Office PHI staff limited to in-building sign-in (CA policy).'}, + @{Name='SG-CA-BreakGlass'; Desc='Break-glass accounts excluded from all Conditional Access policies.'} +) + +foreach ($g in $groupsToCreate) { + try { + $existing = Get-ADGroup -Identity $g.Name -ErrorAction Stop + Log 'SKIP' "$($g.Name) already exists" + continue + } catch { + # group missing - proceed to create + } + if ($doExecute) { + try { + New-ADGroup -Name $g.Name -SamAccountName $g.Name -GroupCategory Security -GroupScope Global ` + -Path $groupsOU -Description $g.Desc + Log 'DID' "Created $($g.Name)" + $script:stats.created++ + } catch { + Log 'FAIL' "Create $($g.Name) failed: $_" + $script:stats.errors++ + } + } else { + Log 'WOULD' "Create $($g.Name) (Global Security) in $groupsOU" + Write-Output " desc: $($g.Desc)" + $script:stats.created++ + } +} + +# ---------------------------------------------------------------------------- +Section '4. DisplayName cosmetic fixes (3 users)' +# ---------------------------------------------------------------------------- +$displayFixes = @( + @{SAM='Crystal.Rodriguez'; Current='Crystal Rodriguez'; Target='Crystal Rodriguez'} + @{SAM='howard'; Current='howard'; Target='Howard Dax'} + @{SAM='Cathy.Kingston'; Current='Cathy.Kingston'; Target='Cathy Kingston'} +) +foreach ($d in $displayFixes) { + try { + $u = Get-ADUser -Identity $d.SAM -Properties DisplayName -ErrorAction Stop + if ($u.DisplayName -eq $d.Target) { + Log 'SKIP' "$($d.SAM) DisplayName already '$($d.Target)'" + continue + } + if ($doExecute) { + Set-ADUser -Identity $d.SAM -DisplayName $d.Target + Log 'DID' "$($d.SAM) DisplayName: '$($u.DisplayName)' -> '$($d.Target)'" + $script:stats.updated++ + } else { + Log 'WOULD' "$($d.SAM) DisplayName: '$($u.DisplayName)' -> '$($d.Target)'" + $script:stats.updated++ + } + } catch { + Log 'WARN' "$($d.SAM) lookup failed: $_" + } +} + +# ---------------------------------------------------------------------------- +Section '5. Summary' +# ---------------------------------------------------------------------------- +Write-Output "Mode: $mode" +Write-Output "Created: $($script:stats.created)" +Write-Output "Moved: $($script:stats.moved)" +Write-Output "Updated: $($script:stats.updated)" +Write-Output "Skipped: $($script:stats.skipped)" +Write-Output "Errors: $($script:stats.errors)" +Write-Output "" +Write-Output "Backup dir: $backupDir" +Write-Output "" + +if (-not $doExecute) { + Write-Output 'DRY-RUN complete. To execute:' + Write-Output ' 1. Review the [WOULD] lines above' + Write-Output ' 2. Re-run this script with $doExecute = $true' + Write-Output ' 3. Compare post-state vs pre-state CSVs in the backup dir' +} else { + Write-Output 'EXECUTE complete. Recommended next steps:' + Write-Output ' 1. Re-run in DRY-RUN to confirm 0 [WOULD] entries (idempotency check)' + Write-Output " 2. Export users-post.csv for the audit trail (in $backupDir)" + Write-Output ' 3. Proceed to Gate G2 (M365 role-account shared-mailbox conversion)' +} + +Write-Output "" +Write-Output "Completed at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')" diff --git a/clients/cascades-tucson/reports/2026-04-22-g1-ad-audit.md b/clients/cascades-tucson/reports/2026-04-22-g1-ad-audit.md new file mode 100644 index 0000000..9ba9936 --- /dev/null +++ b/clients/cascades-tucson/reports/2026-04-22-g1-ad-audit.md @@ -0,0 +1,426 @@ +# CS-SERVER G1 AD Audit + +**Command ID:** e83befb7-4c8c-4f24-9b24-56cb641e6464 +**Run:** 2026-04-23T02:55:50.634848Z +**Exit:** 0 + +## STDOUT + +``` +G1 AD Audit - 2026-04-22 19:55:51 -07:00 +Host: CS-SERVER + +============================================================================ +== 1. Forest / Domain / Schema +============================================================================ +Forest FQDN: cascades.local +Forest mode: Windows2016Forest +Domain mode: Windows2016Domain +Domain DN: DC=cascades,DC=local +NetBIOS: CASCADES +UPN suffixes (forest): + - cascadestucson.com +Schema objectVersion: 88 + +FSMO roles: + Schema Master: CS-SERVER.cascades.local + Domain Naming Master: CS-SERVER.cascades.local + PDC Emulator: CS-SERVER.cascades.local + RID Master: CS-SERVER.cascades.local + Infrastructure Master: CS-SERVER.cascades.local + +============================================================================ +== 2. OU structure +============================================================================ +Total OUs: 18 + + OU=Administrative,OU=Departments,DC=cascades,DC=local users=5 groups=0 computers=0 + OU=Care-Assisted Living,OU=Departments,DC=cascades,DC=local users=4 groups=0 computers=0 + OU=Care-Memorycare,OU=Departments,DC=cascades,DC=local users=2 groups=0 computers=0 + OU=Culinary,OU=Departments,DC=cascades,DC=local users=4 groups=0 computers=0 + OU=Departments,DC=cascades,DC=local users=0 groups=0 computers=0 + OU=Domain Controllers,DC=cascades,DC=local users=0 groups=0 computers=1 + OU=Groups,DC=cascades,DC=local users=0 groups=1 computers=0 + OU=Housekeeping,OU=Departments,DC=cascades,DC=local users=1 groups=0 computers=0 + OU=Life Enrichment,OU=Departments,DC=cascades,DC=local users=2 groups=0 computers=0 + OU=Maintenance,OU=Departments,DC=cascades,DC=local users=2 groups=0 computers=0 + OU=Marketing,OU=Departments,DC=cascades,DC=local users=4 groups=0 computers=0 + OU=Nurses,OU=Care-Assisted Living,OU=Departments,DC=cascades,DC=local users=0 groups=0 computers=0 + OU=Resident Services,OU=Departments,DC=cascades,DC=local users=8 groups=0 computers=0 + OU=ServiceAccounts,DC=cascades,DC=local users=1 groups=0 computers=0 + OU=Shared PCs,OU=Workstations,DC=cascades,DC=local users=0 groups=0 computers=0 + OU=Staff PCs,OU=Workstations,DC=cascades,DC=local users=0 groups=0 computers=6 + OU=Transportation,OU=Departments,DC=cascades,DC=local users=3 groups=0 computers=0 + OU=Workstations,DC=cascades,DC=local users=0 groups=0 computers=0 + +Users in default CN=Users container (not ideal - should be in an OU for sync scope): + Count: 8 + - Administrator (Administrator) + - directoryshare (directoryshare) + - Guest (Guest) + - krbtgt (krbtgt) + - localadmin (localadmin) + - QBDataServiceUser34 (QBDataServiceUser34) + - Receptionist (RECEPTIONIST) + - sysadmin (Sysadmin) + +============================================================================ +== 3. All AD users - identity attributes for sync match +============================================================================ +Total users: 44 +Enabled: 42 +Disabled: 2 + +Per-user identity dump (focus: anything that affects soft-match): + +SAM | UPN | Mail | ProxyAddrs | PwdLastSet | PwdNeverExp | Enabled | OU +--- +Administrator | | | | 2024-08-04 | True | True | CN=Users +Allison.Reibschied | Allison.Reibschied@cascadestucson.com | | | 2026-03-13 | False | True | OU=Administrative,OU=Departments +Alyssa.Brooks | Alyssa.Brooks@cascadestucson.com | | | 2025-12-12 | False | True | OU=Culinary,OU=Departments +Ashley.Jensen | Ashley.Jensen@cascadestucson.com | | | NULL | False | True | OU=Administrative,OU=Departments +britney.thompson | britney.thompson@cascadestucson.com | | | NULL | False | True | OU=Care-Assisted Living,OU=Departments +Cathy.Kingston | Cathy.Kingston@cascadestucson.com | | | 2025-12-12 | False | True | OU=Resident Services,OU=Departments +Christina.DuPras | Christina.DuPras@cascadestucson.com | | | 2026-01-06 | False | True | OU=Resident Services,OU=Departments +Christine.Nyanzunda | Christine.Nyanzunda@cascadestucson.com | | | NULL | False | True | OU=Care-Memorycare,OU=Departments +Christopher.Holick | Christopher.Holick@cascadestucson.com | | | 2025-12-12 | False | True | OU=Transportation,OU=Departments +Crystal.Rodriguez | Crystal.Rodriguez@cascadestucson.com | | | 2026-04-20 | False | True | OU=Marketing,OU=Departments +Culinary | Culinary@cascades.local | | | 2024-12-27 | True | True | OU=Culinary,OU=Departments +directoryshare | directoryshare@cascades.local | | | 2025-12-01 | True | True | CN=Users +Guest | | | | NULL | True | False | CN=Users +howard | howard@cascadestucson.com | | | 2025-08-11 | True | True | OU=Administrative,OU=Departments +JD.Martin | JD.Martin@cascadestucson.com | | | NULL | False | True | OU=Culinary,OU=Departments +John.Trozzi | John.Trozzi@cascadestucson.com | | | NULL | False | True | OU=Maintenance,OU=Departments +Julian.Crim | Julian.Crim@cascadestucson.com | | | 2025-12-12 | False | True | OU=Transportation,OU=Departments +karen.rossini | karen.rossini@cascadestucson.com | | | 2025-12-12 | False | True | OU=Care-Assisted Living,OU=Departments +krbtgt | | | | 2024-08-28 | False | False | CN=Users +Kyla.QuickTiffany | Kyla.QuickTiffany@cascadestucson.com | | | 2026-04-13 | False | True | OU=Resident Services,OU=Departments +lauren.hasselman | lauren.hasselman@cascadestucson.com | | | 2026-04-01 | False | True | OU=Administrative,OU=Departments +localadmin | | | | 2024-12-03 | True | True | CN=Users +Lois.Lane | Lois.Lane@cascadestucson.com | | | 2025-12-22 | True | True | OU=Care-Assisted Living,OU=Departments +Lupe.Sanchez | Lupe.Sanchez@cascadestucson.com | | | 2025-12-12 | False | True | OU=Housekeeping,OU=Departments +Matt.Brooks | Matt.Brooks@cascadestucson.com | | | NULL | False | True | OU=Maintenance,OU=Departments +Megan.Hiatt | Megan.Hiatt@cascadestucson.com | | | NULL | False | True | OU=Marketing,OU=Departments +Meredith.Kuhn | Meredith.Kuhn@cascadestucson.com | | | NULL | False | True | OU=Administrative,OU=Departments +Michelle.Shestko | Michelle.Shestko@cascadestucson.com | | | NULL | False | True | OU=Resident Services,OU=Departments +QBDataServiceUser34 | | | | 2024-10-02 | True | True | CN=Users +Ramon.Castaneda | Ramon.Castaneda@cascadestucson.com | | | NULL | False | True | OU=Culinary,OU=Departments +Ray.Rai | Ray.Rai@cascadestucson.com | | | 2025-12-12 | False | True | OU=Resident Services,OU=Departments +Receptionist | RECEPTIONIST@cascades.local | | | 2026-04-07 | True | True | CN=Users +Richard.Adams | Richard.Adams@cascadestucson.com | | | 2025-12-12 | False | True | OU=Transportation,OU=Departments +saleshare | | | | 2025-10-27 | False | True | OU=Marketing,OU=Departments +Sebastian.Leon | Sebastian.Leon@cascadestucson.com | | | NULL | False | True | OU=Resident Services,OU=Departments +Sharon.Edwards | Sharon.Edwards@cascadestucson.com | | | 2026-04-13 | False | True | OU=Life Enrichment,OU=Departments +Shelby.Trozzi | Shelby.Trozzi@cascadestucson.com | | | 2025-12-11 | True | True | OU=Care-Memorycare,OU=Departments +Sheldon.Gardfrey | Sheldon.Gardfrey@cascadestucson.com | | | 2025-12-12 | False | True | OU=Resident Services,OU=Departments +Shontiel.Nunn | Shontiel.Nunn@cascadestucson.com | | | 2025-12-12 | False | True | OU=Resident Services,OU=Departments +Susan.Hicks | Susan.Hicks@cascadestucson.com | | | 2026-04-13 | False | True | OU=Life Enrichment,OU=Departments +svc-audit-upload | | | | 2026-04-17 | True | True | OU=ServiceAccounts +sysadmin | sysadmin@cascadestucson.com | | | 2024-09-29 | True | True | CN=Users +Tamra.Matthews | Tamra.Matthews@cascadestucson.com | | | NULL | False | True | OU=Marketing,OU=Departments +Veronica.Feller | Veronica.Feller@cascadestucson.com | | | NULL | False | True | OU=Care-Assisted Living,OU=Departments + +============================================================================ +== 4. Soft-match risk scan - accounts likely to duplicate in Entra +============================================================================ +--- Users with NO proxyAddresses and NO mail (soft-match engine has nothing to work with): + - Administrator (UPN: ) + - localadmin (UPN: ) + - Meredith.Kuhn (UPN: Meredith.Kuhn@cascadestucson.com) + - John.Trozzi (UPN: John.Trozzi@cascadestucson.com) + - Megan.Hiatt (UPN: Megan.Hiatt@cascadestucson.com) + - Crystal.Rodriguez (UPN: Crystal.Rodriguez@cascadestucson.com) + - Tamra.Matthews (UPN: Tamra.Matthews@cascadestucson.com) + - Lois.Lane (UPN: Lois.Lane@cascadestucson.com) + - Christina.DuPras (UPN: Christina.DuPras@cascadestucson.com) + - Christine.Nyanzunda (UPN: Christine.Nyanzunda@cascadestucson.com) + - Susan.Hicks (UPN: Susan.Hicks@cascadestucson.com) + - Ashley.Jensen (UPN: Ashley.Jensen@cascadestucson.com) + - Veronica.Feller (UPN: Veronica.Feller@cascadestucson.com) + - Sebastian.Leon (UPN: Sebastian.Leon@cascadestucson.com) + - JD.Martin (UPN: JD.Martin@cascadestucson.com) + - Matt.Brooks (UPN: Matt.Brooks@cascadestucson.com) + - Ramon.Castaneda (UPN: Ramon.Castaneda@cascadestucson.com) + - Michelle.Shestko (UPN: Michelle.Shestko@cascadestucson.com) + - Sharon.Edwards (UPN: Sharon.Edwards@cascadestucson.com) + - sysadmin (UPN: sysadmin@cascadestucson.com) + - QBDataServiceUser34 (UPN: ) + - Culinary (UPN: Culinary@cascades.local) + - Receptionist (UPN: RECEPTIONIST@cascades.local) + - britney.thompson (UPN: britney.thompson@cascadestucson.com) + - howard (UPN: howard@cascadestucson.com) + - saleshare (UPN: ) + - directoryshare (UPN: directoryshare@cascades.local) + - Shelby.Trozzi (UPN: Shelby.Trozzi@cascadestucson.com) + - karen.rossini (UPN: karen.rossini@cascadestucson.com) + - Alyssa.Brooks (UPN: Alyssa.Brooks@cascadestucson.com) + - Lupe.Sanchez (UPN: Lupe.Sanchez@cascadestucson.com) + - Sheldon.Gardfrey (UPN: Sheldon.Gardfrey@cascadestucson.com) + - Cathy.Kingston (UPN: Cathy.Kingston@cascadestucson.com) + - Shontiel.Nunn (UPN: Shontiel.Nunn@cascadestucson.com) + - Ray.Rai (UPN: Ray.Rai@cascadestucson.com) + - Richard.Adams (UPN: Richard.Adams@cascadestucson.com) + - Julian.Crim (UPN: Julian.Crim@cascadestucson.com) + - Christopher.Holick (UPN: Christopher.Holick@cascadestucson.com) + - lauren.hasselman (UPN: lauren.hasselman@cascadestucson.com) + - Allison.Reibschied (UPN: Allison.Reibschied@cascadestucson.com) + - Kyla.QuickTiffany (UPN: Kyla.QuickTiffany@cascadestucson.com) + - svc-audit-upload (UPN: ) + +--- Users whose UPN suffix is NOT cascadestucson.com (will mismatch M365 target unless renamed): + - Culinary UPN=Culinary@cascades.local + - Receptionist UPN=RECEPTIONIST@cascades.local + - directoryshare UPN=directoryshare@cascades.local + +--- Users whose SAM does not match their UPN prefix (name mismatch candidates): + +--- Users with DisplayName different from Given+Surname (may cause display oddities post-sync): + - SAM=Crystal.Rodriguez display='Crystal Rodriguez' expected='Crystal Rodriguez' + - SAM=howard display='howard' expected='Howard Dax' + - SAM=Cathy.Kingston display='Cathy.Kingston' expected='Cathy Kingston' + +============================================================================ +== 5. Password hygiene (affects whether sync-derived sign-in will work) +============================================================================ +--- Enabled users with null PasswordLastSet (never set a password): + - Meredith.Kuhn whenCreated=2024-08-28 + - John.Trozzi whenCreated=2024-08-28 + - Megan.Hiatt whenCreated=2024-08-28 + - Tamra.Matthews whenCreated=2024-08-28 + - Christine.Nyanzunda whenCreated=2024-08-28 + - Ashley.Jensen whenCreated=2024-08-28 + - Veronica.Feller whenCreated=2024-08-28 + - Sebastian.Leon whenCreated=2024-08-28 + - JD.Martin whenCreated=2024-08-28 + - Matt.Brooks whenCreated=2024-08-28 + - Ramon.Castaneda whenCreated=2024-08-28 + - Michelle.Shestko whenCreated=2024-08-28 + - britney.thompson whenCreated=2025-06-12 + +--- Users with PasswordNotRequired=True: + - Guest enabled=False + +--- Users with PasswordNeverExpires=True: + - Administrator PwdLastSet=08/04/2024 18:39:34 Description='Built-in account for administering the computer/domain' + - localadmin PwdLastSet=12/03/2024 15:22:15 Description='' + - Lois.Lane PwdLastSet=12/22/2025 09:52:55 Description='' + - sysadmin PwdLastSet=09/29/2024 21:23:28 Description='' + - QBDataServiceUser34 PwdLastSet=10/02/2024 12:22:12 Description='This account has been established to run the QuickBooks database system.' + - Culinary PwdLastSet=12/27/2024 10:17:03 Description='' + - Receptionist PwdLastSet=04/07/2026 12:54:47 Description='' + - howard PwdLastSet=08/11/2025 13:18:03 Description='Home Offie' + - directoryshare PwdLastSet=12/01/2025 10:02:12 Description='' + - Shelby.Trozzi PwdLastSet=12/11/2025 14:03:27 Description='' + - svc-audit-upload PwdLastSet=04/17/2026 15:21:13 Description='Write-only access to \\CS-SERVER\AuditDrop$. Used by Syncro audit upload script. Do not use interactively.' + +--- Currently locked-out accounts: + +krbtgt password last set: 08/28/2024 10:02:37 (age: 602 days) + (best practice: rotate every 180 days; > 180 is a known audit finding) + +============================================================================ +== 6. Role-based / shared accounts (should NOT sync) +============================================================================ +Accounts whose SamAccountName looks role-based (should be EXCLUDED from sync scope): + - Culinary enabled=True OU=CN=Culinary,OU=Culinary,OU=Departments,DC=cascades,DC=local + - Receptionist enabled=True OU=CN=RECEPTIONIST,CN=Users,DC=cascades,DC=local + - saleshare enabled=True OU=CN=saleshare,OU=Marketing,OU=Departments,DC=cascades,DC=local + - directoryshare enabled=True OU=CN=directoryshare,CN=Users,DC=cascades,DC=local + +Service / built-in accounts (context - usually excluded from sync): + - Administrator enabled=True + - Guest enabled=False + - localadmin enabled=True + - krbtgt enabled=False + - sysadmin enabled=True + - QBDataServiceUser34 enabled=True + - howard enabled=True + +============================================================================ +== 7. Likely-departed accounts still enabled (HIPAA termination risk) +============================================================================ +Enabled accounts with no logon activity in 90+ days: + - britney.thompson lastLogon=never + - karen.rossini lastLogon=never + - Shelby.Trozzi lastLogon=never + - Ramon.Castaneda lastLogon=never + - Matt.Brooks lastLogon=never + - Christopher.Holick lastLogon=never + - Michelle.Shestko lastLogon=never + - Ray.Rai lastLogon=never + - Shontiel.Nunn lastLogon=never + - Julian.Crim lastLogon=never + - Richard.Adams lastLogon=never + - Lupe.Sanchez lastLogon=never + - Alyssa.Brooks lastLogon=never + - Cathy.Kingston lastLogon=never + - Sheldon.Gardfrey lastLogon=never + - JD.Martin lastLogon=never + - Tamra.Matthews lastLogon=never + - Megan.Hiatt lastLogon=never + - svc-audit-upload lastLogon=never + - Lois.Lane lastLogon=never + - John.Trozzi lastLogon=never + - Kyla.QuickTiffany lastLogon=never + - Meredith.Kuhn lastLogon=never + - Ashley.Jensen lastLogon=never + - Veronica.Feller lastLogon=never + - Sebastian.Leon lastLogon=never + - Christine.Nyanzunda lastLogon=never + - saleshare lastLogon=2025-12-08 + - Christina.DuPras lastLogon=2026-01-06 + +============================================================================ +== 8. AD groups inventory +============================================================================ +Total groups: 55 + +Group name | Scope | Category | Members | Created | Description +--- +Access Control Assistance Operators | DomainLocal | Security | 0 | 2024-08-28 | Members of this group can remotely query authorization attributes and permissions for resources on this computer. +Account Operators | DomainLocal | Security | 0 | 2024-08-28 | Members can administer domain user and group accounts +Administrators | DomainLocal | Security | 4 | 2024-08-28 | Administrators have complete and unrestricted access to the computer/domain +Allowed RODC Password Replication Group | DomainLocal | Security | 0 | 2024-08-28 | Members in this group can have their passwords replicated to all read-only domain controllers in the domain +AuditUploaders | Global | Security | 1 | 2026-04-17 | Members can WRITE-ONLY to \\CS-SERVER\AuditDrop$. No read/list. +Backup Operators | DomainLocal | Security | 0 | 2024-08-28 | Backup Operators can override security restrictions for the sole purpose of backing up or restoring files +Cert Publishers | DomainLocal | Security | 0 | 2024-08-28 | Members of this group are permitted to publish certificates to the directory +Certificate Service DCOM Access | DomainLocal | Security | 0 | 2024-08-28 | Members of this group are allowed to connect to Certification Authorities in the enterprise +Cloneable Domain Controllers | Global | Security | 0 | 2024-08-28 | Members of this group that are domain controllers may be cloned. +Cryptographic Operators | DomainLocal | Security | 0 | 2024-08-28 | Members are authorized to perform cryptographic operations. +Denied RODC Password Replication Group | DomainLocal | Security | 8 | 2024-08-28 | Members in this group cannot have their passwords replicated to any read-only domain controllers in the domain +DHCP Administrators | DomainLocal | Security | 0 | 2025-12-22 | Members who have administrative access to the DHCP Service +DHCP Users | DomainLocal | Security | 0 | 2025-12-22 | Members who have view-only access to the DHCP service +Distributed COM Users | DomainLocal | Security | 0 | 2024-08-28 | Members are allowed to launch, activate and use Distributed COM objects on this machine. +DnsAdmins | DomainLocal | Security | 0 | 2024-08-28 | DNS Administrators Group +DnsUpdateProxy | Global | Security | 0 | 2024-08-28 | DNS clients who are permitted to perform dynamic updates on behalf of some other clients (such as DHCP servers). +Domain Admins | Global | Security | 2 | 2024-08-28 | Designated administrators of the domain +Domain Computers | Global | Security | 0 | 2024-08-28 | All workstations and servers joined to the domain +Domain Controllers | Global | Security | 0 | 2024-08-28 | All domain controllers in the domain +Domain Guests | Global | Security | 0 | 2024-08-28 | All domain guests +Domain Users | Global | Security | 0 | 2024-08-28 | All domain users +Enterprise Admins | Universal | Security | 1 | 2024-08-28 | Designated administrators of the enterprise +Enterprise Key Admins | Universal | Security | 0 | 2024-08-28 | Members of this group can perform administrative actions on key objects within the forest. +Enterprise Read-only Domain Controllers | Universal | Security | 0 | 2024-08-28 | Members of this group are Read-Only Domain Controllers in the enterprise +Event Log Readers | DomainLocal | Security | 0 | 2024-08-28 | Members of this group can read event logs from local machine +Group Policy Creator Owners | Global | Security | 1 | 2024-08-28 | Members in this group can modify group policy for the domain +Guests | DomainLocal | Security | 2 | 2024-08-28 | Guests have the same access as members of the Users group by default, except for the Guest account which is further restricted +Hyper-V Administrators | DomainLocal | Security | 0 | 2024-08-28 | Members of this group have complete and unrestricted access to all features of Hyper-V. +IIS_IUSRS | DomainLocal | Security | 0 | 2024-08-28 | Built-in group used by Internet Information Services. +Incoming Forest Trust Builders | DomainLocal | Security | 0 | 2024-08-28 | Members of this group can create incoming, one-way trusts to this forest +Key Admins | Global | Security | 0 | 2024-08-28 | Members of this group can perform administrative actions on key objects within the domain. +KitchenAdmin | Global | Security | 0 | 2025-12-11 | +MemoryCareDepartment | Global | Security | 0 | 2025-12-11 | +Network Configuration Operators | DomainLocal | Security | 0 | 2024-08-28 | Members in this group can have some administrative privileges to manage configuration of networking features +Performance Log Users | DomainLocal | Security | 0 | 2024-08-28 | Members of this group may schedule logging of performance counters, enable trace providers, and collect event traces both locally and via remote access to this computer +Performance Monitor Users | DomainLocal | Security | 0 | 2024-08-28 | Members of this group can access performance counter data locally and remotely +Pre-Windows 2000 Compatible Access | DomainLocal | Security | 1 | 2024-08-28 | A backward compatibility group which allows read access on all users and groups in the domain +Print Operators | DomainLocal | Security | 0 | 2024-08-28 | Members can administer printers installed on domain controllers +Protected Users | Global | Security | 0 | 2024-08-28 | Members of this group are afforded additional protections against authentication security threats. See http://go.microsoft.com/fwlink/?LinkId=298939 for more information. +QuickBooks Access | DomainLocal | Security | 3 | 2024-10-25 | +RAS and IAS Servers | DomainLocal | Security | 0 | 2024-08-28 | Servers in this group can access remote access properties of users +RDS Endpoint Servers | DomainLocal | Security | 2 | 2024-08-28 | Servers in this group run virtual machines and host sessions where users RemoteApp programs and personal virtual desktops run. This group needs to be populated on servers running RD Connection Broker. RD Session Host servers and RD Virtualization Host servers used in the deployment need to be in this group. +RDS Management Servers | DomainLocal | Security | 2 | 2024-08-28 | Servers in this group can perform routine administrative actions on servers running Remote Desktop Services. This group needs to be populated on all servers in a Remote Desktop Services deployment. The servers running the RDS Central Management service must be included in this group. +RDS Remote Access Servers | DomainLocal | Security | 1 | 2024-08-28 | Servers in this group enable users of RemoteApp programs and personal virtual desktops access to these resources. In Internet-facing deployments, these servers are typically deployed in an edge network. This group needs to be populated on servers running RD Connection Broker. RD Gateway servers and RD Web Access servers used in the deployment need to be in this group. +Read-only Domain Controllers | Global | Security | 0 | 2024-08-28 | Members of this group are Read-Only Domain Controllers in the domain +Remote Desktop Users | DomainLocal | Security | 1 | 2024-08-28 | Members in this group are granted the right to logon remotely +Remote Management Users | DomainLocal | Security | 0 | 2024-08-28 | Members of this group can access WMI resources over management protocols (such as WS-Management via the Windows Remote Management service). This applies only to WMI namespaces that grant access to the user. +Replicator | DomainLocal | Security | 0 | 2024-08-28 | Supports file replication in a domain +Roaming | Global | Security | 0 | 2025-03-18 | +Schema Admins | Universal | Security | 1 | 2024-08-28 | Designated administrators of the schema +Server Operators | DomainLocal | Security | 0 | 2024-08-28 | Members can administer domain servers +Storage Replica Administrators | DomainLocal | Security | 0 | 2024-08-28 | Members of this group have complete and unrestricted access to all features of Storage Replica. +Terminal Server License Servers | DomainLocal | Security | 0 | 2024-08-28 | Members of this group can update user accounts in Active Directory with information about license issuance, for the purpose of tracking and reporting TS Per User CAL usage +Users | DomainLocal | Security | 6 | 2024-08-28 | Users are prevented from making accidental or intentional system-wide changes and can run most applications +Windows Authorization Access Group | DomainLocal | Security | 1 | 2024-08-28 | Members of this group have access to the computed tokenGroupsGlobalAndUniversal attribute on User objects + +Security groups our rollout plan assumes (checking existence): + [MISSING] SG-External-Signin-Allowed (needs creation) + [MISSING] SG-Caregivers (needs creation) + [MISSING] SG-FrontDesk (needs creation) + [MISSING] SG-CourtesyPatrol (needs creation) + [MISSING] SG-Drivers (needs creation) + [MISSING] SG-Management-RW (needs creation) + [MISSING] SG-Sales-RW (needs creation) + [MISSING] SG-Culinary-RW (needs creation) + [MISSING] SG-IT-RW (needs creation) + [MISSING] SG-Receptionist-RW (needs creation) + [MISSING] SG-Directory-RW (needs creation) + [MISSING] SG-Server-RW (needs creation) + [MISSING] SG-Chat-RW (needs creation) + [MISSING] SG-Office-PHI-External (needs creation) + [MISSING] SG-Office-PHI-Internal (needs creation) + [MISSING] SG-CA-BreakGlass (needs creation) + +============================================================================ +== 9. Computers (for sync scope decision) +============================================================================ +Total computer accounts: 8 +Enabled: 8 +Disabled: 0 + +Computer | OS | Enabled | LastLogon +--- +ACCT2-PC | Windows 11 Pro for Workstations | True | 2026-04-22 +CRYSTAL-PC | Windows 11 Pro | True | 2026-04-16 +CS-QB | Windows 10 Pro | True | 2026-04-16 +CS-SERVER | Windows Server 2019 Standard | True | 2026-04-22 +DESKTOP-1ISF081 | Windows 10 Pro | True | 2025-03-22 +DESKTOP-DLTAGOI | Windows 11 Pro for Workstations | True | 2026-04-13 +DESKTOP-H6QHRR7 | Windows 11 Pro for Workstations | True | 2026-04-13 +DESKTOP-ROK7VNM | Windows 11 Pro for Workstations | True | 2026-04-13 + +============================================================================ +== 10. Existing Entra Connect / MSOL account check +============================================================================ +[OK] No MSOL_* accounts in AD (clean install target) + +============================================================================ +== 11. Recycle Bin + deleted objects +============================================================================ +AD Recycle Bin enabled: True +Deleted objects (sample of 10): + - Deleted Objects deletedFrom='' + - Lupe Sanchez +DEL:7234271e-68da-467e-aa9f-b505ec62e06f deletedFrom='CN=Users,DC=cascades,DC=local' + - Anna Pitzlin +DEL:e0a3ddf4-a062-494a-9ca3-d4887acea4d9 deletedFrom='CN=Users,DC=cascades,DC=local' + - Nela Durut-Azizi +DEL:744b63c4-c4d0-4740-b5b9-a864ac4a6e64 deletedFrom='CN=Users,DC=cascades,DC=local' + - Haris Durut +DEL:7b8e8152-2f9b-481f-8776-33a1949c95b5 deletedFrom='CN=Users,DC=cascades,DC=local' + - Jodi Ramstack +DEL:831930f5-0d71-403a-8360-7ffcc6ec3a62 deletedFrom='CN=Users,DC=cascades,DC=local' + - Monica Ramirez +DEL:23bbc7b2-43c2-4db8-a8f7-97a93f48272e deletedFrom='CN=Users,DC=cascades,DC=local' + - Nuria Diaz +DEL:11f586e9-ea83-4db5-aa74-cca5696abbcf deletedFrom='CN=Users,DC=cascades,DC=local' + - Cathy Reece +DEL:c10591da-72ee-4074-9b50-1d004bc46eb2 deletedFrom='CN=Users,DC=cascades,DC=local' + - Kelly Wallace +DEL:1940c00c-6382-449b-be19-8460e74b8317 deletedFrom='CN=Users,DC=cascades,DC=local' + +============================================================================ +== 12. DNS / DC connectivity (sync path) +============================================================================ +--- Key outbound hostnames (already verified clean in readiness check, re-verify): + [OK] login.microsoftonline.com -> + [OK] login.windows.net -> + [OK] adminwebservice.microsoftonline.com -> + +============================================================================ +== 13. DC health quick recheck +============================================================================ +(Any entries above mean a dcdiag warning/failure; otherwise silent = all pass.) + +============================================================================ +== 14. NTP time sync (re-verify) +============================================================================ +Source: time.nist.gov,0x8 +System.Object[] + +============================================================================ +== Done +============================================================================ +Completed at 04/22/2026 19:55:54 + +``` diff --git a/clients/cascades-tucson/reports/2026-04-22-g1-dryrun.md b/clients/cascades-tucson/reports/2026-04-22-g1-dryrun.md new file mode 100644 index 0000000..194fce7 --- /dev/null +++ b/clients/cascades-tucson/reports/2026-04-22-g1-dryrun.md @@ -0,0 +1,244 @@ +# G1 AD Hygiene Dry-Run + +**Command ID:** 110f0836-9fa7-4773-b82c-e7f0eb9b5bbe +**Exit:** 0 +**Completed:** 2026-04-23T03:26:52.186400Z + +## STDOUT + +``` +G1 AD Hygiene - 2026-04-22 20:26:50 -07:00 +Host: CS-SERVER +Mode: DRY-RUN (no changes) +Backup dir: D:\Backups\g1-hygiene-2026-04-22-202650 + +============================================================================ +== 0. Pre-state backup (always runs) +============================================================================ +[OK] Exported users-pre.csv +[OK] Exported groups-pre.csv +[OK] Exported ous-pre.csv + +[OK] Pre-state saved at D:\Backups\g1-hygiene-2026-04-22-202650 +Rollback commands (if needed after execute): + - proxyAddresses: Set-ADUser from users-pre.csv column ProxyAddresses + - OU moves: Move-ADObject back to old DistinguishedName + - Groups created today: Remove-ADGroup (safe since memberless) + +============================================================================ +== 1. OU=Excluded-From-Sync + move 4 role accounts +============================================================================ +[WOULD] Create OU=Excluded-From-Sync (ProtectedFromAccidentalDeletion=true) +[WOULD] Move Culinary from OU=Culinary,OU=Departments,DC=cascades,DC=local to OU=Excluded-From-Sync,DC=cascades,DC=local +[WOULD] Move Receptionist from CN=Users,DC=cascades,DC=local to OU=Excluded-From-Sync,DC=cascades,DC=local +[WOULD] Move saleshare from OU=Marketing,OU=Departments,DC=cascades,DC=local to OU=Excluded-From-Sync,DC=cascades,DC=local +[WOULD] Move directoryshare from CN=Users,DC=cascades,DC=local to OU=Excluded-From-Sync,DC=cascades,DC=local + +============================================================================ +== 2. Populate proxyAddresses (34 users - live data from M365 Graph 2026-04-22) +============================================================================ +[WOULD] Allison.Reibschied + before: + after: SMTP:Allison.Reibschied@cascadestucson.com + mail= -> Allison.Reibschied@cascadestucson.com +[WOULD] Alyssa.Brooks + before: + after: SMTP:alyssa.brooks@cascadestucson.com + mail= -> alyssa.brooks@cascadestucson.com +[WOULD] Ashley.Jensen + before: + after: SMTP:ashley.jensen@cascadestucson.com; smtp:ashley.jenson@cascadestucson.com + mail= -> ashley.jensen@cascadestucson.com +[WOULD] britney.thompson + before: + after: SMTP:Britney.Thompson@cascadestucson.com + mail= -> Britney.Thompson@cascadestucson.com +[WOULD] Cathy.Kingston + before: + after: SMTP:cathy.kingston@cascadestucson.com + mail= -> cathy.kingston@cascadestucson.com +[WOULD] Christina.DuPras + before: + after: SMTP:christina.dupras@cascadestucson.com + mail= -> christina.dupras@cascadestucson.com +[WOULD] Christine.Nyanzunda + before: + after: SMTP:christine.nyanzunda@cascadestucson.com + mail= -> christine.nyanzunda@cascadestucson.com +[WOULD] Christopher.Holick + before: + after: SMTP:christopher.holick@cascadestucson.com + mail= -> christopher.holick@cascadestucson.com +[WOULD] Crystal.Rodriguez + before: + after: SMTP:crystal.rodriguez@cascadestucson.com; smtp:crystal.suszek@cascadestucson.com + mail= -> crystal.rodriguez@cascadestucson.com +[WOULD] howard + before: + after: SMTP:dax.howard@cascadestucson.com; smtp:cara.lespron@cascadestucson.com + mail= -> dax.howard@cascadestucson.com +[WOULD] JD.Martin + before: + after: SMTP:jd.martin@cascadestucson.com + mail= -> jd.martin@cascadestucson.com +[WOULD] John.Trozzi + before: + after: SMTP:john.trozzi@cascadestucson.com + mail= -> john.trozzi@cascadestucson.com +[WOULD] Julian.Crim + before: + after: SMTP:julian.crim@cascadestucson.com + mail= -> julian.crim@cascadestucson.com +[WOULD] karen.rossini + before: + after: SMTP:karen.rossini@cascadestucson.com + mail= -> karen.rossini@cascadestucson.com +[WOULD] Kyla.QuickTiffany + before: + after: SMTP:kyla.quicktiffany@cascadestucson.com + mail= -> kyla.quicktiffany@cascadestucson.com +[WOULD] lauren.hasselman + before: + after: SMTP:lauren.hasselman@cascadestucson.com + mail= -> lauren.hasselman@cascadestucson.com +[WOULD] Lois.Lane + before: + after: SMTP:lois.lane@cascadestucson.com + mail= -> lois.lane@cascadestucson.com +[WOULD] Lupe.Sanchez + before: + after: SMTP:lupe.sanchez@cascadestucson.com + mail= -> lupe.sanchez@cascadestucson.com +[WOULD] Matt.Brooks + before: + after: SMTP:matthew.brooks@cascadestucson.com + mail= -> matthew.brooks@cascadestucson.com +[WOULD] Megan.Hiatt + before: + after: SMTP:megan.hiatt@cascadestucson.com + mail= -> megan.hiatt@cascadestucson.com +[WOULD] Meredith.Kuhn + before: + after: SMTP:meredith.kuhn@cascadestucson.com + mail= -> meredith.kuhn@cascadestucson.com +[WOULD] Michelle.Shestko + before: + after: SMTP:michelle.shestko@cascadestucson.com + mail= -> michelle.shestko@cascadestucson.com +[WOULD] Ramon.Castaneda + before: + after: SMTP:ramon.castaneda@cascadestucson.com; smtp:ramon.castanada@cascadestucson.com; smtp:ramon.casteneda@cascadestucson.com + mail= -> ramon.castaneda@cascadestucson.com +[WOULD] Ray.Rai + before: + after: SMTP:ray.rai@cascadestucson.com + mail= -> ray.rai@cascadestucson.com +[WOULD] Richard.Adams + before: + after: SMTP:richard.adams@cascadestucson.com + mail= -> richard.adams@cascadestucson.com +[WOULD] Sebastian.Leon + before: + after: SMTP:sebastian.leon@cascadestucson.com + mail= -> sebastian.leon@cascadestucson.com +[WOULD] Sharon.Edwards + before: + after: SMTP:sharon.edwards@cascadestucson.com + mail= -> sharon.edwards@cascadestucson.com +[WOULD] Shelby.Trozzi + before: + after: SMTP:Shelby.Trozzi@cascadestucson.com + mail= -> Shelby.Trozzi@cascadestucson.com +[WOULD] Sheldon.Gardfrey + before: + after: SMTP:sheldon.gardfrey@cascadestucson.com + mail= -> sheldon.gardfrey@cascadestucson.com +[WOULD] Shontiel.Nunn + before: + after: SMTP:shontiel.nunn@cascadestucson.com + mail= -> shontiel.nunn@cascadestucson.com +[WOULD] Susan.Hicks + before: + after: SMTP:susan.hicks@cascadestucson.com + mail= -> susan.hicks@cascadestucson.com +[WOULD] sysadmin + before: + after: SMTP:sysadmin@cascadestucson.com + mail= -> sysadmin@cascadestucson.com +[WOULD] Tamra.Matthews + before: + after: SMTP:tamra.matthews@cascadestucson.com; smtp:tamra.johnson@cascadestucson.com + mail= -> tamra.matthews@cascadestucson.com +[WOULD] Veronica.Feller + before: + after: SMTP:veronica.feller@cascadestucson.com + mail= -> veronica.feller@cascadestucson.com + +============================================================================ +== 3. Create 16 SG-* security groups (CA / file-share / break-glass) +============================================================================ +[WOULD] Create SG-External-Signin-Allowed (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Members may sign in from outside Cascades building (CA policy target). +[WOULD] Create SG-Caregivers (Global Security) in OU=Groups,DC=cascades,DC=local + desc: All shift-work caregivers. CA policy target for shared-phone mobile policy. +[WOULD] Create SG-FrontDesk (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Front desk receptionists sharing reception PCs. +[WOULD] Create SG-CourtesyPatrol (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Courtesy patrol staff. +[WOULD] Create SG-Drivers (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Transportation drivers (AD accounts being disabled 2026-04-22 - group retained for history). +[WOULD] Create SG-Management-RW (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Read/write on \\CS-SERVER\Management file share (Phase 4). +[WOULD] Create SG-Sales-RW (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Read/write on \\CS-SERVER\SalesDept file share (Phase 4). +[WOULD] Create SG-Culinary-RW (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Read/write on \\CS-SERVER\Culinary file share (Phase 4). +[WOULD] Create SG-IT-RW (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Read/write on \\CS-SERVER\IT file share (Phase 4). +[WOULD] Create SG-Receptionist-RW (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Read/write on \\CS-SERVER\Receptionist file share (Phase 4). +[WOULD] Create SG-Directory-RW (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Read/write on \\CS-SERVER\directoryshare file share (Phase 4). +[WOULD] Create SG-Server-RW (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Read/write on \\CS-SERVER\Server share (IT admin, Phase 4). +[WOULD] Create SG-Chat-RW (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Read/write on \\CS-SERVER\chat file share (Phase 4). +[WOULD] Create SG-Office-PHI-External (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Office PHI staff with external sign-in permission (CA policy). +[WOULD] Create SG-Office-PHI-Internal (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Office PHI staff limited to in-building sign-in (CA policy). +[WOULD] Create SG-CA-BreakGlass (Global Security) in OU=Groups,DC=cascades,DC=local + desc: Break-glass accounts excluded from all Conditional Access policies. + +============================================================================ +== 4. DisplayName cosmetic fixes (3 users) +============================================================================ +[WOULD] Crystal.Rodriguez DisplayName: 'Crystal Rodriguez' -> 'Crystal Rodriguez' +[WOULD] howard DisplayName: 'howard' -> 'Howard Dax' +[WOULD] Cathy.Kingston DisplayName: 'Cathy.Kingston' -> 'Cathy Kingston' + +============================================================================ +== 5. Summary +============================================================================ +Mode: DRY-RUN (no changes) +Created: 17 +Moved: 4 +Updated: 37 +Skipped: 0 +Errors: 0 + +Backup dir: D:\Backups\g1-hygiene-2026-04-22-202650 + +DRY-RUN complete. To execute: + 1. Review the [WOULD] lines above + 2. Re-run this script with $doExecute = $true + 3. Compare post-state vs pre-state CSVs in the backup dir + +Completed at 2026-04-22 20:26:51 -07:00 + +``` + +stderr: +``` + +``` diff --git a/clients/cascades-tucson/reports/2026-04-22-g1-execute.md b/clients/cascades-tucson/reports/2026-04-22-g1-execute.md new file mode 100644 index 0000000..fc50224 --- /dev/null +++ b/clients/cascades-tucson/reports/2026-04-22-g1-execute.md @@ -0,0 +1,228 @@ +# G1 AD Hygiene - EXECUTE + +**Command ID:** d49bb8dd-4916-4634-bf0c-c46bbcfcd81b +**Exit:** 0 +**Completed:** 2026-04-23T03:32:39.186512Z + +## STDOUT + +``` +G1 AD Hygiene - 2026-04-22 20:32:32 -07:00 +Host: CS-SERVER +Mode: EXECUTE +Backup dir: D:\Backups\g1-hygiene-2026-04-22-203232 + +============================================================================ +== 0. Pre-state backup (always runs) +============================================================================ +[OK] Exported users-pre.csv +[OK] Exported groups-pre.csv +[OK] Exported ous-pre.csv + +[OK] Pre-state saved at D:\Backups\g1-hygiene-2026-04-22-203232 +Rollback commands (if needed after execute): + - proxyAddresses: Set-ADUser from users-pre.csv column ProxyAddresses + - OU moves: Move-ADObject back to old DistinguishedName + - Groups created today: Remove-ADGroup (safe since memberless) + +============================================================================ +== 1. OU=Excluded-From-Sync + move 4 role accounts +============================================================================ +[DID] Created OU=Excluded-From-Sync +[DID] Moved Culinary: OU=Culinary,OU=Departments,DC=cascades,DC=local -> OU=Excluded-From-Sync,DC=cascades,DC=local +[DID] Moved Receptionist: CN=Users,DC=cascades,DC=local -> OU=Excluded-From-Sync,DC=cascades,DC=local +[DID] Moved saleshare: OU=Marketing,OU=Departments,DC=cascades,DC=local -> OU=Excluded-From-Sync,DC=cascades,DC=local +[DID] Moved directoryshare: CN=Users,DC=cascades,DC=local -> OU=Excluded-From-Sync,DC=cascades,DC=local + +============================================================================ +== 2. Populate proxyAddresses (34 users - live data from M365 Graph 2026-04-22) +============================================================================ +[DID] Allison.Reibschied + before: + after: SMTP:Allison.Reibschied@cascadestucson.com + mail=Allison.Reibschied@cascadestucson.com +[DID] Alyssa.Brooks + before: + after: SMTP:alyssa.brooks@cascadestucson.com + mail=alyssa.brooks@cascadestucson.com +[DID] Ashley.Jensen + before: + after: SMTP:ashley.jensen@cascadestucson.com; smtp:ashley.jenson@cascadestucson.com + mail=ashley.jensen@cascadestucson.com +[DID] britney.thompson + before: + after: SMTP:Britney.Thompson@cascadestucson.com + mail=Britney.Thompson@cascadestucson.com +[DID] Cathy.Kingston + before: + after: SMTP:cathy.kingston@cascadestucson.com + mail=cathy.kingston@cascadestucson.com +[DID] Christina.DuPras + before: + after: SMTP:christina.dupras@cascadestucson.com + mail=christina.dupras@cascadestucson.com +[DID] Christine.Nyanzunda + before: + after: SMTP:christine.nyanzunda@cascadestucson.com + mail=christine.nyanzunda@cascadestucson.com +[DID] Christopher.Holick + before: + after: SMTP:christopher.holick@cascadestucson.com + mail=christopher.holick@cascadestucson.com +[DID] Crystal.Rodriguez + before: + after: SMTP:crystal.rodriguez@cascadestucson.com; smtp:crystal.suszek@cascadestucson.com + mail=crystal.rodriguez@cascadestucson.com +[DID] howard + before: + after: SMTP:dax.howard@cascadestucson.com; smtp:cara.lespron@cascadestucson.com + mail=dax.howard@cascadestucson.com +[DID] JD.Martin + before: + after: SMTP:jd.martin@cascadestucson.com + mail=jd.martin@cascadestucson.com +[DID] John.Trozzi + before: + after: SMTP:john.trozzi@cascadestucson.com + mail=john.trozzi@cascadestucson.com +[DID] Julian.Crim + before: + after: SMTP:julian.crim@cascadestucson.com + mail=julian.crim@cascadestucson.com +[DID] karen.rossini + before: + after: SMTP:karen.rossini@cascadestucson.com + mail=karen.rossini@cascadestucson.com +[DID] Kyla.QuickTiffany + before: + after: SMTP:kyla.quicktiffany@cascadestucson.com + mail=kyla.quicktiffany@cascadestucson.com +[DID] lauren.hasselman + before: + after: SMTP:lauren.hasselman@cascadestucson.com + mail=lauren.hasselman@cascadestucson.com +[DID] Lois.Lane + before: + after: SMTP:lois.lane@cascadestucson.com + mail=lois.lane@cascadestucson.com +[DID] Lupe.Sanchez + before: + after: SMTP:lupe.sanchez@cascadestucson.com + mail=lupe.sanchez@cascadestucson.com +[DID] Matt.Brooks + before: + after: SMTP:matthew.brooks@cascadestucson.com + mail=matthew.brooks@cascadestucson.com +[DID] Megan.Hiatt + before: + after: SMTP:megan.hiatt@cascadestucson.com + mail=megan.hiatt@cascadestucson.com +[DID] Meredith.Kuhn + before: + after: SMTP:meredith.kuhn@cascadestucson.com + mail=meredith.kuhn@cascadestucson.com +[DID] Michelle.Shestko + before: + after: SMTP:michelle.shestko@cascadestucson.com + mail=michelle.shestko@cascadestucson.com +[DID] Ramon.Castaneda + before: + after: SMTP:ramon.castaneda@cascadestucson.com; smtp:ramon.castanada@cascadestucson.com; smtp:ramon.casteneda@cascadestucson.com + mail=ramon.castaneda@cascadestucson.com +[DID] Ray.Rai + before: + after: SMTP:ray.rai@cascadestucson.com + mail=ray.rai@cascadestucson.com +[DID] Richard.Adams + before: + after: SMTP:richard.adams@cascadestucson.com + mail=richard.adams@cascadestucson.com +[DID] Sebastian.Leon + before: + after: SMTP:sebastian.leon@cascadestucson.com + mail=sebastian.leon@cascadestucson.com +[DID] Sharon.Edwards + before: + after: SMTP:sharon.edwards@cascadestucson.com + mail=sharon.edwards@cascadestucson.com +[DID] Shelby.Trozzi + before: + after: SMTP:Shelby.Trozzi@cascadestucson.com + mail=Shelby.Trozzi@cascadestucson.com +[DID] Sheldon.Gardfrey + before: + after: SMTP:sheldon.gardfrey@cascadestucson.com + mail=sheldon.gardfrey@cascadestucson.com +[DID] Shontiel.Nunn + before: + after: SMTP:shontiel.nunn@cascadestucson.com + mail=shontiel.nunn@cascadestucson.com +[DID] Susan.Hicks + before: + after: SMTP:susan.hicks@cascadestucson.com + mail=susan.hicks@cascadestucson.com +[DID] sysadmin + before: + after: SMTP:sysadmin@cascadestucson.com + mail=sysadmin@cascadestucson.com +[DID] Tamra.Matthews + before: + after: SMTP:tamra.matthews@cascadestucson.com; smtp:tamra.johnson@cascadestucson.com + mail=tamra.matthews@cascadestucson.com +[DID] Veronica.Feller + before: + after: SMTP:veronica.feller@cascadestucson.com + mail=veronica.feller@cascadestucson.com + +============================================================================ +== 3. Create 16 SG-* security groups (CA / file-share / break-glass) +============================================================================ +[DID] Created SG-External-Signin-Allowed +[DID] Created SG-Caregivers +[DID] Created SG-FrontDesk +[DID] Created SG-CourtesyPatrol +[DID] Created SG-Drivers +[DID] Created SG-Management-RW +[DID] Created SG-Sales-RW +[DID] Created SG-Culinary-RW +[DID] Created SG-IT-RW +[DID] Created SG-Receptionist-RW +[DID] Created SG-Directory-RW +[DID] Created SG-Server-RW +[DID] Created SG-Chat-RW +[DID] Created SG-Office-PHI-External +[DID] Created SG-Office-PHI-Internal +[DID] Created SG-CA-BreakGlass + +============================================================================ +== 4. DisplayName cosmetic fixes (3 users) +============================================================================ +[DID] Crystal.Rodriguez DisplayName: 'Crystal Rodriguez' -> 'Crystal Rodriguez' +[DID] howard DisplayName: 'howard' -> 'Howard Dax' +[DID] Cathy.Kingston DisplayName: 'Cathy.Kingston' -> 'Cathy Kingston' + +============================================================================ +== 5. Summary +============================================================================ +Mode: EXECUTE +Created: 17 +Moved: 4 +Updated: 37 +Skipped: 0 +Errors: 0 + +Backup dir: D:\Backups\g1-hygiene-2026-04-22-203232 + +EXECUTE complete. Recommended next steps: + 1. Re-run in DRY-RUN to confirm 0 [WOULD] entries (idempotency check) + 2. Export users-post.csv for the audit trail (in D:\Backups\g1-hygiene-2026-04-22-203232) + 3. Proceed to Gate G2 (M365 role-account shared-mailbox conversion) + +Completed at 2026-04-22 20:32:38 -07:00 + +``` + +stderr: +``` + +``` diff --git a/clients/cascades-tucson/reports/2026-04-22-g1-post-verify.md b/clients/cascades-tucson/reports/2026-04-22-g1-post-verify.md new file mode 100644 index 0000000..dacf1cf --- /dev/null +++ b/clients/cascades-tucson/reports/2026-04-22-g1-post-verify.md @@ -0,0 +1,121 @@ +# G1 AD Hygiene - Post-Execute Idempotency Verification + +**Command ID:** 2bd999a0-b9a1-4599-a44b-d90f32a332ad +**Exit:** 0 +**Completed:** 2026-04-23T03:33:28.670135Z + +## STDOUT + +``` +G1 AD Hygiene - 2026-04-22 20:33:27 -07:00 +Host: CS-SERVER +Mode: DRY-RUN (no changes) +Backup dir: D:\Backups\g1-hygiene-2026-04-22-203327 + +============================================================================ +== 0. Pre-state backup (always runs) +============================================================================ +[OK] Exported users-pre.csv +[OK] Exported groups-pre.csv +[OK] Exported ous-pre.csv + +[OK] Pre-state saved at D:\Backups\g1-hygiene-2026-04-22-203327 +Rollback commands (if needed after execute): + - proxyAddresses: Set-ADUser from users-pre.csv column ProxyAddresses + - OU moves: Move-ADObject back to old DistinguishedName + - Groups created today: Remove-ADGroup (safe since memberless) + +============================================================================ +== 1. OU=Excluded-From-Sync + move 4 role accounts +============================================================================ +[SKIP] OU=Excluded-From-Sync already exists +[SKIP] Culinary already in Excluded-From-Sync +[SKIP] Receptionist already in Excluded-From-Sync +[SKIP] saleshare already in Excluded-From-Sync +[SKIP] directoryshare already in Excluded-From-Sync + +============================================================================ +== 2. Populate proxyAddresses (34 users - live data from M365 Graph 2026-04-22) +============================================================================ +[SKIP] Allison.Reibschied proxyAddresses already current +[SKIP] Alyssa.Brooks proxyAddresses already current +[SKIP] Ashley.Jensen proxyAddresses already current +[SKIP] britney.thompson proxyAddresses already current +[SKIP] Cathy.Kingston proxyAddresses already current +[SKIP] Christina.DuPras proxyAddresses already current +[SKIP] Christine.Nyanzunda proxyAddresses already current +[SKIP] Christopher.Holick proxyAddresses already current +[SKIP] Crystal.Rodriguez proxyAddresses already current +[SKIP] howard proxyAddresses already current +[SKIP] JD.Martin proxyAddresses already current +[SKIP] John.Trozzi proxyAddresses already current +[SKIP] Julian.Crim proxyAddresses already current +[SKIP] karen.rossini proxyAddresses already current +[SKIP] Kyla.QuickTiffany proxyAddresses already current +[SKIP] lauren.hasselman proxyAddresses already current +[SKIP] Lois.Lane proxyAddresses already current +[SKIP] Lupe.Sanchez proxyAddresses already current +[SKIP] Matt.Brooks proxyAddresses already current +[SKIP] Megan.Hiatt proxyAddresses already current +[SKIP] Meredith.Kuhn proxyAddresses already current +[SKIP] Michelle.Shestko proxyAddresses already current +[SKIP] Ramon.Castaneda proxyAddresses already current +[SKIP] Ray.Rai proxyAddresses already current +[SKIP] Richard.Adams proxyAddresses already current +[SKIP] Sebastian.Leon proxyAddresses already current +[SKIP] Sharon.Edwards proxyAddresses already current +[SKIP] Shelby.Trozzi proxyAddresses already current +[SKIP] Sheldon.Gardfrey proxyAddresses already current +[SKIP] Shontiel.Nunn proxyAddresses already current +[SKIP] Susan.Hicks proxyAddresses already current +[SKIP] sysadmin proxyAddresses already current +[SKIP] Tamra.Matthews proxyAddresses already current +[SKIP] Veronica.Feller proxyAddresses already current + +============================================================================ +== 3. Create 16 SG-* security groups (CA / file-share / break-glass) +============================================================================ +[SKIP] SG-External-Signin-Allowed already exists +[SKIP] SG-Caregivers already exists +[SKIP] SG-FrontDesk already exists +[SKIP] SG-CourtesyPatrol already exists +[SKIP] SG-Drivers already exists +[SKIP] SG-Management-RW already exists +[SKIP] SG-Sales-RW already exists +[SKIP] SG-Culinary-RW already exists +[SKIP] SG-IT-RW already exists +[SKIP] SG-Receptionist-RW already exists +[SKIP] SG-Directory-RW already exists +[SKIP] SG-Server-RW already exists +[SKIP] SG-Chat-RW already exists +[SKIP] SG-Office-PHI-External already exists +[SKIP] SG-Office-PHI-Internal already exists +[SKIP] SG-CA-BreakGlass already exists + +============================================================================ +== 4. DisplayName cosmetic fixes (3 users) +============================================================================ +[SKIP] Crystal.Rodriguez DisplayName already 'Crystal Rodriguez' +[SKIP] howard DisplayName already 'Howard Dax' +[SKIP] Cathy.Kingston DisplayName already 'Cathy Kingston' + +============================================================================ +== 5. Summary +============================================================================ +Mode: DRY-RUN (no changes) +Created: 0 +Moved: 0 +Updated: 0 +Skipped: 0 +Errors: 0 + +Backup dir: D:\Backups\g1-hygiene-2026-04-22-203327 + +DRY-RUN complete. To execute: + 1. Review the [WOULD] lines above + 2. Re-run this script with $doExecute = $true + 3. Compare post-state vs pre-state CSVs in the backup dir + +Completed at 2026-04-22 20:33:28 -07:00 + +``` diff --git a/clients/cascades-tucson/reports/2026-04-22-howard-account-cleanup.md b/clients/cascades-tucson/reports/2026-04-22-howard-account-cleanup.md new file mode 100644 index 0000000..06ffb5f --- /dev/null +++ b/clients/cascades-tucson/reports/2026-04-22-howard-account-cleanup.md @@ -0,0 +1,82 @@ +# Howard Account Cleanup - 2026-04-22 + +## Context + +Two separate humans share the name "Howard": +- **Howard Enos** — MSP tech at Computer Guru. Accesses Cascades tenant. +- **Dax Howard** — a person who runs Cascades. Has the `dax.howard@cascadestucson.com` member mailbox. + +Prior documentation (`docs/cloud/m365.md` as it existed) incorrectly mapped the AD `howard` account to Dax Howard's M365 mailbox, conflating the two identities. The G1 hygiene script executed earlier tonight populated the AD `howard` account's `proxyAddresses` / `mail` / `DisplayName` with Dax Howard's attributes — a genuine error that would have caused a duplicate / orphan at Entra Connect sync time. + +## Decision (Howard Enos, 2026-04-22) + +1. **Remove the external guest** `howard@azcomputerguru.com` from Cascades tenant — was redundant; MSP access preserved via `sysadmin@cascadestucson.com`. +2. **Remove the AD `howard` account** — orphan MSP-created account (desc "Home Offie" typo, PasswordNeverExpires, unused group memberships). Not in active use. +3. **Leave `dax.howard@cascadestucson.com` alone** — Dax Howard's real Cascades mailbox. Keep as-is. + +## Pre-state checks (safety) + +- **Global Admin role membership** (Graph): `sysadmin@cascadestucson.com` (Computer Guru Support) holds the Global Administrator role. Admin path preserved after guest removal. +- **Guest's role/group memberships** (Graph): none. Pure sign-in account with no authorization side effects. +- **AD `howard`'s group memberships** (AD): only `Domain Users` (default). No custom groups depending on it. + +## Actions executed + +### 1. Deleted M365 guest `howard@azcomputerguru.com` + +- Tier: `user-manager` (Graph write) +- Object ID: `db2aee97-9c5d-40ce-8610-b75efc3ca906` +- UPN: `howard_azcomputerguru.com#EXT#@NETORGFT4257522.onmicrosoft.com` +- HTTP: 204 (success) +- Verify: subsequent GET returns 404 Request_ResourceNotFound +- **Soft-delete recovery window:** 30 days in Entra (`Restore-MgDirectoryDeletedItem`) + +### 2. Deleted AD user `howard` + +- Ran via GuruRMM on CS-SERVER (agent `6766e973-e703-47c1-be56-76950290f87c`) +- Script: `docs/migration/scripts/ad-howard-delete.ps1` +- Pre-state exported: `D:\Backups\howard-delete-2026-04-22-205158\howard-pre.xml` +- Pre-state captured: + - SAM=howard + - UPN=howard@cascadestucson.com + - Display="Howard Dax" (wrong value from earlier G1 script — was correct to delete) + - Description="Home Offie" (typo) + - mail=dax.howard@cascadestucson.com (wrong value from G1 — was correct to delete) + - proxyAddresses=SMTP:dax.howard@cascadestucson.com, smtp:cara.lespron@cascadestucson.com (wrong — belonged to Dax) + - Groups: Domain Users only +- Removed via `Remove-ADUser -Identity howard -Confirm:$false` +- Verified: `Get-ADUser -Identity howard` returns "Cannot find an object" +- **AD Recycle Bin recovery window:** 180 days +- **Rollback:** `Restore-ADObject -Identity 2050d21f-7649-4033-b1fd-83cfc286b056` + +### 3. Updated docs/cloud/m365.md + +- Corrected the `howard | dax.howard@` mapping line (root cause of the confusion) +- Struck through the `howard@azcomputerguru.com` + `howaed@azcomputerguru.com` external guest entries (both no longer in tenant) + +## Dax Howard — open questions + +Dax Howard has a Cascades M365 member account (`dax.howard@cascadestucson.com`, Business Standard license, alias `cara.lespron@`) but no AD account. As of 2026-04-22, he's: + +- Not on the returned staff CSV from Meredith/John (`reports/cascades-staff-2026-04-22.csv`) +- Not on the working account-setup list (`docs/cloud/cascades-staff-working-list-2026-04-22.md`) +- Has an active licensed mailbox + +**Questions to ask Meredith when convenient:** +1. Who is Dax Howard? (executive / regional / legacy?) +2. Is his mailbox actively used? +3. Should he have an AD account created for hybrid identity? +4. Is the `cara.lespron@` alias still needed, or can it be removed? + +Not blocking Wave 0.5 — he's cloud-only and stays that way unless an AD counterpart is added. + +## Impact on Entra Connect sync plan + +- AD now has 41 enabled users (was 42, howard removed). +- The earlier G1 hygiene run's wrong `proxyAddresses` / `mail` / `DisplayName` attributes on AD `howard` are gone with the account — no lingering mismatch to clean up. +- Dax Howard stays cloud-only; his M365 account will show as "Cloud Only" in Entra admin post-sync, separate from the AD-synced population. No conflict. +- No need to re-run the G1 hygiene script — the deletion is idempotent. + +## Session log for future reference + +Added this cleanup as a concrete lesson: **always verify identity mapping from live Graph data, not from prior doc state**, before bulk AD attribute changes. The G1 script's proxyAddresses mapping came from `docs/cloud/m365.md` which had the wrong assumption baked in. The error was caught immediately by Howard spotting the dual-identity before it reached a sync attempt.