Advertisement
Guest User

ad-reporting-posh

a guest
Jun 10th, 2025
86
0
5 days
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 49.19 KB | None | 0 0
  1. <#
  2. .SYNOPSIS
  3. This script runs a variety of reporting functions against remote trusted domains and logs the output
  4. locally.
  5. .DESCRIPTION
  6. Administrator rights are required. The script first checks for trusted domains in the domain context
  7. it is executed within, then logs those to a file. It then reads that file and cycles through the
  8. trusted domains to check for a DC - preferring the PDC if available - and tests connectivity via
  9. both Test-NetConnection and WinRM. If a DC is reachable, it then adds that DC to the trusted hosts
  10. list and executes the various scriptblocks defined in the beginning of the script. It places the
  11. output of the scriptblocks into an output block, which is displayed and logged to a text file.
  12.  
  13. The group membership section has some limitations: The script cannot query groups local to an
  14. individual server and query client owned domains because Oracle only has a 1-way trust with them.
  15.  
  16. CSV format for group-input.csv:
  17. Domain,GroupName
  18. child1.domain.com,HR Team
  19. child2.domain.com,Finance Admins
  20.  
  21. It is noteworthy that this script allows NTLM authentication in case of Kerberos failures, which
  22. can be considered insecure if executed outside a trusted network. Even within a trusted network
  23. it is not ideal.
  24.  
  25. This script attempts cross-domain lookups when resolving group members, which may fail if ports
  26. are not appropriately open and WinRM traffic is not allowed.
  27. .NOTES
  28. Author: ME | License: CC0
  29. Initially Published: 27 May 2025 1122GMT
  30. Last Updated : 10 Jun 2025 1547GMT
  31. .CHANGELOG
  32. 20250603 - 13.7.6 - Version 13.7.6 is confirmed working with one exception: cross-domain lookups fail
  33. 20250603 - 14.0.1 - Adding cross-domain lookups with an enable/disable flag
  34. 20250603 - 14.1.1 - Moved domain controller lookups and connectivity testing to a function
  35. 20250603 - 14.2.1 - Added additional output file for domains that fail authentication negotiation and included in email
  36. 20250603 - 14.3.1 - Added display and logging of PDC name, fixed DC handling logic to not show incorrect error
  37. 20250603 - 14.3.2 - Added auth test prior to looping through the scripts to avoid unnecessary junk errors.
  38. 20250603 - 15.0.1 - Changed logging function to streaming to save memory usage and improve performance
  39. 20250603 - 15.0.2 - Refactored domain status to use a single array instead of multiple objects for the same reasons
  40. 20250603 - 15.0.3 - Moved domain status logic into Log-Activity function for the same reasons
  41. 20250603 - 16.0.1 - Updated Set-TrustedHost to shorter name and stricter hostname checks for validity to avoid adding malformed or rogue hosts
  42. 20250603 - 16.0.2 - Added toggle to disable console output since this is intended as a background task and it will improve performance to have it off
  43. 20250604 - 16.0.3 - Resolve issue with scripts running anyway when DC is unreachable
  44. 20250604 - 16.0.4 - Remove extraneous $domainsError statements, domain status is tracked in the log-activity function now
  45. 20250605 - 16.0.5 - Added domain-specific subfolder creation
  46. 20250605 - 17.0.1 - Added local domain context and toggle.
  47. 20250605 - 17.1.1 - Solved local group membership query issue by adding a copy of the report inside the local block; TESTED GOOD VERSION
  48. 20250605 - 18.0.1 - Changed logic to execute locally on the host server in order to reduce complexity and allow for GMSA use.
  49. 20250605 - 18.0.2 - Removed run as admin logic, as without the trusted hosts function it's no longer required.
  50. 20250605 - 18.0.3 - Removed redundant GroupMembership block in if(EnableLocalDomain).
  51. 20250605 - 18.0.4 - Implemented new authentication test now that the WinRM tests are deprecated and included deeper error logging.
  52. 20250605 - 18.0.5 - Fixing null group value issue
  53. 20250605 - 18.0.6 - Fixed group splatting issue with lookups
  54. 20250605 - 18.0.7 - Restored all scripts to EnableLocalDomain
  55. 20250605 - 18.1.1 - A lot of small work in the groupmembership section to resolve groups as the right kind of objects and handle the right arrays
  56. as well as adding a section in the EnableLocalDomain to resolve local DC and use it rather than trying to run on the local box
  57. TESTED GOOD AT THIS STAGE
  58. 20250605 - 18.2.1 - Added an old file/folder cleanup function with logging
  59. 20250606 - 18.2.2 - Added lookup for domain admins group to verify group resolution.
  60. 20250609 - 19.0.1 - Adding new logic to allow for queries of all domains via variable toggle.
  61. 20250609 - 19.0.2 - Fixed localRemoteComputer reference in enableLocalDomain loop
  62. 20250609 - 19.1.1 - Fixed logic for all domains toggle missing in trusted loop
  63. 20250609 - 1.0.0 - Reset version now that we're using threading; implemented parallel processing of domains
  64. 20250609 - 1.1.0 - Switched to using function instead of inline processing to reduce complexity for local domain processing
  65. 20250609 - 1.1.1 - Added memory tracking for each report, cleaned up logic, added error passthrough for jobs
  66. 20250609 - 2.0.1 - Replaced start-job with threadjob, an improved MS provided multithreading module
  67. 20250610 - 2.1.1 - Removed streaming log and went back to objects since the stream was locking a file and crashing the script
  68. #>
  69.  
  70. $scriptStartTime = Get-Date
  71.  
  72. # --- Configuration ---
  73. $Date = Get-Date -Format "HH-mm-M-d-yyyy"
  74. $DayStamp = Get-Date -Format "yyyy-MM-dd"
  75. $rootFolder = "C:\Scripts\ADreports"
  76. $outputRoot = Join-Path $rootFolder "test"
  77. $dailyFolder = Join-Path $outputRoot $DayStamp
  78. $activityLogPath = Join-Path $outputRoot "ActivityLog-$Date.csv"
  79. $global:GroupInputFolder = Join-Path $rootFolder "group-input"
  80. $failedAuthPath = Join-Path $dailyFolder "FailedAuthentication-$Date.txt"
  81. $DomainStatusPath = Join-Path $OutputRoot "DomainStatus-$Date.csv"
  82. $RetentionDays = 30
  83. $MaxParallelDomainJobs = 30
  84.  
  85. # --- Functionality Toggles ---
  86. $EnableAllGroupsQuery = $true # Set to $true to query all groups in each domain instead of only those in the CSV
  87. $EnableCrossDomainMemberLookups = $true # Set to $false to disable recursive user lookups across domains
  88. $EnableConsoleOutput = $true # Set to $false to disable all real-time console output - scheduled tasks should be $false!
  89. $EnableLocalDomain = $true # Set to $false to not process the domain of the server running the script
  90. $sendEmail = $true # Set to $false to disable sending email
  91.  
  92. # --- Email Config ---
  93. $emailTo = "[email protected]"
  94. $emailFrom = "[email protected]"
  95. $smtpServer = "mail.domain.com"
  96. $smtpPort = 25
  97.  
  98. # --- Script Info ---
  99. $scriptversion = "v2.1.1"
  100. $scriptauthor = "me"
  101. $scriptupdated = "2025-06-10"
  102.  
  103. # Ensure output dirs
  104. foreach ($path in @($outputRoot, $dailyFolder)) {
  105. if (-not (Test-Path $path)) {
  106. New-Item -ItemType Directory -Path $path -Force | Out-Null
  107. }
  108. }
  109.  
  110. # Initialize domain status array
  111. $domainStatusTable = @{}
  112.  
  113. # Initialize logs
  114. $activityLog = @()
  115.  
  116. function Log-Activity {
  117. param (
  118. [string]$DomainName,
  119. [string]$RemoteComputer,
  120. [string]$ScriptName,
  121. [string]$ActionType,
  122. [string]$Message
  123. )
  124. $entry = [PSCustomObject]@{
  125. DomainName = $DomainName
  126. RemoteComputer = $RemoteComputer
  127. ScriptName = $ScriptName
  128. ActionType = $ActionType
  129. Message = $Message
  130. }
  131. $global:activityLog += $entry
  132. $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
  133. $color = switch ($ActionType.ToUpper()) {
  134. "INFO" { "Cyan" }
  135. "SUCCESS" { "Green" }
  136. "WARNING" { "Yellow" }
  137. "ERROR" { "Red" }
  138. "RUNTIME" { "White" }
  139. "NOTIFY" { "Blue" }
  140. default { "White" }
  141. }
  142. Write-Host "[$ts] [$ActionType] [$DomainName] [$RemoteComputer] [$ScriptName] $Message" -ForegroundColor $color
  143.  
  144. # === Real-time domain status tracking ===
  145. if ($DomainName -and $DomainName -notlike "<*>" -and $DomainName -ne "<Local>") {
  146. $statusPriority = @{ "SUCCESS" = 1; "WARNING" = 2; "ERROR" = 3 }
  147.  
  148. # Determine if new actionType updates status
  149. $current = if ($domainStatusTable.ContainsKey($DomainName)) {
  150. $domainStatusTable[$DomainName]
  151. } else {
  152. "SUCCESS"
  153. }
  154.  
  155. if ($statusPriority.ContainsKey($ActionType.ToUpper()) -and
  156. $statusPriority[$ActionType.ToUpper()] -gt $statusPriority[$current]) {
  157. $domainStatusTable[$DomainName] = $ActionType.ToUpper()
  158. } elseif (-not $domainStatusTable.ContainsKey($DomainName)) {
  159. $domainStatusTable[$DomainName] = "SUCCESS"
  160. }
  161. }
  162.  
  163. }
  164.  
  165. function Clean-OldReports {
  166. param (
  167. [string]$ReportRoot,
  168. [int]$RetentionDays = 30
  169. )
  170.  
  171. $scriptName = "Cleanup"
  172. Log-Activity "<Init>" "<Local>" $scriptName "INFO" "Running cleanup for folders older than $RetentionDays days under $ReportRoot"
  173.  
  174. try {
  175. $cutoff = (Get-Date).AddDays(-$RetentionDays)
  176.  
  177. $oldFolders = Get-ChildItem -Path $ReportRoot -Directory -ErrorAction Stop |
  178. Where-Object {
  179. $_.Name -match '^\d{4}-\d{2}-\d{2}$' -and $_.LastWriteTime -lt $cutoff
  180. }
  181.  
  182. if ($oldFolders.Count -eq 0) {
  183. Log-Activity "<Init>" "<Local>" $scriptName "INFO" "No old folders found to delete."
  184. return
  185. }
  186.  
  187. foreach ($folder in $oldFolders) {
  188. try {
  189. # Log each file before deletion
  190. $files = Get-ChildItem -Path $folder.FullName -Recurse -File -ErrorAction SilentlyContinue
  191. foreach ($file in $files) {
  192. $sizeKB = [math]::Round($file.Length / 1KB, 2)
  193. Log-Activity "<Cleanup>" $file.FullName $scriptName "INFO" "Deleting file ($sizeKB KB)"
  194. }
  195.  
  196. # Now delete the folder
  197. Remove-Item -Path $folder.FullName -Recurse -Force -ErrorAction Stop
  198. Log-Activity "<Cleanup>" "<Local>" $scriptName "SUCCESS" "Deleted folder: $($folder.FullName)"
  199. } catch {
  200. Log-Activity "<Cleanup>" "<Local>" $scriptName "ERROR" "Failed to delete folder $($folder.FullName): $($_.Exception.Message)"
  201. }
  202. }
  203. } catch {
  204. Log-Activity "<Cleanup>" "<Local>" $scriptName "ERROR" "Cleanup process failed: $($_.Exception.Message)"
  205. }
  206. }
  207.  
  208.  
  209. function Test-DomainAuth {
  210. param (
  211. [string]$DomainName,
  212. [string]$RemoteComputer
  213. )
  214.  
  215. try {
  216. Get-ADDomain -Server $RemoteComputer -ErrorAction Stop | Out-Null
  217. Log-Activity -DomainName $DomainName -RemoteComputer $RemoteComputer -ScriptName "AuthTest" -ActionType "SUCCESS" -Message "Authenticated successfully."
  218. return $true
  219. } catch {
  220. $outer = $_.Exception
  221. $deepest = $outer
  222. while ($deepest.InnerException) {
  223. $deepest = $deepest.InnerException
  224. }
  225.  
  226. $baseMessage = "Authentication failed against ${RemoteComputer}: $($outer.Message)"
  227. $innerNote = if ($deepest -ne $outer) { " | InnerException: $($deepest.Message)" } else { "" }
  228. $fullMessage = "$baseMessage$innerNote"
  229.  
  230. Log-Activity -DomainName $DomainName -RemoteComputer $RemoteComputer -ScriptName "AuthTest" -ActionType "ERROR" -Message $fullMessage
  231. return $false
  232. }
  233. }
  234.  
  235. function Resolve-TrustedDomainDC {
  236. param (
  237. [Parameter(Mandatory)][string]$DomainName
  238. )
  239.  
  240. $logName = "DC-Resolve"
  241. $testedDCs = @()
  242. $pdc = $null
  243.  
  244. # Attempt to discover and log the Preferred PDC
  245. try {
  246. $pdc = Get-ADDomainController -Discover -Service "PrimaryDC" -DomainName $DomainName -ErrorAction Stop
  247. $pdcName = $pdc.DNSHostName
  248. if (-not $pdcName) { $pdcName = $pdc.HostName }
  249. if (-not $pdcName) { $pdcName = $pdc.Name }
  250. Log-Activity $DomainName $pdcName $logName "INFO" "Preferred PDC discovered: $pdcName"
  251. } catch {
  252. Log-Activity $DomainName "<None>" $logName "WARNING" "PDC resolution failed: $($_.Exception.Message)"
  253. }
  254.  
  255. # Try the preferred PDC first
  256. if ($pdc) {
  257. $pdcCandidates = @($pdc.DNSHostName, $pdc.HostName, $pdc.Name) | Where-Object { $_ }
  258.  
  259. foreach ($candidate in $pdcCandidates) {
  260. $testedDCs += $candidate
  261. if (Test-Connection -ComputerName $candidate -Count 1 -Quiet) {
  262. Log-Activity $DomainName $candidate $logName "INFO" "Selected preferred PDC: $candidate"
  263. return @{ Computer = $candidate; DcsTested = $testedDCs }
  264. }
  265. }
  266.  
  267. Log-Activity $DomainName "<None>" $logName "WARNING" "Could not reach preferred PDC after testing known candidates"
  268. }
  269.  
  270. # Try fallback DCs
  271. try {
  272. $dcs = Get-ADDomainController -Filter * -Server $DomainName
  273. foreach ($dc in $dcs) {
  274. $candidate = $dc.DNSHostName
  275. if (-not $candidate) { $candidate = $dc.HostName }
  276. if (-not $candidate) { $candidate = $dc.Name }
  277. if (-not $candidate) { continue }
  278.  
  279. $testedDCs += $candidate
  280. Log-Activity $DomainName $candidate $logName "INFO" "Testing fallback DC: $candidate"
  281.  
  282. if (Test-Connection -ComputerName $candidate -Count 1 -Quiet) {
  283. Log-Activity $DomainName $candidate $logName "INFO" "Fallback DC selected: $candidate"
  284. return @{ Computer = $candidate; DcsTested = $testedDCs }
  285. } else {
  286. Log-Activity $DomainName $candidate $logName "WARNING" "Fallback DC $candidate unreachable via ping — skipping"
  287. }
  288. }
  289.  
  290. $dcList = $testedDCs -join ", "
  291. Log-Activity $DomainName "<None>" $logName "ERROR" "All fallback DCs failed. Tested: $dcList"
  292. } catch {
  293. Log-Activity $DomainName "<None>" $logName "ERROR" "Fallback DC lookup failed: $($_.Exception.Message)"
  294. }
  295.  
  296. return @{ Computer = $null; DcsTested = $testedDCs }
  297. }
  298.  
  299. function Send-EmailReport {
  300. param ([string]$Date)
  301.  
  302. Log-Activity "<AllDomains>" "<Local>" "EmailReport" "INFO" "Email sending is ENABLED. Attempting to send report..."
  303.  
  304. try {
  305. $activityLog = Import-Csv -Path $activityLogPath -Encoding UTF8
  306.  
  307. $warningsAndErrors = $activityLog | Where-Object { $_.ActionType -match "ERROR|WARNING" }
  308. $countErrors = ($warningsAndErrors | Where-Object { $_.ActionType -eq "ERROR" }).Count
  309. $countWarnings = ($warningsAndErrors | Where-Object { $_.ActionType -eq "WARNING" }).Count
  310.  
  311. $failedAuthReport = ""
  312. if (Test-Path $failedAuthPath) {
  313. $failedAuthDomains = Get-Content $failedAuthPath | Where-Object { $_ -match '\S' }
  314. $failedAuthReport = if ($failedAuthDomains.Count -gt 0) {
  315. "`nDomains with Failed Authentication:`n" + ($failedAuthDomains -join "`n") + "`n"
  316. } else {
  317. "`nNo authentication failures were detected.`n"
  318. }
  319. }
  320.  
  321. $crossDomainStatus = if ($EnableCrossDomainMemberLookups) { "ENABLED" } else { "DISABLED" }
  322.  
  323. # Grouping for summary
  324. $domainsSuccess = ($activityLog | Where-Object { $_.ActionType -eq "SUCCESS" }).DomainName | Sort-Object -Unique
  325. $domainsWarning = ($activityLog | Where-Object { $_.ActionType -eq "WARNING" }).DomainName | Sort-Object -Unique
  326. $domainsError = ($activityLog | Where-Object { $_.ActionType -eq "ERROR" }).DomainName | Sort-Object -Unique
  327.  
  328. $summaryHeader = @"
  329. Summary of Domain Processing:
  330. Success : $($domainsSuccess.Count) - $($domainsSuccess -join ", ")
  331. Warnings: $($domainsWarning.Count) - $($domainsWarning -join ", ")
  332. Errors : $($domainsError.Count) - $($domainsError -join ", ")
  333.  
  334. Cross-domain member resolution: $crossDomainStatus
  335. $failedAuthReport
  336. "@
  337.  
  338. $details = if ($warningsAndErrors.Count -eq 0) {
  339. "No errors or warnings occurred during the audit."
  340. } else {
  341. $warningsAndErrors | ForEach-Object {
  342. "[{0}] [{1}] [{2}] [{3}] {4}" -f $_.ActionType, $_.DomainName, $_.RemoteComputer, $_.ScriptName, $_.Message
  343. } | Out-String
  344. }
  345.  
  346. $scriptInfoLine = "Script: $($MyInvocation.MyCommand.Name) | Version: $scriptversion | Author: $scriptauthor | Last Updated: $scriptupdated`n`n"
  347. $body = $scriptInfoLine + $summaryHeader + $details
  348. $subject = "AD Audit Summary: $countErrors ERROR(s), $countWarnings WARNING(s) - $Date"
  349.  
  350. $attachments = @($activityLogPath)
  351. if (Test-Path $failedAuthPath) {
  352. $attachments += $failedAuthPath
  353. }
  354.  
  355. Send-MailMessage -To $emailTo -From $emailFrom -Subject $subject -Body $body -SmtpServer $smtpServer -Port $smtpPort -Attachments $attachments
  356. Log-Activity "<AllDomains>" "<Local>" "EmailReport" "NOTIFY" "Email sent to $emailTo with activity log and any failed authentication log"
  357. } catch {
  358. Log-Activity "<AllDomains>" "<Local>" "EmailReport" "ERROR" "Failed to send email: $_"
  359. }
  360. }
  361.  
  362. function Get-AllGroupInputsFromCsvFolder {
  363. [CmdletBinding()]
  364. param ()
  365.  
  366. $scriptName = "GroupInputLoader"
  367. $groupData = @() # Native array of PSCustomObjects
  368.  
  369. if (-not (Test-Path $global:GroupInputFolder)) {
  370. Log-Activity "<Init>" "<Local>" $scriptName "WARNING" "Group input folder does not exist: $global:GroupInputFolder"
  371. return $groupData
  372. }
  373.  
  374. $csvFiles = Get-ChildItem -Path $global:GroupInputFolder -Filter *.csv -File -ErrorAction SilentlyContinue
  375.  
  376. if ($csvFiles.Count -eq 0) {
  377. Log-Activity "<Init>" "<Local>" $scriptName "WARNING" "No CSV files found in folder: $global:GroupInputFolder"
  378. return $groupData
  379. }
  380.  
  381. foreach ($csvFile in $csvFiles) {
  382. try {
  383. $rawData = Import-Csv -Path $csvFile.FullName
  384. if ($rawData.Count -eq 0) {
  385. Log-Activity "<Init>" "<Local>" $scriptName "WARNING" "$($csvFile.Name) is empty"
  386. continue
  387. }
  388.  
  389. $sampleHeaders = $rawData[0].PSObject.Properties.Name
  390. $domainCol = $sampleHeaders | Where-Object { $_ -match 'domain' } | Select-Object -First 1
  391. $groupCol = $sampleHeaders | Where-Object { $_ -match 'group' } | Select-Object -First 1
  392.  
  393. if (-not $domainCol -or -not $groupCol) {
  394. Log-Activity "<Init>" "<Local>" $scriptName "WARNING" "Could not identify Domain and Group columns in $($csvFile.Name)"
  395. continue
  396. }
  397.  
  398. $entryCount = 0
  399. foreach ($entry in $rawData) {
  400. $domainVal = $entry.$domainCol
  401. $groupVal = $entry.$groupCol
  402.  
  403. if ([string]::IsNullOrWhiteSpace($domainVal) -or [string]::IsNullOrWhiteSpace($groupVal)) {
  404. continue
  405. }
  406.  
  407. # Strip quotes and whitespace
  408. $cleanDomain = ($domainVal -replace "^[\'""]+|[\'""]+$", "").ToLower().Trim()
  409. $cleanGroup = ($groupVal -replace "^[\'""]+|[\'""]+$", "").Trim()
  410.  
  411. if ($cleanDomain -and $cleanGroup) {
  412. $groupData += [PSCustomObject]@{
  413. Domain = $cleanDomain
  414. GroupName = $cleanGroup
  415. }
  416. $entryCount++
  417. }
  418. }
  419.  
  420. Log-Activity "<Init>" "<Local>" $scriptName "INFO" "Loaded $entryCount valid entries from $($csvFile.Name) using columns '$domainCol' and '$groupCol'"
  421. } catch {
  422. Log-Activity "<Init>" "<Local>" $scriptName "ERROR" "Failed to process $($csvFile.FullName): $_"
  423. }
  424. }
  425.  
  426. return $groupData
  427. }
  428.  
  429. if (-not $EnableAllGroupsQuery) {
  430. $global:GroupInputArray = @(Get-AllGroupInputsFromCsvFolder)
  431. }
  432.  
  433. function Start-DomainJobs {
  434. param (
  435. [array]$TrustList,
  436. [string]$OutputRoot,
  437. [string]$Date,
  438. [bool]$EnableAllGroupsQuery,
  439. [bool]$EnableCrossDomainMemberLookups,
  440. [bool]$EnableConsoleOutput,
  441. [array]$GroupInputArray,
  442. [scriptblock]$GroupMembershipScript,
  443. [string]$ActivityLogPath,
  444. [int]$ThrottleLimit,
  445. [string]$DailyFolder
  446. )
  447.  
  448. $domainJobs = @()
  449.  
  450. foreach ($trust in $TrustList) {
  451. $domainName = $trust.Name
  452.  
  453. while ($domainJobs.Count -ge $ThrottleLimit) {
  454. # Remove any nulls before processing
  455. $validJobs = $domainJobs | Where-Object { $_ -ne $null }
  456.  
  457. # Log any null jobs (if they appear)
  458. if ($validJobs.Count -lt $domainJobs.Count) {
  459. $nullCount = $domainJobs.Count - $validJobs.Count
  460. Log-Activity "<Parallel>" "<Local>" "JobLaunch" "WARNING" "Detected $nullCount job(s) failed to start and were removed from tracking."
  461. }
  462.  
  463. # Update working job list
  464. $domainJobs = $validJobs
  465.  
  466. # Wait for any job to complete
  467. $finished = if ($domainJobs.Count -gt 0) {
  468. Wait-Job -Job $domainJobs -Any -Timeout 5
  469. } else {
  470. $null
  471. }
  472.  
  473. if ($finished) {
  474. try {
  475. Receive-Job -Job $finished | Out-Null
  476. } catch {
  477. Log-Activity "<Parallel>" "<Local>" "JobReceive" "ERROR" "Exception while receiving completed job output: $_"
  478. }
  479.  
  480. # Remove job from tracking
  481. Remove-Job -Job $finished
  482. $domainJobs = $domainJobs | Where-Object { $_ -and $_.State -eq 'Running' }
  483. }
  484. }
  485.  
  486.  
  487.  
  488. # Start parallel job
  489. $job = Start-ThreadJob -ScriptBlock {
  490. param (
  491. $DomainName,
  492. $Trust,
  493. $OutputRoot,
  494. $Date,
  495. $EnableAllGroupsQuery,
  496. $EnableCrossDomainMemberLookups,
  497. $EnableConsoleOutput,
  498. $GroupInputArray,
  499. $GroupMembershipScript,
  500. $ActivityLogPath,
  501. $DailyFolder
  502. )
  503.  
  504. $script:EnableConsoleOutput = $EnableConsoleOutput
  505.  
  506. Import-Module ActiveDirectory
  507.  
  508. # Includee the required functions
  509. function Log-Activity {
  510. param (
  511. [string]$DomainName,
  512. [string]$RemoteComputer,
  513. [string]$ScriptName,
  514. [string]$ActionType,
  515. [string]$Message
  516. )
  517.  
  518. $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
  519.  
  520. # Prepare CSV-safe line
  521. $csvSafeMessage = '"' + ($Message -replace '"', '""') + '"'
  522. $logLine = "$DomainName,$RemoteComputer,$ScriptName,$ActionType,$csvSafeMessage"
  523.  
  524. # === Write to disk using Add-Content (safe in parallel jobs) ===
  525. try {
  526. Add-Content -Path $ActivityLogPath -Value $logLine -Encoding UTF8 -ErrorAction Stop
  527. } catch {
  528. # Failsafe logging fallback (optional)
  529. Write-Warning "[$ts] [LogFail] Could not write log line to ${ActivityLogPath}: $_"
  530. }
  531.  
  532. # === Optional console output ===
  533. if ($EnableConsoleOutput) {
  534. $color = switch ($ActionType.ToUpper()) {
  535. "INFO" { "Cyan" }
  536. "SUCCESS" { "Green" }
  537. "WARNING" { "Yellow" }
  538. "ERROR" { "Red" }
  539. "RUNTIME" { "White" }
  540. "NOTIFY" { "Blue" }
  541. default { "White" }
  542. }
  543. Write-Host "[$ts] [$ActionType] [$DomainName] [$RemoteComputer] [$ScriptName] $Message" -ForegroundColor $color
  544. }
  545. }
  546.  
  547. function Resolve-TrustedDomainDC {
  548. param (
  549. [Parameter(Mandatory)][string]$DomainName
  550. )
  551.  
  552. $logName = "DC-Resolve"
  553. $testedDCs = @()
  554. $pdc = $null
  555.  
  556. # Attempt to discover and log the Preferred PDC
  557. try {
  558. $pdc = Get-ADDomainController -Discover -Service "PrimaryDC" -DomainName $DomainName -ErrorAction Stop
  559. $pdcName = $pdc.DNSHostName
  560. if (-not $pdcName) { $pdcName = $pdc.HostName }
  561. if (-not $pdcName) { $pdcName = $pdc.Name }
  562. Log-Activity $DomainName $pdcName $logName "INFO" "Preferred PDC discovered: $pdcName"
  563. } catch {
  564. Log-Activity $DomainName "<None>" $logName "WARNING" "PDC resolution failed: $($_.Exception.Message)"
  565. }
  566.  
  567. # Try the preferred PDC first
  568. if ($pdc) {
  569. $pdcCandidates = @($pdc.DNSHostName, $pdc.HostName, $pdc.Name) | Where-Object { $_ }
  570.  
  571. foreach ($candidate in $pdcCandidates) {
  572. $testedDCs += $candidate
  573. if (Test-Connection -ComputerName $candidate -Count 1 -Quiet) {
  574. Log-Activity $DomainName $candidate $logName "INFO" "Selected preferred PDC: $candidate"
  575. return @{ Computer = $candidate; DcsTested = $testedDCs }
  576. }
  577. }
  578.  
  579. Log-Activity $DomainName "<None>" $logName "WARNING" "Could not reach preferred PDC after testing known candidates"
  580. }
  581.  
  582. # Try fallback DCs
  583. try {
  584. $dcs = Get-ADDomainController -Filter * -Server $DomainName
  585. foreach ($dc in $dcs) {
  586. $candidate = $dc.DNSHostName
  587. if (-not $candidate) { $candidate = $dc.HostName }
  588. if (-not $candidate) { $candidate = $dc.Name }
  589. if (-not $candidate) { continue }
  590.  
  591. $testedDCs += $candidate
  592. Log-Activity $DomainName $candidate $logName "INFO" "Testing fallback DC: $candidate"
  593.  
  594. if (Test-Connection -ComputerName $candidate -Count 1 -Quiet) {
  595. Log-Activity $DomainName $candidate $logName "INFO" "Fallback DC selected: $candidate"
  596. return @{ Computer = $candidate; DcsTested = $testedDCs }
  597. } else {
  598. Log-Activity $DomainName $candidate $logName "WARNING" "Fallback DC $candidate unreachable via ping — skipping"
  599. }
  600. }
  601.  
  602. $dcList = $testedDCs -join ", "
  603. Log-Activity $DomainName "<None>" $logName "ERROR" "All fallback DCs failed. Tested: $dcList"
  604. } catch {
  605. Log-Activity $DomainName "<None>" $logName "ERROR" "Fallback DC lookup failed: $($_.Exception.Message)"
  606. }
  607.  
  608. return @{ Computer = $null; DcsTested = $testedDCs }
  609. }
  610. function Test-DomainAuth {
  611. param (
  612. [string]$DomainName,
  613. [string]$RemoteComputer
  614. )
  615.  
  616. try {
  617. Get-ADDomain -Server $RemoteComputer -ErrorAction Stop | Out-Null
  618. Log-Activity -DomainName $DomainName -RemoteComputer $RemoteComputer -ScriptName "AuthTest" -ActionType "SUCCESS" -Message "Authenticated successfully."
  619. return $true
  620. } catch {
  621. $outer = $_.Exception
  622. $deepest = $outer
  623. while ($deepest.InnerException) {
  624. $deepest = $deepest.InnerException
  625. }
  626.  
  627. $baseMessage = "Authentication failed against ${RemoteComputer}: $($outer.Message)"
  628. $innerNote = if ($deepest -ne $outer) { " | InnerException: $($deepest.Message)" } else { "" }
  629. $fullMessage = "$baseMessage$innerNote"
  630.  
  631. Log-Activity -DomainName $DomainName -RemoteComputer $RemoteComputer -ScriptName "AuthTest" -ActionType "ERROR" -Message $fullMessage
  632. return $false
  633. }
  634. }
  635.  
  636. # Lightweight NTLM/Kerberos detection
  637. try {
  638. $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
  639. $authMethod = $identity.AuthenticationType
  640. $user = $identity.Name
  641.  
  642. if ($authMethod -eq "NTLM") {
  643. Log-Activity -DomainName $DomainName -RemoteComputer "<Local>" -ScriptName "AuthCheck" -ActionType "WARNING" -Message "Using NTLM authentication for $user — Kerberos likely failed or unavailable."
  644. } else {
  645. Log-Activity -DomainName $DomainName -RemoteComputer "<Local>" -ScriptName "AuthCheck" -ActionType "INFO" -Message "Using $authMethod authentication for $user."
  646. }
  647. } catch {
  648. Log-Activity -DomainName $DomainName -RemoteComputer "<Local>" -ScriptName "AuthCheck" -ActionType "WARNING" -Message "Could not determine authentication method: $($_.Exception.Message)"
  649. }
  650. # Define the domain and create the domain-specific subfolder for the reports
  651. $domainName = $trust.Name
  652. $domainFolder = Join-Path $dailyFolder $domainName
  653. if (-not (Test-Path $domainFolder)) {
  654. New-Item -ItemType Directory -Path $domainFolder -Force | Out-Null
  655. }
  656.  
  657. $dcStartTime = Get-Date
  658. $dcResult = Resolve-TrustedDomainDC -DomainName $domainName
  659. $remoteComputer = [string]$dcResult.Computer
  660.  
  661. # Catch if there's a null value or no DC was reachable and stop processing this loop
  662. if (-not $remoteComputer) {
  663. Log-Activity $domainName "<None>" "DC-Select" "ERROR" "No reachable DC found for $domainName"
  664. continue
  665. }
  666.  
  667. # Test to make sure AD cmdlets work talking to the remote DC, no point in running the reports otherwise
  668. if (-not (Test-DomainAuth -DomainName $domainName -RemoteComputer $remoteComputer)) {
  669. continue
  670. }
  671.  
  672. $dcDuration = (Get-Date) - $dcStartTime
  673. $dcTime = "{0:F3}" -f $dcDuration.TotalSeconds
  674. Log-Activity $domainName "<Local>" "DC-Select" "RUNTIME" "DC selection took $dcTime seconds"
  675.  
  676. # Here we define the reports themselves
  677. $reportScripts = @(
  678. @{ Name = "AD_User_Export"; Script = {
  679. param([string]$Server)
  680. Get-ADUser -Server $Server -Filter * -Properties UserPrincipalName, whenCreated, LastLogonDate, PasswordLastSet, PasswordNeverExpires, PasswordExpired, PasswordNotRequired, Enabled, ObjectGUID |
  681. Select-Object Name, SamAccountName, UserPrincipalName, whenCreated, LastLogonDate, PasswordLastSet, PasswordNeverExpires, PasswordExpired, PasswordNotRequired, Enabled, ObjectGUID
  682. }},
  683. @{ Name = "AdminCount"; Script = {
  684. param([string]$Server)
  685. Get-ADUser -Server $Server -Filter 'adminCount -eq 1' -Properties adminCount, Name, SamAccountName, UserPrincipalName |
  686. Select-Object Name, SamAccountName, UserPrincipalName, adminCount
  687. }},
  688. @{ Name = "DomainTrusts"; Script = {
  689. param([string]$Server)
  690. Get-ADTrust -Server $Server -Filter * |
  691. Select-Object Name, TrustType, TrustDirection, TrustAttributes
  692. }}
  693. )
  694.  
  695. # Some error handling here for the group membership script
  696. if (-not $domainName) {
  697. Log-Activity "<Init>" "<Local>" "GroupMembership" "WARNING" "Target domainName is null or empty — GroupMembership will be skipped."
  698. } elseif (-not $EnableAllGroupsQuery -and (-not $GroupInputArray -or $GroupInputArray.Count -eq 0)) {
  699. Log-Activity "<Init>" "<Local>" "GroupMembership" "WARNING" "Group input array is empty — Domain-specific GroupMembership report will be skipped."
  700. } else {
  701. # Filter for matching domain entries only (or the script will take days to run)
  702. if ($EnableAllGroupsQuery) {
  703. try {
  704. $domainGroupEntries = Get-ADGroup -Server $remoteComputer -Filter * | Select-Object @{Name="Domain";Expression={$domainName}}, Name
  705. $domainGroupEntries = $domainGroupEntries | ForEach-Object {
  706. [PSCustomObject]@{
  707. Domain = $_.Domain
  708. GroupName = $_.Name
  709. }
  710. }
  711. Log-Activity $domainName $remoteComputer "GroupMembership" "INFO" "Queried all groups in $domainName — total: $($domainGroupEntries.Count)"
  712. } catch {
  713. Log-Activity $domainName $remoteComputer "GroupMembership" "ERROR" "Failed to retrieve all groups for ${domainName}: $_"
  714. $domainGroupEntries = @()
  715. }
  716. } else {
  717. $domainGroupEntries = $GroupInputArray | Where-Object { $_.Domain -eq $domainName }
  718. Log-Activity $domainName "<Debug>" "DomainDebug" "INFO" "Filtered $($domainGroupEntries.Count) group entries for $domainName"
  719. }
  720. Log-Activity $domainName "<Debug>" "DomainDebug" "INFO" "Filtering group entries for $domainName"
  721.  
  722. # Pre-check: Can we resolve Domain Admins in this domain?
  723. $domainAdminCheck = $null
  724. try {
  725. $domainAdminCheck = Get-ADGroup -Server $remoteComputer -Identity "Domain Admins" -ErrorAction Stop
  726. Log-Activity $domainName $remoteComputer "GroupMembership" "INFO" "Verified Domain Admins group exists: $($domainAdminCheck.DistinguishedName)"
  727. } catch {
  728. Log-Activity $domainName $remoteComputer "GroupMembership" "ERROR" "Cannot resolve Domain Admins group — skipping group membership lookups for this domain"
  729. $domainGroupEntries = @()
  730. }
  731.  
  732. if ($domainGroupEntries.Count -gt 0) {
  733. $reportScripts += @{
  734. Name = "GroupMembership"
  735. Script = $groupMembershipScript
  736. Arguments = @($domainGroupEntries, $domainName, $remoteComputer)
  737. }
  738. Log-Activity "<Init>" "<Local>" "GroupMembership" "INFO" "Found $($domainGroupEntries.Count) group entries for $domainName"
  739. } else {
  740. Log-Activity "<Init>" "<Local>" "GroupMembership" "WARNING" "No matching group entries found for $domainName — skipping"
  741. }
  742. }
  743.  
  744. # Process the reports defined earlier
  745. foreach ($script in $reportScripts) {
  746. $name = $script.Name
  747. $csvPath = Join-Path $domainFolder "$domainName-$name-$Date.csv"
  748. $warnPath = Join-Path $domainFolder "$domainName-$name-Warnings.txt"
  749. $memBefore = [GC]::GetTotalMemory($false)
  750. $scriptStart = Get-Date
  751.  
  752. try {
  753. if ($script.ContainsKey("Arguments")) {
  754. $argList = $script.Arguments
  755. # Listing individual arguments is necessary to avoid splatting the array.
  756. $result = & $script.Script $argList[0] $argList[1] $argList[2]
  757. } else {
  758. $result = & $script.Script -Server $remoteComputer
  759. }
  760.  
  761. if ($name -eq "GroupMembership") {
  762. $result.Results | Export-Csv -Path $csvPath -NoTypeInformation -Force
  763. $result.Warnings | Out-File -FilePath $warnPath -Encoding UTF8 -Force
  764. Log-Activity $domainName $remoteComputer $name "SUCCESS" "Exported $($result.Results.Count) users with $($result.Warnings.Count) warnings"
  765. } else {
  766. $result | Export-Csv -Path $csvPath -NoTypeInformation -Force
  767. Log-Activity $domainName $remoteComputer $name "SUCCESS" "Exported $($result.Count) records"
  768. }
  769. } catch {
  770. Log-Activity $domainName $remoteComputer $name "ERROR" "Failed to run script: $_"
  771. } finally {
  772. $memAfter = [GC]::GetTotalMemory($false)
  773. $memDelta = $memAfter - $memBefore
  774. $memUsedMB = "{0:N2}" -f ($memDelta / 1MB)
  775. $scriptDuration = (Get-Date) - $scriptStart
  776. $scriptTime = "{0:F3}" -f $scriptDuration.TotalSeconds
  777. Log-Activity $domainName $remoteComputer $name "RUNTIME" "$name execution took $scriptTime seconds"
  778. Log-Activity $domainName $remoteComputer $name "RUNTIME" "$name memory delta: $memUsedMB MB"
  779. }
  780. }
  781. } -ArgumentList $domainName, $trust, $OutputRoot, $Date, $EnableAllGroupsQuery, $EnableCrossDomainMemberLookups, $EnableConsoleOutput, $GroupInputArray, $GroupMembershipScript, $ActivityLogPath, $DailyFolder
  782.  
  783. if ($job) {
  784. $domainJobs += $job
  785. } else {
  786. Log-Activity "<Parallel>" $domainName "JobLaunch" "ERROR" "Failed to start job for $domainName"
  787. }
  788. }
  789.  
  790. # Wait for any remaining jobs to finish
  791. $domainJobs = $domainJobs | Where-Object { $_ -ne $null }
  792.  
  793. if ($domainJobs.Count -eq 0) {
  794. Log-Activity "<Parallel>" "<Local>" "JobMonitor" "WARNING" "No valid jobs were found to complete. All may have failed to launch."
  795. }
  796.  
  797. foreach ($j in $domainJobs) {
  798. $jobDomain = "<Unknown>"
  799. try {
  800. # Attempt to receive output and identify source domain
  801. if ($j.ChildJobs.Count -gt 0 -and $j.ChildJobs[0].JobStateInfo.Location) {
  802. $jobDomain = $j.ChildJobs[0].JobStateInfo.Location
  803. }
  804.  
  805. Receive-Job -Job $j -ErrorAction Stop | Out-Null
  806. } catch {
  807. Log-Activity $jobDomain "<Unknown>" "JobReceive" "ERROR" "Exception receiving job output: $_"
  808. }
  809.  
  810. if ($j.State -eq 'Failed' -or $j.HasMoreData -eq $false) {
  811. if ($j.ChildJobs[0].JobStateInfo.Reason) {
  812. $reason = $j.ChildJobs[0].JobStateInfo.Reason.Exception.Message
  813. Log-Activity $jobDomain "<Unknown>" "JobFailure" "ERROR" "Background job failed: $reason"
  814. } else {
  815. Log-Activity $jobDomain "<Unknown>" "JobFailure" "ERROR" "Background job ended in failed state without specific error."
  816. }
  817. }
  818.  
  819. Remove-Job -Job $j
  820. }
  821. }
  822.  
  823. # Log script start time
  824. Log-Activity "<SCRIPT>" "$env:hostname" "INFO" "Script starting at $DayStamp"
  825.  
  826. # Check that Start-ThreadJob is available
  827. if (-not (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue)) {
  828. Write-Warning "Start-ThreadJob is not available. Please install the ThreadJob module: Install-Module ThreadJob"
  829. Exit 1
  830. }
  831.  
  832.  
  833. # GroupMembership script now filters for target domain, not current domain (previous version used the context domain causing lookups to fail)
  834. $groupMembershipScript = {
  835. param (
  836. [object[]]$inputArray,
  837. [string]$targetDomain,
  838. [string]$remoteComputer,
  839. [bool]$EnableCrossDomainMemberLookups = $false
  840. )
  841.  
  842. Import-Module ActiveDirectory
  843. $results = New-Object System.Collections.Generic.List[Object]
  844. $warnings = New-Object System.Collections.Generic.List[string]
  845. $visited = @{}
  846.  
  847. function Get-DomainFromDN {
  848. param ($dn)
  849. if ($dn -match "DC=") {
  850. return ($dn -split "," | Where-Object { $_ -like "DC=*" } | ForEach-Object { $_ -replace "DC=", "" }) -join "."
  851. }
  852. }
  853.  
  854. function Normalize-Domain {
  855. param ($domainName)
  856. if ($null -eq $domainName) { return $null }
  857. return $domainName.ToLower().Trim()
  858. }
  859.  
  860. function Recurse-Members {
  861. param (
  862. $GroupDN,
  863. $Trail,
  864. $GroupDomain,
  865. $GroupName
  866. )
  867.  
  868. if (-not $GroupDN) {
  869. $warnings.Add("Skipping group with empty DN — GroupDomain: '$GroupDomain', GroupName: '$GroupName'")
  870. return
  871. }
  872.  
  873. foreach ($memberDN in $group.Member) {
  874. $memberDomain = Get-DomainFromDN -dn $memberDN
  875. $normalizedTarget = $targetDomain.ToLower()
  876. $member = $null
  877.  
  878. if ($memberDomain -eq $normalizedTarget) {
  879. try {
  880. $member = Get-ADObject -Server $remoteComputer -Identity $memberDN -Properties objectClass, samAccountName, name, distinguishedName
  881. $warnings += "Fetched member: $($member.SamAccountName) from $memberDomain"
  882. } catch {
  883. $warnings += "Failed to get member '$memberDN': $($_.Exception.Message)"
  884. continue
  885. }
  886. } elseif ($EnableCrossDomainMemberLookups) {
  887. $foreignDC = $null
  888. try {
  889. $foreignDCInfo = Get-ADDomainController -Discover -DomainName $memberDomain -Service PrimaryDC -ErrorAction Stop
  890. $foreignDC = $foreignDCInfo.DNSHostName
  891. $warnings += "Discovered foreign DC for ${memberDomain}: $foreignDC"
  892. } catch {
  893. $warnings += "Could not resolve DC for ${memberDomain}: $($_.Exception.Message)"
  894. continue
  895. }
  896.  
  897. if ($foreignDC) {
  898. try {
  899. $member = Get-ADObject -Server $foreignDC -Identity $memberDN -Properties objectClass, samAccountName, name, distinguishedName
  900. $warnings += "Fetched foreign member: $($member.SamAccountName) from $memberDomain"
  901. } catch {
  902. $warnings += "Failed remote fetch for $memberDN from ${foreignDC}: $($_.Exception.Message)"
  903. continue
  904. }
  905. }
  906. } else {
  907. $warnings += "Skipping cross-domain lookup for $memberDN (domain=$memberDomain) — feature disabled"
  908. continue
  909. }
  910.  
  911. if ($null -eq $member) { continue }
  912.  
  913. if ($member.objectClass -eq 'user') {
  914. $results.Add([PSCustomObject]@{
  915. GroupDomain = $GroupDomain
  916. GroupName = $GroupName
  917. UserDomain = $memberDomain
  918. SamAccountName = $member.SamAccountName
  919. NestedTrail = $Trail
  920. })
  921. } elseif ($member.objectClass -eq 'group') {
  922. $key = "$memberDomain\$($member.DistinguishedName)"
  923. if (-not $visited.ContainsKey($key)) {
  924. $visited[$key] = $true
  925. $nextTrail = if ($Trail) { "$Trail > $($member.Name)@$memberDomain" } else { "$($member.Name)@$memberDomain" }
  926. Recurse-Members -GroupDN $member.DistinguishedName -Trail $nextTrail -GroupDomain $GroupDomain -GroupName $GroupName
  927. }
  928. }
  929. }
  930. }
  931.  
  932.  
  933. # Sanity check
  934. if ($null -eq $inputArray -or $inputArray.Count -eq 0) {
  935. $warnings.Add("Input array was null or empty. Skipping execution.")
  936. return @{ Results = $results; Warnings = $warnings }
  937. }
  938.  
  939. foreach ($entry in $inputArray) {
  940. try {
  941. $resolvedGroup = Get-ADGroup -Server $remoteComputer -Identity $entry.GroupName -Properties DistinguishedName -ErrorAction Stop
  942. $key = "$targetDomain\$($resolvedGroup.DistinguishedName)"
  943. if (-not $visited.ContainsKey($key)) {
  944. $visited[$key] = $true
  945. Recurse-Members -GroupDN $resolvedGroup.DistinguishedName -Trail "" -GroupDomain $targetDomain -GroupName $resolvedGroup.Name
  946. }
  947. } catch {
  948. $warnings.Add("Could not resolve group '$($entry.GroupName)' in domain '$targetDomain': $($_.Exception.Message)")
  949. }
  950. }
  951. Log-Activity "<Debug>" "<Local>" "GroupMembership" "INFO" "inputArray type: $($inputArray.GetType().FullName), count: $($inputArray.Count)"
  952. return @{ Results = $results; Warnings = $warnings }
  953. }
  954.  
  955. # Domain and trust discovery that exports the trust list for logging purposes and import purposes
  956. Import-Module ActiveDirectory
  957.  
  958. # Get the forest name, get the trust list, and export to a file.
  959. try {
  960. $Forest = Get-ADForest -ErrorAction Stop
  961. $ForestName = $Forest.Name
  962. $trusts = Get-ADTrust -Filter * -ErrorAction Stop | Where-Object { $_.TrustDirection -ne 'Inbound' -and $_.Name -notlike "*.EXEMPTDOMAINS.com" }
  963. $trustList = $trusts | Select-Object Name, TrustType, TrustDirection, TrustAttributes
  964. $csvPath1 = Join-Path $outputRoot "$ForestName-DomainTrusts-$Date.csv"
  965. $trustList | Export-Csv -Path $csvPath1 -NoTypeInformation
  966. Log-Activity -DomainName "<LocalForest>" -RemoteComputer "<Local>" -ScriptName "Get-ADTrust" -ActionType "INFO" -Message "Exported domain trusts to $csvPath1"
  967. } catch {
  968. Log-Activity -DomainName "<LocalForest>" -RemoteComputer "<Local>" -ScriptName "Get-ADTrust" -ActionType "ERROR" -Message $_.Exception.Message
  969. Exit
  970. }
  971.  
  972. # Import the trusted domains list from the file
  973. $trusts = Import-Csv -Path $csvPath1
  974.  
  975. # If the EnableLocalDomain variable is true, process the host's local domain
  976. if ($EnableLocalDomain) {
  977. $contextDomain = (Get-ADDomain).DNSRoot
  978.  
  979. # Create a pseudo-trust object for compatibility
  980. $localTrust = [PSCustomObject]@{
  981. Name = $contextDomain
  982. }
  983.  
  984. Start-DomainJobs -TrustList @($localTrust) `
  985. -OutputRoot $outputRoot `
  986. -Date $Date `
  987. -EnableAllGroupsQuery $EnableAllGroupsQuery `
  988. -EnableCrossDomainMemberLookups $EnableCrossDomainMemberLookups `
  989. -EnableConsoleOutput $EnableConsoleOutput `
  990. -GroupInputArray $global:GroupInputArray `
  991. -GroupMembershipScript $groupMembershipScript `
  992. -ActivityLogPath $activityLogPath `
  993. -ThrottleLimit $MaxParallelDomainJobs `
  994. -DailyFolder $dailyFolder
  995. }
  996.  
  997. # Now call the function to loop through each trust and find a reachable DC preferring the PDC
  998. Start-DomainJobs -TrustList $trusts `
  999. -OutputRoot $outputRoot `
  1000. -Date $Date `
  1001. -EnableAllGroupsQuery $EnableAllGroupsQuery `
  1002. -EnableCrossDomainMemberLookups $EnableCrossDomainMemberLookups `
  1003. -EnableConsoleOutput $EnableConsoleOutput `
  1004. -GroupInputArray $global:GroupInputArray `
  1005. -GroupMembershipScript $groupMembershipScript `
  1006. -ActivityLogPath $activityLogPath `
  1007. -ThrottleLimit $MaxParallelDomainJobs `
  1008. -DailyFolder $dailyFolder
  1009.  
  1010. # Streamed summary logging — no need to re-import full log from disk
  1011. try {
  1012. $successes = $domainStatusTable.GetEnumerator() | Where-Object { $_.Value -eq "SUCCESS" } | ForEach-Object { $_.Key }
  1013. $warnings = $domainStatusTable.GetEnumerator() | Where-Object { $_.Value -eq "WARNING" } | ForEach-Object { $_.Key }
  1014. $errors = $domainStatusTable.GetEnumerator() | Where-Object { $_.Value -eq "ERROR" } | ForEach-Object { $_.Key }
  1015.  
  1016. if ($successes.Count -gt 0) {
  1017. Log-Activity "<Summary>" "<Local>" "Summary" "INFO" ("Domains with SUCCESS : " + ($successes -join ", "))
  1018. }
  1019. if ($warnings.Count -gt 0) {
  1020. Log-Activity "<Summary>" "<Local>" "Summary" "INFO" ("Domains with WARNINGS: " + ($warnings -join ", "))
  1021. }
  1022. if ($errors.Count -gt 0) {
  1023. Log-Activity "<Summary>" "<Local>" "Summary" "INFO" ("Domains with ERRORS : " + ($errors -join ", "))
  1024. }
  1025. } catch {
  1026. Log-Activity "<Summary>" "<Local>" "Summary" "ERROR" "Failed to summarize domain results: $_"
  1027. }
  1028.  
  1029. # Export the domain status table to CSV and strip any " marks from the output
  1030. try {
  1031. $domainStatusCsv = $domainStatusTable.GetEnumerator() | ForEach-Object {
  1032. [PSCustomObject]@{
  1033. DomainName = $_.Key
  1034. Status = $_.Value
  1035. }
  1036. }
  1037.  
  1038. $domainStatusCsv | Sort-Object DomainName | Export-Csv -Path $domainStatusPath -NoTypeInformation -Force
  1039.  
  1040. Log-Activity "<Summary>" "<Local>" "DomainStatus" "INFO" "Exported domain status table to $domainStatusPath"
  1041. } catch {
  1042. Log-Activity "<Summary>" "<Local>" "DomainStatus" "ERROR" "Failed to export domain status table: $($_.Exception.Message)"
  1043. }
  1044.  
  1045. # Send the report via email if the $sendEmail variable is $true
  1046. if ($sendEmail) { Send-EmailReport -Date $Date }
  1047. else {
  1048. Log-Activity "<AllDomains>" "<Local>" "EmailReport" "INFO" "Email sending is DISABLED. No report was sent."
  1049. }
  1050.  
  1051. # Run function to clean old reports, activity logs, and so on.
  1052. Clean-OldReports -ReportRoot $outputRoot -RetentionDays $RetentionDays
  1053.  
  1054. $scriptDuration = (Get-Date) - $scriptStartTime
  1055. $scriptTimeMin = "{0:F3}" -f $scriptDuration.TotalMinutes
  1056. Log-Activity "<Summary>" "<Local>" "Script" "RUNTIME" "Total script execution time: $scriptTimeMin minutes"
  1057. Log-Activity "<Summary>" "<Local>" "Script" "INFO" "Successfully completed script at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement