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") # 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" Out-LogFile "-StartDate, -EndDate, -SessionCommand, -ResultSize, -SessionID" 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") Out-Logfile $cmd # Run the initial command $Output = $null # $Output = New-Object System.Collections.ArrayList # Setup our run variable $Run = $true # Since we have more than 1k results we need to keep returning results until we have them all while ($Run) { $Output += (Invoke-Expression $cmd) # Check for null results if so warn and stop if ($null -eq $Output) { Out-LogFile ("[WARNING] - Unified Audit log returned no results.") $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 ("[WARNING] - Returned Result count was 0") $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.") $Run = $false } # Output the current progress Out-LogFile ("Retrieved:" + $Output[-1].ResultIndex.tostring().PadRight(5, " ") + " Total: " + $Output[-1].ResultCount) } } # Convert our list to an array and return it [array]$Output = $Output return $Output } Function Get-AzureADPSPermissions { <# .SYNOPSIS Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). .DESCRIPTION ists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). .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-AzureADPSPermissions.ps1 | Export-Csv -Path "permissions.csv" -NoTypeInformation Generates a CSV report of all permissions granted to all apps. .EXAMPLE PS C:\> .\Get-AzureADPSPermissions.ps1 -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" } Get all apps which have application permissions for Directory.Read.All. .EXAMPLE PS C:\> .\Get-AzureADPSPermissions.ps1 -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId") Gets all permissions granted to all apps and includes additional properties for users and service principals. .LINK https://gist.github.com/psignoret/9d73b00b377002456b24fcb808265c23 #> [CmdletBinding()] param( [switch] $DelegatedPermissions, [switch] $ApplicationPermissions, [string[]] $UserProperties = @("DisplayName"), [string[]] $ServicePrincipalProperties = @("DisplayName"), [switch] $ShowProgress, [int] $PrecacheSize = 999 ) # Get tenant details to test that Connect-AzureAD has been called try { $tenant_details = Get-AzureADTenantDetail } catch { throw "You must call Connect-AzureAD before running this script." } Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f ` $tenant_details.ObjectId, ` ($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name) # An in-memory cache of objects by {object ID} andy by {object class, object ID} $script:ObjectByObjectId = @{} $script:ObjectByObjectClassId = @{} # Function to add an object to the cache function CacheObject ($Object) { if ($Object) { if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) { $script:ObjectByObjectClassId[$Object.ObjectType] = @{} } $script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object $script:ObjectByObjectId[$Object.ObjectId] = $Object } } # Function to retrieve an object from the cache (if it's there), or from Azure AD (if not). function GetObjectByObjectId ($ObjectId) { if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) { Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId) try { $object = Get-AzureADObjectByObjectId -ObjectId $ObjectId CacheObject -Object $object } catch { Write-Verbose "Object not found." } } return $script:ObjectByObjectId[$ObjectId] } # Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode) # or by iterating over all ServicePrincipal objects. The latter is required if there are more than # 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD. function GetOAuth2PermissionGrants ([switch]$FastMode) { if ($FastMode) { Get-AzureADOAuth2PermissionGrant -All $true } else { $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } { if ($ShowProgress) { Write-Progress -Activity "Retrieving delegated permissions..." ` -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` -PercentComplete (($i / $servicePrincipalCount) * 100) } $client = $_.Value Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId } } } $empty = @{} # Used later to avoid null checks # Get all ServicePrincipal objects and add to the cache Write-Verbose "Retrieving all ServicePrincipal objects..." Get-AzureADServicePrincipal -All $true | ForEach-Object { CacheObject -Object $_ } $servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { # Get one page of User objects and add to the cache Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize) Get-AzureADUser -Top $PrecacheSize | Where-Object { CacheObject -Object $_ } Write-Verbose "Testing for OAuth2PermissionGrants bug before querying..." $fastQueryMode = $false try { # There's a bug in Azure AD Graph which does not allow for directly listing # oauth2PermissionGrants if there are more than 999 of them. The following line will # trigger this bug (if it still exists) and throw an exception. $null = Get-AzureADOAuth2PermissionGrant -Top 999 $fastQueryMode = $true } catch { if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) { Write-Verbose ("Fast query for delegated permissions failed, using slow method...") } else { throw $_ } } # Get all existing OAuth2 permission grants, get the client, resource and scope details Write-Verbose "Retrieving OAuth2PermissionGrants..." GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object { $grant = $_ 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 properties for client and resource service principals if ($ServicePrincipalProperties.Count -gt 0) { $client = GetObjectByObjectId -ObjectId $grant.ClientId $resource = GetObjectByObjectId -ObjectId $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 properties for principal (will all be null if there's no principal) if ($UserProperties.Count -gt 0) { $principal = $empty if ($grant.PrincipalId) { $principal = GetObjectByObjectId -ObjectId $grant.PrincipalId } foreach ($propertyName in $UserProperties) { $grantDetails["Principal$propertyName"] = $principal.$propertyName } } Return New-Object PSObject -Property $grantDetails } } } } if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { # Iterate over all ServicePrincipal objects and get app permissions Write-Verbose "Retrieving AppRoleAssignments..." $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } { if ($ShowProgress) { Write-Progress -Activity "Retrieving application permissions..." ` -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` -PercentComplete (($i / $servicePrincipalCount) * 100) if ($i -eq $servicePrincipalCount){ Write-Progress -Completed -Activity "Retrieving application permissions..." ` } } $sp = $_.Value Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true ` | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object { $assignment = $_ $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id } $grantDetails = [ordered]@{ "PermissionType" = "Application" "ClientObjectId" = $assignment.PrincipalId "ResourceObjectId" = $assignment.ResourceId "Permission" = $appRole.Value } # Add properties for client and resource service principals if ($ServicePrincipalProperties.Count -gt 0) { $client = GetObjectByObjectId -ObjectId $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 ++ } } Return New-Object PSObject -Property $grantDetails } } } } <# .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) { Write-Host -ForegroundColor Green " IpStack.com now requires an API access key to gather GeoIP information from their API. Please get a Free access key from https://ipstack.com/ and provide it below. " # 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) $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 } } <# .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 #> Function Get-SimpleAdminAuditLog { 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 Import-AzureAuthenticationLogs { <# .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-AzureAuthenticationLogs 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 IAgreeToTheEula Agrees to the EULA on the command line to skip the prompt. .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 EULA If you have agreed to the EULA or not .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]$IAgreeToTheEula, [switch]$SkipUpdate, [int]$DaysToLookBack, [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 { 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 { 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 Get-Eula { if ([string]::IsNullOrEmpty($Hawk.EULA)) { Write-Information (' DISCLAIMER: 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. ') # Prompt the user to agree with EULA $title = "Disclaimer" $message = "Do you agree with the above disclaimer?" $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Logs agreement and continues use of the Hawk Functions." $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Stops execution of Hawk Functions" $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 { Write-Information "`n" Return ("Agreed " + (Get-Date)).ToString() } 1 { Write-Information "Aborting Cmdlet" Write-Error -Message "Failure to agree with EULA" -ErrorAction Stop break } } } else { Return $Hawk.EULA } } Function New-ApplicationInsight { # Initialize Application Insights client $insightkey = "b69ffd8b-4569-497c-8ee7-b71b8257390e" if ($Null -eq $Client) { Write-Information "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 $notification = New-Object -ComObject Wscript.Shell $Output =$notification.Popup("Hawk has been updated to support MGGraph due to MSONLINE deprecation. Please click OK to continue", 0, "Hawk Update", 0x00000040) 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" ### Validating EULA ### if ($IAgreeToTheEula) { # Customer has accepted the EULA on the command line [string]$Eula = ("Agreed " + (Get-Date)) } else { [string]$Eula = Get-Eula } #### 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 "`nFirst 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 Write-Information ("Calculating Start Date from current date minus " + $StartRead + " days.") [DateTime]$StartDate = ((Get-Date).AddDays(-$StartRead)).Date Write-Information ("Setting StartDate by Calculation to " + $StartDate + "`n") } 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 } Write-Information ("Setting StartDate by Date to " + $StartDate + "`n") } 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 "`nLast 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)) { Write-Information ("Setting End Date to Today") [DateTime]$EndDate = ((Get-Date).AddDays(1)).Date } else { # Calculate our startdate setting it to midnight Write-Information ("Calculating End Date from current date minus " + $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 ("Setting EndDate by Calculation to " + $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) EULA = $Eula } # 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" Out-LogFile ("*** Version " + (Get-Module Hawk).version + " ***") Out-LogFile $Hawk #### 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) $global:HawkAppData | ConvertTo-Json | Out-File -FilePath $HawkAppdataPath -Force } <# .SYNOPSIS Writes output to a log file with a time date stamp .DESCRIPTION Writes output to a log file with a time date stamp .PARAMETER string Log Message .PARAMETER action What is happening .PARAMETER notice Verbose notification .PARAMETER silentnotice Silent notification .EXAMPLE Out-LogFile Sends messages to the log file .NOTES This is will depracted soon. #> Function Out-LogFile { Param ( [string]$string, [switch]$action, [switch]$notice, [switch]$silentnotice ) 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 = $true $LogOutput = $true # Get the current date [string]$date = Get-Date -Format G # Deal with each switch and what log string it should put out and if any special output # Action indicates that we are starting to do something if ($action) { [string]$logstring = ( "[" + $date + "] - [ACTION] - " + $string) } # If notice is true the we should write this to interesting.txt as well elseif ($notice) { [string]$logstring = ( "[" + $date + "] - ## INVESTIGATE ## - " + $string) # Build the file name for Investigate stuff log [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt" $logstring | Out-File -FilePath $InvestigateFile -Append } # For silent we need to supress the screen output elseif ($silentnotice) { [string]$logstring = ( "Addtional Information: " + $string) # Build the file name for Investigate stuff log [string]$InvestigateFile = Join-Path (Split-Path $LogFile -Parent) "_Investigate.txt" $logstring | Out-File -FilePath $InvestigateFile -Append # Supress screen and normal log output $ScreenOutput = $false $LogOutput = $false } # Normal output else { [string]$logstring = ( "[" + $date + "] - " + $string) } # Write everything to our log file if ($LogOutput) { $logstring | Out-File -FilePath $LogFile -Append } # Output to the screen 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 "[ERROR] - No output type specified on object" 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) $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) $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" } 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) $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) # 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) # 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) $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) $AllObject | Format-List * | Out-File $filename -Append } # Otherwise overwrite else { Out-LogFile ("Writing Data to " + $filename) $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) # 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) $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) $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) } } <# .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 have a connection with the AzureAD Cmdlets .DESCRIPTION Test if we have a connection with the AzureAD Cmdlets .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES General notes #> Function Test-AzureADConnection { $TestModule = Get-Module AzureAD -ListAvailable -ErrorAction SilentlyContinue $MinimumVersion = New-Object -TypeName Version -ArgumentList "2.0.2.140" if ($null -eq $TestModule) { Out-LogFile "Please Install the AzureAD Module with the following command:" Out-LogFile "Install-Module AzureAD" break } # Since we are not null pull the highest version else { $TestModuleVersion = ($TestModule | Sort-Object -Property Version -Descending)[0].version } # Test the version we need at least 2.0.2.140 if ($TestModuleVersion -lt $MinimumVersion) { Out-LogFile ("AzureAD Module Installed Version: " + $TestModuleVersion) Out-LogFile ("Miniumum Required Version: " + $MinimumVersion) Out-LogFile "Please update the module with: Update-Module AzureAD" break } # Do nothing else { } try { $Null = Get-AzureADTenantDetail -ErrorAction Stop } catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException] { #Out-LogFile "Please connect to AzureAD prior to running this cmdlet" Out-LogFile "Connecting-AzureAD" Connect-AzureAD } } <# .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" Out-LogFile "Connecting to EXO using Exchange Online Module" 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 { # Get tenant details to test that Connect-MgGraph has been called try { $null = Get-MgOrganization -ErrorAction stop } catch { # Write to the screen if we don't have a log file path yet if ([string]::IsNullOrEmpty($Hawk.Logfile)) { Write-Output "Connecting to MGGraph using MGGraph Module" } # Otherwise output to the log file else { Out-LogFile "Connecting to MGGraph using MGGraph Module" } # Connect to the MG Graph. The following scopes allow to retrieve Domain, Organization, and Sku data from the Graph. Connect-MGGraph -Scopes "User.Read.All", "Directory.Read.All" Select-MgProfile -Name "v1.0" } }#End Function Test-GraphConnection <# .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" # 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) [Reflection.Assembly]::LoadFile($dll) if ($Error.Count -gt 0) { Out-Logfile "[WARNING] - DLL Failed to load can't process IPs" 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 "[WARNING] - Unable to retrieve JSON file" 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") Out-LogFile ("Found " + $ipv4.count + " unique MSFT IPv4 address ranges") # 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" 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 } } <# .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 "[ERROR] - Unable to determine if input is a UserPrincipalName" Out-LogFile "Please provide a UPN or array of objects with propertly UserPrincipalName populated" 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 "[ERROR] - Unable to determine if input is a UserPrincipalName" Out-LogFile "Please provide a UPN or array of objects with propertly UserPrincipalName populated" Write-Error "Unable to determine if input is a User Principal Name" -ErrorAction Stop } } <# .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-HawkTenantAppAndSPNCredentialDetails { <# .SYNOPSIS Tenant Azure Active Directory Applications and Service Principal Credential details export. Must be connected to Azure-AD using the Connect-AzureAD cmdlet .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 certificat or password used to access corporate data, then knowing the key creation time will intrumental to determing the time frame of when an attacker had access to data. .EXAMPLE Get-HawkTenantAppAndSPNCredentialDetails Gets all Tenant Application and Service Principal Details .OUTPUTS SPNCertsAndSecrets.csv ApplicationCertsAndSecrets .LINK https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureadapplicationkeycredential?view=azureadps-2.0 .NOTES #> BEGIN{ #Initializing Hawk Object if not present if ([string]::IsNullOrEmpty($Hawk.FilePath)) { Initialize-HawkGlobalObject } Test-AzureADConnection Out-LogFile "Collecting Azure AD Service Principals" $spns = Get-MgServicePrincipal -all | Sort-Object -Property DisplayName Out-LogFile "Collecting Azure AD Registered Applications" $apps = Get-MgApplication -all $true | Sort-Object -Property DisplayName } PROCESS{ Out-LogFile "Exporting Service Principal Certificate and Password details" foreach ($spn in $spns) { $keys = $spn.keycredentials foreach ($key in $keys){ $newapp = [PSCustomObject]@{ AppName = $spn.DisplayName AppObjectID = $spn.ObjectID KeyID = $key.KeyID StartDate = $key.startdate EndDate = $key.endDate KeyType = $Key.Type CredType = "X509Certificate" } $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -json -append } } foreach ($spn in $spns) { $passwords = $spn.PasswordCredentials foreach ($pass in $passwords){ $newapp = [PSCustomObject]@{ AppName = $spn.DisplayName AppObjectID = $spn.ObjectID KeyID = $pass.KeyID StartDate = $pass.startdate EndDate = $pass.endDate KeyType = $null CredType = "PasswordSecret" } $newapp | Out-MultipleFileType -FilePrefix "SPNCertsAndSecrets" -csv -json -append } } Out-LogFile "Exporting Registered Applications Certificate and Password details" foreach ($app in $apps) { $keys = $app.keycredentials foreach ($key in $keys){ $newapp = [PSCustomObject]@{ AppName = $app.DisplayName AppObjectID = $app.ObjectID KeyID = $key.KeyID StartDate = $key.startdate EndDate = $key.endDate KeyType = $Key.Type CredType = "X509Certificate" } $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -json -append } } foreach ($app in $apps) { $passwords = $app.PasswordCredentials foreach ($pass in $passwords){ $newapp = [PSCustomObject]@{ AppName = $app.DisplayName AppObjectID = $app.ObjectID KeyID = $pass.KeyID StartDate = $pass.startdate EndDate = $pass.endDate KeyType = $pass.Type CredType = "PasswordSecret" } $newapp | Out-MultipleFileType -FilePrefix "ApplicationCertsAndSecrets" -csv -json -append } } }#End Process END{ Out-Logfile "Completed exporting Azure AD Service Principal and App Registration Certificate and Password Details" } #End End }#End Function 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" } 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" } } 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 "[ERROR] - Start date is over 90 days in the past" 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) } 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-HawkTenantAZAdmins{ <# .SYNOPSIS Tenant Azure Active Directory Administrator export. Must be connected to Azure-AD using the Connect-AzureAD cmdlet .DESCRIPTION Tenant Azure Active Directory 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-HawkTenantAZAdmins Gets all Azure AD Admins .OUTPUTS AzureADAdministrators.csv .LINK https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureaddirectoryrolemember?view=azureadps-2.0 .NOTES #> BEGIN{ #Initializing Hawk Object if not present if ([string]::IsNullOrEmpty($Hawk.FilePath)) { Initialize-HawkGlobalObject } Out-LogFile "Gathering Azure AD Administrators" Test-AzureADConnection } PROCESS{ $roles = foreach ($role in Get-MgDirectoryRole){ $admins = (Get-MGDirectoryRoleMember -DirectoryRoleId $role.id) if ([string]::IsNullOrWhiteSpace($admins)) { [PSCustomObject]@{ AdminGroupName = $role.DisplayName Members = "No Members" } } foreach ($admin in $admins){ if($admin.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.user"){ [PSCustomObject]@{ AdminGroupName = $role.DisplayName Members = $admin.AdditionalProperties.userPrincipalName } } else{ [PSCustomObject]@{ AdminGroupName = $role.DisplayName Members = $admin.AdditionalProperties.displayName } } } } $roles | Out-MultipleFileType -FilePrefix "AzureADAdministrators" -csv -json } END{ Out-LogFile "Completed exporting Azure AD Admins" } }#End Function Function Get-HawkTenantAzureADUsers{ <# .SYNOPSIS This function will export all the Azure Active Directory users. .DESCRIPTION This function will export all the Azure Active Directory users to .csv file. This data can be used as a reference for user presence and details about the user for additional context at a later time. This is a point in time users enumeration. Date created can be of help when determining account creation date. .EXAMPLE PS C:\>Get-HawkTenantAzureADUsers Exports all Azure AD users to .csv .OUTPUTS AzureADUPNS.csv .LINK https://docs.microsoft.com/en-us/powershell/module/azuread/get-azureaduser?view=azureadps-2.0 .NOTES #> BEGIN{ #Initializing Hawk Object if not present if ([string]::IsNullOrEmpty($Hawk.FilePath)) { Initialize-HawkGlobalObject } Out-LogFile "Gathering Azure AD Users" Test-AzureADConnection }#End BEGIN PROCESS{ $users = foreach ($user in (Get-MGUser -All $True)){ $userproperties = $user | Select-Object userprincipalname, id, usertype, CreatedDateTime, AccountEnabled foreach ($properties in $userproperties){ [PSCustomObject]@{ UserPrincipalname = $userproperties.userprincipalname ObjectID = $userproperties.id UserType = $userproperties.UserType DateCreated = $userproperties.createdDateTime AccountEnabled = $userproperties.AccountEnabled } } } $users | Sort-Object -property UserPrincipalname | Out-MultipleFileType -FilePrefix "AzureADUsers" -csv -json }#End PROCESS END{ Out-Logfile "Completed exporting Azure AD users" }#End END }#End Function 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-HawkTenantRBACChanges 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" # 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." } # 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 "Admin Audit Log" Get-AdminAuditLogConfig | Out-MultipleFileType -FilePrefix "AdminAuditLogConfig" -txt -xml Out-LogFile "Organization Configuration" Get-OrganizationConfig| Out-MultipleFileType -FilePrefix "OrgConfig" -xml -txt Out-LogFile "Remote Domains" Get-RemoteDomain | Out-MultipleFileType -FilePrefix "RemoteDomain" -xml -csv -json Out-LogFile "Transport Rules" Get-TransportRule | Out-MultipleFileType -FilePrefix "TransportRules" -xml -csv -json Out-LogFile "Transport Configuration" Get-TransportConfig | Out-MultipleFileType -FilePrefix "TransportConfig" -xml -csv -json } Function Get-HawkTenantConsentGrants { <# .SYNOPSIS Gathers application grants .DESCRIPTION Used the script from https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants to gather information about application and delegate grants. Attempts to detect high risk grants for review. .OUTPUTS File: Consent_Grants.csv Path: \Tenant Description: Output of all consent grants .EXAMPLE Get-HawkTenantConsentGrants Gathers Grants #> Out-LogFile "Gathering Oauth / Application Grants" Test-AzureADConnection Send-AIEvent -Event "CmdRun" # Gather the grants # Using the script from the article https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants [array]$Grants = Get-AzureADPSPermissions -ShowProgress [bool]$flag = $false # Search the Grants for the listed bad grants that we can detect if ($Grants.consenttype -contains 'AllPrinciples') { Out-LogFile "Found at least one `'AllPrinciples`' 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' -notice Out-LogFile 'https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants#inventory-apps-with-access-in-your-organization' -notice } else { Out-LogFile "To review this data follow:" Out-LogFile "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants#inventory-apps-with-access-in-your-organization" } $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." } # 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" } }#End Function Get-HawkTenantDomainActivity Function Get-HawkTenantEDiscoveryConfiguration { <# .SYNOPSIS Looks for users that have e-discovery rights. Find any roles that have access to key edisocovery cmdlets and output the users who have those rights .DESCRIPTION Searches for all roles that have e-discovery cmdlets. Searches for all users / groups that have access to those roles. .OUTPUTS File: EDiscoveryRoles.csv Path: \ Description: All roles that have access to the New-MailboxSearch and Search-Mailbox cmdlets File: EDiscoveryRoles.xml Path: \XML Description: All roles that have access to the New-MailboxSearch and Search-Mailbox cmdlets as CLI XML File: EDiscoveryRoleAssignments.csv Path: \ Description: All users that are assigned one of the discovered roles File: EDiscoveryRoleAssignments.xml Path: \XML Description: All users that are assigned one of the discovered roles as CLI XML .EXAMPLE Get-HawkTenantEDiscoveryConfiguration Runs the cmdlet against the current logged in tenant and outputs ediscovery information #> Test-EXOConnection Send-AIEvent -Event "CmdRun" Out-LogFile "Gathering Tenant information about E-Discovery Configuration" -action # Nulling our our role arrays [array]$Roles = $null [array]$RoleAssignements = $null # Look for E-Discovery Roles and who they might be assigned to $EDiscoveryCmdlets = "New-MailboxSearch", "Search-Mailbox" # Find any roles that have these critical ediscovery cmdlets in them # Bad actors with sufficient rights could have created new roles so we search for them Foreach ($cmdlet in $EDiscoveryCmdlets) { [array]$Roles = $Roles + (Get-ManagementRoleEntry ("*\" + $cmdlet)) } # Select just the unique entries based on role name $UniqueRoles = Select-UniqueObject -ObjectArray $Roles -Property Role Out-LogFile ("Found " + $UniqueRoles.count + " Roles with E-Discovery Rights") $UniqueRoles | Out-MultipleFileType -FilePrefix "EDiscoveryRoles" -csv -xml -json # Get everyone who is assigned one of these roles Foreach ($Role in $UniqueRoles) { [array]$RoleAssignements = $RoleAssignements + (Get-ManagementRoleAssignment -Role $Role.role -Delegating $false) } Out-LogFile ("Found " + $RoleAssignements.count + " Role Assignements for these Roles") $RoleAssignements | Out-MultipleFileType -FilePreFix "EDiscoveryRoleAssignments" -csv -xml -json } 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" } # 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-HawkTenantEXOAdmins{ <# .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" 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" } }#End Function Function Get-HawkTenantInboxRules { <# .SYNOPSIS Gets inbox rules and forwarding directly from all mailboxes in the org. .DESCRIPTION Uses Start-RobustCloudCommand to gather data from each mailbox in the org. Gathers inbox rules with Get-HawkUserInboxRule Gathers forwarding with Get-HawkUserEmailForwarding .PARAMETER CSVPath Path to a CSV file with a list of users to run against. CSV header should have DisplayName,PrimarySMTPAddress at minimum .PARAMETER UserPrincipalName The UPN of the user that will authenticate against Exchange Online. .OUTPUTS See Help for Get-HawkUserInboxRule for inbox rule output See Help for Get-HawkUserEmailForwarding for email forwarding output File: Robust.log Path: \ Description: Logfile for Start-RobustCloudCommand .EXAMPLE Start-HawkTenantInboxRules -UserPrincipalName userx@tenantdomain.onmicrosoft.com Runs Get-HawkUserInboxRule and Get-HawkUserEmailForwarding against all mailboxes in the org. The UserPrincipalName is the Admin/User who is running the cmdlet. .EXAMPLE Start-HawkTenantInboxRules -csvpath c:\temp\myusers.csv Runs Get-HawkUserInboxRule and Get-HawkUserEmailForwarding against all mailboxes listed in myusers.csv.The UserPrincipalName is the Admin/User who is running the cmdlet. .LINK https://gallery.technet.microsoft.com/office/Start-RobustCloudCommand-69fb349e #> 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" } 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" # 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") # 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()}" # 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 =====" Out-LogFile $cmd Invoke-Expression $cmd Out-LogFile "Process Complete" } 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'" }#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" }#End End }#End Function # Search for any changes made to RBAC in the search window and report them Function Get-HawkTenantRBACChanges { <# .SYNOPSIS Looks for any changes made to Roles Based Access Control .DESCRIPTION Searches the EXO Audit logs for the following commands being run. New-ManagementRole Remove-ManagementRole New-ManagementRoleAssignment Remove-ManagementRoleAssignment Set-MangementRoleAssignment New-ManagementScope Remove-ManagementScope Set-ManagementScope .OUTPUTS File: Simple_RBAC_Changes.csv Path: \ Description: All RBAC cmdlets that were run in an easy to read format File: RBAC_Changes.csv Path: \ Description: All RBAC changes in Raw format File: RBAC_Changes.xml Path: \XML Description: All RBAC changes as a CLI XML .EXAMPLE Get-HawkTenantRBACChanges Looks for all RBAC changes in the tenant within the search window #> Test-EXOConnection Send-AIEvent -Event "CmdRun" Out-LogFile "Gathering any changes to RBAC configuration" -action # Search EXO audit logs for any RBAC changes [array]$RBACChanges = Search-AdminAuditLog -Cmdlets New-ManagementRole, New-ManagementRoleAssignment, New-ManagementScope, Remove-ManagementRole, Remove-ManagementRoleAssignment, Set-MangementRoleAssignment, Remove-ManagementScope, Set-ManagementScope -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate # If there are any results push them to an output file if ($RBACChanges.Count -gt 0) { Out-LogFile ("Found " + $RBACChanges.Count + " Changes made to Roles Based Access Control") $RBACChanges | Get-SimpleAdminAuditLog | Out-MultipleFileType -FilePrefix "Simple_RBAC_Changes" -csv -json $RBACChanges | Out-MultipleFileType -FilePrefix "RBAC_Changes" -csv -xml -json } # Otherwise report no results found else { Out-LogFile "No RBAC Changes found." } } 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." 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) } # 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" ) $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") $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 Search-HawkTenantEXOAuditLog { <# .SYNOPSIS Searches the admin audit logs for possible bad actor activities .DESCRIPTION Searches the Exchange admin audkit logs for a number of possible bad actor activies. * New inbox rules * Changes to user forwarding configurations * Changes to user mailbox permissions * Granting of impersonation rights .OUTPUTS File: Simple_New_InboxRule.csv Path: \ Description: cmdlets to create any new inbox rules in a simple to read format File: New_InboxRules.xml Path: \XML Description: Search results for any new inbox rules in CLI XML format File: _Investigate_Simple_New_InboxRule.csv Path: \ Description: cmdlets to create inbox rules that forward or delete email in a simple format File: _Investigate_New_InboxRules.xml Path: \XML Description: Search results for newly created inbox rules that forward or delete email in CLI XML File: _Investigate_New_InboxRules.txt Path: \ Description: Search results of newly created inbox rules that forward or delete email File: Simple_Forwarding_Changes.csv Path: \ Description: cmdlets that change forwarding settings in a simple to read format File: Forwarding_Changes.xml Path: \XML Description: Search results for cmdlets that change forwarding settings in CLI XML File: Forwarding_Recipients.csv Path: \ Description: List of unique Email addresses that were setup to recieve email via forwarding File: Simple_Mailbox_Permissions.csv Path: \ Description: Cmdlets that add permissions to users in a simple to read format File: Mailbox_Permissions.xml Path: \XML Description: Search results for cmdlets that change permissions in CLI XML File: _Investigate_Impersonation_Roles.csv Path: \ Description: List all users with impersonation rights if we find more than the default of one File: _Investigate_Impersonation_Roles.csv Path: \XML Description: List all users with impersonation rights if we find more than the default of one as CLI XML File: Impersonation_Rights.csv Path: \ Description: List all users with impersonation rights if we only find the default one File: Impersonation_Rights.csv Path: \XML Description: List all users with impersonation rights if we only find the default one as CLI XML .EXAMPLE Search-HawkTenantEXOAuditLog Searches the tenant audit logs looking for changes that could have been made in the tenant. #> Test-EXOConnection Send-AIEvent -Event "CmdRun" Out-LogFile "Searching EXO Audit Logs" -Action Out-LogFile "Searching Entire Admin Audit Log for Specific cmdlets" #Make sure our values are null $TenantInboxRules = $Null $TenantSetInboxRules = $Null $TenantRemoveInboxRules = $Null # Search for the creation of ANY inbox rules Out-LogFile "Searching for ALL Inbox Rules Created in the Shell" -action [array]$TenantInboxRules = Search-UnifiedAuditLog -RecordType ExchangeAdmin -Operations New-InboxRule -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate # If we found anything report it and log it if ($TenantInboxRules.count -gt 0) { Out-LogFile ("Found " + $TenantInboxRules.count + " Inbox Rule(s) created from PowerShell") $TenantInboxRules | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "Simple_New_InboxRule" -csv -json $TenantInboxRules | Out-MultipleFileType -fileprefix "New_InboxRules" -csv -json } # Search for the Modification of ANY inbox rules Out-LogFile "Searching for ALL Inbox Rules Modified in the Shell" -action [array]$TenantSetInboxRules = Search-AdminAuditLog -Cmdlets Set-InboxRule -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate # If we found anything report it and log it if ($TenantSetInboxRules.count -gt 0) { Out-LogFile ("Found " + $TenantSetInboxRules.count + " Inbox Rule(s) created from PowerShell") $TenantSetInboxRules | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "Simple_Set_InboxRule" -csv -json $TenantSetInboxRules | Out-MultipleFileType -fileprefix "Set_InboxRules" -csv -json } # Search for the Modification of ANY inbox rules Out-LogFile "Searching for ALL Inbox Rules Removed in the Shell" -action [array]$TenantRemoveInboxRules = Search-AdminAuditLog -Cmdlets Remove-InboxRule -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate # If we found anything report it and log it if ($TenantRemoveInboxRules.count -gt 0) { Out-LogFile ("Found " + $TenantRemoveInboxRules.count + " Inbox Rule(s) created from PowerShell") $TenantRemoveInboxRules | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "Simple_Remove_InboxRule" -csv -json $TenantRemoveInboxRules | Out-MultipleFileType -fileprefix "Remove_InboxRules" -csv -json } # Searching for interesting inbox rules Out-LogFile "Searching for Interesting Inbox Rules Created in the Shell" -action [array]$InvestigateInboxRules = Search-AdminAuditLog -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate -cmdlets New-InboxRule -Parameters ForwardTo, ForwardAsAttachmentTo, RedirectTo, DeleteMessage # if we found a rule report it and output it to the _Investigate files if ($InvestigateInboxRules.count -gt 0) { Out-LogFile ("Found " + $InvestigateInboxRules.count + " Inbox Rules that should be investigated further.") -notice $InvestigateInboxRules | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "_Investigate_Simple_New_InboxRule" -csv -json -Notice $InvestigateInboxRules | Out-MultipleFileType -fileprefix "_Investigate_New_InboxRules" -xml -txt -Notice } # Look for changes to user forwarding Out-LogFile "Searching for user Forwarding Changes" -action [array]$TenantForwardingChanges = Search-AdminAuditLog -Cmdlets Set-Mailbox -Parameters ForwardingAddress, ForwardingSMTPAddress -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate if ($TenantForwardingChanges.count -gt 0) { Out-LogFile ("Found " + $TenantForwardingChanges.count + " Change(s) to user Email Forwarding") -notice $TenantForwardingChanges | Get-SimpleAdminAuditLog | Out-MultipleFileType -FilePrefix "Simple_Forwarding_Changes" -csv -json -Notice $TenantForwardingChanges | Out-MultipleFileType -FilePrefix "Forwarding_Changes" -xml -Notice # Make sure our output array is null [array]$Output = $null # Checking if addresses were added or removed # If added compile a list Foreach ($Change in $TenantForwardingChanges) { # Get the user object modified $user = ($Change.CmdletParameters | Where-Object ($_.name -eq "Identity")).value # Check the ForwardingSMTPAddresses first if ([string]::IsNullOrEmpty(($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingSMTPAddress" }).value)) { } # If not null then push the email address into $output else { [array]$Output = $Output + ($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingSMTPAddress" }) | Select-Object -Property @{Name = "UserModified"; Expression = { $user } }, @{Name = "TargetSMTPAddress"; Expression = { $_.value.split(":")[1] } } } # Check ForwardingAddress if ([string]::IsNullOrEmpty(($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingAddress" }).value)) { } else { # Here we get back a recipient object in EXO not an SMTP address # So we need to go track down the recipient object $recipient = Get-EXORecipient (($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingAddress" }).value) -ErrorAction SilentlyContinue # If we can't resolve the recipient we need to log that if ($null -eq $recipient) { Out-LogFile ("Unable to resolve forwarding Target Recipient " + ($Change.CmdletParameters | Where-Object { $_.name -eq "ForwardingAddress" })) -notice } # If we can resolve it then we need to push the address the mail was being set to into $output else { # Determine the type of recipient and handle as needed to get out the SMTP address Switch ($recipient.RecipientType) { # For mailcontact we needed the external email address MailContact { [array]$Output += $recipient | Select-Object -Property @{Name = "UserModified"; Expression = { $user } }; @{Name = "TargetSMTPAddress"; Expression = { $_.ExternalEmailAddress.split(":")[1] } } } # For all others I believe primary will work Default { [array]$Output += $recipient | Select-Object -Property @{Name = "UserModified"; Expression = { $user } }; @{Name = "TargetSMTPAddress"; Expression = { $_.PrimarySmtpAddress } } } } } } } # Output our email address user modified pairs Out-logfile ("Found " + $Output.count + " email addresses set to be forwarded mail") -notice $Output | Out-MultipleFileType -FilePrefix "Forwarding_Recipients" -csv -json -Notice } # Look for changes to mailbox permissions Out-LogFile "Searching for Mailbox Permissions Changes" -Action [array]$TenantMailboxPermissionChanges = Search-AdminAuditLog -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate -cmdlets Add-MailboxPermission if ($TenantMailboxPermissionChanges.count -gt 0) { Out-LogFile ("Found " + $TenantMailboxPermissionChanges.count + " changes to mailbox permissions") $TenantMailboxPermissionChanges | Get-SimpleAdminAuditLog | Out-MultipleFileType -fileprefix "Simple_Mailbox_Permissions" -csv -json $TenantMailboxPermissionChanges | Out-MultipleFileType -fileprefix "Mailbox_Permissions" -xml ## TODO: Possibly check who was added with permissions and see how old their accounts are } # Look for change to impersonation access Out-LogFile "Searching Impersonation Access" -action [array]$TenantImpersonatingRoles = Get-ManagementRoleEntry "*\Impersonate-ExchangeUser" if ($TenantImpersonatingRoles.count -gt 1) { Out-LogFile ("Found " + $TenantImpersonatingRoles.count + " Impersonation Roles. Default is 1") -notice $TenantImpersonatingRoles | Out-MultipleFileType -fileprefix "_Investigate_Impersonation_Roles" -csv -json -xml -Notice } elseif ($TenantImpersonatingRoles.count -eq 0) { } else { $TenantImpersonatingRoles | Out-MultipleFileType -fileprefix "Impersonation_Roles" -csv -json -xml } $Output = $null # Search all impersonation roles for users that have access foreach ($Role in $TenantImpersonatingRoles) { [array]$Output += Get-ManagementRoleAssignment -Role $Role.role -GetEffectiveUsers -Delegating:$false } if ($Output.count -gt 1) { Out-LogFile ("Found " + $Output.count + " Users/Groups with Impersonation rights. Default is 1") -notice $Output | Out-MultipleFileType -fileprefix "Impersonation_Rights" -csv -json -xml $Output | Out-MultipleFileType -fileprefix "_Investigate_Impersonation_Rights" -csv -json -xml -Notice } elseif ($Output.count -eq 1) { Out-LogFile ("Found default number of Impersonation users") $Output | Out-MultipleFileType -fileprefix "Impersonation_Rights" -csv -json -xml } else { } } 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, JSON, and XML 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() 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("Exchange Audit Log", "Search audit logs")) { Out-LogFile "Running Search-HawkTenantEXOAuditLog" -action Search-HawkTenantEXOAuditLog } if ($PSCmdlet.ShouldProcess("EDiscovery Logs", "Get eDiscovery logs")) { Out-LogFile "Running Get-HawkTenantEDiscoveryLogs" -action Get-HawkTenantEDiscoveryLogs } 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-HawkTenantRBACChanges" -action Get-HawkTenantRBACChanges } 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-HawkTenantEXOAdmins" -action Get-HawkTenantEXOAdmins } if ($PSCmdlet.ShouldProcess("Consent Grants", "Get consent grants")) { Out-LogFile "Running Get-HawkTenantConsentGrants" -action Get-HawkTenantConsentGrants } if ($PSCmdlet.ShouldProcess("Azure Admins", "Get Azure admin list")) { Out-LogFile "Running Get-HawkTenantAZAdmins" -action Get-HawkTenantAZAdmins } if ($PSCmdlet.ShouldProcess("App and SPN Credentials", "Get credential details")) { Out-LogFile "Running Get-HawkTenantAppAndSPNCredentialDetails" -action Get-HawkTenantAppAndSPNCredentialDetails } if ($PSCmdlet.ShouldProcess("Azure AD Users", "Get Azure AD user list")) { Out-LogFile "Running Get-HawkTenantAzureADUsers" -action Get-HawkTenantAzureADUsers } } Function Get-HawkUserAdminAudit { <# .SYNOPSIS Searches the EXO Audit logs for any commands that were run against the provided user object. .DESCRIPTION Searches the EXO Audit logs for any commands that were run against the provided user object. Limited by the provided search period. .PARAMETER UserPrincipalName UserPrincipalName of the user you're investigating .OUTPUTS File: Simple_User_Changes.csv Path: \<user> Description: All cmdlets that were run against the user in a simple format. .EXAMPLE Get-HawkUserAdminAudit -UserPrincipalName user@company.com Gets all changes made to user@company.com and ouputs them to the csv and xml files. #> 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 # Get all changes to this user from the admin audit logs [array]$UserChanges = Search-AdminAuditLog -ObjectIDs $MailboxName -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate # If there are any results push them to an output file if ($UserChanges.Count -gt 0) { Out-LogFile ("Found " + $UserChanges.Count + " changes made to this user") $UserChanges | Get-SimpleAdminAuditLog | Out-MultipleFileType -FilePrefix "Simple_User_Changes" -csv -json -user $User $UserChanges | Out-MultipleFileType -FilePrefix "User_Changes" -csv -json -user $User } # Otherwise report no results found else { Out-LogFile "No User Changes found." } } } 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) $UserLogonLogs += Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -UserIds " + $User + " -RecordType " + $Type) } # Make sure we have results if ($null -eq $UserLogonLogs) { Out-LogFile "[ERROR] - No results found when searching UAL for AzureActiveDirectoryAccountLogon events" } else { # Expand out the AuditData and convert from JSON Out-LogFile "Converting AuditData" $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 -le 0){} else { Out-LogFile ("[ERROR] - " + $FailedConversions.Count + " Entries failed JSON Conversion") $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" } # Convert to human readable and export Out-LogFile "Converting to Human Readable" (Import-AzureAuthenticationLogs -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." } # 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" $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" 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" } # 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 thru each object recieved 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 # If it is write a warning and skip the rest of the script [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 then ask for creds and push them into the global scope if ($null -eq $EWSCredential) { Out-LogFile "Please provide credentials that have impersonation rights to the mailbox you are looking to check" $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" } 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-Host ("Setting up connection for " + $emailaddress) -ForegroundColor Green $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); # If we have the global URL for EWS then we just use it since it should all be the same in this case # Otherwise we need to get it via autodiscover if ($null -eq $EWSUrl) { $exchService.AutodiscoverUrl($emailAddress, { $true }) $exchService.url | Set-Variable -name EWSUrl -Scope Global } else { $exchService.url = $EWSUrl } # Set the connection up for impersonation so that we log into the mailbox we want not the one we have creds for $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) # Using the exchService object connect and retrieve all inbox rules # This DID NOT work since it didn't pull back the hidden rule # $rules = $exchService.GetInboxRules($EmailAddress) # Bind to the inbox folder try { $inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox) } catch { # If we don't have rights to impersonate throw in the log file and provide a better error if ($_.Exception.innerexception -like "*permission to impersonate*") { Out-LogFile ("[ERROR] - Account does not have Impersonation Rights on Mailbox: " + $EmailAddress) Out-LogFile "https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-configure-impersonation" Write-Error $_ -ErrorAction Stop } # If it isn't an impersonation error throw it and stop else { Write-Error $_ -ErrorAction Stop } } # Setup the search $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) # Null our return arry and populate it [array]$ruleArray = $null $ruleResults | ForEach-Object { [array]$ruleArray += $_ } # Set our found flag to false $FoundHidden = $false # Check each rule Foreach ($rule in $ruleArray) { # If either Rule Name or Rule Provider are null then we need to flag it and return the priority of the rule 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 } } # If the flag wasn't set then we need to log that we didn't find any hidden rules for th euser if ($FoundHidden -eq $false) { Out-LogFile ("No Hidden rules found for mailbox: " + $EmailAddress) } # return $ruleArray } } # 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" } 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" } 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 Check if mailbox auditing is enabled for the user. If it is pulls the mailbox audit logs from the time period specified for the investigation. Will pull from the Unified Audit Log and the Mailbox Audit Log .PARAMETER UserPrincipalName Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs. .OUTPUTS File: Exchange_UAL_Audit.csv Path: \<User> Description: All Exchange related audit events found in the Unified Audit Log. File: Exchange_Mailbox_Audit.csv Path: \<User> Description: All Exchange related audit events found in the Mailbox Audit Log. .EXAMPLE Get-HawkUserMailboxAuditing -UserPrincipalName user@contoso.com Search for all Mailbox Audit logs from user@contoso.com .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 #> param ( [Parameter(Mandatory = $true)] [array]$UserPrincipalName ) Function Get-MailboxAuditLogsFiveDaysAtATime { param( [Parameter(Mandatory = $true)] [datetime]$StartDate, [Parameter(Mandatory = $true)] [datetime]$EndDate, [Parameter(Mandatory = $true)] $User ) # Setup the initial start date [datetime]$RangeStart = $StartDate do { # Get the end of the Range we are going to gather data for [datetime] $RangeEnd = ($RangeStart.AddDays(5)) # Do the actual search Out-LogFile ("Searching Range " + [string]$RangeStart + " To " + [string]$RangeEnd) [array]$Results += Search-MailboxAuditLog -StartDate $RangeStart -EndDate $RangeEnd -identity $User -ShowDetails -ResultSize 250000 # Set the RangeStart = to the RangeEnd so we do the next range $RangeStart = $RangeEnd } # While the start range is less than the end date we need to keep pulling in 5 day increments while ($RangeStart -le $EndDate) # Return the results object Return $Results } ### MAIN ### 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 " + $User) -action # Test if mailbox auditing is enabled $mbx = Get-Mailbox -identity $User if ($mbx.AuditEnabled -eq $true) { # if enabled pull the mailbox auditing from the unified audit logs Out-LogFile "Mailbox Auditing is enabled." Out-LogFile "Searching Unified Audit Log for Exchange Related Events" $UnifiedAuditLogs = Get-AllUnifiedAuditLogEntry -UnifiedSearch ("Search-UnifiedAuditLog -UserIDs " + $User + " -RecordType ExchangeItem") | select-object -Expandproperty AuditData | convertfrom-json Out-LogFile ("Found " + $UnifiedAuditLogs.Count + " Exchange audit records.") # Output the data we found $UnifiedAuditLogs | Out-MultipleFileType -FilePrefix "Exchange_UAL_Audit" -User $User -csv -json # Search the MailboxAuditLogs as well since they may have different/more information Out-LogFile "Searching Exchange Mailbox Audit Logs (this can take some time)" $MailboxAuditLogs = Get-MailboxAuditLogsFiveDaysAtATime -StartDate $Hawk.StartDate -EndDate $Hawk.EndDate -User $User Out-LogFile ("Found " + $MailboxAuditLogs.Count + " Exchange Mailbox audit records.") # Output the data we found $MailboxAuditLogs | Out-MultipleFileType -FilePrefix "Exchange_Mailbox_Audit" -User $User -csv -json } # If auditing is not enabled log it and move on else { Out-LogFile ("Auditing not enabled for " + $User) } } } 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 ("[ERROR] - Failed to find Primary SMTP Address for user: " + $User) 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) [array]$MobileDevices = Get-MobileDevice -mailbox $User if ($Null -eq $MobileDevices) { Out-Logfile ("No devices found for user: " + $User) } else { Out-Logfile ("Found " + $MobileDevices.count + " Devices") # 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 Email 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 Start-HawkUserPWNCheck -Email user@company.com Returns the pwn state of the email address provided #> param([array]$Email) # if there is no value of hibpkey then we need to get it from the user 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.50 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" } # Verify our UPN input [array]$UserArray = Test-UserObject -ToTest $Email $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() try { $Result = Invoke-WebRequest $InvokeURL -Headers $headers -userAgent 'Hawk' -ErrorAction Stop } catch { switch ($Error[0].exception.response.statuscode) { NotFound { write-host "Email Not Found to be Pwned" return } Default { write-host "[ERROR] - Failure to retrieve pwned status" write-host $Error return } } } # Convert the result into a PS 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 Start-Sleep -Milliseconds 1500 } } # String together the hawk user functions to pull data for a single user Function Start-HawkUserInvestigation { <# .SYNOPSIS Gathers common data about a provided user. .DESCRIPTION Runs all Hawk users related cmdlets against the specified user and gathers the data. Cmdlet Information Gathered ------------------------- ------------------------- Get-HawkTenantConfigurationn 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 users logons Get-HawkUserMailboxAuditing Searches the unified audit log for mailbox auditing information Get-HawkUserAdminAudit Searches the EXO Audit logs for any commands that were run against the provided user object. Get-HawkUserMessageTrace Pulls the email sent by the user in the last 7 days. .PARAMETER UserPrincipalName Single UPN of a user, commans seperated list of UPNs, or array of objects that contain UPNs. .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 #> param ( [Parameter(Mandatory = $true)] [array]$UserPrincipalName ) #Checking to see if Logging filepath is set if ([string]::IsNullOrEmpty($Hawk.FilePath)) { Initialize-HawkGlobalObject } Out-LogFile "Investigating Users" Send-AIEvent -Event "CmdRun" # Pull the tenent configuration Get-HawkTenantConfiguration # Verify our UPN input [array]$UserArray = Test-UserObject -ToTest $UserPrincipalName foreach ($Object in $UserArray) { [string]$User = $Object.UserPrincipalName Out-LogFile "Running Get-HawkUserConfiguration" -action Get-HawkUserConfiguration -User $User Out-LogFile "Running Get-HawkUserInboxRule" -action Get-HawkUserInboxRule -User $User Out-LogFile "Running Get-HawkUserEmailForwarding" -action Get-HawkUserEmailForwarding -User $User Out-LogFile "Running Get-HawkUserAutoReply" -action Get-HawkUserAutoReply -User $User Out-LogFile "Running Get-HawkUserAuthHistory" -action Get-HawkUserAuthHistory -User $user -ResolveIPLocations Out-LogFile "Running Get-HawkUserMailboxAuditing" -action Get-HawkUserMailboxAuditing -User $User Out-LogFile "Running Get-HawkUserAdminAudit" -action Get-HawkUserAdminAudit -User $User Out-LogFile "Running Get-HawkUserMessageTrace" -action Get-HawkUserMessageTrace -user $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 |