Hawk.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\Hawk.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName Hawk.Import.DoDotSource -Fallback $false
if ($Hawk_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName Hawk.Import.IndividualFiles -Fallback $false
if ($Hawk_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }

function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
 
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
 
            This provides a central location to react to files being imported, if later desired
 
        .PARAMETER Path
            The path to the file to load
 
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
 
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )

    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }

    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }

    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }

    # Execute Postimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }

    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'Hawk' -Language 'en-US'

<#
.SYNOPSIS
    Add objects to the hawk app data
.DESCRIPTION
    Add objects to the hawk app data
.PARAMETER Name
    Name variable
.PARAMETER Value
    Value of of retieved data
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Add-HawkAppData {
    param
    (
        [string]$Name,
        [string]$Value
    )

    Out-LogFile ("Adding " + $value + " to " + $Name + " in HawkAppData") -Action

    # Test if our HawkAppData variable exists
    if ([bool](get-variable HawkAppData -ErrorAction SilentlyContinue)) {
        $global:HawkAppData | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
    }
    else {
        $global:HawkAppData = New-Object -TypeName PSObject
        $global:HawkAppData | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
    }

    # make sure we then write that out to the appdata storage
    Out-HawkAppData

}

<#
.SYNOPSIS
    Compress all hawk data for upload
    Compresses all files located in the $Hawk.FilePath folder
.DESCRIPTION
    Compress all hawk data for upload
    Compresses all files located in the $Hawk.FilePath folder
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Compress-HawkData {
    Out-LogFile ("Compressing all data in " + $Hawk.FilePath + " for Upload")
    # Make sure we don't already have a zip file
    if ($null -eq (Get-ChildItem *.zip -Path $Hawk.filepath)) { }
    else {
        Out-LogFile ("Removing existing zip file(s) from " + $Hawk.filepath)
        $allfiles = Get-ChildItem *.zip -Path $Hawk.FilePath
        # Remove the existing zip files
        foreach ($file in $allfiles) {
            $Error.Clear()
            Remove-Item $File.FullName -Confirm:$false -ErrorAction SilentlyContinue
            # Make sure we didn't throw an error when we tried to remove them
            if ($Error.Count -gt 0) {
                Out-LogFile "Unable to remove existing zip files from " + $Hawk.filepath + " please remove them manually"
                Write-Error -Message ("Unable to remove existing zip files from " + $Hawk.filepath + " please remove them manually") -ErrorAction Stop
            }
            else { }
        }
    }



    # Get all of the files in the output directory
    #[array]$allfiles = Get-ChildItem -Path $Hawk.filepath -Recurse
    #Out-LogFile ("Found " + $allfiles.count + " files to add to zip")

    # create the zip file name
    [string]$zipname = "Hawk_" + (Split-path $Hawk.filepath -Leaf) + ".zip"
    [string]$zipfullpath = Join-Path $env:TEMP $zipname

    Out-LogFile ("Creating temporary zip file " + $zipfullpath)

    # Load the zip assembly
    Add-Type -Assembly System.IO.Compression.FileSystem

    # Create the zip file from the current hawk file directory
    [System.IO.Compression.ZipFile]::CreateFromDirectory($Hawk.filepath, $zipfullpath)

    # Move the item from the temp directory to the full filepath
    Out-LogFile ("Moving file to the " + $hawk.filepath + " directory")
    Move-Item $zipfullpath (Join-Path $Hawk.filepath $zipname)

}

<#
.SYNOPSIS
    Convert a reportxml to html
.DESCRIPTION
    Convert a reportxml to html
.PARAMETER xml
    XML format
.PARAMETER xsl
    XLS format
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Convert-ReportToHTML {
    param
    (
        [Parameter(Mandatory = $true)]
        $Xml,
        [Parameter(Mandatory = $true)]
        $Xsl
    )

    begin {
        # Make sure that the files are there
        if (!(test-path $Xml)) {
            Write-Error "XML File not found for conversion" -ErrorAction Stop
        }
        if (!(test-path $Xsl)) {
            Write-Error "XSL File not found for Conversion" -ErrorAction Stop
        }
    }

    process {
        # Create the output file name
        $OutputFile = Join-Path (Split-path $xml) ((split-path $xml -Leaf).split(".")[0] + ".html")

        # Run the transform on the XML and produce the HTML
        $xslt = New-Object System.Xml.Xsl.XslCompiledTransform;
        $xslt.Load($xsl);
        $xslt.Transform($xml, $OutputFile);
    }
    end
    { }
}

Function Get-AllUnifiedAuditLogEntry {
    <#
    .SYNOPSIS
        Make sure we get back all of the unified audit log results for the search we are doing
    .DESCRIPTION
        Make sure we get back all of the unified audit log results for the search we are doing
    .PARAMETER UnifiedSearch
        The search parameters
    .PARAMETER StartDate
        The start date provided by user during Hawk Object Initialization
    .PARAMETER EndDate
        The end date provide by the user during Hawk Object Initialization
    .EXAMPLE
        Get-AllUnifiedAuditLogEntry
        Gets all unified auditlog entries
    .NOTES
        General notes
    #>

        param
        (
            [Parameter(Mandatory = $true)]
            [string]$UnifiedSearch,
            [datetime]$StartDate = $Hawk.StartDate,
            [datetime]$EndDate = $Hawk.EndDate
        )
    
        # Validate the incoming search command
        if (($UnifiedSearch -match "-StartDate") -or ($UnifiedSearch -match "-EndDate") -or ($UnifiedSearch -match "-SessionCommand") -or ($UnifiedSearch -match "-ResultSize") -or ($UnifiedSearch -match "-SessionId")) {
            Out-LogFile "Do not include any of the following in the Search Command" -isError
            Out-LogFile "-StartDate, -EndDate, -SessionCommand, -ResultSize, -SessionID" -isError
            Write-Error -Message "Unable to process search command, switch in UnifiedSearch that is handled by this cmdlet specified" -ErrorAction Stop
        }
    
        # Make sure key variables are null
        [string]$cmd = $null
    
        # build our search command to execute
        $cmd = $UnifiedSearch + " -StartDate `'" + (get-date ($StartDate) -UFormat %m/%d/%Y) + "`' -EndDate `'" + (get-date ($endDate) -UFormat %m/%d/%Y) + "`' -SessionCommand ReturnLargeSet -resultsize 5000 -sessionid " + (Get-Date -UFormat %H%M%S)
        Out-LogFile ("Running Unified Audit Log Search") -Action
        Out-Logfile $cmd -NoDisplay
    
        # Run the initial command
        $Output = $null
        # $Output = New-Object System.Collections.ArrayList
    
        # Setup our run variable
        $Run = $true
    
        # Convert the command string into a scriptblock to avoid Invoke-Expression
        $searchScript = [ScriptBlock]::Create($cmd)
    
        # Since we have more than 1k results we need to keep returning results until we have them all
        while ($Run) {
            $Output += & $searchScript
    
            # Check for null results if so warn and stop
            if ($null -eq $Output) {
                Out-LogFile ("Unified Audit log returned no results.") -Information
                $Run = $false
            }
            # Else continue
            else {
                # Sort our result set to make sure the higest number is in the last position
                $Output = $Output | Sort-Object -Property ResultIndex
    
                # if total result count returned is 0 then we should warn and stop
                if ($Output[-1].ResultCount -eq 0) {
                    Out-LogFile ("Returned Result count was 0") -Information
                    $Run = $false
                }
                # if our resultindex = our resultcount then we have everything and should stop
                elseif ($Output[-1].Resultindex -ge $Output[-1].ResultCount) {
                    Out-LogFile ("Retrieved all results.") -Information
                    $Run = $false
                }
    
                # Output the current progress
                Out-LogFile ("Retrieved:" + $Output[-1].ResultIndex.tostring().PadRight(5, " ") + " Total: " + $Output[-1].ResultCount) -Information
            }
        }
    
        # Convert our list to an array and return it
        [array]$Output = $Output
        return $Output
    }

Function Get-AzureADPSPermission {
    <#
    .SYNOPSIS
        Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments).
     
    .DESCRIPTION
        Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments)
        using Microsoft Graph API. This function retrieves and formats permission information for analysis
        of application and delegated permissions in your tenant.
     
    .PARAMETER DelegatedPermissions
        If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions
        switch is set, both application and delegated permissions will be returned.
     
    .PARAMETER ApplicationPermissions
        If set, will return application permissions. If neither this switch nor the DelegatedPermissions
        switch is set, both application and delegated permissions will be returned.
     
    .PARAMETER UserProperties
        The list of properties of user objects to include in the output. Defaults to DisplayName only.
     
    .PARAMETER ServicePrincipalProperties
        The list of properties of service principals (i.e. apps) to include in the output.
        Defaults to DisplayName only.
     
    .PARAMETER ShowProgress
        Whether or not to display a progress bar when retrieving application permissions (which could take some time).
     
    .PARAMETER PrecacheSize
        The number of users to pre-load into a cache. For tenants with over a thousand users,
        increasing this may improve performance of the script.
     
    .EXAMPLE
        PS C:\> Get-AzureADPSPermission | Export-Csv -Path "permissions.csv" -NoTypeInformation
        Generates a CSV report of all permissions granted to all apps.
     
    .EXAMPLE
        PS C:\> Get-AzureADPSPermission -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" }
        Get all apps which have application permissions for Directory.Read.All.
     
    .EXAMPLE
        PS C:\> Get-AzureADPSPermission -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId")
        Gets all permissions granted to all apps and includes additional properties for users and service principals.
     
    .NOTES
        This function requires Microsoft.Graph PowerShell module and appropriate permissions:
        - Application.Read.All
        - Directory.Read.All
    #>

        [CmdletBinding()]
        param(
            [switch] $DelegatedPermissions,
            [switch] $ApplicationPermissions,
            [string[]] $UserProperties = @("DisplayName"),
            [string[]] $ServicePrincipalProperties = @("DisplayName"),
            [switch] $ShowProgress,
            [System.Int32] $PrecacheSize = 999
        )
    
        # Verify Graph connection
        try {
            $tenant_details = Get-MgOrganization
        }
        catch {
            throw "You must call Connect-MgGraph before running this script."
        }
        Write-Verbose ("TenantId: {0}" -f $tenant_details.Id)
    
        # Cache objects
        $script:ObjectByObjectId = @{}
        $script:ObjectByObjectType = @{
            'ServicePrincipal' = @{}
            'User' = @{}
        }
    
        function CacheObject ($Object, $Type) {
            if ($Object) {
                $script:ObjectByObjectType[$Type][$Object.Id] = $Object
                $script:ObjectByObjectId[$Object.Id] = $Object
            }
        }
    
        function GetObjectByObjectId ($ObjectId) {
            if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) {
                Write-Verbose ("Querying Graph API for object '{0}'" -f $ObjectId)
                try {
                    $object = Get-MgDirectoryObject -DirectoryObjectId $ObjectId
                    # Determine type from OdataType
                    $type = $object.AdditionalProperties.'@odata.type'.Split('.')[-1]
                    CacheObject -Object $object -Type $type
                }
                catch {
                    Write-Verbose "Object not found."
                }
            }
            return $script:ObjectByObjectId[$ObjectId]
        }
    
        # Cache all service principals
        Write-Verbose "Retrieving all ServicePrincipal objects..."
        $servicePrincipals = Get-MgServicePrincipal -All
        foreach($sp in $servicePrincipals) {
            CacheObject -Object $sp -Type 'ServicePrincipal'
        }
        $servicePrincipalCount = $servicePrincipals.Count
    
        # Cache users
        Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize)
        $users = Get-MgUser -Top $PrecacheSize
        foreach($user in $users) {
            CacheObject -Object $user -Type 'User'
        }
    
        if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
            Write-Verbose "Retrieving OAuth2PermissionGrants..."
            $oauth2Grants = Get-MgOAuth2PermissionGrant -All
    
            foreach ($grant in $oauth2Grants) {
                if ($grant.Scope) {
                    $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object {
                        $scope = $_
    
                        $grantDetails = [ordered]@{
                            "PermissionType" = "Delegated"
                            "ClientObjectId" = $grant.ClientId
                            "ResourceObjectId" = $grant.ResourceId
                            "Permission" = $scope
                            "ConsentType" = $grant.ConsentType
                            "PrincipalObjectId" = $grant.PrincipalId
                        }
    
                        # Add service principal properties
                        if ($ServicePrincipalProperties.Count -gt 0) {
                            $client = $script:ObjectByObjectId[$grant.ClientId]
                            $resource = $script:ObjectByObjectId[$grant.ResourceId]
    
                            $insertAtClient = 2
                            $insertAtResource = 3
                            foreach ($propertyName in $ServicePrincipalProperties) {
                                $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
                                $insertAtResource++
                                $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
                                $insertAtResource++
                            }
                        }
    
                        # Add user properties
                        if ($UserProperties.Count -gt 0) {
                            $principal = if ($grant.PrincipalId) {
                                $script:ObjectByObjectId[$grant.PrincipalId]
                            } else { @{} }
    
                            foreach ($propertyName in $UserProperties) {
                                $grantDetails["Principal$propertyName"] = $principal.$propertyName
                            }
                        }
    
                        New-Object PSObject -Property $grantDetails
                    }
                }
            }
        }
    
        if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
            Write-Verbose "Retrieving AppRoleAssignments..."
    
            $i = 0
            foreach ($sp in $servicePrincipals) {
                if ($ShowProgress) {
                    Write-Progress -Activity "Retrieving application permissions..." `
                                -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
                                -PercentComplete (($i / $servicePrincipalCount) * 100)
                }
    
                $appRoleAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -All
    
                foreach ($assignment in $appRoleAssignments) {
                    if ($assignment.PrincipalType -eq "ServicePrincipal") {
                        $resource = $script:ObjectByObjectId[$assignment.ResourceId]
                        $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.AppRoleId }
    
                        $grantDetails = [ordered]@{
                            "PermissionType" = "Application"
                            "ClientObjectId" = $assignment.PrincipalId
                            "ResourceObjectId" = $assignment.ResourceId
                            "Permission" = $appRole.Value
                        }
    
                        if ($ServicePrincipalProperties.Count -gt 0) {
                            $client = $script:ObjectByObjectId[$assignment.PrincipalId]
    
                            $insertAtClient = 2
                            $insertAtResource = 3
                            foreach ($propertyName in $ServicePrincipalProperties) {
                                $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
                                $insertAtResource++
                                $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
                                $insertAtResource++
                            }
                        }
    
                        New-Object PSObject -Property $grantDetails
                    }
                }
            }
    
            if ($ShowProgress) {
                Write-Progress -Completed -Activity "Retrieving application permissions..."
            }
        }
    }

Function Get-HawkUserPath {
    <#
    .SYNOPSIS
        Gets the output folder path for a specific user in Hawk
    .DESCRIPTION
        Creates and returns the full path to a user's output folder within the Hawk
        file structure. Creates the folder if it doesn't exist.
    .PARAMETER User
        The UserPrincipalName of the user to create/get path for
    .EXAMPLE
        Get-HawkUserPath -User "user@contoso.com"
 
        Returns the full path to the user's output folder and creates it if it doesn't exist
    .OUTPUTS
        System.String
        Returns the full path to the user's output folder
    .NOTES
        Internal function used by Hawk cmdlets to manage user-specific output folders
    #>

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

    # Check if Hawk global object exists
    if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
        Initialize-HawkGlobalObject
    }

    # Join the Hawk filepath with the user's UPN for the output folder
    $userPath = Join-Path -Path $Hawk.FilePath -ChildPath $User

    # Create directory if it doesn't exist
    if (-not (Test-Path -Path $userPath)) {
        Out-LogFile "Making output directory for user $userPath"
        New-Item -Path $userPath -ItemType Directory -Force | Out-Null
    }

    return $userPath
}


<#
.SYNOPSIS
    Get the Location of an IP using the freegeoip.net rest API
.DESCRIPTION
    Get the Location of an IP using the freegeoip.net rest API
.PARAMETER IPAddress
    IP address of geolocation
.EXAMPLE
    Get-IPGeolocation
    Gets all IP Geolocation data of IPs that recieved
.NOTES
    General notes
#>

Function Get-IPGeolocation {

    Param
    (
        [Parameter(Mandatory = $true)]
        $IPAddress
    )

    # If we don't have a HawkAppData variable then we need to read it in
    if (!([bool](get-variable HawkAppData -erroraction silentlycontinue))) {
        Read-HawkAppData
    }

    # if there is no value of access_key then we need to get it from the user
    if ($null -eq $HawkAppData.access_key) {

        Out-LogFile "IpStack.com now requires an API access key to gather GeoIP information from their API.`nPlease get a Free access key from https://ipstack.com/ and provide it below." -Information

        # get the access key from the user
        $Accesskey = Read-Host "ipstack.com accesskey"

        # add the access key to the appdata file
        Add-HawkAppData -name access_key -Value $Accesskey
    }
    else {
        $Accesskey = $HawkAppData.access_key
    }

    # Check the global IP cache and see if we already have the IP there
    if ($IPLocationCache.ip -contains $IPAddress) {
        return ($IPLocationCache | Where-Object { $_.ip -eq $IPAddress } )
        Write-Verbose ("IP Cache Hit: " + [string]$IPAddress)
    }
    elseif ($IPAddress -eq "<null>"){
        write-Verbose ("Null IP Provided: " + $IPAddress)
                $hash = @{
                IP               = $IPAddress
                CountryName      = "NULL IP"
                RegionName       = "Unknown"
                RegionCode       = "Unknown"
                ContinentName    = "Unknown"
                City             = "Unknown"
                KnownMicrosoftIP = "Unknown"
            }
    }
    # If not then we need to look it up and populate it into the cache
    else {
        # URI to pull the data from
        $resource = "http://api.ipstack.com/" + $ipaddress + "?access_key=" + $Accesskey

        # Return Data from web
        $Error.Clear()
        $geoip = Invoke-RestMethod -Method Get -URI $resource -ErrorAction SilentlyContinue

        if (($Error.Count -gt 0) -or ($null -eq $geoip.type)) {
            Out-LogFile ("Failed to retreive location for IP " + $IPAddress) -isError
            $hash = @{
                IP               = $IPAddress
                CountryName      = "Failed to Resolve"
                RegionName       = "Unknown"
                RegionCode       = "Unknown"
                ContinentName    = "Unknown"
                City             = "Unknown"
                KnownMicrosoftIP = "Unknown"
            }
        }
        else {
            # Determine if this IP is known to be owned by Microsoft
            [string]$isMSFTIP = Test-MicrosoftIP -IPToTest $IPAddress -type $geoip.type
            if ($isMSFTIP){
                $MSFTIP =  $isMSFTIP
            }
            # Push return into a response object
            $hash = @{
                IP               = $geoip.ip
                CountryName      = $geoip.country_name
                ContinentName    = $geoip.continent_name
                RegionName       = $geoip.region_name
                RegionCode       = $geoip.region_code
                City             = $geoip.City
                KnownMicrosoftIP = $MSFTIP
            }
            $result = New-Object PSObject -Property $hash
        }

        # Push the result to the global IPLocationCache
        [array]$Global:IPlocationCache += $result

        # Return the result to the user
        return $result
    }
}

Function Get-SimpleAdminAuditLog {
    <#
    .SYNOPSIS
        Convert output from search-adminauditlog to be more human readable
    .DESCRIPTION
        Convert output from search-adminauditlog to be more human readable
    .PARAMETER SearchResults
        Results from query
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    Param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)
        ]
        $SearchResults
    )

    # Setup to process incoming results
    Begin {

        # Make sure the array is null
        [array]$ResultSet = $null

    }

    # Process thru what ever is comming into the script
    Process {

        # Deal with each object in the input
        $searchresults | ForEach-Object {

            # Reset the result object
            $Result = New-Object PSObject

            # Get the alias of the User that ran the command
            [string]$user = $_.caller

            # If it is null then replace with *** for admin call
            if ([string]::IsNullOrEmpty($user)) { $user = "***" }

            # if we have 'on behalf of' then we need to do some more processing to get the right value
            elseif ($_.caller -like "*on behalf of*") {
                $split = $_.caller.split("/")
                $Start = (($Split[3].split(" "))[0]).TrimEnd('"')
                $End = $Split[-1].trimend('"')

                [string]$User = $Start + " on behalf of " + $end
            }
            # If there is a / in the username lests simply it
            elseif ($_.caller -contains "/") {
                [string]$user = ($_.caller.split("/"))[-1]
            }
            # If none of the above or true just pass it thru
            else {
                [string]$user = $_.caller
            }

            # Build the command that was run
            $switches = $_.cmdletparameters
            [string]$FullCommand = $_.cmdletname

            # Get all of the switchs and add them in "human" form to the output
            foreach ($parameter in $switches) {

                # Format our values depending on what they are so that they are as close
                # a match as possible for what would have been entered
                switch -regex ($parameter.value) {

                    # If we have a multi value array put in then we need to break it out and add quotes as needed
                    '[;]'    {

                        # Reset the formatted value string
                        $FormattedValue = $null

                        # Split it into an array
                        $valuearray = $switch.current.split(";")

                        # For each entry in the array add quotes if needed and add it to the formatted value string
                        $valuearray | ForEach-Object {
                            if ($_ -match "[ \t]") { $FormattedValue = $FormattedValue + "`"" + $_ + "`";" }
                            else { $FormattedValue = $FormattedValue + $_ + ";" }
                        }

                        # Clean up the trailing ;
                        $FormattedValue = $FormattedValue.trimend(";")

                        # Add our switch + cleaned up value to the command string
                        $FullCommand = $FullCommand + " -" + $parameter.name + " " + $FormattedValue
                    }

                    # If we have a value with spaces add quotes
                    '[ \t]' { $FullCommand = $FullCommand + " -" + $parameter.name + " `"" + $switch.current + "`"" }

                    # If we have a true or false format them with :$ in front ( -allow:$true )
                    '^True$|^False$'    { $FullCommand = $FullCommand + " -" + $parameter.name + ":`$" + $switch.current }

                    # Otherwise just put the switch and the value
                    default { $FullCommand = $FullCommand + " -" + $parameter.name + " " + $switch.current }

                }
            }

            # Format our modified object
            if ([string]::IsNullOrEmpty($_.objectModified)) { $ObjModified = "" }
            else {
                $ObjModified = ($_.objectmodified.split("/"))[-1]
                $ObjModified = ($ObjModified.split("\"))[-1]
            }

            # Get just the name of the cmdlet that was run
            [string]$cmdlet = $_.CmdletName

            # Build the result object to return our values
            $Result | Add-Member -MemberType NoteProperty -Value $user -Name Caller
            $Result | Add-Member -MemberType NoteProperty -Value $cmdlet -Name Cmdlet
            $Result | Add-Member -MemberType NoteProperty -Value $FullCommand -Name FullCommand
            $Result | Add-Member -MemberType NoteProperty -Value ($_.rundate).ToUniversalTime() -Name 'RunDate(UTC)'
            $Result | Add-Member -MemberType NoteProperty -Value $ObjModified -Name ObjectModified

            # Add the object to the array to be returned
            $ResultSet = $ResultSet + $Result

        }
    }

    # Final steps
    End {
        # Return the array set
        Return $ResultSet
    }
}

function Get-SimpleUnifiedAuditLog {
    <#
    .SYNOPSIS
        Flattens nested Microsoft 365 Unified Audit Log records into a simplified format.
 
    .DESCRIPTION
        This function processes Microsoft 365 Unified Audit Log records by converting nested JSON data
        (stored in the AuditData property) into a flat structure suitable for analysis and export.
        It handles complex nested objects, arrays, and special cases like parameter collections.
 
        The function:
        - Preserves base record properties
        - Flattens nested JSON structures
        - Provides special handling for Parameters collections
        - Creates human-readable command reconstructions
        - Supports type preservation for data analysis
 
    .PARAMETER Record
        A PowerShell object representing a unified audit log record. Typically, this is the output
        from Search-UnifiedAuditLog and should contain both base properties and an AuditData
        property containing a JSON string of additional audit information.
 
    .PARAMETER PreserveTypes
        When specified, maintains the original data types of values instead of converting them
        to strings. This is useful when the output will be used for further PowerShell processing
        rather than export to CSV/JSON.
 
    .EXAMPLE
        $auditLogs = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -RecordType ExchangeAdmin
        $auditLogs | Get-SimpleUnifiedAuditLog | Export-Csv -Path "AuditLogs.csv" -NoTypeInformation
 
        Processes Exchange admin audit logs and exports them to CSV with all nested properties flattened.
 
    .EXAMPLE
        $userChanges = Search-UnifiedAuditLog -UserIds user@domain.com -Operations "Add-*"
        $userChanges | Get-SimpleUnifiedAuditLog -PreserveTypes |
            Where-Object { $_.ResultStatus -eq $true } |
            Select-Object CreationTime, Operation, FullCommand
 
        Gets all "Add" operations for a specific user, preserves data types, filters for successful operations,
        and selects specific columns.
 
    .OUTPUTS
        Collection of PSCustomObjects with flattened properties from both the base record and AuditData.
        Properties include:
        - All base record properties (RecordType, CreationDate, etc.)
        - Flattened nested objects with property names using dot notation
        - Individual parameters as Param_* properties
        - ParameterString containing all parameters in a readable format
        - FullCommand showing reconstructed PowerShell command (when applicable)
 
    .NOTES
        Author: Jonathan Butler
        Version: 2.0
        Development Date: December 2024
 
        The function is designed to handle any RecordType from the Unified Audit Log and will
        automatically adapt to changes in the audit log schema. Special handling is implemented
        for common patterns like Parameters collections while maintaining flexibility for
        other nested structures.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSObject]$Record,

        [Parameter(Mandatory = $false)]
        [switch]$PreserveTypes
    )

    begin {
        # Collection to store processed results
        $Results = @()

        function ConvertTo-FlatObject {
            <#
            .SYNOPSIS
                Recursively flattens nested objects into a single-level hashtable.
 
            .DESCRIPTION
                Internal helper function that converts complex nested objects into a flat structure
                using dot notation for property names. Handles special cases like Parameters arrays
                and preserves type information when requested.
            #>

            param (
                [Parameter(Mandatory = $true)]
                [PSObject]$InputObject,

                [Parameter(Mandatory = $false)]
                [string]$Prefix = "",

                [Parameter(Mandatory = $false)]
                [switch]$PreserveTypes
            )

            # Initialize hashtable for flattened properties
            $flatProperties = @{}

            # Process each property of the input object
            foreach ($prop in $InputObject.PSObject.Properties) {
                # Build the property key name, incorporating prefix if provided
                $key = if ($Prefix) { "${Prefix}_$($prop.Name)" } else { $prop.Name }

                # Special handling for Parameters array - common in UAL records
                if ($prop.Name -eq 'Parameters' -and $prop.Value -is [Array]) {
                    # Create human-readable parameter string
                    $paramStrings = foreach ($param in $prop.Value) {
                        "$($param.Name)=$($param.Value)"
                    }
                    $flatProperties['ParameterString'] = $paramStrings -join ' | '

                    # Create individual parameter properties
                    foreach ($param in $prop.Value) {
                        $paramKey = "Param_$($param.Name)"
                        $flatProperties[$paramKey] = $param.Value
                    }

                    # Reconstruct full command if Operation property exists
                    if ($InputObject.Operation) {
                        $paramStrings = foreach ($param in $prop.Value) {
                            # Format parameter values based on content
                            $value = switch -Regex ($param.Value) {
                                '\s' { "'$($param.Value)'" } # Quote values containing spaces
                                '^True$|^False$' { "`$$($param.Value.ToLower())" } # Format booleans
                                ';' { "'$($param.Value)'" } # Quote values containing semicolons
                                default { $param.Value }
                            }
                            "-$($param.Name) $value"
                        }
                        $flatProperties['FullCommand'] = "$($InputObject.Operation) $($paramStrings -join ' ')"
                    }
                    continue
                }

                # Handle different value types
                switch ($prop.Value) {
                    # Recursively process nested hashtables
                    { $_ -is [System.Collections.IDictionary] } {
                        $nestedObject = ConvertTo-FlatObject -InputObject $_ -Prefix $key -PreserveTypes:$PreserveTypes
                        $flatProperties += $nestedObject
                    }
                    # Process arrays (excluding Parameters which was handled above)
                    { $_ -is [System.Collections.IList] -and $prop.Name -ne 'Parameters' } {
                        if ($_.Count -gt 0) {
                            if ($_[0] -is [PSObject]) {
                                # Handle array of objects
                                for ($i = 0; $i -lt $_.Count; $i++) {
                                    $nestedObject = ConvertTo-FlatObject -InputObject $_[$i] -Prefix "${key}_${i}" -PreserveTypes:$PreserveTypes
                                    $flatProperties += $nestedObject
                                }
                            }
                            else {
                                # Handle array of simple values
                                $flatProperties[$key] = $_ -join "|"
                            }
                        }
                        else {
                            # Handle empty arrays
                            $flatProperties[$key] = [string]::Empty
                        }
                    }
                    # Recursively process nested objects
                    { $_ -is [PSObject] } {
                        $nestedObject = ConvertTo-FlatObject -InputObject $_ -Prefix $key -PreserveTypes:$PreserveTypes
                        $flatProperties += $nestedObject
                    }
                    # Handle simple values
                    default {
                        if ($PreserveTypes) {
                            # Keep original type if PreserveTypes is specified
                            $flatProperties[$key] = $_
                        }
                        else {
                            # Convert values to appropriate types
                            $flatProperties[$key] = switch ($_) {
                                { $_ -is [datetime] } { $_ }
                                { $_ -is [bool] } { $_ }
                                { $_ -is [int] } { $_ }
                                { $_ -is [long] } { $_ }
                                { $_ -is [decimal] } { $_ }
                                { $_ -is [double] } { $_ }
                                default { [string]$_ }
                            }
                        }
                    }
                }
            }

            return $flatProperties
        }
    }

    process {
        try {
            # Extract base properties excluding AuditData
            $baseProperties = $Record | Select-Object * -ExcludeProperty AuditData

            # Process AuditData if present
            $auditData = $Record.AuditData | ConvertFrom-Json
            if ($auditData) {
                # Flatten the audit data
                $flatAuditData = ConvertTo-FlatObject -InputObject $auditData -PreserveTypes:$PreserveTypes

                # Combine base properties with flattened audit data
                $combinedProperties = @{}
                $baseProperties.PSObject.Properties | ForEach-Object { $combinedProperties[$_.Name] = $_.Value }
                $flatAuditData.GetEnumerator() | ForEach-Object { $combinedProperties[$_.Key] = $_.Value }

                # Create and store the result
                $Results += [PSCustomObject]$combinedProperties
            }
        }
        catch {
            # Handle and log any processing errors
            Write-Warning "Error processing record: $_"
            $errorProperties = @{
                RecordType   = $Record.RecordType
                CreationDate = Get-Date
                Error        = $_.Exception.Message
                Record       = $Record
            }
            $Results += [PSCustomObject]$errorProperties
        }
    }

    end {
        # Define the ordered common schema properties
        $orderedProperties = @(
            'CreationTime',
            'Workload',
            'RecordType',
            'Operation',
            'ResultStatus',
            'ClientIP',
            'UserId',
            'Id',
            'OrganizationId',
            'UserType',
            'UserKey',
            'ObjectId',
            'Scope',
            'AppAccessContext'
        )

        # Process each result to ensure proper property ordering
        $orderedResults = $Results | ForEach-Object {
            $orderedObject = [ordered]@{}

            # Add ordered common schema properties first
            foreach ($prop in $orderedProperties) {
                if ($_.PSObject.Properties.Name -contains $prop) {
                    $orderedObject[$prop] = $_.$prop
                }
            }

            # Add ParameterString if it exists
            if ($_.PSObject.Properties.Name -contains 'ParameterString') {
                $orderedObject['ParameterString'] = $_.ParameterString

                # Add all Param_* properties immediately after ParameterString
                $_.PSObject.Properties |
                    Where-Object { $_.Name -like 'Param_*' } |
                    Sort-Object Name |
                    ForEach-Object {
                        $orderedObject[$_.Name] = $_.Value
                    }
            }

            # Add all remaining properties that aren't already added
            $_.PSObject.Properties |
                Where-Object {
                    $_.Name -notin $orderedProperties -and
                    $_.Name -ne 'ParameterString' -and
                    $_.Name -notlike 'Param_*'
                } |
                ForEach-Object {
                    $orderedObject[$_.Name] = $_.Value
                }

            # Return the ordered object
            [PSCustomObject]$orderedObject
        }

        # Return all processed results with ordered properties
        $orderedResults
    }
}


Function Import-AzureAuthenticationLog {
    <#
    .SYNOPSIS
        Takes in a set of azure Authentication logs and combines them into a unified output
    .DESCRIPTION
        Takes in a set of azure Authentication logs and combines them into a unified output
    .PARAMETER JsonConvertedLogs
        Logs that are converted
    .EXAMPLE
        Import-AzureAuthenticationLog
        Imprts Azure Auth logs
    .NOTES
        General notes
    #>

        Param([array]$JsonConvertedLogs)
    
        # Null out the output object
        $Listoutput = $null
        $baseproperties = $null
        $i = 0
    
        # Create the output list array
        $ListOutput = New-Object System.Collections.ArrayList
        $baseproperties = New-Object System.Collections.ArrayList
    
        # Process each entry in the array
        foreach ($entry in $JsonConvertedLogs) {
    
            if ([bool]($i % 25)) { }
            Else {
                Write-Progress -Activity "Converting Json Entries" -CurrentOperation ("Entry " + $i) -PercentComplete (($i / $JsonConvertedLogs.count) * 100) -Status ("Processing")
            }
    
            # null out a temp object and create it as a new custom ps object
            $processedentry = $null
            $processedentry = New-Object -TypeName PSobject
    
            # Look at each member of the entry ... we want to process each in turn and add them to a new object
            foreach ($member in ($entry | get-member -MemberType NoteProperty)) {
    
                # Identity unique properties and add to property list of base object if not present
                if ($baseproperties -contains $member.name) { }
                else {
                    $baseproperties.add($member.name) | Out-Null
                }
    
                # Switch statement to deal with known "special" properties
                switch ($member.name) {
                    # Extended properties can contain addtional values so we need to expand those
                    ExtendedProperties {
                        # Null check
                        if ($null -eq $entry.ExtendedProperties) { }
                        else {
                            # expand out each entry and add it to the base properties and to the property of our exported object
                            Foreach ($Object in $entry.ExtendedProperties) {
                                # Identity unique properties and add to property list of base object if not present
                                if ($baseproperties -contains $object.name) { }
                                else {
                                    $baseproperties.add($object.name) | out-null
                                }
    
                                # For some entries a property can appear in ExtendedProperties and as a normal property
                                # We need to deal with this situation
                                try {
                                    # Now add the entry from extendedproperties to the overall properties list
                                    $processedentry | Add-Member -MemberType NoteProperty -Name $object.name -Value $object.value -ErrorAction SilentlyContinue
                                }
                                catch {
                                    if ((($error[0].FullyQualifiedErrorId).split(",")[0]) -eq "MemberAlreadyExists") { }
                                }
                            }
    
                            # Convert our extended properties into a string and add that just for fidelity
                            # null the output string
                            [string]$epstring = $null
    
                            # Convert into a string that is , seperated but with : seperating name and value
                            foreach ($ep in $entry.extendedproperties) {
                                [string]$epstring += $ep.name + ":" + $ep.v + ","
                            }
    
                            # We also still want to add extendedproperties in as is just for fidelity
                            $processedentry | Add-Member -MemberType NoteProperty -Name ExtendedProperties -Value ($epstring.TrimEnd(","))
                        }
                    }
                    # Need to convert this from a system object into a string
                    # This is an initial pass at this might be a better way to do it
                    Actor {
                        if ($null -eq $entry.actor) { }
                        else {
                            # null the output string
                            [string]$actorstring = $null
    
                            # Convert into a string that is , seperated but with : seperating ID and type
                            foreach ($actor in $entry.actor) {
                                [string]$actorstring += $actor.id + ":" + $actor.type + ","
                            }
    
                            # Add the string to the output
                            $processedentry | Add-Member -MemberType NoteProperty -Name "Actor" -Value ($actorstring.TrimEnd(","))
                        }
                    }
                    Target {
                        if ($null -eq $entry.target) { }
                        else {
                            # null the output string
                            [string]$targetstring = $null
    
                            # Convert into a string that is , seperated but with : seperating ID and type
                            foreach ($target in $entry.target) {
                                [string]$targetstring += $target.id + ":" + $target.type + ","
                            }
    
                            # Add the string to the output
                            $processedentry | Add-Member -MemberType NoteProperty -Name "Target" -Value ($targetstring.TrimEnd(","))
                        }
                    }
                    Creationtime {
                        $processedentry | Add-Member -MemberType NoteProperty -Name CreationTime -value (get-date $entry.Creationtime -format g)
                    }
                    Default {
                        # For some entries a property can appear in ExtendedProperties and as a normal property
                        # We need to deal with this situation
                        try {
                            # Now add the entry from extendedproperties to the overall properties list
                            $processedentry | Add-Member -MemberType NoteProperty -Name $member.name -Value $entry.($member.name) -ErrorAction SilentlyContinue
                        }
                        catch {
                            if ((($error[0].FullyQualifiedErrorId).split(",")[0]) -eq "MemberAlreadyExists") { }
                        }
                    }
                }
            }
    
            # Increment our counter
            $i++
    
            # Add to output object
            $Listoutput.add($processedentry) | Out-Null
        }
    
        Write-Progress -Completed -Activity "Converting Json Entries" -Status " "
    
        # Build a base object using all unique property names
        $baseobject = $null
        $baseobject = New-Object -TypeName PSobject
        foreach ($propertyname in $baseproperties) {
            switch ($propertyname) {
                CreationTime { $baseobject | Add-Member -MemberType NoteProperty -Name $propertyname -Value (get-date 01/01/1900 -format g) }
                Default { $baseobject | Add-Member -MemberType NoteProperty -Name $propertyname -Value "Base" }
            }
        }
    
        # Add that object to the output
        $Listoutput.add($baseobject) | Out-Null
    
        # Base object HAS to be the first entry in the output so that when it is written to CSV it includes all properties
        [array]$sortedoutput = $Listoutput | Sort-Object -Property creationtime
        $sortedoutput = $sortedoutput | Where-Object { $_.ClientIP -ne 'Base' }
    
        # Build an ordered arry to use to order the output coloums
        # Key coloums that we want ordered at the begining of the output
        [array]$baseorder = "CreationTime", "UserId", "Workload", "ClientIP", "CountryName", "KnownMicrosoftIP"
    
        foreach ($coloumheader in $baseorder) {
            # If the coloum header exists as one of our base properties then add to to coloumorder array and remove from baseproperties list
            if ($baseproperties -contains $coloumheader) {
                [array]$coloumorder += $coloumheader
                $baseproperties.remove($coloumheader)
            }
            else { }
        }
    
        # Add all of the remaining base properties to the sort order array
        [array]$coloumorder += $baseproperties
    
        $sortedoutput = $sortedoutput | Select-Object $coloumorder
    
        # write-host $baseproperties
        return $sortedoutput
    }

Function Initialize-HawkGlobalObject {
    <#
.SYNOPSIS
    Create global variable $Hawk for use by all Hawk cmdlets.
.DESCRIPTION
    Creates the global variable $Hawk and populates it with information needed by the other Hawk cmdlets.
 
    * Checks for latest version of the Hawk module
    * Creates path for output files
    * Records target start and end dates for searches
.PARAMETER Force
    Switch to force the function to run and allow the variable to be recreated
.PARAMETER SkipUpdate
    Skips checking for the latest version of the Hawk Module
.PARAMETER DaysToLookBack
    Defines the # of days to look back in the availible logs.
    Valid values are 1-90
.PARAMETER StartDate
    First day that data will be retrieved
.PARAMETER EndDate
    Last day that data will be retrieved
.PARAMETER FilePath
    Provide an output file path.
.OUTPUTS
    Creates the $Hawk global variable and populates it with a custom PS object with the following properties
 
    Property Name Contents
    ========== ==========
    FilePath Path to output files
    DaysToLookBack Number of day back in time we are searching
    StartDate Calculated start date for searches based on DaysToLookBack
    EndDate One day in the future
    WhenCreated Date and time that the variable was created
.EXAMPLE
    Initialize-HawkGlobalObject -Force
 
    This Command will force the creation of a new $Hawk variable even if one already exists.
#>

    [CmdletBinding()]
    param
    (
        [switch]$Force,
        [switch]$SkipUpdate,
        [DateTime]$StartDate,
        [DateTime]$EndDate,
        [string]$FilePath
    )

    Function Test-LoggingPath {
        param([string]$PathToTest)

        # First test if the path we were given exists
        if (Test-Path $PathToTest) {

            # If the path exists verify that it is a folder
            if ((Get-Item $PathToTest).PSIsContainer -eq $true) {
                Return $true
            }
            # If it is not a folder return false and write an error
            else {
                Write-Information ("Path provided " + $PathToTest + " was not found to be a folder.")
                Return $false
            }
        }
        # If it doesn't exist then return false and write an error
        else {
            Write-Information ("Directory " + $PathToTest + " Not Found")
            Return $false
        }
    }

    Function New-LoggingFolder {
        [CmdletBinding(SupportsShouldProcess)]
        param([string]$RootPath)

        # Create a folder ID based on date
        [string]$TenantName = (Get-MGDomain | Where-Object { $_.isDefault }).ID
        [string]$FolderID = "Hawk_" + $TenantName.Substring(0, $TenantName.IndexOf('.')) + "_" + (Get-Date -UFormat %Y%m%d_%H%M).tostring()

        # Add that ID to the given path
        $FullOutputPath = Join-Path $RootPath $FolderID

        # Just in case we run this twice in a min lets not throw an error
        if (Test-Path $FullOutputPath) {
            Write-Information "Path Exists"
        }
        # If it is not there make it
        else {
            Write-Information ("Creating subfolder with name " + $FullOutputPath)
            $null = New-Item $FullOutputPath -ItemType Directory
        }

        Return $FullOutputPath
    }

    Function Set-LoggingPath {
        [CmdletBinding(SupportsShouldProcess)]
        param ([string]$Path)

        # If no value of Path is provided prompt and gather from the user
        if ([string]::IsNullOrEmpty($Path)) {

            # Setup a while loop so we can get a valid path
            Do {

                # Ask the customer for the output path
                [string]$UserPath = Read-Host "Please provide an output directory"

                # If the path is valid then create the subfolder
                if (Test-LoggingPath -PathToTest $UserPath) {

                    $Folder = New-LoggingFolder -RootPath $UserPath
                    $ValidPath = $true
                }
                # If the path if not valid then we need to loop thru again
                else {
                    Write-Information ("Path not a valid Directory " + $UserPath)
                    $ValidPath = $false
                }

            }
            While ($ValidPath -eq $false)
        }
        # If a value if provided go from there
        else {
            # If the provided path is valid then we can create the subfolder
            if (Test-LoggingPath -PathToTest $Path) {
                $Folder = New-LoggingFolder -RootPath $Path
            }
            # If the provided path fails validation then we just need to stop
            else {
                Write-Error ("Provided Path is not valid " + $Path) -ErrorAction Stop
            }
        }

        Return $Folder
    }

    Function New-ApplicationInsight {
        [CmdletBinding(SupportsShouldProcess)]
        param()
        # Initialize Application Insights client
        $insightkey = "b69ffd8b-4569-497c-8ee7-b71b8257390e"
        if ($Null -eq $Client) {
            Write-Output "Initializing Application Insights"
            $Client = New-AIClient -key $insightkey
        }
    }

    ### Main ###
    $InformationPreference = "Continue"

    if (($null -eq (Get-Variable -Name Hawk -ErrorAction SilentlyContinue)) -or ($Force -eq $true) -or ($null -eq $Hawk)) {

        # Setup Applicaiton insights
        New-ApplicationInsight

        ### Checking for Updates ###
        # If we are skipping the update log it
        if ($SkipUpdate) {
            Write-Information "Skipping Update Check"
        }
        # Check to see if there is an Update for Hawk
        else {
            Update-HawkModule
        }

        # Test if we have a connection to Microsoft Graph
        Write-Information "Testing Graph Connection"
        Test-GraphConnection

        # If the global variable Hawk doesn't exist or we have -force then set the variable up
        Write-Information "Setting Up initial Hawk environment variable"

        #### Checking log path and setting up subdirectory ###
        # If we have a path passed in then we need to check that otherwise ask
        if ([string]::IsNullOrEmpty($FilePath)) {
            [string]$OutputPath = Set-LoggingPath
        }
        else {
            [string]$OutputPath = Set-LoggingPath -path $FilePath
        }

        # We need to ask for start and end date if daystolookback was not set
        if ($null -eq $StartDate) {

            # Read in our # of days back or the actual start date
            $StartRead = Read-Host "`nPlease Enter First Day of Search Window (1-90, Date, Default 90)"

            # Determine if the input was a date time
            # True means it was NOT a datetime
            if ($Null -eq ($StartRead -as [DateTime])) {
                #### Not a Date time ####

                # if we have a null entry (just hit enter) then set startread to the default of 90
                if ([string]::IsNullOrEmpty($StartRead)) { $StartRead = 90 }
                elseif (($StartRead -gt 90) -or ($StartRead -lt 1)) {
                    Write-Information "Value provided is outside of valid Range 1-90"
                    Write-Information "Setting StartDate to default of Today - 90 days"
                    $StartRead = 90
                }

                # Calculate our startdate setting it to midnight
                [DateTime]$StartDate = ((Get-Date).AddDays(-$StartRead)).Date
                Write-Information ("Start Date: " + $StartDate + "")
            }
            elseif (!($null -eq ($StartRead -as [DateTime]))) {
                #### DATE TIME Provided ####

                # Convert the input to a date time object
                [DateTime]$StartDate = (Get-Date $StartRead).Date

                # Test to make sure the date time is > 90 and < today
                if ($StartDate -ge ((Get-date).AddDays(-90).Date) -and ($StartDate -le (Get-Date).Date)) {
                    #Valid Date do nothing
                }
                else {
                    Write-Information ("Date provided beyond acceptable range of 90 days.")
                    Write-Information ("Setting date to default of Today - 90 days.")
                    [DateTime]$StartDate = ((Get-Date).AddDays(-90)).Date
                }
            }
            else {
                Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop
            }
        }

        if ($null -eq $EndDate) {
            # Read in the end date
            $EndRead = Read-Host "`nPlease Enter Last Day of Search Window (1-90, date, Default Today)"

            # Determine if the input was a date time
            # True means it was NOT a datetime
            if ($Null -eq ($EndRead -as [DateTime])) {
                #### Not a Date time ####

                # if we have a null entry (just hit enter) then set startread to the default of 90
                if ([string]::IsNullOrEmpty($EndRead)) {
                    [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date
                }
                else {
                    # Calculate our startdate setting it to midnight
                    Write-Information ("End Date: " + $EndRead + " days.")
                    # Subtract 1 from the EndRead entry so that we get one day less for the purpose of how searching works with times
                    [DateTime]$EndDate = ((Get-Date).AddDays( - ($EndRead - 1))).Date
                }

                # Validate that the start date is further back in time than the end date
                if ($StartDate -gt $EndDate) {
                    Write-Error "StartDate Cannot be More Recent than EndDate" -ErrorAction Stop
                }
                else {
                    Write-Information ("End Date: " + $EndDate + "`n")
                }
            }
            elseif (!($null -eq ($EndRead -as [DateTime]))) {
                #### DATE TIME Provided ####

                # Convert the input to a date time object
                [DateTime]$EndDate = ((Get-Date $EndRead).AddDays(1)).Date

                # Test to make sure the end date is newer than the start date
                if ($StartDate -gt $EndDate) {
                    Write-Information "EndDate Selected was older than start date."
                    Write-Information "Setting EndDate to today."
                    [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date
                }
                elseif ($EndDate -gt (get-Date).AddDays(2)) {
                    Write-Information "EndDate to Far in the furture."
                    Write-Information "Setting EndDate to Today."
                    [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date
                }

                Write-Information ("Setting EndDate by Date to " + $EndDate + "`n")
            }

            else {
                Write-Error "Invalid date information provided. Could not determine if this was a date or an integer." -ErrorAction Stop
            }
        }

        # Determine if we have access to a P1 or P2 Azure Ad License
        # EMS SKU contains Azure P1 as part of the sku
        # This uses Graph instead of MSOL
        Test-GraphConnection
        if ([bool] (Get-MgSubscribedSku | Where-Object { ($_.SkuPartNumber -like "*aad_premium*") -or ($_.SkuPartNumber -like "*EMS*") -or ($_.SkuPartNumber -like "*E5*") -or ($_.SkuPartNumber -like "*G5*") } )) {
            Write-Information "Advanced Azure AD License Found"
            [bool]$AdvancedAzureLicense = $true
        }
        else {
            Write-Information "Advanced Azure AD License NOT Found"
            [bool]$AdvancedAzureLicense = $false
        }

        # Configuration Example, currently not used
        #TODO: Implement Configuration system across entire project
        Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value $Days -PassThru | Register-PSFConfig
        if ($OutputPath) {
            Set-PSFConfig -Module 'Hawk' -Name 'FilePath' -Value $OutputPath -PassThru | Register-PSFConfig
        }

        #TODO: Discard below once migration to configuration is completed
        $Output = [PSCustomObject]@{
            FilePath             = $OutputPath
            DaysToLookBack       = $Days
            StartDate            = $StartDate
            EndDate              = $EndDate
            AdvancedAzureLicense = $AdvancedAzureLicense
            WhenCreated          = (Get-Date -Format g)
        }

        # Create the script hawk variable
        Write-Information "Setting up Script Hawk environment variable`n"
        New-Variable -Name Hawk -Scope Script -value $Output -Force
        Out-LogFile "Script Variable Configured" -Information
        Out-LogFile ("Hawk Version: " + (Get-Module Hawk).version) -Information
        # Print each property of $Hawk on its own line
        foreach ($prop in $Hawk.PSObject.Properties) {
            # If the property value is $null or an empty string, display "N/A"
            $value = if ($null -eq $prop.Value -or [string]::IsNullOrEmpty($prop.Value.ToString())) {
                "N/A"
            } else {
                $prop.Value
            }
        
            Out-LogFile ("{0} = {1}" -f $prop.Name, $value) -Information
        }
        #### End of IF
    }

    else {
        Write-Information "Valid Hawk Object already exists no actions will be taken."
    }
}

<#
.SYNOPSIS
    Output hawk appdata to a file
.DESCRIPTION
    Output hawk appdata to a file
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Out-HawkAppData {
    $HawkAppdataPath = join-path $env:LOCALAPPDATA "Hawk\Hawk.json"
    $HawkAppdataFolder = join-path $env:LOCALAPPDATA "Hawk"

    # test if the folder exists
    if (test-path $HawkAppdataFolder) { }
    # if it doesn't we need to create it
    else {
        $null = New-Item -ItemType Directory -Path $HawkAppdataFolder
    }

    Out-LogFile ("Recording HawkAppData to file " + $HawkAppdataPath) -Action
    $global:HawkAppData | ConvertTo-Json | Out-File -FilePath $HawkAppdataPath -Force
}

Function Out-LogFile {
    <#
    .SYNOPSIS
        Writes output to a log file with a time date stamp.
    .DESCRIPTION
        Writes output to a log file with a time date stamp and appropriate prefixes
        based on the type of message. By default, messages are also displayed on the screen
        unless the -NoDisplay switch is used.
 
        Message types:
        - Action: Represent ongoing operations or procedures.
        - Error: Represent failures, exceptions, or error conditions that prevented successful execution.
        - Investigate (notice, silentnotice): Represent events that require attention or hold
        investigative value.
        - Information: Represent successful completion or informational status updates
        that do not require action or investigation.
 
    .PARAMETER string
        The log message to be written.
 
    .PARAMETER action
        Switch indicating the log entry is describing an action being performed.
 
    .PARAMETER isError
        Switch indicating the log entry represents an error condition or failure.
        The output is prefixed with [ERROR] in the log file.
 
    .PARAMETER notice
        Switch indicating the log entry requires investigation or special attention.
 
    .PARAMETER silentnotice
        Switch indicating additional investigative information that should not be
        displayed on the screen. This is logged to the file but suppressed in console output.
 
    .PARAMETER NoDisplay
        Switch indicating the message should only be written to the log file,
        not displayed in the console.
 
    .PARAMETER Information
        Switch indicating the log entry provides informational status or completion messages,
        for example: "Retrieved all results" or "Completed data export successfully."
 
    .EXAMPLE
        Out-LogFile "Routine scan completed."
 
        Writes a simple log message with a timestamp to the log file and displays it on the screen.
 
    .EXAMPLE
        Out-LogFile "Starting mailbox export operation" -action
 
        Writes a log message indicating an action is being performed.
        The output is prefixed with [ACTION] in the log file.
 
    .EXAMPLE
        Out-LogFile "Failed to connect to Exchange Online" -isError
 
        Writes a log message indicating an error condition.
        The output is prefixed with [ERROR] in the log file.
 
    .EXAMPLE
        Out-LogFile "Detected suspicious login attempt from external IP" -notice
 
        Writes a log message indicating a situation requiring investigation.
        The output is prefixed with [INVESTIGATE] and also recorded in a separate _Investigate.txt file.
 
    .EXAMPLE
        Out-LogFile "User mailbox configuration details" -silentnotice
 
        Writes investigative detail to the log and _Investigate.txt file without printing to the console.
        This is useful for adding detail to a previously logged [INVESTIGATE] event without cluttering the console.
 
    .EXAMPLE
        Out-LogFile "Retrieved all results successfully" -Information
 
        Writes a log message indicating a successful or informational event.
        The output is prefixed with [INFO], suitable for status updates or completion notices.
         
    .EXAMPLE
        Out-LogFile "Executing periodic health check" -NoDisplay
 
        Writes a log message to the file without displaying it on the console,
        useful for routine logging that doesn't need immediate user visibility.
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true)]
        [string]$string,
        [switch]$action,
        [switch]$notice,
        [switch]$silentnotice,
        [switch]$isError,
        [switch]$NoDisplay,
        [switch]$Information
    )

    Write-PSFMessage -Message $string -ModuleName Hawk -FunctionName (Get-PSCallstack)[1].FunctionName

    # Make sure we have the Hawk Global Object
    if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
        Initialize-HawkGlobalObject
    }

    # Get our log file path
    $LogFile = Join-path $Hawk.FilePath "Hawk.log"
    $ScreenOutput = -not $NoDisplay
    $LogOutput = $true

    # Get the current date
    [string]$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    [string]$logstring = ""

    # Build the log string based on the type of message
    if ($action) {
        $logstring = "[$timestamp] - [ACTION] - $string"
    }
    elseif ($isError) {
        $logstring = "[$timestamp] - [ERROR] - $string"
    }
    elseif ($notice) {
        $logstring = "[$timestamp] - [INVESTIGATE] - $string"

        # Write to the investigation file
        [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt"
        $logstring | Out-File -FilePath $InvestigateFile -Append
    }
    elseif ($silentnotice) {
        $logstring = "[$timestamp] - [INVESTIGATE] - Additional Information: $string"

        # Write to the investigation file
        [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt"
        $logstring | Out-File -FilePath $InvestigateFile -Append

        # Suppress regular output for silentnotice
        $ScreenOutput = $false
        $LogOutput = $false
    }
    elseif ($Information) {
        $logstring = "[$timestamp] - [INFO] - $string"
    }
    else {
        $logstring = "[$timestamp] - $string"
    }

    # Write to log file if enabled
    if ($LogOutput) {
        $logstring | Out-File -FilePath $LogFile -Append
    }

    # Write to screen if enabled
    if ($ScreenOutput) {
        Write-Information -MessageData $logstring -InformationAction Continue
    }
}

<#
.SYNOPSIS
    Sends the output of a cmdlet to a txt file and a clixml file
.DESCRIPTION
    Sends the output of a cmdlet to a txt file and a clixml file
.PARAMETER Object
    Incoming object data
.PARAMETER FilePrefix
    File name
.PARAMETER User
    User that the data is being exported from
.PARAMETER Append
    Change existing file
.PARAMETER xml
    xml file format
.PARAMETER csv
    csv file format
.PARAMETER txt
    txt file format
.PARAMETER json
    Export data in JSON format. The data will be converted using ConvertTo-Json with a depth of 100 to preserve object structure.
.PARAMETER Notice
    Notification that data retrieved meets the investigation criteria
.EXAMPLE
    Out-MultipleFileTime
    Determined what file is being used for export of data
.NOTES
    Need to review invesigation criteria of data being exported
#>

Function Out-MultipleFileType {
    param
    (
        [Parameter (ValueFromPipeLine = $true)]
        $Object,
        [Parameter (Mandatory = $true)]
        [string]$FilePrefix,
        [string]$User,
        [switch]$Append = $false,
        [switch]$xml = $false,
        [Switch]$csv = $false,
        [Switch]$txt = $false,
        [Switch]$json = $false,
        [Switch]$Notice

    )

    begin {

        # If no file types were specified then we need to error out here
        if (($xml -eq $false) -and ($csv -eq $false) -and ($txt -eq $false) -and ($json -eq $false)) {
            Out-LogFile "No output type specified on object" -isError
            Write-Error -Message "No output type specified on object" -ErrorAction Stop
        }

        # Null out our array
        [array]$AllObject = $null

        # Set the output path
        if ([string]::IsNullOrEmpty($User)) {
            $path = join-path $Hawk.filepath "\Tenant"
            # Test the path if it is there do nothing otherwise create it
            if (test-path $path) { }
            else {
                Out-LogFile ("Making output directory for Tenant " + $Path) -Action
                $Null = New-Item $Path -ItemType Directory
            }
        }
        else {
            $path = join-path $Hawk.filepath $user

            # Set a bool so we know this is a user output
            [bool]$UserOutput = $true
            # Build short name of user so that it is easier to read
            [string]$ShortUser = ($User.split('@'))[0]

            # Test the path if it is there do nothing otherwise create it
            if (test-path $path) { }
            else {
                Out-LogFile ("Making output directory for user " + $Path) -Action
                $Null = New-Item $Path -ItemType Directory
            }
        }

    }

    process {
        # Collect up all of the incoming data into a single object for processing and output
        [array]$AllObject = $AllObject + $Object

    }

    end {
        if ($null -eq $AllObject) {
            Out-LogFile "No Data Found" -Information
        }
        else {

            # Determine what file type or types we need to write this object into and output it
            # Output XML File
            if ($xml -eq $true) {
                # lets put the xml files in a seperate directory to not clutter things up
                $xmlpath = Join-path $Path XML
                if (Test-path $xmlPath) { }
                else {
                    Out-LogFile ("Making output directory for xml files " + $xmlPath) -Action
                    $null = New-Item $xmlPath -ItemType Directory
                }

                # Build the file name and write it out
                if ($UserOutput) {
                    $filename = Join-Path $xmlpath ($FilePrefix + "_" + $ShortUser + ".xml")
                }
                else {
                    $filename = Join-Path $xmlPath ($FilePrefix + ".xml")
                }
                Out-LogFile ("Writing Data to " + $filename) -Action

                # Output our objects to clixml
                $AllObject | Export-Clixml $filename

                # If notice is set we need to write the file name to _Investigate.txt
                if ($Notice) { Out-LogFile -string ($filename) -silentnotice }
            }

            # Output CSV file
            if ($csv -eq $true) {
                # Build the file name
                if ($UserOutput) {
                    $filename = Join-Path $Path ($FilePrefix + "_" + $ShortUser + ".csv")
                }
                else {
                    $filename = Join-Path $Path ($FilePrefix + ".csv")
                }

                # If we have -append then append the data
                if ($append) {

                    Out-LogFile ("Appending Data to " + $filename) -NoDisplay

                    # Write it out to csv making sture to append
                    $AllObject | Export-Csv $filename -NoTypeInformation -Append -Encoding UTF8
                }

                # Otherwise overwrite
                else {
                    Out-LogFile ("Writing Data to " + $filename) -Action
                    $AllObject | Export-Csv $filename -NoTypeInformation -Encoding UTF8
                }

                # If notice is set we need to write the file name to _Investigate.txt
                if ($Notice) { Out-LogFile -string ($filename) -silentnotice }
            }

            # Output Text files
            if ($txt -eq $true) {
                # Build the file name
                if ($UserOutput) {
                    $filename = Join-Path $Path ($FilePrefix + "_" + $ShortUser + ".txt")
                }
                else {
                    $filename = Join-Path $Path ($FilePrefix + ".txt")
                }

                # If we have -append then append the data
                if ($Append) {
                    Out-LogFile ("Appending Data to " + $filename) -NoDisplay
                    $AllObject | Format-List * | Out-File $filename -Append
                }

                # Otherwise overwrite
                else {
                    Out-LogFile ("Writing Data to " + $filename) -Action
                    $AllObject | Format-List * | Out-File $filename
                }

                # If notice is set we need to write the file name to _Investigate.txt
                if ($Notice) { Out-LogFile -string ($filename) -silentnotice }
            }

            # Output JSON file
            if ($json -eq $true) {
                # Build the file name
                if ($UserOutput) {
                    $filename = Join-Path $Path ($FilePrefix + "_" + $ShortUser + ".json")
                }
                else {
                    $filename = Join-Path $Path ($FilePrefix + ".json")
                }

                # If we have -append then append the data
                if ($append) {

                    Out-LogFile ("Appending Data to " + $filename) -NoDisplay

                    # Write it out to json making sture to append
                    $AllObject | ConvertTo-Json -Depth 100 | Out-File -FilePath $filename -Append
                }

                # Otherwise overwrite
                else {
                    Out-LogFile ("Writing Data to " + $filename) -Action
                    $AllObject | ConvertTo-Json -Depth 100 | Out-File -FilePath $filename
                }

                # If notice is set we need to write the file name to _Investigate.txt
                if ($Notice) { Out-LogFile -string ($filename) -silentnotice }
            }
        }
    }

}

Function Out-Report {
    <#
    .SYNOPSIS
        Adds the data to an XML report
    .DESCRIPTION
        Adds the data to an XML report
    .PARAMETER Identity
        User Identity for the report
    .PARAMETER Property
        xml property
    .PARAMETER Value
        Value of xml element
    .PARAMETER Description
        Description of element
    .PARAMETER State
        Color mapping
    .PARAMETER Link
        Element link
    .EXAMPLE
        Out-Report
        Add the data to an XML report
    .NOTES
        General notes
    #>

        Param
        (
            [Parameter(Mandatory = $true)]
            [string]$Identity,
            [Parameter(Mandatory = $true)]
            [string]$Property,
            [Parameter(Mandatory = $true)]
            [string]$Value,
            [string]$Description,
            [string]$State,
            [string]$Link
        )
    
        # Force the case on all our critical values
        #$Property = $Property.tolower()
        #$Identity = $Identity.tolower()
    
        # Set our output path
        # Single report file for all outputs user/tenant/etc.
        # This might change in the future???
        $reportpath = Join-path $hawk.filepath report.xml
    
        # Switch statement to handle the state to color mapping
        switch ($State) {
            Warning { $highlighcolor = "#FF8000" }
            Success { $highlighcolor = "Green" }
            Error { $highlighcolor = "#8A0808" }
            default { $highlighcolor = "Light Grey" }
        }
    
        # Check if we have our XSL file in the output directory
        $xslpath = Join-path $hawk.filepath Report.xsl
    
        if (Test-Path $xslpath ) { }
        else {
            # Copy the XSL file into the current output path
            $sourcepath = join-path (split-path (Get-Module Hawk).path) report.xsl
            if (test-path $sourcepath) {
                Copy-Item -Path $sourcepath -Destination $hawk.filepath
            }
            # If we couldn't find it throw and error and stop
            else {
                Write-Error ("Unable to find transform file " + $sourcepath) -ErrorAction Stop
            }
        }
    
        # See if we have already created a report file
        # If so we need to import it
        if (Test-path $reportpath) {
            $reportxml = $null
            [xml]$reportxml = get-content $reportpath
        }
        # Since we have NOTHING we will create a new XML and just add / save / and exit
        else {
            Out-LogFile ("Creating new Report file" + $reportpath)
            # Create the report xml object
            $reportxml = New-Object xml
    
            # Create the xml declaraiton and stylesheet
            $reportxml.AppendChild($reportxml.CreateXmlDeclaration("1.0", $null, $null)) | Out-Null
            # $xmlstyle = "type=`"text/xsl`" href=`"https://csshawk.azurewebsites.net/report.xsl`""
            # $reportxml.AppendChild($reportxml.CreateProcessingInstruction("xml-stylesheet",$xmlstyle)) | Out-Null
    
            # Create all of the needed elements
            $newreport = $reportxml.CreateElement("report")
            $newentity = $reportxml.CreateElement("entity")
            $newentityidentity = $reportxml.CreateElement("identity")
            $newentityproperty = $reportxml.CreateElement("property")
            $newentitypropertyname = $reportxml.CreateElement("name")
            $newentitypropertyvalue = $reportxml.CreateElement("value")
            $newentitypropertycolor = $reportxml.CreateElement("color")
            $newentitypropertydescription = $reportxml.CreateElement("description")
            $newentitypropertylink = $reportxml.CreateElement("link")
    
            ### Build the XML from the bottom up ###
            # Add the property values to the entity object
            $newentityproperty.AppendChild($newentitypropertyname) | Out-Null
            $newentityproperty.AppendChild($newentitypropertyvalue) | Out-Null
            $newentityproperty.AppendChild($newentitypropertycolor) | Out-Null
            $newentityproperty.AppendChild($newentitypropertydescription) | Out-Null
            $newentityproperty.AppendChild($newentitypropertylink) | Out-Null
    
            # Set the values for the leaf nodes we just added
            $newentityproperty.name = $Property
            $newentityproperty.value = $Value
            $newentityproperty.color = $highlighcolor
            $newentityproperty.description = $Description
            $newentityproperty.link = $Link
    
            # Add the identity element to the entity and set its value
            $newentity.AppendChild($newentityidentity) | Out-Null
            $newentity.identity = $Identity
    
            # Add the property to the entity
            $newentity.AppendChild($newentityproperty) | Out-Null
    
            # Add the entity to the report
            $newreport.AppendChild($newentity) | Out-Null
    
            # Add the whole thing to the xml root
            $reportxml.AppendChild($newreport) | Out-Null
    
            # save the xml
            $reportxml.save($reportpath)
        }
    
        # We need to check if an entity with the ID $identity already exists
        if ($reportxml.report.entity.identity.contains($Identity)) { }
        # Didn't find and entity so we are going to create the whole thing and once
        else {
            # Create all of the needed elements
            $newentity = $reportxml.CreateElement("entity")
            $newentityidentity = $reportxml.CreateElement("identity")
            $newentityproperty = $reportxml.CreateElement("property")
            $newentitypropertyname = $reportxml.CreateElement("name")
            $newentitypropertyvalue = $reportxml.CreateElement("value")
            $newentitypropertycolor = $reportxml.CreateElement("color")
            $newentitypropertydescription = $reportxml.CreateElement("description")
            $newentitypropertylink = $reportxml.CreateElement("link")
    
            ### Build the XML from the bottom up ###
            # Add the property values to the entity object
            $newentityproperty.AppendChild($newentitypropertyname) | Out-Null
            $newentityproperty.AppendChild($newentitypropertyvalue) | Out-Null
            $newentityproperty.AppendChild($newentitypropertycolor) | Out-Null
            $newentityproperty.AppendChild($newentitypropertydescription) | Out-Null
            $newentityproperty.AppendChild($newentitypropertylink) | Out-Null
    
            # Set the values for the leaf nodes we just added
            $newentityproperty.name = $Property
            $newentityproperty.value = $Value
            $newentityproperty.color = $highlighcolor
            $newentityproperty.description = $Description
            $newentityproperty.link = $Link
    
            # Add them together and set values
            $newentity.AppendChild($newentityidentity) | Out-Null
            $newentity.identity = $Identity
            $newentity.AppendChild($newentityproperty) | Out-Null
    
            # Add the new entity stub back to the XML
            $reportxml.report.AppendChild($newentity) | Out-Null
        }
    
        # Now we need to check for the property we are looking to add
        # The property exists so we need to update it
        if (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property.name.contains($Property)) {
            ### Update existing property ###
            (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).value = $Value
            (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).color = $highlighcolor
            (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).description = $Description
            (($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).property | Where-Object { $_.name -eq $Property }).link = $Link
        }
        # We need to add the property to the entity
        else {
            ### Add new property to existing Entity ###
            # Create the elements that we are going to need
            $newproperty = $reportxml.CreateElement("property")
            $newname = $reportxml.CreateElement("name")
            $newvalue = $reportxml.CreateElement("value")
            $newcolor = $reportxml.CreateElement("color")
            $newdescription = $reportxml.CreateElement("description")
            $newlink = $reportxml.CreateElement("link")
    
            # Add on all of the elements
            $newproperty.AppendChild($newname) | Out-Null
            $newproperty.AppendChild($newvalue) | Out-Null
            $newproperty.AppendChild($newcolor) | Out-Null
            $newproperty.AppendChild($newdescription) | Out-Null
            $newproperty.AppendChild($newlink) | Out-Null
    
            # Set the values
            $newproperty.name = $Property
            $newproperty.value = $Value
            $newproperty.color = $highlighcolor
            $newproperty.description = $Description
            $newproperty.link = $Link
    
            # Add the newly created property to the entity
            ($reportxml.report.entity | Where-Object { $_.identity -eq $Identity }).AppendChild($newproperty) | Out-Null
        }
    
        # Make sure we save our changes
        $reportxml.Save($reportpath)
    
        # Convert it to HTML and Save
        Convert-ReportToHTML -Xml $reportpath -Xsl $xslpath
    }

<#
.SYNOPSIS
    Read in hawk app data if it is there
.DESCRIPTION
    Read in hawk app data if it is there
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Read-HawkAppData {
    $HawkAppdataPath = join-path $env:LOCALAPPDATA "Hawk\Hawk.json"

    # check to see if our xml file is there
    if (test-path $HawkAppdataPath) {
        Out-LogFile ("Reading file " + $HawkAppdataPath) -Action
        $global:HawkAppData = ConvertFrom-Json -InputObject ([string](Get-Content $HawkAppdataPath))
    }
    # if we don't have an xml file then do nothing
    else {
        Out-LogFile ("No HawkAppData File found " + $HawkAppdataPath) -Information
    }
}

<#
.SYNOPSIS
    Returns a collection of unique objects filtered by a single property
.DESCRIPTION
    Returns a collection of unique objects filtered by a single property
.PARAMETER ObjectArray
    Array of objects
.PARAMETER Property
    Property of the collection of unique objects
.EXAMPLE
    Select-UniqueObject
    Selects unique objects for investigation
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Select-UniqueObject {
    param
    (
        [Parameter(Mandatory = $true)]
        [array]$ObjectArray,
        [Parameter(Mandatory = $true)]
        [string]$Property
    )

    # Null out our output array
    [array]$Output = $null

    # Get the ID of the unique objects based on the sort property
    [array]$UniqueObjectID = $ObjectArray | Select-Object -Unique -ExpandProperty $Property

    # Select the whole object based on the unique names found
    foreach ($Name in $UniqueObjectID) {
        [array]$Output = $Output + ($ObjectArray | Where-Object { $_.($Property) -eq $Name } | Select-Object -First 1)
    }

    return $Output

}

<#
.SYNOPSIS
    Sleeps X seconds and displays a progress bar
.DESCRIPTION
    Sleeps X seconds and displays a progress bar
.PARAMETER sleeptime
    Lengthe of sleep time
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Start-SleepWithProgress {
    Param([int]$sleeptime)

    # Loop Number of seconds you want to sleep
    For ($i = 0; $i -le $sleeptime; $i++) {
        $timeleft = ($sleeptime - $i);

        # Progress bar showing progress of the sleep
        Write-Progress -Activity "Sleeping" -CurrentOperation "$Timeleft More Seconds" -PercentComplete (($i / $sleeptime) * 100);

        # Sleep 1 second
        start-sleep 1
    }

    Write-Progress -Completed -Activity "Sleeping"
}

<#
.SYNOPSIS
    Test if we are connected to the compliance center online and connect if not
.DESCRIPTION
    Test if we are connected to the compliance center online and connect if not
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Test-CCOConnection {
    Write-Output "Not yet implemented"
}

<#
.SYNOPSIS
    Test if we are connected to Exchange Online and connect if not
.DESCRIPTION
    Test if we are connected to Exchange Online and connect if not
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Test-EXOConnection {
    # In all cases make sure we are "connected" to EXO
    try {
        $null = Get-OrganizationConfig -erroraction stop
    }
    catch [System.Management.Automation.CommandNotFoundException] {
        # Connect to EXO if we couldn't find the command
        Out-LogFile "Not Connected to Exchange Online" -Information
        Out-LogFile "Connecting to EXO using Exchange Online Module" -Action
        Connect-ExchangeOnline
    }
}

<#
.SYNOPSIS
    Test if we are connected to Graph and connect if not
.DESCRIPTION
    Test if we are connected to Graph and connect if not
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    https://learn.microsoft.com/en-us/powershell/microsoftgraph/get-started?view=graph-powershell-1.0
 
#>

Function Test-GraphConnection {
    try {
        $null = Get-MgOrganization -ErrorAction Stop
    }
    catch {
        # Fallback if $Hawk is not initialized
        if ($null -eq $Hawk) {
            Write-Output "Connecting to MGGraph using MGGraph Module"
        }
        else {
            # $Hawk exists, so we can safely use Out-LogFile
            Out-LogFile -String "Connecting to MGGraph using MGGraph Module" -Action
        }

        Connect-MGGraph
    }
}


<#
.SYNOPSIS
    Determine if an IP listed in on the O365 XML list
.DESCRIPTION
    Determine if an IP listed in on the O365 XML list
    This function uses the System.Net.IPNetwork.dll to parse the IP Addresses. This is the only use for this DLL
.PARAMETER IPtoTest
    IP that is being tested against the Microsoft IP List
.PARAMETER Type
    Checking for ipv 6 or ipv4
.EXAMPLE
    Test-MicrosoftIP
    Test wether or not the IP retrieved is a Microsoft IP
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Test-MicrosoftIP {
    param
    (
        [Parameter(Mandatory = $true)]
        [string]$IPToTest,
        [Parameter(Mandatory = $true)]
        [string]$Type
    )

    # Check if we have imported all of our IP Addresses
    if ($null -eq $MSFTIPList) {
        Out-Logfile "Building MSFTIPList" -Action

        # Load our networking dll pulled from https://github.com/lduchosal/ipnetwork
        [string]$dll = join-path (Split-path (((get-module Hawk)[0]).path) -Parent) "\bin\System.Net.IPNetwork.dll"

        $Error.Clear()
        Out-LogFile ("Loading Networking functions from " + $dll) -Action
        [Reflection.Assembly]::LoadFile($dll)

        if ($Error.Count -gt 0) {
            Out-Logfile "DLL Failed to load can't process IPs" -isError
            Return "Unknown"
        }

        $Error.clear()

        $MSFTJSON = (Invoke-WebRequest -uri ("https://endpoints.office.com/endpoints/Worldwide?ClientRequestId=" + (new-guid).ToString())).content | ConvertFrom-Json

        if ($Error.Count -gt 0) {
            Out-Logfile "Unable to retrieve JSON file" -isError
            Return "Unknown"
        }

        # Make sure our arrays are null
        [array]$ipv6 = $Null
        [array]$ipv4 = $Null

        # Put all of the IP addresses from the JSON into a simple array
        Foreach ($Entry in $MSFTJSON) {
            $IPList += $Entry.IPs
        }

        # Throw out duplicates
        $IPList = $IPList | Select-Object -Unique

        # Add the IP Addresses into either the v4 or v6 arrays
        Foreach ($ip in $IPList) {
            if ($ip -like "*.*") {
                $ipv4 += $ip
            }
            else {
                $ipv6 += $ip
            }
        }

        Out-LogFile ("Found " + $ipv6.Count + " unique MSFT IPv6 address ranges") -Information
        Out-LogFile ("Found " + $ipv4.count + " unique MSFT IPv4 address ranges") -Information

        # New up using our networking dll we need to pull these all in as network objects
        foreach ($ip in $ipv6) {
            [array]$ipv6objects += [System.Net.IPNetwork]::Parse($ip)
        }
        foreach ($ip in $ipv4) {
            [array]$ipv4objects += [System.Net.IPNetwork]::Parse($ip)
        }

        # Now create our output object
        $output = $Null
        $output = New-Object -TypeName PSObject
        $output | Add-Member -MemberType NoteProperty -Value $ipv6objects -Name IPv6Objects
        $output | Add-Member -MemberType NoteProperty -Value $ipv4objects -Name IPv4Objects

        # Create a global variable to hold our IP list so we can keep using it
        Out-LogFile "Creating global variable `$MSFTIPList" -Action
        New-Variable -Name MSFTIPList -Value $output -Scope global
    }

    # Determine if we have an ipv6 or ipv4 address
    if ($Type -like "ipv6") {

        # Compare to the IPv6 list
        [int]$i = 0
        [int]$count = $MSFTIPList.ipv6objects.count - 1
        # Compare each IP to the ip networks to see if it is in that network
        # If we get back a True or we are beyond the end of the list then stop
        do {
            # Test the IP
            $parsedip = [System.Net.IPAddress]::Parse($IPToTest)
            $test = [System.Net.IPNetwork]::Contains($MSFTIPList.ipv6objects[$i], $parsedip)
            $i++
        }
        until(($test -eq $true) -or ($i -gt $count))

        # Return the value of test true = in MSFT network
        Return $test
    }
    else {
        # Compare to the IPv4 list
        [int]$i = 0
        [int]$count = $MSFTIPList.ipv4objects.count - 1

        # Compare each IP to the ip networks to see if it is in that network
        # If we get back a True or we are beyond the end of the list then stop
        do {
            # Test the IP
            $parsedip = [System.Net.IPAddress]::Parse($IPToTest)
            $test = [System.Net.IPNetwork]::Contains($MSFTIPList.ipv4objects[$i], $parsedip)
            $i++
        }
        until(($test -eq $true) -or ($i -gt $count))

        # Return the value of test true = in MSFT network
        Return $test
    }
}

Function Test-RecipientAge {
<#
.SYNOPSIS
    Check to see if a recipient object was created since our start date
.DESCRIPTION
    Check to see if a recipient object was created since our start date.
    This will be used to determine if a new user has been created within the time frame specified.
.PARAMETER RecipientID
    Recipient object ID that is being retrieved
.EXAMPLE
    Test-RecipientAge
    Will test to see if the recipient object was created since the start date
.NOTES
    General notes
#>

    Param([string]$RecipientID)

    $recipient = Get-Recipient -Identity $RecipientID -erroraction SilentlyContinue
    # Verify that we got something back
    if ($null -eq $recipient) {
        Return 2
    }
    # If the date created is newer than our StartDate return non zero (1)
    elseif ($recipient.whencreated -gt $Hawk.StartDate) {
        Return 1
    }
    # If it is older than the start date return 0
    else {
        Return 0
    }

}

Function Test-SuspiciousInboxRule {
    <#
    .SYNOPSIS
        Internal helper function to detect suspicious inbox rule patterns.
     
    .DESCRIPTION
        Analyzes inbox rule properties to identify potentially suspicious configurations
        like external forwarding, message deletion, or targeting of security-related content.
        Used by both rule creation and modification audit functions.
     
    .PARAMETER Rule
        The parsed inbox rule object to analyze.
     
    .PARAMETER Reasons
        [ref] array to store the reasons why a rule was flagged as suspicious.
     
    .OUTPUTS
        Boolean indicating if the rule matches suspicious patterns.
        Populates the Reasons array parameter with explanations if suspicious.
     
    .EXAMPLE
        $reasons = @()
        $isSuspicious = Test-SuspiciousInboxRule -Rule $ruleObject -Reasons ([ref]$reasons)
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory = $true)]
        [object]$Rule,

        [Parameter(Mandatory = $true)]
        [ref]$Reasons
    )

    $isSuspicious = $false
    $suspiciousReasons = @()

    # Check forwarding/redirection configurations
    if ($Rule.Param_ForwardTo) { 
        $isSuspicious = $true
        $suspiciousReasons += "forwards to: $($Rule.Param_ForwardTo)" 
    }
    if ($Rule.Param_ForwardAsAttachmentTo) { 
        $isSuspicious = $true
        $suspiciousReasons += "forwards as attachment to: $($Rule.Param_ForwardAsAttachmentTo)" 
    }
    if ($Rule.Param_RedirectTo) { 
        $isSuspicious = $true
        $suspiciousReasons += "redirects to: $($Rule.Param_RedirectTo)" 
    }

    # Check deletion/move to deleted items
    if ($Rule.Param_DeleteMessage) { 
        $isSuspicious = $true
        $suspiciousReasons += "deletes messages" 
    }
    if ($Rule.Param_MoveToFolder -eq 'Deleted Items') { 
        $isSuspicious = $true
        $suspiciousReasons += "moves to Deleted Items" 
    }

    # Check for suspicious keywords in subject filters
    if ($Rule.Param_SubjectContainsWords -match 'password|credentials|login|secure|security') {
        $isSuspicious = $true
        $suspiciousReasons += "suspicious subject filter: $($Rule.Param_SubjectContainsWords)"
    }

    # Check for targeting of security-related senders
    if ($Rule.Param_From -match 'security|admin|support|microsoft|helpdesk') {
        $isSuspicious = $true
        $suspiciousReasons += "targets security sender: $($Rule.Param_From)"
    }

    # Update the reasons array with our findings
    $Reasons.Value = $suspiciousReasons

    return $isSuspicious
}

