PSSplunkSearch.psm1

Function Connect-Splunk
{
    <#
    .SYNOPSIS
        Script to establish a connection to Splunk
    .DESCRIPTION
        Script to establish a connection to Splunk
    .PARAMETER Server
        Name of the Splunk server
    .PARAMETER Port
        Port number used by the Splunk server, default is 8089
    .PARAMETER Credential
        Username and password needed to authenticate
    .EXAMPLE
        Connect-Splunk -Server splunk.yourdomain.com
    .EXAMPLE
        Connect-Splunk -Server splunk.yourdomain.com -Port 9999
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true,
            Position=0)]
        [string]$Server,

        [Parameter(Mandatory=$false,
            Position=1)]
        [int]$Port = 8089,

        [Parameter(Mandatory=$true)]
        [pscredential]$Credential
    )

    If ((-not (Get-Variable SplunkConnect -Scope Script -ErrorAction SilentlyContinue)) -or $Script:SplunkConnect.Expires -lt (Get-Date))
    {
        $AuthSplat = @{
            Uri             = "https://$($server):$port/services/auth/login"
            UseBasicParsing = $true
            Body            = "username=$($Credential.UserName);password=$($Credential.GetNetworkCredential().Password)"
            Method          = "Post"
            ContentType     = "application/x-www-form-urlencoded"
            ErrorAction     = "Stop"
        }

        Try {
            $Return = Invoke-RestMethod @AuthSplat
        }
        Catch {
            Write-Error "Unable to authenticate to Splunk ($server), error: $_"
        }

        $Header = @{
            Authorization = "Splunk $($Return.response.sessionKey)"
        }

        $Script:SplunkConnect = [PSCustomObject]@{
            BaseUri     = "https://$($server):$port"
            Header      = $Header
            Expires     = (Get-Date).AddHours(6)
        }
    }
    Else
    {
        Write-Verbose "Already have a valid connection to Splunk"
    }
}
Function Disconnect-Splunk
{
    <#
    .SYNOPSIS
        Script to delete the connection variable to Splunk
    .DESCRIPTION
        Script to delete the connection variable to Splunk
    .EXAMPLE
        Disconnect-Splunk
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding()]
    Param ()

    Process {
        Remove-Variable -Name SplunkConnect -Scope Script -ErrorAction SilentlyContinue
    }
}
Function Get-SplunkSearchJob
{
    <#
    .SYNOPSIS
        Script to retrieve detailed information about a submitted search job
    .DESCRIPTION
        This script will retrieve detailed information about a search job you (or someone) has submitted.
        It does not retrieve any gathered results of the job.
    .PARAMETER sid
        This is the sid of the job. You can use Get-SplunkSearchJobList to locate your job and get the
        sid. The sid is also given when you run Start-SplunkSearch.
    .EXAMPLE
        Get-SplunkSearchJobList -Filter "*4740*" | Select-Object -First 1 | Get-SplunkSearchJob

        This example will use Get-SplunkSearchJobList to find any jobs with the text 4740 (user locked out)
        in the name field--name field is always the text of the full search. If there are multiple returns it
        will filter to only the first one, then retrieve the detailed information from that job.
    .EXAMPLE
        Get-SplunkSearchJob -sid 123456789.12345

        This will retrieve the job information for the above sid.
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true,
            Position=0,
            ValueFromPipelineByPropertyName=$true)]
        [string]$sid
    )

    Begin {
        ValidateSplunk
        Write-Verbose -Message "Starting Get-SplunkSearchJob"
    }

    Process {
        $Splat = @{
            Uri = "/services/search/jobs/$sid"
        }
        $Result = Invoke-SplunkMethod @Splat
        $Data = $Result |
            Select-Object -ExpandProperty entry |
            Select-Object Id,Updated,Published,Author,Name
        $Content = $Result.entry.content

        $Properties = $Content | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
        ForEach ($Property in $Properties)
        {
            $Data | Add-Member -MemberType NoteProperty -Name $Property -Value $Content.$Property
        }

        Write-Output $Data
    }
}
Function Get-SplunkSearchJobList
{
    <#
    .SYNOPSIS
        This will list all of the jobs currently stored on the Splunk server (whether they are running or
        not).
    .DESCRIPTION
        Retrieve a list of jobs. Output is limited to just enough to identify the job from the search
        criteria. Results can be piped into Get-SplunkSearchJob to get more detailed information about the
        job.
    .PARAMETER Filter
        Use this parameter to filter the results as needed. Supports wildcards.
    .EXAMPLE
        Get-SplunkSearchJobList -Filter "*4740*"

        This example will use Get-SplunkSearchJobList to find any jobs with the text 4740 (user locked out)
        in the name field--name field is always the text of the full search.
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$false,
            Position=0)]
        [string]$Filter
    )

    Begin {
        ValidateSplunk
        Write-Verbose -Message "Starting Get-SplunkSearchJobList"
    }

    Process {
        $Splat = @{
            Uri         = "/services/search/jobs"
            ErrorAction = "Stop"
        }
        $Data = Invoke-SplunkMethod @Splat |
            Select-Object -ExpandProperty entry |
            Select-Object @{Name = "sid";Expression={ $_.content.sid}},
                @{Name = "Published";Expression={ Get-Date $_.published }},
                @{Name = "Name"; Expression={ $_.name}}

        If ($Filter)
        {
            $Data = $Data | Where-Object Name -like $Filter
        }

        Write-Output $Data
    }
}
Function Invoke-SplunkMethod
{
    <#
    .SYNOPSIS
        This makes the API calls to Splunk
    .DESCRIPTION
        Used by most of the functions within this module as a common way to make API calls to Splunk. This
        has been made available to the user in case you need to do some functions not currently covered by
        the module.
    .PARAMETER Uri
        The Connect-Splunk function saves the server name and port of the Splunk server, so this would be
        the full path after that.

        Example:
            Full path: https://splunk.yourdomain.com:8089/services/search/jobs
            You would enter: /services/search/jobs
    .PARAMETER Body
        Hashtable of parameters needed by the API endpoint. Make sure to follow the case set out in the
        Splunk API reference, Splunk is case sensitive.

        Example:
            @{
                offset = 50
                count = 50
            }
    .PARAMETER Method
        Web method for the API endpoint. Must be "GET","POST","PUT" or "DELETE". GET is the default.
    .EXAMPLE
        Invoke-SplunkMethod -Uri "/services/search/jobs"

        This would do a GET call to the "/services/search/jobs" endpoint.
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding()]
    Param (
        [string]$Uri,
        [hashtable]$Body,
        [ValidateSet("GET","POST","PUT","DELETE")]
        [string]$Method = "GET"
    )

    Process {
        If ($Uri[0] -ne "/")
        {
            $Uri = "/$Uri"
        }

        $Uri = "$($Uri)?output_mode=json"

        $RestSplat = @{
            Uri             = "$($Script:SplunkConnect.BaseUri)$Uri"
            Header          = $Script:SplunkConnect.Header
            Method          = $Method
            Body            = $Body
            UseBasicParsing = $true
            Verbose         = $false
            ErrorAction     = "Stop"
        }
        Try {
            $Response = Invoke-RestMethod @RestSplat
        }
        Catch {
            Write-Error "Error retrieving query: $_"
            Return
        }

        # paging
        Return $Response
    }
}
Function Receive-SplunkSearch
{
    <#
    .SYNOPSIS
        Use this to retrieve results from your completed Splunk search
    .DESCRIPTION
        This function is used to retrieve the search results from the designated search you created.
    .PARAMETER sid
        This is the sid associated with your search job.
    .PARAMETER ReceiveCount
        Default is 250 items.

        With larger queries you can get hundreds, if not thousands of results. To not kill your Splunk
        server this function limits the number items that can be retrieved by any single API call. This
        is being done in the background and you will get all results as output of this function.
    .EXAMPLE
        Start-SplunkSearch -Query "EventCode=4740" | Wait-SplunkSearch | Receive-SplunkSearch

        Starts a search looking for event id 4740, waits for the job to complete and then retrieves the results
    .EXAMPLE
        Get-SplunkSearchJobList -Filter "*4740*" | Receive-SplunkSearch
    .EXAMPLE
        Receive-SplunkSearch -sid 123456789.12345
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true,
            Position=0,
            ValueFromPipelineByPropertyName=$true)]
        [string]$sid,

        [int]$ReceiveCount = 250
    )

    Begin {
        ValidateSplunk
        Write-Verbose -Message "Starting Receive-SplunkSearch"
    }

    Process {
        $GetJob = Get-SplunkSearchJob -sid $sid

        $Top = 0
        If ($GetJob.resultCount -gt $ReceiveCount)
        {
            $Top = [math]::Ceiling($GetJob.resultCount / $ReceiveCount) - 1
        }
        $OffsetCount = 0
        $Data = ForEach ($Offset in (0..$Top))
        {
            If ($Offset -gt 0)
            {
                $OffsetCount += $ReceiveCount
            }
            $RetrieveSplat = @{
                Uri  = "/services/search/jobs/$sid/results"
                Body = @{
                    count  = $ReceiveCount
                    offset = $OffsetCount
                }
            }
            Invoke-SplunkMethod @RetrieveSplat | Select-Object -ExpandProperty results
        }
        $Data | Add-Member -MemberType ScriptProperty -Name "Date" -Value { Get-Date $this._time }
        Write-Output $Data
    }
}
Function Remove-SplunkSearch
{
    <#
    .SYNOPSIS
        Delete the search job
    .DESCRIPTION
        When you've retrieved the search results needed, you can remote the search and it's results from the
        server using this function.
    .PARAMETER sid
        This is the sid associated with your search job.
    .EXAMPLE
        Remove-SplunkSearch -sid 123456789.12345

        Removes the specified search job.
    .EXAMPLE
        Get-SplunkSearchJobList -Filter "*4740*" | Remove-SplunkSearch
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding(SupportsShouldProcess=$true,
        ConfirmImpact="High")]
    Param (
        [Parameter(Mandatory=$true,
            ValueFromPipelineByPropertyName=$true)]
        [string]$sid,

        [switch]$Force
    )

    Begin {
        ValidateSplunk
        Write-Verbose -Message "Starting Remove-SplunkSearch"
    }

    Process {
        $Job = Get-SplunkSearchJob -sid $sid -ErrorAction Stop

        $DeleteSplat = @{
            Uri         = "/services/search/jobs/$sid"
            Method      = "DELETE"
            ErrorAction = "Stop"
        }

        If ($Force -or $PSCmdlet.ShouldProcess("Remove this Splunk job?", $Job.Name))
        {
            Invoke-SplunkMethod @DeleteSplat
        }
    }
}
Function Start-SplunkSearch
{
    <#
    .SYNOPSIS
        Start a search job on your Splunk server
    .DESCRIPTION
        This will start a search job on your Splunk server, returning just the sid number of that job.
        Use Wait-SplunkSearch to watch the job until it finishes, and then Receive-SplunkSearch to
        retrieve the results.
    .PARAMETER Query
        This is the query (using Splunk's query language) for your search
    .PARAMETER Start
        Start time of your search. To keep you from overwhelming your server this defaults to 1 day
        ago.
    .PARAMETER End
        End time of your search. By default this will be now.
    .PARAMETER Index
        Specify the index you wish to search. This is an optional parameter and you could include the
        index in your Query if you wanted to.
    .EXAMPLE
        Remove-SplunkSearch -sid 123456789.12345

        Removes the specified search job.
    .EXAMPLE
        Get-SplunkSearchJobList -Filter "*4740*" | Remove-SplunkSearch
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    Param (
        [Parameter(Mandatory=$true)]
        [string]$Query,

        [Parameter(Mandatory=$false)]
        [datetime]$Start = ((Get-Date).AddDays(-1)),

        [Parameter(Mandatory=$false)]
        [datetime]$End = (Get-Date),

        [Parameter(Mandatory=$false)]
        [string]$Index
    )

    Begin {
        ValidateSplunk
        Write-Verbose -Message "Starting Start-SplunkSearch"
    }

    Process {
        $Search = $Query

        If ($Index -and $Search -notmatch "index ?= ?")
        {
            $Search += " index=$Index"
        }

        $Body = @{
            search        = "search $Search"
            earliest_time = Get-Date $Start.ToUniversalTime() -Format "yyyy-MM-ddTHH:mm:ss"
            latest_time   = Get-Date $End.ToUniversalTime() -Format "yyyy-MM-ddTHH:mm:ss"
        }

        $SearchSplat = @{
            Uri    = "/services/search/jobs"
            Body   = $Body
            Method = "POST"
        }
        $Data = Invoke-SplunkMethod @SearchSplat

        [PSCustomObject]@{
            sid           = $Data.sid
            Search        = $Body.Search
            Earliest_Time = $Body.earliest_time
            Latest_Time   = $Body.latest_time
        }
    }
}

Function ValidateSplunk
{
    <#
    .SYNOPSIS
        Helper script to make sure a connection variable has been created
    #>

    [CmdletBinding()]
    Param ()

    If (-not (Get-Variable SplunkConnect -Scope Script -ErrorAction SilentlyContinue))
    {
        Write-Error "You have not connected to Splunk, please run Connect-Splunk" -ErrorAction Stop
    }
    ElseIf ($Script:SplunkConnect.Expires -lt (Get-Date))
    {
        Write-Error "Your Splunk connection has expired, please run Connect-Splunk again" -ErrorAction Stop
        Disconnect-Splunk
    }
}
Function Wait-SplunkSearch
{
    <#
    .SYNOPSIS
        Wait for a search result job to finish
    .DESCRIPTION
        You can use this function to watch a running search job until it finishes.
    .PARAMETER sid
        This is the sid associated with your search job.
    .EXAMPLE
        Start-SplunkSearch -Query "EventCode=4740" Index="domain_controllers" -Start "2/20/21" -End "2/22/21" | Wait-SplunkSearch

        Begins a search and waits for it to complete.
    .EXAMPLE
        Get-SplunkSearchJobList -Filter "*4740*" | Wait-SplunkSearch

        If this filter returns multiple results, it will wait for the first one before moving on to the second and so on.
    .NOTES
        Author: Martin Pugh
        Twitter: @martin9700
        Spiceworks: Martin9700
        Blog: www.thesurlyadmin.com

        Changelog:
            02/27/21 Initial Release
    .LINK
        https://github.com/martin9700/PSSplunkSearch
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true,
            ValueFromPipelineByPropertyName=$true)]
        [string]$sid
    )

    Begin {
        ValidateSplunk
        Write-Verbose -Message "Starting Wait-SplunkSearch"
    }

    Process {
        $SearchSplat = @{
            Uri         = "/services/search/jobs/$sid"
            Method      = "GET"
            ErrorAction = "Stop"
        }

        $Wait = 0
        Do {
            Start-Sleep -Seconds $Wait
            $GetJob = Invoke-SplunkMethod @SearchSplat
            If (-not $Wait)
            {
                $Start = Get-Date $GetJob.entry.content.request.earliest_time -Format "MM/dd/yyyy HH:mm:ss"
                $End   = Get-Date $GetJob.entry.content.request.latest_time -Format "MM/dd/yyyy HH:mm:ss"
                Write-Verbose "Title: $($GetJob.entry.content.request.search) Start: $Start End: $End"
                $Wait = 8
            }
            Write-Verbose "Job ($sid) status is $($GetJob.entry.content.dispatchState) ($($GetJob.entry.content.runDuration))"
        } Until ($GetJob.entry.content.isDone)

        [PSCustomObject]@{
            sid           = $sid
            Name          = $GetJob.entry.content.request.search
            Earliest_Time = $Start
            Latest_Time   = $End
            RunDuration   = $GetJob.entry.content.runDuration
            Status        = $GetJob.entry.content.dispatchState
        }
    }
}