ADAuditTasks.psm1

#Region '.\Classes\1.ADAuditTasksUser.ps1' 0
class ADAuditTasksUser {
    [string]$UserName
    [string]$FirstName
    [string]$LastName
    [string]$Name
    [string]$UPN
    [string]$LastSignIn
    [string]$Enabled
    [string]$LastSeen
    [string]$OrgUnit
    [string]$Title
    [string]$Manager
    [string]$Department
    [bool]$AccessRequired
    [bool]$NeedMailbox

    [string] ToString() {
        return "ADAuditTasksUser: UserName=$($this.UserName), FirstName=$($this.FirstName), LastName=$($this.LastName), Name=$($this.Name), UPN=$($this.UPN), LastSignIn=$($this.LastSignIn), Enabled=$($this.Enabled), LastSeen=$($this.LastSeen), OrgUnit=$($this.OrgUnit), Title=$($this.Title), Manager=$($this.Manager), Department=$($this.Department), AccessRequired=$($this.AccessRequired), NeedMailbox=$($this.NeedMailbox)"
    }
    ADAuditTasksUser() {
        $this.UserName = 'DefaultUser'
    }

    ADAuditTasksUser(
        [string]$UserName,
        [string]$FirstName,
        [string]$LastName,
        [string]$Name,
        [string]$UPN,
        [string]$LastSignIn,
        [string]$Enabled,
        [string]$LastSeen,
        [string]$OrgUnit,
        [string]$Title,
        [string]$Manager,
        [string]$Department,
        [bool]$AccessRequired,
        [bool]$NeedMailbox
    ) {
        $this.UserName = $UserName
        $this.FirstName = $FirstName
        $this.LastName = $LastName
        $this.Name = $Name
        $this.UPN = $UPN
        $this.LastSignIn = ([DateTime]::FromFileTime($LastSignIn))
        $this.Enabled = $Enabled
        $this.LastSeen = $(
            switch (([DateTime]::FromFileTime($LastSeen))) {
                { ($_ -lt (Get-Date).Adddays(-90)) } { '3+ months'; break }
                { ($_ -lt (Get-Date).Adddays(-60)) } { '2+ months'; break }
                { ($_ -lt (Get-Date).Adddays(-30)) } { '1+ month'; break }
                default { 'Recently' }
            }
        )
        $this.OrgUnit = $OrgUnit -replace '^.*?,(?=[A-Z]{2}=)'
        $this.Title = $Title
        $this.Manager = $(
            switch ($Manager) {
                { if ($_) { return $true } } { "$((Get-ADUser -Identity $Manager).Name)"; break }
                default { 'NotFound' }
            }
        )
        $this.AccessRequired = $AccessRequired
        $this.NeedMailbox = $NeedMailbox
        $this.Department = $Department
    }
}
#EndRegion '.\Classes\1.ADAuditTasksUser.ps1' 68
#Region '.\Classes\2.ADAuditTasksComputer.ps1' 0
class ADAuditTasksComputer {
    [string]$DNSHostName
    [string]$ComputerName
    [bool]$Enabled
    [string]$IPv4Address
    [string]$IPv6Address
    [string]$OperatingSystem
    [string]$LastLogon
    [string]$LastSeen
    [string]$Created
    [string]$Modified
    [string]$Description
    [string]$GroupMemberships
    [string]$OrgUnit
    [string]$KerberosEncryptionType
    [string]$SPNs
    # Default constructor
    ADAuditTasksComputer() {
        $this.ComputerName = 'DefaultComputer'
    }
    # Constructor 1
    ADAuditTasksComputer(
        [string]$DNSHostName,
        [string]$ComputerName,
        [bool]$Enabled,
        [string]$IPv4Address,
        [string]$IPv6Address,
        [string]$OperatingSystem,
        [long]$LastLogon,
        [long]$LastSeen,
        [string]$Created,
        [string]$Modified,
        [string]$Description,
        [string]$OrgUnit,
        [string]$KerberosEncryptionType,
        [string]$SPNs,
        [string]$GroupMemberships
    ) {
        #Begin Contructor 1
        $this.DNSHostName = $DNSHostName
        $this.ComputerName = $ComputerName
        $this.Enabled = $Enabled
        $this.IPv4Address = $IPv4Address
        $this.IPv6Address = $IPv6Address
        $this.OperatingSystem = $OperatingSystem
        $this.LastLogon = ([DateTime]::FromFileTime($LastLogon))
        $this.LastSeen = $(
            switch (([DateTime]::FromFileTime($LastSeen))) {
                # Over 90 Days
                { ($_ -lt (Get-Date).Adddays( - (90))) } { '3+ months'; break }
                # Over 60 Days
                { ($_ -lt (Get-Date).Adddays( - (60))) } { '2+ months'; break }
                # Over 30 Days
                { ($_ -lt (Get-Date).Adddays( - (30))) } { '1+ month'; break }
                default { 'Recently' }
            } # End Switch
        ) # End LastSeen
        $this.Created = $Created
        $this.Modified = $Modified
        $this.Description = $Description
        $this.GroupMemberships = $(
            switch ($GroupMemberships) {
                { if ($_) { return $true } } { $(Get-ADGroupMemberof -SamAccountName $GroupMemberships -AccountType ADComputer); break }
                default { 'GroupsNotFound' }
            }
        )
        $this.OrgUnit = $(($OrgUnit -replace '^.*?,(?=[A-Z]{2}=)') -replace ",", ">")
        $this.KerberosEncryptionType = $(($KerberosEncryptionType | Select-Object -ExpandProperty $_) -replace ", ", " | ")
        $this.SPNs = $SPNs
    }# End Constuctor 1
    # ToString() method override
    [string] ToString() {
        return "ADAuditTasksComputer: $($this.ComputerName), DNS Host Name: $($this.DNSHostName), Enabled: $($this.Enabled), IPv4 Address: $($this.IPv4Address), IPv6 Address: $($this.IPv6Address), Operating System: $($this.OperatingSystem), Last Logon: $($this.LastLogon), Last Seen: $($this.LastSeen), Created: $($this.Created), Modified: $($this.Modified), Description: $($this.Description), Group Memberships: $($this.GroupMemberships), Org Unit: $($this.OrgUnit), Kerberos Encryption Type: $($this.KerberosEncryptionType), SPNs: $($this.SPNs)"
    }
}
#EndRegion '.\Classes\2.ADAuditTasksComputer.ps1' 76
#Region '.\Private\Build-ADAuditTasksComputer.ps1' 0
<#
.SYNOPSIS
Builds ADAuditTasksComputer objects from Active Directory computer objects.
.DESCRIPTION
This function takes an array of Active Directory computer objects and creates
an array of ADAuditTasksComputer objects.
.PARAMETER ADComputers
An array of Active Directory computer objects.
.OUTPUTS
Returns an array of ADAuditTasksComputer objects.
.EXAMPLE
$ADComputers = Get-ADComputer -Filter {OperatingSystem -Like "Windows 10*"} -Properties *
$Export = Build-ADAuditTasksComputer -ADComputers $ADComputers
.NOTES
Author: DrIOSx
#>

function Build-ADAuditTasksComputer {
    param (
        [pscustomobject[]]$ADComputers
    )
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog "Begin ADAUditTasksComputer object creation."

    $Export = $ADComputers | ForEach-Object {
        [ADAuditTasksComputer]::new(
            $_.DNSHostName,
            $_.Name,
            $_.Enabled,
            $_.IPv4Address,
            $_.IPv6Address,
            $_.OperatingSystem,
            $_.lastLogonTimestamp,
            $_.lastLogonTimestamp,
            $_.Created,
            $_.whenChanged,
            $_.Description,
            $_.DistinguishedName,
            (($_.KerberosEncryptionType | Out-String) -replace "`n" -replace "`r"),
            ($_.servicePrincipalName -join " | "),
            $_.Name
        )
    } # End ForEach-Object

    Write-AuditLog "The ADAUditTasksComputer objects were built successfully."
    Write-AuditLog -EndFunction
    return $Export
}
#EndRegion '.\Private\Build-ADAuditTasksComputer.ps1' 53
#Region '.\Private\Build-ADAuditTasksUser.ps1' 0
<#
.SYNOPSIS
    Builds a list of custom objects containing Active Directory user data.
.DESCRIPTION
    This function builds a list of custom objects containing Active Directory
    user data, such as the user's name, last logon timestamp, and manager.
.PARAMETER ADExport
    An array of Microsoft.ActiveDirectory.Management.ADUser objects.
.OUTPUTS
    System.Collections.Generic.List[ADAuditTasksUser]
    A list of custom objects that contains Active Directory user data.
.EXAMPLE
    $adUsers = Get-ADUser -Filter * -Properties *
    $adAuditTasksUsers = Build-ADAuditTasksUser -ADExport $adUsers
    $adAuditTasksUsers
.NOTES
    Author: DrIOSx
#>


function Build-ADAuditTasksUser {
    param (
        [Microsoft.ActiveDirectory.Management.ADUser[]]$ADExport
    )
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog "Begin ADAUditTasksUser object creation."

    $Export = $ADExport | ForEach-Object {
        [ADAuditTasksUser]::new(
            $_.SamAccountName,
            $_.GivenName,
            $_.Surname,
            $_.Name,
            $_.UserPrincipalName,
            $_.LastLogonTimeStamp,
            $_.Enabled,
            $_.LastLogonTimeStamp,
            $_.DistinguishedName,
            $_.Title,
            $_.Manager,
            $_.Department,
            $false,
            $false
        )
    }

    Write-AuditLog "The ADAUditTasksUser object was built successfully."
    Write-auditlog -EndFunction
    return $Export
}
#EndRegion '.\Private\Build-ADAuditTasksUser.ps1' 55
#Region '.\Private\Build-MacIdOUIList.ps1' 0
<#
.SYNOPSIS
    Builds a list of MAC ID OUIs.
.DESCRIPTION
    This function builds a list of MAC ID OUIs. The function retrieves the OUI
    list from the IEEE Standards Association website or from a local CSV file.
.OUTPUTS
    System.Collections.Generic.List[System.Management.Automation.PSCustomObject]
    A list of custom objects that contains MAC ID OUIs.
.EXAMPLE
    $ouilist = Build-MacIdOUIList
    $ouilist
.NOTES
    Author: DrIOSx
#>


function Build-MacIdOUIList {
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog -Message "Retrieving MACID OUI list from https://standards-oui.ieee.org/oui/oui.csv"
    try {
        $ouiobject = Invoke-RestMethod https://standards-oui.ieee.org/oui/oui.csv | ConvertFrom-Csv -ErrorAction Stop
        Write-AuditLog -Message "Successfully downloaded the OUI list!"
        Write-AuditLog -EndFunction
        return $ouiobject
    }
    catch {
        Write-Warning "List not downloaded. Continuing with local MACID OUI list." -WarningAction Continue
        $ouiobject = Import-Csv source\assets\oui.csv
        write-auditlog -message "Successfully imported the local OUI list!"
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\Build-MacIdOUIList.ps1' 38
#Region '.\Private\Build-NetScanObject.ps1' 0
<#
.SYNOPSIS
    Builds a network scan object that includes information about each computer on the network.
.DESCRIPTION
    This function builds a network scan object that includes information about each computer
    on the network. The function takes a network scan object as input and returns a custom
    object with the following properties: ComputerName, IP/DNS, Ping, MacID, ManufacturerName,
    and PortsEnabled.
.PARAMETER NetScanObject
    The network scan object to use as input. The object should have the following properties:
    ComputerName, IP/DNS, and Ping.
.PARAMETER IncludeNoPing
    A switch parameter that specifies whether to include computers that did not respond to
    ping in the output.
.OUTPUTS
    System.Collections.Generic.List[System.Management.Automation.PSCustomObject]
    A list of custom objects that contain information about each computer on the network.
.EXAMPLE
    $NetScanObject = @(
        @{
            ComputerName = "computer1"
            "IP/DNS" = "192.168.1.1"
            Ping = $true
        },
        @{
            ComputerName = "computer2"
            "IP/DNS" = "192.168.1.2"
            Ping = $false
        }
    )
    $scan = Build-NetScanObject -NetScanObject $NetScanObject
    $scan
.NOTES
    Author: DrIOSx
#>

function Build-NetScanObject {
    param(
        $NetScanObject,
        [switch]$IncludeNoPing
    )
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    $ouiobject = Build-MacIdOUIList
    Write-AuditLog "Begin NetScan object creation."
    switch ($IncludeNoPing) {
        $true {
            $scan = $NetSCanObject
        }
        Default {
            $scan = $NetSCanObject | Where-Object { $_.Ping -eq $true }
        }
    }
    $Export = @()
    foreach ($Item in $scan) {
        $portsenabled = ($item.PSObject.Properties | Where-Object { $_.Value -eq $true -and $_.name -ne "Ping" }).Name -join " | "
        $portsenabled = $portsenabled.Replace("Port ", "")
        $SaveErrorPref = $Script:ErrorActionPreference
        $Script:ErrorActionPreference = 'SilentlyContinue'
        $macid = ((arp -a "$($item.ComputerName)" | Select-String '([0-9a-f]{2}-){5}[0-9a-f]{2}').Matches.Value).Replace("-", ":")
        $macpop = $macid.replace(":", "")
        $macsubstr = $macpop.Substring(0, 6)
        $org = ($ouiobject | Where-Object { $_.assignment -eq $macsubstr })."Organization Name"
        $Script:ErrorActionPreference = $SaveErrorPref
        if ($org) {
            [string]$ManufacturerName = $org
        }
        else {
            [string]$ManufacturerName = "NotFound"
        }
        $hash = [ordered]@{
            ComputerName     = $Item.ComputerName
            "IP/DNS"         = $Item."IP/DNS"
            Ping             = $Item.Ping
            MacID            = $macid
            ManufacturerName = $ManufacturerName
            PortsEnabled     = $portsenabled
        } # End Ordered Hash table
        New-Object -TypeName PSCustomObject -Property $hash -OutVariable PSObject | Out-Null
        $Export += $PSObject
    } # End foreach scan
    if ($Export) {
        Write-AuditLog "NetScan object created!"
        Write-AuditLog -EndFunction
        return $Export
    }
    else {
        throw "The ExportObject was Blank"
    }
}
#EndRegion '.\Private\Build-NetScanObject.ps1' 94
#Region '.\Private\Build-ReportArchive.ps1' 0
    <#
    .SYNOPSIS
    Exports data to a CSV file, archives the CSV file and a log file in a zip file, and returns the path to the zip file.
    .DESCRIPTION
    The Build-ReportArchive function exports data to a CSV file, archives the CSV file and a log file in a zip file,
    and returns the path to the zip file. The function takes four parameters: $Export (the data to export),
    $csv (the name of the CSV file to create), $zip (the name of the zip file to create), and $log
    (the name of the log file to create). The function writes information about the export and archive process
    to the log file, and any errors that occur are also logged.
    .PARAMETER AttachmentFolderPath
    Specifies the path to the directory where the CSV, zip, and log files will be created.
    .PARAMETER Export
    Specifies the data to export.
    .PARAMETER csv
    Specifies the name of the CSV file to create.
    .PARAMETER zip
    Specifies the name of the zip file to create.
    .PARAMETER log
    Specifies the name of the log file to create.
    .INPUTS
    The function accepts data as input from the pipeline.
    .OUTPUTS
    The function returns the path to the zip file that contains the archived CSV and log files.
    .EXAMPLE
    PS C:\> $Export = Get-ADUser -Filter *
    PS C:\> $CsvFile = "C:\Temp\ExportedData.csv"
    PS C:\> $ZipFile = "C:\Temp\ExportedData.zip"
    PS C:\> $LogFile = "C:\Temp\ExportedData.log"
    PS C:\> Build-ReportArchive -Export $Export -csv $CsvFile -zip $ZipFile -log $LogFile
 
    In this example, the Build-ReportArchive function is used to export all AD users to a CSV file,
    archive the CSV file and a log file in a zip file, and return the path to the zip file. The
    exported data is passed as input to the function using the $Export parameter, and the names
    of the CSV, zip, and log files are specified using the $csv, $zip, and $log parameters, respectively.
    .NOTES
    This function requires PowerShell 5.0 or later.
    .LINK
    https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.archive/compress-archive
    #>

function Build-ReportArchive {
    [OutputType([string[]])]
    [CmdletBinding()]
    # Define function parameters with help messages
    param (
        [Parameter(
            HelpMessage = 'Active Directory User Enabled or not. Default $true',
            Position = 0,
            ValueFromPipelineByPropertyName = $true
        )]$Export,
        [Parameter(
            HelpMessage = 'CSV File Name',
            Position = 1,
            ValueFromPipelineByPropertyName = $true
        )][string]$csv,
        [Parameter(
            HelpMessage = 'Zip File Name',
            Position = 2,
            ValueFromPipelineByPropertyName = $true
        )][string]$zip,
        [Parameter(
            HelpMessage = 'Log File Name',
            Position = 3,
            ValueFromPipelineByPropertyName = $true
        )][string]$log,
        [Parameter(
            HelpMessage = 'Attachment Folder Path',
            Position = 4,
            ValueFromPipelineByPropertyName = $true
        )][string]$AttachmentFolderPath
    )
    # Initialize variables
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        $ExportFile = $Export
    }
    # Process each object in the pipeline
    process {
        try {
            # Export data to CSV file
            $ExportFile | Export-Csv $csv -NoTypeInformation -Encoding utf8 -ErrorVariable ExportErr -ErrorAction Stop
        }
        catch {
            # Write error to log and re-throw error
            Write-AuditLog "Failed to export CSV: $csv" -Severity Error
            throw $ExportErr
        }
        # Get SHA-256 hash of the CSV file and write to log
        $Sha256Hash = (Get-FileHash $csv).Hash
        Write-AuditLog "Exported CSV SHA256 hash: "
        Write-AuditLog "$($Sha256Hash)"
        # Write information about the export directory and file path to log
        Write-AuditLog "Directory: $AttachmentFolderPath"
        Write-AuditLog "FilePath: $zip"
        Write-AuditLog "Archived CSV and log files to zip file: $zip"
        Write-AuditLog -EndFunction
        write-auditlog -End -OutputPath $log
        #$Script:LogString | Export-Csv $log -NoTypeInformation -Encoding utf8
    }
    # Clean up and archive files
    end {
        try {
            Compress-Archive -Path $csv, $log -DestinationPath $zip -CompressionLevel Optimal -ErrorAction Stop
            Remove-Item $csv, $log -Force
            return [string[]]$zip
        }
        catch {
            throw $_.Exception
        }
    }
} # End Function
#EndRegion '.\Private\Build-ReportArchive.ps1' 116
#Region '.\Private\Convert-ExcelCellName.ps1' 0
function Convert-ExcelCellName {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AllowNull()]
        [string]$CellName
    )

    begin {
        # Handle $null value
        if ($null -eq $CellName) {
            return $null
        }
    }

    process {
        # Trim leading and trailing whitespace
        $convertedName = $CellName.Trim()

        # Remove any invalid characters at the beginning of the name
        while ($convertedName -ne "" -and $convertedName[0] -notin [char[]]'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_') {
            $convertedName = $convertedName.Substring(1)
        }

        # Replace any invalid characters in the middle or at the end
        $convertedName = [Regex]::Replace($convertedName, '[^a-zA-Z0-9_]', '_')

        # Ensure length is no more than 255 characters
        if ($convertedName.Length -gt 255) {
            $convertedName = $convertedName.Substring(0, 255)
        }

        # Handle empty or invalid cell names
        if ([string]::IsNullOrEmpty($convertedName)) {
            throw "Invalid cell name or empty string."
        }

        $convertedName
    }
}
#EndRegion '.\Private\Convert-ExcelCellName.ps1' 41
#Region '.\Private\Get-AdExtendedRight.ps1' 0
Function Get-AdExtendedRight([Microsoft.ActiveDirectory.Management.ADObject] $ADObject) {
    # Initialize an empty array to store extended rights
    $ExportER = @()
    # Loop through each access control entry in the object's security descriptor
    Foreach ($Access in $ADObject.ntsecurityDescriptor.Access) {
        # Ignore deny permissions, well-known identities, and inherited permissions
        if ($Access.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny) { continue }
        if ($Access.IdentityReference -eq "NT AUTHORITY\SYSTEM") { continue }
        if ($Access.IdentityReference -eq "NT AUTHORITY\SELF") { continue }
        if ($Access.IsInherited) { continue }
        # Check if the access control entry grants an extended right
        if ($Access.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight) {
            # Initialize an empty string to store the name of the extended right
            $Right = ""
            # Map the object type of the access control entry to a name of a dangerous extended attribute
            # (see https://technet.microsoft.com/en-us/library/ff405676.aspx)
            switch ($Access.ObjectType) {
                "00299570-246d-11d0-a768-00aa006e0529" { $Right = "User-Force-Change-Password" }
                "45ec5156-db7e-47bb-b53f-dbeb2d03c40" { $Right = "Reanimate-Tombstones" }
                "bf9679c0-0de6-11d0-a285-00aa003049e2" { $Right = "Self-Membership" }
                "ba33815a-4f93-4c76-87f3-57574bff8109" { $Right = "Manage-SID-History" }
                "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" { $Right = "DS-Replication-Get-Changes-All" }
            }
            # If the access control entry grants a dangerous extended right, add it to the array
            if ($Right -ne "") {
                $Rights = [ordered]@{
                    Actor                   = $($Access.IdentityReference)
                    CanActOnThePermissionof = "$($ADObject.name)" + " " + "($($ADObject.DistinguishedName))"
                    WithExtendedRight       = $Right
                }
                $ExportER += New-Object -TypeName PSObject -Property $Rights
                #"$($Access.IdentityReference) can act on the permission of $($ADObject.name) ($($ADObject.DistinguishedName)) with extended right: $Right"
            }
        }
    }
    # Return the array of dangerous extended rights
    return $ExportER
} # End Function
#EndRegion '.\Private\Get-AdExtendedRight.ps1' 39
#Region '.\Private\Get-ADGroupMemberof.ps1' 0
    <#
    .SYNOPSIS
    Gets the names of the groups that a user or computer is a member of.
    .DESCRIPTION
    The Get-ADGroupMemberof function gets the names of the groups that a user or computer is a member of.
    The function takes two parameters: $SamAccountName (the name of the user or computer) and $AccountType
    (the type of account, either ADUser or ADComputer). The function uses a switch statement to determine
    whether to get the groups that a user or computer is a member of, and returns a string containing the
    names of the groups.
    .PARAMETER SamAccountName
    Specifies the name of the user or computer to get the group membership for.
    .PARAMETER AccountType
    Specifies the type of account, either ADUser or ADComputer. The default value is ADUser.
    .OUTPUTS
    The function returns a string containing the names of the groups that the specified user or computer is a member of.
    .EXAMPLE
    PS C:\> Get-ADGroupMemberof -SamAccountName "jdoe" -AccountType "ADUser"
    In this example, the Get-ADGroupMemberof function is used to get the names of the groups that the user "jdoe" is a
    member of. The type of account is specified using the $AccountType parameter.
    .NOTES
    This function requires the ActiveDirectory PowerShell module.
    .LINK
    https://docs.microsoft.com/en-us/powershell/module/activedirectory/
    #>

