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>
355 lines
15 KiB
PowerShell
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
|