<#
.SYNOPSIS
    Determine if we have an array with UPNs or just a single UPN / UPN array unlabeled
.DESCRIPTION
    Determine if we have an array with UPNs or just a single UPN / UPN array unlabeled
.PARAMETER ToTest
    Object which is being tested
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Test-UserObject {
    param ([array]$ToTest)

    # So we take three inputs here to -userprincipalname string,array,and array of strings
    # We need to test the input value and make sure that that are in a form that the Function can understand
    # The function needs them as an array of object with a property of .UserPrincipalName

    #Case 1 - String
    #Case 2 - Array of Strings
    #Check to see if the value of the entry is of type string
    if ($ToTest[0] -is [string]) {
        # Very basic check to see if this is a UPN
        if ($ToTest[0] -match '@') {
            [array]$Output = $ToTest | Select-Object -Property @{Name = "UserPrincipalName"; Expression = { $_ } }
            Return $Output
        }
        else {
            Out-LogFile "Unable to determine if input is a UserPrincipalName" -isError
            Out-LogFile "Please provide a UPN or array of objects with propertly UserPrincipalName populated" -Information
            Write-Error "Unable to determine if input is a User Principal Name" -ErrorAction Stop
        }
    }
    # Case 3 - Array of objects
    # Validate that at least one object in the array contains a UserPrincipalName Property
    elseif ([bool](get-member -inputobject $ToTest[0] -name UserPrincipalName -MemberType Properties)) {
        Return $ToTest
    }
    else {
        Out-LogFile "Unable to determine if input is a UserPrincipalName" -isError
        Out-LogFile "Please provide a UPN or array of objects with propertly UserPrincipalName populated" -Information
        Write-Error "Unable to determine if input is a User Principal Name" -ErrorAction Stop
    }
}

