• Contact Us

    We will get back to you as soon as possible.

  • We do not collect personal information except to the extent you provide that information to use through email, your web browser or through our contact forms. We will not share, sell or otherwise provide non-public information to others without your consent.

SIGN UP FOR OUR NEWSLETTER

  • We do not collect personal information except to the extent you provide that information to use through email, your web browser or through our contact forms. We will not share, sell or otherwise provide non-public information to others without your consent.

Security First – Audit AD Privileged Access
Published on December 9, 2021
Audit Active Directory Privileged Access

Audit Active Directory Privileged Access

Regular and routine audits of Active Directory for privileged access remains a critical and time sensitive security task – oh boy is it!  Audits performed  against a single AD Domain can be relatively straight forward.  However, the same may not be true of an AD Forest containing numerous subdomains. The unpopular complexity of nesting  objects (sometimes cyclical) across, or throughout, a forest obfuscates privileged-visibility and increases an enterprise’s potentiality for breach (i.e. security groups, users, GMSA, foreign principals and other such objects).

Aside from routine audits, businesses ought to maintain tight change-controls, alerting and access reviews for all privileged type-access, including those dedicated development and testing environments.

Privileged access should follow a least access privileged model – meaning one should ONLY possess limited, approved and appropriate access to perform one’s duty – and no more.  In fact, this access should be time-controlled, whereby an individual’s privileged access is checked out and subsequently expired (revoked or suspended) once the access is no longer needed.

With regular review comes the potential for privileged access remediation

The script below captures and provides extensive details of an AD Forest’s privileged access. 


 

Get-ADPrivUser-Details.ps1