function Get-ADGroupMemberof {
    [CmdletBinding()]
    # Define function parameters
    param (
        [string]$SamAccountName,
        [ValidateSet("ADUser", "ADComputer")]
        [string]$AccountType = "ADUser"
    )
    # Process the account name and type
    process {
        switch ($AccountType) {
            "ADComputer" {
                # Get the groups that the computer is a member of
                $GroupStringArray = ((Get-ADComputer -Identity $SamAccountName -Properties memberof).memberof | Get-ADGroup | Select-Object name | Sort-Object name).name
                $GroupString = $GroupStringArray -join " | "
            }
            Default {
                # Get the groups that the user is a member of
                $GroupStringArray = ((Get-ADUser -Identity $SamAccountName -Properties memberof).memberof | Get-ADGroup | Select-Object name | Sort-Object name).name
                $GroupString = $GroupStringArray -join " | "
            }
        }
        # Return a string containing the names of the groups
        return $GroupString
    }
} # End Function
#EndRegion '.\Private\Get-ADGroupMemberof.ps1' 51
#Region '.\Private\Get-MethodDefinition.ps1' 0
function Get-MethodDefinition {
    param(
        [Parameter(Mandatory=$true)]
        [PSObject] $Object,

        [Parameter(Mandatory=$true)]
        [string] $MethodName
    )

    $methodOverloads = $Object.GetType().GetMethods() | Where-Object {$_.Name -eq $MethodName}

    $methodOverloads | ForEach-Object {
        $parameters = ($_.GetParameters() | ForEach-Object { $_.ParameterType.Name + " " + $_.Name }) -join ', '
        $returnType = $_.ReturnType.Name
        "$returnType $MethodName($parameters)"
    }
}
#EndRegion '.\Private\Get-MethodDefinition.ps1' 18
#Region '.\Private\Get-ValidFileName.ps1' 0
function Get-ValidFileName {
    param (
        [Parameter(Mandatory = $true)]
        [string]$String,

        [Parameter(
            HelpMessage = "Specify the character used to replace invalid characters. Default: '_' ",
            Mandatory = $false
        )]
        [ValidateSet('_', '-', '.', ' ')]
        [string]$ReplacementCharacter = '_'
    )

    if ([string]::IsNullOrEmpty($ReplacementCharacter)) {
        throw "Replacement character cannot be empty."
    }

    $illegalChars = [IO.Path]::GetInvalidFileNameChars() -join ''
    $regex = "[{0}]" -f [regex]::Escape($illegalChars)

    $String -replace $regex, $ReplacementCharacter
}
#EndRegion '.\Private\Get-ValidFileName.ps1' 23
#Region '.\Private\Initialize-DirectoryPath.ps1' 0
function Initialize-DirectoryPath {
    <#
.SYNOPSIS
Initializes one or more directory paths if they do not already exist.
.DESCRIPTION
The `Initialize-DirectoryPath` function checks if the specified directory
paths exist. If a path does not exist, the function will create the directory.
If a directory already exists, no action is taken.
.PARAMETER DirectoryPath
The `DirectoryPath` parameter specifies an array of directory paths to be checked
and created if they do not already exist.
.EXAMPLE
Initialize-DirectoryPath -DirectoryPath "C:\Output"
 
This example checks if the "C:\Output" directory exists. If it does not exist,
the function creates the directory. If the directory already exists, no action
is taken.
.EXAMPLE
Initialize-DirectoryPath -DirectoryPath "C:\Output1", "C:\Output2"
 
This example checks if the "C:\Output1" and "C:\Output2" directories exist. If a directory
does not exist, the function creates it. If a directory already exists, no action
is taken.
.NOTES
This function is not visible outside of the module.
.NOTES
Author: DrIOSx
Date: 15-Apr-2023
#>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true
        )]
        [string[]]$DirectoryPath
    )
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Write-AuditLog "Testing $($DirectoryPath.Count) directory path/s:"
    }
    process {
        $processedPaths = @()
        foreach ($Path in $DirectoryPath) {
            $AttachmentFolderPathCheck = Test-Path -Path $Path
            If (!($AttachmentFolderPathCheck)) {
                Try {
                    # If not present then create the dir
                    New-Item -ItemType Directory $Path -Force -ErrorAction Stop | Out-Null
                    Write-AuditLog "The following directory did not exist and will be created: "
                    Write-AuditLog "$($Path)"
                }
                Catch {
                    Write-AuditLog -Message "Directory was not created: $Path" -Severity Error
                    Write-AuditLog "End Log"
                    throw $_.Exception
                }
            }
            $processedPaths += $Path
        }
        Write-AuditLog "Processed directories:"
        $processedPaths | ForEach-Object { Write-AuditLog $_ }
    }
    end {
        Write-AuditLog "Finished testing path/s."
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\Initialize-DirectoryPath.ps1' 75
#Region '.\Private\Initialize-ModuleEnv.ps1' 0
function Initialize-ModuleEnv {
<#
    .SYNOPSIS
    Initializes the environment by installing required PowerShell modules.
    .DESCRIPTION
    This function installs PowerShell modules required by the script. It can install public or pre-release versions of the module, and it supports installation for all users or current user.
    .PARAMETER PublicModuleNames
    An array of module names to be installed. Required when using the Public parameter set.
    .PARAMETER PublicRequiredVersions
    An array of required module versions to be installed. Required when using the Public parameter set.
    .PARAMETER PrereleaseModuleNames
    An array of pre-release module names to be installed. Required when using the Prerelease parameter set.
    .PARAMETER PrereleaseRequiredVersions
    An array of required pre-release module versions to be installed. Required when using the Prerelease parameter set.
    .PARAMETER Scope
    The scope of the module installation. Possible values are "AllUsers" and "CurrentUser". This determines the installation scope of the module.
    .PARAMETER ImportModuleNames
    The specific modules you'd like to import from the installed package to streamline imports. This is used when you want to import only specific modules from a package, rather than all of them.
    .EXAMPLE
    Initialize-ModuleEnv -PublicModuleNames "PSnmap", "Microsoft.Graph" -PublicRequiredVersions "1.3.1","1.23.0" -Scope AllUsers
 
    This example installs the PSnmap and Microsoft.Graph modules in the AllUsers scope with the specified versions.
    .EXAMPLE
    $params1 = @{
        PublicModuleNames = "PSnmap","Microsoft.Graph"
        PublicRequiredVersions = "1.3.1","1.23.0"
        ImportModuleNames = "Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.SignIns"
        Scope = "CurrentUser"
    }
    Initialize-ModuleEnv @params1
 
    This example installs Microsoft.Graph and Pester Modules in the CurrentUser scope with the specified versions.
    It will attempt to only import Microsoft.Graph Modules matching the names in the "ImportModulesNames" array.
    .EXAMPLE
    $params2 = @{
        PrereleaseModuleNames = "Sampler", "Pester"
        PrereleaseRequiredVersions = "2.1.5", "4.10.1"
        Scope = "CurrentUser"
    }
    Initialize-ModuleEnv @params2
    This example installs the PreRelease Sampler and Pester Modules in the CurrentUser scope with the specified versions.
    Double check https://www.powershellgallery.com/packages/<ModuleName>/<ModuleVersionNumber>
    to verify if the "-PreRelease" switch is needed.
    .INPUTS
    None
    .OUTPUTS
    None
    .NOTES
    Author: DrIOSx
    This function makes extensive use of the Write-AuditLog function for logging actions, warnings, and errors. It also uses a script-scope variable $script:VerbosePreference for controlling verbose output.
#>

    [CmdletBinding(DefaultParameterSetName = "Public")]
    param (
        [Parameter(ParameterSetName = "Public", Mandatory)]
        [string[]]$PublicModuleNames,
        [Parameter(ParameterSetName = "Public", Mandatory)]
        [string[]]$PublicRequiredVersions,
        [Parameter(ParameterSetName = "Prerelease", Mandatory)]
        [string[]]$PrereleaseModuleNames,
        [Parameter(ParameterSetName = "Prerelease", Mandatory)]
        [string[]]$PrereleaseRequiredVersions,
        [ValidateSet(
            "AllUsers",
            "CurrentUser"
        )]
        [string]$Scope,
        [string[]]$ImportModuleNames = $null
    )
    # Start logging function execution
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    # Function limit needs to be set higher if installing graph module and if powershell is version 5.1.
    # The Microsoft.Graph module requires an increased function limit.
    # If we're installing this module, set the function limit to 8192.
    if ($PublicModuleNames -match 'Microsoft.Graph' -or $PrereleaseModuleNames -match "Microsoft.Graph") {
        if ($script:MaximumFunctionCount -lt 8192) {
            $script:MaximumFunctionCount = 8192
        }
    }
    # Check and install PowerShellGet.
    # PowerShellGet is required for module management in PowerShell.
    ### https://learn.microsoft.com/en-us/powershell/scripting/gallery/installing-psget?view=powershell-7.3
    # Get all available versions of PowerShellGet
    $PSGetVer = Get-Module -Name PowerShellGet -ListAvailable

    # Initialize flag to false
    $notOneFlag = $false

    # For each module version
    foreach ($module in $PSGetVer) {
        # Check if version is different from "1.0.0.1"
        if ($module.Version -ne "1.0.0.1") {
            $notOneFlag = $true
            break
        }
    }

    # If any version is different from "1.0.0.1", import the latest one
    if ($notOneFlag) {
        # Sort by version in descending order and select the first one (the latest)
        $latestModule = $PSGetVer | Sort-Object Version -Descending | Select-Object -First 1
        # Import the latest version
        Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version
    }
    else {
        switch (Test-IsAdmin) {
            $false {
                Write-AuditLog "PowerShellGet is version 1.0.0.1. Please run this once as an administrator, to update PowershellGet." -Severity Error
                throw "Elevation required to update PowerShellGet!"
            }
            Default {
                Write-AuditLog "You have sufficient privileges to install to the PowershellGet"
            }
        }
        try {
            Write-AuditLog "Install the latest version of PowershellGet from the PSGallery?" -Severity Warning
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
            Install-Module PowerShellGet -AllowClobber -Force -ErrorAction Stop
            Write-AuditLog "PowerShellGet was installed successfully!"
            $PSGetVer = Get-Module -Name PowerShellGet -ListAvailable
            $latestModule = $PSGetVer | Sort-Object Version -Descending | Select-Object -First 1
            Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop
        }
        catch {
            throw $_.Exception
        }
    }
    # End Region PowershellGet Install
    if ($Scope -eq "AllUsers") {
        switch (Test-IsAdmin) {
            $false {
                Write-AuditLog "You must be an administrator to install in the `'AllUsers`' scope." -Severity Error
                Write-AuditLog "If you intended to install the module only for this user, select the `'CurrentUser`' scope." -Severity Error
                throw "Elevation required for `'AllUsers`' scope"
            }
            Default {
                Write-AuditLog "You have sufficient privileges to install to the `'AllUsers`' scope."
            }
        }
    }
    if ($PSCmdlet.ParameterSetName -eq "Public") {
        $modules = $PublicModuleNames
        $versions = $PublicRequiredVersions
    }
    elseif ($PSCmdlet.ParameterSetName -eq "Prerelease") {
        $modules = $PrereleaseModuleNames
        $versions = $PrereleaseRequiredVersions
        $prerelease = $true
    }
    foreach ($module in $modules) {
        $name = $module
        $version = $versions[$modules.IndexOf($module)]
        $installedModule = Get-Module -Name $name -ListAvailable
        switch (($null -eq $ImportModuleNames)) {
            $false {
                $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name }
                Write-AuditLog "Attempting to selecively install module/s:"
            }
            Default {
                $SelectiveImports = $null
                Write-AuditLog "Selective imports were not specified. All functions and commands will be imported."
            }
        }
        # Get Module Object
        switch ($prerelease) {
            $true {
                $message = "The PreRelease module $name version $version is not installed. Would you like to install it?"
                $throwmsg = "You must install the PreRelease module $name version $version to continue"
            }
            Default {
                $message = "The $name module version $version is not installed. Would you like to install it?"
                $throwmsg = "You must install the $name module version $version to continue."
            }
        }
        if (!$installedModule) {
            # Install Required Module
            Write-AuditLog $message -Severity Warning
            try {
                Write-AuditLog "Installing $name module/s version $version -AllowPrerelease:$prerelease."
                $SaveVerbosePreference = $script:VerbosePreference
                Install-Module $name -Scope $Scope -RequiredVersion $version -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false
                $script:VerbosePreference = $SaveVerbosePreference
                Write-AuditLog "$name module successfully installed!"
                if ($SelectiveImports) {
                    foreach ($Mod in $SelectiveImports) {
                        $name = $Mod
                        Write-AuditLog "Selectively importing the $name module."
                        $SaveVerbosePreference = $script:VerbosePreference
                        Import-Module $name -ErrorAction Stop -Verbose:$false
                        $script:VerbosePreference = $SaveVerbosePreference
                        Write-AuditLog "Successfully imported the $name module."
                    }
                }
                else {
                    Write-AuditLog "Importing the $name module."
                    $SaveVerbosePreference = $script:VerbosePreference
                    Import-Module $name -ErrorAction Stop -Verbose:$false
                    $script:VerbosePreference = $SaveVerbosePreference
                    Write-AuditLog "Successfully imported the $name module."
                }
            }
            catch {
                Write-AuditLog $throwmsg -Severity Error
                throw $_.Exception
            }
        }
        else {
            try {
                if ($SelectiveImports) {
                    foreach ($Mod in $SelectiveImports) {
                        $name = $Mod
                        Write-AuditLog "The $name module was found to be installed."
                        Write-AuditLog "Selectively importing the $name module."
                        $SaveVerbosePreference = $script:VerbosePreference
                        Import-Module $name -ErrorAction Stop -Verbose:$false
                        $script:VerbosePreference = $SaveVerbosePreference
                        Write-AuditLog "Successfully imported the $name module."
                        Write-AuditLog -EndFunction
                    }
                }
                else {
                    Write-AuditLog "The $name module was found to be installed."
                    Write-AuditLog "Importing the $name module."
                    $SaveVerbosePreference = $script:VerbosePreference
                    Import-Module $name -ErrorAction Stop -Verbose:$false
                    $script:VerbosePreference = $SaveVerbosePreference
                    Write-AuditLog "Successfully imported the $name module."
                    write-auditlog -EndFunction
                }
            }
            catch {
                Write-AuditLog $throwmsg -Severity Error
                throw $_.Exception
            }
        }
    }
}
#EndRegion '.\Private\Initialize-ModuleEnv.ps1' 242
#Region '.\Private\Install-ADModule.ps1' 0
<#
.SYNOPSIS
    Installs the Active Directory module on a Windows computer.
