hunting-functions.ps1

# read config
. $PSScriptRoot\get-config.ps1

# Main function to get all the data for the Alert Report (gundog hunting v1 functionality)
function get-alertData {
    [CmdletBinding()]
    param (
        [string]$AlertId,
        [string]$tenantId,
        [string]$clientSecret,
        [string]$clientId
    )
    if($AlertId -ne "")
    {
        try {  
            #allIncidents is empty, get all incidents from the last 30 days - don't change this to +30days, all other advanced hunting queries can only do 30days
            $Today = Get-date -Format "yyyy-MM-dd"
            $StartDateAllIncidents = (get-date($Today)).AddDays(-30)
            $StartDateAllIncidentsF = Get-Date $StartDateAllIncidents -Format "yyyy-MM-dd"
            $tempUrl="/incidents?`$filter=createdTime%20gt%20" + $StartDateAllIncidentsF
            $global:allIncidents = get-APIresultMTP -tenantId $tenantId -clientID $clientID -clientSecret $clientSecret -topic "... getting all incidents/alerts" -api $tempUrl
            foreach($inc in $allIncidents)
            {
                foreach($al in $inc.alerts)
                {
                    if($al.alertid -eq $AlertId)
                    {
                        $global:plainalert = $al 
                    }
                }
            }
        }
        catch {
            Write-Host "Error: Query for AlertId failed! This is not your day, Mando." -ForegroundColor red
            $error
        }
    }

    if($error.count -eq 0 -and $null -ne $plainalert)
    {
        #build the $alert object
        $global:alert = new-object psobject
        $alert | add-member Noteproperty Timestamp ($plainalert.creationTime.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty AlertId ($plainalert.alertId.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty Title ($plainalert.title.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty Category ($plainalert.Category.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty ServiceSource ($plainalert.ServiceSource.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty DetectionSource ($plainalert.DetectionSource.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty Entities ($plainalert.Entities.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty DeviceName ($plainalert.devices.deviceDnsName.Where({$_ -ne ""}) | Select-Object -Unique)

        #if the alert has an assigned device ID
        if($plainalert.Devices.mdatpDeviceId -ne "")
        {
            $alert | add-member Noteproperty DeviceId ($plainalert.Devices.mdatpDeviceId.tolower().Where({$_ -ne ""}) | Select-Object -Unique)
        } #if not check entities for device IDs
        else {
            if($plainalert.entities.deviceid -ne "")
            {
                $alert | add-member Noteproperty DeviceId ($plainalert.entities.deviceid.tolower().Where({$_ -ne ""}) | Select-Object -Unique)
            }
        }
        #User Object from Entities
        $alert | add-member Noteproperty AccountName ($plainalert.entities.AccountName.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty AccountDomain ($plainalert.entities.DomainName.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty AccountSid ($plainalert.entities.UserSid.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty FileName ($plainalert.entities.FileName.Where({$_ -ne ""}))
        $alert | add-member Noteproperty SHA1 ($plainalert.entities.sha1.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty SHA256 ($plainalert.entities.sha256.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty Folderpath ($plainalert.entities.filepath.Where({$_ -ne ""}))
        $alert | add-member Noteproperty Urls ($plainalert.entities.url.Where({$_ -ne ""}) | Select-Object -Unique)      
        $alert | add-member Noteproperty EmailSubject ($plainalert.entities.subject.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty EmailSender ($plainalert.entities.sender.Where({$_ -ne ""}) | Select-Object -Unique)
        $alert | add-member Noteproperty EmailDeliveryAction ($plainalert.entities.DeliveryAction.Where({$_ -ne ""}) | Select-Object -Unique)

        #Build the device object
        $global:Device = new-object psobject
        $Device | add-member Noteproperty Name ($alert.DeviceName)
        $Device | add-member Noteproperty Platform $plainalert.Devices.osPlatform
        $Device | add-member Noteproperty Build $plainalert.Devices.osBuild
        $Device | add-member Noteproperty HealthStatus $plainalert.Devices.healthStatus
        $Device | add-member Noteproperty RiskScore $plainalert.Devices.riskScore
        $Device | add-member Noteproperty FirstSeen $plainalert.Devices.firstSeen
        $Device | add-member Noteproperty MachineTags $plainalert.Devices.tags

        #explicit vars needed for advanced hunting
        $DeviceId = $alert.DeviceId
        $global:Timestamp = $alert.Timestamp
        $AccountSid = $alert.AccountSid

        #try to get more identity info via advanced hunting in IdentityInfo table via user SID
        if($null -ne $AccountSid)
        {
            $global:plainIdentity = get-huntingResultMTP -kql "IdentityInfo | where CloudSid =~ '$AccountSid' or OnPremSid =~ '$AccountSid' | take 10" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting identity info"
        }
        else { #if there is no SID, we make a REST call and check for logonusers of the device
            $tempUrl="/machines/$deviceid/logonusers"
            $global:Account=get-DefenderAPIResult -tenantId $tenantId -clientID $clientID -clientSecret $clientSecret -topic "... getting device logons" -api $tempUrl -apiGeoLocation $apiGeoLocation
            $global:AccountName = $Account.value.accountname
            try {
                if($AccountName.GetType().Name -eq "Object[]") #lets see if we have to deal with multiple logon accounts or one
                {
                    $global:AccountName = ($AccountName | Group-Object | Sort-Object count -Descending)[0].name #take the first account
                    $global:plainIdentity = get-huntingResultMTP -kql "IdentityInfo | where AccountName =~ '$AccountName' | take 10" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting identity info"
                    if($null -eq $plainIdentity)
                    {
                        $global:AccountName = ($AccountName | Group-Object | Sort-Object count -Descending)[1].name #take the second account
                        $global:plainIdentity = get-huntingResultMTP -kql "IdentityInfo | where AccountName =~ '$AccountName' | take 10" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting identity info"
                    }
                }
                else {
                    $global:plainIdentity = get-huntingResultMTP -kql "IdentityInfo | where AccountName =~ '$AccountName' | take 10" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting identity info"
                }
            }
            catch {
                $global:plainIdentity = get-huntingResultMTP -kql "IdentityInfo | where AccountName =~ '$AccountName' | take 10" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting identity info"
            }         
        }

        #now take the IdentityInfo results and build a user object
        $global:user = new-object psobject
        $user | add-member Noteproperty AccountUpn ($plainIdentity.AccountUpn.Where({$_ -ne ""}) | Select-Object -Unique)
        $user | add-member Noteproperty Department ($plainIdentity.Department.Where({$_ -ne ""}) | Select-Object -Unique)
        $user | add-member Noteproperty JobTitle ($plainIdentity.JobTitle.Where({$_ -ne ""}) | Select-Object -Unique)
        $user | add-member Noteproperty AccountName ($plainIdentity.AccountName.Where({$_ -ne ""}) | Select-Object -Unique)
        $user | add-member Noteproperty AccountDomain ($plainIdentity.AccountDomain.Where({$_ -ne ""}) | Select-Object -Unique)
        $user | add-member Noteproperty EmailAddress ($plainIdentity.EmailAddress.Where({$_ -ne ""}) | Select-Object -Unique)
        $user | add-member Noteproperty City ($plainIdentity.City.Where({$_ -ne ""}) | Select-Object -Unique)
        $user | add-member Noteproperty Country ($plainIdentity.Country.Where({$_ -ne ""}) | Select-Object -Unique)
        $user | add-member Noteproperty IsAccountEnabled ($plainIdentity.IsAccountEnabled.Where({$_ -ne ""}) | Select-Object -Unique)

        #create some explicit vars we need for advanced hunting
        $upn = $user.AccountUpn
        $emailAddress = $user.EmailAddress

        #associated hunting - main advanced hunting action starts here
        #lets see if we use sha1 or sha256 (prefer sha1 over sha256)
        if($null -ne $alert.sha1) 
        {
            $global:fileHash = $alert.sha1
        } 
        else 
        {
            if($null -ne $alert.sha256)
            {
                $global:fileHash = $alert.sha256
            }
        }
        #if we have a deviceID, hunt the: registry, network, processes and vulnerabilities (last one not via advanced hunting but API)
        if($null -ne $DeviceId -and $DeviceId -ne "")
        {
            if($registryOn) { $global:registry = get-huntingResult -kql "DeviceRegistryEvents | where DeviceId =~ '$DeviceId' | where Timestamp between (datetime_add('$registryT1u', $registryT1, datetime($Timestamp))..datetime_add('$registryT2u', $registryT2, datetime($Timestamp)))" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting registry info" -apiGeoLocation $apiGeoLocation}
            if($networkOn) { 
                $global:network = get-huntingResult -kql "DeviceNetworkEvents | where DeviceId == '$DeviceId' | where Timestamp between (datetime_add('$networkT1u',$networkT1,datetime($Timestamp))..datetime_add('$networkT2u', $networkT2,datetime($Timestamp)))" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting network info" -apiGeoLocation $apiGeoLocation
                if($numberOfEvents -le 100)
                {
                    $numberOfIps = $numberOfEvents
                }
                else {
                    $numberOfIps = 100
                }
                $body = $network.Remoteip.Where({$_ -ne ""}) | Select-Object -Unique | Select-Object -Last $numberOfIps
                $body = $body | ForEach-Object {'"' + $_ + '",'}
                $finalBody = "[" + (-join $body).trimend(",") + "]"
                $global:ipGeoInfo = get-simpleRestCall -url "http://ip-api.com/batch" -body $finalBody -method "POST" -topic "... getting geo-IP info" 
            }
            if($processesOn) { $processes = $null; $global:processes = get-huntingResult -kql "DeviceProcessEvents | where DeviceId =~ '$DeviceId' | where Timestamp between (datetime_add('$processesT1u', $processesT1,datetime($Timestamp))..datetime_add('$processesT2u', $processesT2,datetime($Timestamp)))" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting processes info" -apiGeoLocation $apiGeoLocation }

            if($vulnerabilitiesOn) { 
                $vulnUrl="/vulnerabilities/machinesVulnerabilities?`$filter=machineId eq '$deviceId'"
                $rawVulnerabilities =  get-DefenderAPIResult -tenantId $tenantId -clientID $clientID -clientSecret $clientSecret -topic "... getting all vulnerability info" -api $vulnUrl -apiGeoLocation $apiGeoLocation
                $global:vulnerabilities = $rawVulnerabilities.value
            } 
        }
        #getting fileinfo and filestats from MD API
        if($null -ne $fileHash)
        {
            if($fileHash.GetType().Name -eq "Object[]")
            {
                $filesApiInfo = [System.Collections.ArrayList]@()
                $filesApiStats = [System.Collections.ArrayList]@()
                foreach ($fh in $fileHash) {
                    $fileInfoUrl="/files/" + $fh
                    $fileStatsUrl="/files/" + $fh + "/stats?lookBackHours=48"
                    $filesApiStatsTemp = New-Object psobject
                    $filesApiInfoTemp = New-Object psobject
                    $filesApiInfoTemp = get-DefenderAPIResult -tenantId $tenantId -clientID $clientID -clientSecret $clientSecret -topic "... getting all file info" -api $fileInfoUrl -apiGeoLocation $apiGeoLocation
                    $filesApiStatsTemp = get-DefenderAPIResult -tenantId $tenantId -clientID $clientID -clientSecret $clientSecret -topic "... getting all file statistics" -api $fileStatsUrl -apiGeoLocation $apiGeoLocation
                    if($null -ne $filesApiStatsTemp -and $filesApiStatsTemp -ne "")
                    {
                        $filesApiStats.add($filesApiStatsTemp) | Out-Null
                    }
                    if($null -ne $filesApiInfoTemp -and $filesApiInfoTemp -ne "")
                    { 
                        $filesApiInfo.add($filesApiInfoTemp) | Out-Null
                    }
                }
            }
            else {
                $fileInfoUrl="/files/" + $fileHash
                $fileStatsUrl="/files/" + $fileHash + "/stats?lookBackHours=48"
                $global:filesApiInfo = get-DefenderAPIResult -tenantId $tenantId -clientID $clientID -clientSecret $clientSecret -topic "... getting all file info" -api $fileInfoUrl -apiGeoLocation $apiGeoLocation
                $global:filesApiStats = get-DefenderAPIResult -tenantId $tenantId -clientID $clientID -clientSecret $clientSecret -topic "... getting all file statistics" -api $fileStatsUrl -apiGeoLocation $apiGeoLocation
            }
        }
        #if we have a user UPN, we hunt for sign-ins to AAD, Office-files in MCAS and risky sign-ins (the last via direct api, not advanced hunting)
        if($null -ne $upn -and $upn -ne "")
        {
            if($signinsOn -and $null -eq $signins) { $global:signins = get-huntingResultMTP -kql "AADSignInEventsBeta | where AccountUpn =~ '$upn' | where Timestamp between (datetime_add('$signinsT1u', $signinsT1,datetime($Timestamp))..datetime_add('$signinsT2u', $signinsT2,datetime($Timestamp)))" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting AAD sign-in info" }
            if($officeOn) { $global:office = get-huntingResultMTP -kql "AppFileEvents | where AccountUpn =~ '$upn' | where Timestamp between (datetime_add('$officeT1u', $officeT1,datetime($Timestamp))..datetime_add('$officeT2u', $officeT2,datetime($Timestamp)))" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting Office (MCAS) info" }
            if($riskySignInsOn) {  $global:riskySignIns = get-graphResponse -graphQuery "/beta/riskDetections?`$filter=userPrincipalName eq '$upn'" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting risky sign-ins" }
        }
        #if we have a user email address, we also hunt for the last incoming and outgoing mail from and to the user mailbox
        if($null -ne $emailAddress -and $emailAddress -ne "")
        {
            if($emailsOn) { $global:emails = get-huntingResultMTP -kql "EmailEvents | where RecipientEmailAddress =~ '$emailAddress' or SenderFromAddress =~ '$emailAddress' | where Timestamp between (datetime_add('$emailsT1u', $emailsT1,datetime($Timestamp))..datetime_add('$emailsT2u', $emailsT2,datetime($Timestamp))) | join kind=leftouter EmailUrlInfo on NetworkMessageId | join kind=leftouter EmailAttachmentInfo on NetworkMessageId" -tenantId $tenantId -clientID $clientId -clientSecret $clientSecret -topic "... getting email info" }
        }
    }
    else {
        Write-Host "Error: Query for AlertId failed! This is the way, too!"  -ForegroundColor red
    }
}
# Does generic hunting via the Microsoft Defender for Endpoint API, provide KQL
function get-huntingResult {
    [CmdletBinding()]
    param (
        [string]$kql,
        [string]$clientID,
        [string]$clientSecret,
        [string]$tenantId,
        [string]$topic,
        [string]$irmTimeout,
        [string]$apiGeoLocation
    )
    $error.Clear()
    $oAuthUri = "https://login.microsoftonline.com/" + $tenantId + "/oauth2/token"
    $client_id = $clientID
    $client_secret = $clientSecret
    $authBody = [Ordered] @{
        resource =  "https://" + $apiGeoLocation 
        client_id = $client_id
        client_secret = $client_secret
        grant_type = "client_credentials"
    }
    try {
        $authResponse = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $authBody -ErrorAction Stop
    }
    catch {
        Write-Host "get-huntingResult: failed Invoke-RestMethod (Auth)"   -ForegroundColor red
        $error
    }

    $token = $authResponse.access_token
    $url = "https://" + $apiGeoLocation + "/api/advancedqueries/run" 
    $headers = @{ 
        'Content-Type' = 'application/json'
        Accept = 'application/json'
        Authorization = "Bearer $token" 
    }
    $body = ConvertTo-Json -InputObject @{ 'Query' = $kql }
    write-host $topic -ForegroundColor Green
    #Write-host $kql
    try {
        if($config.globalVars.debugon)
        {
            $webResponse = Invoke-RestMethod -Method Post -Uri $url -Headers $headers -Body $body  -verbose -debug -TimeoutSec $irmTimeout
        }
        else {
            $webResponse = Invoke-RestMethod -Method Post -Uri $url -Headers $headers -Body $body -TimeoutSec $irmTimeout
        }
    }
    catch {
        Write-Host "get-huntingResult: failed Invoke-RestMethod ($url)"      -ForegroundColor red
        Write-Host URL: $url
        $error
    }
    return $webResponse.Results
}
# NON-Advanced Hunting API access in Microsoft Defender for Endpoint
function get-DefenderAPIResult {
    [CmdletBinding()]
    param (
        [string]$api,
        [string]$clientID,
        [string]$clientSecret,
        [string]$tenantId,
        [string]$topic,
        [string]$irmTimeout,
        [string]$apiGeoLocation
    )
    $error.Clear()
    $oAuthUri = "https://login.microsoftonline.com/" + $tenantId + "/oauth2/token"
    $client_id = $clientID
    $client_secret = $clientSecret
    $authBody = [Ordered] @{
        resource =  "https://" + $apiGeoLocation 
        client_id = $client_id
        client_secret = $client_secret
        grant_type = "client_credentials"
    }
    try {
        $authResponse = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $authBody -ErrorAction Stop
    }
    catch {
        Write-Host "get-DefenderAPIResult: failed Invoke-RestMethod (Auth)"   -ForegroundColor red
        $error
    }

    $token = $authResponse.access_token
    $url = "https://" + $apiGeoLocation + "/api" + $api 
    $headers = @{ 
        'Content-Type' = 'application/json'
        Accept = 'application/json'
        Authorization = "Bearer $token" 
    }
    write-host $topic -ForegroundColor Green
    #Write-host $kql
    try {
        if($config.globalVars.debugon)
        {
            $global:webResponse = Invoke-RestMethod -Method get -Uri $url -Headers $headers -verbose -debug -TimeoutSec $irmTimeout
        }
        else {
            $global:webResponse = Invoke-RestMethod -Method get -Uri $url -Headers $headers -TimeoutSec $irmTimeout
        }
    }
    catch {
        Write-Host "get-DefenderAPIResult: failed Invoke-RestMethod ($url)"      -ForegroundColor red
        Write-Host URL: $url
        $error
    }
    return $webResponse
}
# Advanced hunting against the Microsoft 365 Defender (MTP) API
function get-huntingResultMTP {
    [CmdletBinding()]
    param (
        [string]$kql,
        [string]$clientID,
        [string]$clientSecret,
        [string]$tenantId,
        [string]$topic,
        [string]$irmTimeout
    )
    $error.Clear()
    $oAuthUri = "https://login.microsoftonline.com/" + $tenantId + "/oauth2/token"
    $client_id = $clientID
    $client_secret = $clientSecret
    $authBody = [Ordered] @{
        resource =  "https://api.security.microsoft.com" 
        client_id = $client_id
        client_secret = $client_secret
        grant_type = "client_credentials"
    }
    try {
        $authResponse = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $authBody -ErrorAction Stop
    }
    catch {
        Write-Host "get-huntingResultMTP: failed Invoke-RestMethod (Auth) --> $oAuthUri & $authBody"   -ForegroundColor red
        $error
    }
    $token = $authResponse.access_token
    $url = "https://api.security.microsoft.com/api/advancedhunting/run" 
    $headers = @{ 
        'Content-Type' = 'application/json'
        Accept = 'application/json'
        Authorization = "Bearer $token" 
    }
    $body = ConvertTo-Json -InputObject @{ 'Query' = $kql.Replace("`"","'")}
    #write-host $topic -ForegroundColor Green
    try {
        if($config.globalVars.debugon)
        {
            $webResponse = Invoke-RestMethod -Method Post -Uri $url -Headers $headers -Body $body -verbose -debug -TimeoutSec $irmTimeout #-Proxy "http://127.0.0.1:8888"
        } else {
            $webResponse = Invoke-RestMethod -Method Post -Uri $url -Headers $headers -Body $body -TimeoutSec $irmTimeout #-Proxy "http://127.0.0.1:8888"
        }
    }
    catch {
        $errorOutput = "get-huntingResultMTP: failed Invoke-RestMethod ($url) " + $error
        $errorOutput
    }
    $webResponse.Results
}
# NON-Advanced Hunting calls against Microsoft 365 Defender API, does support paging
function get-APIresultMTP {
    [CmdletBinding()]
    param (
        [string]$api,
        [string]$clientID,
        [string]$clientSecret,
        [string]$tenantId,
        [string]$irmTimeout,
        [string]$topic
    )
    $error.Clear()
    $oAuthUri = "https://login.microsoftonline.com/" + $tenantId + "/oauth2/token"
    $client_id = $clientID
    $client_secret = $clientSecret
    $authBody = [Ordered] @{
        resource =  "https://api.security.microsoft.com" 
        client_id = $client_id
        client_secret = $client_secret
        grant_type = "client_credentials"
    }
    try {
        $authResponse = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $authBody -ErrorAction Stop
    }
    catch {
        Write-Host "get-huntingResultMTP: failed Invoke-RestMethod (Auth)"   -ForegroundColor red
        $error
    }
    $token = $authResponse.access_token
    $url = "https://api.security.microsoft.com/api" +  $api
    $headers = @{ 
        'Content-Type' = 'application/json'
        Accept = 'application/json'
        Authorization = "Bearer $token" 
    }
    write-host $topic -ForegroundColor Green
    $result  =  @()
    try {

        if($config.globalVars.debugon)
        {
        $response = Invoke-RestMethod -Method Get -Uri $url -Headers $headers -verbose -debug -TimeoutSec $irmTimeout #-Proxy "http://127.0.0.1:8888"
        }
        else
        {
            $response = Invoke-RestMethod -Method Get -Uri $url -Headers $headers -TimeoutSec $irmTimeout
        }
        $result = $response.value

        while ($null -ne $response.'@odata.nextLink'){
            $nextUri = $response.'@odata.nextLink';
            if($config.globalVars.debugon)
            {
                $response  = Invoke-RestMethod -Method Get -Uri $nextUri -Headers $headers -verbose -debug -TimeoutSec $irmTimeout
            }
            else {
                $response  = Invoke-RestMethod -Method Get -Uri $nextUri -Headers $headers -TimeoutSec $irmTimeout
            }
            $result += $response.value
        }
    }
    catch {
        Write-Host "get-APIresultMTP: failed Invoke-RestMethod ($url)" -ForegroundColor red
        $error
    }
    return $result
}
# common quieries against MS Graph
function get-graphResponse{
    [CmdletBinding()]
    param (
        [string]$graphQuery,
        [string]$tenantId,
        [string]$clientid,
        [string]$clientsecret,
        [string]$topic
    )
    $error.Clear()
    $oAuthUri = "https://login.microsoftonline.com/" + $tenantId + "/oauth2/token"
    $authBody = [Ordered] @{
        resource = "https://graph.microsoft.com" 
        client_id = $clientid
        client_secret = $clientsecret
        grant_type = "client_credentials"
    }
    try {
        $authResponse = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $authBody -ErrorAction Stop
    }
    catch {
        Write-Host "get-graphResponse: failed Invoke-RestMethod (auth)" -ForegroundColor red
        write-host $oAuthUri
        $error
    }

    $token = $authResponse.access_token
    $url = "https://graph.microsoft.com$graphQuery"
    $headers = @{ 
        'Content-Type' = 'application/json'
        Accept = 'application/json'
        Authorization = "Bearer $token" 
    }
    write-host $topic -ForegroundColor green
    try {
        $webResponse = Invoke-RestMethod -Method Get -Uri $url -Headers $headers -ErrorAction Stop
        $webResponse
    }
    catch {
        Write-Host "get-graphResponse: failed Invoke-RestMethod ($url)" -ForegroundColor red
        write-host $oAuthUri
        $error
    }

}
# rest call without authentication, provide full url
function get-simpleRestCall{
    [CmdletBinding()]
    param (
        [string]$url,
        [string]$body,
        [string]$topic,
        [string]$method
    )
    $error.Clear()
    $headers = @{ 
        'Content-Type' = 'application/json'
        Accept = 'application/json'
    }
    write-host $topic -ForegroundColor green
    try {
        $webResponse = Invoke-RestMethod -Method $method -Uri $url -body $body -Headers $headers -ErrorAction Stop
        $webResponse
    }
    catch {
        Write-Host "get-simpleRestCall: failed Invoke-RestMethod ($topic)" -ForegroundColor red
        $error
    } 
}
# all alerts from specified tenant
function huntAllAlerts {
    param (
        [string]$TenantId,
        [string]$searchDurationLastXDays
    )
    # display alerts for the last X days:
    if($searchDurationLastXDays -eq "")
    {
        $searchDurationLastXDays = 30
    }
    $DurationDisplayAllAlerts = -$searchDurationLastXDays
    write-host $Tenant.name -ForegroundColor green
    $Today = Get-date -Format "yyyy-MM-dd"
    $StartDateAllAlerts = (get-date($Today)).AddDays($DurationDisplayAllAlerts)
    $StartDateAllAlertsF = Get-Date $StartDateAllAlerts -Format "yyyy-MM-dd"
    $graphQuery = "/beta/security/alerts?`$filter=createdDateTime%20gt%20" + $StartDateAllAlertsF
    $allAlertsResponse = (get-graphResponse -tenantId $TenantId -clientID $clientID -clientSecret $clientSecret -graphQuery $graphQuery -topic "... searching for all alerts").value
    $allAlertsResponse
}
# kql hunting all tenants
function allTenantAction {
    param (
        [psobject]$allTenants,
        [string]$kql
    )
    $allTenantResults = $null
    $allTenantResults = New-Object psobject 
    $singleTenantResult = $null
    foreach($tenant in $allTenants)
    {
        
        $singleTenantResult = get-huntingResultMTP -tenantId $tenant.TenantId -clientID $clientID -clientSecret $clientSecret -kql $kql 
        if($null -ne $singleTenantResult)
        {
            $tempTenant =$tenant.name
            $allTenantResults | Add-Member NoteProperty $tempTenant $singleTenantResult
        }
    }
    $allTenantResults
}
# kql hunting single tenant
function singleTenantAction {
    param (
        [psobject]$allTenants,
        [string]$tenantNumber,
        [string]$kql
    )
    $allTenantResults = $null
    $allTenantResults = New-Object psobject 
    $singleTenantResult = $null

    $countingTenants = 1
    foreach($tenant in $allTenants)
    {
        if($countingTenants -eq $tenantNumber)
        {
            $singleTenantResult = get-huntingResultMTP -tenantId $tenant.TenantId -clientID $clientID -clientSecret $clientSecret -kql $kql 
            if($null -ne $singleTenantResult)
            {
                $allTenantResults = $singleTenantResult
            }
            else {
                write-host "Not your day Boba - relax in your Bacta Tank." -ForegroundColor red
            }
        }
        $countingTenants = $countingTenants + 1
    }
    $allTenantResults
}
# kql hunting mulitple tenants
function multiTenantAction {
    param (
        [psobject]$allTenants,
        [string]$tenantNumbers,
        [string]$kql
    )
    $allTenantResults = $null
    $allTenantResults = New-Object System.Collections.ArrayList 
    $singleTenantResult = New-Object psobject 
    $hT = $tenantNumbers.Split(",")
    $countingTenants = 1
    foreach($tenant in $allTenants)
    {
        if($hT -contains $countingTenants)
        {
            $singleTenantResult = get-huntingResultMTP -tenantId $tenant.TenantId -clientID $clientID -clientSecret $clientSecret -kql $kql
            if($null -ne $singleTenantResult)
            {
                if(!$config.globalVars.debugOn)
                {
                    Clear-Host
                }
                $singleTenantResult | add-member Noteproperty Tenant $tenant.name.substring(0,3).toupper() 
                $allTenantResults.add($singleTenantResult) | Out-Null
            }
        }
        $countingTenants = $countingTenants + 1 
    }
    return $allTenantResults
}