Public/Reports/Get-DomainReport.ps1

function Get-DomainReport {
    [CmdletBinding()]
    param(
        [ValidateScript({ Test-Path $_ })]
        [string]$ExportPath = $script:Config.ExportPath,
        [switch]$UseParallel
    )

    try {
        # Initialize tracking variables
        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        $results = @{}
        $errors = @{}
        $componentTiming = @{}

        # Define collection components
        $components = @{
            'ForestInfo'     = { Get-ADForestInfo }
            'TrustInfo'      = { Get-ADTrustInfo }
            'Sites'          = { Get-ADSiteInfo }
            'DomainInfo'     = { Get-ADDomainInfo }
            'Users'          = { Get-ADUsers }
            'Computers'      = { Get-ADComputers }
            'Groups'         = { Get-ADGroupsAndMembers }
            'SecurityConfig' = { Get-ADSecurityConfiguration }
        }

        # Collect data either in parallel or sequentially
        if ($UseParallel) {
            Write-Log "Starting parallel data collection..." -Level Info

            # Instead of converting to string, just store the scriptblock directly:
            $componentScripts = $components.GetEnumerator() | ForEach-Object {
                [PSCustomObject]@{
                    Name        = $_.Key
                    ScriptBlock = $_.Value # $_.Value is already a scriptblock
                }
            }

            # Now in the parallel block, just run it directly:
            $parallelResults = $componentScripts | ForEach-Object -ThrottleLimit $script:Config.MaxConcurrentJobs -Parallel {
                $component = $_.Name
                $scriptBlock = $_.ScriptBlock
                $componentSw = [System.Diagnostics.Stopwatch]::StartNew()

                try {
                    $data = & $scriptBlock
                    @{
                        Name          = $component
                        Data          = $data
                        Error         = $null
                        ExecutionTime = $componentSw.ElapsedMilliseconds
                    }
                }
                catch {
                    @{
                        Name          = $component
                        Data          = $null
                        Error         = $_.Exception.Message
                        ExecutionTime = $componentSw.ElapsedMilliseconds
                    }
                }
            }

            # Process parallel results
            foreach ($result in $parallelResults) {
                if ($result.Error) {
                    $errors[$result.Name] = $result.Error
                    Write-Log "Error collecting $($result.Name): $($result.Error)" -Level Error
                    if (-not $ContinueOnError) { throw $result.Error }
                }
                else {
                    $results[$result.Name] = $result.Data
                }
                $componentTiming[$result.Name] = $result.ExecutionTime
            }
        }
        else {
            # Sequential collection
            foreach ($component in $components.Keys) {
                $componentSw = [System.Diagnostics.Stopwatch]::StartNew()
                try {
                    Write-Log "Collecting $component..." -Level Info
                    $results[$component] = & $components[$component]
                    $componentTiming[$component] = $componentSw.ElapsedMilliseconds
                }
                catch {
                    $errors[$component] = $_.Exception.Message
                    $componentTiming[$component] = $componentSw.ElapsedMilliseconds
                    Write-Log "Error collecting ${component}: $($_.Exception.Message)" -Level Error
                    if (-not $ContinueOnError) { throw }
                }
            }
        }

        # Create the final report object
        $domainReport = [PSCustomObject]@{
            CollectionTime     = Get-Date
            CollectionStatus   = if ($errors.Count -eq 0) { "Complete" } else { "Partial" }
            Errors             = $errors
            PerformanceMetrics = $componentTiming
            TotalExecutionTime = $sw.ElapsedMilliseconds
            BasicInfo          = [PSCustomObject]@{
                ForestInfo = $results['ForestInfo']
                TrustInfo  = $results['TrustInfo']
                Sites      = $results['Sites']
                DomainInfo = $results['DomainInfo']
            }
            DomainObjects      = [PSCustomObject]@{
                Users     = $results['Users']
                Computers = $results['Computers']
                Groups    = $results['Groups']
            }
            SecuritySettings   = [PSCustomObject]@{
                SecurityConfig = $results['SecurityConfig']
            }
        }

        # Add report generation metadata
        Add-Member -InputObject $domainReport -MemberType NoteProperty -Name "ReportGeneration" -Value @{
            GeneratedBy       = $env:USERNAME
            GeneratedOn       = Get-Date
            ComputerName      = $env:COMPUTERNAME
            PowerShellVersion = $PSVersionTable.PSVersion.ToString()
        }

        # Add methods to the report object
        Add-DomainReportMethods -DomainReport $domainReport


        # Export the report if requested
        if ($ExportPath) {
            $exportFile = Join-Path $ExportPath ("DomainReport_{0}.json" -f (Get-Date -Format 'yyyyMMdd_HHmmss'))
            $domainReport | ConvertTo-Json -Depth 10 | Out-File $exportFile
            Write-Log "Report exported to: $exportFile" -Level Info
        }

        return $domainReport
    }
    catch {
        Write-Log "Critical error in Get-DomainReport: $($_.Exception.Message)" -Level Error
        throw
    }
    finally {
        $sw.Stop()
        Write-Log "Total execution time: $($sw.ElapsedMilliseconds)ms" -Level Info
    }
}

