```powershell
<#
.SYNOPSIS
    Triage suspected Active Directory DCSync activity using exported event logs.

.DESCRIPTION
    This script scans one or more Windows event log files exported in XML, EVTX, or plain text formats
    and looks for replication-related signals that may indicate DCSync-style abuse.

    It is designed as a defensive triage aid. It does not make any changes to Active Directory,
    does not query remote systems by default, and does not attempt any destructive action.

    The script helps you answer:
      1. Which events look replication-related?
      2. Which account appears to have made the request?
      3. What source host or IP is associated with the activity?
      4. Is the source on an allowlist of approved domain controllers or sync systems?
      5. Is the activity worth alerting on or escalating for review?

    Recommended use:
      - Export relevant security and directory service logs from domain controllers.
      - Review output for unexpected source hosts, accounts, or access patterns.
      - Use the allowlist parameters to suppress expected replication sources.

.PARAMETER Path
    One or more file paths to exported event log files.
    Supported inputs: .xml, .txt, .log, and .evtx (best-effort parsing for exported text/XML).

.PARAMETER ApprovedSource
    Optional list of approved source hostnames or IP addresses.
    Used to reduce noise from known domain controllers, sync servers, or management hosts.

.PARAMETER ApprovedAccount
    Optional list of approved accounts or security principals.
    Use for known synchronization accounts or tightly controlled replication identities.

.PARAMETER IncludeOnlyLikelyReplication
    When set, the script returns only entries that match likely replication-related indicators.
    Default behavior is to return all parsed entries with a risk classification.

.PARAMETER ExportCsv
    Optional path to export the results as CSV for later review.

.EXAMPLE
    .\Invoke-DcsyncTriage.ps1 -Path .\dc-security.xml -ApprovedSource @('DC01','DC02','SYNC01')

.EXAMPLE
    .\Invoke-DcsyncTriage.ps1 -Path .\logs\*.txt -ExportCsv .\dcsync-triage.csv

.NOTES
    This script is intentionally conservative. It flags suspicious patterns for review rather than
    claiming a definitive DCSync determination from a single event alone.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
    [ValidateNotNullOrEmpty()]
    [string[]]$Path,

    [Parameter()]
    [string[]]$ApprovedSource = @(),

    [Parameter()]
    [string[]]$ApprovedAccount = @(),

    [Parameter()]
    [switch]$IncludeOnlyLikelyReplication,

    [Parameter()]
    [ValidateScript({
        if ([string]::IsNullOrWhiteSpace($_)) { return $true }
        $parent = Split-Path -Path $_ -Parent
        if ($parent -and -not (Test-Path -Path $parent)) {
            throw "ExportCsv parent path does not exist: $parent"
        }
        $true
    })]
    [string]$ExportCsv
)

begin {
    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    function Get-TextFromFile {
        param([Parameter(Mandatory)] [string]$LiteralPath)

        $ext = [System.IO.Path]::GetExtension($LiteralPath).ToLowerInvariant()
        switch ($ext) {
            '.xml' { return Get-Content -LiteralPath $LiteralPath -Raw }
            '.txt' { return Get-Content -LiteralPath $LiteralPath -Raw }
            '.log' { return Get-Content -LiteralPath $LiteralPath -Raw }
            '.evtx' {
                # Best-effort support for EVTX files without external modules.
                # If you need deeper parsing, export the relevant events to XML first.
                return (wevtutil qe $LiteralPath /lf:true /f:RenderedXml 2>$null)
            }
            default { return Get-Content -LiteralPath $LiteralPath -Raw }
        }
    }

    function Get-EventBlocks {
        param([Parameter(Mandatory)] [string]$Text)

        if ($Text -match '<Event\b') {
            return [regex]::Matches($Text, '<Event\b.*?</Event>', [System.Text.RegularExpressions.RegexOptions]::Singleline) |
                ForEach-Object { $_.Value }
        }

        # Fallback: treat each line as a potential record when logs are plain text.
        return $Text -split "`r?`n" | Where-Object { $_.Trim() }
    }

    function Get-ValueFromXmlBlock {
        param(
            [Parameter(Mandatory)] [string]$Block,
            [Parameter(Mandatory)] [string[]]$Names
        )

        foreach ($name in $Names) {
            $pattern = "<Data\s+Name=['\"]$([regex]::Escape($name))['\"]>(.*?)</Data>"
            $m = [regex]::Match($Block, $pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
            if ($m.Success) {
                return ($m.Groups[1].Value -replace '&lt;','<' -replace '&gt;','>' -replace '&amp;','&').Trim()
            }
        }
        return $null
    }

    function Get-RiskLabel {
        param(
            [Parameter(Mandatory)] [bool]$IsLikelyReplication,
            [Parameter(Mandatory)] [bool]$SourceAllowed,
            [Parameter(Mandatory)] [bool]$AccountAllowed
        )

        if (-not $IsLikelyReplication) { return 'Informational' }
        if ($SourceAllowed -and $AccountAllowed) { return 'Low' }
        if ($SourceAllowed -xor $AccountAllowed) { return 'Medium' }
        return 'High'
    }

    $results = New-Object System.Collections.Generic.List[object]
}

process {
    foreach ($p in $Path) {
        if (-not (Test-Path -LiteralPath $p)) {
            Write-Warning "Path not found: $p"
            continue
        }

        $text = Get-TextFromFile -LiteralPath $p
        $blocks = Get-EventBlocks -Text $text

        foreach ($block in $blocks) {
            $isXml = $block.TrimStart().StartsWith('<Event')
            $eventId = $null
            $provider = $null
            $timeCreated = $null
            $account = $null
            $source = $null
            $target = $null
            $access = $null
            $message = $block

            if ($isXml) {
                $eventId = Get-ValueFromXmlBlock -Block $block -Names @('EventID')
                $provider = ([regex]::Match($block, '<Provider[^>]*Name=["'']([^"'']+)["'']', 'IgnoreCase')).Groups[1].Value
                $timeCreated = ([regex]::Match($block, '<TimeCreated[^>]*SystemTime=["'']([^"'']+)["'']', 'IgnoreCase')).Groups[1].Value
                $account = Get-ValueFromXmlBlock -Block $block -Names @('SubjectUserName','AccountName','CallerAccount','SubjectAccountName')
                $source = Get-ValueFromXmlBlock -Block $block -Names @('IpAddress','SourceAddress','ClientAddress','WorkstationName','SourceWorkstation')
                $target = Get-ValueFromXmlBlock -Block $block -Names @('TargetUserName','ObjectName','NamingContext','DestinationDRA','TargetDomainName')
                $access = Get-ValueFromXmlBlock -Block $block -Names @('AccessMask','Accesses','Operation','Properties')
            } else {
                if ($block -match 'Event\s*ID\s*[:=]\s*(\d+)') { $eventId = $Matches[1] }
                if ($block -match 'Account\s*[:=]\s*([^;|,]+)') { $account = $Matches[1].Trim() }
                if ($block -match 'Source(?:Host|Address|IP)\s*[:=]\s*([^;|,]+)') { $source = $Matches[1].Trim() }
                if ($block -match 'Target(?:Host|Object|NC|NamingContext)\s*[:=]\s*([^;|,]+)') { $target = $Matches[1].Trim() }
                if ($block -match 'Access(?:Mask|es)?\s*[:=]\s*([^;|,]+)') { $access = $Matches[1].Trim() }
            }

            $replicationHints = @(
                'Replicating Directory Changes',
                'Replicating Directory Changes All',
                'Replicating Directory Changes In Filtered Set',
                'Directory Replication',
                'DRS',
                'GetNCChanges'
            )

            $isLikelyReplication = $false
            foreach ($hint in $replicationHints) {
                if ($block -match [regex]::Escape($hint)) {
                    $isLikelyReplication = $true
                    break
                }
            }

            if ($eventId -in @('4662','5136','4928','4929','4930','4931')) {
                $isLikelyReplication = $true
            }

            $sourceAllowed = $false
            $accountAllowed = $false
            if ($source) {
                $sourceAllowed = $ApprovedSource -contains $source
            }
            if ($account) {
                $accountAllowed = $ApprovedAccount -contains $account
            }

            $risk = Get-RiskLabel -IsLikelyReplication $isLikelyReplication -SourceAllowed $sourceAllowed -AccountAllowed $accountAllowed

            if ($IncludeOnlyLikelyReplication -and -not $isLikelyReplication) {
                continue
            }

            $results.Add([pscustomobject]@{
                File                = $p
                TimeCreated         = $timeCreated
                EventId             = $eventId
                Provider            = $provider
                Account             = $account
                Source              = $source
                Target              = $target
                Access              = $access
                LikelyReplication   = $isLikelyReplication
                ApprovedSource      = $sourceAllowed
                ApprovedAccount     = $accountAllowed
                Risk                = $risk
            })
        }
    }
}

end {
    if ($results.Count -eq 0) {
        Write-Host 'No matching events were found.'
        return
    }

    $results |
        Sort-Object LikelyReplication -Descending, Risk, TimeCreated |
        Format-Table -AutoSize

    if ($ExportCsv) {
        $results | Export-Csv -NoTypeInformation -Path $ExportCsv
        Write-Host "Results exported to: $ExportCsv"
    }
}
```