Private/Get-ChangeSnapshot.ps1

function Get-ChangeSnapshot {
    <#
    .SYNOPSIS
        Takes a point-in-time snapshot of the current state for future baseline comparison.
 
    .DESCRIPTION
        Captures AD object summary (counts by type, key group memberships), GPO list with
        version numbers and modification times, DNS records from AD-integrated zones, and
        server service configurations. Saves the snapshot as a timestamped JSON file.
 
    .PARAMETER OutputPath
        Full file path for the snapshot JSON file.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$OutputPath
    )

    $Snapshot = [ordered]@{
        CaptureTime     = (Get-Date).ToString('o')
        CapturedBy      = "$env:USERDOMAIN\$env:USERNAME"
        ComputerName    = $env:COMPUTERNAME
        ActiveDirectory = $null
        GroupPolicy      = $null
        DNS              = $null
        ServerServices   = $null
    }

    # ================================================================
    # Active Directory Snapshot
    # ================================================================
    Write-Verbose 'Capturing AD snapshot...'
    try {
        $ADData = [ordered]@{
            UserCount     = 0
            GroupCount    = 0
            ComputerCount = 0
            OUCount       = 0
            SensitiveGroupMembers = @{}
        }

        try { $ADData.UserCount     = @(Get-ADUser -Filter * -ErrorAction Stop).Count }     catch { Write-Verbose "Could not count users: $_" }
        try { $ADData.GroupCount    = @(Get-ADGroup -Filter * -ErrorAction Stop).Count }    catch { Write-Verbose "Could not count groups: $_" }
        try { $ADData.ComputerCount = @(Get-ADComputer -Filter * -ErrorAction Stop).Count } catch { Write-Verbose "Could not count computers: $_" }
        try { $ADData.OUCount       = @(Get-ADOrganizationalUnit -Filter * -ErrorAction Stop).Count } catch { Write-Verbose "Could not count OUs: $_" }

        # Capture membership of sensitive groups
        $SensitiveGroups = @('Domain Admins', 'Enterprise Admins', 'Schema Admins', 'Administrators')
        foreach ($GroupName in $SensitiveGroups) {
            try {
                $Members = Get-ADGroupMember -Identity $GroupName -ErrorAction Stop |
                    Select-Object -ExpandProperty SamAccountName
                $ADData.SensitiveGroupMembers[$GroupName] = @($Members)
            }
            catch {
                Write-Verbose "Could not get members of '${GroupName}': $_"
            }
        }

        $Snapshot.ActiveDirectory = $ADData
    }
    catch {
        Write-Warning "AD snapshot failed: $_"
    }

    # ================================================================
    # Group Policy Snapshot
    # ================================================================
    Write-Verbose 'Capturing GPO snapshot...'
    try {
        $GPOs = Get-GPO -All -ErrorAction Stop | ForEach-Object {
            [ordered]@{
                Id               = $_.Id.ToString()
                Name             = $_.DisplayName
                Status           = $_.GpoStatus.ToString()
                CreationTime     = $_.CreationTime.ToString('o')
                ModificationTime = $_.ModificationTime.ToString('o')
                UserVersion      = $_.User.DSVersion
                ComputerVersion  = $_.Computer.DSVersion
                Owner            = $_.Owner
            }
        }
        $Snapshot.GroupPolicy = @($GPOs)
    }
    catch {
        Write-Warning "GPO snapshot failed: $_"
    }

    # ================================================================
    # DNS Snapshot
    # ================================================================
    Write-Verbose 'Capturing DNS snapshot...'
    try {
        $DnsZones = Get-DnsServerZone -ErrorAction Stop |
            Where-Object { $_.ZoneType -ne 'Forwarder' -and $_.IsAutoCreated -eq $false }

        $DnsData = [System.Collections.Generic.List[object]]::new()
        foreach ($Zone in $DnsZones) {
            try {
                $Records = Get-DnsServerResourceRecord -ZoneName $Zone.ZoneName -ErrorAction Stop |
                    Where-Object { $_.RecordType -in @('A', 'AAAA', 'CNAME', 'MX', 'SRV', 'PTR', 'TXT') } |
                    ForEach-Object {
                        $RData = switch ($_.RecordType) {
                            'A'     { $_.RecordData.IPv4Address.ToString() }
                            'AAAA'  { $_.RecordData.IPv6Address.ToString() }
                            'CNAME' { $_.RecordData.HostNameAlias }
                            'MX'    { "$($_.RecordData.Preference) $($_.RecordData.MailExchange)" }
                            'SRV'   { "$($_.RecordData.Priority) $($_.RecordData.Weight) $($_.RecordData.Port) $($_.RecordData.DomainName)" }
                            'PTR'   { $_.RecordData.PtrDomainName }
                            'TXT'   { ($_.RecordData.DescriptiveText -join '; ') }
                            default { $_.RecordData.ToString() }
                        }

                        [ordered]@{
                            ZoneName   = $Zone.ZoneName
                            HostName   = $_.HostName
                            RecordType = $_.RecordType
                            Data       = $RData
                            TTL        = $_.TimeToLive.TotalSeconds
                        }
                    }
                foreach ($Rec in $Records) { $DnsData.Add($Rec) }
            }
            catch {
                Write-Verbose "Could not snapshot records in zone '$($Zone.ZoneName)': $_"
            }
        }
        $Snapshot.DNS = @($DnsData)
    }
    catch {
        Write-Warning "DNS snapshot failed: $_"
    }

    # ================================================================
    # Server Services Snapshot (local machine only for baseline)
    # ================================================================
    Write-Verbose 'Capturing local server services snapshot...'
    try {
        $Services = Get-CimInstance -ClassName Win32_Service -ErrorAction Stop |
            Select-Object Name, DisplayName, StartMode, State, PathName, StartName |
            ForEach-Object {
                [ordered]@{
                    Name        = $_.Name
                    DisplayName = $_.DisplayName
                    StartMode   = $_.StartMode
                    State       = $_.State
                    PathName    = $_.PathName
                    StartName   = $_.StartName
                }
            }
        $Snapshot.ServerServices = @($Services)
    }
    catch {
        Write-Warning "Server services snapshot failed: $_"
    }

    # ================================================================
    # Write snapshot to file
    # ================================================================
    $SnapshotDir = Split-Path -Path $OutputPath -Parent
    if ($SnapshotDir -and -not (Test-Path -Path $SnapshotDir)) {
        New-Item -ItemType Directory -Path $SnapshotDir -Force | Out-Null
    }

    $JsonContent = $Snapshot | ConvertTo-Json -Depth 10
    [System.IO.File]::WriteAllText(
        (Resolve-Path -Path $OutputPath -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path) ??
        [System.IO.Path]::GetFullPath($OutputPath),
        $JsonContent,
        [System.Text.UTF8Encoding]::new($true)
    )

    Write-Verbose "Snapshot saved to $OutputPath"
    return $OutputPath
}