functions/Search-FMALog.ps1

function Search-FMALog {
    <#
    .SYNOPSIS
    Starts a log search on a FortiAnalyzer instance.
 
    .DESCRIPTION
    The Start-FMALogSearch function initiates a log search task on a FortiAnalyzer instance. It allows searching logs
    based on specified criteria such as devices, log types, time range, etc.
 
    .PARAMETER Connection
    Specifies the connection to the FortiAnalyzer instance. If not specified, it uses the last connection
    to an Analyzer obtained by Get-FMLastConnection.
 
    .PARAMETER ADOM
    Specifies the administrative domain (ADOM) from which to initiate the log search task.
 
    .PARAMETER EnableException
    Indicates whether exceptions should be enabled or not. By default, exceptions are enabled.
 
    .PARAMETER Apiver
    Specifies the API version to use. Default is 3.
 
    .PARAMETER CaseSensitive
    Indicates whether the log search is case sensitive or not.
 
    .PARAMETER Device
    Specifies the device(s) to search logs on. Use TabExpansion attribute to provide completion for FortiAnalyzer devices.
 
    .PARAMETER Fields
    Which log attributes should be returned?
 
    .PARAMETER Filter
    Specifies the filter to apply when searching logs. This is a filter string equal to the usage within the analyzer GUI
 
    .PARAMETER Logtype
    Specifies the type of logs to search for. Use ValidateSet attribute to choose from available log types.
 
    .PARAMETER TimeOrder
    Specifies the order of log search results by time. Choose from 'desc' (descending) or 'asc' (ascending).
 
    .PARAMETER TimeRangeStart
    Specifies the start time of the log search range. Mandatory when using the time range.
 
    .PARAMETER TimeRangeEnd
    Specifies the end time of the log search range. Mandatory when using the time range.
 
    .PARAMETER Last
    Specifies the time span from which to search logs. Mandatory when using the time span.
 
    .PARAMETER Timezone
    Specifies the timezone for the log search.
 
    .EXAMPLE
    Search-FMALog -Device "Device1" -Logtype "traffic" -TimeRangeStart (Get-Date).AddDays(-1) -TimeRangeEnd (Get-Date)
 
    Starts a log search task for traffic logs on "Device1" within the last 24 hours.
    .EXAMPLE
    Search-FMALog -Device "Device1" -Logtype "traffic" -Last ([timeSpan]::FromHours(5))
 
    Starts a log search task for traffic logs on "Device1" within the last 5 hours.
    #>

    [CmdletBinding()]
    [OutputType([object[]])]
    param (
        [parameter(Mandatory = $false)]
        $Connection = (Get-FMLastConnection -Type Analyzer),
        [string]$ADOM,
        [bool]$EnableException = $true,
        [long]$Apiver = 3,
        [bool]$CaseSensitive=$false,
        [parameter(mandatory = $false)]
        [PSFramework.TabExpansion.PsfArgumentCompleterAttribute("FortiAnalyzer.Devices")]
        [System.Object[]]$Device,
        [string]$Filter,
        [parameter(mandatory = $true)]
        [ValidateSet('traffic', 'app-ctrl', 'attack', 'content', 'dlp', 'emailfilter', 'event', 'history', 'virus', 'voip', 'webfilter', 'netscan', 'fct-event', 'fct-traffic', 'waf', 'gtp')]
        [string]$Logtype,
        [ValidateSet('desc', 'asc')]
        [string]$TimeOrder,
        [parameter(mandatory = $true, ParameterSetName = "timeRange")]
        [parameter(mandatory = $true, ParameterSetName = "timeSpanFromStart")]
        [parameter(mandatory = $true, ParameterSetName = "MaxLogCountFromStart")]
        [datetime]$TimeRangeStart,
        [parameter(mandatory = $true, ParameterSetName = "timeRange")]
        [parameter(mandatory = $true, ParameterSetName = "timeSpanUntilEnd")]
        [parameter(mandatory = $true, ParameterSetName = "MaxLogCountUntilEnd")]
        [datetime]$TimeRangeEnd,
        [parameter(mandatory = $true, ParameterSetName = "timeSpanUntilEnd")]
        [parameter(mandatory = $true, ParameterSetName = "timeSpanFromStart")]
        [parameter(mandatory = $true, ParameterSetName = "timeSpan")]
        [timespan]$Last,
        [parameter(mandatory = $true, ParameterSetName = "MaxLogCountUntilEnd")]
        [parameter(mandatory = $true, ParameterSetName = "MaxLogCountFromStart")]
        [parameter(mandatory = $true, ParameterSetName = "MaxLogCount")]
        [long]$TargetLogCount,
        [string]$Timezone,
        [ValidateSet('date', 'time', 'id', 'itime', 'euid', 'epid', 'dsteuid', 'dstepid', 'logflag', 'logver', 'type', 'subtype', 'level', 'action', 'policyid', 'sessionid', 'srcip', 'dstip', 'srcport', 'dstport', 'trandisp', 'duration', 'proto', 'sentbyte', 'rcvdbyte', 'sentpkt', 'rcvdpkt', 'logid', 'service', 'app', 'appcat', 'srcintfrole', 'dstintfrole', 'srcserver', 'dstserver', 'policytype', 'eventtime', 'poluuid', 'srcmac', 'mastersrcmac', 'dstmac', 'masterdstmac', 'srchwvendor', 'srcswversion', 'dsthwvendor', 'dstswversion', 'devtype', 'osname', 'dstosname', 'srccountry', 'dstcountry', 'srcintf', 'dstintf', 'policyname', 'tz', 'devid', 'vd', 'dtime', 'itime_t', 'devname')]
        [string[]]$Fields,
        [long]$MaxLogEntries=[long]::MaxValue
    )
    $PageSize = 1000
    # if ($PSCmdlet.ParameterSetName -eq )
    $searchParam = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude Fields, TargetLogCount, Last, MaxLogEntries
    $searchParam.TimeOrder='desc'
    if ($PSCmdlet.ParameterSetName -ne 'timeRange'){
        $tempHash = $PSBoundParameters | ConvertTo-PSFHashtable -Include TimeRangeStart, TimeRangeEnd, TargetLogCount, Last -IncludeEmpty
        $tempHash.last = $tempHash.last.ToString("g")
        Write-PSFMessage "Calculate TimeRangeStart and TimeRangeEnd from $($tempHash|ConvertTo-Json -Compress)"
    }
    switch -regex ($PSCmdlet.ParameterSetName){
        'timeSpan$'{
            $searchParam.TimeRangeEnd = Get-Date
            $searchParam.TimeRangeStart = $searchParam.TimeRangeEnd - $Last
        }
        'timeSpanUntilEnd' {
            $searchParam.TimeRangeStart = $searchParam.TimeRangeEnd - $Last
        }
        'timeSpanFromStart' {
            $searchParam.TimeRangeEnd = $searchParam.TimeRangeStart + $Last
        }
        'MaxLogCount' {
            $searchWindowDuration = Get-PSFConfigValue -FullName 'FortigateManager.Search.DefaultMaxRowDuration'
            If ($MaxLogEntries -eq [long]::MaxValue) {
                $MaxLogEntries = [long]($TargetLogCount*1.1)
                Write-PSFMessage "Adjusting MaxLogEntries from Max([long]) to TargetLogCount plus 10% (=$MaxLogEntries)" -Level Host
            }
        }
        'MaxLogCount$' {
            $searchParam.TimeRangeEnd = Get-Date
            $searchParam.TimeRangeStart = $searchParam.TimeRangeEnd - $searchWindowDuration
        }
        'MaxLogCountUntilEnd' {
            $searchParam.TimeRangeStart = $searchParam.TimeRangeEnd - $searchWindowDuration
        }
        'MaxLogCountFromStart' {
            $searchParam.TimeRangeEnd = Get-Date
            $searchParam.TimeRangeStart = $searchParam.TimeRangeEnd - $searchWindowDuration
        }
    }
    if ($PSCmdlet.ParameterSetName -match 'MaxLogCount') {
        Write-PSFMessage "Performing initial search for duration of $searchWindowDuration"
        $taskId = start-fmalogsearch @searchParam
         if($taskId -eq 0) {
            Stop-PSFFunction -Level Critical -Message "Could not obtain a taskId/start the logsearch"
            return
        }
        $currentStatus = Get-FMALogSearchStatus -TaskId $taskId -Connection $Connection -Adom $ADOM -LoggingLevel Verbose -Wait -EnableException $false
        Remove-FMALogSearch -TaskId $taskId
        if($null -eq $currentStatus){
            Stop-PSFFunction -Level Critical -Message "No current count status available for taskId $taskId" -EnableException $EnableException
        }
        # Applying the Rule of three
        $matchedLogs = $currentStatus."matched-logs"
        if ($matchedLogs -eq 0){
            Write-PSFMessage "Found 0 logs in timespan $($searchWindowDuration.ToString('g')), cannot auto adjust" -Level Warning
            return
        }
        $timeMultiplier=$TargetLogCount/$matchedLogs
        $usedDurationInSeconds = $searchWindowDuration.TotalSeconds
        $neededSeconds = $usedDurationInSeconds * $timeMultiplier
        $newSearchWindowDuration=[timespan]::FromSeconds([long]$neededSeconds)
        Write-PSFMessage "Found $matchedLogs logs in timespan $($searchWindowDuration.ToString('g')), Esteminated $($newSearchWindowDuration.ToString('g')) needed for gathering $TargetLogCount logs"
        switch -regex ($PSCmdlet.ParameterSetName) {
            'MaxLogCount$' {
                $searchParam.TimeRangeEnd = Get-Date
                $searchParam.TimeRangeStart = $searchParam.TimeRangeEnd - $newSearchWindowDuration
            }
            'MaxLogCountUntilEnd' {
                $searchParam.TimeRangeStart = $searchParam.TimeRangeEnd - $newSearchWindowDuration
            }
            'MaxLogCountFromStart' {
                $searchParam.TimeRangeEnd = Get-Date
                $searchParam.TimeRangeStart = $searchParam.TimeRangeEnd - $newSearchWindowDuration
            }
        }
    }
    if ($PSCmdlet.ParameterSetName -ne 'timeRange') {
        Write-PSFMessage "Result of calculation: $($searchParam | ConvertTo-PSFHashtable -Include TimeRangeStart,TimeRangeEnd|ConvertTo-Json -Compress)"
    }
    $taskId = start-fmalogsearch @searchParam
    if ($taskId -eq 0) {
        Stop-PSFFunction -Level Critical -Message "Could not obtain a taskId/start the logsearch"
        return
    }
    $Parameter = @{
        'apiver' = $Apiver
        "limit"  = $PageSize
        "offset" = 0
    } | Remove-FMNullValuesFromHashtable -NullHandler "RemoveAttribute"
    $explicitADOM = Resolve-FMAdom -Connection $Connection -Adom $ADOM -EnableException $EnableException
    Write-PSFMessage ($Parameter | convertto-json)
    $apiCallParameter = @{
        EnableException     = $EnableException
        Connection          = $Connection
        LoggingAction       = "Get-FMALogSearchResults"
        LoggingActionValues = @($Parameter.limit, $Parameter.offset)
        method              = "get"
        Parameter           = $Parameter
        Path                = "/logview/adom/$explicitADOM/logsearch/$taskId"
        LoggingLevel        = "Verbose"
    }
    $dataCollector=[System.Collections.ArrayList]::new()
    # $dataCollector = @()
    $retryCount=Get-PSFConfigValue -FullName 'FortigateManager.Analyzer.RetryCountForStatus' -Fallback 4
    $currentStatus = Get-FMALogSearchStatus -TaskId $taskId -Connection $Connection -Adom $ADOM -LoggingLevel Verbose -Wait

    $maxRows = $currentStatus."matched-logs"
    if ($maxRows -eq 0){
        Write-PSFMessage -Level Warning -Message "Search did not return any data"
        return
    }
    Write-PSFMessage -Level Host -Message "Fetching $maxRows rows of data"
    $collectedRecords = 0
    $i = 0
    do {
        $i++
        $remainingRecords = $maxRows - $dataCollector.Count
        if ($remainingRecords -lt $apiCallParameter.Parameter.limit) {
            $apiCallParameter.Parameter.limit = $remainingRecords
        }
        $percentCompleted = [int]($dataCollector.Count / $maxRows * 100)
        Write-Progress -Activity "Getting logdata" -PercentComplete $percentCompleted -Status "$percentCompleted% completed, $($dataCollector.Count) rows fetched"
        $retryCount = 0
        do {
            $response = Invoke-FMAPI @apiCallParameter #-Verbose
            if ($response.result."return-lines" -eq 0) {
                $retryCount++
                if ($retryCount -gt 15) { Start-Sleep -Seconds 1}
            }
        }until($response.result."return-lines" -gt 0 -or $retryCount -gt 40)
        if ($retryCount -gt 0) {
            Write-PSFMessage "Needed $retryCount retries before the API provided data" -Level Debug
        }
        if ($retryCount -gt 40) {
            Stop-PSFFunction -Message "Needed $retryCount retries before the API provided data" -Level Error
            return
        }
        $collectedRecords += $response.result."return-lines"
        if($Fields){
            [void]$dataCollector.AddRange(([array]($response.result.data | Select-Object -Property $Fields)))
        }else{
            [void]$dataCollector.AddRange(([array]($response.result.data)))
        }
        # $dataCollector += $response.result.data
        $Parameter.Offset = $dataCollector.Count
        $apiCallParameter.LoggingActionValues = @($Parameter.limit, $Parameter.offset)
    }while (($dataCollector.Count -lt $maxRows) -and ($response.result.data.Count -gt 0) -and ($collectedRecords -lt $maxRows) -and ($dataCollector.Count -lt $MaxLogEntries))
    if ($dataCollector.Count -gt $MaxLogEntries) { Write-PSFMessage -Level Warning "Stopped at $($dataCollector.Count) logs as only $MaxLogEntries should have been retrieved"}
    Remove-FMALogSearch -TaskId $taskId
    return $dataCollector.ToArray()
}