[cmdletbinding()]
    Param (
            [Parameter(Mandatory=$False,
                        HelpMessage="Provide Forest Name(s)")]
                            [Array]$Forests,

            [Parameter(Mandatory=$False,
                        HelpMessage="Export Privileged Group(s)")]
                            [Switch]$ExportGroups,

            [Parameter(Mandatory=$False,
                        HelpMessage="File Export Path")]
                            [String]$AltExportPath = $Null
            )

    <#
    .Synopsis
        Script combs through a forest and collects detailed privileged groups and principals.

    .Description
        Script systematically combs through an AD forest and all domains to collect
            a. privileged groups
            b. privileged users
    
        Script Steps:
            1. Cycles through and captures all Forest Domains.
                - Forest
                - Domain
                - Domain SID
                - Domain Controller
                - Domain Distinguished Name (DN)

            2. Cycles through each forest domain and captures privileged & nest groups
                - Group Nest Type: Parent or Nested Group
                - Group Scope: Local Domain or Foreign Domain (e.g. nested from another domain)
                - Group | Parent Domain & Group Name
                - Group | Child Domain & Group Name
                - Group | Interitance Broken
                - Group Member:
                    - Computer Counts Only
                    - Foreign Principals Counts Only
                    - Group Managed Service Accounts (GMSA) Counts Only
                    - Nest Group Counts Only
                    - Direct & Nested Users Counts Only
                        - Percentage of User Duplicates
                - Group | Nested Group DNs and Display Names
                - Optional Output to file

            3. Cycles through each privileged & nested group
            3a. Queries each forest domain to obtain detailed user info
            3b. Exports Detailed Privileged Group and User Information to File (note: all attributes not included)
                - Group Details (see above)
                - User | Forest
                - User | Domain
                - User | Scope - i.e. whether user domain is local or foreign to privileged group
                - User | Expiration Details (custom code to include expired X days ago)
                - User | Enabled/Disabled
                - User | Last Logon (custom code to include logon on X days ago)
                - User | Kerberos Encryption Types
                - User | Memberof Groups
                - User | Privileged Group Count
                - User | Password Details
                - User | User Account Control (UAC) Details

            Default CSV Output: $env:TEMPMMddyy-Data
            Example CSV Output: C:UsersShawn.MayAppDataLocalTemp120321-Data

    .Notes:
        File Name: Get-ADPrivUser-Details.ps1
        Author   : Shawn May
        Email    : shawn@yourdts.com
        Requires : Powershell V3 or greater
        Version  : 1.0


    .PARAMETER Forests
        Optional: Specify list of Forests to query. If empty, queryies local forest

    .PARAMETER ExportGroups
        Optional: Specify whether to export privileged groups
        
    .PARAMETER AltExportPath
        Optional: Specify alternate CSV Output path

    .EXAMPLE
        .Get-ADPrivUser-Details.ps1 -Forests acme.com, warnerbro.com

    .EXAMPLE
        .Get-ADPrivUser-Details.ps1 -ExportGroups

    .EXAMPLE
        .Get-ADPrivUser-Details.ps1 -AltExportPath 'C:tempOutput'
    #>

    # Clears Host
    Clear-Host

    # Acquires Date
    $Date = Get-Date

    # Generates File Date Suffix - e.g. 2020-8-23-23-49
    $StrFileDateSuffix = "{0}-{1}-{2}-{3}-{4}" `
                -f $Date.year, $Date.month, $Date.Day, `
                    $Date.Minute, $Date.Second

    # Generates Export Path
    $RootExportFolder = Join-Path $env:TEMP ((Get-Date -format "MMddyy") + "-Data")

    # Checks Export path parameter was passed to Script
    if (![string]::IsNullOrEmpty($AltExportPath))
        {
        # Replaces variable with alternate export path
        $RootExportFolder = $AltExportPath
        }

    # Checks if export path exists
    if ((Test-Path $RootExportFolder) -eq $False)
        {
        # Try | Catch Statement
        Try {$a = New-Item -ItemType directory -Path $RootExportFolder -ErrorAction Stop}
        Catch {Write-Host "Specified Drive and/or Folder does not exist." `
            -ForegroundColor Yellow ; exit}
        }

# User Class
Class User {
    # Defines Class object attributes
    [Array]    hidden $AddrTypes = @('SMTP','SIP')
    [datetime] hidden $Date
    [Bool]     $IsCriticalSystemObject
    [String]   $ExpireDate
    [String]   $ExpireStatus
    [Bool]     $Expired
    [Bool]     $Stale
    [Bool]     $PWNotRequired
    [Bool]     $PWCantChange
    [Bool]     $PWDoesNotExpire
    [Bool]     $TrustedForDelegation
    [Bool]     $UseDesKeyOnly
    [string]   $SMTPProxyAddr
    [string]   $SIPProxyAddr
    [string]   $LastLogonTimeStamp
    [string]   $LastLogonDate
    [string]   $Memberof
    [Array]    $ArrMemberof
    [Int32]    $GroupCount
    [GUID]     $ObjectGUID
    [string]   $eTypes
    [string]   $UserSID

    # Constructor
    User() {}

    # Constructor
    User($ObjUser)
        {
        $this.ObjectGUID = $ObjUser.ObjectGUID
        $this.SetCriticalFlag($ObjUser.IsCriticalSystemObject)
        $this.GetADExpiration($ObjUser.accountExpires)
        $this.AddrTypes | foreach {$this.GetProxyAddr($ObjUser.proxyAddresses,$_)}
        $this.DecodeLastLogon($ObjUser.LastLogonTimeStamp,$ObjUser.LastLogonDate)
        $this.GetMemberOf($ObjUser.Memberof)
        $this.DecodeUAC($ObjUser.UserAccountControl)
        $this.GetETypes($ObjUser.'msDS-SupportedEncryptionTypes')
        $this.UserSID = $ObjUser.SID.ToString()
        }

    # Translates Kerb Encryption Type
    GetETypes($SupportedEncryptionTypesIn)
        {
        Switch ($SupportedEncryptionTypesIn)
            {
            0 {$this.eTypes = "Not defined - defaults to RC4_HMAC_MD5"}
            1 {$this.eTypes = "DES_DES_CBC_CRC"}
            2 {$this.eTypes = "DES_CBC_MD5"}
            3 {$this.eTypes = "DES_CBC_CRC, DES_CBC_MD5"}
            4 {$this.eTypes = "RC4"}
            5 {$this.eTypes = "DES_CBC_CRC, RC4"}
            6 {$this.eTypes = "DES_CBC_MD5, RC4"}
            7 {$this.eTypes = "DES_CBC_CRC, DES_CBC_MD5, RC4"}
            8 {$this.eTypes = "AES 128"}
            9 {$this.eTypes = "DES_CBC_CRC, AES 128"}
            10 {$this.eTypes = "DES_CBC_MD5, AES 128"}
            11 {$this.eTypes = "DES_CBC_CRC, DES_CBC_MD5, AES 128"}
            12 {$this.eTypes = "RC4, AES 128"}
            13 {$this.eTypes = "DES_CBC_CRC, RC4, AES 128"}
            14 {$this.eTypes = "DES_CBC_MD5, RC4, AES 128"}
            15 {$this.eTypes = "DES_CBC_MD5, DES_CBC_MD5, RC4, AES 128"}
            16 {$this.eTypes = "AES 256"}
            17 {$this.eTypes = "DES_CBC_CRC, AES 256"}
            18 {$this.eTypes = "DES_CBC_MD5, AES 256"}
            19 {$this.eTypes = "DES_CBC_CRC, DES_CBC_MD5, AES 256"}
            20 {$this.eTypes = "RC4, AES 256"}
            21 {$this.eTypes = "DES_CBC_CRC, RC4, AES 256"}
            22 {$this.eTypes = "DES_CBC_MD5, RC4, AES 256"}
            23 {$this.eTypes = "DES_CBC_CRC, DES_CBC_MD5, RC4, AES 256"}
            24 {$this.eTypes = "AES 128, AES 256"}
            25 {$this.eTypes = "DES_CBC_CRC, AES 128, AES 256"}
            26 {$this.eTypes = "DES_CBC_MD5, AES 128, AES 256"}
            27 {$this.eTypes = "DES_CBC_MD5, DES_CBC_MD5, AES 128, AES 256"}
            28 {$this.eTypes = "RC4, AES 128, AES 256"}
            29 {$this.eTypes = "DES_CBC_CRC, RC4, AES 128, AES 256"}
            30 {$this.eTypes = "DES_CBC_MD5, RC4, AES 128, AES 256"}
            31 {$this.eTypes = "DES+A1:C33_CBC_MD5, DES_CBC_MD5, RC4, AES 128, AES 256"}
            default {$this.eTypes = "Not defined - defaults to RC4_HMAC_MD5"}
            }
        }

    # Checks whether object is a critical system objects
    SetCriticalFlag([Bool]$IsCriticalIn)
        {
        if ($IsCriticalIn -is [object])
            {$this.IsCriticalSystemObject = $IsCriticalIn}
        else
            {$this.IsCriticalSystemObject = [DBNull]::Value}
        }

    # Translates Account Expiration Details
    GetADExpiration([Int64]$ExpirationIn)
        {
        # Populates Date Attribute
        $this.Date = Get-Date

        # Checks value - Nonexpiring Value
        if (($ExpirationIn -eq '9223372036854775807') -or `
                ($ExpirationIn[0] -eq 0) -or `
                ([datetime]::fromfiletime($ExpirationIn) -gt [DateTime]::MaxValue.Ticks)) #($lngValue -gt [DateTime]::MaxValue.Ticks))
            {
            $this.ExpireDate   = $Null
            $this.ExpireStatus = "NonExpiring"
            $this.Expired      = $False
            }
        Else
            {
            $lngValue = [datetime]::fromfiletime($ExpirationIn)

            # Assigns Value to Array
            $this.ExpireDate = $lngValue.ToLocalTime().toshortdatestring()
            #$AcctExpires = @($lngValue.ToLocalTime().toshortdatestring())

            if ([DateTime]$this.ExpireDate -gt [DateTime]$this.Date)
                { # Assigns Values to Array (appends)
                $this.ExpireStatus = "Active"
                $this.Expired = $False
                }
            else
                { # Assigns Values to Array (appends)
                $this.ExpireStatus = "Expired"
                $this.Expired = $True
                #$this.Stale = $True
                }
            }
        }

    # Translates Proxy Addresses
    GetProxyAddr([array]$ProxyAddrIn,[string]$AddrFilter)
        {
        $ProxyAddr = $Null
        # Checks if Variable is Object
        #if (($ProxyAddrIn | Measure-Object).count -gt 0)
        if (($ProxyAddrIn | where {(($_).split(":")[0] -match $AddrFilter)}| Measure-Object).count -gt 0)
            { # Cycles through Variable
            $ProxyAddrIn | Foreach `
                { # Splits String (see filter)
                if (($_).split(":")[0] -match $AddrFilter)
                    { # Checks if value is Null
                    if ($ProxyAddr -eq $Null)
                        { # Assigns Value to variable
                        $ProxyAddr = ($_).split(":")[1]} `
                    else
                        { # Appends Value to variable
                        $ProxyAddr += ("," + ($_).split(":")[1])}
                    }
                }
            }
        else
            {$ProxyAddr = "None"}

        if ($AddrFilter -eq $this.AddrTypes[0])
            {$this.SMTPProxyAddr = $ProxyAddr}
        else
            {$this.SIPProxyAddr = $ProxyAddr}
        }

    # Translates Last Logon Details
    DecodeLastLogon($LastLogonTimeStampIn,$LastLogonDateIn)
        {
        # Checks if object
        if ($LastLogonTimeStampIn -is [object])
            {$this.LastLogonTimeStamp = [datetime]::FromFileTime($LastLogonTimeStampIn).toshortdatestring()}
        else
            {$this.LastLogonTimeStamp = $Null}

        # Checks if object
        if ($LastLogonDateIn -is [object])
            {$this.LastLogonDate = ($LastLogonDateIn).toshortdatestring()}
        else
            {$this.LastLogonDate = $Null}
        }

    # Captures MemberOf  Details
    GetMemberOf($MemberofIn)
        {
        # Cycles through array
        foreach ($Member in $MemberofIn)
            {
            # Checks if object is null or empty
            if ([string]::IsNullOrEmpty($this.Memberof))
                {$this.Memberof = $Member}
            else
                {$this.Memberof += ("`n" + $Member)}
            }
        # Measure Object Count
        $this.GroupCount = ($MemberofIn | Measure-Object).count

        # Assigns Values
        $MemberofIn | foreach {$this.ArrMemberof += @($_)}
        }

    # Translates UAC Details
    DecodeUAC($UserAccountControl)
        {
        $this.PWNotRequired = (($UserAccountControl -band 32) -eq 32)
        $this.PWCantChange = (($UserAccountControl -band 64) -eq 64)
        $this.PWDoesNotExpire = (($UserAccountControl -band 65536) -eq 65536)
        $this.TrustedForDelegation = (($UserAccountControl -band 524288) -eq 524288)
        $this.UseDesKeyOnly = (($UserAccountControl -band 2097152) -eq 2097152)
        }

    # Calculates time span
    [Int32]GetDateDiff([datetime]$DateIn, [datetime]$today)
        {Return (New-TimeSpan -Start $DateIn -End $today).Days}
}

# Function | Converts FQDN to DistinguishedName
Function Convert-FQDN2DN {
    [cmdletbinding()]
        Param ([Parameter(Mandatory=$True)]
                [string]$domainFQDN)

    # Splits FQDN in Array 
    $colSplit = $domainFQDN.Split('.')

    # Captures Number of Array
    $FQDNdepth = $colSplit.length
    $DomainDN = ''

    # Cycles Through Array
    For ($i=0; $i -lt ($FQDNdepth); $i++)
        {
        If ($i -eq ($FQDNdepth - 1)) {$Separator=''}
        else {$Separator=','}
        [string]$DomainDN += 'DC=' + $colSplit[$i] + $Separator
        }
    Return $DomainDN
}

# Function | Populates Forest Domains Datatable
Function Build-ForestDomSIDDT {
    [cmdletbinding()]
        Param($ObjForestIn)
    
    # Cycles through all Forest Domains
    foreach ($StrDomain in $ObjForestIn.Domains)
        {
        # Captures single domain controller from each domain in the forest
        $ObjDC = Get-ADDomainController `
            -DomainName $StrDomain `
            -Discover `
            -Service ADWS `
            -ForceDiscover

        # Populates Datatable
        $ObjDTRow_DomSIDs = $DT_DomainSIDs.NewRow()
        $ObjDTRow_DomSIDs.Forest            = $ObjForestIn.RootDomain
        $ObjDTRow_DomSIDs.Domain            = $StrDomain
        $ObjDTRow_DomSIDs.DomainSID         = ((Get-ADDomain $ObjDC.Domain).domainSID).tostring()
        $ObjDTRow_DomSIDs.DomainController  = $ObjDC.HostName[0]
        $ObjDTRow_DomSIDs.DomainDN          = [string](Convert-FQDN2DN ($ObjDC.Domain))
        $DT_DomainSIDs.Rows.Add($ObjDTRow_DomSIDs)
        }
}

# Function | Populates Forest Privileged Group Datatable
Function Build-ForestPrivGroupDT {
    [cmdletbinding()]
        Param($ObjForestIn)

    # Privileged Group Membership for the following groups:
    # Reference: http://support.microsoft.com/kb/243330

    # Calls Function to Obtain list of Forest Domains, DomainSIDs, and Domain Controllers
    Write-Host ('Querying Forest Details: ' + $ObjForestIn.name)

    # Property Sets
    $GrpProps = @("Name","DistinguishedName","adminCount","nTSecurityDescriptor","objectSID")
    $SelProps = @($GrpProps)
    $SelProps += @({$_.nTSecurityDescriptor.AreAccessRulesProtected})
    $NestedGrpProps = @('Name','objectSID','DistinguishedName','objectclass')
        
    # Converts Forest FQDN to DistinguishedName
    $DomainDN = [String](Convert-FQDN2DN -domainFQDN ($ObjForestIn.name))

    # Captures Forest / Domain SID
    $DomainSID = ((Get-ADDomain $ObjForestIn.name).domainSID).tostring()

    # Default Forest & Domain Privileged SIDs
    $DefForestSIDs = @("$DomainSID-518","$DomainSID-519")
    $DefDomainSIDs = @('S-1-5-32-544','S-1-5-32-548','S-1-5-32-549','S-1-5-32-551','S-1-5-32-550')
    $AppendDomSIDs = @('512','517','520','521','553')

    # Cycles through Forest Domains
    foreach ($StrDomain in $ObjForestIn.Domains)
        {
        # Writes message to screen
        Write-Host ('- Querying for Domain Controller: ' + $StrDomain)
        
        # Queryies Domain Controller (Local Parent)
        $ObjDC = Get-ADDomainController `
            -DomainName $StrDomain -Discover `
            -Service ADWS -ForceDiscover

        # -- Builds Unique Domain Privileged SID Collection -- #
            # Creates Array
            $ColDomainSIDs = @()

            # Captures Unique Domain SID
            $DomainSID = ((Get-ADDomain -Server $ObjDC.Domain).domainSID).tostring()

            # Adds Forest Privileged SIDs to Collection
            if ($StrDomain.name -eq $ObjForest.name)
                {$ColDomainSIDs = $DefForestSIDs}

            # Adds Domain Privileged SIDs to Collection
            if ($ColDomainSIDs -is [object]) 
                {$ColDomainSIDs += $DefDomainSIDs}
            else
                {$ColDomainSIDs = $DefDomainSIDs}

            # Adds Domain Privileged SIDs to Collection
            $AppendDomSIDs | foreach `
                {$ColDomainSIDs += ($DomainSID + '-' + $_)}
            
            # Writes message to screen
            Write-Host "- Querying Domain Privileged Group SIDs - DC: $($ObjDC.Hostname[0])"
        # ----------------------------------------- #
        
        # Cycles through Unique Domain Privileged SID Collection
        Foreach ($SID in $ColDomainSIDs)
            {
            # Create Array Varible
            $ColNestedGrps = @()

            # Writes Nested Group Details Datatable
            $ColNestedGrps = Write-toTable -ObjDCIn $ObjDC -StrSIDIn $SID

            # Checks if Variable is object
            if ($ColNestedGrps -is [object])
                {
                # Cycles through Domain Privileged Nested Groups
                foreach ($NestedGrp in $ColNestedGrps) 
                    {
                    # Queries Datatable for Nested Group's Domain Details (i.e. Domain, DN, SID & DC)
                    $ObjNestedDomain = ($DT_DomainSIDs.where({$NestedGrp.objectSID.ToString() -match $_.DomainSid}))

                    # Writes Nested Group Details Datatable
                    Write-toTable -ObjDCIn $ObjDC -StrSIDIn $NestedGrp.objectSID
                    }
                }
            # Nulls variable
            $ColNestedGrps = $Null ; $ObjNestedDomain = $Null
            }
        }

    if ($ExportGroups)
        {
        # GPO Full Report CSV File Name
        $CSVGrpFileName = ("_" + $ObjForestIn.name + "_Forest_PrivGroups_" + $StrFileDateSuffix + ".csv" `
                -f $Date.year, $Date.month, $Date.Day).ToUpper()

        # Join Path to create Export Variable
        $Export = Join-Path $RootExportFolder $CSVGrpFileName
    
        # Exports Datatable to CSV
        $DT_ForestPrivGrps | Export-Csv $Export -NoTypeInformation

        # Post Script: Writes Output to Screen
        $script:ArrOutputCSVs += @("Forest Groups:$($ObjForestIn.Rootdomain) | Output CSV File: $Export")
        }
    }

