Files
claudetools/clients/cascades-tucson/docs/migration/scripts/phase2-ou-cleanup.ps1
Howard Enos 8d975c1b44 import: ingested 160 files from C:\Users\howar\Clients
Howard's personal MSP client documentation folder imported into shared
ClaudeTools repo via /import command. Scope:

Clients (structured MSP docs under clients/<name>/docs/):
- anaise       (NEW)  - 13 files
- cascades-tucson     - 47 files merged (existing had only reports/)
- dataforth           - 18 files merged (alongside incident reports)
- instrumental-music-center - 14 files merged
- khalsa       (NEW)  - 22 files, multi-site (camden, river)
- kittle       (NEW)  - 16 files incl. fix-pdf-preview, gpo-intranet-zone
- lens-auto-brokerage (NEW) - 3 files (name matches SOPS vault)
- _client_template    - 13-file scaffold for new clients

MSP tooling (projects/msp-tools/):
- msp-audit-scripts/ - server_audit.ps1, workstation_audit.ps1, README
- utilities/         - clean_printer_ports, win11_upgrade,
                       screenconnect-toolbox-commands

Credential handling:
- Extracted 1 inline password (Anaise DESKTOP-O8GF4SD / david)
  to SOPS vault: clients/anaise/desktop-o8gf4sd.sops.yaml
- Redacted overview.md with vault reference pattern
- Scanned all 160 files for keys/tokens/connection strings -
  no other credentials found

Skipped:
- Cascades/.claude/settings.local.json (per-machine config)
- Source-root CLAUDE.md (personal, claudetools has its own)
- scripts/server_audit.ps1 and workstation_audit.ps1 at source root
  (identical duplicates of msp-audit-scripts versions)

Memory updates:
- reference_client_docs_structure.md (layout, conventions, active list)
- reference_msp_audit_scripts.md (locations, ScreenConnect 80-char rule)

Session log: session-logs/2026-04-16-howard-client-docs-import.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 19:43:58 -07:00

355 lines
15 KiB
PowerShell