Function Write-HawkBanner {
    <#
    .SYNOPSIS
        Displays the Hawk welcome banner.
    .DESCRIPTION
        Displays an ASCII art banner when starting Hawk operations.
        The banner is sized to fit most terminal windows.
    .EXAMPLE
        Write-HawkBanner
        Displays the Hawk welcome banner
    #>

    [CmdletBinding()]
    param()
    
    $banner = @'
========================================
    __ __ __
   / / / /___ __ __/ /__
  / /_/ / __ `/ | /| / / //_/
 / __ / /_/ /| |/ |/ / ,<
/_/ /_/\__,_/ |__/|__/_/|_|
 
========================================
                              
Microsoft Cloud Security Analysis Tool
https://cloudforensicator.com
 
'@


    Write-Output $banner 
}

<#
.SYNOPSIS
    Show Hawk Help and creates the Hawk_Help.txt file
.DESCRIPTION
    Show Hawk Help and creates the Hawk_Help.txt file
.EXAMPLE
    PS C:\> <example usage>
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

Function Show-HawkHelp {

    Out-LogFile "Creating Hawk Help File"

    $help = "BASIC USAGE INFORMATION FOR THE HAWK MODULE
    ===========================================
    Hawk is in constant development. We will be adding addtional data gathering and information analysis.
    DISCLAIMER:
    ===========================================
    THE SAMPLE SCRIPTS ARE NOT SUPPORTED UNDER ANY MICROSOFT STANDARD SUPPORT
    PROGRAM OR SERVICE. THE SAMPLE SCRIPTS ARE PROVIDED AS IS WITHOUT WARRANTY
    OF ANY KIND. MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING, WITHOUT
    LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR A PARTICULAR
    PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLE SCRIPTS
    AND DOCUMENTATION REMAINS WITH YOU. IN NO EVENT SHALL MICROSOFT, ITS AUTHORS, OR
    ANYONE ELSE INVOLVED IN THE CREATION, PRODUCTION, OR DELIVERY OF THE SCRIPTS BE LIABLE
    FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS
    PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS)
    ARISING OUT OF THE USE OF OR INABILITY TO USE THE SAMPLE SCRIPTS OR DOCUMENTATION,
    EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES
    PURPOSE:
    ===========================================
    The Hawk module has been designed to ease the burden on O365 administrators who are performing
    a forensic analysis in their organization.
    It does NOT take the place of a human reviewing the data generated and is simply here to make
    data gathering easier.
    HOW TO USE:
    ===========================================
    Hawk is divided into two primary forms of cmdlets; user based Cmdlets and Tenant based cmdlets.
    User based cmdlets take the form Verb-HawkUser<action>. They all expect a -user switch and
    will retrieve information specific to the user that is specified. Tenant based cmdlets take
    the form Verb-HawkTenant<Action>. They don't need any switches and will return information
    about the whole tenant.
    A good starting place is the Start-HawkTenantInvestigation this will run all the tenant based
    cmdlets and provide a collection of data to start with. Once this data has been reviewed
    if there are specific user(s) that more information should be gathered on
    Start-HawkUserInvestigation will gather all the User specific information for a single user.
    All Hawk cmdlets include help that provides an overview of the data they gather and a listing
    of all possible output files. Run Get-Help <cmdlet> -full to see the full help output for a
    given Hawk cmdlet.
    Some of the Hawk cmdlets will flag results that should be further reviewed. These will appear
    in _Investigate files. These are NOT indicative of unwanted activity but are simply things
    that should reviewed.
    REVIEW HAWK CODE:
    ===========================================
    The Hawk module is written in PowerShell and only uses cmdlets and function that are availble
    to all O365 customers. Since it is written in PowerShell anyone who has downloaded it can
    and is encouraged to review the code so that they have a clear understanding of what it is doing
    and are comfortable with it prior to running it in their environment.
    To view the code in notepad run the following command in powershell:
        notepad (join-path ((get-module hawk -ListAvailable)[0]).modulebase 'Hawk.psm1')
    To get the path for the module for use in other application run:
        ((Get-module Hawk -listavailable)[0]).modulebase"


    $help | Out-MultipleFileType -FilePrefix "Hawk_Help" -txt

    Notepad (Join-Path $hawk.filepath "Tenant\Hawk_Help.txt")

}


Function Update-HawkModule {
<#
.SYNOPSIS
    Hawk upgrade check
.DESCRIPTION
    Hawk upgrade check
.PARAMETER ElevatedUpdate
    Update Module
.EXAMPLE
    Update-HawkModule
    Checks for update to Hawk Module on PowerShell Gallery
.NOTES
    General notes
#>

    param
    (
        [switch]$ElevatedUpdate
    )

    # If ElevatedUpdate is true then we are running from a forced elevation and we just need to run without prompting
    if ($ElevatedUpdate) {
        # Set upgrade to true
        $Upgrade = $true
    }
    else {

        # See if we can do an upgrade check
        if ($null -eq (Get-Command Find-Module)) { }

        # If we can then look for an updated version of the module
        else {
            Write-Output "Checking for latest version online"
            $onlineversion = Find-Module -name Hawk -erroraction silentlycontinue
            $Localversion = (Get-Module Hawk | Sort-Object -Property Version -Descending)[0]
            Write-Output ("Found Version " + $onlineversion.version + " Online")

            if ($null -eq $onlineversion){
                Write-Output "[ERROR] - Unable to check Hawk version in Gallery"
            }
            elseif (([version]$onlineversion.version) -gt ([version]$localversion.version)) {
                Write-Output "New version of Hawk module found online"
                Write-Output ("Local Version: " + $localversion.version + " Online Version: " + $onlineversion.version)

                # Prompt the user to upgrade or not
                $title = "Upgrade version"
                $message = "A Newer version of the Hawk Module has been found Online. `nUpgrade to latest version?"
                $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Stops the function and provides directions for upgrading."
                $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Continues running current function"
                $options = [System.Management.Automation.Host.ChoiceDescription[]]($Yes, $No)
                $result = $host.ui.PromptForChoice($title, $message, $options, 0)

                # Check to see what the user choose
                switch ($result) {
                    0 { $Upgrade = $true; Send-AIEvent -Event Upgrade -Properties @{"Upgrade" = "True" }
                    }
                    1 { $Upgrade = $false; Send-AIEvent -Event Upgrade -Properties @{"Upgrade" = "False" }
                    }
                }
            }
            # If the versions match then we don't need to upgrade
            else {
                Write-Output "Latest Version Installed"
            }
        }
    }

    # If we determined that we want to do an upgrade make the needed checks and do it
    if ($Upgrade) {
        # Determine if we have an elevated powershell prompt
        If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            # Update the module
            Write-Output "Downloading Updated Hawk Module"
            Update-Module Hawk -Force
            Write-Output "Update Finished"
            Start-Sleep 3

            # If Elevated update then this prompt was created by the Update-HawkModule function and we can close it out otherwise leave it up
            if ($ElevatedUpdate) { exit }

            # If we didn't elevate then we are running in the admin prompt and we need to import the new hawk module
            else {
                Write-Output "Starting new PowerShell Window with the updated Hawk Module loaded"

                # We can't load a new copy of the same module from inside the module so we have to start a new window
                Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk -force" -Verb RunAs
                Write-Warning "Updated Hawk Module loaded in New PowerShell Window. `nPlease Close this Window."
                break
            }

        }
        # If we are not running as admin we need to start an admin prompt
        else {
            # Relaunch as an elevated process:
            Write-Output "Starting Elevated Prompt"
            Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk;Update-HawkModule -ElevatedUpdate" -Verb RunAs -Wait

            Write-Output "Starting new PowerShell Window with the updated Hawk Module loaded"

            # We can't load a new copy of the same module from inside the module so we have to start a new window
            Start-Process powershell.exe -ArgumentList "-noexit -Command Import-Module Hawk -force"
            Write-Warning "Updated Hawk Module loaded in New PowerShell Window. `nPlease Close this Window."
            break
        }
    }
    # Since upgrade is false we log and continue
    else {
        Write-Output "Skipping Upgrade"
    }
}

Function Get-HawkMessageHeader {
    <#
    .SYNOPSIS
    Gathers the header from the an msg file prepares a report
    .DESCRIPTION
    Gathers the header from the an msg file prepares a report
 
    For Best Results:
    * Capture a message which was sent from the bad actor to an internal user.
    * Get a copy of the message from the internal user's mailbox.
    * For transfering the file ensure that the source msg is zipped before emailing.
    * On Recieve the admin should extract the MSG and run this cmdlet against it.
    .PARAMETER MSGFile
    Path to an export MSG file.
    .OUTPUTS
    File: Message_Header.csv
    Path: \<message name>
    Description: Message Header in CSV form
 
    File: Message_Header_RAW.txt
    Path: \<message name>
    Description: Raw header sutible for going into other tools
    .EXAMPLE
    Get-HawkMessageHeader -msgfile 'c:\temp\my suspicious message.msg'
 
    Pulls the header and reviews critical information
 
    #>


    param
    (
        [Parameter(Mandatory = $true)]
        [string]$MSGFile
    )

    # Create the outlook com object
    try {
        $ol = New-Object -ComObject Outlook.Application
    }
    catch [System.Runtime.InteropServices.COMException] {
        # If we throw a com expection most likely reason is outlook isn't installed
        Out-LogFile "Unable to create outlook com object." -error
        Out-LogFile "Please make sure outlook is installed." -error
        Out-LogFile $Error[0]

        Write-Error "Unable to create Outlook Com Object, please ensure outlook is installed" -ErrorAction Stop

    }

    # Create the Hawk object if it isn't there already
    Initialize-HawkGlobalObject
    Send-AIEvent -Event "CmdRun"


    # check to see if we have a valid file path
    if (Test-Path $MSGFile) {

        # Convert a possible relative path to a full path
        $MSGFile = (Resolve-Path $MSGFile).Path

        # Store the file name for later use
        $MSGFileName = $MSGFile | Split-Path -Leaf

        Out-LogFile ("Reading message header from file " + $MSGFile) -action
        # Import the message and start processing the header
        try {
            $msg = $ol.CreateItemFromTemplate($MSGFile)
            $header = $msg.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x007D001E")
        }
        catch {
            Out-LogFile ("Unable to load " + $MSGFile)
            Out-LogFile $Error[0]
            break
        }

        $headersWithLines = $header.split("`n")
    }
    else {
        # If we don't have a valid file path log an error and stop
        Out-LogFile ("Failed to find file " + $MSGFile) -error
        Write-Error -Message "Failed to find file " + $MSGFile -ErrorAction Stop
    }

    # Make sure variables are empty
    [string]$CombinedString = $null
    [array]$Output = $null

    # Read thru each line to pull together each entry into a single object
    foreach ($string in $headersWithLines) {
        # If our string is not null and we have a leading whitespace then this needs to be added to the previous string as part of the same object.
        if (!([string]::IsNullOrEmpty($string)) -and ([char]::IsWhiteSpace($string[0]))) {
            # Do some string clean up
            $string = $string.trimstart()
            $string = $string.trimend()
            $string = " " + $string

            # Push the string together
            [string]$CombinedString += $string
        }

        # If we are here we do a null check just in case but we know the first char is not a whitespace
        # So we have a new "object" that we need to process in
        elseif (!([string]::IsNullOrEmpty($string))) {

            # For the inital pass the string will be null or empty so we need to check for that
            if ([string]::IsNullOrEmpty($CombinedString)) {
                # Create our new string and continue processing
                $CombinedString = ($string.trimend())
            }
            else {
                # We should have everything now so create the object
                $Object = $null
                $Object = New-Object -TypeName PSObject

                # Split the string on the divider and add it to the object
                [array]$StringSplit = $CombinedString -split ":", 2
                $Object | Add-Member -MemberType NoteProperty -Name "Header" -Value $StringSplit[0].trim()
                $Object | Add-Member -MemberType NoteProperty -Name "Value" -Value $StringSplit[1].trim()

                # Add to the output array
                [array]$Output += $Object

                # Create our new string and continue processing
                $CombinedString = $string.trimend()
            }
        }
        else { }
    }

    # Now that we have the header objects in an array we can work on them and output a report
    $receivedHeadersString = $null
    $receivedHeadersObject = $null

    # Null out the output
    [array]$Findings = $null

    # Determine the initial submitting client/ip

    [array]$receivedHeadersString = $Output | Where-Object { $_.header -eq "Received" }
    foreach ($stringHeader in $receivedHeadersString.value) {
        [array]$receivedHeadersObject += Convert-ReceiveHeader -Header $stringHeader
    }

    # Sort the receive header so oldest is at the top
    $receivedHeadersObject = $receivedHeadersObject | Sort-Object -Property ReceivedFromTime

    if ($null -eq $receivedHeadersObject) { }
    else {

        # Determine how it was submitted to the service
        if ($receivedHeadersObject[0].ReceivedBy -like "*outlook.com*") {
            $Findings += (Add-Finding -Name "Submitting Host" -Value $receivedHeadersObject[0].ReceivedBy -Conclusion "Submitted from Office 365" -MoreInformation "Warning - This might have originated from one of your clients")
        }
        else {
            $Findings += (Add-Finding -Name "Submitting Host" -Value $receivedHeadersObject[0].ReceivedBy -Conclusion "Submitted from Internet" -MoreInformation "")
        }

        ### Output to the report the client that submitted
        $Findings += (Add-Finding -Name "Submitting Client" -Value $receivedHeadersObject[0].ReceivedWith -Conclusion "None" -MoreInformation "")
    }

    ### Output the AuthAS type
    $AuthAs = $output | Where-Object { $_.header -like 'X-MS-Exchange-Organization-AuthAs' }
    # Make sure we got something back
    if ($null -eq $AuthAs) { }
    else {
        # If auth is anonymous then it came from the internet
        if ($AuthAs.value -eq "Anonymous") {
            $Findings += (Add-Finding -Name "Authentication Method" -Value $AuthAs.value -Conclusion "Method used to authenticate" -MoreInformation "https://docs.microsoft.com/en-us/exchange/header-firewall-exchange-2013-help")
        }
        else {
            $Findings += (Add-Finding -Name "Authentication Method" -Value $AuthAs.value -Conclusion "Method used to authenticate" -MoreInformation "https://docs.microsoft.com/en-us/exchange/header-firewall-exchange-2013-help")
        }
    }

    ### Determine the AuthMechanism
    $AuthMech = $output | Where-Object { $_.header -like 'X-MS-Exchange-Organization-AuthMechanism' }
    # Make sure we got something back
    if ($null -eq $AuthMech) { }
    else {
        # If auth is anonymous then it came from the internet
        if ($AuthMech.value -eq "04" -or $AuthMech.value -eq "06") {
            $Findings += (Add-Finding -Name "Authentication Mechanism" -Value $AuthMech.value -Conclusion "04 = Credentials Used; 06 = SMTP Authentication" -MoreInformation "https://docs.microsoft.com/en-us/exchange/header-firewall-exchange-2013-help")
        }
        else {
            $Findings += (Add-Finding -Name "Authentication Mechanism" -Value $AuthMech.value -Conclusion "Mechanism used to authenticate" -MoreInformation "https://docs.microsoft.com/en-us/exchange/header-firewall-exchange-2013-help")
        }
    }

    ### Do P1 and P2 match
    $From = $output | Where-Object { $_.header -like 'From' }
    $ReturnPath = $output | Where-Object { $_.header -like 'Return-Path' }

    # Pull out the from string since it can be formatted with a name
    $frommatches = $null
    $frommatches = $From.Value | Select-String -Pattern '(?<=<)([\s\S]*?)(?=>)' -AllMatches

    if ($null -ne $frommatches) {
        # Pull the string from the matches
        [string]$fromString = $frommatches.Matches.Groups[1].Value
    }
    else {
        [string]$fromString = $From.value
    }

    # Check to see if they match
    if ($fromString.trim() -eq $ReturnPath.value.trim()) {
        $Findings += (Add-Finding -Name "P1 P2 Match" -Value ("From: " + $From.value + "; Return-Path: " + $ReturnPath.value) -Conclusion "P1 and P2 Header match" -MoreInformation "")
    }
    else {
        $Findings += (Add-Finding -Name "P1 P2 Match" -Value ("From: " + $From.value + "; Return-Path: " + $ReturnPath.value) -Conclusion "P1 and P2 Header don't Match" -MoreInformation "WARNING - P1 and P2 Header don't Match")
    }

    # Output the Findings
    $Findings | Out-MultipleFileType -FilePrefix "Message_Header_Findings" -user $MSGFileName -csv

    # Output everything to a file
    $Output | Out-MultipleFileType -FilePrefix "Message_Header" -User $MSGFileName -csv

    # Output the RAW Header to the file for use in other tools
    $header | Out-MultipleFileType -FilePrefix "Message_Header_RAW" -user $MSGFileName -txt
}


# Function to create a finding object for adding to the output array
Function Add-Finding {
    param (
        [string]$Name,
        [string]$Value,
        [string]$Conclusion,
        [string]$MoreInformation
    )

    # Create the object
    $Obj = New-Object PSObject

    # Added the needed properties
    $Obj | Add-Member -MemberType NoteProperty -Name "Rule" -Value $Name
    $Obj | Add-Member -MemberType NoteProperty -Name "Value" -Value $Value
    $Obj | Add-Member -MemberType NoteProperty -Name "Conclusion" -Value $Conclusion
    $Obj | Add-Member -MemberType NoteProperty -Name "More Information" -Value $MoreInformation

    # Return the object
    Return $Obj

}

# Processing a received header and returns it as a object
Function Convert-ReceiveHeader {
    #Core code from https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/18/use-powershell-to-parse-email-message-headerspart-1/
    Param
    (
        [Parameter(Mandatory = $true)]
        [String]$Header
    )

    # Remove any leading spaces from the input text
    $Header = $Header.TrimStart()
    $Header = $Header + " "

    # Create our regular expression for pulling out the sections of the header
    $HeaderRegex = 'from([\s\S]*?)by([\s\S]*?)with([\s\S]*?);([(\s\S)*]{32,36})(?:\s\S*?)'

    # Find out different groups with the regex
    $headerMatches = $Header | Select-String -Pattern $HeaderRegex -AllMatches

    # Check if we got back results
    if ($null -ne $headerMatches) {
        # Formatch our with
        Switch -wildcard ($headerMatches.Matches.groups[3].value.trim()) {
            "SMTP*" { $with = "SMTP" }
            "ESMTP*" { $with = "ESMTP" }
            default { $with = $headerMatches.Matches.groups[3].value.trim() }
        }

        # Create the hash to generate the output object
        $fromhash = @{
            ReceivedFrom = $headerMatches.Matches.groups[1].value.trim()
            ReceivedBy   = $headerMatches.Matches.groups[2].value.trim()
            ReceivedWith = $with
            ReceivedTime = [datetime]($headerMatches.Matches.groups[4].value.trim())
        }

        # Put the data into an object and return it
        $Output = New-Object -TypeName PSObject -Property $fromhash
        return $Output
    }
    # If we failed to match then return null
    else {
        return $null
    }
}

Function Get-HawkTenantAdminEmailForwardingChange {
    <#
    .SYNOPSIS
        Retrieves audit log entries for email forwarding changes made within the tenant.
 
    .DESCRIPTION
        This function queries the Microsoft 365 Unified Audit Log for events related to email
        forwarding configuration changes (Set-Mailbox with forwarding parameters). It focuses on
        tracking when and by whom forwarding rules were added or modified, helping identify potential
        unauthorized data exfiltration attempts.
 
        Key points:
        - Monitors changes to both ForwardingAddress and ForwardingSMTPAddress settings
        - Resolves recipient information for ForwardingAddress values
        - Flags all forwarding changes for review as potential security concerns
        - Provides historical context for forwarding configuration changes
 
    .OUTPUTS
        File: Simple_Forwarding_Changes.csv/.json
        Path: \Tenant
        Description: Simplified view of forwarding configuration changes.
 
        File: Forwarding_Changes.csv/.json
        Path: \Tenant
        Description: Detailed audit log data for forwarding changes.
 
        File: Forwarding_Recipients.csv/.json
        Path: \Tenant
        Description: List of unique forwarding destinations configured.
 
    .EXAMPLE
        Get-HawkTenantAdminEmailForwardingChange
 
        Retrieves all email forwarding configuration changes from the audit logs within the specified
        search window.
    #>

    [CmdletBinding()]
    param()

    # Test the Exchange Online connection to ensure the environment is ready for operations.
    Test-EXOConnection
    # Log the execution of the function for audit and telemetry purposes.
    Send-AIEvent -Event "CmdRun"

    # Initialize timing variables for status updates
    $startTime = Get-Date
    $lastUpdate = $startTime

    # Log the start of the analysis process for email forwarding configuration changes.
    Out-LogFile "Analyzing email forwarding configuration changes from audit logs" -Action

    # Ensure the tenant-specific folder exists to store output files. If not, create it.
    $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant"
    if (-not (Test-Path -Path $TenantPath)) {
        New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null
    }

    try {
        # Define both operations and broader search terms to cast a wider net.
        $searchCommand = @"
Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations @(
    'Set-Mailbox',
    'Set-MailUser',
    'Set-RemoteMailbox',
    'Enable-RemoteMailbox'
)
"@


        # Fetch all specified operations from the audit log
        [array]$AllMailboxChanges = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

        # Log search completion time
        Out-LogFile "Unified Audit Log search completed" -Information

        Out-LogFile "Filtering results for forwarding changes..." -Action

        # Enhanced filtering to catch more types of forwarding changes
        [array]$ForwardingChanges = $AllMailboxChanges | Where-Object {
            $auditData = $_.AuditData | ConvertFrom-Json
            $parameters = $auditData.Parameters
            ($parameters | Where-Object {
                $_.Name -in @(
                    'ForwardingAddress',
                    'ForwardingSMTPAddress',
                    'ExternalEmailAddress',
                    'PrimarySmtpAddress',
                    'RedirectTo',             # Added from other LLM suggestion
                    'DeliverToMailboxAndForward',  # Corrected parameter name
                    'DeliverToAndForward'     # Alternative parameter name
                ) -or
                # Check for parameter changes enabling forwarding
                ($_.Name -eq 'DeliverToMailboxAndForward' -and $_.Value -eq 'True') -or
                ($_.Name -eq 'DeliverToAndForward' -and $_.Value -eq 'True')
            })
        }

        Out-LogFile "Completed filtering for forwarding changes" -Information

        if ($ForwardingChanges.Count -gt 0) {
            # Log the number of forwarding configuration changes found.
            Out-LogFile ("Found " + $ForwardingChanges.Count + " change(s) to user email forwarding") -Information

            # Parse the audit data into a simpler format for further processing and output.
            $ParsedChanges = $ForwardingChanges | Get-SimpleUnifiedAuditLog
            if ($ParsedChanges) {
                # Write the simplified data for quick analysis and review.
                $ParsedChanges | Out-MultipleFileType -FilePrefix "Simple_Forwarding_Changes" -csv -json -Notice

                # Write the full audit log data for comprehensive records.
                $ForwardingChanges | Out-MultipleFileType -FilePrefix "Forwarding_Changes" -csv -json -Notice

                # Initialize an array to store processed forwarding destination data.
                $ForwardingDestinations = @()

                Out-LogFile "Beginning detailed analysis of forwarding changes..." -Action
                foreach ($change in $ParsedChanges) {
                    # Add a status update every 30 seconds
                    $currentTime = Get-Date
                    if (($currentTime - $lastUpdate).TotalSeconds -ge 30) {
                        Out-LogFile "Processing forwarding changes... ($($ForwardingDestinations.Count) destinations found so far)" -Action
                        $lastUpdate = $currentTime
                    }

                    $targetUser = $change.ObjectId

                    # Process ForwardingSMTPAddress changes if detected in the audit log.
                    if ($change.Parameters -match "ForwardingSMTPAddress") {
                        $smtpAddress = ($change.Parameters | Select-String -Pattern "ForwardingSMTPAddress:\s*([^,]+)").Matches.Groups[1].Value
                        if ($smtpAddress) {
                            # Add the SMTP forwarding configuration to the destinations array.
                            $ForwardingDestinations += [PSCustomObject]@{
                                UserModified = $targetUser
                                TargetSMTPAddress = $smtpAddress.Split(":")[-1].Trim() # Remove "SMTP:" prefix if present.
                                ChangeType = "SMTP Forwarding"
                                ModifiedBy = $change.UserId
                                ModifiedTime = $change.CreationTime
                            }
                        }
                    }

                    # Process ForwardingAddress changes if detected in the audit log.
                    if ($change.Parameters -match "ForwardingAddress") {
                        $forwardingAddress = ($change.Parameters | Select-String -Pattern "ForwardingAddress:\s*([^,]+)").Matches.Groups[1].Value
                        if ($forwardingAddress) {
                            try {
                                # Attempt to resolve the recipient details from Exchange Online.
                                $recipient = Get-EXORecipient $forwardingAddress -ErrorAction Stop

                                # Determine the recipient's type and extract the appropriate address.
                                $targetAddress = switch ($recipient.RecipientType) {
                                    "MailContact" { $recipient.ExternalEmailAddress.Split(":")[-1] }
                                    default { $recipient.PrimarySmtpAddress }
                                }

                                # Add the recipient forwarding configuration to the destinations array.
                                $ForwardingDestinations += [PSCustomObject]@{
                                    UserModified = $targetUser
                                    TargetSMTPAddress = $targetAddress
                                    ChangeType = "Recipient Forwarding"
                                    ModifiedBy = $change.UserId
                                    ModifiedTime = $change.CreationTime
                                }
                            }
                            catch {
                                # Log a warning if the recipient cannot be resolved.
                                Out-LogFile "Unable to resolve forwarding recipient: $forwardingAddress" -isError
                                # Add an unresolved entry for transparency in the output.
                                $ForwardingDestinations += [PSCustomObject]@{
                                    UserModified = $targetUser
                                    TargetSMTPAddress = "UNRESOLVED:$forwardingAddress"
                                    ChangeType = "Recipient Forwarding (Unresolved)"
                                    ModifiedBy = $change.UserId
                                    ModifiedTime = $change.CreationTime
                                }
                            }
                        }
                    }
                }


                Out-LogFile "Completed processing forwarding changes" -Information

                if ($ForwardingDestinations.Count -gt 0) {
                    # Log the total number of forwarding destinations detected.
                    Out-LogFile ("Found " + $ForwardingDestinations.Count + " forwarding destinations configured") -Information
                    # Write the forwarding destinations data to files for review.
                    $ForwardingDestinations | Out-MultipleFileType -FilePrefix "Forwarding_Recipients" -csv -json -Notice

                    # Log details about each forwarding destination for detailed auditing.
                    foreach ($dest in $ForwardingDestinations) {
                        Out-LogFile "Forwarding configured: $($dest.UserModified) -> $($dest.TargetSMTPAddress) ($($dest.ChangeType)) by $($dest.ModifiedBy) at $($dest.ModifiedTime)" -Notice
                    }
                }
            }
            else {
                # Log a warning if the parsing of audit data fails.
                Out-LogFile "Error: Failed to parse forwarding change audit data" -isError
            }
        }
        else {
            # Log a message if no forwarding changes are found in the logs.
            Out-LogFile "No forwarding changes found in filtered results" -Information
            Out-LogFile "Retrieved $($AllMailboxChanges.Count) total operations, but none involved forwarding changes" -Information
        }
    }
    catch {
        # Log an error if the analysis encounters an exception.
        Out-LogFile "Error analyzing email forwarding changes: $($_.Exception.Message)" -isError
        Write-Error -ErrorRecord $_ -ErrorAction Continue
    }
}
           


Function Get-HawkTenantAdminInboxRuleCreation {
    <#
    .SYNOPSIS
        Retrieves audit log entries for inbox rules that were historically created within the tenant.
 
    .DESCRIPTION
        This function queries the Microsoft 365 Unified Audit Log for events classified as inbox
        rule creation (New-InboxRule). It focuses on historical record-keeping and identifying
        potentially suspicious rules that were created. The logged events do not indicate the
        specific method or interface used to create the rules.
 
        Key points:
        - Displays creation events for inbox rules, including who created them and when.
        - Flags created rules that appear suspicious (e.g., rules that forward externally, delete
          messages, or filter based on suspicious keywords).
        - Does not confirm whether the rules are currently active or still exist.
 
        For current, active rules, use Get-HawkTenantInboxRules.
 
    .OUTPUTS
        File: Simple_Admin_Inbox_Rules_Creation.csv/.json
        Path: \Tenant
        Description: Simplified view of created inbox rule events.
 
        File: Admin_Inbox_Rules_Creation.csv/.json
        Path: \Tenant
        Description: Detailed audit log data for created inbox rules.
 
        File: _Investigate_Admin_Inbox_Rules_Creation.csv/.json
        Path: \Tenant
        Description: A subset of historically created rules flagged as suspicious.
 
    .EXAMPLE
        Get-HawkTenantAdminInboxRuleCreation
 
        Retrieves events for all admin inbox rules created and available within the audit logs within the configured search window.
         
        Remarks: This basic example pulls all inbox rule creations from the audit log and analyzes them for
        suspicious patterns. Output files will be created in the configured Hawk output directory under
        the Tenant subfolder.
    #>

    [CmdletBinding()]
    param()

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    Out-LogFile "Analyzing admin inbox rule creation from audit logs" -Action

    # Create tenant folder if it doesn't exist
    $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant"
    if (-not (Test-Path -Path $TenantPath)) {
        New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null
    }

    try {
        # Search for new inbox rules
        Out-LogFile "Searching audit logs for inbox rule creation events" -Action
        $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations 'New-InboxRule'"
        [array]$NewInboxRules = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

        if ($NewInboxRules.Count -gt 0) {
            Out-LogFile ("Found " + $NewInboxRules.Count + " admin inbox rule changes in audit logs") -Information

            # Process and output the results
            $ParsedRules = $NewInboxRules | Get-SimpleUnifiedAuditLog
            if ($ParsedRules) {
                Out-LogFile "Writing parsed admin inbox rule creation data" -Action
                $ParsedRules | Out-MultipleFileType -FilePrefix "Simple_Admin_Inbox_Rules_Creation" -csv -json
                $NewInboxRules | Out-MultipleFileType -FilePrefix "Admin_Inbox_Rules_Creation" -csv -json

                # Check for suspicious rules using the helper function
                $SuspiciousRules = $ParsedRules | Where-Object {
                    $reasons = @()
                    Test-SuspiciousInboxRule -Rule $_ -Reasons ([ref]$reasons)
                }

                if ($SuspiciousRules) {
                    Out-LogFile "Found suspicious admin inbox rule creation requiring investigation" -Notice

                    Out-LogFile "Writing suspicious rule creation data" -Action
                    $SuspiciousRules | Out-MultipleFileType -FilePrefix "_Investigate_Admin_Inbox_Rules_Creation" -csv -json -Notice

                    # Log details about why each rule was flagged
                    foreach ($rule in $SuspiciousRules) {
                        $reasons = @()
                        if (Test-SuspiciousInboxRule -Rule $rule -Reasons ([ref]$reasons)) {
                            Out-LogFile "Found suspicious rule creation: '$($rule.Param_Name)' created by $($rule.UserId) at $($rule.CreationTime)" -Notice
                            Out-LogFile "Reasons for investigation: $($reasons -join '; ')" -Notice
                        }
                    }
                }
            }
            else {
                Out-LogFile "Error: Failed to parse inbox rule audit data" -isError
            }
        }
        else {
            Out-LogFile "No admin inbox rule creation events found in audit logs" -Information
        }
    }
    catch {
        Out-LogFile "Error analyzing admin inbox rule creation: $($_.Exception.Message)" -isError
        Write-Error -ErrorRecord $_ -ErrorAction Continue
    }
}