# Captures DNs
Function Get-NestDNs {
    [cmdletbinding()]
        Param($ObjIn)

    # Cycles through DNs
    foreach ($DN in $ObjIn)
        {
        # Checks if variables are empty
        if ([string]::IsNullOrEmpty($Temp1) -or [string]::IsNullOrEmpty($Temp2))
            {
            $Temp1 = $DN.DistinguishedName
            $Temp2 = $DN.Name
            }
        else
            {
            $Temp1 += "`n" + $DN.DistinguishedName
            $Temp2 += "`n" + $DN.Name
            }
        }
    $StrNestedDNOut = @()        
    $StrNestedDNOut += $Temp1
    $StrNestedDNOut += $Temp2
    Return [array]$StrNestedDNOut
}

# Function Defines AD & Data Table Attributes    
Function Define-Attributes {
    [cmdletbinding()]
        Param([Parameter(Mandatory=$True)]
                [Int32]$IntGrp)

    # Define Array
    $ArrDTCols = @()
    $ArrDTCols += @('Counter')

    # Define Array
    $Script:ADUserAttribs = @()
    $Script:ADUserAttribs += @('IsCriticalSystemObject')
    $Script:ADUserAttribs += @('AccountExpirationDate','accountExpires')
    $Script:ADUserAttribs += @('Enabled')
    $Script:ADUserAttribs += @('CanonicalName','DisplayName')
    $Script:ADUserAttribs += @('Department','Description')
    $Script:ADUserAttribs += @('Division')
    $Script:ADUserAttribs += @('EmailAddress','mail')
    $Script:ADUserAttribs += @('adminCount','adminDescription')
    $Script:ADUserAttribs += @('ProxyAddresses')
    $Script:ADUserAttribs += @('lastLogonTimestamp','LastLogonDate')
    $Script:ADUserAttribs += @('logonCount')
    $Script:ADUserAttribs += @('msDS-SupportedEncryptionTypes')
    $Script:ADUserAttribs += @('MemberOf','Organization')
    $Script:ADUserAttribs += @('ObjectGUID')
    $Script:ADUserAttribs += @('PasswordLastSet','pwdLastSet')
    $Script:ADUserAttribs += @('PasswordExpired')
    $Script:ADUserAttribs += @('CannotChangePassword')
    $Script:ADUserAttribs += @('PasswordNeverExpires','PasswordNotRequired')
    $Script:ADUserAttribs += @('SamAccountName','ScriptPath')
    $Script:ADUserAttribs += @('SID')
    $Script:ADUserAttribs += @('SIDHistory')
    $Script:ADUserAttribs += @('UserAccountControl','UserPrincipalName')
    $Script:ADUserAttribs += @('whenChanged','whenCreated')
    $Script:ADUserAttribs += @('DistinguishedName')

    Switch ($IntGrp)
        {
            0
                {
                $ArrDTCols += @('Forest')
                $ArrDTCols += @('Domain')
                $ArrDTCols += @('DomainDN')
                $ArrDTCols += @('DomainSID')
                $ArrDTCols += @('DomainController')
                }

    # Privileged Group Data Table Columns
            1
                {
                $ArrDTCols += @('ReportDate')
                $ArrDTCols += @('Grp_NestType')
                $ArrDTCols += @('Grp_Scope')
                $ArrDTCols += @('Grp_Name')
                $ArrDTCols += @('AD_Forest')
                $ArrDTCols += @('Grp_Parent_Domain')
                $ArrDTCols += @('Grp_Parent')
                $ArrDTCols += @('Grp_Child_Domain')
                $ArrDTCols += @('Grp_Child')
                $ArrDTCols += @('AD_DC')
                $ArrDTCols += @('AD_DomainDN')
                $ArrDTCols += @('AD_DomainSID')
                $ArrDTCols += @('Grp_DN')
                $ArrDTCols += @('Grp_SID')
                $ArrDTCols += @('Grp_AdminCount')
                $ArrDTCols += @('Grp_InheritanceBroken')
                $ArrDTCols += @('NestedCompCnt')
                $ArrDTCols += @('NestedForeignPrinCnt')
                $ArrDTCols += @('NestedGMSACnt')
                $ArrDTCols += @('DirectUserCnt')
                $ArrDTCols += @('NestedUserCnt')
                $ArrDTCols += @('UniqueUserCnt')
                $ArrDTCols += @('%UserDups')
                $ArrDTCols += @('NestedGroupCnt')
                $ArrDTCols += @('NestedGroupsDN')
                $ArrDTCols += @('NestedGroupsName')
                }
            
# Privileged User Data Set Columns
            2
                                {
                # Default Array Definition: DataTable Column List
                $ArrDTCols += @('ReportDate')
                $ArrDTCols += @('Grp_NestType')
                $ArrDTCols += @('Grp_Scope')
                $ArrDTCols += @('Grp_Name')
                $ArrDTCols += @('AD_Forest')
                $ArrDTCols += @('Grp_Parent_Domain')
                $ArrDTCols += @('Grp_Parent')
                $ArrDTCols += @('Grp_Child_Domain')
                $ArrDTCols += @('Grp_Child')
                $ArrDTCols += @('AD_DC')
                $ArrDTCols += @('AD_DomainDN')
                $ArrDTCols += @('AD_DomainSID')
                $ArrDTCols += @('Grp_DN')
                $ArrDTCols += @('Grp_SID')
                $ArrDTCols += @('Grp_AdminCount')
                $ArrDTCols += @('Grp_InheritanceBroken')
		$ArrDTCols += @('Stale')
                $ArrDTCols += @('User_Forest')
                $ArrDTCols += @('User_Domain')
                $ArrDTCols += @('User_Scope')
                $ArrDTCols += @('IsCriticalSystemObject')
		$ArrDTCols += @('SamAccountName')
                $ArrDTCols += @('DisplayName')
                $ArrDTCols += @('Enabled')
                $ArrDTCols += @('Expired')
                $ArrDTCols += @('ExpireStatus')
                $ArrDTCols += @('ExpireDate')
                $ArrDTCols += @('ExpirationDate')
                $ArrDTCols += @('ExpirationDays')
                $ArrDTCols += @('AccountExpires')
                $ArrDTCols += @('LastLogonDaysAgo')
                $ArrDTCols += @('LastLogonTimestamp')
                $ArrDTCols += @('LastLogonDate')
                $ArrDTCols += @('LogonCount')
                $ArrDTCols += @('CanonicalName')
                $ArrDTCols += @('Department','Description')
                $ArrDTCols += @('Division')
                $ArrDTCols += @('ETypes')
                $ArrDTCols += @('EmailAddress','mail')
                $ArrDTCols += @('adminCount','adminDescription')
                $ArrDTCols += @('ProxyAddresses')
                $ArrDTCols += @('ProxySMTPAddr','ProxySIPAddr')
                $ArrDTCols += @('MemberOf','GroupCount','PrivGrpCnt','Organization')
                $ArrDTCols += @('pwdExpired')
                $ArrDTCols += @('pwdLastSet')
                $ArrDTCols += @('pwdLastSetDaysAgo')
                $ArrDTCols += @('CannotChangePwd')
                $ArrDTCols += @('pwdNotRequired')
                $ArrDTCols += @('pwdNeverExpires')
                $ArrDTCols += @('ScriptPath')
                $ArrDTCols += @('SIDHistory')
                $ArrDTCols += @('UserAccountControl')
                $ArrDTCols += @('UAC_PWNotRequired','UAC_PWCantChange')
                $ArrDTCols += @('UAC_PWDoesNotExpire','UAC_TrustedForDelegation')
                $ArrDTCols += @('UAC_UseDesKeyOnly','UserPrincipalName')
                $ArrDTCols += @('whenChanged','whenCreatedDays','whenCreated')
                $ArrDTCols += @('DistinguishedName')
                }
        }
    Return $ArrDTCols
}

