La gestione degli account utente è una responsabilità fondamentale per gli amministratori IT. Gli account utente orfani, ovvero non più associati a un utente attivo, comportano rischi quali l’utilizzo inutile di risorse, potenziali fughe di dati e vulnerabilità della sicurezza. Lo script PowerShell qui analizzato offre un approccio automatizzato per identificare e gestire gli account utente orfani in modo efficiente.
Contesto
Negli ambienti multiutente, soprattutto nelle aziende o nelle organizzazioni gestite da Managed Service Provider (MSP), gli account utente vengono spesso abbandonati quando i dipendenti se ne vanno, cambiano ruolo o quando i sistemi vengono ristrutturati. Questi profili orfani si accumulano nel sistema, occupando spazio prezioso su disco e potenzialmente rendendo potenzialmente vulnerabili delle informazioni sensibili. Questo script non solo rileva i profili orfani, ma fornisce anche report dettagliati, aiutando i professionisti IT a mantenere i loro sistemi ordinati e sicuri.
Lo script per gestire gli account utente orfani:
#Requires -Version 5.1 <# .SYNOPSIS Looks for user profile folders that do not have an associated user account. .DESCRIPTION Looks for user profile folders that do not have an associated user account. By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). .EXAMPLE (No Parameters) [Alert] Orphaned profiles found! Username Size Path SID -------- ---- ---- --- UNKNOWN 554.97 MB C:\Users\tuser1 S-1-5-21-1255570301-2732419320-3119746845-1104 UNKNOWN 86.74 MB C:\Users\tuser22 S-1-5-21-3797121902-2219393589-2867574441-1001 PARAMETER: -CustomField "ReplaceMeWithTheNameOfaWYSIWYGcustomField" Specify the name of an optional WYSIWYG Custom Field to save the results in. .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release #> [CmdletBinding()] param ( [Parameter()] [String[]]$DirectoriesToIgnore = ("All Users", "Default", "Default User", "Public"), [Parameter()] [String]$CustomField ) begin { # Set parameters using dynamic script variables. if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $CustomField = $env:wysiwygCustomFieldName } # Function to convert a size in bytes to a more human-readable format function Get-FriendlySize { param($Bytes) # Array of size units $Sizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' # Loop to find the appropriate size unit for ($i = 0; ($Bytes -ge 1kb) -and ($i -lt $Sizes.Count); $i++) { $Bytes /= 1kb } # Number of decimal places to show $decimalPlaces = 2 # If the size is in bytes, no decimal places are needed if ($i -eq 0) { $decimalPlaces = 0 } # Return the rounded size with the appropriate unit if ($Bytes) { "$([System.Math]::Round($Bytes,$decimalPlaces)) $($Sizes[$i])" }else { "0 B" } } # Function to retrieve user profile hives from the registry function Get-UserHives { param ( [Parameter()] [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] [String]$Type = "All", [Parameter()] [String[]]$ExcludedUsers, [Parameter()] [switch]$IncludeDefault ) # Define patterns to match user SIDs based on the Type parameter $Patterns = switch ($Type) { "AzureAD" { "S-1-12-1-(\d+-?){4}$" } "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } } # Retrieve user profiles from the registry $UserProfiles = Foreach ($Pattern in $Patterns) { Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | Where-Object { $_.PSChildName -match $Pattern } | Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, @{Name = "Path"; Expression = { $_.ProfileImagePath } }, @{Name = "Size"; Expression = { $(Get-ChildItem -Path $_.ProfileImagePath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Sum Length -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Sum -ErrorAction SilentlyContinue) } } } # Include the default profile if specified switch ($IncludeDefault) { $True { $DefaultProfile = "" | Select-Object Username, SID, UserHive, Path $DefaultProfile.Username = "Default" $DefaultProfile.SID = "DefaultProfile" $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" $DefaultProfile.Path = "C:\Users\Default" $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username } } } # Iterate over the user profiles to resolve user names from SIDs $UserProfiles | ForEach-Object { $SidObject = $Null $NewUsername = $Null # Convert SID to user name if ($_.SID) { try { $SidObject = New-Object System.Security.Principal.SecurityIdentifier($_.SID) $NewUsername = $SidObject.Translate([System.Security.Principal.NTAccount]) } catch { $NewUsername = $Null } } # Assign the resolved user name or "UNKNOWN" if resolution fails if ($NewUsername.Value) { $_.Username = $NewUsername.Value } else { $_.Username = "UNKNOWN" } } $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Username } } function Set-NinjaProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [String]$Name, [Parameter()] [String]$Type, [Parameter(Mandatory = $True, ValueFromPipeline = $True)] $Value, [Parameter()] [String]$DocumentName ) $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters if ($Characters -ge 200000) { throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") } # If requested to set the field value for a Ninja document, specify it here. $DocumentationParams = @{} if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # The field below requires additional information to set. $NeedsOptions = "Dropdown" if ($DocumentName) { if ($NeedsOptions -contains $Type) { # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 } } else { if ($NeedsOptions -contains $Type) { $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 } } # If an error is received with an exception property, exit the function with that error information. if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. switch ($Type) { "Checkbox" { # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. $NinjaValue = [System.Convert]::ToBoolean($Value) } "Date or Date Time" { # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. $Date = (Get-Date $Value).ToUniversalTime() $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date $NinjaValue = $TimeSpan.TotalSeconds } "Dropdown" { # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID if (-not $Selection) { throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") } $NinjaValue = $Selection } default { # All the other types shouldn't require additional work on the input. $NinjaValue = $Value } } # Set the field differently depending on whether it's a field in a Ninja Document or not. if ($DocumentName) { $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 } else { $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 } if ($CustomField.Exception) { throw $CustomField } } # Function to check if the computer is joined to a domain function Test-IsDomainJoined { if ($PSVersionTable.PSVersion.Major -lt 5) { return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain } else { return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain } } # Function to check if the computer is a domain controller function Test-IsDomainController { $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { Get-WmiObject -Class Win32_OperatingSystem } else { Get-CimInstance -ClassName Win32_OperatingSystem } if ($OS.ProductType -eq "2") { return $true } } function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } function Test-IsDomainReachable { try { $searcher = [adsisearcher]"(&(objectCategory=computer)(name=$env:ComputerName))" $searcher.FindOne() } catch { Write-Host -Object "[Error] Failed to connect to the domain!" Write-Host -Object "[Error] $($_.Exception.Message)" $False } } if (!$ExitCode) { $ExitCode = 0 } } process { # Check if the script is running with elevated privileges if (!(Test-IsElevated)) { Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." exit 1 } # Check if the computer is domain joined, can connect to the domain controller, and is not itself a domain controller if ((Test-IsDomainJoined) -and !(Test-IsDomainReachable) -and !(Test-IsDomainController)) { Write-Host -Object "[Error] Unable to connect to the domain controller! The domain must be reachable to confirm which profiles are orphaned." exit 1 } # Initialize a list to hold user profiles $UserProfiles = New-Object System.Collections.Generic.List[Object] # Retrieve user hives and add them to the list of user profiles try { $UserHives = Get-UserHives -Type "All" } catch { Write-Host -Object "[Error] Failed to retrieve the user profile information from the registry!" Write-Host -Object "$($_.Exception.Message)" exit 1 } $UserHives | ForEach-Object { if ((Test-IsDomainJoined) -and $_.Username -notmatch [Regex]::Escape("$env:ComputerName") -and $_.Username -notmatch "AzureAD" -and $_.Username -ne "UNKNOWN") { try { $ADSIsearch = [adsisearcher]"(objectSid=$($_.SID))" if (!($ADSIsearch.FindOne())) { $_.Username = "UNKNOWN" } } catch { Write-Host -Object "[Error] Failed to connect to the domain to verify the account with the sid $($_.SID) is active." Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } } try { $UserProfiles.Add( [PSCustomObject]@{ SID = $_.SID Username = $_.Username UserHive = $_.UserHive Path = $_.Path Size = $_.Size FriendlySize = (Get-FriendlySize -Bytes $_.Size) } ) } catch { Write-Host -Object "[Error] Failed to add the profile with the SID $($_.SID) to the profile list!" Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } } # Attempt to retrieve the user profiles directory from the registry (typically C:\Users) try { $ProfilesDirectory = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ErrorAction Stop).ProfilesDirectory } catch { Write-Host -Object "[Error] Unable to find the user profiles directory!" Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } # Attempt to get information about directories in the profiles directory that have not already been identified/found try { $ProfilesDirectory | Get-ChildItem -Directory -Force -ErrorAction Stop | Where-Object { $DirectoriesToIgnore -notcontains $_.Name -and $UserProfiles.Path -notcontains $_.FullName } | ForEach-Object { $FriendlySize = $Null $Size = $Null $Hive = $Null # Check if the NTuser.dat file exists in the profile directory if (Test-Path -Path "$($_.FullName)\NTuser.dat" -ErrorAction SilentlyContinue) { $Hive = "$($_.FullName)\NTuser.dat" } else { $Hive = "UNKNOWN" } # Calculate the size of the profile directory $Size = $(Get-ChildItem -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Sum Length -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Sum -ErrorAction SilentlyContinue) if ($Size) { $FriendlySize = Get-FriendlySize -Bytes $Size } # Add the profile information to the list of user profiles $UserProfiles.Add( [PSCustomObject]@{ SID = "UNKNOWN" Username = "UNKNOWN" UserHive = $Hive Path = $_.FullName Size = $Size FriendlySize = $FriendlySize } ) } } catch { Write-Host -Object "[Error] Unable to get all of the user profile information for $($_.FullName)!" Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } # Filter the list to find orphaned profiles (those with "UNKNOWN" as a username) $OrphanedProfiles = $UserProfiles | Where-Object { $_.Username -eq "UNKNOWN" } # If a custom field is specified, generate an HTML report and set the custom field if ($CustomField) { $HTMLreport = $UserProfiles | Sort-Object Size -Descending | Select-Object Username, @{Label = "Size"; Expression = { $_.FriendlySize } }, Path, SID | ConvertTo-Html -Fragment # Highlight orphaned profiles in the HTML report $HTMLreport = $HTMLreport | ForEach-Object { $_ -replace '<tr><td>UNKNOWN</td>', '<tr class="danger"><td>UNKNOWN</td>' } try { Write-Host "Attempting to set Custom Field '$CustomField'." Set-NinjaProperty -Name $CustomField -Value $HTMLreport Write-Host "Successfully set Custom Field '$CustomField'!" } catch { Write-Host "[Error] $($_.Exception.Message)" $ExitCode = 1 } Write-Host "" } # Display a message about orphaned profiles if ($OrphanedProfiles) { Write-Host -Object "[Alert] Orphaned profiles found!" $OrphanedProfiles | Sort-Object Size -Descending | Format-Table Username, @{Label = "Size"; Expression = { $_.FriendlySize } }, Path, SID | Out-String | Write-Host } else { Write-Host -Object "No orphaned profiles found!" } exit $ExitCode } end { }
Risparmia tempo con gli oltre 300 script del Dojo NinjaOne.
Analisi dettagliata
Componenti chiave e flusso di lavoro
1. Inizializzazione e parametri
a. Lo script per identificare e gestire gli account utente orfani inizia con la definizione di parametri quali $DirectoriesToIgnore per escludere specifiche cartelle predefinite (ad esempio, “Public” o “Default”) e $CustomField, che si integra con NinjaOne a scopo di reporting.
b. Una serie di funzioni di aiuto, come Test-IsDomainJoined e Get-FriendlySize, preparano l’ambiente assicurando privilegi adeguati e formattando l’output.
2. Individuazione dei profili
a. La funzionalità principale risiede in Get-UserHives, che interroga il registro di Windows per ottenere informazioni sul profilo utente. Recupera dettagli come il nome utente, l’identificatore di sicurezza (SID), il percorso del profilo e le dimensioni di ogni profilo utente.
b. I profili vengono confrontati con gli utenti attivi del dominio utilizzando i loro SID. Tutti i profili che non corrispondono a un utente attivo vengono segnalati come “SCONOSCIUTI”
3. Generazione di report
a. Lo script per identificare e gestire gli account utente orfani inserisce tutti i profili orfani in un report. Se viene specificato un campo personalizzato, i dati vengono convertiti in un report HTML da integrare con strumenti esterni come NinjaOne.
4. Output e avvisi
a. Se vengono trovati profili orfani, viene visualizzata una tabella formattata che mostra nomi utente, dimensioni del profilo, percorsi e SID.
b. Se non esistono profili orfani, lo script per identificare e gestire gli account utente orfani mostra una tabella vuota.
Casi d’uso potenziali
Caso di studio: Pulizia della rete aziendale
Immagina un MSP incaricato di gestire un’organizzazione di 500 dipendenti. Nel corso degli anni, più account vengono disattivati quando i dipendenti se ne vanno, ma i loro profili rimangono. Utilizzando questo script per identificare e gestire gli account utente orfani, il team IT può:
- Identificare i profili orfani in tutti i sistemi.
- Analizzare l’utilizzo del disco di questi profili.
- Integrare i risultati in un sistema di reporting centralizzato tramite il parametro $CustomField.
- Utilizzare il risultato per prendere decisioni sull’archiviazione, la cancellazione o per effettuare ulteriori indagini.
Confronti
Pulizia manuale
La revisione manuale dei profili utente è un processo lungo e soggetto a errori, che richiede la consultazione del registro di sistema e l’incrocio con le directory degli utenti attivi. Questo script per identificare e gestire gli account utente orfani automatizza il processo, garantendo coerenza e risparmio di tempo.
Strumenti di terze parti
Sebbene alcune soluzioni software siano specializzate nella gestione dei profili degli utenti, spesso comportano costi elevati. Questo script PowerShell offre un’alternativa economica e personalizzabile, soprattutto per le organizzazioni che già utilizzano NinjaOne per la gestione IT.
Domande frequenti
Come fa lo script a identificare gli account orfani?
Confronta le informazioni del profilo utente recuperate dal registro di Windows con gli account del dominio attivo risolvendo i SID. Qualsiasi profilo non associato a un account attivo viene segnalato come orfano.
Lo script per identificare e gestire gli account utente orfani elimina i profili orfani?
No, si limita a identificare e segnalare i profili orfani. Gli amministratori possono esaminare manualmente e decidere le fasi successive.
Questo script per identificare e gestire gli account utente orfani può essere eseguito su sistemi non di dominio?
Sì, ma la capacità di convalidare gli account rispetto a un dominio non è disponibile in questi ambienti.
Quali autorizzazioni sono necessarie per eseguire lo script per identificare e gestire gli account utente orfani?
Per accedere al registro e alle directory dei profili sono necessari i privilegi di amministratore.
Implicazioni
L’identificazione dei profili utente orfani ha implicazioni di vasta portata per la sicurezza IT e l’efficienza:
- Maggiore sicurezza: Riduce il rischio di esposizione di dati sensibili da parte di profili inutilizzati.
- Risorse ottimizzate: Libera spazio su disco e risorse di sistema.
- Garanzia di conformità: Aiuta le organizzazioni a soddisfare i requisiti di governance dei dati e di audit.
Raccomandazioni
- Testa prima dell’implementazione
Esegui sempre lo script per identificare e gestire gli account utente orfani in un ambiente di prova per capire quali risultati genera e assicurati che sia in linea con i requisiti dell’organizzazione. - Esamina attentamente i risultati
I profili orfani segnalati come “UNKNOWN” devono essere ricontrollati per verificare che non siano associati ad account critici. - Integra la reportistica
Sfrutta il parametro $CustomField per integrare i risultati con strumenti come NinjaOne per la gestione e la documentazione centralizzate. - Pianifica controlli regolari
Automatizza l’esecuzione periodica dello script per identificare e gestire gli account utente orfani per garantire la pulizia e la sicurezza dei sistemi.
Considerazioni finali
Una gestione efficiente dei profili utente è fondamentale per mantenere ambienti IT sicuri e ottimizzati. Questo script PowerShell fornisce una soluzione affidabile e automatizzata per identificare i profili orfani, offrendo approfondimenti dettagliati e una perfetta integrazione con strumenti come NinjaOne. Per gli amministratori IT e gli MSP, l’implementazione di queste soluzioni garantisce che i sistemi rimangano sicuri, conformi ed efficienti dal punto di vista delle risorse.
Combinando questo script per identificare e gestire gli account utente orfani con le più ampie funzionalità di gestione IT di NinjaOne, le organizzazioni possono ottenere un’efficienza senza precedenti nella gestione degli account e dei profili degli utenti.