From d5412ba49427ef6f4f1d5e3dd3ea4cc17706bffa Mon Sep 17 00:00:00 2001 From: GUILHERME PINHEIRO Date: Tue, 6 Jan 2026 14:26:45 -0300 Subject: [PATCH 1/3] feat: Add comprehensive AD user query tool v2.0 - Add Get-ADUserInfo.ps1 with interactive menus - Add ADModule.bat as script manager entry point - Reorganize project structure (scripts in /scripts folder) Features: - Single user and batch processing modes - GUI file dialogs with CLI fallback - Multi-domain support with auto-detection - CSV export and profile photo export - All AD attributes display with array expansion - English localization Co-authored-by: @GuilhermeP96 --- ADModule.bat | 114 ++ README.md | 178 ++- scripts/Get-ADUserInfo.ps1 | 1041 +++++++++++++++++ .../Import-ActiveDirectory.ps1 | 0 4 files changed, 1314 insertions(+), 19 deletions(-) create mode 100644 ADModule.bat create mode 100644 scripts/Get-ADUserInfo.ps1 rename Import-ActiveDirectory.ps1 => scripts/Import-ActiveDirectory.ps1 (100%) diff --git a/ADModule.bat b/ADModule.bat new file mode 100644 index 0000000..6cca312 --- /dev/null +++ b/ADModule.bat @@ -0,0 +1,114 @@ +@echo off +chcp 65001 >nul 2>&1 +setlocal EnableDelayedExpansion +title ADModule - Script Manager +cd /d "%~dp0" + +:MENU +cls +echo. +echo ============================================= +echo ADModule - Script Manager +echo ============================================= +echo. +echo Available Scripts: +echo. +echo [1] Get-ADUserInfo - Query AD user attributes +echo (Single user or batch processing) +echo. +echo [2] Import-ActiveDirectory - Import AD module +echo (Load DLL for manual PowerShell use) +echo. +echo [0] Exit +echo. +echo ============================================= +echo. + +set /p choice="Choose an option (0-2): " + +if "%choice%"=="1" goto ADUSER +if "%choice%"=="2" goto IMPORTAD +if "%choice%"=="0" goto EXIT +echo. +echo [!] Invalid option. Press any key to try again... +pause >nul +goto MENU + +:ADUSER +cls +echo. +echo ============================================= +echo Get-ADUserInfo - Options +echo ============================================= +echo. +echo [1] Interactive mode (menu) +echo. +echo [2] Query current logged-in user +echo. +echo [3] Batch process from file +echo. +echo [0] Back to main menu +echo. +echo ============================================= +echo. + +set /p subchoice="Choose an option (0-3): " + +if "%subchoice%"=="1" goto ADUSER_INTERACTIVE +if "%subchoice%"=="2" goto ADUSER_CURRENT +if "%subchoice%"=="3" goto ADUSER_BATCH +if "%subchoice%"=="0" goto MENU +echo. +echo [!] Invalid option. Press any key to try again... +pause >nul +goto ADUSER + +:ADUSER_INTERACTIVE +cls +powershell.exe -ExecutionPolicy Bypass -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; & '.\scripts\Get-ADUserInfo.ps1'" +echo. +pause +goto MENU + +:ADUSER_CURRENT +cls +powershell.exe -ExecutionPolicy Bypass -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; & '.\scripts\Get-ADUserInfo.ps1' -SamAccountName $env:USERNAME -NoMenu" +echo. +pause +goto MENU + +:ADUSER_BATCH +cls +powershell.exe -ExecutionPolicy Bypass -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $script = '.\scripts\Get-ADUserInfo.ps1'; & $script -BatchFile '' -BatchOutput ''" +echo. +pause +goto MENU + +:IMPORTAD +cls +echo. +echo ============================================= +echo Import-ActiveDirectory - PowerShell +echo ============================================= +echo. +echo This will open a PowerShell session with +echo the Active Directory module loaded. +echo. +echo You can use AD cmdlets like: +echo - Get-ADUser +echo - Get-ADGroup +echo - Get-ADComputer +echo. +echo ============================================= +echo. +echo Press any key to start PowerShell session... +pause >nul +cls +powershell.exe -ExecutionPolicy Bypass -NoProfile -NoExit -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; & '.\scripts\Import-ActiveDirectory.ps1'; Write-Host ''; Write-Host '[+] AD Module loaded. You can now use Get-ADUser, Get-ADGroup, etc.' -ForegroundColor Green; Write-Host ''" +goto MENU + +:EXIT +echo. +echo Exiting... +endlocal +exit /b 0 diff --git a/README.md b/README.md index 1cafb39..accb7d2 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,176 @@ # ADModule -Microsoft signed DLL for the ActiveDirectory PowerShell module -Just a backup for the Microsoft's ActiveDirectory PowerShell module from Server 2016 with RSAT and module installed. The DLL is usually found at this path: C:\Windows\Microsoft.NET\assembly\GAC_64\Microsoft.ActiveDirectory.Management +Microsoft signed DLL for the ActiveDirectory PowerShell module. -and the rest of the module files at this path: -C:\Windows\System32\WindowsPowerShell\v1.0\Modules\ActiveDirectory\ +> **Enhanced by [@GuilhermeP96](https://github.com/GuilhermeP96)** - Added interactive tools, batch processing, and script manager. -## Usage -You can copy this DLL to your machine and use it to enumerate Active Directory without installing RSAT and without having administrative privileges. +## Overview -PS C:\\> Import-Module C:\ADModule\Microsoft.ActiveDirectory.Management.dll -Verbose -![Alt text](/img/AD_Module.png?raw=true "ADModule") +This is a backup of Microsoft's ActiveDirectory PowerShell module from Server 2016 with RSAT. The DLL allows you to enumerate Active Directory **without installing RSAT** and **without administrative privileges**. -You can also use the Import-ActiveDirectory.ps1 (Thanks to PR by @D1iv3) to load the script using download-execute cradles and without writing the DLL to disk: +### Original Paths +- DLL: `C:\Windows\Microsoft.NET\assembly\GAC_64\Microsoft.ActiveDirectory.Management` +- Module: `C:\Windows\System32\WindowsPowerShell\v1.0\Modules\ActiveDirectory\` -PS C:\\> iex (new-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/samratashok/ADModule/master/Import-ActiveDirectory.ps1');Import-ActiveDirectory -![Alt text](/img/AD_Module_Array.png?raw=true "ADModule_Array") +## Project Structure +``` +ADModule/ +├── ADModule.bat # Script Manager (main entry point) +├── Microsoft.ActiveDirectory.Management.dll # Microsoft signed AD DLL +├── ActiveDirectory/ # Full AD module files +│ └── ActiveDirectory.psd1 +├── scripts/ +│ ├── Get-ADUserInfo.ps1 # AD user query tool +│ └── Import-ActiveDirectory.ps1 # Module loader +├── img/ +└── README.md +``` -To be able to list all the cmdlets in the module, import the module as well. Remember to import the DLL first. +## Quick Start -PS C:\\> Import-Module C:\ADModule\Microsoft.ActiveDirectory.Management.dll -Verbose +### Option 1: Script Manager (Recommended) +Double-click `ADModule.bat` to open the interactive menu: -PS C:\\> Import-Module C:\AD\Tools\ADModule\ActiveDirectory\ActiveDirectory.psd1 +``` +============================================= + ADModule - Script Manager +============================================= -PS C:\\> Get-Command -Module ActiveDirectory + [1] Get-ADUserInfo - Query AD user attributes + [2] Import-ActiveDirectory - Import AD module + [0] Exit +``` + +### Option 2: Direct PowerShell Usage +```powershell +# Import the DLL +Import-Module .\Microsoft.ActiveDirectory.Management.dll -Verbose + +# Import full module (for all cmdlets) +Import-Module .\ActiveDirectory\ActiveDirectory.psd1 + +# List available commands +Get-Command -Module ActiveDirectory +``` + +## Features + +### Get-ADUserInfo.ps1 +Interactive tool to query AD user information with multiple features: + +#### Parameters +| Parameter | Description | +|-----------|-------------| +| `-SamAccountName` | User login/username to query | +| `-Domain` | AD domain (auto-detected if not specified) | +| `-NoMenu` | Skip interactive menus (direct mode) | +| `-AllFields` | Display all AD attributes | +| `-ExportCsv` | Export data to CSV file | +| `-ExportPhoto` | Export user's profile photo | +| `-BatchFile` | Input file with user list for batch processing | +| `-BatchOutput` | Output CSV path for batch processing | + +#### Usage Examples + +```powershell +# Interactive mode (recommended) +.\scripts\Get-ADUserInfo.ps1 + +# Query specific user +.\scripts\Get-ADUserInfo.ps1 -SamAccountName "john.doe" -NoMenu + +# Show ALL fields (all attributes) +.\scripts\Get-ADUserInfo.ps1 -SamAccountName "john.doe" -AllFields + +# Export to CSV +.\scripts\Get-ADUserInfo.ps1 -SamAccountName "john.doe" -ExportCsv "C:\temp\user.csv" + +# Batch processing (multiple users) +.\scripts\Get-ADUserInfo.ps1 -BatchFile "C:\users.txt" -BatchOutput "C:\export.csv" + +# Multi-domain support +.\scripts\Get-ADUserInfo.ps1 -Domain "domain1.local,domain2.corp" +``` + +#### Batch Processing +The batch mode accepts text files with users in any of these formats: +- One user per line +- Comma-separated: `user1,user2,user3` +- Semicolon-separated: `user1;user2;user3` + +When running batch mode interactively, native Windows file dialogs are used (with CLI fallback if GUI is unavailable). + +#### Output Formats +1. **Summary** - Main fields organized by category (Identification, Organization, Contact, Address, Account Status, AD Location, Groups) +2. **All Fields** - Complete dump of all AD attributes with expanded arrays +3. **CSV Export** - All data exported to CSV file +4. **Photo Export** - User's profile photo (thumbnailPhoto or jpegPhoto) + +### Import-ActiveDirectory.ps1 +Loads the AD module for manual PowerShell usage. Supports: +- Loading from DLL on disk +- Loading from embedded byte array (for download-execute cradles) + +```powershell +# Load from script +.\scripts\Import-ActiveDirectory.ps1 + +# Or with custom DLL path +Import-ActiveDirectory -ActiveDirectoryModule "C:\path\to\dll" +``` ## Benefits -There are many benefits like very low chances of detection by AV, very wide coverage by cmdlets, good filters for cmdlets, signed by Microsoft etc. The most useful one, however, is that this module works flawlessly from PowerShell's Constrained Language Mode -![Alt text](/img/AD_Module_CLM.png?raw=true "ADModule in CLM") +- **No RSAT required** - Works without Remote Server Administration Tools +- **No admin privileges** - Run as standard user +- **Microsoft signed** - Very low AV detection +- **CLM compatible** - Works in PowerShell Constrained Language Mode +- **Auto domain detection** - Automatically finds your AD domain +- **Multi-domain support** - Query multiple domains +- **Batch processing** - Process hundreds of users at once +- **GUI file dialogs** - Native Windows dialogs with CLI fallback + +## Screenshots + +### ADModule Import +![ADModule](img/AD_Module.png?raw=true "ADModule") + +### ADModule Array (Download-Execute) +![ADModule_Array](img/AD_Module_Array.png?raw=true "ADModule_Array") + +### Constrained Language Mode +![ADModule in CLM](img/AD_Module_CLM.png?raw=true "ADModule in CLM") + +## Credits + +- **Original Author**: [Samrat Ashok](https://github.com/samratashok) ([@intikitten](https://twitter.com/intikitten)) +- **Import-ActiveDirectory.ps1**: [@D1iv3](https://github.com/samratashok/ADModule/pull/1) +- **Enhanced Scripts & Tools**: [@GuilhermeP96](https://github.com/GuilhermeP96) + +## Links + +- **Original Repository**: https://github.com/samratashok/ADModule +- **Blog Post**: https://www.labofapenetrationtester.com/2018/10/domain-enumeration-from-PowerShell-CLM.html + +## Changelog +### v2.0.0 (2026-01-06) - @GuilhermeP96 +- Added `Get-ADUserInfo.ps1` - Interactive AD user query tool +- Added `ADModule.bat` - Script manager with menu system +- Reorganized project structure (scripts in `/scripts` folder) +- Features added: + - Interactive menus for user selection and output format + - Automatic domain detection (3 methods + manual fallback) + - Multi-domain support + - Batch processing with GUI file dialogs + - All fields display (all attributes with array expansion) + - CSV export functionality + - Profile photo export (thumbnailPhoto/jpegPhoto) + - UTF-8 encoding support + - English localization -## Blog -https://www.labofapenetrationtester.com/2018/10/domain-enumeration-from-PowerShell-CLM.html +### v1.0.0 - Original +- Microsoft.ActiveDirectory.Management.dll +- Import-ActiveDirectory.ps1 +- ActiveDirectory module files diff --git a/scripts/Get-ADUserInfo.ps1 b/scripts/Get-ADUserInfo.ps1 new file mode 100644 index 0000000..57f0387 --- /dev/null +++ b/scripts/Get-ADUserInfo.ps1 @@ -0,0 +1,1041 @@ +#Requires -Version 5.0 +<# +.SYNOPSIS + Script to query Active Directory user attributes without administrative privileges. + +.DESCRIPTION + This script loads ADModule and queries user attributes in AD. + Attempts to detect the domain automatically. If it fails, prompts the user. + Presents an interactive menu to choose between local user or manual entry. + +.PARAMETER SamAccountName + The login/username of the user to query. If not specified, displays menu. + +.PARAMETER Domain + The domain to use. If not specified, attempts automatic detection. + Can be a single domain or comma-separated list to try multiple. + +.PARAMETER NoMenu + Skips the menu and uses SamAccountName directly. + +.PARAMETER AllFields + Displays ALL AD fields without exception (all available attributes). + +.PARAMETER ExportCsv + Path to export data to CSV. + +.PARAMETER ExportPhoto + Path to export the user's profile photo (if exists). + If not specified and photo exists, saves to %TEMP%. + +.EXAMPLE + .\Get-ADUserInfo.ps1 + Displays interactive menu to choose user. + +.EXAMPLE + .\Get-ADUserInfo.ps1 -SamAccountName USER123 -NoMenu + Queries the user directly without displaying menu. + +.EXAMPLE + .\Get-ADUserInfo.ps1 -SamAccountName USER123 -AllFields + Displays ALL user fields. + +.EXAMPLE + .\Get-ADUserInfo.ps1 -Domain "domain1.local,domain2.corp" + Attempts to connect to specified domains (in order). + +.EXAMPLE + .\Get-ADUserInfo.ps1 -SamAccountName USER123 -ExportCsv "C:\temp\user.csv" + Exports all data to CSV. + +.EXAMPLE + .\Get-ADUserInfo.ps1 -BatchFile "C:\temp\users.txt" -BatchOutput "C:\temp\all_users.csv" + Batch queries users from a text file and exports all to CSV. + +.EXAMPLE + .\Get-ADUserInfo.ps1 -BatchFile "C:\temp\users.txt" + Batch queries users from a text file (opens file dialog for output). +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false, Position = 0)] + [string]$SamAccountName, + + [Parameter(Mandatory = $false)] + [string]$Domain, + + [Parameter(Mandatory = $false)] + [switch]$NoMenu, + + [Parameter(Mandatory = $false)] + [switch]$AllFields, + + [Parameter(Mandatory = $false)] + [string]$ExportCsv, + + [Parameter(Mandatory = $false)] + [string]$ExportPhoto, + + [Parameter(Mandatory = $false)] + [string]$BatchFile, + + [Parameter(Mandatory = $false)] + [string]$BatchOutput +) + +# Function to check if GUI is available +function Test-GuiAvailable { + try { + Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop + return $true + } + catch { + return $false + } +} + +# Function to show Open File dialog (GUI) +function Show-OpenFileDialog { + param( + [string]$Title = "Select File", + [string]$Filter = "Text Files (*.txt)|*.txt|CSV Files (*.csv)|*.csv|All Files (*.*)|*.*", + [string]$InitialDirectory = [Environment]::GetFolderPath('Desktop') + ) + + $guiAvailable = Test-GuiAvailable + + if ($guiAvailable) { + $dialog = New-Object System.Windows.Forms.OpenFileDialog + $dialog.Title = $Title + $dialog.Filter = $Filter + $dialog.InitialDirectory = $InitialDirectory + $dialog.Multiselect = $false + + $result = $dialog.ShowDialog() + + if ($result -eq [System.Windows.Forms.DialogResult]::OK) { + return $dialog.FileName + } + return $null + } + else { + # CLI fallback + Write-Host "" + Write-Host "[!] GUI not available. Enter file path manually." -ForegroundColor Yellow + Write-Host "" + $filePath = Read-Host "Enter full path to input file (users list)" + + if ([string]::IsNullOrWhiteSpace($filePath)) { + return $null + } + + if (Test-Path $filePath) { + return $filePath + } + else { + Write-Host "[X] File not found: $filePath" -ForegroundColor Red + return $null + } + } +} + +# Function to show Save File dialog (GUI) +function Show-SaveFileDialog { + param( + [string]$Title = "Save File", + [string]$Filter = "CSV Files (*.csv)|*.csv|All Files (*.*)|*.*", + [string]$InitialDirectory = [Environment]::GetFolderPath('Desktop'), + [string]$DefaultFileName = "AD_Users_Export.csv" + ) + + $guiAvailable = Test-GuiAvailable + + if ($guiAvailable) { + $dialog = New-Object System.Windows.Forms.SaveFileDialog + $dialog.Title = $Title + $dialog.Filter = $Filter + $dialog.InitialDirectory = $InitialDirectory + $dialog.FileName = $DefaultFileName + $dialog.OverwritePrompt = $true + + $result = $dialog.ShowDialog() + + if ($result -eq [System.Windows.Forms.DialogResult]::OK) { + return $dialog.FileName + } + return $null + } + else { + # CLI fallback + Write-Host "" + Write-Host "[!] GUI not available. Enter file path manually." -ForegroundColor Yellow + Write-Host " Default: " -NoNewline -ForegroundColor DarkGray + Write-Host $DefaultFileName -ForegroundColor Cyan + Write-Host "" + $filePath = Read-Host "Enter full path for output CSV (or press Enter for default in TEMP)" + + if ([string]::IsNullOrWhiteSpace($filePath)) { + $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" + return Join-Path $env:TEMP "AD_Users_Export_$timestamp.csv" + } + + return $filePath + } +} + +# Function to read users from file +function Read-UsersFromFile { + param([string]$FilePath) + + if (-not (Test-Path $FilePath)) { + Write-Host "[X] File not found: $FilePath" -ForegroundColor Red + return @() + } + + $content = Get-Content $FilePath -Raw -Encoding UTF8 + $users = @() + + # Try to parse: comma-separated, semicolon-separated, or line-by-line + if ($content -match ',') { + # Comma-separated + $users = $content -split '[,\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + } + elseif ($content -match ';') { + # Semicolon-separated + $users = $content -split '[;\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + } + else { + # Line-by-line + $users = $content -split '[\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + } + + return $users +} + +# Function to process batch users +function Process-BatchUsers { + param( + [array]$Users, + [string]$OutputPath, + [string]$TargetDomain + ) + + $results = @() + $total = $Users.Count + $current = 0 + $success = 0 + $failed = 0 + + Write-Host "" + Write-Host "=============================================" -ForegroundColor Cyan + Write-Host " BATCH PROCESSING USERS " -ForegroundColor Cyan + Write-Host "=============================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "[*] Total users to process: " -NoNewline -ForegroundColor Yellow + Write-Host $total -ForegroundColor White + Write-Host "" + + foreach ($userName in $Users) { + $current++ + $percent = [math]::Round(($current / $total) * 100) + + Write-Host "[" -NoNewline -ForegroundColor DarkGray + Write-Host "$current/$total" -NoNewline -ForegroundColor Cyan + Write-Host "] " -NoNewline -ForegroundColor DarkGray + Write-Host "Processing: " -NoNewline -ForegroundColor White + Write-Host $userName -NoNewline -ForegroundColor Yellow + Write-Host " ... " -NoNewline + + try { + $user = Get-ADUser -Identity $userName -Server $TargetDomain -Properties * -ErrorAction Stop + + # Build export object + $export = [ordered]@{} + $user.PSObject.Properties | Where-Object { + $_.Name -notin @('PropertyNames', 'AddedProperties', 'RemovedProperties', 'ModifiedProperties', 'PropertyCount', 'nTSecurityDescriptor') + } | ForEach-Object { + $value = $_.Value + if ($value -is [System.Collections.ICollection]) { + $export[$_.Name] = ($value | ForEach-Object { $_.ToString() }) -join "; " + } + elseif ($null -ne $value) { + $export[$_.Name] = $value.ToString() + } + else { + $export[$_.Name] = "" + } + } + + $results += [PSCustomObject]$export + $success++ + Write-Host "OK" -ForegroundColor Green + } + catch { + $failed++ + Write-Host "FAILED" -ForegroundColor Red + Write-Host " Error: $($_.Exception.Message)" -ForegroundColor DarkRed + + # Add failed entry with basic info + $results += [PSCustomObject]@{ + SamAccountName = $userName + _Status = "FAILED" + _Error = $_.Exception.Message + } + } + } + + # Export to CSV + Write-Host "" + Write-Host "=============================================" -ForegroundColor Green + Write-Host " BATCH COMPLETE " -ForegroundColor Green + Write-Host "=============================================" -ForegroundColor Green + Write-Host "" + Write-Host "[+] Successful: " -NoNewline -ForegroundColor Green + Write-Host $success -ForegroundColor White + Write-Host "[-] Failed: " -NoNewline -ForegroundColor Red + Write-Host $failed -ForegroundColor White + Write-Host "" + + if ($results.Count -gt 0) { + try { + $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 + Write-Host "[+] Data exported to: " -NoNewline -ForegroundColor Green + Write-Host $OutputPath -ForegroundColor Cyan + Write-Host "" + + # Ask to open folder + $openFolder = Read-Host "Open output folder? (Y/N)" + if ($openFolder -match '^[Yy]') { + $folder = Split-Path $OutputPath -Parent + Start-Process explorer.exe -ArgumentList $folder + } + } + catch { + Write-Host "[X] Error exporting CSV: $($_.Exception.Message)" -ForegroundColor Red + } + } + + return $results +} + +# Function to display user selection menu +function Show-UserMenu { + param( + [string]$CurrentUser + ) + + Write-Host "" + Write-Host "=============================================" -ForegroundColor Magenta + Write-Host " SELECT USER TO QUERY " -ForegroundColor Magenta + Write-Host "=============================================" -ForegroundColor Magenta + Write-Host "" + Write-Host " [1] Use local logged-in user: " -NoNewline -ForegroundColor Cyan + Write-Host $CurrentUser -ForegroundColor Yellow + Write-Host "" + Write-Host " [2] Enter user manually" -ForegroundColor Cyan + Write-Host "" + Write-Host " [3] Batch process from file (TXT/CSV)" -ForegroundColor Cyan + Write-Host "" + Write-Host " [0] Exit" -ForegroundColor DarkGray + Write-Host "" + Write-Host "=============================================" -ForegroundColor Magenta + Write-Host "" + + do { + $choice = Read-Host "Choose an option (0-3)" + + switch ($choice) { + "1" { + return $CurrentUser + } + "2" { + Write-Host "" + $manualUser = Read-Host "Enter the user login/username" + if ([string]::IsNullOrWhiteSpace($manualUser)) { + Write-Host "[!] User cannot be empty. Try again." -ForegroundColor Red + $choice = $null + } else { + return $manualUser.Trim() + } + } + "3" { + return "__BATCH_MODE__" + } + "0" { + Write-Host "" + Write-Host "Exiting..." -ForegroundColor DarkGray + exit 0 + } + default { + Write-Host "[!] Invalid option. Enter 0, 1, 2 or 3." -ForegroundColor Red + $choice = $null + } + } + } while ($null -eq $choice) +} + +# Function to display output format menu +function Show-OutputMenu { + Write-Host "" + Write-Host "=============================================" -ForegroundColor Magenta + Write-Host " DATA DISPLAY FORMAT " -ForegroundColor Magenta + Write-Host "=============================================" -ForegroundColor Magenta + Write-Host "" + Write-Host " [1] Summary (main fields)" -ForegroundColor Cyan + Write-Host "" + Write-Host " [2] ALL fields (all attributes)" -ForegroundColor Cyan + Write-Host "" + Write-Host " [3] Export to CSV" -ForegroundColor Cyan + Write-Host "" + Write-Host " [4] Export profile photo" -ForegroundColor Cyan + Write-Host "" + Write-Host " [0] Back" -ForegroundColor DarkGray + Write-Host "" + Write-Host "=============================================" -ForegroundColor Magenta + Write-Host "" + + do { + $choice = Read-Host "Choose an option (0-4)" + + switch ($choice) { + "1" { return "summary" } + "2" { return "all" } + "3" { return "csv" } + "4" { return "photo" } + "0" { return "back" } + default { + Write-Host "[!] Invalid option. Enter 0, 1, 2, 3 or 4." -ForegroundColor Red + $choice = $null + } + } + } while ($null -eq $choice) +} + +# Function to display ALL fields (always expanded) +function Show-AllFields { + param($User) + + Write-Host "" + Write-Host "=============================================" -ForegroundColor Green + Write-Host " ALL USER ATTRIBUTES (" -NoNewline -ForegroundColor Green + Write-Host "$($User.PropertyCount)" -NoNewline -ForegroundColor Yellow + Write-Host " fields) " -ForegroundColor Green + Write-Host "=============================================" -ForegroundColor Green + Write-Host "" + + $User.PSObject.Properties | Where-Object { $_.Name -notin @('PropertyNames', 'AddedProperties', 'RemovedProperties', 'ModifiedProperties', 'PropertyCount') } | Sort-Object Name | ForEach-Object { + $name = $_.Name + $value = $_.Value + + # Format special values + if ($null -eq $value -or $value -eq '') { + Write-Host ("{0,-45}" -f $name) -NoNewline -ForegroundColor Yellow + Write-Host ": " -NoNewline + Write-Host "(empty)" -ForegroundColor DarkGray + } + elseif ($value -is [System.Collections.ICollection]) { + if ($value.Count -eq 0) { + Write-Host ("{0,-45}" -f $name) -NoNewline -ForegroundColor Yellow + Write-Host ": " -NoNewline + Write-Host "(empty)" -ForegroundColor DarkGray + } + else { + # Always expand arrays/collections + Write-Host ("{0,-45}" -f $name) -NoNewline -ForegroundColor Yellow + Write-Host ": " -NoNewline -ForegroundColor White + Write-Host "[$($value.Count) items]" -ForegroundColor Magenta + + $index = 0 + foreach ($item in $value) { + $index++ + $itemStr = $item.ToString() + + # For AD DNs, extract friendly name + if ($itemStr -match '^CN=([^,]+)') { + $friendlyName = $Matches[1] + Write-Host ("{0,45} " -f "") -NoNewline + Write-Host ("[{0:D2}] " -f $index) -NoNewline -ForegroundColor DarkGray + Write-Host $friendlyName -NoNewline -ForegroundColor Cyan + Write-Host " ($itemStr)" -ForegroundColor DarkGray + } + elseif ($item -is [byte]) { + # For byte arrays, display in hex + if ($index -eq 1) { + $hexValues = ($value | ForEach-Object { '{0:X2}' -f $_ }) -join ' ' + if ($hexValues.Length -gt 100) { + $hexValues = $hexValues.Substring(0, 100) + "..." + } + Write-Host ("{0,45} " -f "") -NoNewline + Write-Host "[HEX] " -NoNewline -ForegroundColor DarkGray + Write-Host $hexValues -ForegroundColor Gray + } + break + } + else { + Write-Host ("{0,45} " -f "") -NoNewline + Write-Host ("[{0:D2}] " -f $index) -NoNewline -ForegroundColor DarkGray + Write-Host $itemStr -ForegroundColor White + } + } + } + } + elseif ($value -is [DateTime]) { + Write-Host ("{0,-45}" -f $name) -NoNewline -ForegroundColor Yellow + Write-Host ": " -NoNewline + Write-Host $value.ToString("dd/MM/yyyy HH:mm:ss") -ForegroundColor Cyan + } + elseif ($value -is [bool]) { + Write-Host ("{0,-45}" -f $name) -NoNewline -ForegroundColor Yellow + Write-Host ": " -NoNewline + $color = if ($value) { "Green" } else { "Red" } + Write-Host $value.ToString() -ForegroundColor $color + } + else { + Write-Host ("{0,-45}" -f $name) -NoNewline -ForegroundColor Yellow + Write-Host ": " -NoNewline + Write-Host $value.ToString() -ForegroundColor White + } + } + + Write-Host "" + Write-Host "=============================================" -ForegroundColor Green +} + +# Function to export to CSV +function Export-ToCsv { + param($User, [string]$Path) + + if ([string]::IsNullOrWhiteSpace($Path)) { + $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" + $Path = Join-Path $env:TEMP "ADUser_$($User.SamAccountName)_$timestamp.csv" + } + + try { + $export = @{} + $User.PSObject.Properties | Where-Object { $_.Name -notin @('PropertyNames', 'AddedProperties', 'RemovedProperties', 'ModifiedProperties', 'PropertyCount', 'nTSecurityDescriptor') } | ForEach-Object { + $value = $_.Value + if ($value -is [System.Collections.ICollection]) { + $export[$_.Name] = ($value | ForEach-Object { $_.ToString() }) -join "; " + } + elseif ($null -ne $value) { + $export[$_.Name] = $value.ToString() + } + else { + $export[$_.Name] = "" + } + } + + [PSCustomObject]$export | Export-Csv -Path $Path -NoTypeInformation -Encoding UTF8 + + Write-Host "" + Write-Host "[+] Data exported to: " -NoNewline -ForegroundColor Green + Write-Host $Path -ForegroundColor Cyan + Write-Host "" + + return $Path + } + catch { + Write-Host "[X] Error exporting: $_" -ForegroundColor Red + return $null + } +} + +# Function to export profile photo +function Export-UserPhoto { + param( + $User, + [string]$Path, + [string]$Domain + ) + + # Fetch photo (requires specific query as it doesn't come with -Properties *) + try { + $userWithPhoto = Get-ADUser -Identity $User.SamAccountName -Server $Domain -Properties thumbnailPhoto, jpegPhoto -ErrorAction Stop + } + catch { + Write-Host "[!] Error fetching photo: $($_.Exception.Message)" -ForegroundColor DarkYellow + return $null + } + + $photoData = $null + $photoSource = $null + + # Check thumbnailPhoto first (most common) + if ($userWithPhoto.thumbnailPhoto -and $userWithPhoto.thumbnailPhoto.Length -gt 0) { + $photoData = $userWithPhoto.thumbnailPhoto + $photoSource = "thumbnailPhoto" + } + # Fallback to jpegPhoto + elseif ($userWithPhoto.jpegPhoto -and $userWithPhoto.jpegPhoto.Length -gt 0) { + $photoData = $userWithPhoto.jpegPhoto + $photoSource = "jpegPhoto" + } + + if (-not $photoData) { + return @{ + HasPhoto = $false + Message = "User does not have a photo registered in AD" + } + } + + # Define output path + if ([string]::IsNullOrWhiteSpace($Path)) { + $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" + $Path = Join-Path $env:TEMP "ADPhoto_$($User.SamAccountName)_$timestamp.jpg" + } + + try { + [System.IO.File]::WriteAllBytes($Path, $photoData) + + return @{ + HasPhoto = $true + Path = $Path + Size = $photoData.Length + Source = $photoSource + } + } + catch { + return @{ + HasPhoto = $true + Error = $_.Exception.Message + } + } +} + +# Get current system user +$CurrentLoggedUser = $env:USERNAME + +# Configuration +$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path +$DllPath = Join-Path $ScriptPath "Microsoft.ActiveDirectory.Management.dll" +$ModulePath = Join-Path $ScriptPath "ActiveDirectory\ActiveDirectory.psd1" + +# Function to request domain from user +function Request-DomainFromUser { + Write-Host "" + Write-Host "=============================================" -ForegroundColor Yellow + Write-Host " DOMAIN NOT DETECTED AUTOMATICALLY " -ForegroundColor Yellow + Write-Host "=============================================" -ForegroundColor Yellow + Write-Host "" + Write-Host "Enter the Active Directory domain." -ForegroundColor White + Write-Host "You can enter multiple domains separated by comma." -ForegroundColor DarkGray + Write-Host "Example: " -NoNewline -ForegroundColor DarkGray + Write-Host "company.local, company.corp, ad.company.com" -ForegroundColor Cyan + Write-Host "" + + do { + $inputDomain = Read-Host "Domain(s)" + if ([string]::IsNullOrWhiteSpace($inputDomain)) { + Write-Host "[!] Domain cannot be empty. Try again." -ForegroundColor Red + } + } while ([string]::IsNullOrWhiteSpace($inputDomain)) + + # Return array of domains (cleaned) + return ($inputDomain -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) +} + +# Function to test domain connection +function Test-DomainConnection { + param([string]$DomainName) + + try { + $null = Get-ADDomainController -DomainName $DomainName -Discover -ErrorAction Stop + return $true + } + catch { + return $false + } +} + +# Function to detect domain +function Get-CurrentDomain { + try { + # Method 1: Environment variable + if ($env:USERDNSDOMAIN) { + Write-Verbose "Domain detected via USERDNSDOMAIN: $env:USERDNSDOMAIN" + return $env:USERDNSDOMAIN + } + + # Method 2: WMI/CIM + $computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue + if ($computerSystem.Domain -and $computerSystem.Domain -ne "WORKGROUP") { + Write-Verbose "Domain detected via WMI: $($computerSystem.Domain)" + return $computerSystem.Domain + } + + # Method 3: .NET + $domainInfo = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + if ($domainInfo.Name) { + Write-Verbose "Domain detected via .NET: $($domainInfo.Name)" + return $domainInfo.Name + } + } + catch { + Write-Verbose "Could not detect domain automatically: $_" + } + + return $null +} + +# Banner +Write-Host "" +Write-Host "=============================================" -ForegroundColor Cyan +Write-Host " AD User Info - Query without Admin/RSAT " -ForegroundColor Cyan +Write-Host "=============================================" -ForegroundColor Cyan +Write-Host "" + +# Check for batch mode from parameter +$BatchMode = $false +if ($BatchFile) { + $BatchMode = $true +} + +# Determine which user to query +if ($BatchMode) { + $TargetUser = "__BATCH_MODE__" + Write-Host "[*] Batch mode (from parameter)" -ForegroundColor DarkGray +} +elseif ($NoMenu -and $SamAccountName) { + # Direct mode without menu + $TargetUser = $SamAccountName + Write-Host "[*] Direct mode (no menu)" -ForegroundColor DarkGray +} +elseif ($SamAccountName) { + # Has user as parameter, but displays menu to confirm + Write-Host "[*] User provided via parameter: " -NoNewline -ForegroundColor Yellow + Write-Host $SamAccountName -ForegroundColor White + $TargetUser = Show-UserMenu -CurrentUser $CurrentLoggedUser +} +else { + # Display menu to choose + $TargetUser = Show-UserMenu -CurrentUser $CurrentLoggedUser +} + +# Handle batch mode selection from menu +if ($TargetUser -eq "__BATCH_MODE__") { + $BatchMode = $true + + # Get input file + if (-not $BatchFile) { + Write-Host "" + Write-Host "[*] Select the input file with user list..." -ForegroundColor Yellow + $BatchFile = Show-OpenFileDialog -Title "Select User List File" -Filter "Text Files (*.txt)|*.txt|CSV Files (*.csv)|*.csv|All Files (*.*)|*.*" + + if (-not $BatchFile) { + Write-Host "[X] No file selected. Exiting." -ForegroundColor Red + exit 1 + } + } + + Write-Host "[*] Input file: " -NoNewline -ForegroundColor Green + Write-Host $BatchFile -ForegroundColor Cyan + + # Get output file + if (-not $BatchOutput) { + Write-Host "" + Write-Host "[*] Select the output CSV file..." -ForegroundColor Yellow + $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" + $BatchOutput = Show-SaveFileDialog -Title "Save Results As" -DefaultFileName "AD_Users_Export_$timestamp.csv" + + if (-not $BatchOutput) { + Write-Host "[X] No output file selected. Exiting." -ForegroundColor Red + exit 1 + } + } + + Write-Host "[*] Output file: " -NoNewline -ForegroundColor Green + Write-Host $BatchOutput -ForegroundColor Cyan +} + +if (-not $BatchMode) { + Write-Host "" + Write-Host "[*] Selected user: " -NoNewline -ForegroundColor Green + Write-Host $TargetUser -ForegroundColor White +} + +# Detect or use specified domain +$DomainList = @() + +if ($Domain) { + # Domain(s) provided via parameter - can be comma-separated list + $DomainList = $Domain -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + Write-Host "[*] Specified domain(s): " -NoNewline -ForegroundColor Yellow + Write-Host ($DomainList -join ', ') -ForegroundColor White +} +else { + Write-Host "[*] Detecting domain automatically..." -ForegroundColor Yellow + $DetectedDomain = Get-CurrentDomain + + if ($DetectedDomain) { + $DomainList = @($DetectedDomain) + Write-Host "[+] Domain detected: " -NoNewline -ForegroundColor Green + Write-Host $DetectedDomain -ForegroundColor White + } + else { + # Request domain from user + $DomainList = Request-DomainFromUser + Write-Host "[*] Provided domain(s): " -NoNewline -ForegroundColor Yellow + Write-Host ($DomainList -join ', ') -ForegroundColor White + } +} + +# Use the first domain in the list as primary +$TargetDomain = $DomainList[0] + +# Load AD module +Write-Host "" +Write-Host "[*] Loading ADModule..." -ForegroundColor Yellow + +try { + # Check if DLL exists + if (-not (Test-Path $DllPath)) { + throw "DLL not found at: $DllPath" + } + + # Import DLL + Import-Module $DllPath -ErrorAction Stop + Write-Host "[+] DLL loaded successfully" -ForegroundColor Green + + # Try to import full module (for more cmdlets) + if (Test-Path $ModulePath) { + try { + Import-Module $ModulePath -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + Write-Host "[+] Full module loaded" -ForegroundColor Green + } + catch { + Write-Host "[!] .psd1 module not loaded (basic cmdlets available)" -ForegroundColor DarkYellow + } + } +} +catch { + Write-Host "[X] Error loading module: $_" -ForegroundColor Red + exit 1 +} + +# Handle batch mode processing +if ($BatchMode) { + # Read users from file + $usersToProcess = Read-UsersFromFile -FilePath $BatchFile + + if ($usersToProcess.Count -eq 0) { + Write-Host "[X] No users found in the file or file is empty." -ForegroundColor Red + exit 1 + } + + Write-Host "" + Write-Host "[*] Found " -NoNewline -ForegroundColor Green + Write-Host $usersToProcess.Count -NoNewline -ForegroundColor Yellow + Write-Host " user(s) in file" -ForegroundColor Green + + # Process batch + $batchResults = Process-BatchUsers -Users $usersToProcess -OutputPath $BatchOutput -TargetDomain $TargetDomain + + Write-Host "" + exit 0 +} + +# Single user query mode +Write-Host "" +Write-Host "[*] Querying user: " -NoNewline -ForegroundColor Yellow +Write-Host $TargetUser -ForegroundColor White + +# Try each domain in the list until successful +$User = $null +$SuccessDomain = $null + +foreach ($tryDomain in $DomainList) { + Write-Host "[*] Trying domain: " -NoNewline -ForegroundColor Yellow + Write-Host $tryDomain -ForegroundColor White + + try { + $User = Get-ADUser -Identity $TargetUser -Server $tryDomain -Properties * -ErrorAction Stop + $SuccessDomain = $tryDomain + Write-Host "[+] Successfully connected to domain: " -NoNewline -ForegroundColor Green + Write-Host $tryDomain -ForegroundColor White + break + } + catch { + Write-Host "[!] Failed on domain $tryDomain : $($_.Exception.Message)" -ForegroundColor DarkYellow + } +} + +if (-not $User) { + Write-Host "" + Write-Host "[X] Could not query user in any of the provided domains." -ForegroundColor Red + Write-Host "[!] Please verify that:" -ForegroundColor Yellow + Write-Host " - You are connected to the corporate network/VPN" -ForegroundColor Yellow + Write-Host " - The domain name is correct" -ForegroundColor Yellow + Write-Host " - The user '$TargetUser' exists in the domain" -ForegroundColor Yellow + exit 1 +} + +$TargetDomain = $SuccessDomain +Write-Host "" + +try { + + # Determine output format + if ($AllFields) { + $outputFormat = "all" + } + elseif ($ExportCsv) { + $outputFormat = "csv" + } + elseif (-not $NoMenu) { + $outputFormat = Show-OutputMenu + if ($outputFormat -eq "back") { + Write-Host "Operation cancelled." -ForegroundColor DarkGray + exit 0 + } + } + else { + $outputFormat = "summary" + } + + # Display according to chosen format + switch ($outputFormat) { + "all" { + Show-AllFields -User $User + } + "csv" { + $csvPath = if ($ExportCsv) { $ExportCsv } else { $null } + Export-ToCsv -User $User -Path $csvPath + } + "photo" { + $photoPath = if ($ExportPhoto) { $ExportPhoto } else { $null } + $photoResult = Export-UserPhoto -User $User -Path $photoPath -Domain $TargetDomain + + if ($photoResult.HasPhoto) { + if ($photoResult.Path) { + Write-Host "" + Write-Host "[+] Photo exported successfully!" -ForegroundColor Green + Write-Host " Source: " -NoNewline -ForegroundColor White + Write-Host $photoResult.Source -ForegroundColor Cyan + Write-Host " Size: " -NoNewline -ForegroundColor White + Write-Host "$([math]::Round($photoResult.Size / 1024, 2)) KB" -ForegroundColor Cyan + Write-Host " File: " -NoNewline -ForegroundColor White + Write-Host $photoResult.Path -ForegroundColor Yellow + Write-Host "" + + # Ask if user wants to open + $openPhoto = Read-Host "Do you want to open the photo? (Y/N)" + if ($openPhoto -match '^[Yy]') { + Start-Process $photoResult.Path + } + } + else { + Write-Host "[X] Error saving photo: $($photoResult.Error)" -ForegroundColor Red + } + } + else { + Write-Host "" + Write-Host "[!] $($photoResult.Message)" -ForegroundColor DarkYellow + Write-Host "" + } + } + default { + # Display summary (default format) + Write-Host "=============================================" -ForegroundColor Green + Write-Host " USER INFORMATION " -ForegroundColor Green + Write-Host "=============================================" -ForegroundColor Green + Write-Host "" + + # Basic data + Write-Host "--- Identification ---" -ForegroundColor Cyan + Write-Host ("Full Name: {0}" -f $User.DisplayName) + Write-Host ("First Name: {0}" -f $User.GivenName) + Write-Host ("Last Name: {0}" -f $User.Surname) + Write-Host ("Login (SAM): {0}" -f $User.SamAccountName) + Write-Host ("UPN: {0}" -f $User.UserPrincipalName) + Write-Host ("Email: {0}" -f $User.EmailAddress) + Write-Host ("Employee ID: {0}" -f $User.EmployeeID) + Write-Host ("Employee Number: {0}" -f $User.EmployeeNumber) + Write-Host "" + + # Organization + Write-Host "--- Organization ---" -ForegroundColor Cyan + Write-Host ("Title/Position: {0}" -f $User.Title) + Write-Host ("Department: {0}" -f $User.Department) + Write-Host ("Company: {0}" -f $User.Company) + Write-Host ("Manager: {0}" -f $User.Manager) + Write-Host ("Office: {0}" -f $User.Office) + Write-Host ("Description: {0}" -f $User.Description) + Write-Host "" + + # Contact + Write-Host "--- Contact ---" -ForegroundColor Cyan + Write-Host ("Telephone: {0}" -f $User.TelephoneNumber) + Write-Host ("Mobile: {0}" -f $User.MobilePhone) + Write-Host ("Fax: {0}" -f $User.Fax) + Write-Host ("Home Phone: {0}" -f $User.HomePhone) + Write-Host "" + + # Address + Write-Host "--- Address ---" -ForegroundColor Cyan + Write-Host ("Street: {0}" -f $User.StreetAddress) + Write-Host ("City: {0}" -f $User.City) + Write-Host ("State: {0}" -f $User.State) + Write-Host ("Postal Code: {0}" -f $User.PostalCode) + Write-Host ("Country: {0}" -f $User.Country) + Write-Host "" + + # Account status + Write-Host "--- Account Status ---" -ForegroundColor Cyan + Write-Host ("Account Enabled: {0}" -f $User.Enabled) + Write-Host ("Account Locked: {0}" -f $User.LockedOut) + Write-Host ("Password Expired: {0}" -f $User.PasswordExpired) + Write-Host ("Password Never Expires:{0}" -f $User.PasswordNeverExpires) + Write-Host ("Last Logon: {0}" -f $User.LastLogonDate) + Write-Host ("Password Changed: {0}" -f $User.PasswordLastSet) + Write-Host ("Account Created: {0}" -f $User.Created) + Write-Host ("Last Modified: {0}" -f $User.Modified) + Write-Host "" + + # AD Location + Write-Host "--- AD Location ---" -ForegroundColor Cyan + Write-Host ("Distinguished Name:{0}" -f $User.DistinguishedName) + Write-Host ("Canonical Name: {0}" -f $User.CanonicalName) + Write-Host ("SID: {0}" -f $User.SID) + Write-Host ("GUID: {0}" -f $User.ObjectGUID) + Write-Host "" + + # Check profile photo + Write-Host "--- Profile Photo ---" -ForegroundColor Cyan + $photoCheck = Export-UserPhoto -User $User -Path $null -Domain $TargetDomain + if ($photoCheck.HasPhoto -and $photoCheck.Path) { + Write-Host "Photo available: " -NoNewline + Write-Host "Yes" -ForegroundColor Green + Write-Host ("Size: {0} KB" -f [math]::Round($photoCheck.Size / 1024, 2)) + Write-Host ("Source: {0}" -f $photoCheck.Source) + Write-Host ("Saved to: {0}" -f $photoCheck.Path) + } + else { + Write-Host "Photo available: " -NoNewline + Write-Host "No" -ForegroundColor DarkGray + } + Write-Host "" + + # Groups (if available) + if ($User.MemberOf) { + Write-Host "--- Groups (MemberOf) ---" -ForegroundColor Cyan + $User.MemberOf | ForEach-Object { + $groupName = ($_ -split ',')[0] -replace 'CN=', '' + Write-Host (" - {0}" -f $groupName) + } + Write-Host "" + } + + Write-Host "=============================================" -ForegroundColor Green + Write-Host "" + } + } + + # Return object for later use + $User +} +catch { + Write-Host "[X] Error processing user data: " -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + exit 1 +} diff --git a/Import-ActiveDirectory.ps1 b/scripts/Import-ActiveDirectory.ps1 similarity index 100% rename from Import-ActiveDirectory.ps1 rename to scripts/Import-ActiveDirectory.ps1 From 05ce456c5da53bdb7f7c1d440e3d1bc3d8e5fc85 Mon Sep 17 00:00:00 2001 From: GUILHERME PINHEIRO Date: Tue, 6 Jan 2026 15:35:27 -0300 Subject: [PATCH 2/3] fix: Correct DLL path for scripts subfolder structure --- scripts/Get-ADUserInfo.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/Get-ADUserInfo.ps1 b/scripts/Get-ADUserInfo.ps1 index 57f0387..4d7173e 100644 --- a/scripts/Get-ADUserInfo.ps1 +++ b/scripts/Get-ADUserInfo.ps1 @@ -608,8 +608,9 @@ $CurrentLoggedUser = $env:USERNAME # Configuration $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path -$DllPath = Join-Path $ScriptPath "Microsoft.ActiveDirectory.Management.dll" -$ModulePath = Join-Path $ScriptPath "ActiveDirectory\ActiveDirectory.psd1" +$RootPath = Split-Path -Parent $ScriptPath # Go up one level from /scripts to root +$DllPath = Join-Path $RootPath "Microsoft.ActiveDirectory.Management.dll" +$ModulePath = Join-Path $RootPath "ActiveDirectory\ActiveDirectory.psd1" # Function to request domain from user function Request-DomainFromUser { From f429d31e07aedafb969505b793c28aeeb8134e7e Mon Sep 17 00:00:00 2001 From: GUILHERME PINHEIRO Date: Tue, 6 Jan 2026 15:43:03 -0300 Subject: [PATCH 3/3] feat: Add password expiration date calculation - Query domain password policy (MaxPasswordAge) - Calculate and display expiration date with days remaining - Color-coded status (OK, Warning, Expires Soon, EXPIRED) - Fallback to NET USER if policy query fails --- scripts/Get-ADUserInfo.ps1 | 125 ++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 3 deletions(-) diff --git a/scripts/Get-ADUserInfo.ps1 b/scripts/Get-ADUserInfo.ps1 index 4d7173e..247bb8a 100644 --- a/scripts/Get-ADUserInfo.ps1 +++ b/scripts/Get-ADUserInfo.ps1 @@ -603,6 +603,99 @@ function Export-UserPhoto { } } +# Function to calculate password expiration date +function Get-PasswordExpirationDate { + param( + $User, + [string]$Domain + ) + + try { + # Check if password never expires + if ($User.PasswordNeverExpires -eq $true) { + return @{ + ExpirationDate = $null + DaysRemaining = $null + Status = "Never Expires" + MaxPwdAgeDays = $null + } + } + + # Check if PasswordLastSet is null (password must be changed at next logon) + if ($null -eq $User.PasswordLastSet) { + return @{ + ExpirationDate = $null + DaysRemaining = 0 + Status = "Must Change at Next Logon" + MaxPwdAgeDays = $null + } + } + + # Get domain password policy + $domainPolicy = Get-ADDefaultDomainPasswordPolicy -Server $Domain -ErrorAction Stop + $maxPwdAge = $domainPolicy.MaxPasswordAge + + # If maxPwdAge is 0 or not set, passwords don't expire + if ($maxPwdAge.TotalDays -eq 0 -or $null -eq $maxPwdAge) { + return @{ + ExpirationDate = $null + DaysRemaining = $null + Status = "Policy: No Expiration" + MaxPwdAgeDays = $null + } + } + + # Calculate expiration date + $expirationDate = $User.PasswordLastSet.AddDays($maxPwdAge.TotalDays) + $daysRemaining = [math]::Ceiling(($expirationDate - (Get-Date)).TotalDays) + + # Determine status + $status = if ($daysRemaining -lt 0) { + "EXPIRED" + } elseif ($daysRemaining -le 7) { + "Expires Soon!" + } elseif ($daysRemaining -le 14) { + "Warning" + } else { + "OK" + } + + return @{ + ExpirationDate = $expirationDate + DaysRemaining = $daysRemaining + Status = $status + MaxPwdAgeDays = [math]::Round($maxPwdAge.TotalDays) + } + } + catch { + # Fallback: try using NET USER command + try { + $netUserOutput = net user $User.SamAccountName /domain 2>&1 + $expiresLine = $netUserOutput | Where-Object { $_ -match "(senha expira|password expires)" } + if ($expiresLine -match "(\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2}:\d{2})") { + $expirationDate = [DateTime]::Parse($Matches[1]) + $daysRemaining = [math]::Ceiling(($expirationDate - (Get-Date)).TotalDays) + $status = if ($daysRemaining -lt 0) { "EXPIRED" } elseif ($daysRemaining -le 7) { "Expires Soon!" } else { "OK" } + + return @{ + ExpirationDate = $expirationDate + DaysRemaining = $daysRemaining + Status = $status + MaxPwdAgeDays = $null + } + } + } + catch { } + + return @{ + ExpirationDate = $null + DaysRemaining = $null + Status = "Could not determine" + MaxPwdAgeDays = $null + } + } +} + # Get current system user $CurrentLoggedUser = $env:USERNAME @@ -985,14 +1078,40 @@ try { Write-Host "--- Account Status ---" -ForegroundColor Cyan Write-Host ("Account Enabled: {0}" -f $User.Enabled) Write-Host ("Account Locked: {0}" -f $User.LockedOut) - Write-Host ("Password Expired: {0}" -f $User.PasswordExpired) - Write-Host ("Password Never Expires:{0}" -f $User.PasswordNeverExpires) Write-Host ("Last Logon: {0}" -f $User.LastLogonDate) - Write-Host ("Password Changed: {0}" -f $User.PasswordLastSet) Write-Host ("Account Created: {0}" -f $User.Created) Write-Host ("Last Modified: {0}" -f $User.Modified) Write-Host "" + # Password Info + Write-Host "--- Password Info ---" -ForegroundColor Cyan + Write-Host ("Password Changed: {0}" -f $User.PasswordLastSet) + Write-Host ("Password Expired: {0}" -f $User.PasswordExpired) + Write-Host ("Never Expires: {0}" -f $User.PasswordNeverExpires) + + # Calculate password expiration + $pwdExpInfo = Get-PasswordExpirationDate -User $User -Domain $TargetDomain + if ($pwdExpInfo.ExpirationDate) { + Write-Host ("Password Expires: {0}" -f $pwdExpInfo.ExpirationDate.ToString("dd/MM/yyyy HH:mm:ss")) -NoNewline + + # Color-code based on days remaining + $daysColor = switch ($pwdExpInfo.Status) { + "EXPIRED" { "Red" } + "Expires Soon!" { "Red" } + "Warning" { "Yellow" } + default { "Green" } + } + Write-Host (" ({0} days)" -f $pwdExpInfo.DaysRemaining) -ForegroundColor $daysColor + } + else { + Write-Host ("Password Expires: {0}" -f $pwdExpInfo.Status) -ForegroundColor DarkGray + } + + if ($pwdExpInfo.MaxPwdAgeDays) { + Write-Host ("Policy Max Age: {0} days" -f $pwdExpInfo.MaxPwdAgeDays) -ForegroundColor DarkGray + } + Write-Host "" + # AD Location Write-Host "--- AD Location ---" -ForegroundColor Cyan Write-Host ("Distinguished Name:{0}" -f $User.DistinguishedName)