Function Get-HawkTenantAdminInboxRuleModification {
    <#
    .SYNOPSIS
        Retrieves audit log entries for inbox rules that were historically modified within the tenant.
 
    .DESCRIPTION
        This function queries the Microsoft 365 Unified Audit Logs for events classified as
        inbox rule modification (Set-InboxRule). It focuses on past changes to existing rules,
        helping identify suspicious modifications (e.g., forwarding to external addresses,
        enabling deletion, or targeting sensitive keywords).
 
        The logged events do not indicate how or where the modification took place, only that
        an inbox rule was changed at a given time by a specific account.
 
        Key points:
        - Shows modification events for inbox rules, including who modified them and when.
        - Flags modifications that may be suspicious based on predefined criteria.
        - Does not indicate whether the rules are currently active or still exist.
 
        For current, active rules, use Get-HawkTenantInboxRules.
 
    .OUTPUTS
        File: Simple_Admin_Inbox_Rules_Modification.csv/.json
        Path: \Tenant
        Description: Simplified view of inbox rule modification events.
 
        File: Admin_Inbox_Rules_Modification.csv/.json
        Path: \Tenant
        Description: Detailed audit log data for modified inbox rules.
 
        File: _Investigate_Admin_Inbox_Rules_Modification.csv/.json
        Path: \Tenant
        Description: A subset of historically modified rules flagged as suspicious.
 
    .EXAMPLE
        Get-HawkTenantAdminInboxRuleModification
 
        Retrieves events for all admin inbox rules modified and available within the audit logs within the configured search window.
         
        Remarks: This basic example pulls all inbox rule modification logs from the audit log and analyzes them for
        suspicious patterns. Output files will be created in the configured Hawk output directory under
        the Tenant subfolder.
    #>

    #>
    [CmdletBinding()]
    param()

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    Out-LogFile "Analyzing admin inbox rule modifications from audit logs" -Action

    # Create tenant folder if it doesn't exist
    $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant"
    if (-not (Test-Path -Path $TenantPath)) {
        New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null
    }

    try {
        # Search for modified inbox rules
        Out-LogFile "Searching audit logs for inbox rule modification events" -Action
        $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations 'Set-InboxRule'"
        [array]$ModifiedInboxRules = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

        if ($ModifiedInboxRules.Count -gt 0) {
            Out-LogFile ("Found " + $ModifiedInboxRules.Count + " admin inbox rule modifications in audit logs") -Information

            # Process and output the results
            $ParsedRules = $ModifiedInboxRules | Get-SimpleUnifiedAuditLog
            if ($ParsedRules) {
                Out-LogFile "Writing parsed admin inbox rule modification data" -Action
                $ParsedRules | Out-MultipleFileType -FilePrefix "Simple_Admin_Inbox_Rules_Modification" -csv -json
                $ModifiedInboxRules | Out-MultipleFileType -FilePrefix "Admin_Inbox_Rules_Modification" -csv -json

                # Check for suspicious modifications using the helper function
                $SuspiciousModifications = $ParsedRules | Where-Object {
                    $reasons = @()
                    Test-SuspiciousInboxRule -Rule $_ -Reasons ([ref]$reasons)
                }

                if ($SuspiciousModifications) {
                    Out-LogFile "Found suspicious rule modifications requiring investigation" -Notice

                    Out-LogFile "Writing suspicious rule modification data" -Action
                    $SuspiciousModifications | Out-MultipleFileType -FilePrefix "_Investigate_Admin_Inbox_Rules_Modification" -csv -json -Notice

                    # Log details about why each modification was flagged
                    foreach ($rule in $SuspiciousModifications) {
                        $reasons = @()
                        if (Test-SuspiciousInboxRule -Rule $rule -Reasons ([ref]$reasons)) {
                            Out-LogFile "Found suspicious rule modification: '$($rule.Param_Name)' modified by $($rule.UserId) at $($rule.CreationTime)" -Notice
                            Out-LogFile "Reasons for investigation: $($reasons -join '; ')" -Notice
                        }
                    }
                }
            }
            else {
                Out-LogFile "Error: Failed to parse inbox rule audit data" -isError
            }
        }
        else {
            Out-LogFile "No inbox rule modifications found in audit logs" -Information
        }
    }
    catch {
        Out-LogFile "Error analyzing admin inbox rule modifications: $($_.Exception.Message)" -isError
        Write-Error -ErrorRecord $_ -ErrorAction Continue
    }
}

Function Get-HawkTenantAdminInboxRuleRemoval {
    <#
    .SYNOPSIS
        Retrieves audit log entries for inbox rules that were removed within the tenant.
 
    .DESCRIPTION
        This function queries the Microsoft 365 Unified Audit Log for events classified as inbox
        rule removal (Remove-InboxRule). It focuses on historical record-keeping and identifying
        when inbox rules were removed and by whom. The logged events do not indicate the
        specific method or interface used to remove the rules.
 
        Key points:
        - Displays removal events for inbox rules, including who removed them and when.
        - Flags removals that might be suspicious (e.g., rules that were forwarding externally).
        - Provides historical context for rule removals during investigations.
 
        For current, active rules, use Get-HawkTenantInboxRules.
 
    .OUTPUTS
        File: Simple_Admin_Inbox_Rules_Removal.csv/.json
        Path: \Tenant
        Description: Simplified view of removed inbox rule events.
 
        File: Admin_Inbox_Rules_Removal.csv/.json
        Path: \Tenant
        Description: Detailed audit log data for removed inbox rules.
 
        File: _Investigate_Admin_Inbox_Rules_Removal.csv/.json
        Path: \Tenant
        Description: A subset of historically removed rules flagged as suspicious.
 
    .EXAMPLE
        Get-HawkTenantAdminInboxRuleRemoval
 
        Retrieves events for all removed inbox rules from the audit logs within the specified
        search window, highlighting any that appear suspicious.
    #>

    [CmdletBinding()]
    param()

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    Out-LogFile "Analyzing admin inbox rule removals from audit logs" -Action

    # Create tenant folder if it doesn't exist
    $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant"
    if (-not (Test-Path -Path $TenantPath)) {
        New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null
    }

    try {
        # Search for removed inbox rules
        Out-LogFile "Searching audit logs for inbox rule removals" -action
        $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations 'Remove-InboxRule'"
        [array]$RemovedInboxRules = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

        if ($RemovedInboxRules.Count -gt 0) {
            Out-LogFile ("Found " + $RemovedInboxRules.Count + " admin inbox rule removals in audit logs") -Information

            # Process and output the results
            $ParsedRules = $RemovedInboxRules | Get-SimpleUnifiedAuditLog
            if ($ParsedRules) {
                # Output simple format for easy analysis
                $ParsedRules | Out-MultipleFileType -FilePrefix "Simple_Admin_Inbox_Rules_Removal" -csv -json

                # Output full audit logs for complete record
                $RemovedInboxRules | Out-MultipleFileType -FilePrefix "Admin_Inbox_Rules_Removal" -csv -json

                # Check for suspicious removals
                $SuspiciousRemovals = $ParsedRules | Where-Object {
                    $reasons = @()
                    Test-SuspiciousInboxRule -Rule $_ -Reasons ([ref]$reasons)
                }

                if ($SuspiciousRemovals) {
                    Out-LogFile "Found suspicious admin inbox rule removals requiring investigation" -Notice

                    # Output files with timestamps
                    $csvPath = Join-Path -Path $TenantPath -ChildPath "_Investigate_Admin_Inbox_Rules_Removal.csv"
                    $jsonPath = Join-Path -Path $TenantPath -ChildPath "_Investigate_Admin_Inbox_Rules_Removal.json"
                    Out-LogFile "Additional Information: $csvPath" -Notice
                    Out-LogFile "Additional Information: $jsonPath" -Notice

                    $SuspiciousRemovals | Out-MultipleFileType -FilePrefix "_Investigate_Admin_Inbox_Rules_Removal" -csv -json -Notice

                    # Log details about why each removal was flagged
                    foreach ($rule in $SuspiciousRemovals) {
                        $reasons = @()
                        if (Test-SuspiciousInboxRule -Rule $rule -Reasons ([ref]$reasons)) {
                            Out-LogFile "Found suspicious rule removal: '$($rule.Param_Name)' removed by $($rule.UserId) at $($rule.CreationTime)" -Notice
                            Out-LogFile "Reasons for investigation: $($reasons -join '; ')" -Notice
                        }
                    }
                }
            }
            else {
                Out-LogFile "Error: Failed to parse inbox rule removal audit data" -isError
            }
        }
        else {
            Out-LogFile "No inbox rule removals found in audit logs" -Information
        }
    }
    catch {
        Out-LogFile "Error analyzing admin inbox rule removals: $($_.Exception.Message)" -isError
        Write-Error -ErrorRecord $_ -ErrorAction Continue
    }
}

Function Get-HawkTenantAdminMailboxPermissionChange {
    <#
    .SYNOPSIS
        Retrieves audit log entries for mailbox permission changes within the tenant.
 
    .DESCRIPTION
        Searches the Unified Audit Log for mailbox permission changes and flags any grants
        of FullAccess, SendAs, or Send on Behalf permissions for investigations.
        Excludes normal system operations on Discovery Search Mailboxes.
 
    .OUTPUTS
        File: Simple_Mailbox_Permission_Change.csv/.json
        Path: \Tenant
        Description: Simplified view of mailbox permission changes.
 
        File: Mailbox_Permission_Change.csv/.json
        Path: \Tenant
        Description: Detailed audit log data for permission changes.
 
        File: _Investigate_Mailbox_Permission_Change.csv/.json
        Path: \Tenant
        Description: Permission changes that granted sensitive rights.
 
    .EXAMPLE
        Get-HawkTenantAdminMailboxPermissionChange
 
        Retrieves mailbox permission change events from the audit logs.
    #>

    [CmdletBinding()]
    param()

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    Out-LogFile "Analyzing mailbox permission changes from audit logs" -Action

    # Create tenant folder if it doesn't exist
    $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant"
    if (-not (Test-Path -Path $TenantPath)) {
        New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null
    }

    try {
        # Search for mailbox permission changes
        Out-LogFile "Searching audit logs for mailbox permission changes" -action
        $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations 'Add-MailboxPermission','Add-RecipientPermission','Add-ADPermission'"
        [array]$PermissionChanges = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

        if ($PermissionChanges.Count -gt 0) {
            Out-LogFile ("Found " + $PermissionChanges.Count + " mailbox permission changes in audit logs") -Information

            # Process and output the results
            $ParsedChanges = $PermissionChanges | Get-SimpleUnifiedAuditLog
            if ($ParsedChanges) {
                # Output simple format for easy analysis
                $ParsedChanges | Out-MultipleFileType -FilePrefix "Simple_Mailbox_Permission_Change" -csv -json

                # Output full audit logs for complete record
                $PermissionChanges | Out-MultipleFileType -FilePrefix "Mailbox_Permission_Change" -csv -json

                # Check for sensitive permissions, excluding Discovery Search Mailbox system operations
                $SensitiveGrants = $ParsedChanges | Where-Object {
                    # First check if this is potentially sensitive permission
                    ($_.Param_AccessRights -match 'FullAccess|SendAs' -or
                     $_.Operation -eq 'Add-ADPermission' -or
                     $_.Operation -match 'Add-RecipientPermission') -and
                    # Then exclude DiscoverySearchMailbox system operations
                    -not (
                        $_.UserId -eq "NT AUTHORITY\SYSTEM (Microsoft.Exchange.ServiceHost)" -and 
                        $_.ObjectId -like "*DiscoverySearchMailbox*" -and
                        $_.Param_User -like "*Discovery Management*"
                    )
                }

                if ($SensitiveGrants) {
                    Out-LogFile "Found sensitive permission grants requiring investigation" -Notice
                    $SensitiveGrants | Out-MultipleFileType -FilePrefix "_Investigate_Mailbox_Permission_Change" -csv -json -Notice

                    # Log details about sensitive permission grants
                    foreach ($change in $SensitiveGrants) {
                        $permType = if ($change.Param_AccessRights -match 'FullAccess') {
                            "FullAccess"
                        } elseif ($change.Param_AccessRights -match 'SendAs' -or 
                                 $change.Operation -eq 'Add-ADPermission' -or
                                 $change.Operation -match 'Add-RecipientPermission') {
                            "SendAs/Send on Behalf"
                        } else {
                            "Other sensitive permission"
                        }
                        
                        Out-LogFile "Permission change by $($change.UserId) at $($change.CreationTime)" -Notice
                        Out-LogFile "Details: Granted $permType to $($change.Param_User) on mailbox $($change.Param_Identity)" -Notice
                    }
                }
            }
            else {
                Out-LogFile "Error: Failed to parse mailbox permission audit data" -isError
            }
        }
        else {
            Out-LogFile "No mailbox permission changes found in audit logs" -Information
        }
    }
    catch {
        Out-LogFile "Error analyzing mailbox permission changes: $($_.Exception.Message)" -isError
        Write-Error -ErrorRecord $_ -ErrorAction Continue
    }
}

Function Get-HawkTenantAppAndSPNCredentialDetail {
    <#
    .SYNOPSIS
        Tenant Azure Active Directory Applications and Service Principal Credential details export using Microsoft Graph.
    .DESCRIPTION
        Tenant Azure Active Directory Applications and Service Principal Credential details export. Credential details can be used to
        review when credentials were created for an Application or Service Principal. If a malicious user created a certificate or password
        used to access corporate data, then knowing the key creation time will be instrumental to determining the time frame of when an attacker
        had access to data.
    .EXAMPLE
        Get-HawkTenantAppAndSPNCredentialDetail
        Gets all Tenant Application and Service Principal Details
    .OUTPUTS
        SPNCertsAndSecrets.csv
        ApplicationCertsAndSecrets
    .LINK
        https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list
        https://learn.microsoft.com/en-us/graph/api/application-list
    .NOTES
        Updated to use Microsoft Graph API instead of AzureAD module
    #>

    [CmdletBinding()]
    param()

    BEGIN {
        if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
            Initialize-HawkGlobalObject
        }

        # Create Tenant folder path if it doesn't exist
        $tenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant"
        if (-not (Test-Path -Path $tenantPath)) {
            New-Item -Path $tenantPath -ItemType Directory -Force | Out-Null
        }

        Test-GraphConnection
        Send-AIEvent -Event "CmdRun"

        # Initialize arrays to collect all results
        $spnResults = @()
        $appResults = @()

        Out-LogFile "Collecting Entra ID Service Principals" -Action
        try {
            $spns = Get-MgServicePrincipal -All | Sort-Object -Property DisplayName
            Out-LogFile "Collecting Entra ID Registered Applications" -Action
            $apps = Get-MgApplication -All | Sort-Object -Property DisplayName
        }
        catch {
            Out-LogFile "Error retrieving Service Principals or Applications: $($_.Exception.Message)" -isError
            Write-Error -ErrorRecord $_ -ErrorAction Continue
        }
    }

    PROCESS {
        try {
            Out-LogFile "Exporting Service Principal Certificate and Password details" -Action
            foreach ($spn in $spns) {
                # Process key credentials
                foreach ($key in $spn.KeyCredentials) {
                    $newapp = [PSCustomObject]@{
                        AppName     = $spn.DisplayName
                        AppObjectID = $spn.Id
                        KeyID       = $key.KeyId
                        StartDate   = $key.StartDateTime
                        EndDate     = $key.EndDateTime
                        KeyType     = $key.Type
                        CredType    = "X509Certificate"
                    }
                    # Add to array for JSON output
                    $spnResults += $newapp
                    # Output individual record to CSV
                    $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -append
                }

                # Process password credentials
                foreach ($pass in $spn.PasswordCredentials) {
                    $newapp = [PSCustomObject]@{
                        AppName     = $spn.DisplayName
                        AppObjectID = $spn.Id
                        KeyID       = $pass.KeyId
                        StartDate   = $pass.StartDateTime
                        EndDate     = $pass.EndDateTime
                        KeyType     = $null
                        CredType    = "PasswordSecret"
                    }
                    # Add to array for JSON output
                    $spnResults += $newapp
                    # Output individual record to CSV
                    $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -append
                }
            }

            # Output complete SPN results array as single JSON
            if ($spnResults.Count -gt 0) {
                $spnResults | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $tenantPath -ChildPath "SPNCertsAndSecrets.json")
            }

            Out-LogFile "Exporting Registered Applications Certificate and Password details" -Action
            foreach ($app in $apps) {
                # Process key credentials
                foreach ($key in $app.KeyCredentials) {
                    $newapp = [PSCustomObject]@{
                        AppName     = $app.DisplayName
                        AppObjectID = $app.Id
                        KeyID       = $key.KeyId
                        StartDate   = $key.StartDateTime
                        EndDate     = $key.EndDateTime
                        KeyType     = $key.Type
                        CredType    = "X509Certificate"
                    }
                    # Add to array for JSON output
                    $appResults += $newapp
                    # Output individual record to CSV
                    $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -append
                }

                # Process password credentials
                foreach ($pass in $app.PasswordCredentials) {
                    $newapp = [PSCustomObject]@{
                        AppName     = $app.DisplayName
                        AppObjectID = $app.Id
                        KeyID       = $pass.KeyId
                        StartDate   = $pass.StartDateTime
                        EndDate     = $pass.EndDateTime
                        KeyType     = $pass.Type
                        CredType    = "PasswordSecret"
                    }
                    # Add to array for JSON output
                    $appResults += $newapp
                    # Output individual record to CSV
                    $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -append
                }
            }

            # Output complete application results array as single JSON
            if ($appResults.Count -gt 0) {
                $appResults | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $tenantPath -ChildPath "ApplicationCertsAndSecrets.json")
            }
        }
        catch {
            Out-LogFile "Error processing credentials: $($_.Exception.Message)" -isError
            Write-Error -ErrorRecord $_ -ErrorAction Continue
        }
    }

    END {
        Out-Logfile "Completed exporting Azure AD Service Principal and App Registration Certificate and Password Details" -Information
    }
}

Function Get-HawkTenantAuditLog{
<#
.SYNOPSIS
Retrieves all Azure AD audit logs for a specified tenant and exports them to a CSV file.
 
.DESCRIPTION
The Get-HawkTenantAuditLogs function retrieves all Azure AD audit logs for a specified tenant using the Microsoft Graph API. The audit logs are then exported to a CSV file using the Out-MultipleFileType function from the Hawk module.
 
.EXAMPLE
PS C:\> Get-HawkTenantAuditLogs
 
This example retrieves all Azure AD audit logs for the "contoso.onmicrosoft.com" tenant and exports them to a CSV file.
 
.NOTES
This function requires the Microsoft Graph PowerShell module and the Hawk module to be installed. You can install these modules using the following commands:
 
Install-Module -Name Microsoft.Graph
Install-Module -Name Hawk
 
.LINK
https://docs.microsoft.com/en-us/graph/api/resources/auditlog?view=graph-rest-1.0
 
#>

BEGIN{
    #Initializing Hawk Object if not present
    if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
        Initialize-HawkGlobalObject
    }
    Out-LogFile "Gathering Azure AD Audit Logs events" -Action
}
PROCESS{
        $auditLogsResponse = Get-MgAuditLogDirectoryAudit -All
        foreach ($auditLog in $auditLogsResponse) {
            $auditLogs += [PSCustomObject]@{
                Id = $auditLog.Id
                Category = $auditLog.Category
                Result = $auditLog.Result
                ResultReason = $auditLog.ResultReason
                ActivityDisplayName = $auditLog.ActivityDisplayName
                ActivityDateTime = $auditLog.ActivityDateTime
                Target = $auditLog.TargetResources[0].DisplayName
                Type = $auditLog.Target.TargetResources[0].Type
                UserPrincipalName = $auditLog.TargetResources[0].UserPrincipalName
                UserType = $auditLog.UserType
            }
        }
    }
    END{
        $auditLogs | Sort-Object -Property ActivityDateTime | Out-MultipleFileType -FilePrefix "AzureADAuditLog" -csv -json
        Out-Logfile "Completed exporting Azure AD audit logs" -Information
    }
}

Function Get-HawkTenantAuthHistory {
<#
.SYNOPSIS
    Gathers 48 hours worth of Unified Audit logs.
    Pulls everyting into a CSV file.
.DESCRIPTION
    Connects to EXO and searches the unified audit log file only a date time filter.
    Searches in 15 minute increments to ensure that we gather all data.
    Should be used once you have used other commands to determine a "window" that needs more review.
.PARAMETER StartDate
    Start date of authentication audit log search
.PARAMETER IntervalMinutes
    Time interval for increments
.OUTPUTS
    File: Audit_Log_Full_<date>.csv
    Path: \Tenant
    Description: Audit data for ALL users over a 48 hour period
.EXAMPLE
    Get-HawkTenantAuthHistory -StartDate "10/25/2018"
 
    Gathers 48 hours worth of audit data starting at midnight on October 25th 2018
#>


    Param (
        [Parameter(Mandatory = $true)]
        [datetime]$StartDate,
        [int]$IntervalMinutes = 15
    )

    # # Try to convert the submitted date into [datetime] format
    # try {
    # [datetime]$DateToStartSearch = Get-Date $StartDate
    # }
    # catch {
    # Out-Logfile "[ERROR] - Unable to convert submitted date"
    # break
    # }

    # Make sure the start date isn't more than 90 days in the past
    if ((Get-Date).adddays(-91) -gt $StartDate) {
        Out-Logfile "Start date is over 90 days in the past" -isError
        break
    }

    Test-EXOConnection

    # Setup inial start and end time for the search
    [datetime]$CurrentStart = $StartDate
    [datetime]$CurrentEnd = $StartDate.AddMinutes($IntervalMinutes)

    # Hard stop for the end time for 48 hours this is to be a good citizen and to ensure that we actually get the data back
    [datetime]$end = $StartDate.AddHours(48)

    # Setup our file prefix so we can run multiple times with out collision
    [string]$prefix = Get-Date ($StartDate) -UFormat %Y_%d_%m

    # Current count so we can setup a file name and other stuff
    [int]$CurrentCount = 0

    # Create while loop so we go thru things in intervals until we hit the end
    while ($currentStart -lt $end) {
        # Pull the unified audit log results
        [array]$output = Get-AllUnifiedAuditLogEntry -UnifiedSearch "Search-UnifiedAuditLog" -StartDate $currentStart -EndDate $currentEnd

        # See if we have results if so push to csv file
        if ($null -eq $output) {
            Out-LogFile ("No results found for time period " + $CurrentStart + " - " + $CurrentEnd) -Information
        }
        else {
            $output | Out-MultipleFileType -FilePrefix "Audit_Log_Full_$prefix" -Append -csv -json
        }

        # Move our start and end times forward
        $currentStart = $currentEnd
        $currentEnd = $currentEnd.AddMinutes($intervalMinutes)

        # Increment our count
        $CurrentCount++
    }
}

Function Get-HawkTenantAzureAppAuditLog{
<#
.SYNOPSIS
    Gathers common data about a tenant.
.DESCRIPTION
    Runs all Hawk Basic tenant related cmdlets and gathers the data.
 
    Cmdlet Information Gathered
    ------------------------- -------------------------
    Get-HawkTenantConfigurationn Basic Tenant information
    Get-HawkTenantEDiscoveryConfiguration Looks for changes to ediscovery configuration
    Search-HawkTenantEXOAuditLog Searches the EXO audit log for activity
    Get-HawkTenantRBACChange Looks for changes to Roles Based Access Control
.OUTPUTS
    See help from individual cmdlets for output list.
    All outputs are placed in the $Hawk.FilePath directory
.EXAMPLE
    Start-HawkTenantInvestigation
 
    Runs all of the tenant investigation cmdlets.
#>

Begin {
    #Initializing Hawk Object if not present
    if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
        Initialize-HawkGlobalObject
    }
    Out-LogFile "Gathering Tenant information" -Action
    Test-EXOConnection
}#End BEGIN