#Requires -RunAsAdministrator
<#
.SYNOPSIS
Phase 2.1: OU Structure Cleanup on CS-SERVER.
.DESCRIPTION
Audits and removes duplicate/empty root-level OUs, fixes misspelling,
handles CN=Users account cleanup. Run BEFORE phase2-ad-setup.ps1.
Run on CS-SERVER via ScreenConnect.
.NOTES
Step 1: Read-only audit (always runs)
Step 2: Delete duplicate root OUs (requires $DeleteOUs = $true)
Step 3: Delete empty Managment/MemCare/Sales OUs (requires $DeleteOUs = $true)
Step 4: Delete/disable stale accounts in CN=Users (requires $DeleteAccounts = $true)
Step 5: Flag Lupe.Sanchez for review
#>
Import-Module ActiveDirectory -ErrorAction Stop
# --- SAFETY FLAGS ---
$DeleteOUs = $false # Set $true to delete empty root-level OUs
$DeleteAccounts = $false # Set $true to delete/disable stale accounts in CN=Users
$Domain = "DC=cascades,DC=local"
Write-Host "=== Phase 2.1: OU Structure Cleanup ===" -ForegroundColor Cyan
Write-Host ""
# ============================================================
# STEP 1: Audit root-level duplicate OUs (READ-ONLY)
# ============================================================
Write-Host "--- Step 1: Auditing Root-Level Duplicate OUs ---" -ForegroundColor Yellow
Write-Host "These OUs exist at root AND under Departments. Root copies should be empty." -ForegroundColor DarkGray
Write-Host ""
$rootDuplicateOUs = @(
"OU=Administrative,$Domain",
"OU=Care-Assisted Living,$Domain",
"OU=Care-Memorycare,$Domain",
"OU=Culinary,$Domain",
"OU=Housekeeping,$Domain",
"OU=Life Enrichment,$Domain",
"OU=Maintenance,$Domain",
"OU=Marketing,$Domain",
"OU=Resident Services,$Domain",
"OU=Transportation,$Domain"
)
$allEmpty = $true
$ouResults = @{}
foreach ($ou in $rootDuplicateOUs) {
$ouName = ($ou -split ',')[0] -replace 'OU=',''
try {
$objects = Get-ADObject -SearchBase $ou -SearchScope OneLevel -Filter * -ErrorAction Stop
$props = Get-ADOrganizationalUnit $ou -Properties gPLink, ProtectedFromAccidentalDeletion -ErrorAction Stop
Write-Host " === $ouName (root) ===" -ForegroundColor White
if ($objects) {
$allEmpty = $false
$ouResults[$ou] = "HAS_OBJECTS"
foreach ($obj in $objects) {
Write-Host " $($obj.ObjectClass): $($obj.Name)" -ForegroundColor Red
}
} else {
$ouResults[$ou] = "EMPTY"
Write-Host " Empty" -ForegroundColor Green
}
$gpLink = if ($props.gPLink) { $props.gPLink } else { "(none)" }
Write-Host " GPLink: $gpLink" -ForegroundColor DarkGray
Write-Host " Protected: $($props.ProtectedFromAccidentalDeletion)" -ForegroundColor DarkGray
Write-Host ""
}
catch {
Write-Host " === $ouName (root) ===" -ForegroundColor White
Write-Host " NOT FOUND — may already be deleted" -ForegroundColor DarkGray
$ouResults[$ou] = "NOT_FOUND"
Write-Host ""
}
}
# Also audit Managment, MemCare, Sales
Write-Host "--- Auditing Managment, MemCare, Sales Root OUs ---" -ForegroundColor Yellow
Write-Host ""
$miscRootOUs = @(
"OU=Managment,$Domain",
"OU=MemCare,$Domain",
"OU=Sales,$Domain"
)
foreach ($ou in $miscRootOUs) {
$ouName = ($ou -split ',')[0] -replace 'OU=',''
try {
$objects = Get-ADObject -SearchBase $ou -SearchScope OneLevel -Filter * -ErrorAction Stop
$props = Get-ADOrganizationalUnit $ou -Properties gPLink, ProtectedFromAccidentalDeletion -ErrorAction Stop
Write-Host " === $ouName (root) ===" -ForegroundColor White
if ($objects) {
$allEmpty = $false
$ouResults[$ou] = "HAS_OBJECTS"
foreach ($obj in $objects) {
Write-Host " $($obj.ObjectClass): $($obj.Name)" -ForegroundColor Red
}
} else {
$ouResults[$ou] = "EMPTY"
Write-Host " Empty" -ForegroundColor Green
}
$gpLink = if ($props.gPLink) { $props.gPLink } else { "(none)" }
Write-Host " GPLink: $gpLink" -ForegroundColor DarkGray
Write-Host " Protected: $($props.ProtectedFromAccidentalDeletion)" -ForegroundColor DarkGray
Write-Host ""
}
catch {
Write-Host " === $ouName (root) ===" -ForegroundColor White
Write-Host " NOT FOUND — may already be deleted" -ForegroundColor DarkGray
$ouResults[$ou] = "NOT_FOUND"
Write-Host ""
}
}
if ($allEmpty) {
Write-Host " All audited OUs are EMPTY — safe to delete." -ForegroundColor Green
} else {
Write-Host " WARNING: Some OUs contain objects! Review output above before deleting." -ForegroundColor Red
}
Write-Host ""
# ============================================================
# STEP 2: Delete root-level duplicate OUs (if empty)
# ============================================================
Write-Host "--- Step 2: Delete Root-Level Duplicate Department OUs ---" -ForegroundColor Yellow
if ($DeleteOUs) {
foreach ($ou in $rootDuplicateOUs) {
$ouName = ($ou -split ',')[0] -replace 'OU=',''
if ($ouResults[$ou] -eq "EMPTY") {
try {
# Remove accidental deletion protection
Set-ADOrganizationalUnit $ou -ProtectedFromAccidentalDeletion $false -ErrorAction Stop
# Delete the OU
Remove-ADOrganizationalUnit $ou -Confirm:$false -ErrorAction Stop
Write-Host " [OK] Deleted root-level OU: $ouName" -ForegroundColor Green
}
catch {
Write-Host " [ERROR] Failed to delete $ouName : $_" -ForegroundColor Red
}
}
elseif ($ouResults[$ou] -eq "HAS_OBJECTS") {
Write-Host " [SKIP] $ouName has objects — move them to Departments\$ouName first!" -ForegroundColor Red
}
elseif ($ouResults[$ou] -eq "NOT_FOUND") {
Write-Host " [SKIP] $ouName already deleted" -ForegroundColor DarkGray
}
}
} else {
Write-Host " [WARN] Deletion SKIPPED — set `$DeleteOUs = `$true after reviewing audit output" -ForegroundColor Yellow
}
Write-Host ""
# ============================================================
# STEP 3: Delete empty Managment, MemCare, Sales root OUs
# ============================================================
Write-Host "--- Step 3: Delete Managment, MemCare, Sales Root OUs ---" -ForegroundColor Yellow
if ($DeleteOUs) {
foreach ($ou in $miscRootOUs) {
$ouName = ($ou -split ',')[0] -replace 'OU=',''
if ($ouResults[$ou] -eq "EMPTY") {
try {
Set-ADOrganizationalUnit $ou -ProtectedFromAccidentalDeletion $false -ErrorAction Stop
Remove-ADOrganizationalUnit $ou -Confirm:$false -ErrorAction Stop
Write-Host " [OK] Deleted root-level OU: $ouName" -ForegroundColor Green
}
catch {
Write-Host " [ERROR] Failed to delete $ouName : $_" -ForegroundColor Red
}
}
elseif ($ouResults[$ou] -eq "HAS_OBJECTS") {
Write-Host " [SKIP] $ouName has objects — review and move/delete contents first!" -ForegroundColor Red
}
elseif ($ouResults[$ou] -eq "NOT_FOUND") {
Write-Host " [SKIP] $ouName already deleted" -ForegroundColor DarkGray
}
}
} else {
Write-Host " [WARN] Deletion SKIPPED — set `$DeleteOUs = `$true after reviewing audit output" -ForegroundColor Yellow
}
Write-Host ""
# ============================================================
# STEP 4: Handle stale accounts in CN=Users
# ============================================================
Write-Host "--- Step 4: CN=Users Account Cleanup ---" -ForegroundColor Yellow
# First, show what's in CN=Users
Write-Host " Current accounts in CN=Users:" -ForegroundColor DarkGray
$usersContainer = "CN=Users,$Domain"
$cnUsers = Get-ADUser -SearchBase $usersContainer -SearchScope OneLevel -Filter * -Properties Enabled, Description, LastLogonDate
foreach ($u in $cnUsers | Sort-Object Enabled, Name) {
$status = if ($u.Enabled) { "Enabled " } else { "Disabled" }
$lastLogon = if ($u.LastLogonDate) { $u.LastLogonDate.ToString("yyyy-MM-dd") } else { "Never" }
Write-Host " [$status] $($u.SamAccountName) — Last logon: $lastLogon" -ForegroundColor $(if ($u.Enabled) { "White" } else { "DarkGray" })
}
Write-Host ""
# Already disabled — delete immediately
$disabledToDelete = @(
"Anna.Pitzlin",
"Nela.Durut-Azizi",
"Jodi.Ramstack",
"Monica.Ramirez"
)
# Enabled but former employees — disable then delete
$enabledToRemove = @(
"alyssa.brooks",
"ann.dery",
"Cathy.Reece",
"Haris.Durut",
"Isabella.Islas",
"Kelly.Wallace",
"Nuria.Diaz"
)
if ($DeleteAccounts) {
Write-Host " Deleting disabled accounts from CN=Users..." -ForegroundColor Yellow
foreach ($acct in $disabledToDelete) {
try {
# Remove from all groups first (especially Domain Admins — Monica.Ramirez!)
$user = Get-ADUser $acct -Properties MemberOf -ErrorAction Stop
foreach ($group in $user.MemberOf) {
$groupName = (Get-ADGroup $group).Name
if ($groupName -ne "Domain Users") {
Remove-ADGroupMember -Identity $group -Members $acct -Confirm:$false -ErrorAction SilentlyContinue
Write-Host " [OK] Removed $acct from $groupName" -ForegroundColor Green
}
}
Remove-ADUser -Identity $acct -Confirm:$false -ErrorAction Stop
Write-Host " [OK] Deleted: $acct" -ForegroundColor Green
}
catch {
Write-Host " [SKIP] $acct not found or error: $_" -ForegroundColor DarkGray
}
}
Write-Host ""
Write-Host " Disabling + deleting former employee accounts..." -ForegroundColor Yellow
foreach ($acct in $enabledToRemove) {
try {
# Disable first
Disable-ADAccount -Identity $acct -ErrorAction SilentlyContinue
# Remove from all groups
$user = Get-ADUser $acct -Properties MemberOf -ErrorAction Stop
foreach ($group in $user.MemberOf) {
$groupName = (Get-ADGroup $group).Name
if ($groupName -ne "Domain Users") {
Remove-ADGroupMember -Identity $group -Members $acct -Confirm:$false -ErrorAction SilentlyContinue
Write-Host " [OK] Removed $acct from $groupName" -ForegroundColor Green
}
}
Remove-ADUser -Identity $acct -Confirm:$false -ErrorAction Stop
Write-Host " [OK] Disabled + Deleted: $acct" -ForegroundColor Green
}
catch {
Write-Host " [SKIP] $acct not found or error: $_" -ForegroundColor DarkGray
}
}
} else {
Write-Host " [WARN] Account deletion SKIPPED — set `$DeleteAccounts = `$true to execute" -ForegroundColor Yellow
Write-Host ""
Write-Host " Disabled accounts to delete:" -ForegroundColor Yellow
foreach ($acct in $disabledToDelete) { Write-Host " - $acct" -ForegroundColor DarkGray }
Write-Host " Enabled accounts to disable + delete (NOT in HR):" -ForegroundColor Yellow
foreach ($acct in $enabledToRemove) { Write-Host " - $acct" -ForegroundColor DarkGray }
}
Write-Host ""
# ============================================================
# STEP 5: Flag Lupe.Sanchez for review
# ============================================================
Write-Host "--- Step 5: Lupe.Sanchez Review ---" -ForegroundColor Yellow
try {
$lupe = Get-ADUser -Identity "Lupe.Sanchez" -Properties Enabled, LastLogonDate, Description, DistinguishedName -ErrorAction Stop
$guadalupe = Get-ADUser -Identity "Guadalupe.Sanchez" -Properties Enabled, LastLogonDate, Description, DistinguishedName -ErrorAction SilentlyContinue
Write-Host " Lupe.Sanchez:" -ForegroundColor White
Write-Host " DN: $($lupe.DistinguishedName)" -ForegroundColor DarkGray
Write-Host " Enabled: $($lupe.Enabled)" -ForegroundColor $(if ($lupe.Enabled) { "Yellow" } else { "DarkGray" })
Write-Host " Last Logon: $($lupe.LastLogonDate)" -ForegroundColor DarkGray
Write-Host ""
if ($guadalupe) {
Write-Host " Guadalupe.Sanchez (possible duplicate):" -ForegroundColor White
Write-Host " DN: $($guadalupe.DistinguishedName)" -ForegroundColor DarkGray
Write-Host " Enabled: $($guadalupe.Enabled)" -ForegroundColor DarkGray
Write-Host " Last Logon: $($guadalupe.LastLogonDate)" -ForegroundColor DarkGray
Write-Host ""
Write-Host " ** REVIEW: Lupe.Sanchez may be a duplicate of Guadalupe.Sanchez (Housekeeping)." -ForegroundColor Red
Write-Host " ** Both accounts exist. Check with client which to keep." -ForegroundColor Red
}
}
catch {
Write-Host " Lupe.Sanchez not found in AD" -ForegroundColor DarkGray
}
Write-Host ""
# ============================================================
# STEP 6: Accounts that should STAY in CN=Users
# ============================================================
Write-Host "--- Accounts staying in CN=Users (system/service) ---" -ForegroundColor Yellow
$keepInUsers = @("Administrator", "Guest", "krbtgt", "localadmin", "sysadmin", "QBDataServiceUser34")
foreach ($acct in $keepInUsers) {
try {
$user = Get-ADUser $acct -Properties Enabled -ErrorAction SilentlyContinue
if ($user) {
$status = if ($user.Enabled) { "Enabled" } else { "Disabled" }
Write-Host " [$status] $acct — Staying in CN=Users (system/service account)" -ForegroundColor DarkGray
}
}
catch {}
}
Write-Host ""
# Accounts needing client decision
Write-Host "--- Accounts needing client decision ---" -ForegroundColor Yellow
Write-Host " Receptionist — shared account, currently in CN=Users. Move to Departments\Resident Services?" -ForegroundColor Yellow
Write-Host " directoryshare — shared account, currently in CN=Users. Keep as service account?" -ForegroundColor Yellow
Write-Host " Lupe.Sanchez — see review above. Possible duplicate of Guadalupe.Sanchez." -ForegroundColor Yellow
Write-Host ""
# ============================================================
# SUMMARY: Final OU structure
# ============================================================
Write-Host "=== Final OU Structure ===" -ForegroundColor Cyan
Write-Host ""
$allOUs = Get-ADOrganizationalUnit -Filter * -Properties ProtectedFromAccidentalDeletion |
Sort-Object DistinguishedName |
Select-Object Name, DistinguishedName, ProtectedFromAccidentalDeletion
foreach ($ou in $allOUs) {
# Calculate depth for indentation
$depth = ($ou.DistinguishedName -split ',' | Where-Object { $_ -match '^OU=' }).Count - 1
$indent = " " * $depth
$protected = if ($ou.ProtectedFromAccidentalDeletion) { "" } else { " [UNPROTECTED]" }
Write-Host " $indent$($ou.Name)$protected" -ForegroundColor White
}
Write-Host ""
Write-Host "=== OU Cleanup Complete ===" -ForegroundColor Cyan
Write-Host "Next: Run phase2-ad-setup.ps1 (security fixes, groups, computer moves)" -ForegroundColor Green