.DESCRIPTION
    This function installs the Active Directory module on a Windows computer.
    The appropriate installation method is determined based on the operating
    system version and build number.
.NOTES
    The function requires elevation to install the Active Directory module.
.EXAMPLE
    Install-ADModule
.INPUTS
    None.
.OUTPUTS
    None.
.NOTES
    Author: DrIOSx
#>

function Install-ADModule {
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    # Setup Variables
    $SaveVerbosePreference = $script:VerbosePreference
    $script:VerbosePreference = 'SilentlyContinue'
    Get-CimInstance -Class Win32_OperatingSystem -ErrorAction Stop -OutVariable OS -Verbose:$false | Out-Null
    $script:VerbosePreference = $SaveVerbosePreference
    $OSName = ($OS).Name.Split('|')[0]
    $OSBuildNumber = $($OS.BuildNumber)
    $OSVersion = $($OS.Version)
    try {
        $SaveVerbosePreference = $script:VerbosePreference
        Import-Module ActiveDirectory -ErrorAction Stop -Verbose:$false
        $script:VerbosePreference = $SaveVerbosePreference

        Write-AuditLog "The ActiveDirectory Module was successfully imported."
        Write-AuditLog "OS: $OSName Build: $OSBuildNumber, Version: $OSVersion"
    }
    catch {
        if (!(Test-IsAdmin)) {
            Write-AuditLog "You must be run the script as an administrator to install ActiveDirectory module!"
            Write-AuditLog "Once you've installed the module, susequent runs will not need elevation!"
            throw "Installation requires elevation."
        }
        if (($OSBuildNumber -lt 17763) -and ($OSName -notmatch "Windows Server") ) {
            # Exit Function if windows version is less than Windows 10 October 2018 (1809)
            Write-AuditLog "Get installation instructions and download Remote Server Administration Tools (RSAT):"
            Write-AuditLog "https://www.microsoft.com/en-us/download/details.aspx?id=45520"
            throw "Install the appropriate RSAT module for $OSName Build: $OSBuildNumber, Version: $OSVersion."
        }
        # Write-AuditLog Warning (-WarningAction Inquire)
        Write-AuditLog "The ActiveDirectory module is not installed, would you like attempt to install it?" -Severity Warning
        try {
            Write-AuditLog "Potentially compatible OS: $OSName Build: $OSBuildNumber, Version: $OSVersion."
            Write-AuditLog "Installing ActiveDirectory Module."
            # Run the command to install AD module based on OS
            if ($OSName -match "Windows Server") {
                # If Windows Server
                Write-AuditLog "OS matched `"Windows Server`"."
                Write-AuditLog "Importing ServerManager Module."
                Import-Module ServerManager -ErrorAction Stop
                Write-AuditLog "Using Install-WindowsFeature RSAT-AD-PowerShell -IncludeAllSubFeature to install ActiveDirectory Module."
                Install-WindowsFeature RSAT-AD-PowerShell -IncludeAllSubFeature -ErrorAction Stop
            }
            else {
                # If Windows Client
                Write-AuditLog "OperatingSystem: $OSName is not like `"Windows Server`" and"
                Write-AuditLog "OSBuild: $OSBuildNumber is greater than 17763 (Windows 10 October 2018 (1809) Update)."
                Write-AuditLog "Retrieving RSAT.ActiveDirectory Feature using Get-WindowsCapability -Online"
                Get-WindowsCapability -Online | `
                    Where-Object { $_.Name -like "Rsat.ActiveDirectory*" } -ErrorAction Stop -OutVariable ADRSATModule | Out-Null
                $RSATModuleName = $($ADRSATModule.Name)
                Write-AuditLog "Installing $RSATModuleName features."
                Add-WindowsCapability -Online -Name $RSATModuleName -ErrorAction Stop
            }
        }
        catch {
            Write-AuditLog "The ActiveDirectory module failed to install."
            throw $_.Exception
        } # End Region try/catch ActiveDirectory import
        finally {
            try {
                Write-AuditLog "Attempting to import the ActiveDirectory module."
                $SaveVerbosePreference = $script:VerbosePreference
                Import-Module ActiveDirectory -ErrorAction Stop -Verbose:$false
                $script:VerbosePreference = $SaveVerbosePreference
                Write-AuditLog "The ActiveDirectory module was imported!"
                Write-AuditLog -EndFunction
            }
            catch {
                Write-AuditLog "The ActiveDirectory module failed to import."
                throw $_.Exception
            }
        }
    } # End Import Catch
}
#EndRegion '.\Private\Install-ADModule.ps1' 100
#Region '.\Private\Request-DedupedObject.ps1' 0
function Request-DedupedObject {
    <#
    .SYNOPSIS
        Returns a deduplicated version of a CSV object based on
        a specified suspect property and filter property.
    .DESCRIPTION
        The `Request-DedupedObject` function takes in three parameters: the suspect property, the filter property, and a CSV object,
        and returns a deduplicated version of the CSV object. The function groups the CSV object by the suspect property,
        sorts each group by the filter property in descending order, and selects the first item from each group.
    .PARAMETER DupedPropertySuspect
        Specifies the name of the property to group the CSV object by. This parameter is required.
    .PARAMETER FilterProperty
        Specifies the name of the property to sort each group by. This parameter is required.
    .PARAMETER csv
        Specifies the CSV object to deduplicate. This parameter is required.
    .INPUTS
        DupedPropertySuspect: Specifies the name of the property to group the CSV object by.
        FilterProperty: Specifies the name of the property to sort each group by.
        csv: Specifies the CSV object to deduplicate.
    .OUTPUTS
        A deduplicated version of the CSV object.
    .EXAMPLE
        $csv = Import-Csv -Path C:\data.csv
        $deduplicated = Request-DedupedObject -DupedPropertySuspect "Name" -FilterProperty "Date" -csv $csv
        $deduplicated | Export-Csv -Path C:\deduplicated_data.csv -NoTypeInformation
 
        This example imports a CSV file, deduplicates it based on the "Name" property and the "Date" property, and exports the deduplicated data to a new CSV file.
    .NOTES
        Author: DrIOSx
        Date: 4/12/2023
        Version: 0.1.0
    #>

    [OutputType([PSObject[]])]
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true
        )]
        [AllowEmptyCollection()]
        [PSObject[]]$csv,
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$FilterProperty,
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$DupedPropertySuspect
    )
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Write-AuditLog "Begin deduplication for $DupedPropertySuspect based on datetime filter $FilterProperty."
        if ($csv.Count -eq 0) {
            return [PSObject[]]@()
        }
        $sampleOutput = @()
        $random = New-Object Random
    }
    process {
        $grouped = $csv | Group-Object -Property $DupedPropertySuspect
        $progressCount = 0
        $startTime = Get-Date
        $deduped = foreach ($group in $grouped) {
            $progressCount++
            $elapsedTime = (Get-Date) - $startTime
            $timePerGroup = $elapsedTime.TotalSeconds / $progressCount
            $estimatedTimeRemaining = ($grouped.Count - $progressCount) * $timePerGroup

            # Update progress every 60 groups
            if ($progressCount % 60 -eq 0) {
                Write-Progress -Activity "Deduplicating CSV" -Status "Processing groups" -PercentComplete (($progressCount / $grouped.Count) * 100) -SecondsRemaining $estimatedTimeRemaining
            }

            if ($group.Count -eq 1) {
                $group.Group
            }
            else {
                $selectedRow = $group.Group | Sort-Object -Property $FilterProperty -Descending | Select-Object -First 1

                # Randomly select 5 samples
                if ($sampleOutput.Count -lt 5 -and ($random.Next(1, 100) -le 20)) {
                    $sample = New-Object PSObject -Property @{
                        Name     = $group.Name
                        Oldest   = $group.Group | Sort-Object $FilterProperty | Select-Object -First 1
                        Latest   = $group.Group | Sort-Object $FilterProperty -Descending | Select-Object -First 1
                        Selected = $selectedRow
                    }
                    $sampleOutput += $sample
                }
                $selectedRow
            }
        }
    }
    end {
        Write-AuditLog "##### Random Sample Comparisons #####"
        foreach ($sample in $sampleOutput) {
            Write-AuditLog "Name: $($sample.Name)"
            Write-AuditLog "Oldest: $($sample.Oldest.$FilterProperty)"
            Write-AuditLog "Latest: $($sample.Latest.$FilterProperty)"
            Write-AuditLog "Selected: $($sample.Selected.$FilterProperty)"
            Write-AuditLog "-----------------------------------"
        }
        Write-AuditLog "End deduplication for `"$DupedPropertySuspect`" based on datetime filter `"$FilterProperty`"."
        Write-AuditLog -EndFunction
        return $deduped
    }
}
#EndRegion '.\Private\Request-DedupedObject.ps1' 116
#Region '.\Private\Test-IsAdmin.ps1' 0
function Test-IsAdmin {
    <#
    .SYNOPSIS
    Checks if the current user is an administrator on the machine.
    .DESCRIPTION
    This private function returns a Boolean value indicating whether
    the current user has administrator privileges on the machine.
    It does this by creating a new WindowsPrincipal object, passing
    in a WindowsIdentity object representing the current user, and
    then checking if that principal is in the Administrator role.
    .INPUTS
    None.
    .OUTPUTS
    Boolean. Returns True if the current user is an administrator, and False otherwise.
    .EXAMPLE
    PS C:\> Test-IsAdmin
    True
    #>


    # Create a new WindowsPrincipal object for the current user and check if it is in the Administrator role
    (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
#EndRegion '.\Private\Test-IsAdmin.ps1' 23
#Region '.\Private\Test-Private.ps1' 0
function Test-Private
{
    <#
      .SYNOPSIS
      This is a sample Private function only visible within the module.
 
      .DESCRIPTION
      This sample function is not exported to the module and only return the data passed as parameter.
 
      .EXAMPLE
      $null = Test-Private -PrivateData 'NOTHING TO SEE HERE'
 
      .PARAMETER PrivateData
      The PrivateData parameter is what will be returned without transformation.
 
      #>

    [cmdletBinding()]
    [OutputType([string])]
    param
    (
        [Parameter()]
        [String]
        $PrivateData
    )

    process
    {
        Write-Output $PrivateData
    }

}

#EndRegion '.\Private\Test-Private.ps1' 33
#Region '.\Private\Write-AuditLog.ps1' 0
function Write-AuditLog {
    <#
    .SYNOPSIS
        Writes log messages to the console and updates the script-wide log variable.
    .DESCRIPTION
        The Write-AuditLog function writes log messages to the console based on the severity (Verbose, Warning, or Error) and updates
        the script-wide log variable ($script:LogString) with the log entry. You can use the Start, End, and EndFunction switches to
        manage the lifecycle of the logging.
    .INPUTS
        System.String
        You can pipe a string to the Write-AuditLog function as the Message parameter.
        You can also pipe an object with a Severity property as the Severity parameter.
    .OUTPUTS
        None
        The Write-AuditLog function doesn't output any objects to the pipeline. It writes messages to the console and updates the
        script-wide log variable ($script:LogString).
    .PARAMETER BeginFunction
        Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .PARAMETER Message
        The message string to log.
    .PARAMETER Severity
        The severity of the log message. Accepted values are 'Information', 'Warning', and 'Error'. Defaults to 'Information'.
    .PARAMETER Start
        Initializes the script-wide log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function.
    .PARAMETER End
        Sets the message to "End Log" and exports the log to a CSV file if the OutputPath parameter is provided.
    .PARAMETER EndFunction
        Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .PARAMETER OutputPath
        The file path for exporting the log to a CSV file when using the End switch.
    .EXAMPLE
        Write-AuditLog -Message "This is a test message."
 
        Writes a test message with the default severity (Information) to the console and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -Message "This is a warning message." -Severity "Warning"
 
        Writes a warning message to the console and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -Start
 
        Initializes the log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function.
    .EXAMPLE
        Write-AuditLog -BeginFunction
 
        Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -EndFunction
 
        Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -End -OutputPath "C:\Logs\auditlog.csv"
 
        Sets the message to "End Log", adds it to the log variable, and exports the log to a CSV file.
    .NOTES
    Author: DrIOSx
#>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        ###
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Input a Message string.',
            Position = 0,
            ParameterSetName = 'Default',
            ValueFromPipeline = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Message,
        ###
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Information, Warning or Error.',
            Position = 1,
            ParameterSetName = 'Default',
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Information', 'Warning', 'Error')]
        [string]$Severity = 'Information',
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'End'
        )]
        [switch]$End,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'BeginFunction'
        )]
        [switch]$BeginFunction,
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'EndFunction'
        )]
        [switch]$EndFunction,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Start'
        )]
        [switch]$Start,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'End'
        )]
        [string]$OutputPath
    )
    begin {
        $ErrorActionPreference = "SilentlyContinue"
        # Define variables to hold information about the command that was invoked.
        $ModuleName = $Script:MyInvocation.MyCommand.Name -replace '\..*'
        $FuncName = (Get-PSCallStack)[1].Command
        $ModuleVer = $MyInvocation.MyCommand.Version.ToString()
        # Set the error action preference to continue.
        $ErrorActionPreference = "Continue"
    }
    process {
        try {
            $Function = $($FuncName + '.v' + $ModuleVer)
            if ($Start) {
                $script:LogString = @()
                $Message = '+++ Begin Log | ' + $Function + ' |'
            }
            elseif ($BeginFunction) {
                $Message = '>>> Begin Function Log | ' + $Function + ' |'
            }
            $logEntry = [pscustomobject]@{
                Time      = ((Get-Date).ToString('yyyy-MM-dd hh:mmTss'))
                Module    = $ModuleName
                PSVersion = ($PSVersionTable.PSVersion).ToString()
                PSEdition = ($PSVersionTable.PSEdition).ToString()
                IsAdmin   = $(Test-IsAdmin)
                User      = "$Env:USERDOMAIN\$Env:USERNAME"
                HostName  = $Env:COMPUTERNAME
                InvokedBy = $Function
                Severity  = $Severity
                Message   = $Message
                RunID     = -1
            }
            if ($BeginFunction) {
                $maxRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Measure-Object -Property RunID -Maximum).Maximum
                if ($null -eq $maxRunID) { $maxRunID = -1 }
                $logEntry.RunID = $maxRunID + 1
            }
            else {
                $lastRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Select-Object -Last 1).RunID
                if ($null -eq $lastRunID) { $lastRunID = 0 }
                $logEntry.RunID = $lastRunID
            }
            if ($EndFunction) {
                $FunctionStart = "$((($script:LogString | Where-Object {$_.InvokedBy -eq $Function -and $_.RunId -eq $lastRunID } | Sort-Object Time)[0]).Time)"
                $startTime = ([DateTime]::ParseExact("$FunctionStart", 'yyyy-MM-dd hh:mmTss', $null))
                $endTime = Get-Date
                $timeTaken = $endTime - $startTime
                $Message = '<<< End Function Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec"
                $logEntry.Message = $Message
            }
            elseif ($End) {
                $startTime = ([DateTime]::ParseExact($($script:LogString[0].Time), 'yyyy-MM-dd hh:mmTss', $null))
                $endTime = Get-Date
                $timeTaken = $endTime - $startTime
                $Message = '--- End Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec"
                $logEntry.Message = $Message
            }
            $script:LogString += $logEntry
            switch ($Severity) {
                'Warning' {
                    Write-Warning ('[WARNING] ! ' + $Message)
                    $UserInput = Read-Host "Warning encountered! Do you want to continue? (Y/N)"
                    if ($UserInput -eq 'N') {
                        Write-Output "Script execution stopped by user!"
                        exit
                    }
                }
                'Error'       { Write-Error ('[ERROR] X - ' + $FuncName + ' ' + $Message) -ErrorAction Continue }
                'Verbose'     { Write-Verbose ('[VERBOSE] ~ ' + $Message) }
                Default { Write-Information ('[INFORMATION] * ' + $Message)  -InformationAction Continue}
            }
        }
        catch {
            throw "Write-AuditLog encountered an error (process block): $($_.Exception.Message)"
        }

    }
    end {
        try {
            if ($End) {
                if (-not [string]::IsNullOrEmpty($OutputPath)) {
                    $script:LogString | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8
                    Write-Verbose "LogPath: $(Split-Path -Path $OutputPath -Parent)"
                }
                else {
                    throw "OutputPath is not specified for End action."
                }
            }
        }
        catch {
            throw "Error in Write-AuditLog (end block): $($_.Exception.Message)"
        }
    }
}
#EndRegion '.\Private\Write-AuditLog.ps1' 205
#Region '.\Public\Convert-NmapXMLToCSV.ps1' 0
function Convert-NmapXMLToCSV {
<#
    .SYNOPSIS
    Converts an Nmap XML scan output file to a CSV file.
 
    .DESCRIPTION
    The Convert-NmapXMLToCSV function takes an Nmap XML scan output
    file as input and converts it into a CSV file. The function
    extracts information about IP addresses, hostnames, open and
    closed ports, services, service versions, and operating systems.
    The output CSV file is saved to the specified folder or to
    C:\temp\NmapXMLToCSV by default.
 
    .PARAMETER InputXml
    A string containing the full path to the Nmap XML file that needs to be converted.
 
    .PARAMETER AttachmentFolderPath
    The output folder path where the converted CSV file will be saved.
    Default location is "C:\temp\NmapXMLToCSV".
 
    .EXAMPLE
    Convert-NmapXMLToCSV -InputXml "C:\path\to\nmap.xml" -AttachmentFolderPath "C:\path\to\output"
    This example will convert the contents of "C:\path\to\nmap.xml" into a CSV file and save it in "C:\path\to\output".
 
    .NOTES
    Make sure the input Nmap XML file is properly formatted and contains the necessary
    information for the conversion to work correctly.
 
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Convert-NmapXMLToCSV
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Convert-NmapXMLToCSV
#>


    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Full Path to Nmap xml file.',
            Position = 0,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$InputXml,
        [Parameter(
            HelpMessage = 'Enter output folder path. Default: C:\temp\NmapXMLToCSV',
            Position = 1,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$AttachmentFolderPath = "C:\temp\NmapXMLToCSV"
    )

    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }

        Initialize-DirectoryPath -DirectoryPath $AttachmentFolderPath
        [xml]$nmapXml = Get-Content -Path $InputXml
        [string]$OutputCsv = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss')).$($env:USERDOMAIN).nmapxmltocsv.csv"
        $csvData = @()
        Write-AuditLog "Processing Nmap XML file: $InputXml"
    }
    process {
        foreach ($scanHost in $nmapXml.nmaprun.host) {
            $ip = $scanHost.address | Where-Object { $_.addrtype -eq 'ipv4' } | Select-Object -ExpandProperty addr
            $hostname = $scanHost.hostnames.hostname.name
            # OS Match
            $osMatches = $scanHost.os.osmatch | ForEach-Object { $_.name }
            $os = $osMatches -join '; '
            # Ports
            $openPorts = @()
            $closedPorts = @()
            $services = @()
            $versions = @()
            foreach ($port in $scanHost.ports.port) {
                $state = $port.state.state
                $protocol = $port.protocol
                $portId = $port.portid
                $service = $port.service.name
                $version = $port.service.product
                # Port State
                if ($state -eq 'open') {
                    $openPorts += "$protocol/$portId"
                    $services += $service
                    $versions += $version
                }
                elseif ($state -eq 'closed') {
                    $closedPorts += "$protocol/$portId"
                }
            }
            $openPortsStr = $openPorts -join ', '
            $closedPortsStr = $closedPorts -join ', '
            $servicesStr = $services -join ', '
            $versionsStr = $versions -join ', '
            # PSObject
            $csvData += [PSCustomObject]@{
                IPAddress   = $ip -join ","
                Hostname    = $hostname
                OpenPorts   = $openPortsStr
                ClosedPorts = $closedPortsStr
                Services    = $servicesStr
                Versions    = $versionsStr
                OS          = $os
            }
            Write-AuditLog "Processed host: $ip"
        } # End Region Foreach
    }

    end {
        $csvData | Export-Csv -Path $OutputCsv -NoTypeInformation
        Write-AuditLog "Nmap XML file converted to CSV: $OutputCsv"
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Public\Convert-NmapXMLToCSV.ps1' 118
#Region '.\Public\Get-ADActiveUserAudit.ps1' 0
function Get-ADActiveUserAudit {
<#
    .SYNOPSIS
    Gets active but stale AD User accounts that haven't logged in within the last 90 days by default.
    .DESCRIPTION
    Audit's Active Directory taking "days" as the input for how far back to check for a user's last sign in.
    Output can be piped to a csv manually, or automatically to C:\temp\ADActiveUserAudit or a specified path
    in "AttachmentFolderPath" using the -Report Switch.
 
    Any user account that is enabled and not signed in over 90 days is a candidate for removal.
    .EXAMPLE
    PS C:\> Get-ADActiveUserAudit
    .EXAMPLE
    PS C:\> Get-ADActiveUserAudit -Report -Verbose
    .EXAMPLE
    PS C:\> Get-ADActiveUserAudit -Enabled $false -DaysInactive 30 -AttachmentFolderPath "C:\temp\MyNewFolderName" -Report -Verbose
    .PARAMETER Report
    Add report output as csv to DirPath directory.
    .PARAMETER AttachmentFolderPath
    Default path is C:\temp\ADActiveUserAudit.
    This is the folder where attachments are going to be saved.
    .PARAMETER Enabled
    If "$false", will also search disabled users.
    .PARAMETER DaysInactive
    How far back in days to look for sign ins. Outside of this window, users are considered "Inactive"
    .NOTES
    Outputs to C:\temp\ADActiveUserAudit by default.
    For help type: help Get-ADActiveUserAudit -ShowWindow
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-ADActiveUserAudit
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-ADActiveUserAudit
#>

    [OutputType([ADAuditTasksUser])]
    [CmdletBinding()]
    param (
        [Parameter(
            HelpMessage = 'Active Directory User Enabled or not. Default $true',
            Position = 0,
            ValueFromPipelineByPropertyName = $true
        )]
        [bool]$Enabled = $true,
        [Parameter(
            HelpMessage = 'Days back to check for recent sign in. Default: 90 days',
            Position = 1,
            ValueFromPipelineByPropertyName = $true
        )]
        [int]$DaysInactive = 90,
        [Parameter(
            HelpMessage = 'Enter output folder path. Default: C:\temp\ADActiveUserAudit',
            Position = 2,
            ValueFromPipeline = $true
        )]
        [string]$AttachmentFolderPath = "C:\temp\ADActiveUserAudit",
        [Parameter(
            HelpMessage = 'Switch to export output to a csv and zipped to Directory C:\temp. Default: $false',
            Position = 3,
            ValueFromPipelineByPropertyName = $true
        )]
        [switch]$Report
    )
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        $ScriptFunctionName = $MyInvocation.MyCommand.Name -replace '\..*'
        ### ActiveDirectory Module Install
        if ($env:USERNAME -eq 'SYSTEM') {
            $DomainSuffix = $env:USERDOMAIN
        } else {
            $DomainSuffix = $env:USERDNSDOMAIN
        }

        try {
            Install-ADModule -ErrorAction Stop -Verbose
        }
        catch {
            throw $_.Exception
        } ### End ADModule Install
        # Create Directory Path if it does not exist.
        Initialize-DirectoryPath -DirectoryPath $AttachmentFolderPath
        # Gather ADUser Properties to search for.
        $propsArray =
        "SamAccountName",
        "GivenName",
        "Surname",
        "Name",
        "UserPrincipalName",
        "LastLogonTimeStamp",
        "Enabled",
        "LastLogonTimeStamp",
        "DistinguishedName",
        "Title",
        "Manager",
        "Department"
        # Log the properties being retrieved.
        Write-AuditLog "###############################################"
        Write-AuditLog "Retrieving the following ADUser properties: "
        Write-AuditLog "$($propsArray -join " | ")"
        # Establish timeframe to review.
        $time = (Get-Date).Adddays( - ($DaysInactive))
        # Log the search criteria.
        Write-AuditLog "Searching for users who have not signed in within the last $DaysInactive days."
        Write-AuditLog "Where property Enabled = $Enabled"
        # Pause for 2 seconds to avoid potential race conditions.
        Start-Sleep 2
    }
    process {
        # Get Users
        Get-ADUser -Filter { LastLogonTimeStamp -lt $time -and Enabled -eq $Enabled } `
            -Properties $propsArray -OutVariable ADExport | Out-Null
        # Create custom object for the output
        $Export = Build-ADAuditTasksUser -ADExport $ADExport
    } # End Process
    end {
        # Log success message.
        Write-AuditLog "The $ScriptFunctionName Export was successful."

        # Log output object properties.
        Write-AuditLog "There are $($Export.Count) objects listed with the following properties: "
        Write-AuditLog "$(($Export | Get-Member -MemberType property ).Name -join " | ")"
        # Export to csv and zip, if requested.
        if ($Report) {
            # Add Datetime to filename.
            $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($DomainSuffix)"
            # Create FileNames.
            $csv = "$ExportFileName.csv"
            $zip = "$ExportFileName.zip"
            $log = "$ExportFileName.AuditLog.csv"
            # Call the Build-ReportArchive function to create the archive.
            Write-AuditLog -Endfunction
            Build-ReportArchive -Export $Export -csv $csv -zip $zip -log $log -AttachmentFolderPath $AttachmentFolderPath -ErrorAction SilentlyContinue -ErrorVariable BuildErr
        }
        else {
            # Log message indicating that the function is returning the output object.
            Write-AuditLog "Returning output object."
            Start-Sleep 1
            Write-AuditLog -Endfunction
            return $Export
        }
    }
}
#EndRegion '.\Public\Get-ADActiveUserAudit.ps1' 146
#Region '.\Public\Get-ADHostAudit.ps1' 0
function Get-ADHostAudit {
    <#
.SYNOPSIS
    Active Directory Server and Workstation Audit with Report export option (Can also be piped to CSV if Report isn't specified).
.DESCRIPTION
    Audits Active Directory for hosts that haven't signed in for a specified number of days. Output can be piped to a CSV manually, or automatically saved to C:\temp\ADHostAudit or a specified directory using the -Report switch.
 
    Use the Tab key to cycle through the -HostType parameter.
.EXAMPLE
    PS C:\> Get-ADHostAudit -HostType WindowsServers -Report -Verbose
.EXAMPLE
    PS C:\> Get-ADHostAudit -HostType WindowsWorkstations -Report -Verbose
.EXAMPLE
    PS C:\> Get-ADHostAudit -HostType "Non-Windows" -Report -Verbose
.EXAMPLE
    PS C:\> Get-ADHostAudit -OSType "2008" -DirPath "C:\Temp\" -Report -Verbose
.PARAMETER HostType
    Specifies the type of hosts to search for. Valid values are WindowsServers, WindowsWorkstations, and Non-Windows.
.PARAMETER OSType
    Specifies the operating system to search for. There is no need to add wildcards.
.PARAMETER DaystoConsiderAHostInactive
    Specifies the number of days to consider a host as inactive.
.PARAMETER Report
    Saves a CSV report to the specified directory.
.PARAMETER AttachmentFolderPath
    Specifies the directory where attachments will be saved.
.PARAMETER Enabled
    If set to $false, the function will also search for disabled computers.
.NOTES
    By default, output is saved to C:\temp\ADHostAudit.
    For more information, type: Get-Help Get-ADHostAudit -ShowWindow
.LINK
https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-ADHostAudit
.LINK
https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-ADHostAudit
#>



    [OutputType([pscustomobject])]
    [CmdletBinding(DefaultParameterSetName = 'HostType')]
    param (
        [ValidateSet("WindowsServers", "WindowsWorkstations", "Non-Windows")]
        [Parameter(
            ParameterSetName = 'HostType',
            Mandatory = $true,
            Position = 0,
            HelpMessage = 'Name filter attached to users.',
            ValueFromPipeline = $true
        )]
        [string]$HostType,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'OSType',
            Position = 0,
            HelpMessage = 'Enter a Specific OS Name or first few letters of the OS to Search for in ActiveDirectory',
            ValueFromPipeline = $true
        )]
        [string]$OSType,
        [Parameter(
            Position = 1,
            HelpMessage = 'How many days back to consider an AD Computer last sign in as active',
            ValueFromPipelineByPropertyName = $true
        )]
        [int]$DaystoConsiderAHostInactive = 90,
        [Parameter(
            Position = 2,
            HelpMessage = 'Switch to output to directory specified in DirPath parameter',
            ValueFromPipelineByPropertyName = $true
        )]
        [switch]$Report,
        [Parameter(
            Position = 3,
            HelpMessage = 'Enter the working directory you wish the report to save to. Default creates C:\temp'
        )]
        [string]$AttachmentFolderPath = 'C:\temp\ADHostAudit',
        [Parameter(
            HelpMessage = 'Search for Enabled or Disabled hosts',
            ValueFromPipelineByPropertyName = $true
        )]
        [bool]$Enabled = $true
    )
    begin {
        # Create logging object
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        # Get the name of the script function
        $ScriptFunctionName = $MyInvocation.MyCommand.Name -replace '\..*'
        # Check if the Active Directory module is installed and install it if necessary
        if ($env:USERNAME -eq 'SYSTEM') {
            $DomainSuffix = $env:USERDOMAIN
        } else {
            $DomainSuffix = $env:USERDNSDOMAIN
        }
        try {
            Install-ADModule -ErrorAction Stop -Verbose
        }
        catch {
            throw $_.Exception
        } ### End ADModule Install
        # Calculate the time that is considered a host inactive
        $time = (Get-Date).Adddays( - ($DaystoConsiderAHostInactive))
        # Check if the attachment folder exists and create it if it does not
        Initialize-DirectoryPath -DirectoryPath $AttachmentFolderPath
        # Determine the host type and set the appropriate search criteria
        switch ($PsCmdlet.ParameterSetName) {
            'HostType' {
                if ($HostType -eq "WindowsWorkstations") {
                    $FileSuffix = "Workstations"
                    Write-AuditLog "###############################################"
                    Write-AuditLog "Searching Windows Workstations......"
                    Start-Sleep 2
                }
                elseif ($HostType -eq "Non-Windows") {
                    $POSIX = $true
                    $FileSuffix = "Non-Windows"
                    Write-AuditLog "###############################################"
                    Write-AuditLog "Searching Non-Windows Computer Objects......"
                    Start-Sleep 2
                }
                elseif ($HostType -eq "WindowsServers") {
                    $OSPicked = "*Server*"
                    $FileSuffix = "Servers"
                    Write-AuditLog "###############################################"
                    Write-AuditLog "Searching Windows Servers......"
                    Start-Sleep 2
                }
            }
            'OSType' {
                $OSPicked = '*' + $OSType + '*'
                $FileSuffix = $OSType
                Write-AuditLog "###############################################"
                Write-AuditLog "Searching OSType $OsType......"
                Start-Sleep 2
            }
        }
        # Set the properties to retrieve for the host objects
        $propsArray =
        "Created",
        "Description",
        "DNSHostName",
        "Enabled",
        "IPv4Address",
        "IPv6Address",
        "KerberosEncryptionType",
        "lastLogonTimestamp",
        "Name",
        "OperatingSystem",
        "DistinguishedName",
        "servicePrincipalName",
        "whenChanged"

    } # End Begin
    process {
        # Log the search criteria
        Write-AuditLog "Searching computers that have logged in within the last $DaystoConsiderAHostInactive days."
        Write-AuditLog "Where property Enabled = $Enabled"
        Start-Sleep 2
        # Determine the Active Directory computers to include in the report
        if ($OSPicked) {
            Write-AuditLog "And Operating System is like: $OSPicked."
            Get-ADComputer -Filter { (LastLogonTimeStamp -gt $time) -and (Enabled -eq $Enabled) -and (OperatingSystem -like $OSPicked) }`
            -Properties $propsArray -OutVariable ADComps | Out-Null
        }
        elseif ($POSIX) {
            Write-AuditLog "And Operating System is: Non-Windows(POSIX)."
            Get-ADComputer -Filter { OperatingSystem -notlike "*windows*" -and OperatingSystem -notlike "*server*" -and Enabled -eq $Enabled -and lastlogontimestamp -gt $time }`
                -Properties $propsArray -OutVariable ADComps | Out-Null
        }
        else {
            Write-AuditLog "And Operating System is -like `"*windows*`" -and Operating System -notlike `"*server*`" (Workstations)."
            Get-ADComputer -Filter { OperatingSystem -like "*windows*" -and OperatingSystem -notlike "*server*" -and Enabled -eq $Enabled -and lastlogontimestamp -gt $time } `
                -Properties $propsArray -OutVariable ADComps | Out-Null
        }
        # Create a new object for each Active Directory computer with the selected properties and store the results in an array
        $Export = Build-ADAuditTasksComputer -ADComputer $ADComps
    } # End Process
    end {
        # If there the export is not empty
        if ($Export) {
            # Create a message that lists the properties that were exported
            $ExportMembers = "Export: $(($Export | Get-Member -MemberType property ).Name -join " | ")"
            # Log a successful export message and list the exported properties and the number of objects exported
            Write-AuditLog "The $ScriptFunctionName Export was successful."
            Write-AuditLog "There are $($Export.Count) objects listed with the following properties: "
            Write-AuditLog "$ExportMembers"
            # If the -Report switch is used, create a report archive and log the output
            if ($Report) {
                # Add Datetime to filename
                $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($DomainSuffix)"
                # Create FileNames
                $csv = "$ExportFileName.$FileSuffix.csv"
                $zip = "$ExportFileName.$FileSuffix.zip"
                $log = "$ExportFileName.$FileSuffix.AuditLog.csv"
                Write-AuditLog -EndFunction
                Build-ReportArchive -Export $Export -csv $csv -zip $zip -log $log -AttachmentFolderPath $AttachmentFolderPath -ErrorVariable BuildErr
            }
            # If the -Report switch is not used, return the output object
            else {
                Write-AuditLog "Returning output object."
                Start-Sleep 1
                Write-AuditLog -EndFunction
                return $Export
            }
        }
        else {
            # If there is no output, log message and create an audit log file
            $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($DomainSuffix)"
            $log = Join-Path -Path "$ExportFileName." -ChildPath "$FileSuffix.AuditLog.csv"
            $Script:LogString += Write-AuditLog "There is no output for the specified host type $FileSuffix"
            Write-AuditLog -End -OutputPath $log
            # If the -Report switch is not used, return null
            if (-not $Report) {
                return $null
            }
            else {
                return $log
            }
        }
    } # End End
}
#EndRegion '.\Public\Get-ADHostAudit.ps1' 225
#Region '.\Public\Get-ADUserLogonAudit.ps1' 0
function Get-ADUserLogonAudit {
<#
    .SYNOPSIS
    Retrieves the most recent LastLogon timestamp for a specified Active Directory user
    account from all domain controllers and outputs it as a DateTime object.
    .DESCRIPTION
    This function takes a SamAccountName input parameter for a specific user account and
    retrieves the most recent LastLogon timestamp for that user from all domain controllers
    in the Active Directory environment. It then returns the LastLogon timestamp as a DateTime
    object. The function also checks the availability of each domain controller before querying
    it, and writes an audit log with a list of available and unavailable domain controllers.
    .PARAMETER SamAccountName
    Specifies the SamAccountName of the user account to be checked for the most recent LastLogon timestamp.
    .INPUTS
    A SamAccountName string representing the user account to be checked.
    .OUTPUTS
    A DateTime object representing the most recent LastLogon timestamp for the specified user account.
    .EXAMPLE
    Get-ADUserLogonAudit -SamAccountName "jdoe"
    Retrieves the most recent LastLogon timestamp for the user account with the SamAccountName
    "jdoe" from all domain controllers in the Active Directory environment.
    .NOTES
    This function is designed to be run on the primary domain controller, but it can be run on
    any domain controller in the environment. It requires the Active Directory PowerShell module
    and appropriate permissions to read user account data. The function may take some time to complete
    if the Active Directory environment is large or the domain controllers are geographically distributed.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-ADUserLogonAudit
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-ADUserLogonAudit
#>

    [CmdletBinding()]
    [OutputType([datetime])]
    param (
        [Alias("Identity", "UserName", "Account")]
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Enter the SamAccountName',
            ValueFromPipeline = $true
        )]
        $SamAccountName
    )
    process {
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog "###############################################"
        # Check if the Active Directory module is installed and install it if necessary
        try {
            Install-ADModule -ErrorAction Stop -Verbose
        }
        catch {
            throw $_.Exception
        } ### End ADModule Install
        #Get all domain controllers
        $DomainControllers = Get-ADDomainController -Filter { Name -like "*" }
        $Comps = $DomainControllers.name
        #Create a hash table to store the parameters for Get-ADObject command
        $Params = @{}
        $Params.ComputerName = @()
        #Create a hash table to store domain controllers that are not available for queries
        $NoRemoteAccess = @{}
        $NoRemoteAccess.NoRemoteAccess = @()
        #Loop through all domain controllers to check for remote access
        foreach ($comp in $comps) {
            $testRemoting = Test-WSMan -ComputerName $comp -ErrorAction SilentlyContinue
            if ($null -ne $testRemoting ) {
                $params.ComputerName += $comp
            }
            else {
                $NoRemoteAccess.NoRemoteAccess += $comp
            }
        }
        #Write audit logs for domain controllers that are available for queries
        if ($params.ComputerName) {
            Write-AuditLog "The following DC's were available for WSMan:"
            Write-AuditLog "$($params.ComputerName)"
        }
        #Write audit logs for domain controllers that are not available for queries
        if ($NoRemoteAccess.NoRemoteAccess) {
            Write-AuditLog "The following DC's were unavailable and weren't included:"
            Write-AuditLog "$($NoRemoteAccess.NoRemoteAccess)"
        }
        #Get the AD user object based on the given SamAccountName
        $user = Get-ADUser -Identity $SamAccountName
        #Initialize a variable to store the latest lastLogon time
        $time = 0
        #Initialize an array to store DateTime objects from all domain controllers
        $dt = @()
        #Loop through all domain controllers to get the lastLogon time of the user
        foreach ($dc in $params.ComputerName) {
            $user | Get-ADObject -Server $dc -Properties lastLogon -OutVariable usertime -ErrorAction SilentlyContinue | Out-Null
            if ($usertime.LastLogon -gt $time) {
                $time = $usertime.LastLogon
            }
            $dt += [DateTime]::FromFileTime($time)
        }
        Write-AuditLog -EndFunction
        #Sort the array of DateTime objects in descending order and return the latest DateTime object
        return ($dt | Sort-Object -Descending)[0]
    }
}
#EndRegion '.\Public\Get-ADUserLogonAudit.ps1' 106
#Region '.\Public\Get-ADUserPrivilegeAudit.ps1' 0
function Get-ADUserPrivilegeAudit {
    <#
    .SYNOPSIS
    Produces three object outputs: PrivilegedGroups, AdExtendedRights, and possible service accounts.
    .DESCRIPTION
    The Get-ADUserPrivilegeAudit function produces reports on privileged groups, AD extended rights, and possible service accounts. If the -Report switch is used, the reports will be created in the specified folder. To instantiate variables with the objects, provide three objects on the left side of the assignment:
 
    Example: $a,$b,$c = Get-ADUserPrivilegeAudit -Verbose
 
    The objects will be populated with privileged groups, AD extended rights, and possible service accounts, respectively.
    .EXAMPLE
    Get-ADUserPrivilegeAudit -Verbose
    Gets the reports as three separate objects. To instantiate variables with the objects, provide three objects on the left side of the assignment:
    Example: $a,$b,$c = Get-ADUserPrivilegeAudit -Verbose
    The objects will be populated with privileged groups, AD extended rights, and possible service accounts, respectively.
    .EXAMPLE
    Get-ADUserPrivilegeAudit -Report -Verbose
    Returns three reports to the default folder, C:\temp\ADUserPrivilegeAudit, in a single zip file.
    .PARAMETER AttachmentFolderPath
    Specifies the path of the folder where you want to save attachments. The default path is C:\temp\ADUserPrivilegeAudit.
    .PARAMETER Report
    Adds report output as CSV to the directory specified by AttachmentFolderPath.
    .NOTES
    This function requires the ActiveDirectory module.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-ADUserPrivilegeAudit
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-ADUserPrivilegeAudit
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject[]], [string], [System.Object[]])]
    param (
        # Input parameter: output folder path for generated reports
        [Parameter(
            HelpMessage = ' Enter output folder path. Default: C:\temp\ADUserPrivilegeAudit ',
            Position = 0,
            ValueFromPipeline = $true
        )]
        [string]$AttachmentFolderPath = 'C:\temp\ADUserPrivilegeAudit',
        # Input parameter: switch to export output to a CSV and zip to the specified directory
        [Parameter(
            HelpMessage = 'Switch to export output to a CSV and zipped to Directory C:\temp\ADUserPrivilegeAudit Default: $false',
            Position = 1,
            ValueFromPipelineByPropertyName = $true
        )]
        [switch]$Report
    )
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        # Get name of the function
        $ScriptFunctionName = $MyInvocation.MyCommand.Name -replace '\..*'
        if ($env:USERNAME -eq 'SYSTEM') {
            $DomainSuffix = $env:USERDOMAIN
        } else {
            $DomainSuffix = $env:USERDNSDOMAIN
        }
        # Check if ActiveDirectory module is installed
        ### ActiveDirectory Module Install
        try {
            Install-ADModule -ErrorAction Stop -Verbose
        }
        catch {
            throw $_.Exception
        } ### End ADModule Install
        # Create output directory if it does not already exist
        Initialize-DirectoryPath -DirectoryPath $AttachmentFolderPath
        # Create Privilege Groups Array.
        $AD_PrivilegedGroups = @(
            'Enterprise Admins',
            'Schema Admins',
            'Domain Admins',
            'Administrators',
            'Cert Publishers',
            'Account Operators',
            'Server Operators',
            'Backup Operators',
            'Print Operators',
            'DnsAdmins',
            'DnsUpdateProxy',
            'DHCP Administrators'
        )
        # Time Variables
        $time90 = (Get-Date).Adddays( - (90))
        $time60 = (Get-Date).Adddays( - (60))
        $time30 = (Get-Date).Adddays( - (30))
        # Create Arrays
        $members = @()
        $ADUsers = @()
        # AD Groups to search for.
        Write-AuditLog "###############################################"
        Write-AuditLog "Retriving info from the following priveledged groups: "
        Write-AuditLog "$($AD_PrivilegedGroups -join " | ")"
        Start-Sleep 2
    }
    process {
        # Iterate through each group in $AD_PrivilegedGroups
        foreach ($group in $AD_PrivilegedGroups) {
            # Clear the GroupMember variable and retrieve all members of the current group
            Clear-Variable GroupMember -ErrorAction SilentlyContinue
            Get-ADGroupMember -Identity $group -Recursive -OutVariable GroupMember | Out-Null
            # Select the desired properties for each member and add custom properties to the output
            $GroupMember | Select-Object SamAccountName, Name, ObjectClass, `
            @{N = 'PriviledgedGroup'; E = { $group } }, `
            @{N = 'Enabled'; E = { (Get-ADUser -Identity $_.samaccountname).Enabled } }, `
            @{N = 'PasswordNeverExpires'; E = { (Get-ADUser -Identity $_.samaccountname -Properties PasswordNeverExpires).PasswordNeverExpires } }, `
            @{N = 'LastLogin'; E = { [DateTime]::FromFileTime((Get-ADUser -Identity $_.samaccountname -Properties lastLogonTimestamp).lastLogonTimestamp) } }, `
            @{N = 'LastSeen'; E = {
                    switch ([DateTime]::FromFileTime((Get-ADUser -Identity $_.samaccountname -Properties lastLogonTimestamp).lastLogonTimestamp)) {
                        # Over 90 Days
                        { ($_ -lt $time90) } { '3+ months'; break }
                        # Over 60 Days
                        { ($_ -lt $time60) } { '2+ months'; break }
                        # Over 90 Days
                        { ($_ -lt $time30) } { '1+ month'; break }
                        default { 'Recently' }
                    }
                }
            }, `
            @{N = 'OrgUnit'; E = { $_.DistinguishedName -replace '^.*?,(?=[A-Z]{2}=)' } }, `
            @{N = 'GroupMemberships'; E = { Get-ADGroupMemberof -SamAccountName $_.samaccountname } }, `
                Title, `
            @{N = 'Manager'; E = { (Get-ADUser -Identity $_.manager).Name } }, `
            @{N = 'SuspectedSvcAccount'; E = {
                    # Check if the account is a suspected service account based on PasswordNeverExpires or servicePrincipalName
                    if (((Get-ADUser -Identity $_.samaccountname -Properties PasswordNeverExpires).PasswordNeverExpires) -or ( $null -ne  ((Get-ADUser -Identity $_.samaccountname -Properties servicePrincipalName).servicePrincipalName) ) ) {
                        return $true
                    }
                    else {
                        return $false
                    }
                } # End Expression
            }, # End Named Expression SuspectedSvcAccount
            Department, AccessRequired, NeedMailbox -OutVariable members | Out-Null
            # Add the member objects to $ADUsers array
            $ADUsers += $members
        }
        # Create an array to store the output objects
        $Export = @()
        # Iterate through each member in $ADUsers and create a custom object with desired properties
        foreach ($User in $ADUsers) {
            $hash = [ordered]@{
                PriviledgedGroup     = $User.PriviledgedGroup
                SamAccountName       = $User.SamAccountName
                Name                 = $User.Name
                ObjectClass          = $User.ObjectClass
                LastLogin            = $User.LastLogin
                LastSeen             = $User.LastSeen
                GroupMemberships     = $User.GroupMemberships
                Title                = $User.Title
                Manager              = $User.Manager
                Department           = $User.Department
                OrgUnit              = $User.OrgUnit
                Enabled              = $User.Enabled
                PasswordNeverExpires = $User.PasswordNeverExpires
                SuspectedSvcAccount  = $User.SuspectedSvcAccount
                AccessRequired       = $false
                NeedMailbox          = $true
            }
            New-Object -TypeName PSCustomObject -Property $hash -OutVariable PSObject | Out-Null
            $Export += $PSObject
        }
        # Log success message for $ScriptFunctionName export
        Write-AuditLog "The $ScriptFunctionName Export was successful."
        # Log count and properties of objects in $Export
        Write-AuditLog "There are $($Export.Count) objects listed with the following properties: "
        Write-AuditLog "$(($Export | Get-Member -MemberType noteproperty ).Name -join " | ")"

        # Get PDC
        $dc = (Get-ADDomainController -Discover -DomainName $DomainSuffix -Service PrimaryDC).Name
        # Get DN of AD Root.
        $rootou = (Get-ADRootDSE).defaultNamingContext
        # Get AD objects from the PDC for the root ou. #TODO Check
        $Allobjects = Get-ADObject -Server $dc -SearchBase $rootou -SearchScope subtree -LDAPFilter `
            "(&(objectclass=user)(objectcategory=person))" -Properties ntSecurityDescriptor -ResultSetSize $null

        # Create $Export2 object by looping through all objects in $Allobjects and retrieving extended rights
        $Export2 = Foreach ($ADObject in $Allobjects) {
            Get-AdExtendedRight $ADObject
        }
        # Log success message for extended permissions export
        Write-AuditLog "The Extended Permissions Export was successful."
        # Log count and properties of objects in $Export2
        Write-AuditLog "There are $($Export2.Count) objects listed with the following properties: "
        Write-AuditLog "$(($Export2 | Get-Member -MemberType noteproperty ).Name -join " | ")"

        # Export Delegated access, allowed protocols, and Destination Services by filtering for relevant properties
        $Export3 = Get-ADObject -Filter { (msDS-AllowedToDelegateTo -like '*') -or (UserAccountControl -band 0x0080000) -or (UserAccountControl -band 0x1000000) } `
            -prop samAccountName, msDS-AllowedToDelegateTo, servicePrincipalName, userAccountControl | `
            Select-Object DistinguishedName, ObjectClass, samAccountName, `
        @{N = 'servicePrincipalName'; E = { $_.servicePrincipalName -join " | " } }, `
        @{N = 'DelegationStatus'; E = { if ($_.UserAccountControl -band 0x80000) { 'AllServices' }else { 'SpecificServices' } } }, `
        @{N = 'AllowedProtocols'; E = { if ($_.UserAccountControl -band 0x1000000) { 'Any' }else { 'Kerberos' } } }, `
        @{N = 'DestinationServices'; E = { $_.'msDS-AllowedToDelegateTo' } }

        # Log success message for delegated permissions export
        Write-AuditLog "The delegated permissions Export was successful."
        # Log count and properties of objects in $Export3
        Write-AuditLog "There are $($Export3.Count) objects listed with the following properties: "
        Write-AuditLog "$(($Export3 | Get-Member -MemberType noteproperty ).Name -join " | ")"
    }
    end {
        if ($Report) {
            # Add Datetime to filename
            $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($DomainSuffix)"
            # Create FileNames
            $csv1 = "$ExportFileName.csv"
            $csv2 = "$ExportFileName.ExtendedPermissions.csv"
            $csv3 = "$ExportFileName.PossibleServiceAccounts.csv"
            $zip1 = "$ExportFileName.zip"
            $log = "$ExportFileName.AuditLog.csv"
            # Export results to CSV files
            $Export | Export-Csv $csv1 -NoTypeInformation
            $Export2 | Export-Csv $csv2 -NoTypeInformation
            $Export3 | Export-Csv $csv3 -NoTypeInformation
            # Compute SHA256 hash for each CSV file
            $csv1Sha256Hash = (Get-FileHash $csv1).Hash
            $csv2Sha256Hash = (Get-FileHash $csv2).Hash
            $csv3Sha256Hash = (Get-FileHash $csv3).Hash
            # Log SHA256 hash for each CSV file
            Write-AuditLog "Exported CSV $csv1 SHA256 hash: "
            Write-AuditLog "$($csv1Sha256Hash)"
            Write-AuditLog "Exported CSV $csv2 SHA256 hash: "
            Write-AuditLog "$($csv2Sha256Hash)"
            Write-AuditLog "Exported CSV $csv3 SHA256 hash: "
            Write-AuditLog "$($csv3Sha256Hash)"
            # Log directory path and ZIP file path
            Write-AuditLog "Directory: $AttachmentFolderPath"
            Write-AuditLog "Returning string filepath of: "
            Write-AuditLog "FilePath: $zip1"
            # Export audit log to CSV file
            # $Script:LogString | Export-Csv $log -NoTypeInformation -Encoding utf8
            # Compress CSV files and audit log into a ZIP file
            Write-AuditLog -End -OutputPath $log
            Compress-Archive $csv1, $csv2, $csv3, $log -DestinationPath $zip1 -CompressionLevel Optimal
            # Remove CSV and audit log files
            Remove-Item $csv1, $csv2, $csv3, $log -Force
            # Return ZIP file path
            return $zip1
        }
        else {
            # Return output objects
            Write-AuditLog "Returning 3 output objects. Instantiate object Example: `$a, `$b, `$c, = Get-ADUserPrivilegedAudit"
            Write-AuditLog -EndFunction
            Start-Sleep 1
            return $Export, $Export2, $Export3
        }
    }
}
#EndRegion '.\Public\Get-ADUserPrivilegeAudit.ps1' 254
#Region '.\Public\Get-ADUserWildCardAudit.ps1' 0
function Get-ADUserWildCardAudit {
<#
    .SYNOPSIS
    Takes a search string to find commonly named accounts.
    .DESCRIPTION
    Takes a search string to find commonly named accounts. For example, if you
    commonly name service accounts with the prefix "svc", use "svc" for the
    WildCardIdentifier to search for names that contain "svc".
    .EXAMPLE
    Get-ADUserWildCardAudit -WildCardIdentifier "svc" -Report -Verbose
 
    Searches for all user accounts that are named like the search string "svc".
    .PARAMETER Report
    Add report output as csv to AttachmentFolderPath directory.
    .PARAMETER AttachmentFolderPath
    Default path is C:\temp\ADUserWildCardAudit. This is the folder where attachments are going to be saved.
    .PARAMETER Enabled
    If "$false", will also search disabled users.
    .PARAMETER DaysInactive
    How far back in days to look for sign ins. Outside of this window, users are considered "Inactive"
    .PARAMETER WildCardIdentifier
    The search string to look for in the name of the account. Case does not matter. Do not add a
    wildcard (*) as it will do this automatically.
    .NOTES
    This function requires the ActiveDirectory module.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-ADUserWildCardAudit
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-ADUserWildCardAudit
#>


    [OutputType([ADAuditTasksUser])]
    [CmdletBinding()]
    param (
        [Parameter(
            HelpMessage = 'Active Directory User Enabled or not. Default $true',
            Position = 0,
            ValueFromPipelineByPropertyName = $true
        )]
        [bool]$Enabled = $true,
        [Parameter(
            HelpMessage = 'Days back to check for recent sign in. Default: 90 days',
            Position = 1,
            ValueFromPipelineByPropertyName = $true
        )]
        [int]$DaysInactive = 90,
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name filter attached to users.',
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$WildCardIdentifier,
        [Parameter(
            HelpMessage = 'Enter output folder path. Default: C:\temp\ADUserWildCardAudit',
            Position = 3,
            ValueFromPipeline = $true
        )]
        [string]$AttachmentFolderPath = "C:\temp\ADUserWildCardAudit",
        [Parameter(
            HelpMessage = 'Switch to export output to a csv and zipped to Directory C:\temp. Default: $false',
            Position = 4,
            ValueFromPipelineByPropertyName = $true
        )]
        [switch]$Report
    )
    begin {
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
        $ScriptFunctionName = $MyInvocation.MyCommand.Name -replace '\..*'
        if ($env:USERNAME -eq 'SYSTEM') {
            $DomainSuffix = $env:USERDOMAIN
        } else {
            $DomainSuffix = $env:USERDNSDOMAIN
        }
        ### ActiveDirectory Module Install
        try {
            Install-ADModule -ErrorAction Stop -Verbose
        }
        catch {
            throw $_.Exception
        } ### End ADModule Install
        # Create Directory Path
        Initialize-DirectoryPath -DirectoryPath $AttachmentFolderPath
        # ADUser Properties to search for.
        $propsArray =
        "SamAccountName",
        "GivenName",
        "Surname",
        "Name",
        "UserPrincipalName",
        "LastLogonTimeStamp",
        "Enabled",
        "LastLogonTimeStamp",
        "DistinguishedName",
        "Title",
        "Manager",
        "Department"
        Write-AuditLog "###############################################"
        Write-AuditLog "Retriving the following ADUser properties: "
        Write-AuditLog "$($propsArray -join " | ")"
        # Establish timeframe to review.
        Write-AuditLog "Searching for accounts using search string `"$WildCardIdentifier`" "
        Start-Sleep 2
    }
    process {
        $time = (Get-Date).Adddays( - ($DaysInactive))
        # Get Users
        write-auditlog "Enabled is: $Enabled"
        $WildCardIdentifierstring = '*' + $WildCardIdentifier + '*'
        Get-ADUser -Filter { Name -like $WildCardIdentifierstring -and LastLogonTimeStamp -lt $time -and Enabled -eq $Enabled } `
        -Properties $propsArray -OutVariable ADExport | Out-Null
        Write-AuditLog "Creating a custom object from ADUser output."
        $Export = Build-ADAuditTasksUser -ADExport $ADExport
    }
    end {
        Write-AuditLog "The $ScriptFunctionName Export was successful."
        Write-AuditLog "There are $($Export.Count) objects listed with the following properties: "
        Write-AuditLog "$(($Export | Get-Member -MemberType property ).Name -join " | ")"
        if ($Report) {
            # Add Datetime to filename
            $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($DomainSuffix)"
            # Create FileNames
            $csv = "$ExportFileName.csv"
            $zip = "$ExportFileName.zip"
            $log = "$ExportFileName.AuditLog.csv"
            Write-AuditLog -EndFunction
            Build-ReportArchive -Export $Export -csv $csv -zip $zip -log $log -AttachmentFolderPath $AttachmentFolderPath -ErrorVariable BuildErr
        }
        else {
            Write-AuditLog "Returning output object."
            Write-AuditLog -EndFunction
            Start-Sleep 1
            return $Export
        }
    }
}
#EndRegion '.\Public\Get-ADUserWildCardAudit.ps1' 141
#Region '.\Public\Get-HostTag.ps1' 0
function Get-HostTag {
    <#
    .SYNOPSIS
    Creates a host name or tag based on predetermined criteria for as many as 999 hosts at a time.
    .DESCRIPTION
    A longer description of the function, its purpose, common use cases, etc.
    .EXAMPLE
    Get-HostTag -PhysicalOrVirtual Physical -Prefix "CSN" -SystemOS 'Windows Server' -DeviceFunction 'Application Server' -HostCount 5
        CSN-PWSVAPP001
        CSN-PWSVAPP002
        CSN-PWSVAPP003
        CSN-PWSVAPP004
        CSN-PWSVAPP005
 
        This creates the name of the host under 15 characters and numbers them. Prefix can be 2-3 characters.
    .PARAMETER PhysicalOrVirtual
    Tab through selections to add 'P' or 'V' for physical or virtual to host tag.
    .PARAMETER Prefix
    Enter the 2-3 letter prefix. Good for prefixing company initials, locations, or other.
    .PARAMETER SystemOS
    Use tab to cycle through the following options:
        "Cisco ASA", "Android", "Apple IOS",
        "Dell Storage Center", "MACOSX",
        "Dell Power Edge", "Embedded", "Embedded Firmware",
        "Cisco IOS", "Linux", "Qualys", "Citrix ADC (Netscaler)",
        "Windows Thin Client", "VMWare",
        "Nutanix", "TrueNas", "FreeNas",
        "ProxMox", "Windows Workstation", "Windows Server",
        "Windows Server Core", "Generic OS", "Generic HyperVisor"
    .PARAMETER DeviceFunction
    Use tab to cycle through the following options:
        "Application Server", "Backup Server", "Directory Server",
        "Email Server", "Firewall", "FTP Server",
        "Hypervisor", "File Server", "NAS File Server",
        "Power Distribution Unit", "Redundant Power Supply", "SAN Appliance",
        "SQL Server", "Uninteruptable Power Supply", "Web Server",
        "Management", "Blade Enclosure", "Blade Enclosure Switch",
        "SAN specific switch", "General server/Network switch", "Generic Function Device"
    .PARAMETER HostCount
    Enter a number from 1 to 999 for how many hostnames you'd like to create.
    .NOTES
    Additional information about the function, usage tips, etc.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-HostTag
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-HostTag
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]],[string[]])]
    # Define the parameters for the function
    param (
        # Define the first parameter, which is mandatory
        [Parameter(
            MandaTory = $true, # This parameter is mandatory
            Position = 0, # This parameter should be the first one in the list
            HelpMessage = 'Enter 2 character site code or prefix for your devices', # Help message for the parameter
            ValueFromPipelineByPropertyName = $true  # This parameter can be piped to
        )]
        [ValidateSet("Physical", "Virtual")]  # This parameter can only have these values
        [string]$PhysicalOrVirtual, # The variable that will hold the value of this parameter
        # Define the second parameter, which is mandatory
        [Parameter(
            MandaTory = $true, # This parameter is mandatory
            Position = 1, # This parameter should be the second one in the list
            HelpMessage = 'Enter 2 to 3 character site code or prefix for your devices', # Help message for the parameter
            ValueFromPipelineByPropertyName = $true  # This parameter can be piped to
        )]
        [ValidateLength(2, 3)]  # This parameter can only have a value of length 2 or 3
        [string]$Prefix, # The variable that will hold the value of this parameter
        # Define the third parameter, which is mandatory
        [Parameter(
            MandaTory = $true, # This parameter is mandatory
            Position = 2, # This parameter should be the third one in the list
            HelpMessage = 'Tab complete to pick from a list of System OSs', # Help message for the parameter
            ValueFromPipelineByPropertyName = $true  # This parameter can be piped to
        )]
        [ValidateSet(
            "Cisco ASA", "Android", "Apple IOS",
            "Dell Storage Center", "MACOSX",
            "Dell Power Edge", "Embedded", "Embedded Firmware",
            "Cisco IOS", "Linux", "Qualys", "Citrix ADC (Netscaler)",
            "Windows Thin Client", "VMWare",
            "Nutanix", "TrueNas", "FreeNas",
            "ProxMox", "Windows Workstation", "Windows Server",
            "Windows Server Core", "Generic OS", "Generic HyperVisor"
        )]  # This parameter can only have values from this list
        [string]$SystemOS, # The variable that will hold the value of this parameter
        [Parameter(
            MandaTory = $true,
            Position = 3,
            HelpMessage = 'Tab complete to pick from a list of Device Functions',
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateSet(
            "Application Server", "Backup Server", "Directory Server",
            "Email Server", "Firewall", "FTP Server",
            "Hypervisor", "File Server", "NAS File Server",
            "Power Distribution Unit", "Redundant Power Supply", "SAN Appliance",
            "SQL Server", "Uninteruptable Power Supply", "Web Server",
            "Management", "Blade Enclosure", "Blade Enclosure Switch",
            "SAN specific switch", "General server/Network switch", "Generic Function Device",
            "Cache Server","Load Balancer"
        )]
        [string]$DeviceFunction,
        [Parameter(
            Position = 4,
            HelpMessage = 'Enter the number of host names you want to create between 1 and 254',
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateRange(1, 999)]
        [int]$HostCount = 1
    )
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        switch ($DeviceFunction) {
            "Application Server" { $DFunction = "APP" }
            "Backup Server" { $DFunction = "BAK" }
            "Directory Server" { $DFunction = "DIR" }
            "Email Server" { $DFunction = "EML" }
            "Firewall" { $DFunction = "FRW" }
            "FTP Server" { $DFunction = "FTP" }
            "Hypervisor" { $DFunction = "HYP" }
            "File Server" { $DFunction = "FIL" }
            "NAS File Server" { $DFunction = "NAS" }
            "Power Distribution Unit" { $DFunction = "PDU" }
            "Redundant Power Supply" { $DFunction = "RPS" }
            "SAN Appliance" { $DFunction = "SAN" }
            "SQL Server" { $DFunction = "SQL" }
            "Uninteruptable Power Supply" { $DFunction = "UPS" }
            "Web Server" { $DFunction = "WEB" }
            "Management" { $DFunction = "MGT" }
            "Cache Server" { $DFunction = "CSH" }
            "Load Balancer" { $DFunction = "BAL" }
            "Blade Enclosure" { $DFunction = "BLDENC" }
            "Blade Enclosure Switch" { $DFunction = "SW-BLD" }
            "SAN specific Switch" { $DFunction = "SW-SAN" }
            "General Server/Network Switch" { $DFunction = "SW-SVR" }
            Default { $DFunction = "XDV" }
        }
        switch ($SystemOS) {
            "Cisco ASA" { $OSTxt = "ASA" }
            "Android" { $OSTxt = "DRD" }
            "Apple IOS" { $OSTxt = "IOS" }
            "Dell Storage Center" { $OSTxt = "DLS" }
            "MACOSX" { $OSTxt = "MAC" }
            "Dell Power Edge" { $OSTxt = "DPE" }
            "Embedded" { $OSTxt = "EMD" }
            "Embedded Firmware" { $OSTxt = "EFW" }
            "Cisco IOS" { $OSTxt = "COS" }
            "Linux" { $OSTxt = "NIX" }
            "Qualys" { $OSTxt = "QLS" }
            "Citrix ADC (Netscaler)" { $OSTxt = "ADC" }
            "Windows Thin Client" { $OSTxt = "WTC" }
            "VMWare" { $OSTxt = "VMW" }
            "Nutanix" { $OSTxt = "NTX" }
            "TrueNas" { $OSTxt = "FNS" }
            "FreeNas" { $OSTxt = "XDV" }
            "ProxMox" { $OSTxt = "PMX" }
            "Windows Workstation" { $OSTxt = "WWS" }
            "Windows Server" { $OSTxt = "WSV" }
            "Windows Server Core" { $OSTxt = "WSC" }
            "Generic OS" { $OSTxt = "GOS" }
            Default { $OSTxt = "GHV" }
        }
        switch ($PhysicalOrVirtual) {
            "Physical" { $DevType = "P" }
            Default { $DevType = "V" }
        }
    }
    process {
        Write-AuditLog "The prefix is $Prefix"
        1..$HostCount | ForEach-Object {
            $CustomName1 = "-" + $DevType + $OSTxt + $DFunction + $('{0:d3}' -f [int]$_)
            $CustomName = $Prefix + $CustomName1
            $CustomName
        }
        # Create Device Name
    }
    end {
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Public\Get-HostTag.ps1' 188
#Region '.\Public\Get-NetworkAudit.ps1' 0
function Get-NetworkAudit {
<#
    .SYNOPSIS
    Discovers the local network and runs port scans on all hosts found for specific or default sets of ports, displaying MAC ID vendor info.
    .DESCRIPTION
    Scans the network for open ports specified by the user or default ports if no ports are specified. Creates reports if the report switch is active and adds MAC ID vendor info if found.
 
    NOTES:
    - This function requires the PSnmap module. If not found, it will be installed automatically.
    - The throttle limit determines the number of concurrent threads during scanning.
    - The scan rate is limited to 32 hosts per second to ensure network stability.
    - The total scan time and data transferred depend on the number of hosts.
    - The average network bandwidth is approximately 32 kilobits per second.
    .PARAMETER Ports
    Specifies the ports to scan. If not provided, the function uses default ports:
    "21", "22", "23", "25", "53", "67", "68", "80", "443",
    "88", "464", "123", "135", "137", "138", "139",
    "445", "389", "636", "514", "587", "1701",
    "3268", "3269", "3389", "5985", "5986"
 
    To specify ports, provide an integer or an array of integers. Example: "22", "80", "443"
    .PARAMETER Report
    Generates a report in the C:\temp folder if specified.
    .PARAMETER LocalSubnets
    Scans subnets connected to the local device. It will not scan outside of the hosting device's subnet.
    .PARAMETER NoHops
    Prevents scans across a gateway.
    .PARAMETER AddService
    Includes the service name associated with each port in the output.
    .PARAMETER Computers
    Scans a single host or an array of hosts using subnet ID in CIDR notation, IP address, NETBIOS name, or FQDN in double quotes.
    Example: "10.11.1.0/24", "10.11.2.0/24"
    .PARAMETER ThrottleLimit
    Specifies the number of concurrent threads. Default: 32.
    .PARAMETER ScanOnPingFail
    Scans a host even if ping fails.
    .EXAMPLE
    Get-NetworkAudit -Report
    Generates a report of the network audit results in the C:\temp folder.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-NetworkAudit
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-NetworkAudit
#>


    [OutputType([pscustomobject])]
    [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param (
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [ValidateRange(1, 65535)]
        [Int32[]]$Ports,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Default',
            HelpMessage = 'Automatically find and scan local attached subnets',
            ValueFromPipelineByPropertyName = $true,
            Position = 1
        )]
        [switch]$LocalSubnets,
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Computers',
            HelpMessage = 'Scan host or array of hosts using Subet ID in CIDR Notation, IP, NETBIOS, or FQDN in "quotes"',
            ValueFromPipelineByPropertyName = $true,
            Position = 1
        )]
        [string[]]$Computers,
        [Parameter(
            HelpMessage = 'Number of concurrent threads. Default: 32.',
            ValueFromPipelineByPropertyName = $true,
            Position = 2
        )]
        [Int32]$ThrottleLimit = 32,
        [Parameter(
            HelpMessage = 'Build a list of IPs that are not beyond 1 hop.',
            ValueFromPipelineByPropertyName = $true
        )]
        [switch]$NoHops,
        [Parameter(
            HelpMessage = 'Add Service Name to Port Number in output.',
            ValueFromPipelineByPropertyName = $true
        )]
        [switch]$AddService,
        [Parameter(
            HelpMessage = 'Output a report to C:\temp. The function will output the full path to the report as a string.',
            ValueFromPipelineByPropertyName = $true
        )]
        [switch]$Report,
        [Parameter(
            HelpMessage = 'Scan all hosts even if ping fails.',
            ValueFromPipelineByPropertyName = $true
        )]
        [switch]$ScanOnPingFail
    )
    begin {
        # Create logging object
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        # Begin Logging
        Write-AuditLog "Begin Log"
        # Check if PSnmap module is installed, if not install it.
        # Tested Version:
        # https://www.powershellgallery.com/packages/PSnmap/1.3.1 Updated: 7/18/2018
        $params = @{
            PublicModuleNames      = "PSnmap"
            PublicRequiredVersions = "1.3.1"
            Scope                  = "CurrentUser"
        }
        Initialize-ModuleEnv @params
        # Set default ports to scan
        if (!($ports)) {
            [Int32[]]$ports = "21", "22", "23", "25", "53", "67", "68", "80", "443", `
                "88", "464", "123", "135", "137", "138", "139", `
                "445", "389", "636", "514", "587", "1701", `
                "3268", "3269", "3389", "5985", "5986"
        }
        switch ($ScanOnPingFail) {
            $true { $noping = $true }
            Default { $noping = $false }
        }
    } # End of begin block
    process {
        if ($LocalSubnets) {
            # Get connected networks on the local device.
            $internetadapter = Get-NetIPConfiguration -Detailed | Where-Object { $_.NetProfile.IPv4Connectivity -eq "Internet" }
            $subnetcidr = "$($internetadapter.IPv4Address.IPAddress)/$($internetadapter.IPv4Address.PrefixLength)"
            $CalcSub = Invoke-PSipcalc -NetworkAddress $subnetcidr -Enumerate
            # Get subnet in CIDR format
            $subnet = "$($CalcSub.NetworkAddress)/$($CalcSub.NetworkLength)"
            # Get DHCP server for the network
            $DHCPServer = (Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.IPAddress -eq $($internetadapter.IPv4Address.IPAddress) }).DHCPServer
            # Create Network Scan Object
            Write-AuditLog "Beggining scan of subnet $($subnet) for the following ports:"
            Write-AuditLog "$(($ports | Out-String -Stream) -join ",")"
            # Begin Reigion Build NetworkAudit Object
            if ($NoHops) {
                $IPRange = $CalcSub.IPEnumerated
                # Use a foreach loop to test each IP address
                $NonRoutedIPs, $FailedIps = Get-QuickPing -IPRange $IPRange -TTL 1
                if ($null -ne $NonRoutedIPs) {
                    Write-AuditLog "Local IPs object is populated."
                    Write-AuditLog "Scan found $($NonRoutedIPs.count) IPs to scan."
                    Write-AuditLog "There were $($FailedIps.count) IPs that failed to scan."
                    if ( $PSCmdlet.ShouldProcess( "NoHops", "Please confirm the following ips are ok to scan before proceeding:`n$($NonRoutedIPs -join ",")" ) ) {
                        Write-AuditLog "Begin Invoke-PSnmap"
                        $NetworkAudit = Invoke-PSnmap -ComputerName $NonRoutedIPs -Port $ports -ThrottleLimit $ThrottleLimit -Dns -NoSummary -ScanOnPingFail:$ScanOnPingFail -AddService:$AddService
                    } # End Region If $PSCmdlet.ShouldProcess
                }
                else {
                    throw "No Hosts found to scan!"
                }
            }
            else {
                $NetworkAudit = Invoke-PSnmap -ComputerName $subnet -Port $ports -ThrottleLimit $ThrottleLimit -Dns -NoSummary -ScanOnPingFail:$ScanOnPingFail -AddService:$AddService
            }
            # End Reigion Build Network Audit Object
            # Write out information about the network scan.
            Write-AuditLog "##########################################"
            Write-AuditLog "Network scan for Subnet $($Subnet) completed."
            Write-AuditLog "DHCP Server: $($DHCPServer)"
            Write-AuditLog "Gateway: $($internetadapter.IPv4DefaultGateway.nexthop)"
            Write-AuditLog "##########################################"
            Write-AuditLog "Starting with $(($NetworkAudit).count) output objects."
            # Filter devices that don't ping as no results will be found.
            $scan = Build-NetScanObject -NetScanObject $NetworkAudit -IncludeNoPing:$noping #-IncludeNoPing
            Write-AuditLog "Created $(($scan).count) output objects for the following hosts:"
            Write-AuditLog "$(($scan | Select-Object "IP/DNS")."IP/DNS" -join ", ")"
            # Normalize Subnet text for filename.
            $subnetText = $(($subnet.Replace("/", "_")))
            # Add the scan to the function output.
            $results = $scan
        } # End If $LocalSubnets
        elseif ($Computers) {
            $Subnet = $Computers
            if ($NoHops) {
                $IPRange = $Subnet
                $NonRoutedIPs, $FailedIps = Get-QuickPing -IPRange $IPRange -TTL 1
                if ($null -ne $NonRoutedIPs ) {
                    Write-AuditLog "Local IPs object is populated."
                    Write-AuditLog "Scan found $($NonRoutedIPs.count) IPs to scan."
                    if ($FailedIps -eq "NoIPs") {
                        $FailedIpsCount = 0
                    }
                    else {
                        $FailedIpsCount = $FailedIps.count
                    }
                    Write-AuditLog "There were $FailedIpsCount IPs that failed to scan."
                    # Begin Region If $PSCmdlet.ShouldProcess
                    if ( $PSCmdlet.ShouldProcess( "NoHops", "Please confirm the following ips are ok to scan before proceeding:`n$($NonRoutedIPs -join ",")" ) ) {
                        Write-AuditLog "Begin Invoke-PSnmap"
                        $scan = Invoke-PSnmap -ComputerName $NonRoutedIPs -Port $ports -ThrottleLimit $ThrottleLimit -Dns -NoSummary -AddService:$AddService
                    } # End Region If $PSCmdlet.ShouldProcess
                    $results = Build-NetScanObject -NetScanObject $scan -IncludeNoPing:$noping
                }
                else {
                    throw "No Hosts found to scan!"
                }
            }
            else {
                switch ($ScanOnPingFail) {
                    $true { $noping = $true }
                    Default { $noping = $false }
                }
                Write-AuditLog "Begin Invoke-PSnmap"
                $scan = Invoke-PSnmap -ComputerName $Subnet -Port $ports -ThrottleLimit $ThrottleLimit -Dns -NoSummary -AddService:$AddService
                $results = Build-NetScanObject -NetScanObject $scan -IncludeNoPing:$noping
            }
        }
    }
    # Process Close
    end {
        if ($Report) {
            $csv = "C:\temp\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss')).$($env:USERDOMAIN)_HostScan_$subnetText.csv"
            $zip = $csv -replace ".csv", ".zip"
            $log = $csv -replace ".csv", ".AuditLog.csv"
            Write-AuditLog -EndFunction
            Build-ReportArchive -Export $results -csv $csv -zip $zip -log $log -AttachmentFolderPath "C:\temp" -ErrorVariable BuildErr
        }
        else {
            return $results
        }
    }# End Close
}
#EndRegion '.\Public\Get-NetworkAudit.ps1' 231
#Region '.\Public\Get-QuickPing.ps1' 0
function Get-QuickPing {
<#
    .SYNOPSIS
    Performs a quick ping on a range of IP addresses and returns an array of IP addresses
    that responded to the ping and an array of IP addresses that failed to respond.
    .DESCRIPTION
    This function performs a quick ping on a range of IP addresses specified by the IPRange parameter.
    The ping is done with a Time-to-Live (TTL) value of 128 (by default). The function returns an array
    of IP addresses that responded to the ping and an array of IP addresses that failed to respond.
    This function has specific behaviors depending on the PowerShell version. For PowerShell 7 and
    above, it uses the 'Test-Connection' cmdlet's '-OutVariable' parameter.
    .PARAMETER IPRange
    Specifies a range of IP addresses to ping. Can be a string with a single IP address.
    .PARAMETER TTL
    Specifies the Time-to-Live (TTL) value to use for the ping. The default value is 128.
    .PARAMETER BufferSize
    Specifies the size of the buffer to use for the ping. The default value is 16.
    .PARAMETER Count
    Specifies the number of times to send the ping request. The default value is 1.
    .EXAMPLE
    Get-QuickPing -IPRange 192.168.1.1
    Performs a quick ping on the IP address 192.168.1.1 with a TTL of 128 and returns an
    array of IP addresses that responded to the ping and an array of IP addresses that
    failed to respond.
    .EXAMPLE
    Get-QuickPing -IPRange "192.168.1.1", "192.168.1.2", "192.168.1.3"
    Performs a quick ping on the IP addresses 192.168.1.1, 192.168.1.2, and 192.168.1.3 with
    a TTL of 128 and returns an array of IP addresses that responded to the ping and an array
    of IP addresses that failed to respond.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-QuickPing
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-QuickPing
#>

    param (
        $IPRange,
        [int]$TTL = 128,
        [int32]$BufferSize = 16,
        [int32]$Count = 1
    )
    begin {
        if (!$Script:LogString) {
            if (!($script:LogString)) {
                Write-AuditLog -Start
            }
            else {
                Write-AuditLog -BeginFunction
            }
        }
        $FailedToPing = @()
        $Success = @()
        $TotalIPs = $IPRange.Count
        $ProcessedIPs = 0
    }
    process {
        foreach ($IP in $IPRange) {
            $ProcessedIPs++
            $ProgressPercentage = ($ProcessedIPs / $TotalIPs) * 100
            Write-Progress -Activity "Scanning IP addresses" -Status "Scanning $IP ($ProcessedIPs of $TotalIPs)" -PercentComplete $ProgressPercentage
            try {
                if ($PSVersionTable.PSVersion.Major -ge 7) {
                    [void](Test-Connection $IP -BufferSize $BufferSize -TimeToLive $TTL -Count $Count -ErrorAction Stop -OutVariable test)
                    if ($test.Status -eq 'Success') {
                        Write-AuditLog "$IP Found!" -Severity Information
                        $Success += $IP
                    }
                    else {
                        $FailedToPing += $IP
                    }
                }
                else {
                    try {
                        [void](Test-Connection $IP -BufferSize $BufferSize -TimeToLive $TTL -Count $Count -ErrorAction Stop)
                        Write-AuditLog "$IP Found!"
                        $Success += $IP
                    }
                    catch {
                        $FailedToPing += $IP
                    }
                }
            }
            catch { throw $_.Exception}
        }
        if ($null -eq $FailedToPing) {
            $FailedtoPing = "NoIPs"
        }
        if ($null -eq $Success) {
            $FailedtoPing = "NoIPs"
        }
    }
    end {
        Write-AuditLog -EndFunction
        return $Success, $FailedToPing
    }
}
#EndRegion '.\Public\Get-QuickPing.ps1' 96
#Region '.\Public\Get-WebCertAudit.ps1' 0
function Get-WebCertAudit {
<#
    .SYNOPSIS
    Retrieves the certificate information for a web server.
    .DESCRIPTION
    The Get-WebCert function retrieves the certificate information for
    a web server by creating a TCP connection and using SSL to retrieve
    the certificate information.
    .PARAMETER Url
    The URL of the web server.
    .EXAMPLE
    Get-WebCert -Url "https://www.example.com"
    This example retrieves the certificate information for the web server at https://www.example.com.
    .OUTPUTS
    PSCustomObject
    Returns a PowerShell custom object with the following properties:
 
    Subject: The subject of the certificate.
    Thumbprint: The thumbprint of the certificate.
    Expires: The expiration date of the certificate.
    .NOTES
    This function requires access to the target web server over port 443 (HTTPS).
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Get-WebCertAudit
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Get-WebCertAudit
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [string[]]$Url
    )
    begin {
        #Write-AuditLog -Start
        $Export = @()
    }
    process {
        foreach ($link in $Url) {
            $Req = [System.Net.Sockets.TcpClient]::new($link, '443')
            $Stream = [System.Net.Security.SslStream]::new($Req.GetStream())
            $Stream.AuthenticateAsClient($link)
            $hash = [ordered]@{
                URL        = $link
                Subject    = $Stream.RemoteCertificate.Subject
                Thumbprint = $Stream.RemoteCertificate.GetCertHashString()
                Expires    = $Stream.RemoteCertificate.GetExpirationDateString()
            }
            New-Object -TypeName PSCustomObject -Property $hash -OutVariable PSObject | Out-Null
            $Export += $PSObject
        }
    }
    end {
        #Write-AuditLog -EndFunction
        return $Export
    }
}
#EndRegion '.\Public\Get-WebCertAudit.ps1' 57
#Region '.\Public\Join-CSVFile.ps1' 0
function Join-CSVFile {
<#
    .SYNOPSIS
    Joins multiple CSV files with the same headers into a single CSV file.
    .DESCRIPTION
    The Join-CSVFiles function takes an array of CSV file paths, reads their
    contents, and merges them into a single CSV file. The output file is saved
    to the specified folder. All input CSV files must have the same headers
    for the function to work correctly.
    .PARAMETER CSVFilePaths
    An array of strings containing the file paths of the CSV files to be merged.
    .PARAMETER AttachmentFolderPath
    The output folder path where the merged CSV file will be saved. Default location is "C:\temp\MergedCSV".
    .EXAMPLE
    Join-CSVFiles -CSVFilePaths @("C:\path\to\csv1.csv", "C:\path\to\csv2.csv") -AttachmentFolderPath "C:\path\to\output.csv"
 
    This example will merge the contents of "C:\path\to\csv1.csv" and
    "C:\path\to\csv2.csv" into a single CSV file and save it in "C:\path\to\output.csv".
    .NOTES
    Make sure the input CSV files have the same headers and formatting for the function to work properly.
    .OUTPUTS
    None. The function outputs a merged CSV file to the specified folder.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Join-CSVFiles
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Join-CSVFiles
#>

    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string[]]$CSVFilePaths,
        [Parameter(
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$AttachmentFolderPath = "C:\temp\MergedCSV"
    )

    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Initialize-DirectoryPath -DirectoryPath $AttachmentFolderPath
        [string]$OutputCsv = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss')).$($env:USERDOMAIN).JoinedCSVs.csv"
        $baseHeaders = $null
        $mergedData = @()
        Write-AuditLog "Starting CSV file merge"
    }
    process {
        foreach ($csvPath in $CSVFilePaths) {
            if (-not (Test-Path -Path $csvPath -PathType Leaf)) {
                Write-AuditLog "File not found: $csvPath"
                throw "File not found: $csvPath"
            }
            $csvContent = Import-Csv -Path $csvPath
            if ($csvContent.Count -eq 0) {
                Write-AuditLog "Empty CSV file: $csvPath" -Severity Warning
                throw "Empty CSV file: $csvPath"
            }
            if ($null -eq $baseHeaders) {
                $baseHeaders = $csvContent[0].PSObject.Properties.Name
            }
            $currentHeaders = $csvContent[0].PSObject.Properties.Name
            if ($null -ne (Compare-Object -ReferenceObject $baseHeaders -DifferenceObject $currentHeaders)) {
                Write-AuditLog "CSV headers do not match for file: $csvPath" -Severity Error
                continue
            }
            $mergedData += $csvContent
            Write-AuditLog "Processed CSV file: $csvPath"
        }
    }
    end {
        $mergedData | Export-Csv -Path $OutputCsv -NoTypeInformation
        Write-AuditLog "CSV file merge completed: $OutputCsv"
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Public\Join-CSVFile.ps1' 83
#Region '.\Public\Merge-ADAuditZip.ps1' 0
function Merge-ADAuditZip {
    <#
    .SYNOPSIS
    Combines multiple audit report files into a single compressed ZIP file.
    .DESCRIPTION
    The Merge-ADAuditZip function combines multiple audit report files into a single
    compressed ZIP file. The function takes an array of file paths, a maximum file
    size for the output ZIP file, an output folder for the merged file, and an optional
    switch to open the directory of the merged file after creation.
    .PARAMETER FilePaths
    Specifies an array of file paths to be merged into a single compressed ZIP file.
    .PARAMETER MaxFileSize
    Specifies the maximum file size (in bytes) for the output ZIP file. The default
    value is 24 MB.
    .PARAMETER OutputFolder
    Specifies the output folder for the merged compressed ZIP file. The default folder
    is C:\temp.
    .PARAMETER OpenDirectory
    Specifies an optional switch to open the directory of the merged compressed ZIP
    file after creation.
    .EXAMPLE
    $workstations = Get-ADHostAudit -HostType WindowsWorkstations -Report
    $servers = Get-ADHostAudit -HostType WindowsServers -Report
    $nonWindows = Get-ADHostAudit -HostType "Non-Windows" -Report
    Merge-ADAuditZip -FilePaths $workstations, $servers, $nonWindows
 
    This example combines three audit reports for Windows workstations, Windows servers,
    and non-Windows hosts into a single compressed ZIP file.
    .EXAMPLE
    Merge-ADAuditZip -FilePaths C:\AuditReports\Report1.csv,C:\AuditReports\Report2.csv -MaxFileSize 50MB -OutputFolder C:\MergedReports -OpenDirectory
 
    This example merges two audit reports into a single compressed ZIP file with a maximum file size of 50 MB, an output folder of C:\MergedReports,
    and opens the directory of the merged compressed ZIP file after creation.
    .NOTES
    This function will split the output file into multiple parts if the maximum
    file size is exceeded. If the size exceeds the limit, a new ZIP file will be
    created with an incremental number added to the file name.
 
    This function may or may not work with various types of input.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Merge-ADAuditZip
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Merge-ADAuditZip
    #>

    param(
        [string[]]$FilePaths,
        [int]$MaxFileSize = 24MB,
        [string]$OutputFolder = "C:\temp",
        [switch]$OpenDirectory
    )
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    # Remove any blank file paths from the array
    if ($env:USERNAME -eq 'SYSTEM') {
        $DomainSuffix = $env:USERDOMAIN
    } else {
        $DomainSuffix = $env:USERDNSDOMAIN
    }
    $FilePaths = $FilePaths | Where-Object { $_ }
    # Create the output directory if it doesn't exist
    Initialize-DirectoryPath -DirectoryPath $OutputFolder
    # Create a hashtable to store the file sizes
    $fileSizes = @{}
    foreach ($filePath in $FilePaths) {
        $fileSizes[$filePath] = (Get-Item $filePath).Length
    }
    # Sort the files by size in descending order
    $sortedFiles = $fileSizes.GetEnumerator() | Sort-Object -Property Value -Descending | Select-Object -ExpandProperty Name
    # Build the output path
    $dateTimeString = (Get-Date).ToString('yyyy-MM-dd_hh.mm.ss')
    $domainName = $DomainSuffix
    $partCounter = 0
    $outputFileName = "$($dateTimeString)_$($domainName)_CombinedAudit.zip"
    $outputPath = Join-Path $OutputFolder $outputFileName
    # Add files to the zip until the maximum size is reached
    $currentSize = 0
    $filesToAdd = @()
    foreach ($filePath in $sortedFiles) {
        if (($currentSize + $fileSizes[$filePath]) -gt $MaxFileSize) {
            if ($partCounter -eq 0) {
                # If adding the next file would exceed the maximum size
                # Create a zip file with the current batch of files
                $partCounter++
                $outputFileName = "$($dateTimeString)_$($domainName)_CombinedAudit-part{0}.zip" -f $partCounter
                $outputPath = Join-Path $OutputFolder $outputFileName
            }
            Compress-Archive -Path $filesToAdd -DestinationPath $outputPath
            $filesToAdd = @() # Clear the list of files to add
            $currentSize = 0 # Reset current size counter
            $partCounter++
            $outputFileName = "$($dateTimeString)_$($domainName)_CombinedAudit-part{0}.zip" -f $partCounter
            $outputPath = Join-Path $OutputFolder $outputFileName
        }
        $filesToAdd += $filePath # Add the current file to the list of files to add
        $currentSize += $fileSizes[$filePath] # Add the size of the current file to the current size counter
    }
    # Create a zip file with the remaining files
    if ($filesToAdd) {
        Write-AuditLog "Compressing Archive with files $filesToAdd."
        Compress-Archive -Path $filesToAdd -DestinationPath $outputPath
    }

    foreach ($filePath in $FilePaths) {
        if ($filePath) {
            Remove-Item -Path $filePath -Force
        }
    }
    # Remove the original files
    if ($OpenDirectory) {
        # If the OpenDirectory switch is used
        Write-AuditLog "Build Complete. Opening output directory."
        Write-AuditLog -EndFunction
        Invoke-Item (Split-Path $outputPath) # Open the directory of the merged zip file
        return $outputPath
    }
    else {
        Write-AuditLog "Build Complete. Returning output file path."
        Write-AuditLog -EndFunction
        return $outputPath # Otherwise, only return the path of the merged zip file
    }
}

#EndRegion '.\Public\Merge-ADAuditZip.ps1' 127
#Region '.\Public\Merge-NmapToADHostAudit.ps1' 0
function Merge-NmapToADHostAudit {
<#
    .SYNOPSIS
    Merges Nmap network audit data with Active Directory host audit data.
    .DESCRIPTION
    The Merge-NmapToADHostAudit function takes in two CSV files, one containing Nmap network
    audit data and the other containing Active Directory host audit data. It merges the data
    based on matching IP addresses and hostnames, and exports the merged data to a new CSV file.
    Additionally, it exports any unmatched Nmap data to a separate CSV file.
    .PARAMETER ADAuditCsv
    The path to the Active Directory host audit CSV file.
    .PARAMETER NmapCsv
    The path to the Nmap network audit CSV file.
    .PARAMETER AttachmentFolderPath
    The output folder path where the merged CSV file and unmatched Nmap data CSV file will
    be saved. Default location is "C:\temp\NmapToADHostAudit".
    .EXAMPLE
    Merge-NmapToADHostAudit -ADAuditCsv "C:\path\to\ADAudit.csv" -NmapCsv "C:\path\to\NmapAudit.csv" -AttachmentFolderPath "C:\path\to\output"
 
    This example will merge the Active Directory host audit data in "C:\path\to\ADAudit.csv"
    with the Nmap network audit data in "C:\path\to\NmapAudit.csv" and save the merged data
    to a new CSV file in "C:\path\to\output". Unmatched Nmap data will also be saved to a
    separate CSV file in the same output folder.
    .NOTES
    Make sure the input CSV files have the correct headers and formatting for the function to work properly.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Merge-NmapToADHostAudit
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Merge-NmapToADHostAudit
#>

    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$ADAuditCsv,
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$NmapCsv,
        [Parameter(
            HelpMessage = 'Enter output folder path. Default: C:\temp\NmapXMLTOCSV',
            Position = 1,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]$AttachmentFolderPath = "C:\temp\NmapToADHostAudit"
    )
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }

        Initialize-DirectoryPath -DirectoryPath $AttachmentFolderPath
        # Variables
        $adAuditData = Import-Csv -Path $ADAuditCsv
        $nmapData = Import-Csv -Path $NmapCsv
        [string]$OutputCsv = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss')).$($env:USERDOMAIN).NmapJoinedADHostAudit.csv"
        [string]$UnmatchedNmapOutputCsv = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss')).$($env:USERDOMAIN).NmapUnjoinedToADAudit.csv"
    }
    process {
        Write-AuditLog "Processing Nmap data and grouping by hostname and IP address"
        # Group Nmap data by hostname and IP address
        $nmapDataGrouped = $nmapData | Group-Object -Property @{Expression = { $_.Hostname + $_.IPAddress } }
        # Combine the port, service, and version information
        $nmapDataGrouped = $nmapDataGrouped | ForEach-Object {
            $nmapRow = $_.Group[0]
            $hostname = $nmapRow.Hostname
            $ipAddresses = $_.Group | ForEach-Object { $_.IPAddress } | Sort-Object | Get-Unique
            $openPorts = $_.Group | ForEach-Object { $_.OpenPorts } | Sort-Object | Get-Unique
            $closedPorts = $_.Group | ForEach-Object { $_.ClosedPorts } | Sort-Object | Get-Unique
            $services = $_.Group | ForEach-Object { $_.Services } | Sort-Object | Get-Unique
            $versions = $_.Group | ForEach-Object { $_.Versions } | Sort-Object | Get-Unique
            $os = $_.Group | ForEach-Object { $_.OS } | Sort-Object | Get-Unique
            [PSCustomObject]@{
                Hostname    = $hostname
                IPAddress   = ($ipAddresses -join ', ')
                OpenPorts   = ($openPorts -join ', ')
                ClosedPorts = ($closedPorts -join ', ')
                Services    = ($services -join ', ')
                Versions    = ($versions -join ', ')
                OS          = ($os -join '; ')
            }
        }
        $mergedData = @()
        $unmatchedNmapData = @()
        Write-AuditLog "Processing ADAudit data and merging with Nmap data"
        # Process ADAudit data
        foreach ($adRow in $adAuditData) {
            $ip = $adRow.IPv4Address
            $hostname = $adRow.DNSHostName
            # ROws by ip and hostname
            $nmapRowsByIP = $nmapDataGrouped | Where-Object { $_.IPAddress -eq $ip }
            $nmapRowsByHostname = $nmapDataGrouped | Where-Object { $_.Hostname -eq $hostname }
            $nmapRow = if ($nmapRowsByIP) { $nmapRowsByIP[0] } elseif ($nmapRowsByHostname) { $nmapRowsByHostname[0] } else { $null }
            if (!$hostname -and $nmapRow.Hostname) {
                $hostname = $nmapRow.Hostname
            }
            if (!$ip -and $nmapRow.IPAddress) {
                $ip = $nmapRow.IPAddress
            }
            # Find additional IPs with the same hostname
            $additionalIPs = ($nmapDataGrouped | Where-Object { $_.Hostname -eq $hostname -and $_.IPAddress -ne $ip } | ForEach-Object { $_.IPAddress }) -join ', '
            # Consolidate duplicate hostnames and IP addresses before removing them from $nmapDataGrouped
            if ($nmapRowsByHostname.Count -gt 1) {
                $openPorts = ($nmapRowsByHostname.OpenPorts | ForEach-Object { $_.Split(', ') } | Sort-Object | Get-Unique) -join ', '
                $closedPorts = ($nmapRowsByHostname.ClosedPorts | ForEach-Object { $_.Split(', ') } | Sort-Object | Get-Unique) -join ', '
                $services = ($nmapRowsByHostname.Services | ForEach-Object { $_.Split(', ') } | Sort-Object | Get-Unique) -join ', '
                $versions = ($nmapRowsByHostname.Versions | ForEach-Object { $_.Split(', ') } | Sort-Object | Get-Unique) -join ', '
                $os = ($nmapRowsByHostname.OS | ForEach-Object { $_.Split('; ') } | Sort-Object | Get-Unique) -join '; '
                $nmapRow = [PSCustomObject]@{
                    Hostname    = $hostname
                    IPAddress   = ($nmapRowsByHostname.IPAddress | Sort-Object | Get-Unique) -join ', '
                    OpenPorts   = $openPorts
                    ClosedPorts = $closedPorts
                    Services    = $services
                    Versions    = $versions
                    OS          = $os
                }
            }
            $nmapDataGrouped = $nmapDataGrouped | Where-Object { !($_.Hostname -eq $hostname) } # Remove the matched additional IPs

            $mergedRow = [PSCustomObject]@{
                DNSHostName            = $hostname
                ComputerName           = $adRow.ComputerName
                Enabled                = $adRow.Enabled
                IPv4Address            = $ip
                AdditionalIPs          = $additionalIPs
                IPv6Address            = $adRow.IPv6Address
                OperatingSystem        = $adRow.OperatingSystem
                LastLogon              = $adRow.LastLogon
                LastSeen               = $adRow.LastSeen
                Created                = $adRow.Created
                Modified               = $adRow.Modified
                Description            = $adRow.Description
                GroupMemberships       = $adRow.GroupMemberships
                OrgUnit                = $adRow.OrgUnit
                KerberosEncryptionType = $adRow.KerberosEncryptionType
                SPNs                   = $adRow.SPNs
                Nmap_OpenPorts         = $nmapRow.OpenPorts
                Nmap_ClosedPorts       = $nmapRow.ClosedPorts
                Nmap_Services          = $nmapRow.Services
                Nmap_Versions          = $nmapRow.Versions
                Nmap_OS                = $nmapRow.OS
            }
            $mergedData += $mergedRow
            $nmapDataGrouped = $nmapDataGrouped | Where-Object { $_.IPAddress -ne $nmapRow.IPAddress -and $_.Hostname -ne $nmapRow.Hostname } # Remove matched Nmap row
        }
        Write-AuditLog "Processing unmatched Nmap data"
        # Process unmatched Nmap data
        foreach ($nmapRow in $nmapDataGrouped) {
            $mergedRow = [PSCustomObject]@{
                DNSHostName            = $nmapRow.Hostname
                ComputerName           = $null
                Enabled                = $null
                IPv4Address            = $nmapRow.IPAddress
                AdditionalIPs          = $null
                IPv6Address            = $null
                OperatingSystem        = $null
                LastLogon              = $null
                LastSeen               = $null
                Created                = $null
                Modified               = $null
                Description            = $null
                GroupMemberships       = $null
                OrgUnit                = $null
                KerberosEncryptionType = $null
                SPNs                   = $null
                Nmap_OpenPorts         = $nmapRow.OpenPorts
                Nmap_ClosedPorts       = $nmapRow.ClosedPorts
                Nmap_Services          = $nmapRow.Services
                Nmap_Versions          = $nmapRow.Versions
                Nmap_OS                = $nmapRow.OS
            }
            $mergedData += $mergedRow
            $unmatchedNmapData += $nmapRow # Add the unmatched Nmap row to the separate list
        }
    }
    end {
        Write-AuditLog "Exporting merged data to CSV file: $OutputCsv"
        $mergedData | Export-Csv -Path $OutputCsv -NoTypeInformation
        Write-AuditLog "Exporting unmatched Nmap data to CSV file: $UnmatchedNmapOutputCsv"
        $unmatchedNmapData | Export-Csv -Path $UnmatchedNmapOutputCsv -NoTypeInformation
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Public\Merge-NmapToADHostAudit.ps1' 191
#Region '.\Public\Send-AuditEmail.ps1' 0
function Send-AuditEmail {
<#
    .SYNOPSIS
    This is a wrapper function for Send-MailKitMessage and takes string arrays as input.
    .DESCRIPTION
    Other Audit tasks can be used as the -AttachmentFiles parameter when used with the report switch.
    .EXAMPLE
    Send-AuditEmail -SMTPServer "smtp.office365.com" -Port 587 -UserName "Username@contoso.com" `
    -From "Username@contoso.com" -To "user@anothercompany.com" -Pass (Read-Host -AsSecureString) -AttachmentFiles "$(Get-ADActiveUserAudit -Report)" -SSL
 
    This will automatically send the report zip via email to the parameters specified.
    There is no cleanup of files. Please cleanup the directory of zip's if neccessary.
    .EXAMPLE
    Send-AuditEmail -SMTPServer "smtp.office365.com" -Port 587 -UserName "Username@contoso.com" `
    -From "Username@contoso.com" -To "user@anothercompany.com" -AttachmentFiles "$(Get-ADActiveUserAudit -Report)" -FunctionApp "MyVaultFunctionApp" `
    -Function "MyClientSpecificFunction" -Token "ABCDEF123456" -SSL
 
    This will automatically send the report zip via email to the parameters specified.
    There is no cleanup of files. Please cleanup the directory of zip's if neccessary.
    .PARAMETER SMTPServer
    The SMTP Server address. For example: "smtp.office365.com"
    .PARAMETER AttachmentFiles
    The full filepath to the zip you are sending:
        -AttachmentFiles "C:\temp\ADHostAudit\2023-01-04_03.45.14_Get-ADHostAudit_AD.CONTOSO.COM.Servers.zip"
 
    The Audit reports output this filename if the "-Report" switch is used allowing it to be nested in this parameter
    for ease of automation.
    .PARAMETER Port
    The following ports can be used to send email:
        "993", "995", "587", "25"
    .PARAMETER UserName
    The Account authorized to send email via SMTP. From parameter is usually the same.
    .PARAMETER SSL
    Switch to ensure SSL is used during transport.
    .PARAMETER From
    This is who the email will appear to originate from. This is either the same as the UserName,
    or, if delegated, access to an email account the Username account has delegated permissions to send for.
    Link:
        https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/give-mailbox-permissions-to-another-user?view=o365-worldwide
    .PARAMETER To
    This is the mailbox who will be the recipient of the communication.
    .PARAMETER Subject
    The subject is automatically populated with the name of the function that ran the script,
    as well as the domain and hostname.
 
    If you specify subject in the parameters, it will override the default with your subject.
    .PARAMETER Body
    The body of the message, pre-populates with the same data as the subject line. Specify body text
    in the function parameters to override.
    .PARAMETER Pass
    Takes a SecureString as input. The password must be added to the command by using:
        -Pass (Read-Host -AsSecureString)
        You will be promted to enter the password for the UserName parameter.
    .PARAMETER Function
    If you are using the optional function feature and created a password retrieval function,
    this is the name of the function in Azure AD that accesses the vault.
    .PARAMETER FunctionApp
    If you are using the optional function feature, this is the name of the function app in Azure AD.
    .PARAMETER Token
    If you are using the optional function feature, this is the api token for the specific function.
    Ensure you are using the "Function Key" and NOT the "Host Key" to ensure access is only to the specific funtion.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Send-AuditEmail
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Send-AuditEmail
#>

    [CmdletBinding(DefaultParameterSetName = 'Pass')]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Enter the Zip file paths as comma separated array with quotes for each filepath',
            ValueFromPipelineByPropertyName = $true
        )][string[]]$AttachmentFiles, # Array of paths to zip files that will be attached to the email
        [string]$SMTPServer, # SMTP server for sending the email
        [Parameter(
            HelpMessage = 'Enter the port number for the mail relay',
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateSet("993", "995", "587", "25")]
        [int]$Port, # Port number for the mail relay
        [string]$UserName, # Username for SMTP authentication
        [switch]$SSL, # Whether to use SSL for the SMTP connection
        [string]$From, # Email address for the sender
        [string]$To, # Email address for the recipient
        [string]$Subject = "$($script:MyInvocation.MyCommand.Name -replace '\..*') report ran for $($env:USERDOMAIN) on host $($env:COMPUTERNAME).", # Email subject line
        [string]$Body = "$($script:MyInvocation.MyCommand.Name -replace '\..*') report ran for $($env:USERDOMAIN) on host $($env:COMPUTERNAME).", # Email body text
        [Parameter(
            ParameterSetName = 'Pass',
            HelpMessage = 'Enter this as the parameter: (Read-Host -AsSecureString)'
        )]
        [securestring]$Pass, # SecureString containing the password for SMTP authentication
        [Parameter(
            ParameterSetName = 'Func',
            Mandatory = $true,
            HelpMessage = 'Enter the name of the Function as showing in the function app'
        )]
        [string]$Function, # Name of the function in the Azure Function App
        [Parameter(
            ParameterSetName = 'Func',
            Mandatory = $true,
            HelpMessage = 'Enter the name of the function app'
        )]
        [string]$FunctionApp, # Name of the Azure Function App
        [Parameter(
            ParameterSetName = 'Func',
            Mandatory = $true,
            HelpMessage = 'Enter the API key associated with the function. Not the Host Key.'
        )]
        [string]$Token, # API key for the Azure Function App
        [Parameter(
            ParameterSetName = 'Func',
            Mandatory = $true,
            HelpMessage = 'Enter the local Client Certificate thumbprint associated and previously uploaded to the function.'
        )]
        [string]$CertificateThumbprint
    )
    begin {
        # Install/Import Required Module
        # Tested Version:
        # https://www.powershellgallery.com/packages/Send-MailKitMessage/3.2.0-preview1 Updated: 11/8/2021
        $params = @{
            PrereleaseModuleNames      = "Send-MailKitMessage"
            PrereleaseRequiredVersions = "3.2.0-preview1"
            Scope                      = "CurrentUser"
        }
        Initialize-ModuleEnv @params
        # Create recipient list
        $RecipientList = [MimeKit.InternetAddressList]::new()
        $RecipientList.Add([MimeKit.InternetAddress]$To)
        # Create attachment list
        $AttachmentList = [System.Collections.Generic.List[string]]::new()
        foreach ($currentItem in $attachmentfiles) {
            $AttachmentList.Add("$currentItem")
        }
        # From
        $From = [MimeKit.MailboxAddress]$From
        # Mail Account variable
        $User = $UserName
        if ($Pass) {
            # If the -Pass parameter is provided, set the credentials to the value of the parameter.
            $Credential = `
                [System.Management.AuTomation.PSCredential]::new($User, $Pass)
        }
        elseif ($FunctionApp) {
            # If a function app name and API key are provided, retrieve credentials from the function app URL.
            $url = "https://$($FunctionApp).azurewebsites.net/api/$($Function)"
            $cert = Get-ChildItem -Path Cert:\LocalMachine\My\ | Where-Object { $_.Thumbprint -eq $CertificateThumbprint }
            $a, $b = (Invoke-RestMethod -Uri $url -Method Get -Headers @{ 'x-functions-key' = "$Token" } -Certificate $cert ).split(',')
            $Credential = `
                [System.Management.AuTomation.PSCredential]::new($User, (ConvertTo-SecureString -String $a -Key $b.split(' ')) )
        }

    }
    Process {
        # Set the parameters for the email message
        $Parameters = @{
            "UseSecureConnectionIfAvailable" = $SSL
            "Credential"                     = $Credential
            "SMTPServer"                     = $SMTPServer
            "Port"                           = $Port
            "From"                           = $From
            "RecipientList"                  = $RecipientList
            "Subject"                        = $Subject
            "TextBody"                       = $Body
            "AttachmentList"                 = $AttachmentList
        }
        # Send the email using the Send-MailKitMessage cmdlet with the parameters above
        Send-MailKitMessage @Parameters
    }
    End {
        # Clear sensitive variables from memory
        Clear-Variable -Name "a", "b", "Credential", "Token" -Scope Local -ErrorAction SilentlyContinue
    }
}
#EndRegion '.\Public\Send-AuditEmail.ps1' 175
#Region '.\Public\Submit-FTPUpload.ps1' 0
function Submit-FTPUpload {
<#
    .SYNOPSIS
    Uploads a file to an FTP server using the WinSCP module.
    .DESCRIPTION
    The Submit-FTPUpload function uploads a file to an FTP server using the WinSCP module.
    The function takes several parameters, including the FTP server name, the username and
    password of the account to use, the protocol to use, and the file to upload.
    .PARAMETER FTPUserName
    Specifies the username to use when connecting to the FTP server.
    .PARAMETER Password
    Specifies the password to use when connecting to the FTP server.
    .PARAMETER FTPHostName
    Specifies the name of the FTP server to connect to.
    .PARAMETER Protocol
    Specifies the protocol to use when connecting to the FTP server. The default value is SFTP.
    .PARAMETER FTPSecure
    Specifies the level of security to use when connecting to the FTP server. The default value is None.
    .PARAMETER SshHostKeyFingerprint
    Specifies the fingerprint of the SSH host key to use when connecting to the FTP server. This parameter is mandatory with SFTP and SCP.
    .PARAMETER LocalFilePath
    Specifies the local path to the file to upload to the FTP server.
    .PARAMETER RemoteFTPPath
    Specifies the remote path to upload the file to on the FTP server.
    .OUTPUTS
    The function does not generate any output.
    .EXAMPLE
    PS C:\> Submit-FTPUpload -FTPUserName "username" -Password $Password -FTPHostName "ftp.example.com" -Protocol "Sftp" -FTPSecure "None" -SshHostKeyFingerprint "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff" -LocalFilePath "C:\temp\file.txt" -RemoteFTPPath "/folder"
 
    In this example, the Submit-FTPUpload function is used to upload a file to an FTP server.
    The FTP server is named "ftp.example.com" and the file to upload is located at "C:\temp\file.txt".
    The SSH host key fingerprint is also provided.
    .NOTES
    This function requires the WinSCP PowerShell module.
    .LINK
    https://github.com/CriticalSolutionsNetwork/ADAuditTasks/wiki/Submit-FTPUpload
    .LINK
    https://criticalsolutionsnetwork.github.io/ADAuditTasks/#Submit-FTPUpload
    .LINK
    https://winscp.net/eng/docs/library_powershell
#>

    [CmdletBinding()]
    param (
        [string]$FTPUserName, # FTP username
        [securestring]$Password, # FTP password
        [string]$FTPHostName, # FTP host name
        [ValidateSet("Sftp", "SCP", "FTP", "Webdav", "s3")]
        [string]$Protocol = "Sftp", # FTP protocol
        [ValidateSet("None", "Implicit ", "Explicit")]
        [string]$FTPSecure = "None", # FTP security
        #[int]$FTPPort = 0, # Not used
        # Mandatory with SFTP/SCP
        [string[]]$SshHostKeyFingerprint, # SSH host key fingerprint
        #[string]$SshPrivateKeyPath, # Not used
        [string[]]$LocalFilePath, # Local file path
        # Send-WinSCPItem
        # './remoteDirectory'
        [string]$RemoteFTPPath # Remote FTP path
    )
    process {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        # This script will run in the context of the user. Please be sure it's a local admin with cached credentials.
        # Required Modules
        Import-Module WinSCP
        # Capture credentials.
        $Credential = [System.Management.Automation.PSCredential]::new($FTPUserName, $Password)
        # Open the session using the SessionOptions object.
        $sessionOption = New-WinSCPSessionOption -Credential $Credential -HostName $FTPHostName -SshHostKeyFingerprint $SshHostKeyFingerprint -Protocol $Protocol -FtpSecure $FTPSecure
        # New-WinSCPSession sets the PSDefaultParameterValue of the WinSCPSession parameter for all other cmdlets to this WinSCP.Session object.
        # You can set it to a variable if you would like, but it is only necessary if you will have more then one session open at a time.
        $WinSCPSession = New-WinSCPSession -SessionOption $sessionOption
        # Check if the remote FTP path exists. If it doesn't, create it.
        if (!(Test-WinSCPPath -Path $RemoteFTPPath -WinSCPSession $WinSCPSession)) {
            New-WinSCPItem -Path $RemoteFTPPath -ItemType Directory -WinSCPSession $WinSCPSession
        }
        # Upload each file in the local file path array to the remote FTP path.
        $errorindex = 0
        foreach ($File in $LocalFilePath) {
            $sendvar = Send-WinSCPItem -Path $File -Destination $RemoteFTPPath -WinSCPSession $WinSCPSession -ErrorAction Stop -ErrorVariable SendWinSCPErr
            if ($sendvar.IsSuccess -eq $false) {
                Write-AuditLog $SendWinSCPErr -Severity Error
                $errorindex += 1
            }
        }
        # If there was an error during the file upload, throw an error and exit.
        if ($errorindex -ne 0) {
            Write-Output "Error"
            throw 1
        }
        # Close and remove the session object.
        Remove-WinSCPSession -WinSCPSession $WinSCPSession
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Public\Submit-FTPUpload.ps1' 100