Private/AD/Checks/Invoke-ADTrustChecks.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Invoke-ADTrustChecks { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$AuditData ) $checkDefs = Get-AuditCategoryDefinitions -Category 'ADTrustChecks' $findings = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($check in $checkDefs.checks) { $funcName = "Test-Recon$($check.id -replace '-', '')" if (Get-Command $funcName -ErrorAction SilentlyContinue) { try { $finding = & $funcName -AuditData $AuditData -CheckDefinition $check if ($finding) { $findings.Add($finding) } } catch { $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'ERROR' ` -CurrentValue "Check failed: $_")) } } else { $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'SKIP' ` -CurrentValue 'Check not yet implemented')) } } return @($findings) } # ── ADTRUST-001: Trust Relationships Enumeration ───────────────────────── function Test-ReconADTRUST001 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts) if ($trusts.Count -eq 0 -or ($trusts.Count -eq 1 -and $null -eq $trusts[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } # Filter out null entries $trusts = @($trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } $inbound = @($trusts | Where-Object { $_.TrustDirection -eq 'Inbound' }).Count $outbound = @($trusts | Where-Object { $_.TrustDirection -eq 'Outbound' }).Count $bidir = @($trusts | Where-Object { $_.TrustDirection -eq 'Bidirectional' }).Count $forestTr = @($trusts | Where-Object { $_.ForestTransitive }).Count $trustSummary = @($trusts | ForEach-Object { @{ TrustPartner = $_.TrustPartner Direction = $_.TrustDirection Type = $_.TrustType IsTransitive = $_.IsTransitive ForestTrust = $_.ForestTransitive WithinForest = $_.WithinForest SIDFiltering = $_.SIDFilteringEnabled SelectiveAuth = $_.SelectiveAuthentication IsAzureAD = $_.IsAzureAD WhenCreated = $_.WhenCreated WhenChanged = $_.WhenChanged } }) $currentValue = "$($trusts.Count) trust relationship(s) found: $bidir bidirectional, $inbound inbound, $outbound outbound" if ($forestTr -gt 0) { $currentValue += ", $forestTr forest transitive" } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ TrustCount = $trusts.Count Inbound = $inbound Outbound = $outbound Bidirectional = $bidir ForestTrusts = $forestTr TrustSummary = $trustSummary } } # ── ADTRUST-002: Trust Direction Analysis ───────────────────────────────── function Test-ReconADTRUST002 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } # Inbound and bidirectional trusts allow external users to authenticate into this domain $inboundTrusts = @($trusts | Where-Object { $_.TrustDirection -eq 'Inbound' -or $_.TrustDirection -eq 'Bidirectional' }) if ($inboundTrusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "All $($trusts.Count) trust(s) are outbound only - no inbound authentication paths" ` -Details @{ TotalTrusts = $trusts.Count InboundTrustCount = 0 } } $inboundDetails = @($inboundTrusts | ForEach-Object { @{ TrustPartner = $_.TrustPartner Direction = $_.TrustDirection Type = $_.TrustType WithinForest = $_.WithinForest IsTransitive = $_.IsTransitive } }) $partnerNames = @($inboundTrusts | ForEach-Object { "$($_.TrustPartner) ($($_.TrustDirection))" }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue "$($inboundTrusts.Count) inbound/bidirectional trust(s) allow external authentication: $($partnerNames -join '; ')" ` -Details @{ InboundTrustCount = $inboundTrusts.Count TotalTrusts = $trusts.Count InboundTrustDetails = $inboundDetails } } # ── ADTRUST-003: Trust Transitivity Analysis ───────────────────────────── function Test-ReconADTRUST003 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } $transitiveTrusts = @($trusts | Where-Object { $_.IsTransitive -eq $true }) if ($transitiveTrusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "All $($trusts.Count) trust(s) are non-transitive" ` -Details @{ TotalTrusts = $trusts.Count; TransitiveCount = 0 } } $transitiveDetails = @($transitiveTrusts | ForEach-Object { @{ TrustPartner = $_.TrustPartner Direction = $_.TrustDirection Type = $_.TrustType ForestTransitive = $_.ForestTransitive WithinForest = $_.WithinForest } }) $partnerNames = @($transitiveTrusts | ForEach-Object { "$($_.TrustPartner) ($($_.TrustDirection))" }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue "$($transitiveTrusts.Count) transitive trust(s) extend authentication paths beyond direct partners: $($partnerNames -join '; ')" ` -Details @{ TransitiveCount = $transitiveTrusts.Count TotalTrusts = $trusts.Count TransitiveTrustDetails = $transitiveDetails } } # ── ADTRUST-004: SID Filtering Status ──────────────────────────────────── function Test-ReconADTRUST004 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } # SID filtering should be enabled on all trusts except intra-forest trusts # (within-forest trusts inherently share the same security boundary) $externalTrusts = @($trusts | Where-Object { $_.WithinForest -ne $true }) if ($externalTrusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'All trusts are intra-forest. SID filtering enforcement not applicable for within-forest trusts' ` -Details @{ IntraForestOnly = $true; TotalTrusts = $trusts.Count } } $unfiltered = @($externalTrusts | Where-Object { $_.SIDFilteringEnabled -eq $false }) if ($unfiltered.Count -gt 0) { $unfilteredDetails = @($unfiltered | ForEach-Object { @{ TrustPartner = $_.TrustPartner Direction = $_.TrustDirection Type = $_.TrustType SIDFiltering = $_.SIDFilteringEnabled ForestTrust = $_.ForestTransitive TrustAttributes = $_.TrustAttributes } }) $partnerNames = @($unfiltered | ForEach-Object { $_.TrustPartner }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue "$($unfiltered.Count) external trust(s) lack SID filtering (quarantine): $($partnerNames -join ', '). Attackers in trusted domains can inject privileged SIDs" ` -Details @{ UnfilteredCount = $unfiltered.Count TotalExternalTrusts = $externalTrusts.Count UnfilteredTrusts = $unfilteredDetails } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "SID filtering enabled on all $($externalTrusts.Count) external trust(s)" ` -Details @{ TotalExternalTrusts = $externalTrusts.Count } } # ── ADTRUST-005: SID History Abuse Detection ───────────────────────────── function Test-ReconADTRUST005 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } # Check external trusts where SID filtering is disabled, allowing SID history injection $externalTrusts = @($trusts | Where-Object { $_.WithinForest -ne $true }) if ($externalTrusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'All trusts are intra-forest. SID history abuse via external trusts not applicable' ` -Details @{ IntraForestOnly = $true } } $sidHistoryTrusts = @($externalTrusts | Where-Object { $_.SIDFilteringEnabled -eq $false }) if ($sidHistoryTrusts.Count -gt 0) { $affectedDetails = @($sidHistoryTrusts | ForEach-Object { @{ TrustPartner = $_.TrustPartner Direction = $_.TrustDirection SIDFiltering = $_.SIDFilteringEnabled SIDHistory = $_.SIDHistoryEnabled } }) $partnerNames = @($sidHistoryTrusts | ForEach-Object { $_.TrustPartner }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue "$($sidHistoryTrusts.Count) external trust(s) have SID filtering disabled, allowing SID history injection: $($partnerNames -join ', ')" ` -Details @{ AffectedCount = $sidHistoryTrusts.Count TotalExternalTrusts = $externalTrusts.Count AffectedTrusts = $affectedDetails } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "SID history injection is blocked on all $($externalTrusts.Count) external trust(s) (SID filtering is active)" ` -Details @{ TotalExternalTrusts = $externalTrusts.Count } } # ── ADTRUST-006: Selective Authentication Status ───────────────────────── function Test-ReconADTRUST006 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } # Selective authentication is not applicable to intra-forest trusts $externalTrusts = @($trusts | Where-Object { $_.WithinForest -ne $true }) if ($externalTrusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'All trusts are intra-forest. Selective authentication is not applicable' ` -Details @{ IntraForestOnly = $true; TotalTrusts = $trusts.Count } } $noSelectiveAuth = @($externalTrusts | Where-Object { $_.SelectiveAuthentication -ne $true }) if ($noSelectiveAuth.Count -gt 0) { $affectedDetails = @($noSelectiveAuth | ForEach-Object { @{ TrustPartner = $_.TrustPartner Direction = $_.TrustDirection Type = $_.TrustType SelectiveAuthentication = $_.SelectiveAuthentication ForestTrust = $_.ForestTransitive } }) $partnerNames = @($noSelectiveAuth | ForEach-Object { $_.TrustPartner }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue "$($noSelectiveAuth.Count) external trust(s) lack selective authentication: $($partnerNames -join ', '). All authenticated users from trusted domains can access any permitted resource" ` -Details @{ MissingSelectiveAuth = $noSelectiveAuth.Count TotalExternalTrusts = $externalTrusts.Count AffectedTrusts = $affectedDetails } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "Selective authentication enabled on all $($externalTrusts.Count) external trust(s)" ` -Details @{ TotalExternalTrusts = $externalTrusts.Count } } # ── ADTRUST-007: Azure AD Hybrid Trust Security ───────────────────────── function Test-ReconADTRUST007 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } $azureTrusts = @($trusts | Where-Object { $_.IsAzureAD -eq $true }) if ($azureTrusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No Azure AD trust relationships detected' ` -Details @{ AzureTrustCount = 0 TotalTrusts = $trusts.Count Note = 'Azure AD Connect may still be present without creating a trust object. Check for AZUREADSSOACC computer account if hybrid identity is expected.' } } $azureDetails = @($azureTrusts | ForEach-Object { @{ TrustPartner = $_.TrustPartner FlatName = $_.FlatName Direction = $_.TrustDirection Type = $_.TrustType SelectiveAuthentication = $_.SelectiveAuthentication SIDFiltering = $_.SIDFilteringEnabled WhenCreated = $_.WhenCreated WhenChanged = $_.WhenChanged } }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue "$($azureTrusts.Count) Azure AD trust(s) detected. Verify Azure AD Connect is on a hardened Tier 0 server, PHS is preferred over PTA/federation, and cloud-only break-glass accounts are configured" ` -Details @{ AzureTrustCount = $azureTrusts.Count AzureTrustDetails = $azureDetails TotalTrusts = $trusts.Count } } # ── ADTRUST-008: Foreign Domain Trust Enumeration ──────────────────────── function Test-ReconADTRUST008 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } # External trusts are those outside the forest (not within-forest, not forest-transitive) # Also flag MIT/realm trusts and TreatAsExternal trusts $foreignTrusts = @($trusts | Where-Object { (-not $_.WithinForest -and -not $_.ForestTransitive) -or $_.TreatAsExternal -eq $true -or $_.TrustType -eq 'MIT' }) if ($foreignTrusts.Count -gt 0) { $trustDetails = @($foreignTrusts | ForEach-Object { @{ TrustPartner = $_.TrustPartner Direction = $_.TrustDirection TrustType = $_.TrustType IsTransitive = $_.IsTransitive SIDFiltering = $_.SIDFilteringEnabled SelectiveAuthentication = $_.SelectiveAuthentication TreatAsExternal = $_.TreatAsExternal WhenCreated = $_.WhenCreated } }) $partnerNames = @($foreignTrusts | ForEach-Object { "$($_.TrustPartner) ($($_.TrustDirection), $($_.TrustType))" }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue "$($foreignTrusts.Count) foreign/external domain trust(s) detected: $($partnerNames -join '; '). Verify security agreements and review annually" ` -Details @{ ForeignTrustCount = $foreignTrusts.Count ForeignTrusts = $trustDetails TotalTrusts = $trusts.Count } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No foreign/external domain trusts detected among $($trusts.Count) trust(s)" ` -Details @{ TotalTrusts = $trusts.Count } } # ── ADTRUST-009: Orphaned Trust Detection ──────────────────────────────── function Test-ReconADTRUST009 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } $now = [datetime]::UtcNow $staleThresholdDays = 365 $orphanedTrusts = [System.Collections.Generic.List[hashtable]]::new() foreach ($trust in $trusts) { $isOrphaned = $false $reason = '' # Check if the trust partner name is empty or missing if ([string]::IsNullOrWhiteSpace($trust.TrustPartner)) { $isOrphaned = $true $reason = 'Trust partner name is empty or missing' } # Check if the trust SID is missing (may indicate the partner domain no longer exists) if (-not $isOrphaned -and [string]::IsNullOrWhiteSpace($trust.TrustSID)) { $isOrphaned = $true $reason = 'Trust SID is missing or unresolvable' } # Check if WhenChanged is very old (>365 days) suggesting the trust is stale if (-not $isOrphaned -and $trust.WhenChanged) { try { $whenChanged = if ($trust.WhenChanged -is [datetime]) { $trust.WhenChanged } else { [datetime]::Parse($trust.WhenChanged.ToString()) } $daysSinceChange = ($now - $whenChanged).TotalDays if ($daysSinceChange -gt $staleThresholdDays) { $isOrphaned = $true $reason = "Trust object not modified in $([Math]::Round($daysSinceChange, 0)) days (>$staleThresholdDays days)" } } catch { Write-Verbose "Could not parse WhenChanged for trust $($trust.TrustPartner): $_" } } # Check if WhenChanged is null (no modification date available) if (-not $isOrphaned -and $null -eq $trust.WhenChanged) { $isOrphaned = $true $reason = 'WhenChanged attribute not available - trust age cannot be determined' } if ($isOrphaned) { $orphanedTrusts.Add(@{ TrustPartner = $trust.TrustPartner Direction = $trust.TrustDirection TrustType = $trust.TrustType TrustSID = $trust.TrustSID WhenChanged = $trust.WhenChanged Reason = $reason }) } } if ($orphanedTrusts.Count -gt 0) { $trustNames = @($orphanedTrusts | ForEach-Object { "$($_.TrustPartner) ($($_.Reason))" }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue "$($orphanedTrusts.Count) potentially orphaned trust(s) detected: $($trustNames -join '; ')" ` -Details @{ OrphanedTrustCount = $orphanedTrusts.Count OrphanedTrusts = @($orphanedTrusts) ThresholdDays = $staleThresholdDays TotalTrusts = $trusts.Count } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No orphaned trusts detected among $($trusts.Count) trust(s)" ` -Details @{ TotalTrusts = $trusts.Count ThresholdDays = $staleThresholdDays } } # ── ADTRUST-010: Trust Key Age and Rotation ────────────────────────────── function Test-ReconADTRUST010 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0 } } $now = [datetime]::UtcNow $keyAgeThresholdDays = 180 $staleTrustKeys = [System.Collections.Generic.List[hashtable]]::new() $healthyCount = 0 $unknownCount = 0 foreach ($trust in $trusts) { $whenChanged = $null if ($trust.WhenChanged) { try { $whenChanged = if ($trust.WhenChanged -is [datetime]) { $trust.WhenChanged } else { [datetime]::Parse($trust.WhenChanged.ToString()) } } catch { Write-Verbose "Could not parse WhenChanged for trust $($trust.TrustPartner): $_" } } if ($null -eq $whenChanged) { $unknownCount++ $staleTrustKeys.Add(@{ TrustPartner = $trust.TrustPartner Direction = $trust.TrustDirection WhenChanged = 'Unknown' DaysSinceChange = -1 }) continue } $daysSinceChange = ($now - $whenChanged).TotalDays if ($daysSinceChange -gt $keyAgeThresholdDays) { $staleTrustKeys.Add(@{ TrustPartner = $trust.TrustPartner Direction = $trust.TrustDirection WhenChanged = $whenChanged.ToString('yyyy-MM-dd') DaysSinceChange = [Math]::Round($daysSinceChange, 0) }) } else { $healthyCount++ } } if ($staleTrustKeys.Count -gt 0) { $partnerNames = @($staleTrustKeys | ForEach-Object { if ($_.DaysSinceChange -ge 0) { "$($_.TrustPartner) (last changed $($_.DaysSinceChange) days ago)" } else { "$($_.TrustPartner) (change date unknown)" } }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue "$($staleTrustKeys.Count) trust(s) not modified in over $keyAgeThresholdDays days (trust key may not have been rotated): $($partnerNames -join '; ')" ` -Details @{ StaleKeyCount = $staleTrustKeys.Count HealthyCount = $healthyCount UnknownCount = $unknownCount ThresholdDays = $keyAgeThresholdDays StaleTrustKeys = @($staleTrustKeys) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "All $($trusts.Count) trust(s) have been modified within the last $keyAgeThresholdDays days" ` -Details @{ TrustCount = $trusts.Count ThresholdDays = $keyAgeThresholdDays } } # ── ADTRUST-011: Trust Hierarchy Visualization ─────────────────────────── function Test-ReconADTRUST011 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $trusts = @($AuditData.Trusts | Where-Object { $null -ne $_ }) if ($trusts.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No trust relationships found' ` -Details @{ TrustCount = 0; Topology = 'Isolated' } } # Determine the current domain name for the topology map $domainName = '' if ($AuditData.Domain -and $AuditData.Domain.DomainName) { $domainName = $AuditData.Domain.DomainName } elseif ($AuditData.Connection -and $AuditData.Connection.DomainDN) { $domainName = ($AuditData.Connection.DomainDN -replace '^DC=', '' -replace ',DC=', '.').ToLower() } else { $domainName = 'This Domain' } # Build text-based trust topology $topologyLines = [System.Collections.Generic.List[string]]::new() $topologyLines.Add("Trust Topology for: $domainName") $topologyLines.Add("$('=' * (22 + $domainName.Length))") foreach ($trust in $trusts) { $directionArrow = switch ($trust.TrustDirection) { 'Inbound' { '<--' } 'Outbound' { '-->' } 'Bidirectional' { '<->' } default { '---' } } $trustFlags = [System.Collections.Generic.List[string]]::new() if ($trust.ForestTransitive) { $trustFlags.Add('Forest') } elseif ($trust.WithinForest) { $trustFlags.Add('IntraForest') } else { $trustFlags.Add('External') } if ($trust.IsTransitive) { $trustFlags.Add('Transitive') } if ($trust.IsAzureAD) { $trustFlags.Add('AzureAD') } if (-not $trust.SIDFilteringEnabled -and -not $trust.WithinForest) { $trustFlags.Add('NO-SID-FILTER') } if (-not $trust.SelectiveAuthentication -and -not $trust.WithinForest) { $trustFlags.Add('NO-SELECTIVE-AUTH') } if ($trust.UsesRC4Encryption) { $trustFlags.Add('RC4') } $flagStr = if ($trustFlags.Count -gt 0) { " [$($trustFlags -join ', ')]" } else { '' } $topologyLines.Add(" $domainName $directionArrow $($trust.TrustPartner)$flagStr") } $topologyText = $topologyLines -join "`n" # Build structured topology data $topologyData = @($trusts | ForEach-Object { @{ Source = $domainName Target = $_.TrustPartner FlatName = $_.FlatName Direction = $_.TrustDirection Type = if ($_.ForestTransitive) { 'Forest' } elseif ($_.WithinForest) { 'IntraForest' } elseif ($_.IsAzureAD) { 'AzureAD' } else { 'External' } Transitive = $_.IsTransitive SIDFiltering = $_.SIDFilteringEnabled SelectiveAuth = $_.SelectiveAuthentication UsesRC4 = $_.UsesRC4Encryption NoTGTDelegation = $_.NoTGTDelegation PIMTrust = $_.PIMTrust TrustSID = $_.TrustSID WhenCreated = $_.WhenCreated WhenChanged = $_.WhenChanged } }) # Compile summary statistics $directionCounts = @{ Inbound = @($trusts | Where-Object { $_.TrustDirection -eq 'Inbound' }).Count Outbound = @($trusts | Where-Object { $_.TrustDirection -eq 'Outbound' }).Count Bidirectional = @($trusts | Where-Object { $_.TrustDirection -eq 'Bidirectional' }).Count Disabled = @($trusts | Where-Object { $_.TrustDirection -eq 'Disabled' }).Count } $securitySummary = @{ SIDFilteringEnabled = @($trusts | Where-Object { $_.SIDFilteringEnabled }).Count SelectiveAuthEnabled = @($trusts | Where-Object { $_.SelectiveAuthentication }).Count TransitiveTrusts = @($trusts | Where-Object { $_.IsTransitive }).Count RC4Trusts = @($trusts | Where-Object { $_.UsesRC4Encryption }).Count } $currentValue = "Trust topology: $domainName has $($trusts.Count) trust relationship(s) - $($directionCounts.Bidirectional) bidirectional, $($directionCounts.Inbound) inbound, $($directionCounts.Outbound) outbound" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ DomainName = $domainName TrustCount = $trusts.Count DirectionCounts = $directionCounts SecuritySummary = $securitySummary TopologyText = $topologyText TopologyData = $topologyData } } |