Public/Invoke-ChangeAudit.ps1

function Invoke-ChangeAudit {
    <#
    .SYNOPSIS
        Main orchestrator - runs all change detection and generates a consolidated report.
 
    .DESCRIPTION
        Calls Get-ADChanges, Get-GPOChanges, and optionally Get-DNSChanges and
        Get-ServerConfigChanges. Generates an HTML dashboard report, optionally saves
        a JSON snapshot for future baseline comparisons, and returns a summary of
        total changes by category and severity.
 
    .PARAMETER HoursBack
        Number of hours to look back for changes. Default is 24. Range: 1-720.
 
    .PARAMETER OutputPath
        Directory path for report output. Default is .\Reports.
 
    .PARAMETER IncludeDNS
        Switch to include DNS change detection.
 
    .PARAMETER IncludeServerConfig
        Switch to include server configuration change detection.
 
    .PARAMETER ComputerName
        Server names for server configuration checks (used with -IncludeServerConfig).
 
    .PARAMETER BaselinePath
        Optional path to a previous snapshot JSON file for baseline comparison mode.
 
    .EXAMPLE
        Invoke-ChangeAudit -HoursBack 48
        Runs AD and GPO change detection for the last 48 hours.
 
    .EXAMPLE
        Invoke-ChangeAudit -HoursBack 24 -IncludeDNS -IncludeServerConfig -ComputerName 'WEB01','SQL02'
        Runs full change detection including DNS and server config checks.
 
    .EXAMPLE
        Invoke-ChangeAudit -BaselinePath .\Snapshots\baseline-2026-02-15.json
        Compares current state against a saved baseline snapshot.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateRange(1, 720)]
        [int]$HoursBack = 24,

        [Parameter()]
        [string]$OutputPath = '.\Reports',

        [Parameter()]
        [switch]$IncludeDNS,

        [Parameter()]
        [switch]$IncludeServerConfig,

        [Parameter()]
        [string[]]$ComputerName,

        [Parameter()]
        [string]$BaselinePath
    )

    begin {
        $StartTime = Get-Date
        $AllChanges = [System.Collections.Generic.List[PSCustomObject]]::new()

        # Create output directory if it does not exist
        if (-not (Test-Path -Path $OutputPath)) {
            New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
            Write-Verbose "Created output directory: $OutputPath"
        }

        # Load baseline snapshot if provided
        $Baseline = $null
        if ($BaselinePath) {
            if (Test-Path -Path $BaselinePath) {
                Write-Verbose "Loading baseline snapshot from $BaselinePath..."
                try {
                    $Baseline = Get-Content -Path $BaselinePath -Raw -Encoding UTF8 | ConvertFrom-Json
                    Write-Verbose "Baseline loaded: captured at $($Baseline.CaptureTime)"
                }
                catch {
                    Write-Warning "Failed to load baseline snapshot: $_. Continuing without baseline."
                }
            }
            else {
                Write-Warning "Baseline file not found at '$BaselinePath'. Continuing without baseline."
            }
        }
    }

    process {
        # ================================================================
        # Phase 1: Active Directory Changes
        # ================================================================
        Write-Verbose '=== Phase 1: Active Directory Changes ==='
        try {
            $ADChanges = Get-ADChanges -HoursBack $HoursBack -Verbose:($VerbosePreference -eq 'Continue')
            if ($ADChanges) {
                foreach ($Change in $ADChanges) {
                    $AllChanges.Add($Change)
                }
                Write-Verbose " Found $(@($ADChanges).Count) AD changes."
            }
            else {
                Write-Verbose " No AD changes detected."
            }
        }
        catch {
            Write-Warning "AD change detection failed: $_"
        }

        # ================================================================
        # Phase 2: Group Policy Changes
        # ================================================================
        Write-Verbose '=== Phase 2: Group Policy Changes ==='
        try {
            $GPOChanges = Get-GPOChanges -HoursBack $HoursBack -IncludeLinkChanges -Verbose:($VerbosePreference -eq 'Continue')
            if ($GPOChanges) {
                foreach ($Change in $GPOChanges) {
                    $AllChanges.Add($Change)
                }
                Write-Verbose " Found $(@($GPOChanges).Count) GPO changes."
            }
            else {
                Write-Verbose " No GPO changes detected."
            }
        }
        catch {
            Write-Warning "GPO change detection failed: $_"
        }

        # ================================================================
        # Phase 3: DNS Changes (optional)
        # ================================================================
        if ($IncludeDNS) {
            Write-Verbose '=== Phase 3: DNS Changes ==='
            try {
                $DNSChanges = Get-DNSChanges -HoursBack $HoursBack -Verbose:($VerbosePreference -eq 'Continue')
                if ($DNSChanges) {
                    foreach ($Change in $DNSChanges) {
                        $AllChanges.Add($Change)
                    }
                    Write-Verbose " Found $(@($DNSChanges).Count) DNS changes."
                }
                else {
                    Write-Verbose " No DNS changes detected."
                }
            }
            catch {
                Write-Warning "DNS change detection failed: $_"
            }
        }

        # ================================================================
        # Phase 4: Server Configuration Changes (optional)
        # ================================================================
        if ($IncludeServerConfig) {
            Write-Verbose '=== Phase 4: Server Configuration Changes ==='
            if (-not $ComputerName -or $ComputerName.Count -eq 0) {
                Write-Warning "IncludeServerConfig specified but no ComputerName provided. Skipping server config checks."
            }
            else {
                try {
                    $ServerChanges = Get-ServerConfigChanges -ComputerName $ComputerName -HoursBack $HoursBack -Verbose:($VerbosePreference -eq 'Continue')
                    if ($ServerChanges) {
                        foreach ($Change in $ServerChanges) {
                            $AllChanges.Add($Change)
                        }
                        Write-Verbose " Found $(@($ServerChanges).Count) server config changes."
                    }
                    else {
                        Write-Verbose " No server config changes detected."
                    }
                }
                catch {
                    Write-Warning "Server config change detection failed: $_"
                }
            }
        }

        # ================================================================
        # Phase 5: Baseline Comparison (if baseline provided)
        # ================================================================
        if ($Baseline) {
            Write-Verbose '=== Phase 5: Baseline Comparison ==='

            # Compare AD object counts
            if ($Baseline.ActiveDirectory) {
                try {
                    $CurrentUsers     = @(Get-ADUser -Filter * -ErrorAction Stop).Count
                    $CurrentGroups    = @(Get-ADGroup -Filter * -ErrorAction Stop).Count
                    $CurrentComputers = @(Get-ADComputer -Filter * -ErrorAction Stop).Count

                    if ($Baseline.ActiveDirectory.UserCount -and $CurrentUsers -ne $Baseline.ActiveDirectory.UserCount) {
                        $Diff = $CurrentUsers - $Baseline.ActiveDirectory.UserCount
                        $AllChanges.Add([PSCustomObject]@{
                            ChangeTime = Get-Date
                            ChangeType = if ($Diff -gt 0) { 'ObjectsAdded' } else { 'ObjectsRemoved' }
                            Category   = 'ActiveDirectory'
                            ObjectName = 'User Count'
                            ObjectType = 'BaselineComparison'
                            ChangedBy  = ''
                            OldValue   = $Baseline.ActiveDirectory.UserCount.ToString()
                            NewValue   = $CurrentUsers.ToString()
                            Detail     = "User count changed from $($Baseline.ActiveDirectory.UserCount) to $CurrentUsers (delta: $Diff) since baseline"
                            Source     = 'Baseline Comparison'
                            Severity   = if ([Math]::Abs($Diff) -gt 5) { 'High' } else { 'Medium' }
                        })
                    }

                    if ($Baseline.ActiveDirectory.GroupCount -and $CurrentGroups -ne $Baseline.ActiveDirectory.GroupCount) {
                        $Diff = $CurrentGroups - $Baseline.ActiveDirectory.GroupCount
                        $AllChanges.Add([PSCustomObject]@{
                            ChangeTime = Get-Date
                            ChangeType = if ($Diff -gt 0) { 'ObjectsAdded' } else { 'ObjectsRemoved' }
                            Category   = 'ActiveDirectory'
                            ObjectName = 'Group Count'
                            ObjectType = 'BaselineComparison'
                            ChangedBy  = ''
                            OldValue   = $Baseline.ActiveDirectory.GroupCount.ToString()
                            NewValue   = $CurrentGroups.ToString()
                            Detail     = "Group count changed from $($Baseline.ActiveDirectory.GroupCount) to $CurrentGroups (delta: $Diff) since baseline"
                            Source     = 'Baseline Comparison'
                            Severity   = if ([Math]::Abs($Diff) -gt 3) { 'High' } else { 'Medium' }
                        })
                    }

                    if ($Baseline.ActiveDirectory.ComputerCount -and $CurrentComputers -ne $Baseline.ActiveDirectory.ComputerCount) {
                        $Diff = $CurrentComputers - $Baseline.ActiveDirectory.ComputerCount
                        $AllChanges.Add([PSCustomObject]@{
                            ChangeTime = Get-Date
                            ChangeType = if ($Diff -gt 0) { 'ObjectsAdded' } else { 'ObjectsRemoved' }
                            Category   = 'ActiveDirectory'
                            ObjectName = 'Computer Count'
                            ObjectType = 'BaselineComparison'
                            ChangedBy  = ''
                            OldValue   = $Baseline.ActiveDirectory.ComputerCount.ToString()
                            NewValue   = $CurrentComputers.ToString()
                            Detail     = "Computer count changed from $($Baseline.ActiveDirectory.ComputerCount) to $CurrentComputers (delta: $Diff) since baseline"
                            Source     = 'Baseline Comparison'
                            Severity   = 'Medium'
                        })
                    }
                }
                catch {
                    Write-Warning "AD baseline comparison failed: $_"
                }
            }

            # Compare GPO versions
            if ($Baseline.GroupPolicy) {
                try {
                    $CurrentGPOs = Get-GPO -All -ErrorAction Stop
                    foreach ($BaselineGPO in $Baseline.GroupPolicy) {
                        $CurrentGPO = $CurrentGPOs | Where-Object { $_.Id.ToString() -eq $BaselineGPO.Id }
                        if (-not $CurrentGPO) {
                            $AllChanges.Add([PSCustomObject]@{
                                ChangeTime = Get-Date
                                ChangeType = 'Deleted'
                                Category   = 'GroupPolicy'
                                ObjectName = $BaselineGPO.Name
                                ObjectType = 'BaselineComparison'
                                ChangedBy  = ''
                                OldValue   = "GPO existed in baseline"
                                NewValue   = 'GPO no longer exists'
                                Detail     = "GPO '$($BaselineGPO.Name)' was deleted since baseline (captured $($Baseline.CaptureTime))"
                                Source     = 'Baseline Comparison'
                                Severity   = 'High'
                            })
                        }
                        elseif ($CurrentGPO.ModificationTime -gt [datetime]$BaselineGPO.ModificationTime) {
                            $AllChanges.Add([PSCustomObject]@{
                                ChangeTime = $CurrentGPO.ModificationTime
                                ChangeType = 'Modified'
                                Category   = 'GroupPolicy'
                                ObjectName = $CurrentGPO.DisplayName
                                ObjectType = 'BaselineComparison'
                                ChangedBy  = ''
                                OldValue   = "Modified: $($BaselineGPO.ModificationTime)"
                                NewValue   = "Modified: $($CurrentGPO.ModificationTime)"
                                Detail     = "GPO '$($CurrentGPO.DisplayName)' modified since baseline (was: $($BaselineGPO.ModificationTime), now: $($CurrentGPO.ModificationTime))"
                                Source     = 'Baseline Comparison'
                                Severity   = 'Medium'
                            })
                        }
                    }

                    # Check for new GPOs not in baseline
                    $BaselineGpoIds = $Baseline.GroupPolicy | Select-Object -ExpandProperty Id
                    foreach ($GPO in $CurrentGPOs) {
                        if ($GPO.Id.ToString() -notin $BaselineGpoIds) {
                            $AllChanges.Add([PSCustomObject]@{
                                ChangeTime = $GPO.CreationTime
                                ChangeType = 'Created'
                                Category   = 'GroupPolicy'
                                ObjectName = $GPO.DisplayName
                                ObjectType = 'BaselineComparison'
                                ChangedBy  = ''
                                OldValue   = 'Did not exist in baseline'
                                NewValue   = "Created: $($GPO.CreationTime)"
                                Detail     = "New GPO '$($GPO.DisplayName)' created since baseline (captured $($Baseline.CaptureTime))"
                                Source     = 'Baseline Comparison'
                                Severity   = 'Medium'
                            })
                        }
                    }
                }
                catch {
                    Write-Warning "GPO baseline comparison failed: $_"
                }
            }
        }

        # ================================================================
        # Phase 6: Generate outputs
        # ================================================================
        Write-Verbose '=== Generating Reports ==='

        # Sort all changes chronologically (newest first)
        $SortedChanges = $AllChanges | Sort-Object ChangeTime -Descending

        # Generate timestamp for file names
        $Timestamp = (Get-Date -Format 'yyyy-MM-dd_HHmmss')

        # Generate HTML dashboard
        $HtmlPath = Join-Path -Path $OutputPath -ChildPath "ChangeReport_$Timestamp.html"
        try {
            New-HtmlDashboard -Changes $SortedChanges -OutputPath $HtmlPath -HoursBack $HoursBack -AuditStartTime $StartTime
            Write-Verbose "HTML report saved to: $HtmlPath"
        }
        catch {
            Write-Warning "Failed to generate HTML report: $_"
        }

        # Save a snapshot for future baseline comparison
        $SnapshotPath = Join-Path -Path $OutputPath -ChildPath "Snapshot_$Timestamp.json"
        try {
            Get-ChangeSnapshot -OutputPath $SnapshotPath
            Write-Verbose "Snapshot saved to: $SnapshotPath"
        }
        catch {
            Write-Warning "Failed to save snapshot: $_"
        }
    }

    end {
        $EndTime = Get-Date
        $Duration = $EndTime - $StartTime

        # Build summary
        $Summary = [PSCustomObject]@{
            AuditStarted    = $StartTime
            AuditCompleted  = $EndTime
            Duration        = $Duration.ToString('mm\:ss')
            HoursBack       = $HoursBack
            TotalChanges    = $SortedChanges.Count
            ByCategory      = [PSCustomObject]@{
                ActiveDirectory = @($SortedChanges | Where-Object Category -eq 'ActiveDirectory').Count
                GroupPolicy     = @($SortedChanges | Where-Object Category -eq 'GroupPolicy').Count
                DNS             = @($SortedChanges | Where-Object Category -eq 'DNS').Count
                ServerConfig    = @($SortedChanges | Where-Object Category -eq 'ServerConfig').Count
            }
            BySeverity      = [PSCustomObject]@{
                Critical = @($SortedChanges | Where-Object Severity -eq 'Critical').Count
                High     = @($SortedChanges | Where-Object Severity -eq 'High').Count
                Medium   = @($SortedChanges | Where-Object Severity -eq 'Medium').Count
                Low      = @($SortedChanges | Where-Object Severity -eq 'Low').Count
            }
            ReportPath      = $HtmlPath
            SnapshotPath    = $SnapshotPath
            Changes         = $SortedChanges
        }

        Write-Host "`n===== CHANGE AUDIT SUMMARY =====" -ForegroundColor Cyan
        Write-Host "Duration: $($Summary.Duration)"
        Write-Host "Period: Last $HoursBack hours"
        Write-Host "Total Changes: $($Summary.TotalChanges)" -ForegroundColor $(if ($Summary.TotalChanges -gt 0) { 'Yellow' } else { 'Green' })
        Write-Host ''
        Write-Host "By Category:" -ForegroundColor White
        Write-Host " Active Directory: $($Summary.ByCategory.ActiveDirectory)"
        Write-Host " Group Policy: $($Summary.ByCategory.GroupPolicy)"
        Write-Host " DNS: $($Summary.ByCategory.DNS)"
        Write-Host " Server Config: $($Summary.ByCategory.ServerConfig)"
        Write-Host ''
        Write-Host "By Severity:" -ForegroundColor White
        if ($Summary.BySeverity.Critical -gt 0) { Write-Host " Critical: $($Summary.BySeverity.Critical)" -ForegroundColor Red }
        if ($Summary.BySeverity.High -gt 0)     { Write-Host " High: $($Summary.BySeverity.High)" -ForegroundColor DarkYellow }
        if ($Summary.BySeverity.Medium -gt 0)   { Write-Host " Medium: $($Summary.BySeverity.Medium)" -ForegroundColor Yellow }
        if ($Summary.BySeverity.Low -gt 0)      { Write-Host " Low: $($Summary.BySeverity.Low)" -ForegroundColor Gray }
        Write-Host ''
        Write-Host "Report: $HtmlPath" -ForegroundColor Green
        Write-Host "Snapshot: $SnapshotPath" -ForegroundColor Green
        Write-Host "================================`n" -ForegroundColor Cyan

        return $Summary
    }
}