# Example usage:
# $report = Get-DomainReport -UseParallel

function Add-DomainReportMethods {
    param (
        [Parameter(Mandatory)]
        [PSCustomObject]$DomainReport
    )

    # Add custom ToString() method for BasicInfo
    $basicInfoToString = {
        $forest = if ($this.ForestInfo.Name) { $this.ForestInfo.Name } else { "N/A" }
        $domain = if ($this.DomainInfo.DomainName) { $this.DomainInfo.DomainName } else { "N/A" }
        $sites = if ($this.Sites.TotalSites) { $this.Sites.TotalSites } else { "0" }
        $trusts = if ($this.TrustInfo) { $this.TrustInfo.Count } else { "0" }
        
        return "forest=$forest, domain=$domain, sites=$sites, trusts=$trusts"
    }

    # Add custom ToString() method for DomainObjects
    $domainObjectsToString = {
        $users = if ($this.Users) { $this.Users.Count } else { "0" }
        $computers = if ($this.Computers) { $this.Computers.Count } else { "0" }
        $groups = if ($this.Groups) { $this.Groups.Count } else { "0" }
        
        return "users=$users, computers=$computers, groups=$groups"
    }

    # Add custom ToString() method for SecuritySettings
    $securitySettingsToString = {
        $spns = if ($this.SecurityConfig.SPNConfiguration) { 
            $this.SecurityConfig.SPNConfiguration.Count 
        }
        else { 
            "0" 
        }
        
        $acls = if ($this.SecurityConfig.ObjectACLs) { 
            $this.SecurityConfig.ObjectACLs.Count 
        }
        else { 
            "0" 
        }
        
        return "SPNs=$spns, ACLs=$acls"
    }
    # Add the ToString methods to each object
    Add-Member -InputObject $DomainReport.BasicInfo -MemberType ScriptMethod -Name "ToString" -Value $basicInfoToString -Force
    Add-Member -InputObject $DomainReport.DomainObjects -MemberType ScriptMethod -Name "ToString" -Value $domainObjectsToString -Force
    Add-Member -InputObject $DomainReport.SecuritySettings -MemberType ScriptMethod -Name "ToString" -Value $securitySettingsToString -Force

    # Define a scriptblock for scanning common ports on all computers
    $scanPorts = {
        param(
            [int[]]$Ports = (80, 443, 445, 3389, 5985),
            [int]$Timeout = 1000
        )

        # Verify we have DomainObjects.Computers
        if (-not $this.DomainObjects.Computers) {
            Write-Host "No computers found in the domain report. Cannot scan ports."
            return $null
        }

        $results = @()

        foreach ($comp in $this.DomainObjects.Computers) {
            # Check if the host is alive before scanning
            if (-not $comp.IsAlive) {
                Write-Host "Skipping $($comp.Name) because IsAlive=$($comp.IsAlive)"
                continue
            }

            # Determine the target hostname or name
            $target = if ($comp.DNSHostName) { $comp.DNSHostName } else { $comp.Name }

            if ([string]::IsNullOrEmpty($target)) {
                Write-Host "Invalid target for $($comp.Name): No resolvable DNSHostName or Name."
                continue
            }

            foreach ($port in $Ports) {
                $tcp = New-Object System.Net.Sockets.TcpClient
                try {
                    $asyncResult = $tcp.BeginConnect($target, $port, $null, $null)
                    $wait = $asyncResult.AsyncWaitHandle.WaitOne($Timeout)
                    
                    if ($wait -and $tcp.Connected) {
                        $tcp.EndConnect($asyncResult)
                        $results += [PSCustomObject]@{
                            Computer = $target
                            Port     = $port
                            Status   = "Open"
                        }
                    }
                    else {
                        $results += [PSCustomObject]@{
                            Computer = $target
                            Port     = $port
                            Status   = "Closed/Filtered"
                        }
                    }
                }
                catch {
                    $results += [PSCustomObject]@{
                        Computer = $target
                        Port     = $port
                        Status   = "Error: $($_.Exception.Message)"
                    }
                }
                finally {
                    $tcp.Close()
                }
            }
        }

        # Store results in the domain report
        if (-not $this.PSObject.Properties.Name.Contains('NetworkPortScanResults')) {
            Add-Member -InputObject $this -MemberType NoteProperty -Name 'NetworkPortScanResults' -Value $results
        }
        else {
            $this.NetworkPortScanResults = $results
        }

        return $this.NetworkPortScanResults
    }

    # Define a scriptblock for scanning ports on a single target
    $scanTargetPorts = {
        param(
            [Parameter(Mandatory = $true)]
            $ADComputer,
            [Parameter(Mandatory = $true)]
            [int[]]$Ports
        )

        # Check if the host is alive before scanning
        if (-not $ADComputer.IsAlive) {
            Write-Host "Skipping $($ADComputer.Name) because IsAlive=$($ADComputer.IsAlive)"
            return $null
        }

        # Determine the target hostname or name
        $target = if ($ADComputer.DNSHostName) { $ADComputer.DNSHostName } else { $ADComputer.Name }

        if ([string]::IsNullOrEmpty($target)) {
            Write-Host "Invalid target. The specified ADComputer has no resolvable DNSHostName or Name."
            return $null
        }

        $results = @()

        foreach ($port in $Ports) {
            $tcp = New-Object System.Net.Sockets.TcpClient
            try {
                # Attempt connection with a 1 second timeout
                $asyncResult = $tcp.BeginConnect($target, $port, $null, $null)
                $wait = $asyncResult.AsyncWaitHandle.WaitOne(1000)

                if ($wait -and $tcp.Connected) {
                    $tcp.EndConnect($asyncResult)
                    $results += [PSCustomObject]@{
                        Computer = $target
                        Port     = $port
                        Status   = "Open"
                    }
                }
                else {
                    $results += [PSCustomObject]@{
                        Computer = $target
                        Port     = $port
                        Status   = "Closed/Filtered"
                    }
                }
            }
            catch {
                $results += [PSCustomObject]@{
                    Computer = $target
                    Port     = $port
                    Status   = "Error: $($_.Exception.Message)"
                }
            }
            finally {
                $tcp.Close()
            }
        }

        return $results
    }

    # Add method to find suspicious SPNs
    $findSuspiciousSPNs = {
        $spnResults = Find-SuspiciousSPNs -Computers $this.DomainObjects.Computers -Users $this.DomainObjects.Users
        
        if (-not $this.SecuritySettings.PSObject.Properties.Name.Contains('SuspiciousSPNs')) {
            Add-Member -InputObject $this.SecuritySettings -MemberType NoteProperty -Name 'SuspiciousSPNs' -Value $spnResults
        }
        else {
            $this.SecuritySettings.SuspiciousSPNs = $spnResults
        }
        
        return $spnResults
    }

    $displaySuspiciousSPNs = {
        if (-not $this.SecuritySettings.PSObject.Properties.Name.Contains('SuspiciousSPNs')) {
            Write-Log "No suspicious SPNs found. Running FindSuspiciousSPNs..." -Level Info
            $this.FindSuspiciousSPNs()
        }
    
        if ($this.SecuritySettings.SuspiciousSPNs) {
            Write-Log "`nSuspicious SPNs Found:" -Level Warning
            $this.SecuritySettings.SuspiciousSPNs | ForEach-Object {
                Write-Log "`nObject: $($_.ObjectName) ($($_.ObjectType))" -Level Warning
                Write-Log "`nRisk Level: $($_.RiskLevel)" -Level $(if ($_.RiskLevel -eq 'High') { 'Error' } else { 'Warning' })
                $_.SuspiciousSPNs.GetEnumerator() | ForEach-Object {
                    Write-Log " SPN: $($_.Key)" -Level Warning
                    Write-Log " Reason: $($_.Value)" -Level Warning
                }
            }
        }
        else {
            Write-Log "`nNo suspicious SPNs found." -Level Info
        }
    }

    Add-Member -InputObject $DomainReport -MemberType ScriptMethod -Name "ScanCommonPorts" -Value $scanPorts -Force
    Add-Member -InputObject $DomainReport -MemberType ScriptMethod -Name "ScanTargetPorts" -Value $scanTargetPorts -Force
    Add-Member -InputObject $DomainReport -MemberType ScriptMethod -Name "FindSuspiciousSPNs" -Value $findSuspiciousSPNs
    Add-Member -InputObject $DomainReport -MemberType ScriptMethod -Name "DisplaySuspiciousSPNs" -Value $displaySuspiciousSPNs
}