# Function cycles through and writes privileged group details to Data Table
Function Write-toTable {
    [cmdletbinding()]
        Param($ObjDCIn, $StrSIDIn)

    # Clears Errors
    $Error.Clear()

    # LDAP Filter - SID
    $LDAPFilter  = "(objectsid=$($StrSIDIn))"

    # Assigns Variable Value
    $GrpScope = 'Local Domain'
    $StrDashes = '--'
    $Color = 'Yellow'

    # Checks if Nest Groups is an object
    if ($ObjNestedDomain -is [object])
        {
        # Checks if Group's Domain SID Matches Parent Domain being queried
        if ($ObjNestedDomain.DomainSID -ne $DomainSID)
            {
            # Queryies Foreign Domain Controller (Local Parent)
            $ObjDCIn = Get-ADDomainController `
                -DomainName $ObjNestedDomain.Domain -Discover `
                -Service ADWS -ForceDiscover

            $GrpScope = 'Foreign Domain'
            $DomainSID = $ObjNestedDomain.domainSID
            }
        # Captures instance of group in Domain
        $ObjGrp = Get-ADObject -LDAPFilter $LDAPFilter `
                        -Server $ObjDCIn.HostName[0] `
                            -Properties $GrpProps  | Select-Object $SelProps
        # Assigns Variable Value
        $NestType = ('Nested Group')

        $DomChildGrp = $ObjDCIn.domain
        $ChildGrp = $ObjGrp.name

        $StrDashes = '---'
        $Color = 'Green'
        }
    else
        {
        # Captures instance of group in Domain
        $ObjGrp = Get-ADObject -LDAPFilter $LDAPFilter `
                        -Server $ObjDCIn.HostName[0] `
                            -Properties $GrpProps  | Select-Object $SelProps
        
        # Populates variables
        $NestType = 'Parent Group'
        $Script:DomParentGrp = $ObjDCIn.domain
        $Script:ParentGrp = $ObjGrp.name
        $DomChildGrp = 'n/a'
        $ChildGrp = 'n/a'
        }
    
    # Writes output to screen
    Write-Host ($StrDashes + ' Querying Privileged ' + $NestType + ' Details: ' + $ObjGrp.Name + ' - ' + $GrpScope) `
                    -ForegroundColor $Color

    # LDAP Filter - Objects
    $LDAPFilter  = "(memberof:1.2.840.113556.1.4.1941:=$($ObjGrp.DistinguishedName))"
    $NestedObjIn = @(Get-ADObject -LDAPFilter $LDAPFilter `
                                    -Server ($ObjDCIn.HostName[0] + ":3268")`
                                    -Properties $NestedGrpProps | `
                                        Select-Object $NestedGrpProps | Sort-Object Objectclass)

    # Assigns Variable Value
    [Int32]$DirUserCnt = 0
    [Int32]$NestedUserCnt = 0
    [Int32]$TotalUniqueCnt = 0
    $TmpUsers = @()

    # LDAP Query
    $LDAPQuery = "(memberof=$($ObjGrp.DistinguishedName))"

    # Queries GC Cycles through forest datatable - Searches AD & counts (user objects) members of privileged group
    $TmpUsers += Get-ADUser -LDAPFilter $LDAPQuery `
                    -Server ($ObjDCIn.HostName[0] + ":3268")

    # Measure User Count
    $DirUserCnt += ($TmpUsers | Measure-Object).count
    $NestedUserCnt += $DirUserCount

    # Queries LDAP - Cycles Through Nested Groups - counts (user objects) members of privileged group
    $NestedGrpsIn = $NestedObjIn | where {$_.objectclass -eq 'group'} 
    Foreach ($NestedGrp in $NestedGrpsIn)
        {
        $TmpUsers += Get-ADUser -LDAPFilter "(memberof:1.2.840.113556.1.4.1941:=$($NestedGrp.DistinguishedName))"  `
                -Server ($DT_DomainSIDs.where({$NestedGrp.objectSID.ToString() `
                            -match $_.DomainSID}).domaincontroller + ":389") 
        }

    $NestedUserCnt += ($TmpUsers | Measure-Object).count
    $TotalUniqueCnt = (($TmpUsers).DistinguishedName | Sort-Object {$_} | Get-Unique | Measure-Object).count

    # Creates Array Variable
    $NestedDetails = @()
    if (($NestedGrpsIn | Measure-Object).count -gt 0)
        {$NestedDetails = Get-NestDNs -ObjIn $NestedGrpsIn}

    # Creates New Datatable Row
    $ObjDTRow_PrivGrps = $DT_ForestPrivGrps.NewRow()

    # Adds Rows and Values to Datatable
    $ObjDTRow_PrivGrps.ReportDate            = ($Date).ToShortDateString()
    $ObjDTRow_PrivGrps.Grp_NestType          = $NestType
    $ObjDTRow_PrivGrps.Grp_Scope             = $GrpScope
    $ObjDTRow_PrivGrps.Grp_Name              = $ObjGrp.Name
    $ObjDTRow_PrivGrps.AD_Forest             = $ObjDCIn.Forest
    $ObjDTRow_PrivGrps.Grp_Parent_Domain     = $DomParentGrp
    $ObjDTRow_PrivGrps.Grp_Parent            = $ParentGrp
    $ObjDTRow_PrivGrps.Grp_Child_Domain      = $DomChildGrp
    $ObjDTRow_PrivGrps.Grp_Child             = $ChildGrp
    $ObjDTRow_PrivGrps.AD_DC                 = $ObjDCIn.HostName[0]
    $ObjDTRow_PrivGrps.AD_DomainDN           = [string](Convert-FQDN2DN ($ObjDCIn.Domain))
    $ObjDTRow_PrivGrps.AD_DomainSID          = $DomainSID
    $ObjDTRow_PrivGrps.Grp_DN                = $ObjGrp.DistinguishedName
    $ObjDTRow_PrivGrps.Grp_SID               = $ObjGrp.objectSID.ToString()
    $ObjDTRow_PrivGrps.Grp_AdminCount        = $ObjGrp.adminCount
    $ObjDTRow_PrivGrps.Grp_InheritanceBroken = $ObjGrp.'$_.nTSecurityDescriptor.AreAccessRulesProtected'
    $ObjDTRow_PrivGrps.NestedGroupCnt        = ($NestedGrpsIn | Measure-Object).count
    $ObjDTRow_PrivGrps.DirectUserCnt         = $DirUserCnt
    $ObjDTRow_PrivGrps.NestedForeignPrinCnt  = ($NestedObjIn | where {$_.objectclass -eq 'foreignSecurityPrincipal'} | Measure-Object).count
    $ObjDTRow_PrivGrps.NestedCompCnt         = ($NestedObjIn | where {$_.objectclass -eq 'computer'} | Measure-Object).count
    $ObjDTRow_PrivGrps.NestedGMSACnt         = ($NestedObjIn | where {$_.objectclass -eq 'msDS-GroupManagedServiceAccount'} | Measure-Object).count
    $ObjDTRow_PrivGrps.NestedUserCnt         = $NestedUserCnt
    if ($NestedUserCnt -gt 0)
        {$ObjDTRow_PrivGrps.('%UserDups')    = (($NestedUserCnt - $TotalUniqueCnt)/$NestedUserCnt).ToString("P")}
    else
        {$ObjDTRow_PrivGrps.('%UserDups')    = (0).ToString("P")}
    $ObjDTRow_PrivGrps.UniqueUserCnt         = $TotalUniqueCnt
    $ObjDTRow_PrivGrps.NestedGroupsDN        = if ($NestedDetails.Count -eq 0) {"None"} else {$NestedDetails[0]}
    $ObjDTRow_PrivGrps.NestedGroupsName      = if ($NestedDetails.Count -eq 0) {"None"} else {$NestedDetails[1]}
    $DT_ForestPrivGrps.Rows.Add($ObjDTRow_PrivGrps)

    if ($ObjNestedDomain -isnot [object])
        {Return ,$NestedGrpsIn}
}

# Function builds Datatable
Function Build-DataTable {
    [cmdletbinding()]
        Param (
            [Parameter(Mandatory=$True)]
                [String]$StrDTTitleIn,
            [Parameter(Mandatory=$True)]
                [Array]$ArrAttribsIn
            )

    # Creates DataTable
    $ObjDTOut = $Null
    $ObjDTOut = New-Object system.data.datatable $StrDTTitleIn

    # Cycles through & assigns Column-Names to DataTable
    Foreach ($DTColumn in $ArrAttribsIn)
        {
        # Checks and defines custom column & properties - Int32
        if ($DTColumn -eq 'Counter' `
            -or $DTColumn -eq 'NestedCompCnt' `
            -or $DTColumn -eq 'NestedForeignPrinCnt' `
            -or $DTColumn -eq 'NestedGMSACnt' `
            -or $DTColumn -eq 'NestedGroupCnt' `
            -or $DTColumn -eq 'DirectUserCnt' `
            -or $DTColumn -eq 'NestedUserCnt' `
            -or $DTColumn -eq 'UniqueUserCnt')
            {
            $DTColTmp = New-Object System.data.datacolumn $DTColumn,([Int32])
            
            # Custom Int32 - auto incrementing value
            if ($DTColumn -eq 'Counter')
                {
                $DTColTmp.AutoIncrement = $True
                $DTColTmp.AutoIncrementSeed = 1
                $DTColTmp.AutoIncrementStep = 1
                }
            }
        elseif (($DTColumn -eq 'Enabled') -or `
                    ($DTColumn -eq 'PasswordExpired') -or `
                    ($DTColumn -eq 'CannotChangePassword') -or `
                    ($DTColumn -eq 'PasswordNeverExpires') -or `
                    ($DTColumn -eq 'PasswordNotRequired') -or `
                    ($DTColumn -eq 'UAC_PWNotRequired') -or `
                    ($DTColumn -eq 'UAC_PWCantChange') -or `
                    ($DTColumn -eq 'UAC_PWDoesNotExpire') -or `
                    ($DTColumn -eq 'UAC_TrustedForDelegation') -or `
                    ($DTColumn -eq 'UAC_UseDesKeyOnly') -or `
                    ($DTColumn -eq 'CannotChangePassword'))
            {$DTColTmp =  new-object Data.DataColumn $DTColumn,([Boolean])}
        
        elseif (($DTColumn -eq 'UserAccountControl') -or `
                    ($DTColumn -eq 'GroupCount') -or `
                    ($DTColumn -eq 'PrivGrpCnt'))
            {$DTColTmp =  new-object Data.DataColumn $DTColumn,([Int32])}

        elseif (($DTColumn -eq 'whenCreated') -or `
                    ($DTColumn -eq 'whenChanged'))
            {$DTColTmp =  new-object Data.DataColumn $DTColumn,([DateTime])}
        else
            {$DTColTmp = New-Object System.data.datacolumn $DTColumn,([string])}
        
        $ObjDTOut.Columns.Add($DTColTmp) 
        }

    # Returns DataTable
    Return ,$ObjDTOut
}

# Function Captures Forest Object
Function Get-ADForestObjects {
    Param ([Parameter(Mandatory=$False)] [string]$ForestNameIn)

    $StrError = "Unable to Locate Forest - $ForestNameIn `n`Exiting Script"

    #if forest variable is not specified, retrieve current forest
    If (!$ForestNameIn)     
        {
        Try {$ObjForestOut = Get-ADForest}
        Catch {Write-Warning $StrError}
        }
    else
        {
        Try {$ObjForestOut = Get-ADForest -Server $ForestNameIn}
        Catch {Write-Warning $StrError}
        }
    Return $ObjForestOut
}

# Function obtains user privileged group
Function Get-ADPrivUsers {
    [cmdletbinding()]
        Param([string]$ForestIn)
    
    # Clears Host
    Clear-Host

    # Captures Forest Object
    $ObjForest = Get-ADForestObjects -ForestName $ForestIn

        # Retrieves Attributes for Datatable
        $ArrDTCol = Define-Attributes -IntGrp 0

        # Builds Forest/Domain SIDS Datatable 
        $DT_DomainSIDs = Build-DataTable -StrDTTitleIn 'DomainSIDs' -ArrAttribsIn $ArrDTCol

        # Builds (populates) Forest/Domain SID Datatable
        Build-ForestDomSIDDT -ObjForestIn $ObjForest

        # Retrieves Attributes for Datatable    
        $ArrDTCol = Define-Attributes -IntGrp 1
    
        # Creates Forest Privileged Group Datatable 
        $DT_ForestPrivGrps = Build-DataTable -StrDTTitleIn 'ForestPrivGroups' -ArrAttribsIn $ArrDTCol
    
        # Builds (populates) Forest Privileged Group Datatable 
        Build-ForestPrivGroupDT -ObjForestIn $ObjForest

        # Build Data View for Querying
        $DV_ForestPrivGrps = New-Object System.Data.Dataview($DT_ForestPrivGrps)

    # Retrieves Attributes for Datatable
    $ArrDTCol = Define-Attributes -IntGrp 2

    # Builds User Datatable
    $DT_ADUserObjs = Build-DataTable -StrDTTitleIn "ForestUsers" -ArrAttribsIn $ArrDTCol

    #################################
    # GPO Full Report CSV File Name
        $CSVFileName = ("_" + $ObjForest.rootDomain + "_Forest_PrivUsers_" + $StrFileDateSuffix + ".csv").ToUpper()
        $Export = Join-Path $RootExportFolder $CSVFileName

    # Cycle Through Forest Privilaged Groups (i.e. Data Table)
    foreach ($ForestPrivGrp in $DV_ForestPrivGrps)
        {

        # Builds LDAP Query - Filters for:
        # - User Objects
        # - MemberOf attribute Contains Privileged Group DN
        $StrLDAPFilterOut   = "(&"
        $StrLDAPFilterOut  +=  "(objectClass=user)"
        $StrLDAPFilterOut  +=  "(objectclass=person)"
        $StrLDAPFilterOut  +=  "(Memberof=$($ForestPrivGrp.Grp_DN))"
        $StrLDAPFilterOut  += ")"

        if ($SubsetCount -eq 0)
            {$SubsetCount = $Null}

        # Create Temporary Array Variable - Forest Privileged Users
        $ArrADUserObjs = @()

        # Properties Array
        $TmpADUserAttribs = @()
        $TmpADUserAttribs += @('SID')
        $TmpADUserAttribs += @('DistinguishedName')

        # Try / Catch Statement
        Try {
            # Query 1 of 2 - Initial forest wide query (first pass to obtain limited user details)
            #  - Privileged Users' SID & DistinguishedName
            $ForestADUserObjs = Get-ADUser -LDAPFilter $StrLDAPFilterOut `
                                            -Properties $TmpADUserAttribs `
                                            -Server ($ForestPrivGrp.AD_DC + ":3268") `
                                            -ResultSetSize $SubsetCount -ErrorAction Stop `
                                                | Select-Object $TmpADUserAttribs
                                    
            }
        Catch 
            {# Throws Error
            Write-Host "Error Connecting to DC:" $ForestPrivGrp.AD_DC -ForegroundColor Red
            }

        # Select Array
        $GrpSelect = @()
        $GrpSelect += @{n='Grp_NestType'         ; e={$ForestPrivGrp.Grp_NestType} }
        $GrpSelect += @{n='Grp_Scope'            ; e={$ForestPrivGrp.Grp_Scope} }
        $GrpSelect += @{n='Grp_Name'             ; e={$ForestPrivGrp.Grp_Name} }
        $GrpSelect += @{n='AD_Forest'            ; e={$ForestPrivGrp.AD_Forest} }
        $GrpSelect += @{n='Grp_Parent_Domain'    ; e={$ForestPrivGrp.Grp_Parent_Domain} }
        $GrpSelect += @{n='Grp_Parent'           ; e={$ForestPrivGrp.Grp_Parent} }
        $GrpSelect += @{n='Grp_Child_Domain'     ; e={$ForestPrivGrp.Grp_Child_Domain} }
        $GrpSelect += @{n='Grp_Child'            ; e={$ForestPrivGrp.Grp_Child} }
        $GrpSelect += @{n='AD_DC'                ; e={$ForestPrivGrp.AD_DC} }
        $GrpSelect += @{n='AD_DomainDN'          ; e={$ForestPrivGrp.AD_DomainDN} }
        $GrpSelect += @{n='AD_DomainSID'         ; e={$ForestPrivGrp.AD_DomainSID} }
        $GrpSelect += @{n='Grp_DN'               ; e={$ForestPrivGrp.Grp_DN} }
        $GrpSelect += @{n='Grp_SID'              ; e={$ForestPrivGrp.Grp_SID} }
        $GrpSelect += @{n='Grp_AdminCount'       ; e={$ForestPrivGrp.Grp_AdminCount} }
        $GrpSelect += @{n='Grp_InheritanceBroken'; e={$ForestPrivGrp.InheritanceBroken} }
        $GrpSelect += $ADUserAttribs # appends Previously defined user attributes.

        # Creates Array Variable - Forest Privileged Users
        if ($ArrADUserObjsOut -isnot [object])
            {
            # Create new array variable
            $ArrADUserObjsOut = @()
            }

        # Try / Catch Statement
        foreach ($ObjUser in $ForestADUserObjs)
            {
            # Queryies Domain Datatable for User's home domain DC
            $PrefDC = ($DT_DomainSIDs.where({$ObjUser.SID.ToString() -match $_.DomainSID}).domaincontroller + ":389")
            
            # Builds LDAP Query - Filters for specific users
            $StrLDAPFilterOut = "(DistinguishedName=$($ObjUser.DistinguishedName))"

            # Query 2 of 2 - Domain query - Specific Privileged Users Details
            $ArrADUserObjsOut += @(Get-ADUser -LDAPFilter $StrLDAPFilterOut `
                                    -Properties $ADUserAttribs `
                                    -Server $PrefDC `
                                    -ResultSetSize $SubsetCount | `
                                        Select-Object $GrpSelect)
            }
        }
                                   
    # Checks if query returned objects
    if ($ArrADUserObjsOut -is [object])
        {
        # Cycles through AD User Objects
        $ArrADUserObjsOut | & `
                {
                process
                    {
                        # Clears Error buffer
                        $Global:Error.Clear()

                        # Generates User Class Object
                        [User]$MyUser = [User]::New($_)

                        # Queryies Domain Data Table for User Forest & Domain
                        $TmpUserForest = $DT_DomainSIDs.where({$MyUser.UserSID -match $_.DomainSID}).Forest
                        $TmpUserDomain = $DT_DomainSIDs.where({$MyUser.UserSID -match $_.DomainSID}).Domain

                        $UserScope = 'Local to Group'
                        if ($_.Grp_Child_Domain -eq 'n/a')
                            {if ($_.Grp_Parent_Domain -ne $TmpUserDomain) {$UserScope = 'Foreign to Group'}}
                        elseif ($_.Grp_Child_Domain -ne $TmpUserDomain) {$UserScope = 'Foreign to Group'}

                        # Creates new Data Table Row
                        $ObjDTRow = $DT_ADUserObjs.NewRow()

                        # Populates group details/values to Datatable Row
                        $ObjDTRow.ReportDate            = ($Date).ToShortDateString()
                        $ObjDTRow.Grp_NestType          = $_.Grp_NestType
                        $ObjDTRow.Grp_Scope             = $_.Grp_Scope
                        $ObjDTRow.Grp_Name              = $_.Grp_Name
                        $ObjDTRow.AD_Forest             = $_.AD_Forest
                        $ObjDTRow.Grp_Parent_Domain     = $_.Grp_Parent_Domain
                        $ObjDTRow.Grp_Parent            = $_.Grp_Parent
                        $ObjDTRow.Grp_Child_Domain      = $_.Grp_Child_Domain
                        $ObjDTRow.Grp_Child             = $_.Grp_Child 
                        $ObjDTRow.AD_DC                 = $_.AD_DC
                        $ObjDTRow.AD_DomainDN           = $_.AD_DomainDN
                        $ObjDTRow.AD_DomainSID          = $_.AD_DomainSID
                        $ObjDTRow.Grp_DN                = $_.Grp_DN
                        $ObjDTRow.Grp_SID               = $_.Grp_SID
                        $ObjDTRow.Grp_AdminCount        = $_.Grp_AdminCount
                        $ObjDTRow.Grp_InheritanceBroken = $_.Grp_InheritanceBroken

                        # Populates user details/values to Datatable Row
                        $ObjDTRow.User_Forest           = $TmpUserForest
                        $ObjDTRow.User_Domain           = $TmpUserDomain
                        $ObjDTRow.User_Scope            = $UserScope
                        $ObjDTRow.IsCriticalSystemObject= $MyUser.IsCriticalSystemObject
                        $ObjDTRow.SamAccountName        = $_.SamAccountName
                        $ObjDTRow.DisplayName           = $_.DisplayName
                        $ObjDTRow.Enabled               = $_.enabled
                        $ObjDTRow.Expired               = $MyUser.Expired
                        $ObjDTRow.ExpireStatus          = $MyUser.ExpireStatus
                        $ObjDTRow.ExpireDate            = $MyUser.ExpireDate

                        # Calculates Account Expiration Details
                        if ($_.AccountExpirationDate -is [object])
                            {
                            $ObjDTRow.ExpirationDate    = $_.AccountExpirationDate.ToShortDateString()
                            $ObjDTRow.ExpirationDays    = $MyUser.GetDateDiff($_.AccountExpirationDate, $Date)
                            }
                        else
                            {$ObjDTRow.ExpirationDate   = $Null ; $ObjDTRow.ExpirationDays  = $Null}

                        $ObjDTRow.AccountExpires   = $_.accountExpires
                        
                        # Calculates number of days ago the user logged on
                        if (![string]::isnullorempty($MyUser.lastLogonDate))
                            {$ObjDTRow.LastLogonDaysAgo = $MyUser.GetDateDiff($MyUser.lastLogonDate, $Date)}

                        $ObjDTRow.LastLogonTimestamp   = $MyUser.lastLogonTimestamp
                        $ObjDTRow.LastLogonDate        = $MyUser.lastLogonDate
                        $ObjDTRow.LogonCount           = $_.logonCount
                        $ObjDTRow.UserPrincipalName    = $_.UserPrincipalName
                        $ObjDTRow.CanonicalName        = $_.CanonicalName
                        $ObjDTRow.Department           = $_.Department
                        $ObjDTRow.Description          = $_.Description
                        $ObjDTRow.Division             = $_.Division
                        $ObjDTRow.ETypes               = $MyUser.ETypes
                        $ObjDTRow.EmailAddress         = $_.EmailAddress
                        $ObjDTRow.mail                 = $_.mail
                        $ObjDTRow.adminCount           = $_.adminCount
                        $ObjDTRow.adminDescription     = $_.adminDescription
                        $ObjDTRow.ProxySMTPAddr        = $MyUser.SMTPProxyAddr
                        $ObjDTRow.ProxySIPAddr         = $MyUser.SIPProxyAddr
                        $ObjDTRow.MemberOf             = $MyUser.MemberOf
                        $ObjDTRow.GroupCount           = $MyUser.GroupCount
                        
                        # Cycles through memberof - counts number of privileged groups
                        [Int32]$PrivGrpCnt = 0
                        $MyUser.ArrMemberof | foreach {
                                $DV_ForestPrivGrps.RowFilter = "Grp_DN = '$_'"
                                if ($DV_ForestPrivGrps.count -gt 0)
                                    {$PrivGrpCnt++}}

                        $ObjDTRow.PrivGrpCnt                = $PrivGrpCnt
                        $ObjDTRow.Organization              = $_.Organization
                        $ObjDTRow.pwdExpired                = $_.PasswordExpired
                        $ObjDTRow.pwdLastSet                = $_.PasswordLastSet
                        
                        if (![string]::isnullorempty($_.PasswordLastSet))
                            {$ObjDTRow.pwdLastSetDaysAgo    = $MyUser.GetDateDiff($_.PasswordLastSet, $Date)}

                        $ObjDTRow.pwdLastSet                = $_.pwdLastSet
                        $ObjDTRow.CannotChangePwd           = $_.CannotChangePassword
                        $ObjDTRow.pwdNotRequired            = $_.PasswordNotRequired
                        $ObjDTRow.pwdNeverExpires           = $_.PasswordNeverExpires
                        
                        $ObjDTRow.SIDHistory                = ($_.SIDHistory | Measure-Object).count
                        $ObjDTRow.UserAccountControl        = $_.UserAccountControl
                        
                        $ObjDTRow.UAC_PWNotRequired         = $MyUser.PWNotRequired
                        $ObjDTRow.UAC_PWCantChange          = $MyUser.PWCantChange
                        $ObjDTRow.UAC_PWDoesNotExpire       = $MyUser.PWDoesNotExpire
                        $ObjDTRow.UAC_TrustedForDelegation  = $MyUser.TrustedForDelegation
                        $ObjDTRow.UAC_UseDesKeyOnly         = $MyUser.UseDesKeyOnly
                        $ObjDTRow.whenChanged               = $_.whenChanged.toshortdatestring()
                        $ObjDTRow.whenCreatedDays           = $MyUser.GetDateDiff($_.whenCreated, $Date)
                        $ObjDTRow.whenCreated               = $_.whenCreated.toshortdatestring()
                        $ObjDTRow.DistinguishedName         = $_.DistinguishedName

                        $Stale = $Null
                        if (($ObjDTRow.Enabled) -and ($BolFilter))
                            {
                            if ([string]::IsNullOrEmpty($ObjDTRow.LastLogonDays))
                                {
                                if ([int32]($ObjDTRow.whenCreatedDays) -ge $Days)
                                    {$Stale = "Stale - Enabled & LastLogon Blank & WhenCreated > $Days Days"}
                                }
                            elseif ([Int32]($ObjDTRow.LastLogonDays) -ge $Days)
                                {$Stale = "Stale - Enabled & LastLogon > $Days Days"}
                            }
                                
                        $ObjDTRow.Stale                 = $Stale

                        # Hash Table
                        $TempdataOut = [ordered]@{}
                        $TempdataOut += @{'#'=([string]($ObjDTRow.Counter) + " of " + [string]($ArrADUserObjsOut.count))}
                        $TempdataOut += @{'Forest' = $DT_DomainSIDs.where({$MyUser.UserSID -match $_.DomainSID}).Forest}
                        $TempdataOut += @{'Domain' = $DT_DomainSIDs.where({$MyUser.UserSID -match $_.DomainSID}).Domain}
                        $TempdataOut += @{'Domain Conroller' = $DT_DomainSIDs.where({$MyUser.UserSID -match $_.DomainSID}).DomainController}
                        $TempdataOut += @{'SamAcctName' = $_.SamAccountName}
                        $TempdataOut += @{'User Scope' = $UserScope}

                        # Writes Hash Table to Screen Output
                        Write-Output (New-Object -TypeName PSObject -Property $TempdataOut)

                        $ObjDTRow | Export-Csv $Export -NoTypeInformation -Append
                        
                        $ObjDTRow = $Null
                        $MyUser = $Null
                        }
                }

        # Post Script: Writes Output to Screen
        $script:ArrOutputCSVs += @("Forest Users:$($ObjForest.Rootdomain) | Output CSV File: $Export")
        }
    
    # Nulls Datatables
    $DT_DomainSIDs = $Null
    $DT_ForestPrivGrps = $Null
    $DT_ADUserObjs = $Null
}

    # Creates Arrau Variable
    $Script:ArrOutputCSVs = @()

    # Cycles through Forest Array
    $Forests | Foreach {Get-ADPrivUsers $_}

    # Cycles through text output
    Foreach ($StrOutput in $ArrOutputCSVs)
        {
        Write-Host "-------------------------"
        Write-Host $StrOutput `n
        }

CORE PRACTICES

  • Implementing correct solutions
  • Bringing the correct talent (professional-staff-augmentation or project team)
  • Alignment to the business functional & functional direction
  • Maintaining agility with communication and options
  • Ensure to have a properly scoped project and accurate roadmap eliminating fluff

LATEST VIDEOS

Stay Well

Here are some incredibly-simple videos to watch & share with co-workers, family and friend on staying well: