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 } } |