PROCESS{
# Make sure our variables are null
$AzureApplicationActivityEvents = $null

Out-LogFile "Searching Unified Audit Logs Azure Activities" -Action
Out-LogFile "Searching for Application Activities" -Action

# Search the unified audit log for events related to application activity
# https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants
$AzureApplicationActivityEvents = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -RecordType 'AzureActiveDirectory' -Operations 'Add OAuth2PermissionGrant.','Consent to application.' ")

# If null we found no changes to nothing to do here
if ($null -eq $AzureApplicationActivityEvents){
    Out-LogFile "No Application related events found in the search time frame." -Information
}

# If not null then we must have found some events so flag them
else {
    Out-LogFile "Application Rights Activity found." -Notice
    Out-LogFile "Please review these Azure_Application_Audit.csv to ensure any changes are legitimate." -Notice

    # Go thru each even and prepare it to output to CSV
    Foreach ($event in $AzureApplicationActivityEvents){

        $event.auditdata | ConvertFrom-Json | Select-Object -Property Id,
            Operation,
            ResultStatus,
            Workload,
            ClientIP,
            UserID,
            @{Name='ActorUPN';Expression={($_.ExtendedProperties | Where-Object {$_.Name -eq 'actorUPN'}).value}},
            @{Name='targetName';Expression={($_.ExtendedProperties | Where-Object {$_.Name -eq 'targetName'}).value}},
            @{Name='env_time';Expression={($_.ExtendedProperties | Where-Object {$_.Name -eq 'env_time'}).value}},
            @{Name='correlationId';Expression={($_.ExtendedProperties | Where-Object {$_.Name -eq 'correlationId'}).value}}`
            | Out-MultipleFileType -fileprefix "Azure_Application_Audit" -csv -json -append
    }
}
}#End PROCESS
END{
Out-LogFile "Completed gathering Tenant App Audit Logs" -Action
}#End END
}


Function Get-HawkTenantConfiguration {
<#
.SYNOPSIS
    Gather basic tenant configuration and saves the output to a text file
.DESCRIPTION
    Gather basic tenant configuration and saves the output to a text file
    Gathers information about tenant wide settings
    * Admin Audit Log Configuration
    * Organization Configuration
    * Remote domains
    * Transport Rules
    * Transport Configuration
.EXAMPLE
    PS C:\> Get-HawkTenantConfiguration
    Explanation of what the example does
.INPUTS
    Inputs (if any)
.OUTPUTS
    File: AdminAuditLogConfig.txt
    Path: \
    Description: Output of Get-AdminAuditlogConfig
 
    File: AdminAuditLogConfig.xml
    Path: \XML
    Description: Output of Get-AdminAuditlogConfig as CLI XML
 
    File: OrgConfig.txt
    Path: \
    Description: Output of Get-OrganizationConfig
 
    File: OrgConfig.xml
    Path: \XML
    Description: Output of Get-OrganizationConfig as CLI XML
 
    File: RemoteDomain.txt
    Path: \
    Description: Output of Get-RemoteDomain
 
    File: RemoteDomain.xml
    Path: \XML
    Description: Output of Get-RemoteDomain as CLI XML
 
    File: TransportRules.txt
    Path: \
    Description: Output of Get-TransportRule
 
    File: TransportRules.xml
    Path: \XML
    Description: Output of Get-TransportRule as CLI XML
 
    File: TransportConfig.txt
    Path: \
    Description: Output of Get-TransportConfig
 
    File: TransportConfig.xml
    Path: \XML
    Description: Output of Get-TransportConfig as CLI XML
.NOTES
    TODO: Put in some analysis ... flag some key things that we know we should
#>

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    #Check Audit Log Config Setting and make sure it is enabled
    Out-LogFile "Gathering Tenant Configuration Information" -action

    Out-LogFile "Gathering Admin Audit Log" -action
    Get-AdminAuditLogConfig | Out-MultipleFileType -FilePrefix "AdminAuditLogConfig" -txt -xml

    Out-LogFile "Gathering Organization Configuration" -action
    Get-OrganizationConfig| Out-MultipleFileType -FilePrefix "OrgConfig" -xml -txt

    Out-LogFile "Gathering Remote Domains" -action
    Get-RemoteDomain | Out-MultipleFileType -FilePrefix "RemoteDomain" -xml -csv -json

    Out-LogFile "Gathering Transport Rules" -action
    Get-TransportRule | Out-MultipleFileType -FilePrefix "TransportRules" -xml -csv -json

    Out-LogFile "Gathering Transport Configuration" -action
    Get-TransportConfig | Out-MultipleFileType -FilePrefix "TransportConfig" -xml -csv -json
}

Function Get-HawkTenantConsentGrant {
    <#
.SYNOPSIS
    Gathers application grants using Microsoft Graph
 
.DESCRIPTION
    Uses Microsoft Graph to gather information about application and delegate grants.
    Attempts to detect high risk grants for review. This function is used to identify
    potentially risky application permissions and consent grants in your tenant.
 
.EXAMPLE
    Get-HawkTenantConsentGrant
    Gathers and analyzes all OAuth grants in the tenant.
 
.OUTPUTS
    File: Consent_Grants.csv
    Path: \Tenant
    Description: Output of all consent grants with details about permissions and access
 
.NOTES
    This function requires the following Microsoft Graph permissions:
    - Application.Read.All
    - Directory.Read.All
#>

    [CmdletBinding()]
    param()

    Out-LogFile "Gathering OAuth / Application Grants" -Action

    Test-GraphConnection

    # Gather the grants using the internal Graph-based implementation
    [array]$Grants = Get-AzureADPSPermission -ShowProgress
    [bool]$flag = $false

    # Search the Grants for the listed bad grants that we can detect
    if ($Grants.ConsentType -contains 'AllPrincipals') {
        Out-LogFile "Found at least one 'AllPrincipals' Grant" -notice
        $flag = $true
    }
    if ([bool]($Grants.Permission -match 'all')) {
        Out-LogFile "Found at least one 'All' Grant" -notice
        $flag = $true
    }

    if ($flag) {
        Out-LogFile 'Review the information at the following link to understand these results' -Information
        Out-LogFile 'https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants' -Information
    }
    else {
        Out-LogFile "To review this data follow:" -Information
        Out-LogFile "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants" -Information
    }

    $Grants | Out-MultipleFileType -FilePrefix "Consent_Grants" -csv -json
}

# Search for any changes made to RBAC in the search window and report them
Function Get-HawkTenantDomainActivity {
<#
.SYNOPSIS
    Looks for any changes made to M365 Domains. Permissions required to make the changes that thsi function is
    looking for is "Domain Name Administrator" or "Global Administrator
.DESCRIPTION
    Searches the EXO Audit logs for the following commands being run.
    Set-AccpetedDomain
    Add-FederatedDomain
    New-AcceptedDomain
    Update Domain
    Add Verified Domain
    Add Unverified Domain
    .OUTPUTS
 
    File: Domain_Activity_Changes.csv
    Path: \
    Description: All Domain activity actions
 
    File: Domain_Activity_Changes.xml
    Path: \XML
    Description: All Domain configuration actions
.EXAMPLE
    Get-HawkTenantDomainActivity
 
    Searches for all Domain configuration actions
#>

    BEGIN{
        Test-EXOConnection
        Send-AIEvent -Event "CmdRun"
        Out-LogFile "Gathering any changes to Domain configuration settings" -action
    }
    PROCESS{
        # Search UAL audit logs for any Domain configuration changes
        $DomainConfigurationEvents = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -RecordType 'AzureActiveDirectory' -Operations 'Set-AcceptedDomain','Add-FederatedDomain','Update Domain','Add verified domain', 'Add unverified domain', 'remove unverified domain'")
        # If null we found no changes to nothing to do here
            if ($null -eq $DomainConfigurationEvents){
            Out-LogFile "No Domain configuration changes found." -Information
        }
        # If not null then we must have found some events so flag them
        else{
            Out-LogFile "Domain configuration changes found." -Notice
            Out-LogFile "Please review these Domain_Changes_Audit to ensure any changes are legitimate." -Notice

            # Go thru each even and prepare it to output to CSV
            Foreach ($event in $DomainConfigurationEvents){
                $log1 = $event.auditdata | ConvertFrom-Json
                <#
                $domainarray = $log1.ModifiedProperties
                $useragentarray = $log1.ExtendedProperties
                if ($domainarray){
                    $result1 = ($log1.ModifiedProperties.NewValue).Split('"')
                    $Domain = $result1[1]
                }
                else {
                    $Domain = "Domain Not Provided by Audit Log"
                }
                if ($useragentarray){
                    $result2 = ($log1.ExtendedProperties.Value).Split('"')
                    $UserAgentString = $result2[3]
                }
                else {
                    $UserAgentString = "User Agent String Found"
                }
            $newlog = $log1 | Select-Object -Property CreationTime,
                Id,
                Workload,
                Operation,
                ResultStatus,
                UserID,
                @{Name='Domain';Expression={$Domain}},
                @{Name='User Agent String';Expression={$UserAgentString}},
                @{Name='Target';Expression={($_.Target.ID)}}
            #>

            $event | Out-MultipleFileType -fileprefix "Domain_Changes_Audit" -csv -append
            $log1 | Out-MultipleFileType -fileprefix "Domain_Changes_Audit" -json -append
            }
        }
    }
END{
    Out-LogFile "Completed gathering Domain configuration changes" -Information
}
}#End Function Get-HawkTenantDomainActivity


Function Get-HawkTenantEDiscoveryConfiguration {
    <#
    .SYNOPSIS
        Gets complete eDiscovery configuration data across built-in and custom role assignments.
 
    .DESCRIPTION
        Retrieves comprehensive eDiscovery permissions data from two distinct sources in Exchange Online:
 
        1. Built-in Exchange Online Role Groups:
        - Standard eDiscovery roles like "Discovery Management"
        - Pre-configured with specific eDiscovery capabilities
        - Managed through Exchange admin center
        - Typically used for organization-wide eDiscovery access
        - Includes mailbox search and hold capabilities
        - Part of Microsoft's default security model
 
        2. Custom Management Role Entries:
        - User-created roles with eDiscovery permissions
        - Can be tailored for specific business needs
        - May include subset of eDiscovery capabilities
        - Often created for specialized teams or scenarios
        - Requires careful monitoring for security
        - May grant permissions through role assignments
        - Can include cmdlets like:
            * New-MailboxSearch
            * Search-Mailbox
 
        The function captures all properties and relationships to provide a complete
        view of who has eDiscovery access and how those permissions were granted.
        This helps security teams audit and manage eDiscovery permissions effectively.
 
    .OUTPUTS
        File: EDiscoveryRoles.csv/.json
        Path: \Tenant
        Description: Complete data about standard Exchange Online eDiscovery role groups
        Contains: Role names, members, assigned permissions, creation dates, and all
                associated properties for built-in eDiscovery roles
 
        File: CustomEDiscoveryRoles.csv/.json
        Path: \Tenant
        Description: Complete data about custom roles with eDiscovery permissions
        Contains: Custom role definitions, assignments, scope, creation dates, and all
                configurable properties for user-created roles with eDiscovery access
 
    .EXAMPLE
        Get-HawkTenantEDiscoveryConfiguration
 
        Returns complete, unfiltered eDiscovery permission data showing both built-in
        role groups and custom role assignments that grant eDiscovery access.
 
    .NOTES
        Built-in roles provide consistent, pre-configured access while custom roles
        offer flexibility but require more oversight. Regular review of both types
        is recommended for security compliance.
    #>

    [CmdletBinding()]
    param()

    #TO DO: UPDATE THIS FUNCTION TO FIND E-Discovery roles created via the graph API

    BEGIN {
        if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
            Initialize-HawkGlobalObject
        }

        Test-EXOConnection
        Send-AIEvent -Event "CmdRun"

        Out-LogFile "Gathering complete E-Discovery Configuration" -action

        # Create tenant folder if needed
        $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant"
        if (-not (Test-Path -Path $TenantPath)) {
            New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null
        }

        # Null out role arrays
        [array]$Roles = $null
        [array]$RoleAssignements = $null
    }

    PROCESS {
        try {
            #region Exchange Online Role Groups - Full Data
            Out-LogFile "Gathering all Exchange Online role entries with eDiscovery cmdlets" -Action
            
            # Find any roles that have eDiscovery cmdlets
            $EDiscoveryCmdlets = "New-MailboxSearch", "Search-Mailbox"
            
            foreach ($cmdlet in $EDiscoveryCmdlets) {
                [array]$Roles = $Roles + (Get-ManagementRoleEntry ("*\" + $cmdlet))
            }

            # Select just the unique entries based on role name
            if ($Roles) {
                $UniqueRoles = $Roles | Sort-Object -Property Role -Unique

                Out-LogFile ("Found " + $UniqueRoles.Count + " Roles with E-Discovery Rights") -Information
                
                # Save complete role data
                $UniqueRoles | ConvertTo-Json -Depth 100 | 
                    Out-File (Join-Path -Path $TenantPath -ChildPath "EDiscoveryRoles.json")
                $UniqueRoles | Export-Csv -Path (Join-Path -Path $TenantPath -ChildPath "EDiscoveryRoles.csv") -NoTypeInformation

                # Get everyone who is assigned one of these roles
                foreach ($Role in $UniqueRoles) {
                    [array]$RoleAssignements = $RoleAssignements + (Get-ManagementRoleAssignment -Role $Role.Role -Delegating $false)
                }

                if ($RoleAssignements) {
                    Out-LogFile ("Found " + $RoleAssignements.Count + " Role Assignments for these Roles") -Information
                    
                    # Save complete assignment data
                    $RoleAssignements | ConvertTo-Json -Depth 100 | 
                        Out-File (Join-Path -Path $TenantPath -ChildPath "CustomEDiscoveryRoles.json")
                    $RoleAssignements | Export-Csv -Path (Join-Path -Path $TenantPath -ChildPath "CustomEDiscoveryRoles.csv") -NoTypeInformation
                }
                else {
                    Out-LogFile "No role assignments found" -Information
                }
            }
            else {
                Out-LogFile "No roles with eDiscovery cmdlets found" -Information
            }

            #endregion
        }
        catch {
            Out-LogFile "Error gathering eDiscovery configuration: $($_.Exception.Message)" -isError
            Write-Error -ErrorRecord $_ -ErrorAction Continue
        }
    }

    END {
        Out-LogFile "Completed gathering eDiscovery configuration" -Information
    }
}

Function Get-HawkTenantEDiscoveryLog {
    <#
    .SYNOPSIS
        Gets Unified Audit Logs (UAL) data for eDiscovery
    .DESCRIPTION
        Searches the Unified Audit Log (UAL) for eDiscovery events and activities.
        This includes searches, exports, and management activities related to
        eDiscovery cases. The function checks for any eDiscovery activities within
        the timeframe specified in the Hawk global configuration object.
         
        The results can help identify:
        * When eDiscovery searches were performed
        * Who performed eDiscovery activities
        * Which cases were accessed or modified
        * What operations were performed
 
    .EXAMPLE
        Get-HawkTenantEDiscoveryLog
 
        This will search for all eDiscovery-related activities in the Unified Audit Log
        for the configured time period and export the results to CSV format.
 
    .EXAMPLE
        $logs = Get-HawkTenantEDiscoveryLog
        $logs | Where-Object {$_.Operation -eq "SearchCreated"}
 
        This example shows how to retrieve eDiscovery logs and filter for specific
        operations like new search creation.
 
    .OUTPUTS
        File: eDiscoveryLogs.csv
        Path: \Tenant
        Description: Contains all eDiscovery activities found in the UAL with fields for:
        - CreationTime: When the activity occurred
        - Id: Unique identifier for the activity
        - Operation: Type of eDiscovery action performed
        - Workload: The workload where the activity occurred
        - UserID: User who performed the action
        - Case: eDiscovery case name
        - CaseId: Unique identifier for the eDiscovery case
        - Cmdlet: Command that was executed (if applicable)
    #>

    # Search UAL audit logs for any Domain configuration changes
    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    Out-LogFile "Gathering any eDiscovery logs" -action

    # Search UAL audit logs for any Domain configuration changes
    $eDiscoveryLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -RecordType 'Discovery'")
    # If null we found no changes to nothing to do here
    if ($null -eq $eDiscoveryLogs) {
        Out-LogFile "No eDiscovery Logs found" -Information
    }

    # If not null then we must have found some events so flag them
    else {
        Out-LogFile "eDiscovery Log have been found." -Notice
        Out-LogFile "Please review these eDiscoveryLogs.csv to validate the activity is legitimate." -Notice
        # Go thru each even and prepare it to output to CSV
        Foreach ($log in $eDiscoveryLogs) {
            $log1 = $log.auditdata | ConvertFrom-Json
            $report = $log1  | Select-Object -Property CreationTime,
            Id,
            Operation,
            Workload,
            UserID,
            Case,
            @{Name = 'CaseID'; Expression = { ($_.ExtendedProperties | Where-Object { $_.Name -eq 'CaseId' }).value } },
            @{Name = 'Cmdlet'; Expression = { ($_.Parameters | Where-Object { $_.Name -eq 'Cmdlet' }).value } }

            $report | Out-MultipleFileType -fileprefix "eDiscoveryLogs" -csv -append
        }

    }
}


Function Get-HawkTenantEntraIDAdmin {
    <#
    .SYNOPSIS
        Tenant Microsoft Entra ID Administrator export using Microsoft Graph.
    .DESCRIPTION
        Tenant Microsoft Entra ID Administrator export. Reviewing administrator access is key to knowing who can make changes
        to the tenant and conduct other administrative actions to users and applications.
    .EXAMPLE
        Get-HawkTenantEntraIDAdmin
        Gets all Entra ID Admins
    .OUTPUTS
        EntraIDAdministrators.csv
        EntraIDAdministrators.json
    .LINK
        https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.directorymanagement/get-mgdirectoryrole
    .NOTES
        Requires Microsoft.Graph.Identity.DirectoryManagement module
    #>

        [CmdletBinding()]
        param()

        BEGIN {
            # Initializing Hawk Object if not present
            if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
                Initialize-HawkGlobalObject
            }
            Out-LogFile "Gathering Microsoft Entra ID Administrators" -Action

            # Verify Graph API connection
            Test-GraphConnection
            Send-AIEvent -Event "CmdRun"
        }

        PROCESS {
            try {
                # Retrieve all directory roles from Microsoft Graph
                $directoryRoles = Get-MgDirectoryRole -ErrorAction Stop
                Out-LogFile "Retrieved $(($directoryRoles | Measure-Object).Count) directory roles" -Information

                # Process each role and its members
                $roles = foreach ($role in $directoryRoles) {
                    # Get all members assigned to current role
                    $members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -ErrorAction Stop

                    # Handle roles with no members
                    if (-not $members) {
                        [PSCustomObject]@{
                            AdminGroupName = $role.DisplayName
                            Members = "No Members"
                            MemberType = "None"  # Added member type for better analysis
                            ObjectId = $null
                        }
                    }
                    else {
                        # Process each member of the role
                        foreach ($member in $members) {
                            # Check if member is a user
                            if ($member.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.user") {
                                [PSCustomObject]@{
                                    AdminGroupName = $role.DisplayName
                                    Members = $member.AdditionalProperties.userPrincipalName
                                    MemberType = "User"
                                    ObjectId = $member.Id
                                }
                            }
                            else {
                                # Handle groups and service principals
                                [PSCustomObject]@{
                                    AdminGroupName = $role.DisplayName
                                    Members = $member.AdditionalProperties.displayName
                                    MemberType = ($member.AdditionalProperties.'@odata.type' -replace '#microsoft.graph.', '')
                                    ObjectId = $member.Id
                                }
                            }
                        }
                    }
                }

                # Export results if any roles were found
                if ($roles) {
                    $roles | Out-MultipleFileType -FilePrefix "EntraIDAdministrators" -csv -json
                    Out-LogFile "Successfully exported Microsoft Entra ID Administrators data" -Information
                }
                else {
                    Out-LogFile "No administrator roles found or accessible" -Information
                }
            }
            catch {
                # Handle and log any errors during execution
                Out-LogFile "Error retrieving Microsoft Entra ID Administrators: $($_.Exception.Message)" -isError
                Write-Error -ErrorRecord $_ -ErrorAction Continue
            }
        }

        END {
            Out-LogFile "Completed exporting Microsoft Entra ID Admins" -Information
        }
    }

Function Get-HawkTenantEntraIDUser {
    <#
    .SYNOPSIS
        This function will export all the Entra ID users (formerly Azure AD users).
    .DESCRIPTION
        This function exports all the Entra ID users to a .csv file, focusing on properties
        relevant for digital forensics and incident response. Properties include user identity,
        account status, and account dates.
 
        Note: SignInActivity requires additional AuditLog.Read.All permission and is currently commented out.
    .EXAMPLE
        PS C:\>Get-HawkTenantEntraIDUser
        Exports all Entra ID users with DFIR-relevant properties to .csv and .json files.
    .OUTPUTS
        EntraIDUsers.csv, EntraIDUsers.json
    .LINK
        https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=powershell
    .NOTES
        Updated to use Microsoft Graph SDK instead of AzureAD module.
        Properties selected for DFIR relevance.
    #>

    BEGIN {
        # Initialize the Hawk environment if not already done
        if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
            Initialize-HawkGlobalObject
        }
        Out-LogFile "Gathering Entra ID Users" -Action

        # Ensure we have a valid Graph connection
        Test-GraphConnection
    }
    PROCESS {
        # Get all users with specific properties needed for DFIR
        # -Property parameter optimizes API call to only retrieve needed fields
        $users = Get-MgUser -All -Property UserPrincipalName,    # Primary user identifier
            DisplayName,                                         # User's display name
            Id,                                                 # Unique object ID
            AccountEnabled,                                     # Account status (active/disabled)
            CreatedDateTime,                                    # Account creation timestamp
            DeletedDateTime,                                    # Account deletion timestamp (if applicable)
            LastPasswordChangeDateTime,                         # Last password modification
            Mail |                                             # Primary email address
            Select-Object UserPrincipalName,
                DisplayName,
                Id,
                AccountEnabled,
                CreatedDateTime,
                DeletedDateTime,
                LastPasswordChangeDateTime,
                Mail

        # Only process if users were found
        if ($users) {
            # Sort by UPN and export to both CSV and JSON formats
            $users | Sort-Object -Property UserPrincipalName |
                Out-MultipleFileType -FilePrefix "EntraIDUsers" -csv -json
        }
        else {
            Out-LogFile "No users found" -Information
        }
    }
    END {
        Out-Logfile "Completed exporting Entra ID users" -Information
    }
 }

Function Get-HawkTenantEXOAdmin{
<#
.SYNOPSIS
    Exchange Online Administrator export. Must be connected to Exchange Online using the Connect-EXO cmdlet
.DESCRIPTION
    After connecting to Exchange Online, this script will enumerate Exchange Online
    role group members and export the results to a .CSV file. Reviewing EXO admins can assist with determining
    who can change Exchange Online configurations and view
.EXAMPLE
    PS C:\> Export-EXOAdmin -EngagementFolder foldername
    Exports Exchange Admins UserPrincipalName to .csv
.OUTPUTS
    EXOAdmins.csv
.NOTES
#>

BEGIN{
    Out-LogFile "Gathering Exchange Online Administrators" -Action

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"
}
PROCESS{
    $roles = foreach ($Role in Get-RoleGroup){
        $ExchangeAdmins = Get-RoleGroupMember -Identity $Role.Identity | Select-Object -Property *
            foreach ($admin in $ExchangeAdmins){
                if([string]::IsNullOrWhiteSpace($admin.WindowsLiveId)){
                    [PSCustomObject]@{
                        ExchangeAdminGroup = $Role.Name
                        Members= $admin.DisplayName
                        RecipientType = $admin.RecipientType
                    }
                }
                else{
                    [PSCustomObject]@{
                        ExchangeAdminGroup = $Role.Name
                        Members = $admin.WindowsLiveId
                        RecipientType = $admin.RecipientType
                    }
                }
            }
        }
    $roles | Out-MultipleFileType -FilePrefix "ExchangeOnlineAdministrators" -csv -json

}
END{
    Out-Logfile "Completed exporting Exchange Online Admins" -Information
}

}#End Function


Function Get-HawkTenantInboxRule {
    <#
    .SYNOPSIS
        Retrieves the currently active inbox rules and forwarding settings from all (or specified) mailboxes.
 
    .DESCRIPTION
        This function directly queries each mailbox in the organization to list its currently configured
        inbox rules and email forwarding settings. It provides a real-time snapshot of what rules are
        active right now, as opposed to historical audit data.
 
        Key points:
        - Directly collects the current state of each mailbox’s rules using Get-HawkUserInboxRule.
        - Also gathers forwarding settings from Get-HawkUserEmailForwarding.
        - Does not rely on audit logs; instead, uses live mailbox data.
         
        For historical records of when rules were created and past suspicious activity, use Get-HawkTenantInboxRuleHistory.
 
    .PARAMETER CSVPath
        A CSV file specifying a list of users to query.
        Expected columns: DisplayName, PrimarySMTPAddress (minimum).
 
    .PARAMETER UserPrincipalName
        The UPN of the admin or account used to authenticate against Exchange Online.
 
    .OUTPUTS
        This function calls Get-HawkUserInboxRule and Get-HawkUserEmailForwarding.
        For detailed information about the output, see their respective help documentation.
 
        File: Robust.log
        Path: \
        Description: The log file generated by Start-RobustCloudCommand, which is used to retrieve
                     the rules and forwarding information from each mailbox.
 
    .EXAMPLE
        Start-HawkTenantInboxRules -UserPrincipalName userx@tenantdomain.onmicrosoft.com
 
        Retrieves the current inbox rules and forwarding for all mailboxes in the organization.
 
    .EXAMPLE
        Start-HawkTenantInboxRules -csvpath c:\temp\myusers.csv -UserPrincipalName admin@tenantdomain.onmicrosoft.com
 
        Retrieves the current inbox rules and forwarding for all mailboxes listed in myusers.csv.
 
    .LINK
        https://gallery.technet.microsoft.com/office/Start-RobustCloudCommand-69fb349e
 
    .NOTES
        - This function shows the current (live) rules and forwarding settings.
        - For historical data on when rules were created, refer to Get-HawkTenantInboxRuleHistory.
    #>



    param (
        [string]$CSVPath,
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Prompt the user that this is going to take a long time to run
    $title = "Long Running Command"
    $message = "Running this search can take a very long time to complete (~1min per user). `nDo you wish to continue?"
    $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue operation"
    $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Exit Cmdlet"
    $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
    $result = $host.ui.PromptForChoice($title, $message, $options, 0)
    # If yes log and continue
    # If no log error and exit
    switch ($result) {
        0 { Out-LogFile "Starting full Tenant Search" -Action}
        1 { Write-Error -Message "User Stopped Cmdlet" -ErrorAction Stop }
    }

    # Get the exo PS session
    $exopssession = get-pssession | Where-Object { ($_.ConfigurationName -eq 'Microsoft.Exchange') -and ($_.State -eq 'Opened') }

    # Gather all of the mailboxes
    Out-LogFile "Getting all Mailboxes" -Action

    # If we don't have a value for csvpath then gather all users in the tenant
    if ([string]::IsNullOrEmpty($CSVPath)) {
        $AllMailboxes = Invoke-Command -Session $exopssession -ScriptBlock { Get-Recipient -RecipientTypeDetails UserMailbox -ResultSize Unlimited | Select-Object -Property DisplayName, PrimarySMTPAddress }
        $Allmailboxes | Out-MultipleFileType -FilePrefix "All_Mailboxes" -csv -json
    }
    # If we do read that in
    else {
        # Import the csv with error checking
        $error.clear()
        $AllMailboxes = Import-Csv $CSVPath
        if ($error.Count -gt 0) {
            Write-Error "Problem importing csv file aborting" -ErrorAction Stop
        }
    }

    # Report how many mailboxes we are going to operate on
    Out-LogFile ("Found " + $AllMailboxes.count + " Mailboxes") -Information

    # Path for robust log file
    # $RobustLog = Join-path $Hawk.FilePath "Robust.log"

    # Build the command we are going to need to run with Start-RobustCloudCommand
    # $cmd = "Start-RobustCloudCommand -UserPrincipalName " + $UserPrincipalName + " -logfile `$RobustLog -recipients `$AllMailboxes -scriptblock {Get-HawkUserInboxRule -UserPrincipalName `$input.PrimarySmtpAddress.tostring()}"
    $AllMailboxes | ForEach-Object {
        Start-RobustCloudCommand -UserPrincipalName $UserPrincipalName -LogFile $RobustLog -Recipients $_ -ScriptBlock {
            Get-HawkUserInboxRule -UserPrincipalName $_.PrimarySmtpAddress
        }
    }
    


    # Invoke our Start-Robust command to get all of the inbox rules
    Out-LogFile "===== Starting Robust Cloud Command to gather user inbox rules for all tenant users =====" -Action
    # Out-LogFile $cmd
    # Invoke-Expression $cmd

    # Build the command directly without using Invoke-Expression
    $AllMailboxes | ForEach-Object {
        Start-RobustCloudCommand -UserPrincipalName $UserPrincipalName -LogFile $RobustLog -Recipients $_ -ScriptBlock {
            Get-HawkUserInboxRule -UserPrincipalName $_.PrimarySmtpAddress
        }
    }

    Out-LogFile "Process Complete" -Information
}

Function Get-HawkTenantMailItemsAccessed {
<#
.SYNOPSIS
    This will export MailboxItemsAccessed operations from the Unified Audit Log (UAL). Must be connected to Exchange Online
    using the Connect-EXO or Connect-ExchangeOnline module. M365 E5 or G5 license is required for this function to work.
    This telemetry will ONLY be availabe if Advanced Auditing is enabled for the M365 tenant.
.DESCRIPTION
    Recent attacker activities have illuminated the use of the Graph API to read user mailbox contents. This will export
    logs that will be present if the attacker is using the Graph API for such actions. Note: NOT all graph API actions against
    a mailbox are malicious. Review the results of this function and look for Application IDs that are associated with a
    suspicious application ID.
.PARAMETER ApplicationID
    Malicious Application ID that you're investigating
.EXAMPLE
    Get-HawkTenantMailItemsAccessed
    Gets MailItemsAccess from Unified Audit Log (UAL) that corresponds to the App ID that is provided
.OUTPUTS
    MailItemsAccessed.csv
 
.LINK
    https://www.microsoft.com/security/blog/2020/12/21/advice-for-incident-responders-on-recovery-from-systemic-identity-compromises/
 
.NOTES
    "OperationnProperties" and "Folders" will return "System.Object" as they are nested JSON within the AuditData field.
    You will need to conduct individual log pull and review via PowerShell or other SIEM to determine values
    for those fields.
 
#>

    [cmdletbinding()]
    param(
        [parameter(Mandatory)]
        [string]$ApplicationID

    )
BEGIN {
    Out-LogFile "Starting Unified Audit Log (UAL) search for 'MailItemsAccessed'" -Action

}#End Begin

PROCESS{
    $MailboxItemsAccessed = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -Operations 'MailItemsAccessed' -FreeText $ApplicationID ")

    $MailboxItemsAccessed | Select-Object -ExpandProperty AuditData | Convertfrom-Json | Out-MultipleFileType -FilePrefix "MailItemsAccessed" -csv -json
}#End Process

END{

    Out-Logfile "Completed exporting MailItemsAccessed logs" -Information
}#End End


}#End Function

Function Get-HawkTenantRBACChange {
    <#
    .SYNOPSIS
        Looks for any changes made to Role-Based Access Control (RBAC).
    .DESCRIPTION
        Searches the Unified Audit Logs for commands related to RBAC management including role,
        role assignment, role entry, role group, and management scope changes. This helps track
        administrative permission changes across the tenant.
 
        Uses Get-AllUnifiedAuditLogEntry to ensure complete retrieval of all audit records,
        handling pagination automatically for large result sets.
 
        The function searches for operations including:
        - Role management (New/Remove/Set-ManagementRole)
        - Role assignments (New/Remove/Set-ManagementRoleAssignment)
        - Management scopes (New/Remove/Set-ManagementScope)
        - Role entries (New/Remove/Set-ManagementRoleEntry)
        - Role groups (New/Remove/Set/Add/Remove-RoleGroup*)
 
    .OUTPUTS
        File: Simple_RBAC_Changes.csv
        Path: \Tenant
        Description: Simplified view of RBAC changes in CSV format
 
        File: RBAC_Changes.csv
        Path: \Tenant
        Description: Detailed RBAC changes in CSV format
 
        File: RBAC_Changes.json
        Path: \Tenant
        Description: Raw audit data in JSON format for detailed analysis
 
    .EXAMPLE
        Get-HawkTenantRBACChange
 
        Searches for and reports all RBAC changes in the tenant within the configured search window.
    #>

    [CmdletBinding()]
    param()

    # Verify EXO connection and send telemetry
    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    Out-LogFile "Gathering any changes to RBAC configuration" -action

    # Define the operations to search for
    [array]$RBACOperations = @(
        "New-ManagementRole",
        "Remove-ManagementRole",
        "New-ManagementRoleAssignment",
        "Remove-ManagementRoleAssignment",
        "Set-ManagementRoleAssignment",
        "New-ManagementScope",
        "Remove-ManagementScope",
        "Set-ManagementScope",
        "New-ManagementRoleEntry",
        "Remove-ManagementRoleEntry",
        "Set-ManagementRoleEntry",
        "New-RoleGroup",
        "Remove-RoleGroup",
        "Set-RoleGroup",
        "Add-RoleGroupMember",
        "Remove-RoleGroupMember"
    )

    # Create tenant folder if it doesn't exist
    $TenantPath = Join-Path -Path $Hawk.FilePath -ChildPath "Tenant"
    if (-not (Test-Path -Path $TenantPath)) {
        New-Item -Path $TenantPath -ItemType Directory -Force | Out-Null
    }

    try {
        # Build search command for Get-AllUnifiedAuditLogEntry
        $searchCommand = "Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations " +
            "'$($RBACOperations -join "','")'"

        Out-LogFile "Searching for RBAC changes using Unified Audit Log." -Action

        # Get all RBAC changes using Get-AllUnifiedAuditLogEntry
        [array]$RBACChanges = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

        # Process results if any found
        if ($RBACChanges.Count -gt 0) {
            Out-LogFile ("Found " + $RBACChanges.Count + " changes made to Roles-Based Access Control") -Information

            # Parse changes using Get-SimpleUnifiedAuditLog
            $ParsedChanges = $RBACChanges | Get-SimpleUnifiedAuditLog

            # Output results if successfully parsed
            if ($ParsedChanges) {
                # Write simple format for easy analysis
                $ParsedChanges | Out-MultipleFileType -FilePrefix "Simple_RBAC_Changes" -csv -json

                # Write full audit logs for complete record
                $RBACChanges | Out-MultipleFileType -FilePrefix "RBAC_Changes" -csv -json
            }
            else {
                Out-LogFile "Error: Failed to parse RBAC changes" -isError
            }
        }
        else {
            Out-LogFile "No RBAC changes found." -Information
        }
    }
    catch {
        Out-LogFile "Error searching for RBAC changes: $($_.Exception.Message)" -isError
        Write-Error -ErrorRecord $_ -ErrorAction Continue
    }
}

Function Search-HawkTenantActivityByIP {
<#
.SYNOPSIS
    Gathers logon activity based on a submitted IP Address.
.DESCRIPTION
    Pulls logon activity from the Unified Audit log based on a provided IP address.
    Processes the data to highlight successful logons and the number of users accessed by a given IP address.
.PARAMETER IPaddress
    IP address to investigate
.OUTPUTS
 
    File: All_Events.csv
    Path: \<IP>
    Description: All logon events
 
    File: All_Events.xml
    Path: \<IP>\xml
    Description: Client XML of all logon events
 
    File: Success_Events.csv
    Path: \<IP>
    Description: All logon events that were successful
 
    File: Unique_Users_Attempted.csv
    Path: \<IP>
    Description: List of Unique users that this IP tried to log into
 
    File: Unique_Users_Success.csv
    Path: \<IP>
    Description: Unique Users that this IP succesfully logged into
 
    File: Unique_Users_Success.xml
    Path: \<IP>\XML
    Description: Client XML of unique users the IP logged into
.EXAMPLE
 
    Search-HawkTenantActivityByIP -IPAddress 10.234.20.12
 
    Searches for all Logon activity from IP 10.234.20.12.
#>

    param
    (
        [parameter(Mandatory = $true)]
        [string]$IpAddress
    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Replace an : in the IP address with . since : isn't allowed in a directory name
    $DirectoryName = $IpAddress.replace(":", ".")

    # Make sure we got only a single IP address
    if ($IpAddress -like "*,*") {
        Out-LogFile "Please provide a single IP address to search." -Information
        Write-Error -Message "Please provide a single IP address to search." -ErrorAction Stop
    }

    Out-LogFile ("Searching for events related to " + $IpAddress) -action

    # Gather all of the events related to these IP addresses
    [array]$ipevents = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -IPAddresses " + $IPAddress )

    # If we didn't get anything back log it
    if ($null -eq $ipevents) {
        Out-LogFile ("No IP logon events found for IP "    + $IpAddress) -Information
    }

    # If we did then process it
    else {

        # Expand out the Data and convert from JSON
        [array]$ipeventsexpanded = $ipevents | Select-object -ExpandProperty AuditData | ConvertFrom-Json
        Out-LogFile ("Found " + $ipeventsexpanded.count + " related to provided IP" ) -Information
        $ipeventsexpanded | Out-MultipleFileType -FilePrefix "All_Events" -csv -json -User $DirectoryName

        # Get the logon events that were a success
        [array]$successipevents = $ipeventsexpanded | Where-Object { $_.ResultStatus -eq "success" }
        Out-LogFile ("Found " + $successipevents.Count + " Successful logons related to provided IP") -Information
        $successipevents | Out-MultipleFileType -FilePrefix "Success_Events" -csv -json -User $DirectoryName

        # Select all unique users accessed by this IP
        [array]$uniqueuserlogons = Select-UniqueObject -ObjectArray $ipeventsexpanded -Property "UserID"
        Out-LogFile ("IP " + $ipaddress + " has tried to access " + $uniqueuserlogons.count + " users") -notice
        $uniqueuserlogons | Out-MultipleFileType -FilePrefix "Unique_Users_Attempted" -csv -json -User $DirectoryName -Notice

        if ($null -eq $uniqueuserlogonssuccess) {
            Out-LogFile ("No Successful Logon Events found for this IP: " + $IpAddress)
        }
        else {
            [array]$uniqueuserlogonssuccess = Select-UniqueObject -ObjectArray $successipevents -Property "UserID"
            Out-LogFile ("IP " + $IpAddress + " SUCCESSFULLY accessed " + $uniqueuserlogonssuccess.count + " users") -notice
            $uniqueuserlogonssuccess | Out-MultipleFileType -FilePrefix "Unique_Users_Success" -csv -json -User $DirectoryName -Notice
        }

    }

}

Function Start-HawkTenantInvestigation {
    <#
.SYNOPSIS
    Gathers common data about a tenant.
.DESCRIPTION
    Runs all Hawk Basic tenant related cmdlets and gathers data about the tenant's configuration,
    security settings, and audit logs. This comprehensive investigation helps identify potential
    security issues and configuration changes.
 
.PARAMETER Confirm
    Prompts for confirmation before running operations that could modify system state.
 
.PARAMETER WhatIf
    Shows what would happen if the command runs. The command is not run.
 
.EXAMPLE
    PS C:\> Start-HawkTenantInvestigation
    Runs a complete tenant investigation, gathering all available data.
 
.EXAMPLE
    PS C:\> Start-HawkTenantInvestigation -WhatIf
    Shows what data gathering operations would be performed without executing them.
 
.EXAMPLE
    PS C:\> Start-HawkTenantInvestigation -Confirm
    Prompts for confirmation before running each data gathering operation.
 
.OUTPUTS
    Various CSV and files containing investigation results.
    See help from individual cmdlets for specific output details.
    All outputs are placed in the $Hawk.FilePath directory.
#>

    [CmdletBinding(SupportsShouldProcess)]
    param()

    Write-HawkBanner

    if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
        Initialize-HawkGlobalObject
    }

    Out-LogFile "Starting Tenant Sweep" -action
    Send-AIEvent -Event "CmdRun"

    # Wrap operations in ShouldProcess checks
    if ($PSCmdlet.ShouldProcess("Tenant Configuration", "Get configuration data")) {
        Out-LogFile "Running Get-HawkTenantConfiguration" -action
        Get-HawkTenantConfiguration
    }

    if ($PSCmdlet.ShouldProcess("EDiscovery Configuration", "Get eDiscovery configuration")) {
        Out-LogFile "Running Get-HawkTenantEDiscoveryConfiguration" -action
        Get-HawkTenantEDiscoveryConfiguration
    }

    if ($PSCmdlet.ShouldProcess("Admin Inbox Rule Creation Audit Log", "Search Admin Inbox Rule Creation")) {
        Out-LogFile "Running Get-HawkTenantAdminInboxRuleCreation" -action
        Get-HawkTenantAdminInboxRuleCreation
    }

    if ($PSCmdlet.ShouldProcess("Admin Inbox Rule Modification Audit Log", "Search Admin Inbox Rule Modification")) {
        Out-LogFile "Running Get-HawkTenantInboxRuleModification" -action
        Get-HawkTenantAdminInboxRuleModification
    }

    if ($PSCmdlet.ShouldProcess("Admin Inbox Rule Removal Audit Log", "Search Admin Inbox Rule Removal")) {
        Out-LogFile "Running Get-HawkTenantAdminInboxRuleRemoval" -action
        Get-HawkTenantAdminInboxRuleRemoval
    }

    if ($PSCmdlet.ShouldProcess("Admin Inbox Rule Permission Change Audit Log", "Search Admin Inbox Permission Changes")) {
        Out-LogFile "Running Get-HawkTenantAdminMailboxPermissionChange" -action
        Get-HawkTenantAdminMailboxPermissionChange
    }
    
    if ($PSCmdlet.ShouldProcess("Admin Email Forwarding Change Change Audit Log", "Search Admin Email Forwarding Changes")) {
        Out-LogFile "Running Get-HawkTenantAdminEmailForwardingChange" -action
        Get-HawkTenantAdminEmailForwardingChange
    }


    if ($PSCmdlet.ShouldProcess("EDiscovery Logs", "Get eDiscovery logs")) {
        Out-LogFile "Running Get-HawkTenantEDiscoveryLog" -action
        Get-HawkTenantEDiscoveryLog
    }

    if ($PSCmdlet.ShouldProcess("Domain Activity", "Get domain activity")) {
        Out-LogFile "Running Get-HawkTenantDomainActivity" -action
        Get-HawkTenantDomainActivity
    }

    if ($PSCmdlet.ShouldProcess("RBAC Changes", "Get RBAC changes")) {
        Out-LogFile "Running Get-HawkTenantRBACChange" -action
        Get-HawkTenantRBACChange
    }

    if ($PSCmdlet.ShouldProcess("Azure App Audit Log", "Get app audit logs")) {
        Out-LogFile "Running Get-HawkTenantAzureAppAuditLog" -action
        Get-HawkTenantAzureAppAuditLog
    }

    if ($PSCmdlet.ShouldProcess("Exchange Admins", "Get Exchange admin list")) {
        Out-LogFile "Running Get-HawkTenantEXOAdmin" -action
        Get-HawkTenantEXOAdmin
    }

    if ($PSCmdlet.ShouldProcess("Consent Grants", "Get consent grants")) {
        Out-LogFile "Running Get-HawkTenantConsentGrant" -action
        Get-HawkTenantConsentGrant
    }

    if ($PSCmdlet.ShouldProcess("Entra ID Admins", "Get Entra ID admin list")) {
        Out-LogFile "Running Get-HawkTenantEntraIDAdmin" -action
        Get-HawkTenantEntraIDAdmin
    }

    if ($PSCmdlet.ShouldProcess("App and SPN Credentials", "Get credential details")) {
        Out-LogFile "Running Get-HawkTenantAppAndSPNCredentialDetail" -action
        Get-HawkTenantAppAndSPNCredentialDetail
    }

    if ($PSCmdlet.ShouldProcess("Entra ID Users", "Get Entra ID user list")) {
        Out-LogFile "Running Get-HawkTenantEntraIDUser" -action
        Get-HawkTenantEntraIDUser
    }
}

Function Get-HawkUserAdminAudit {
    <#
    .SYNOPSIS
        Searches the Unified Audit logs for any commands that were run against the provided user object.
    .DESCRIPTION
        Searches the Unified Audit logs for any commands that were run against the provided user object.
        Uses Get-AllUnifiedAuditLogEntry to ensure complete retrieval of all audit records within the
        specified search period, handling pagination and large result sets automatically.
 
    .PARAMETER UserPrincipalName
        UserPrincipalName of the user you're investigating. Can be a single UPN, comma-separated list,
        or array of objects containing UPNs.
 
    .OUTPUTS
        File: Simple_User_Changes.csv
        Path: \<user>
        Description: All cmdlets that were run against the user in a simple format.
 
        File: User_Changes.csv
        Path: \<user>
        Description: Raw data of all changes made to the user.
 
    .EXAMPLE
        Get-HawkUserAdminAudit -UserPrincipalName user@company.com
 
        Gets all changes made to user@company.com and outputs them to the csv and json files.
 
    .EXAMPLE
        Get-HawkUserAdminAudit -UserPrincipalName (Get-Mailbox -Filter {CustomAttribute1 -eq "VIP"})
 
        Gets admin audit data for all mailboxes with CustomAttribute1 set to "VIP".
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName
    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    foreach ($Object in $UserArray) {
        [string]$User = $Object.UserPrincipalName

        # Get the mailbox name since that is what we store in the admin audit log
        $MailboxName = (Get-Mailbox -Identity $User).Name

        Out-LogFile ("Searching for changes made to: " + $MailboxName) -action

        try {
            # Build search command for Get-AllUnifiedAuditLogEntry
            $searchCommand = "Search-UnifiedAuditLog -UserIds $User -RecordType ExchangeAdmin -Operations '*'"

            # Get all changes for this user using Get-AllUnifiedAuditLogEntry
            [array]$UserChanges = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

            # If there are any results process and output them
            if ($UserChanges.Count -gt 0) {
                Out-LogFile ("Found " + $UserChanges.Count + " changes made to this user") -Information

                # Get the user's output folder path
                $UserFolder = Join-Path -Path $Hawk.FilePath -ChildPath $User

                # Ensure user folder exists
                if (-not (Test-Path -Path $UserFolder)) {
                    New-Item -Path $UserFolder -ItemType Directory -Force | Out-Null
                }

                # Parse and format the changes using Get-SimpleUnifiedAuditLog
                $ParsedChanges = $UserChanges | Get-SimpleUnifiedAuditLog

                # Output the processed results
                if ($ParsedChanges) {
                    $ParsedChanges | Out-MultipleFileType -FilePrefix "Simple_User_Changes" -csv -json -User $User
                }

                # Output the raw changes
                $UserChanges | Out-MultipleFileType -FilePrefix "User_Changes" -csv -json -User $User
            }
            else {
                Out-LogFile "No User Changes found." -Information
            }
        }
        catch {
            Out-LogFile "Error processing audit logs for $User : $_" -isError
            Write-Error -ErrorRecord $_ -ErrorAction Continue
        }
    }
}

Function Get-HawkUserAuthHistory {
<#
.SYNOPSIS
    Gathers ip addresses that logged into the user account
.DESCRIPTION
    Pulls AzureActiveDirectoryAccountLogon events from the unified audit log for the provided user.
 
    If used with -ResolveIPLocations:
    Attempts to resolve the IP location using freegeoip.net
    Will flag ip addresses that are known to be owned by Microsoft using the XML from:
    https://support.office.com/en-us/article/URLs-and-IP-address-ranges-for-Office-365-operated-by-21Vianet-5C47C07D-F9B6-4B78-A329-BFDC1B6DA7A0
.PARAMETER UserPrincipalName
    Single UPN of a user, comma seperated list of UPNs, or array of objects that contain UPNs.
.PARAMETER ResolveIPLocations
    Resolved IP Locations
.OUTPUTS
 
    File: Converted_Authentication_Logs.csv
    Path: \<User>
    Description: All authentication activity for the user in a more readable form
.EXAMPLE
 
    Get-HawkUserAuthHistory -UserPrincipalName user@contoso.com -ResolveIPLocations
 
    Gathers authentication information for user@contoso.com.
    Attempts to resolve the IP locations for all authentication IPs found.
.EXAMPLE
 
    Get-HawkUserAuthHistory -UserPrincipalName (get-mailbox -Filter {Customattribute1 -eq "C-level"}) -ResolveIPLocations
 
    Gathers authentication information for all users that have "C-Level" set in CustomAttribute1
    Attempts to resolve the IP locations for all authentication IPs found.
#>

    param
    (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName,
        [switch]$ResolveIPLocations
    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName
    [array]$RecordTypes = "AzureActiveDirectoryAccountLogon", "AzureActiveDirectory", "AzureActiveDirectoryStsLogon"

    foreach ($Object in $UserArray) {

        [string]$User = $Object.UserPrincipalName

        # Make sure our array is null
        [array]$UserLogonLogs = $null

        Out-LogFile ("Retrieving Logon History for " + $User) -action

        # Get back the account logon logs for the user
        foreach ($Type in $RecordTypes) {
            Out-LogFile ("Searching Unified Audit log for Records of type: " + $Type) -action
            $UserLogonLogs += Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -UserIds " + $User + " -RecordType " + $Type)
        }

        # Make sure we have results
        if ($null -eq $UserLogonLogs) {
            Out-LogFile "No results found when searching UAL for AzureActiveDirectoryAccountLogon events" -isError
        }
        else {

            # Expand out the AuditData and convert from JSON
            Out-LogFile "Converting AuditData" -action
            $ExpandedUserLogonLogs = $null
            $ExpandedUserLogonLogs = New-Object System.Collections.ArrayList
            $FailedConversions = $null
            $FailedConversions = New-Object System.Collections.ArrayList

            # Process our results in a way to deal with JSON Errors
            Foreach ($Entry in $UserLogonLogs){

                try {
                    $jsonEntry = $Entry.AuditData | ConvertFrom-Json
                    $ExpandedUserLogonLogs.Add($jsonEntry) | Out-Null
                }
                catch {
                    $FailedConversions.Add($Entry) | Out-Null
                }
            }

            if ($FailedConversions.Count -le 0) {
                # Do nothing or handle the zero-case
            }
            else {
                Out-LogFile ("$($FailedConversions.Count) Entries failed JSON Conversion") -isError
                $FailedConversions | Out-MultipleFileType -FilePrefix "Failed_Conversion_Authentication_Logs" -User $User -Csv -Json
            }
            

            # Add IP Geo Location information to the data
            if ($ResolveIPLocations) {
                Out-File "Resolving IP Locations"
                # Setup our counter
                $i = 0

                # Loop thru each connection and get the location
                while ($i -lt $ExpandedUserLogonLogs.Count) {

                    if ([bool]($i % 25)) { }
                    Else {
                        Write-Progress -Activity "Looking Up Ip Address Locations" -CurrentOperation $i -PercentComplete (($i / $ExpandedUserLogonLogs.count) * 100)
                    }

                    # Get the location information for this IP address
                    if($ExpandedUserLogonLogs.item($i).clientip){
                    $Location = Get-IPGeolocation -ipaddress $ExpandedUserLogonLogs.item($i).clientip
                    }
                    else {
                        $Location = "IP Address Null"
                    }

                    # Combine the connection object and the location object so that we have a single output ready
                    $ExpandedUserLogonLogs.item($i) = ($ExpandedUserLogonLogs.item($i) | Select-Object -Property *, @{Name = "CountryName"; Expression = { $Location.CountryName } }, @{Name = "RegionCode"; Expression = { $Location.RegionCode } }, @{Name = "RegionName"; Expression = { $Location.RegionName } }, @{Name = "City"; Expression = { $Location.City } }, @{Name = "KnownMicrosoftIP"; Expression = { $Location.KnownMicrosoftIP } })

                    # increment our counter for the progress bar
                    $i++
                }

                Write-Progress -Completed -Activity "Looking Up Ip Address Locations" -Status " "
            }
            else {
                Out-LogFile "ResolveIPLocations not specified" -Information
            }

            # Convert to human readable and export
            Out-LogFile "Converting to Human Readable" -action
            (Import-AzureAuthenticationLog -JsonConvertedLogs $ExpandedUserLogonLogs) | Out-MultipleFileType -fileprefix "Converted_Authentication_Logs" -User $User -csv -json

            # Export RAW data
            $UserLogonLogs | Out-MultipleFileType -fileprefix "Raw_Authentication_Logs" -user $User -csv -json

        }
    }


}


Function Get-HawkUserAutoReply {
<#
.SYNOPSIS
    Pulls AutoReply Configuration for the specified user.
.DESCRIPTION
    Gathers AutoReply configuration for the provided users.
    Looks for AutoReplyState of Enabled and exports the config.
.PARAMETER UserPrincipalName
    Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs.
.OUTPUTS
 
    File: AutoReply.txt
    Path: \<User>
    Description: AutoReplyConfiguration for the user.
.EXAMPLE
 
    Get-HawkUserAutoReply -UserPrincipalName user@contoso.com
 
    Pulls AutoReplyConfiguration for user@contoso.com and looks for AutoReplyState Enabled.
.EXAMPLE
 
    Get-HawkUserAutoReply -UserPrincipalName (get-mailbox -Filter {Customattribute1 -eq "C-level"})
 
    Gathers AutoReplyConfiguration for all users who have "C-Level" set in CustomAttribute1
#>

    param
    (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName

    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    foreach ($Object in $UserArray) {

        [string]$User = $Object.UserPrincipalName

        # Get Autoreply Configuration
        Out-LogFile ("Retrieving AutoReply Configuration: " + $User) -action
        $AutoReply = Get-MailboxAutoReplyConfiguration -Identity  $User

        # Check if the Autoreply is Disabled
        if ($AutoReply.AutoReplyState -eq 'Disabled') {

            Out-LogFile "AutoReply is not enabled or not configured." -Information
        }
        # Output Enabled AutoReplyConfiguration to a generic txt
        else {

            $AutoReply | Out-MultipleFileType -FilePreFix "AutoReply" -User $user -txt
        }
    }

}


Function Get-HawkUserConfiguration {
    <#
.SYNOPSIS
    Gathers baseline information about the provided user.
.DESCRIPTION
    Gathers and records baseline information about the provided user.
    * Get-EXOMailbox
    * Get-EXOMailboxStatistics
    * Get-EXOMailboxFolderStatistics
    * Get-CASMailbox
.PARAMETER UserPrincipalName
    Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs.
.OUTPUTS
 
    File: Mailbox_Info.txt
    Path: \<User>
    Description: Output of Get-EXOMailbox for the user
 
    File: Mailbox_Statistics.txt
    Path : \<User>
    Description: Output of Get-EXOMailboxStatistics for the user
 
    File: Mailbox_Folder_Statistics.txt
    Path : \<User>
    Description: Output of Get-EXOMailboxFolderStatistics for the user
 
    File: CAS_Mailbox_Info.txt
    Path : \<User>
    Description: Output of Get-CasMailbox for the user
.EXAMPLE
    Get-HawkUserConfiguration -user bsmith@contoso.com
 
    Gathers the user configuration for bsmith@contoso.com
.EXAMPLE
 
    Get-HawkUserConfiguration -UserPrincipalName (Get-EXOMailbox -Filter {Customattribute1 -eq "C-level"})
 
    Gathers the user configuration for all users who have "C-Level" set in CustomAttribute1
#>


    param
    (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName
    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    foreach ($Object in $UserArray) {
        [string]$User = $Object.UserPrincipalName

        Out-LogFile ("Gathering information about " + $User) -action

        #Gather mailbox information
        Out-LogFile "Gathering Mailbox Information" -action
        $mbx = Get-EXOMailbox -Identity $user

        # Test to see if we have an archive and include that info as well
        if (!($null -eq $mbx.archivedatabase)) {
            Get-EXOMailboxStatistics -identity $user -Archive | Out-MultipleFileType -FilePrefix "Mailbox_Archive_Statistics" -user $user -txt
        }

        $mbx | Out-MultipleFileType -FilePrefix "Mailbox_Info" -User $User -txt
        Get-EXOMailboxStatistics -Identity $user | Out-MultipleFileType -FilePrefix "Mailbox_Statistics" -User $User -txt
        Get-EXOMailboxFolderStatistics -identity $user | Out-MultipleFileType -FilePrefix "Mailbox_Folder_Statistics" -User $User -txt

        # Gather cas mailbox sessions
        Out-LogFile "Gathering CAS Mailbox Information" -action
        Get-EXOCasMailbox -identity $user | Out-MultipleFileType -FilePrefix "CAS_Mailbox_Info" -User $User -txt
    }
}


Function Get-HawkUserEmailForwarding {
    <#
    .SYNOPSIS
    Pulls mail forwarding configuration for a specified user.
    .DESCRIPTION
    Pulls the values of ForwardingSMTPAddress and ForwardingAddress to see if the user has these configured.
    .PARAMETER UserPrincipalName
    Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs.
    .OUTPUTS
 
    File: _Investigate_Users_WithForwarding.csv
    Path: \
    Description: All users that are found to have forwarding configured.
 
    File: User_ForwardingReport.csv
    Path: \
    Description: Mail forwarding configuration for all searched users; even if null.
 
    File: ForwardingReport.csv
    Path: \<user>
    Description: Forwarding confiruation of the searched user.
    .EXAMPLE
 
    Get-HawkUserEmailForwarding -UserPrincipalName user@contoso.com
 
    Gathers possible email forwarding configured on the user.
 
    .EXAMPLE
 
    Get-HawkUserEmailForwarding -UserPrincipalName (get-mailbox -Filter {Customattribute1 -eq "C-level"})
 
    Gathers possible email forwarding configured for all users who have "C-Level" set in CustomAttribute1
    #>


    param
    (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName
    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    foreach ($Object in $UserArray) {

        [string]$User = $Object.UserPrincipalName

        # Looking for email forwarding stored in AD
        Out-LogFile ("Gathering possible Forwarding changes for: " + $User) -action
        Out-LogFile "Collecting AD Forwarding Settings" -action
        $mbx = Get-Mailbox -identity $User

        # Check if forwarding is configured by user or admin
        if ([string]::IsNullOrEmpty($mbx.ForwardingSMTPAddress) -and [string]::IsNullOrEmpty($mbx.ForwardingAddress)) {
            Out-LogFile "No forwarding configuration found" -Information
        }
        # If populated report it and add to a CSV file of positive finds
        else {
            Out-LogFile ("Found Email forwarding User:" + $mbx.primarySMTPAddress + " ForwardingSMTPAddress:" + $mbx.ForwardingSMTPAddress + " ForwardingAddress:" + $mbx.ForwardingAddress) -notice
            $mbx | Select-Object DisplayName, UserPrincipalName, PrimarySMTPAddress, ForwardingSMTPAddress, ForwardingAddress, DeliverToMailboxAndForward, WhenChangedUTC | Out-MultipleFileType -FilePreFix "_Investigate_Users_WithForwarding" -append -csv -json -notice
        }

        # Add all users searched to a generic output
        $mbx | Select-Object DisplayName, UserPrincipalName, PrimarySMTPAddress, ForwardingSMTPAddress, ForwardingAddress, DeliverToMailboxAndForward, WhenChangedUTC | Out-MultipleFileType -FilePreFix "User_ForwardingReport" -append -csv -json
        # Also add to an output specific to this user
        $mbx | Select-Object DisplayName, UserPrincipalName, PrimarySMTPAddress, ForwardingSMTPAddress, ForwardingAddress, DeliverToMailboxAndForward, WhenChangedUTC | Out-MultipleFileType -FilePreFix "ForwardingReport" -user $user -csv -json

    }
}

Function Get-HawkUserHiddenRule {
    <#
    .SYNOPSIS
    Pulls inbox rules for the specified user using EWS.
    .DESCRIPTION
    Pulls inbox rules for the specified user using EWS.
    Searches the resulting rules looking for "hidden" rules.
 
    Requires impersonation:
    https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-configure-impersonation
 
    Since the rules are hidden we have to pull it as a message instead of a rule.
    That means that the only information we can get back is the ID and Priority of the rule.
    Once a mailbox has been identified as having a hidden rule please use MFCMapi to review and remove the rule as needed.
 
    https://blogs.msdn.microsoft.com/hkong/2015/02/27/how-to-delete-corrupted-hidden-inbox-rules-from-a-mailbox-using-mfcmapi/
    .PARAMETER UserPrincipalName
    Single UPN of a user, comma separated list of UPNs, or array of objects that contain UPNs.
    .PARAMETER EWSCredential
    Credentials of a user that can impersonate the target user/users.
    Gather using (get-credential)
    Does NOT work with MFA protected accounts at this time.
    .OUTPUTS
 
    File: _Investigate.txt
    Path: \
    Description: Adds any hidden rules found here to be investigated
 
    File: EWS_Inbox_rule.csv
    Path: \<User>
    Description: Inbox rules that were found with EWS
    .EXAMPLE
 
    Get-HawkUserHiddenRule -UserPrincipalName user@contoso.com -EWSCredential (get-credential)
 
    Searches user@contoso.com looking for hidden inbox rules using the provided credentials
    .EXAMPLE
 
    Get-HawkUserHiddenRule -UserPrincipalName (get-mailbox -Filter {Customattribute1 -eq "C-level"})
 
    Looks for hidden inbox rules for all users who have "C-Level" set in CustomAttribute1
    #>

    param (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName,
        [System.Management.Automation.PSCredential]$EWSCredential
    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    # Process each object received
    foreach ($Object in $UserArray) {

        # Push the UPN into $user for ease of use
        $user = $Object.UserPrincipalName

        # Determine if the email address is null or empty
        [string]$EmailAddress = (Get-EXOMailbox $user).PrimarySmtpAddress
        if ([string]::IsNullOrEmpty($EmailAddress)) {
            Write-Warning "No SMTP Address found. Skipping."
            return $null
        }

        # If we don't have a credential object, ask for credentials
        if ($null -eq $EWSCredential) {
            Out-LogFile "Please provide credentials that have impersonation rights to the mailbox you are looking to check" -Information
            $EWSCredential = Get-Credential
        }

        # Import the EWS Managed API
        if (Test-Path 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll') {
            Out-LogFile "EWS Managed API Found" -Information
        } else {
            Write-Error "Please install EWS Managed API 2.2 `nhttp://www.microsoft.com/en-us/download/details.aspx?id=42951" -ErrorAction Stop
        }

        # Import the EWS Managed API DLL
        Import-Module 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll'

        # Set up the EWS Connection
        Write-Information ("Setting up connection for " + $EmailAddress)
        $exchService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList ([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_Sp1)
        $exchService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($EWSCredential.Username, $EWSCredential.GetNetworkCredential().Password)

        # Autodiscover or use global EWS URL
        if ($null -eq $EWSUrl) {
            $exchService.AutodiscoverUrl($EmailAddress, { $true })
            $exchService.Url | Set-Variable -Name EWSUrl -Scope Global
        } else {
            $exchService.Url = $EWSUrl
        }

        # Set impersonation
        $exchService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress)

        # Add the Anchor mailbox to the HTTP header
        $exchService.HttpHeaders.Add("X-AnchorMailbox", [string]$EmailAddress)

        # Search for hidden rules
        $SearchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass, "IPM.Rule.Version2.Message")
        $ItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(500)
        $ItemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Associated

        # Create our property set to view
        $PR_RULE_MSG_NAME = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x65EC, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String)
        $PR_RULE_MSG_PROVIDER = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x65EB, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String)
        $PR_PRIORITY = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0026, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer)
        $psPropset = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IDOnly, $PR_RULE_MSG_NAME, $PR_RULE_MSG_PROVIDER, $PR_PRIORITY)

        # Add the property set to the item view
        $ItemView.PropertySet = $psPropset

        # Do the search and return the items
        $ruleResults = $inbox.FindItems($SearchFilter, $ItemView)

        # Check each rule directly from $ruleResults
        $FoundHidden = $false
        foreach ($rule in $ruleResults) {
            if ([string]::IsNullOrEmpty($rule.ExtendedProperties[0].Value) -or [string]::IsNullOrEmpty($rule.ExtendedProperties[1].Value)) {
                $priority = ($rule.ExtendedProperties | Where-Object { $_.PropertyDefinition.Tag -eq 38 }).Value
                Out-LogFile ("Possible Hidden Rule found in mailbox: " + $EmailAddress + " -- Rule Priority: " + $priority) -Notice
                $RuleOutput = $rule | Select-Object -Property ID, @{ Name = "Priority"; Expression = { ($rule.ExtendedProperties | Where-Object { $_.PropertyDefinition -like "*38*" }).Value } }
                $RuleOutput | Out-MultipleFileType -FilePrefix "EWS_Inbox_rule" -Txt -User $user -Append
                $FoundHidden = $true
            }
        }

        # Log if no hidden rules are found
        if ($FoundHidden -eq $false) {
            Out-LogFile ("No Hidden rules found for mailbox: " + $EmailAddress) -Information
        }
    }
}


# Gets user inbox rules and looks for Investigate rules
Function Get-HawkUserInboxRule {
<#
.SYNOPSIS
    Exports inbox rules for the specified user.
.DESCRIPTION
    Gathers inbox rules for the provided uers.
    Looks for rules that forward or delete email and flag them for follow up
.PARAMETER UserPrincipalName
    Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs.
.OUTPUTS
 
    File: _Investigate_InboxRules.csv
    Path: \<User>
    Description: Inbox rules that delete or forward messages.
 
    File: InboxRules.csv
    Path: \<User>
    Description: All inbox rules that were found for the user.
 
    File: All_InboxRules.csv
    Path: \
    Description: All users inbox rules.
.EXAMPLE
 
    Get-HawkUserInboxRule -UserPrincipalName user@contoso.com
 
    Pulls all inbox rules for user@contoso.com and looks for Investigate rules.
.EXAMPLE
 
    Get-HawkUserInboxRule -UserPrincipalName (get-mailbox -Filter {Customattribute1 -eq "C-level"})
 
    Gathers inbox rules for all users who have "C-Level" set in CustomAttribute1
#>


    param
    (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName

    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    foreach ($Object in $UserArray) {

        [string]$User = $Object.UserPrincipalName

        # Get Inbox rules
        Out-LogFile ("Gathering Inbox Rules: " + $User) -action
        $InboxRules = Get-InboxRule -mailbox  $User

        if ($null -eq $InboxRules) { Out-LogFile "No Inbox Rules found" -Information } 
        else {
            # If the rules contains one of a number of known suspecious properties flag them
            foreach ($Rule in $InboxRules) {
                # Set our flag to false
                $Investigate = $false

                # Evaluate each of the properties that we know bad actors like to use and flip the flag if needed
                if ($Rule.DeleteMessage -eq $true) { $Investigate = $true }
                if (!([string]::IsNullOrEmpty($Rule.ForwardAsAttachmentTo))) { $Investigate = $true }
                if (!([string]::IsNullOrEmpty($Rule.ForwardTo))) { $Investigate = $true }
                if (!([string]::IsNullOrEmpty($Rule.RedirectTo))) { $Investigate = $true }

                # If we have set the Investigate flag then report it and output it to a seperate file
                if ($Investigate -eq $true) {
                    Out-LogFile ("Possible Investigate inbox rule found ID:" + $Rule.Identity + " Rule:" + $Rule.Name) -notice
                    # Description is multiline
                    $Rule.Description = $Rule.Description.replace("`r`n", " ").replace("`t", "")
                    $Rule | Out-MultipleFileType -FilePreFix "_Investigate_InboxRules" -user $user -csv -json -append -Notice
                }
            }

            # Description is multiline
            $inboxrulesRawDescription = $InboxRules
            $InboxRules = New-Object -TypeName "System.Collections.ArrayList"

            $inboxrulesRawDescription | ForEach-Object {
                $_.Description = $_.Description.Replace("`r`n", " ").replace("`t", "")

                $null = $InboxRules.Add($_)
            }

            # Output all of the inbox rules to a generic csv
            $InboxRules | Out-MultipleFileType -FilePreFix "InboxRules" -User $user -csv -json

            # Add all of the inbox rules to a generic collection file
            $InboxRules | Out-MultipleFileType -FilePrefix "All_InboxRules" -csv -json -Append
        }

        # Get any Sweep Rules
        # Suggested by Adonis Sardinas
        Out-LogFile ("Gathering Sweep Rules: " + $User) -action
        $SweepRules = Get-SweepRule -Mailbox $User

        if ($null -eq $SweepRules) { Out-LogFile "No Sweep Rules found" -Information}
        else {

            # Output all rules to a user CSV
            $SweepRules | Out-MultipleFileType -FilePreFix "SweepRules" -user $User -csv -json

            # Add any found to the whole tenant list
            $SweepRules | Out-MultipleFileType -FilePreFix "All_SweepRules" -csv -json -append

        }
    }
}


function Get-HawkUserMailboxAuditing {
    <#
    .SYNOPSIS
        Gathers Mailbox Audit data if enabled for the user.
 
    .DESCRIPTION
        Retrieves mailbox audit logs from Microsoft 365 Unified Audit Log, focusing on mailbox
        content access and operations. This function replaces the deprecated Search-MailboxAuditLog
        cmdlet with modern UAL-based auditing.
 
        Migration Changes:
        - Old: Used Search-MailboxAuditLog for direct mailbox audit log access
        - New: Uses Search-UnifiedAuditLog with separate collection of:
          * ExchangeItem records (item-level operations)
          * ExchangeItemGroup records (access patterns)
 
        The new implementation provides:
        - Improved visibility into mailbox item access patterns
        - More consistent data collection across Exchange Online
        - Automatic pagination for large result sets
        - Integration with Microsoft 365 compliance center
        - Separated output files for better data analysis
 
        Note: Administrative actions on mailboxes (like granting permissions) are tracked by
        Get-HawkUserAdminAudit instead of this function.
 
    .PARAMETER UserPrincipalName
        Single UPN of a user, comma-separated list of UPNs, or array of objects that contain UPNs.
 
    .OUTPUTS
        ExchangeItem Records:
        File: ExchangeItem_Simple_{User}.csv/.json
        Path: \<User>
        Description: Flattened item-level operations data in CSV and JSON formats
 
        File: ExchangeItem_Logs_{User}.csv/.json
        Path: \<User>
        Description: Raw item-level operations data in CSV and JSON formats
 
        ExchangeItemGroup Records:
        File: ExchangeItemGroup_Simple_{User}.csv/.json
        Path: \<User>
        Description: Flattened access pattern data in CSV and JSON formats
 
        File: ExchangeItemGroup_Logs_{User}.csv/.json
        Path: \<User>
        Description: Raw access pattern data in CSV and JSON formats
 
    .EXAMPLE
        Get-HawkUserMailboxAuditing -UserPrincipalName user@contoso.com
 
        Search for all Mailbox Audit logs from user@contoso.com, creating separate files for
        item operations and access patterns, each with both raw and processed formats.
 
    .EXAMPLE
        Get-HawkUserMailboxAuditing -UserPrincipalName (Get-Mailbox -Filter {CustomAttribute1 -eq "C-level"})
 
        Search for all Mailbox Audit logs for all users who have "C-Level" set in CustomAttribute1,
        creating separate output files for each user's item operations and access patterns.
 
    .NOTES
        In older versions of Exchange Online, Search-MailboxAuditLog provided direct access to
        mailbox audit data. This has been replaced by the Unified Audit Log which provides a
        more comprehensive and consistent view of mailbox activities through separate record types:
        - ExchangeItem: Tracks specific operations on items
        - ExchangeItemGroup: Tracks access patterns and aggregated activity
 
        Each record type is processed separately and output in multiple formats to support
        different analysis needs:
        - Simple (flattened) formats for easy analysis
        - Raw formats for detailed investigation
        - JSON dumps for programmatic processing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName
    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    foreach ($Object in $UserArray) {
        [string]$User = $Object.UserPrincipalName

        Out-LogFile ("Attempting to Gather Mailbox Audit logs for: " + $User) -action

        # Test if mailbox auditing is enabled
        $mbx = Get-Mailbox -Identity $User
        if ($mbx.AuditEnabled -eq $true) {
            Out-LogFile "Mailbox Auditing is enabled." -Information

            try {
                # Get the user's folder path
                $UserFolder = Join-Path -Path $Hawk.FilePath -ChildPath $User
                if (-not (Test-Path -Path $UserFolder)) {
                    New-Item -Path $UserFolder -ItemType Directory -Force | Out-Null
                }

                # Process ExchangeItem records
                Out-LogFile "Searching Unified Audit Log for ExchangeItem events." -action
                $searchCommand = "Search-UnifiedAuditLog -UserIds $User -RecordType ExchangeItem"
                $itemLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

                if ($itemLogs.Count -gt 0) {
                    Out-LogFile ("Found " + $itemLogs.Count + " ExchangeItem events.") -Information

                    # Process and output flattened data
                    $ParsedItemLogs = $itemLogs | Get-SimpleUnifiedAuditLog
                    if ($ParsedItemLogs) {
                        $ParsedItemLogs | Out-MultipleFileType -FilePrefix "ExchangeItem_Simple" -csv -json -User $User
                    }

                    # Output raw data
                    $itemLogs | Out-MultipleFileType -FilePrefix "ExchangeItem_Logs" -csv -json -User $User
                }
                else {
                    Out-LogFile "No ExchangeItem events found." -Information
                }

                # Process ExchangeItemGroup records
                Out-LogFile "Searching Unified Audit Log for ExchangeItemGroup events." -action
                $searchCommand = "Search-UnifiedAuditLog -UserIds $User -RecordType ExchangeItemGroup"
                $groupLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch $searchCommand

                if ($groupLogs.Count -gt 0) {
                    Out-LogFile ("Found " + $groupLogs.Count + " ExchangeItemGroup events.") -Information

                    # Process and output flattened data
                    $ParsedGroupLogs = $groupLogs | Get-SimpleUnifiedAuditLog
                    if ($ParsedGroupLogs) {
                        $ParsedGroupLogs | Out-MultipleFileType -FilePrefix "ExchangeItemGroup_Simple" -csv -json -User $User
                    }

                    # Output raw data
                    $groupLogs | Out-MultipleFileType -FilePrefix "ExchangeItemGroup_Logs" -csv -json -User $User
                }
                else {
                    Out-LogFile "No ExchangeItemGroup events found." -Information
                }

                # Summary logging
                $totalEvents = ($itemLogs.Count + $groupLogs.Count)
                Out-LogFile "Completed processing $totalEvents total events." -Information
            }
            catch {
                Out-LogFile "Error retrieving audit logs: $($_.Exception.Message)" -isError
                Write-Error -ErrorRecord $_ -ErrorAction Continue
            }
        }
        else {
            Out-LogFile ("Auditing not enabled for " + $User) -Information
            Out-LogFile "Enable auditing to track mailbox access patterns." -Information
        }
    }
}

Function Get-HawkUserMessageTrace {
<#
.SYNOPSIS
    Pull that last 7 days of message trace data for the specified user.
.DESCRIPTION
        Pulls the basic message trace data for the specified user.
        Can only pull the last 7 days as that is all we keep in get-messagetrace
 
        Further investigation will require Start-HistoricalSearch
.PARAMETER UserPrincipalName
Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs.
.OUTPUTS
 
    File: Message_Trace.csv
    Path: \<User>
    Description: Output of Get-MessageTrace -Sender <primarysmtpaddress>
.EXAMPLE
 
    Get-HawkUserMessageTrace -UserPrincipalName user@contoso.com
 
    Gets the message trace for user@contoso.com for the last 7 days
#>


    param
    (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName

    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    # Gather the trace
    foreach ($Object in $UserArray) {

        [string]$User = $Object.UserPrincipalName

        [string]$PrimarySMTP = (Get-Mailbox -identity $User).primarysmtpaddress

        if ([string]::IsNullOrEmpty($PrimarySMTP)) {
            Out-LogFile ("Failed to find Primary SMTP Address for user: " + $User) -isError
            Write-Error ("Failed to find Primary SMTP Address for user: " + $User)
        }
        else {
            # Get the 7 day message trace for the primary SMTP address as the sender
            Out-LogFile ("Gathering messages sent by: " + $PrimarySMTP) -action

            (Get-MessageTrace -Sender $PrimarySMTP) | Out-MultipleFileType -FilePreFix "Message_Trace" -user $User -csv -json
        }
    }
}


Function Get-HawkUserMobileDevice {
<#
.SYNOPSIS
    Gathers mobile devices that are connected to the account
.DESCRIPTION
    Pulls all mobile devices attached to them mailbox using get-mobiledevice
 
    If any devices had their first sync inside of the investigation window it will flag them.
    Investigator should follow up on these devices
.PARAMETER UserPrincipalName
    Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs.
.OUTPUTS
 
    File: MobileDevices.csv
    Path: \<User>
    Description: All mobile devices attached to the mailbox
 
    File: _Investigate_MobileDevice.csv
    Path: \<User>
    Descriptoin: Any devices that were found to have their first sync inside of the investigation window
.EXAMPLE
 
    Get-HawkUserMessageTrace -UserPrincipalName user@contoso.com
 
    Gets the message trace for user@contoso.com for the last 7 days
#>


    param
    (
        [Parameter(Mandatory = $true)]
        [array]$UserPrincipalName

    )

    Test-EXOConnection
    Send-AIEvent -Event "CmdRun"

    # Verify our UPN input
    [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName

    # Gather the trace
    foreach ($Object in $UserArray) {

        [string]$User = $Object.UserPrincipalName

        # Get all mobile devices
        Out-Logfile ("Gathering Mobile Devices for: " + $User) -Action
        [array]$MobileDevices = Get-MobileDevice -mailbox $User

        if ($Null -eq $MobileDevices) {
            Out-Logfile ("No devices found for user: " + $User) -Information
        }
        else {
            Out-Logfile ("Found " + $MobileDevices.count + " Devices") -Information

            # Check each device to see if it was NEW
            # If so flag it for investigation
            foreach ($Device in $MobileDevices){
                if ($Device.FirstSyncTime -gt $Hawk.StartDate){
                    Out-Logfile ("Device found that was first synced inside investigation window") -notice
                    Out-LogFile ("DeviceID: " + $Device.DeviceID) -notice
                    $Device | Out-MultipleFileType -FilePreFix "_Investigate_MobileDevice" -user $user -csv -json -append -Notice
                }
            }

            # Output all devices found
            $MobileDevices | Out-MultipleFileType -FilePreFix "MobileDevices" -user $user -csv -json
        }
    }
}


Function Get-HawkUserPWNCheck {
    <#
    .SYNOPSIS
        Checks an email address against haveibeenpwned.com
    .DESCRIPTION
        Checks a single email address against HaveIBeenPwned. An API key is required and can be obtained from https://haveibeenpwned.com/API/Key for $3.50 a month.
        This script will prompt for the key if $hibpkey is not set as a variable.
    .PARAMETER EmailAddress
        Accepts since EMail address or array of Email address strings.
        DOES NOT Accept an array of objects (it will end up checked the UPN and not the email address)
    .OUTPUTS
        File: Have_I_Been_Pwned.txt
        Path: \<user>
        Description: Information returned from the pwned database
    .EXAMPLE
        Get-HawkUserPWNCheck -EmailAddress user@company.com
 
        Returns the pwn state of the email address provided
    #>


        param(
            [string[]]$EmailAddress
            )

        # if there is no value of hibpkey then we need to get it from the user
        BEGIN {if ($null -eq $hibpkey) {

            Write-Host -ForegroundColor Green "
 
            HaveIBeenPwned.com now requires an API access key to gather Stats with from their API.
 
            Please purchase an API key for `$3.95 a month from get a Free access key from https://haveibeenpwned.com/API/Key and provide it below.
 
            "


            # get the access key from the user
            $hibpkey = Read-Host "haveibeenpwned.com apikey"
            }
        }#End of BEGIN block

        # Verify our UPN input
        PROCESS {[array]$UserArray = Test-UserObject -ToTest $EmailAddress
        $headers=@{'hibp-api-key' = $hibpkey}

        foreach ($Object in $UserArray) {

            [string]$User = $Object.UserPrincipalName

            # Convert the email to URL encoding
            $uriEncodeEmail = [uri]::EscapeDataString($($user))

            # Build and invoke the URL
            $InvokeURL = 'https://haveibeenpwned.com/api/v3/breachedaccount/' + $uriEncodeEmail + '?truncateResponse=false'
            $Error.clear()
            #Will catch the error if the email is not found. 404 error means that the email is not found in the database.
            #https://haveibeenpwned.com/API/v3#ResponseCodes contains the response codes for the API
            try {
                $Result = Invoke-WebRequest -Uri $InvokeURL -Headers $headers -userAgent 'Hawk' -ErrorAction Stop
            }
            catch {
                $StatusCode = $_.Exception.Response.StatusCode
                $ErrorMessage = $_.Exception.Message
                switch ($StatusCode) {
                    NotFound{
                        write-host "Email Provided Not Found in Pwned Database"
                        return
                    }
                    Unauthorized{
                        write-host "Unauthorised Access - API key provided is not valid or has expired"
                        return
                    }
                    Default {
                        write-host $ErrorMessage
                        return
                    }
                }
            }

            # Convert the result into a PS custgom object
            $Pwned = $Result.content | ConvertFrom-Json

            # Output the value
            Out-LogFile ("Email Address found in " + $pwned.count)
            $Pwned | Out-MultipleFileType -FilePreFix "Have_I_Been_Pwned" -user $user -txt


            }
        }#End of PROCESS block
        END {
            Start-Sleep -Milliseconds 1500
        }#End of END block
}#End of Function Get-HawkUserPWNCheck

Function Start-HawkUserInvestigation {
    <#
    .SYNOPSIS
        Gathers common data about a provided user.
     
    .DESCRIPTION
        Runs all Hawk user-related cmdlets against the specified user and gathers the data.
     
        Cmdlet Information Gathered
        ------------------------- -------------------------
        Get-HawkTenantConfiguration Basic Tenant information
        Get-HawkUserConfiguration Basic User information
        Get-HawkUserInboxRule Searches the user for Inbox Rules
        Get-HawkUserEmailForwarding Looks for email forwarding configured on the user
        Get-HawkUserAutoReply Looks for enabled AutoReplyConfiguration
        Get-HawkUserAuthHistory Searches the unified audit log for user logons
        Get-HawkUserMailboxAuditing Searches the unified audit log for mailbox auditing information
        Get-HawkUserAdminAudit Searches the EXO Audit logs for commands run against the provided user
        Get-HawkUserMessageTrace Pulls emails sent by the user in the last 7 days
     
    .PARAMETER UserPrincipalName
        Single UPN of a user, comma-separated list of UPNs, or an array of objects that contain UPNs.
     
    .PARAMETER Confirm
        Prompts for confirmation before running operations that could modify system state.
     
    .PARAMETER WhatIf
        Shows what would happen if the command runs. The command is not actually run.
     
    .OUTPUTS
        See help from individual cmdlets for output list.
        All outputs are placed in the $Hawk.FilePath directory.
     
    .EXAMPLE
        Start-HawkUserInvestigation -UserPrincipalName bsmith@contoso.com
     
        Runs all Get-HawkUser* cmdlets against the user with UPN bsmith@contoso.com.
     
    .EXAMPLE
        Start-HawkUserInvestigation -UserPrincipalName (Get-Mailbox -Filter {CustomAttribute1 -eq "C-level"})
     
        Runs all Get-HawkUser* cmdlets against all users who have "C-Level" set in CustomAttribute1.
     
    .NOTES
        Ensure the Hawk global object is initialized with a valid logging file path before running this function.
    #>

        [CmdletBinding(SupportsShouldProcess = $true)]
        param (
            [Parameter(Mandatory = $true)]
            [array]$UserPrincipalName
        )

        Write-HawkBanner
    
        # Check if the logging filepath is set
        if ([string]::IsNullOrEmpty($Hawk.FilePath)) {
            Initialize-HawkGlobalObject
        }
    
        if ($PSCmdlet.ShouldProcess("Investigating Users")) {
            Out-LogFile "Investigating Users" -Action
            Send-AIEvent -Event "CmdRun"
    
            # Pull the tenant configuration
            Get-HawkTenantConfiguration
    
            # Verify the UPN input
            [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName
    
            foreach ($Object in $UserArray) {
                [string]$User = $Object.UserPrincipalName
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserConfiguration for $User")) {
                    Out-LogFile "Running Get-HawkUserConfiguration" -Action
                    Get-HawkUserConfiguration -User $User
                }
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserInboxRule for $User")) {
                    Out-LogFile "Running Get-HawkUserInboxRule" -Action
                    Get-HawkUserInboxRule -User $User
                }
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserEmailForwarding for $User")) {
                    Out-LogFile "Running Get-HawkUserEmailForwarding" -Action
                    Get-HawkUserEmailForwarding -User $User
                }
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserAutoReply for $User")) {
                    Out-LogFile "Running Get-HawkUserAutoReply" -Action
                    Get-HawkUserAutoReply -User $User
                }
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserAuthHistory for $User")) {
                    Out-LogFile "Running Get-HawkUserAuthHistory" -Action
                    Get-HawkUserAuthHistory -User $User -ResolveIPLocations
                }
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserMailboxAuditing for $User")) {
                    Out-LogFile "Running Get-HawkUserMailboxAuditing" -Action
                    Get-HawkUserMailboxAuditing -User $User
                }
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserAdminAudit for $User")) {
                    Out-LogFile "Running Get-HawkUserAdminAudit" -Action
                    Get-HawkUserAdminAudit -User $User
                }
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserMessageTrace for $User")) {
                    Out-LogFile "Running Get-HawkUserMessageTrace" -Action
                    Get-HawkUserMessageTrace -User $User
                }
    
                if ($PSCmdlet.ShouldProcess("Running Get-HawkUserMobileDevice for $User")) {
                    Out-LogFile "Running Get-HawkUserMobileDevice" -Action
                    Get-HawkUserMobileDevice -User $User
                }
            }
        }
    }
    

<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'Hawk' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'Hawk' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'Hawk' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

#Set-PSFConfig -Module 'Hawk' -Name 'DaysToLookBack' -Value 90 -Initialize -Validation integerpositive -Description 'How long into the past will the project look'

$handler = {
    $paramSetPSFLoggingProvider = @{
        Name           = 'logfile'
        InstanceName   = 'Hawk'
        FilePath       = Join-Path -path $args[0] -ChildPath '%date%_logs.csv'
        TimeFormat       = 'yyyy-MM-dd HH:mm:ss.fff'
        IncludeModules = 'Hawk'
        UTC               = $true
        Enabled           = $true
    }

    Set-PSFLoggingProvider @paramSetPSFLoggingProvider
}
Set-PSFConfig -Module 'Hawk' -Name "FilePath" -Value '' -Initialize -Validation string -Handler $handler -Description 'Path where the module maintains logs and exports data'

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'Hawk.ScriptBlockName' -Scriptblock {
     
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "Hawk.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name Hawk.alcohol
#>


New-PSFLicense -Product 'Hawk' -Manufacturer 'Paul Navarro' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2020-11-25") -Text @"
Copyright (c) 2023 Paul Navarro
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@

#endregion Load compiled code