SentinelOne.psm1

#This module requires Powershell 7 or higher
#Requires -Version 7.0

class SentinelOne
{
    [Hashtable]$APITokens = @{}
    [String]$Path
    [Datetime]$GetDate
    [Int]$RetryIntervalSec = 2
    [Int]$MaximumRetryCount = 2

    #API endpoints
    [Hashtable]$APIEndpoints = @{
        ApiTokenDetails = @{Method = "POST"; URI = "web/api/v2.1/users/api-token-details"};
        GetAgents = @{Method = "GET"; URI = "web/api/v2.1/agents"};
        CreateQueryAndGetQueryid = @{Method = "POST"; URI = "web/api/v2.1/dv/init-query"};
        GetQueryStatus = @{Method = "GET"; URI = "web/api/v2.1/dv/query-status?queryId="};
        GetEvents = @{Method = "GET"; URI = "web/api/v2.1/dv/events?sortBy=createdAt&queryId="};
        GetActivities = @{Method = "GET"; URI = "web/api/v2.1/activities?sortBy=createdAt&sortOrder=desc&limit=1000&activityTypes="};
        FetchFiles = @{Method = "POST"; URI = "web/api/v2.1/agents/{agent_id}/actions/fetch-files"};
        GetSites = @{Method = "GET"; URI = "/web/api/v2.1/sites?limit=1000"};
        GetGroups = @{Method = "GET"; URI = "/web/api/v2.1/groups?limit=200&siteIds="};
        GetExclusions = @{Method = "GET"; URI = "/web/api/v2.1/exclusions?limit=1000&type="};
        SitePolicy = @{Method = "GET"; URI = "web/api/v2.1/sites/{site_id}/policy"}}

    SentinelOne($Path)
    {
        $this.Path = $Path
        $this.ReadAPITokens()
        $this.GetDate = Get-Date
    }

    [PSObject] MakeHTTPRequest($APITokenName, $RequestName, $Parameters)
    {
        $Headers = @{Authorization = "APIToken $($this.APITokens.$APITokenName.APIToken)"}
        $URI = $this.APITokens.$APITokenName.Endpoint + $this.APIEndpoints.$RequestName.URI
        switch ($RequestName)
            {
                "GetAgents" { $URI += $Parameters[0]; break}
                "GetQueryStatus" { $URI += $Parameters[0]; break}
                "GetEvents" { $URI += $Parameters[0] + "&cursor=" + $Parameters[1] + "&limit=" + $Parameters[2]; break}
                "FetchFiles" { $URI = $URI.Replace("{agent_id}", $Parameters[1]); break}
                "GetActivities" { $URI += $Parameters[0]; break}
                "GetGroups" { $URI += $Parameters[0]; break}
                "SitePolicy" { $URI = $URI.Replace("{site_id}", $Parameters[0]); break}
                "GetExclusions" { $URI += $Parameters[1]; $URI += "&cursor=$($Parameters[4])"; ;if ($Parameters[0] -ne "Global") {$URI += "&siteIds=$($Parameters[2])"}; if ($Parameters[0] -eq "Group") {$URI += "&groupIds=$($Parameters[3])"}; break}
                Default {}
            }

        if ($this.APIEndpoints.$RequestName.Method -eq "GET")
        {
            $httpError = ""
            try
            {
                $return = Invoke-RestMethod -Uri $URI -Method GET -Headers $Headers -RetryIntervalSec $this.RetryIntervalSec -MaximumRetryCount $this.MaximumRetryCount -ContentType "application/json"
            }
            catch
            {
                try
                {
                    $httpError = $_
                    $return = $httpError.ErrorDetails.Message | ConvertFrom-Json
                }
                catch
                {
                    $return = $httpError
                }
            }
        }
        else
        {
            $httpError = ""
            try
            {
            #$Parameters[0] should be a POST body, JSON formatted
            $return = Invoke-RestMethod -Uri $URI -Method POST -Headers $Headers -RetryIntervalSec $this.RetryIntervalSec -MaximumRetryCount $this.MaximumRetryCount -ContentType "application/json" -Body $Parameters[0]
            }
            catch
            {
                try
                {
                    $httpError = $_
                    $return = $httpError.ErrorDetails.Message | ConvertFrom-Json
                }
                catch
                {
                    $return = $httpError
                }
            }
        }
        return $return
    }

    [String] ValidateAPIToken($APITokenName, $ThrowIfInvalid)
    {
        $Body = ConvertTo-Json -Compress -InputObject $(@{data = @{apiToken = $this.APITokens.$APITokenName.APIToken}})
        $Http = $this.MakeHTTPRequest($APITokenName, "ApiTokenDetails", @($Body))
        if ($Http.data.expiresAt)
        {
            $this.APITokens.$APITokenName.ExpiresAt = $Http.data.expiresAt
            return "True"
        }
        else
        {
            if ($ThrowIfInvalid)
            {
                throw "Failed to verify API token. Please check Endpoint, APIToken and network connection to the console."
            }
            else
            {
                return $Http
            }
        }
    }

    [Void] SaveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)
    {
        $this.RetryIntervalSec = $RetryIntervalSec
        $this.MaximumRetryCount = $MaximumRetryCount
    }

    [Bool] Hidden ReadAPITokens()
    {
        try
        {
            $read = [System.IO.File]::ReadAllBytes($this.Path)
            $read = [System.Security.Cryptography.ProtectedData]::Unprotect($read, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
            $read = [System.Text.Encoding]::Unicode.GetString($read)
            $read = ConvertFrom-Json -InputObject $read -AsHashtable
        }
        catch
        {
            return $false
        }
        $this.APITokens = $read
        return $true
    }

    [Bool] Hidden WriteAPITokens()
    {
        try
        {
            $write = ConvertTo-Json -InputObject $this.APITokens -Compress
            $write = [System.Text.Encoding]::Unicode.GetBytes($write)
            $write = [System.Security.Cryptography.ProtectedData]::Protect($write, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
            [System.IO.File]::WriteAllBytes($this.Path, $write)
        }
        catch
        {
            throw "Cannot encrypt and/or save API tokens to $($this.Path)"
        }
        return $true
    }

    [Bool] AddAPIToken($APIToken, $Endpoint, $APITokenName, $Description, $DoNotValidateToken)
    {
        #Ensuring API token name is not *
        if ($APITokenName -eq "*")
        {
            throw "Name cannot be equal to *"
        }
        #Ensuring Endpoint URL contains / at the end
        if ($Endpoint -notmatch "/$")
        {
            $Endpoint += "/"
        }
        #Ensuring endpoing looks like a SentineOne URL
        if ($Endpoint -notmatch "^https://[\w\S]+\.sentinelone.net/$")
        {
            throw "Wrong Endpoint provided. Proper format is e.g. https://contoso.sentinelone.net/"
        }

        if ($this.APITokens.ContainsKey($APITokenName))
        {
            throw "Saved API tokens already contain a token with name $APITokenName. Remove existing token with `"Remove-S1APIToken -Name $APITokenName`""
        }
        
        $this.APITokens.Add($APITokenName, @{"APIToken" = $APIToken; "Endpoint" = $Endpoint; "Description" = $Description})

        if ($DoNotValidateToken -eq $false)
        {
            $this.ValidateAPIToken($APITokenName, $true)
        }
    
        $this.WriteAPITokens()
        return $true
    }

    [Void] RemoveAPIToken($APITokenName)
    {
        if ($APITokenName -eq "*")
        {
            throw "API token name cannot be equal to *"
        }
        if (!$this.APITokens.ContainsKey($APITokenName))
        {
            throw "No saved API token with name $APITokenName."
        }
        $this.APITokens.Remove($APITokenName)
        $this.writeAPITokens()
    }

    [String] Hidden PrepareGetFilter($Parameters)
    {
        $filter = ""
    
        foreach ($Key in $Parameters.Keys)
        {
            switch -Exact ($Key)
            {
                "ComputerNameContains" { $filter += "&computerName__contains="+$($Parameters[$Key] -join ","); Break }
                "OSTypes" { $filter += "&osTypes="+$($Parameters[$Key] -join ","); Break }
                "AgentVersions" { $filter += "&agentVersions="+$($Parameters[$Key] -join ","); Break }
                "IsActive" { $filter += "&isActive="+$($Parameters[$Key]); Break }
                "IsInfected" { $filter += "&infected="+$($Parameters[$Key]); Break }
                "IsUpToDate" { $filter += "&isUpToDate="+$($Parameters[$Key]); Break }
                "NumberOfActiveThreatsEqualTo" { $filter += "&activeThreats="+$($Parameters[$Key]); Break }
                "NumberOfActiveThreatsGreaterThan" { $filter += "&activeThreats__gt="+$($Parameters[$Key]); Break }
                "ScanStatus" { $filter += "&scanStatus="+$($Parameters[$Key]); Break }
                "MachineTypes" { $filter += "&machineTypes="+$($Parameters[$Key] -join ","); Break }
                "UserActionsNeeded" { $filter += "&userActionsNeeded="+$($Parameters[$Key] -join ","); Break }
                "NetworkStatuses" { $filter += "&networkStatuses="+$($Parameters[$Key] -join ","); Break }
                "AgentDomains" { $filter += "&domains="+$($Parameters[$Key] -join ","); Break }
                "IsPendingUninstall" { $filter += "&isPendingUninstall="+$($Parameters[$Key]); Break }
                "IsDecommissioned" { $filter += "&isDecommissioned="+$($Parameters[$Key]); Break }
                Default {}
            }
        }
        return $filter
    }
    
    [PSObject] GetAgents($APITokenName, $ResultSize, $Parameters)
    {
        $Return = @()
        $GetAll = $false
        if ($ResultSize -eq "All")
        {
            $ResultSize = 1000
            $GetAll = $true
        }
        $Filter = "?$($this.PrepareGetFilter($Parameters))&limit=$ResultSize"
        $FilterCursor = $Filter
        $TotalAgents = 0
        Do
        {
            $Http = $this.MakeHTTPRequest($APITokenName, "GetAgents", @($FilterCursor))
            if ($Http.errors)
            {
                Write-Host "Error code: $($Http.errors.code)"
                Write-Host "Error detail: $($Http.errors.detail)"
                Write-Host "Error title: $($Http.errors.title)"
                throw "Error while running Get-S1Agent"
            }
            $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty
            $Return += $Http.data
            $FilterCursor = $Filter + "&cursor=$($Http.pagination.nextCursor)"
            if ($Http.pagination.totalItems -gt 0)
            {
                $TotalAgents = $Http.pagination.totalItems
            }
            if ($TotalAgents -gt 1)
            {
                Write-Host "Completed $($Return.Count) agents from $TotalAgents using API token $APITokenName..."
            }
        } While ($GetAll -and $null -ne $Http.pagination.nextCursor)
        return $Return
    }

    [Void] CheckAPITokenName($APITokenNames)
    {
        foreach ($APITokenName in $APITokenNames)
        {
            if(!$this.APITokens.ContainsKey($APITokenName))
            {
                throw "No saved API token with name $APITokenName"
            }
        }
    }
    
    [Datetime] parseRange($range)
    {
        #Relative range
        if ($range -match "^-\d+[hmd]$")
        {
            $number = [int]((Select-String -InputObject $range -Pattern "\d+").Matches.Value)
            switch ((Select-String -InputObject $range -Pattern "[mhd]").Matches.Value)
            {
                "m" { return $this.getDate.AddMinutes($number*-1) }
                "h" { return $this.getDate.AddMinutes($number*60*-1) }
                "d" { return $this.getDate.AddMinutes($number*60*24*-1) }
                Default {throw "Error parsing range"}
            }
        }
        else
        {
            try
            {
                $date = Get-Date -Date $range
            }
            catch
            {
                throw "Cannot parse date"
            }
            return $date
        }
        return $this.getDate
    }

    [String] submitDVQuery($APITokenName, $Query)
    {
        $Http = $this.MakeHTTPRequest($APITokenName, "CreateQueryAndGetQueryid", @($Query))
        if ($Http.errors)
        {
            Write-Host "Error code: $($Http.errors.code)"
            Write-Host "Error detail: $($Http.errors.detail)"
            Write-Host "Error title: $($Http.errors.title)"
            throw "Error while running Get-S1Agent"
        }
        $QueryId = $Http.data.queryId
        if ($QueryId -match "q[a-f0-9]{32}")
        {
            return $QueryId
        }
        else
        {
            throw "DeepVisibility query submission failed."
        }
    }

    [Hashtable] getQueryStatus($APITokenName, $QueryID)
    {
        Start-Sleep 3
        $Http = $this.MakeHTTPRequest($APITokenName, "GetQueryStatus", @($QueryID))
        return @{progressStatus = $Http.data.progressStatus; responseState = $Http.data.responseState; error = $Http.errors}
    }

    [Bool] RequestFileFetch($APITokenName, $AgentID, $File, $Password)
    {
        $PostBody = @{data = @{files = $File; password = $Password}}
        $PostBody = ConvertTo-Json -Compress -InputObject $PostBody
        $Http = $this.MakeHTTPRequest($APITokenName, "FetchFiles", @($PostBody, $AgentID))        
        if ($Http.data.success -eq $true)
        {
            return $true
        }
        return $false
    }

    [PSObject] RequestFileFetchActivityPage($APITokenName, $Code)
    {
        $Http = $this.MakeHTTPRequest($APITokenName, "GetActivities", @($Code))
        $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty
        return $Http.data
    }

    [Bool] RequestFileFetchDownload($APITokenName, $DownloadUrl, $Filename, $SaveEmptyFetch)
    {
        $URI = $this.APITokens[$APITokenName].Endpoint + "web/api/v2.1" + $DownloadUrl
        $OutFile = $(Get-Location).Path + "\" + $Filename + ".zip"
        $ZipFileFetch = Invoke-WebRequest -Uri $URI -Method GET -Headers @{Authorization = "APIToken "+$this.APITokens[$APITokenName].APIToken} -RetryIntervalSec $this.RetryIntervalSec -MaximumRetryCount $this.MaximumRetryCount
        if ($ZipFileFetch.RawContentLength -gt 5000 -or $SaveEmptyFetch)
        {
            #ZIP file is not empty because of its size - no need to unpack in memory. Just saving.
            #SaveEmptyFetch was set. Just saving.
            [System.IO.File]::WriteAllBytes($OutFile, $ZipFileFetch.Content)
            Write-Host "File saved to $OutFile" -ForegroundColor Green
            return $true
        }
        else
        {
            [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression")
            $ZipStream = New-Object System.IO.Memorystream
            $ZipStream.Write($ZipFileFetch.Content,0,$ZipFileFetch.Content.Length)
            $ZipFile = [System.IO.Compression.ZipArchive]::new($ZipStream)
            if ($ZipFile.Entries.Count -gt 1)
            {
                [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression")
                $ZipFileFetch.Content | Set-Content -Path $OutFile -AsByteStream
                Write-Host "File saved to $OutFile" -ForegroundColor Green
                return $true
            }
            else
            {
                Write-Host "$Filename.zip was fetched, but appear to be empty. Not saving." -ForegroundColor Red
                return $false
            }
        }
    }

    [PSObject] GetS1SitePolicy($APITokenName, $SiteId, $SiteName)
    {
        $Http = $this.MakeHTTPRequest($APITokenName, "SitePolicy", @($SiteId))
        $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty
        $Http.data | Add-Member -Value $SiteName -Name "siteName" -MemberType NoteProperty
        $Http.data | Add-Member -Value $SiteId -Name "siteId" -MemberType NoteProperty
        return $Http.data
    }












    [PSObject] GetS1Groups($APITokenName, $SiteId, $SiteName)
    {
        $Http = $this.MakeHTTPRequest($APITokenName, "GetGroups", @($SiteId))
        $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty
        $Http.data | Add-Member -Value $SiteName -Name "siteName" -MemberType NoteProperty
        $Http.data | Add-Member -Value $SiteId -Name "siteId" -MemberType NoteProperty
        return $Http.data
    }


    [PSObject] GetS1Exclusions($APITokenName, $Type, $Scope, $AccountId, $AccountName, $SiteId, $SiteName, $GroupId, $GroupName)
    {
        $Return = @()
        $NextCursor = ""
        if($Scope -eq "Account")
        {
            $SiteId = $AccountId
        }
        Do
        {
            $Http = $this.MakeHTTPRequest($APITokenName, "GetExclusions", @($Scope, $Type, $SiteId, $GroupId, $NextCursor))
            $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty
            $Return += $Http.data
            $NextCursor = $Http.pagination.nextCursor

            if ($Scope -eq "Global")
            {
                $Http.data | Add-Member -Value "" -Name "accountName" -MemberType NoteProperty
                $Http.data | Add-Member -Value "" -Name "accountId" -MemberType NoteProperty        
            }
            else
            {
                $Http.data | Add-Member -Value $AccountName -Name "accountName" -MemberType NoteProperty
                $Http.data | Add-Member -Value $AccountId -Name "accountId" -MemberType NoteProperty                
            }
            if ($Scope -eq "Account")
            {
                $Http.data | Add-Member -Value "Account" -Name "exceptionScope" -MemberType NoteProperty
            }
            elseif ($Scope -eq "Site")
            {
                $Http.data | Add-Member -Value $SiteName -Name "siteName" -MemberType NoteProperty
                $Http.data | Add-Member -Value $SiteId -Name "siteId" -MemberType NoteProperty
                $Http.data | Add-Member -Value "" -Name "groupName" -MemberType NoteProperty
                $Http.data | Add-Member -Value "" -Name "groupId" -MemberType NoteProperty
                $Http.data | Add-Member -Value "Site" -Name "exceptionScope" -MemberType NoteProperty
            }
            elseif ($Scope -eq "Group")
            {
                $Http.data | Add-Member -Value $SiteName -Name "siteName" -MemberType NoteProperty
                $Http.data | Add-Member -Value $SiteId -Name "siteId" -MemberType NoteProperty
                $Http.data | Add-Member -Value $groupName -Name "groupName" -MemberType NoteProperty
                $Http.data | Add-Member -Value $groupId -Name "groupId" -MemberType NoteProperty
                $Http.data | Add-Member -Value "Group" -Name "exceptionScope" -MemberType NoteProperty
            }
            elseif ($Scope -eq "Global")
            {
                $Http.data | Add-Member -Value "" -Name "siteName" -MemberType NoteProperty
                $Http.data | Add-Member -Value "" -Name "siteId" -MemberType NoteProperty
                $Http.data | Add-Member -Value "" -Name "groupName" -MemberType NoteProperty
                $Http.data | Add-Member -Value "" -Name "groupId" -MemberType NoteProperty
                $Http.data | Add-Member -Value "Global" -Name "exceptionScope" -MemberType NoteProperty
            }
        } While ($null -ne $Http.pagination.nextCursor)
        return $Return
    }

    [PSObject] GetS1Sites($APITokenName)
    {
        $Http = $this.MakeHTTPRequest($APITokenName, "GetSites", @())
        $Http.data.sites | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty
        return $Http.data.sites
    }

    [PSObject] GetQueryData($APITokenName, $QueryID, $FetchSize)
    {
        $Return = @()
        $NextCursor = ""
        $TotalItems = 0

        Do
        {
            $Http = $this.MakeHTTPRequest($APITokenName, "GetEvents", @($QueryID, $NextCursor, $FetchSize))
            $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty
            $Return += $Http.data | Select-Object -ExcludeProperty attributes
            $NextCursor = $Http.pagination.nextCursor
            if ($Http.pagination.totalItems -gt 0 -and $TotalItems -eq 0)
            {
                $TotalItems = $Http.pagination.totalItems
            }
            if ($TotalItems -gt 0)
            {
                Write-Host "Fetched $($Return.Count) Deep Visibility events from total $TotalItems using API token $APITokenName..."
            }
        } While ($null -ne $Http.pagination.nextCursor)
        return $Return
    }
}

function Add-S1APIToken
{
    [CmdletBinding()]
    Param(

        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")]
        [String] $APITokenName,

        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token")]
        [ValidateLength(80,80)]
        [String] $APIToken,
            
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token endpoint URL (e.g. https://contoso.sentinelone.net/)")]
        [String] $Endpoint,

        [Parameter(HelpMessage="You can provide and save comments to the API token")]
        [String] $Description = $("API token added $(Get-Date)"),

        [Parameter(HelpMessage="Full path to encrypted file to save API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),
        
        [Switch] $DoNotValidateToken
        )
        
    $API = [SentinelOne]::new($Path)

    if ($API.AddAPIToken($APIToken, $Endpoint, $APITokenName, $Description, $DoNotValidateToken) -eq $true)
    {
        Write-Host "API token `"$APITokenName`" added successfully." -ForegroundColor Green
    }
    else
    {
        Write-Host "Failed to add API token `"$APITokenName`"." -ForegroundColor Red
    }
}

function Get-S1APIToken
{
    [CmdletBinding()]
    Param(
        [Parameter(HelpMessage="Enter SentinelOne API token name")]
        [ValidateNotNullOrEmpty()]
        [String] $APITokenName = "*",

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),

        [Parameter()]
        [ValidateRange(1,10)]
        [Int] $RetryIntervalSec = 1,

        [Parameter()]
        [ValidateRange(1,10)]
        [Int] $MaximumRetryCount = 2,
        
        [Switch] $ValidateAPIToken,
        [Switch] $UnmaskAPIToken
        )

    $API = [SentinelOne]::new($Path)
    $API.SaveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)
    $Tokens = @()

    foreach ($Name in $API.APITokens.Keys)
    {
        $APITokenHashTable = [Ordered]@{APITokenName = $Name; Endpoint = $API.APITokens.$Name.Endpoint; Description = $API.APITokens.$Name.Description; APIToken = ($API.APITokens.$Name.APIToken.Substring(0,5)+"*"*75)}
        if ($UnmaskAPIToken)
        {
            $APITokenHashTable.APIToken = $API.APITokens.$Name.APIToken
        }
        if ($ValidateAPIToken -and ($Name -eq $APITokenName -or $APITokenName -eq "*"))
        {
            $APITokenHashTable.IsValid = $API.ValidateAPIToken($Name, $false)
            $APITokenHashTable.ExpiresAt = $API.APITokens.$Name.ExpiresAt
        }
        $Tokens += [PSCustomObject]$APITokenHashTable
    }
    if ($APITokenName -eq "*")
    {
        return $Tokens
    }
    else
    {
        return ($Tokens | Where-Object APITokenName -eq $APITokenName)
    }
    
}

function Remove-S1APIToken
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")]
        [String] $APITokenName,

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token")
    )

    $API = [SentinelOne]::new($Path)
    $API.RemoveAPIToken($APITokenName)
}

function Get-S1Agent
{
    [CmdletBinding(PositionalBinding = $false)]
    Param(
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")]
        [String[]] $APITokenName,

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({$_ -eq "All" -or ([Int]::Parse($_) -ge 1 -and [int]::Parse($_) -le 1000)})]
        [String] $ResultSize = "1000",

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $RetryIntervalSec = 5,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $MaximumRetryCount = 2,
        
        #Get-S1Agents filters
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String[]] $ComputerNameContains,

        [Parameter()]
        [ValidateSet("linux","macos","windows", "windows_legacy")]
        [String[]] $OSTypes,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String[]] $AgentVersions,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Bool] $IsActive,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Bool] $IsInfected,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Bool] $IsUpToDate,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $NumberOfActiveThreatsEqualTo,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $NumberOfActiveThreatsGreaterThan,

        [Parameter()]
        [ValidateSet("finished","aborted","started", "none")]
        [String] $ScanStatus,

        [Parameter()]
        [ValidateSet("kubernetes node","desktop","laptop", "server", "unknown")]
        [String[]] $MachineTypes,

        [Parameter()]
        [ValidateSet("agent_suppressed_category", "incompatible_os", "incompatible_os_category", "missing_permissions_category", "none", "reboot_category", "reboot_needed",
        "unprotected", "unprotected_category", "upgrade_needed", "user_action_needed", "user_action_needed_fda", "user_action_needed_network", "user_action_needed_rs_fda")]
        [String[]] $UserActionsNeeded,

        [Parameter()]
        [ValidateSet("connected", "connecting", "disconnected", "disconnecting")]
        [String[]] $NetworkStatuses,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String[]] $AgentDomains,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Bool] $IsPendingUninstall,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Bool] $IsDecommissioned
    )
    
    $API = [SentinelOne]::new($Path)
    $API.CheckAPITokenName($APITokenName)
    $API.SaveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)
    $Return = @()
    foreach ($Name in $APITokenName)
    {
        $Return += $API.GetAgents($Name, $ResultSize, $PSBoundParameters)
    }
    return $Return
}

function Get-S1DeepVisibility
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")]
        [String[]] $APITokenName,

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({[Int]::Parse($_) -ge 1 -and [int]::Parse($_) -le 20000})]
        [String] $ResultSize = "1000",

        [ValidateNotNullOrEmpty()]
        [ValidateRange(1,1000)]
        [Int] $FetchSize = 500,

        [ValidateNotNullOrEmpty()]
        [Int] $RetryIntervalSec = 5,

        [ValidateNotNullOrEmpty()]
        [Int] $MaximumRetryCount = 36,

        [Parameter(HelpMessage="Enter Deep Visibility search query", ParameterSetName="Advanced")]
        [ValidateNotNullOrEmpty()]
        [String] $Query,
        
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $EndpointName,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $Sha256,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $Sha1,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $Md5,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $FilePath,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $IP,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $DNS,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $Name,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $CmdLine,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $UserName,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $DstPort,
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()]
        [ValidateSet("ip", "dns", "process", "cross_process", "indicators", "file", "registry", "scheduled_task", "url", "command_script", "logins")]
        [String] $ObjectType,
        
        [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()]
        [ValidateSet("Login", "`"Registry Key Export`"", "Logout", "Unknown", "`"Pre Execution Detection`"", "Command Script", "HEAD", "DELETE", "Registry Key Security Changed", "File Scan", "PUT", "Remote Thread Creation", "OPTIONS", "DNS Unresolved", "Task Register", "Task Delete", "Task Update", "Duplicate Thread Handle", "IP Listen", "Task Start", "CONNECT", "GET", "Registry Value Create", "DNS Resolved", "Registry Key Create", "Process Creation", "Open Remote Process Handle", "Behavioral Indicators", "Duplicate Process Handle", "Task Trigger", "POST", "File Deletion", "Registry Value Modified", "Registry Value Delete", "Registry Key Delete", "Not Reported", "IP Connect", "File Modification", "File Creation", "File Rename")]
        [String] $EventType,
        
        [Parameter(Mandatory, HelpMessage="Enter Deep Visibility search range")]
        [String] $Earliest,
        
        [Parameter(HelpMessage="Enter Deep Visibility search range")]
        [ValidateNotNullOrEmpty()]
        [String] $Latest
        )
        
    $API = [SentinelOne]::new($Path)
    $API.CheckAPITokenName($APITokenName)
    $API.SaveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)

    $fromDate = $API.ParseRange($Earliest)
    $fromDate = $(Get-Date -Date $($(Get-Date -Date $fromDate).ToUniversalTime()) -Format O)
    
    if($PSBoundParameters.ContainsKey("Latest"))
    {
        $toDate = $API.parseRange($Latest)
        $toDate = $(Get-Date -Date $($(Get-Date -Date $toDate).ToUniversalTime()) -Format O)
        if ($fromDate -gt $toDate)
        {
            throw "Latest is before Earliest!"
        }
    }
    else
    {
        $Latest = $(Get-Date)
        $toDate = $(Get-Date -Date $($(Get-Date -Date $Latest).ToUniversalTime()) -Format O)
    }

    Write-Host "Search time:" -ForegroundColor Green
    Write-Host " From: $(Get-Date -Date $($(Get-Date -Date $fromDate).ToUniversalTime()) -Format "dddd, MMMM dd, yyyy HH:mm:ss.fff")"
    Write-Host " To: $(Get-Date -Date $($(Get-Date -Date $toDate).ToUniversalTime()) -Format "dddd, MMMM dd, yyyy HH:mm:ss.fff")"
   
    #Building DV query
    $QueryToRun = ""
    foreach ($Key in $PSBoundParameters.Keys)
    {
        switch -Exact ($Key)
        {
            "Query" { $QueryToRun = " AND "+$query; Break }
            "Sha256" {$QueryToRun += " AND Sha256 ContainsCIS `""+$Sha256+"`""; Break }
            "Sha1" {$QueryToRun += " AND Sha1 ContainsCIS `""+$Sha1+"`""; Break }
            "Md5" {$QueryToRun += " AND Md5 ContainsCIS `""+$Md5+"`""; Break }
            "FilePath" {$QueryToRun += " AND FilePath ContainsCIS `""+$FilePath+"`""; Break }
            "IP" {$QueryToRun += " AND IP ContainsCIS `""+$IP+"`""; Break }
            "DNS" {$QueryToRun += " AND DNS ContainsCIS `""+$DNS+"`""; Break }
            "Name" {$QueryToRun += " AND Name ContainsCIS `""+$Name+"`""; Break }
            "CmdLine" {$QueryToRun += " AND CmdLine ContainsCIS `""+$CmdLine+"`""; Break }
            "UserName" {$QueryToRun += " AND UserName ContainsCIS `""+$UserName+"`""; Break }
            "EndpointName" {$QueryToRun += " AND EndpointName ContainsCIS `""+$EndpointName+"`""; Break }
            "ObjectType"  {$QueryToRun += " AND ObjectType = `""+$ObjectType+"`""; Break } 
            "EventType"  {$QueryToRun += " AND EventType = `""+$EventType+"`""; Break } 
            "DstPort"  {$QueryToRun += " AND DstPort = `""+$DstPort+"`""; Break } 
            Default {}
        }
    }
    $QueryToRun = $QueryToRun.Substring(5, $QueryToRun.Length-5)
    Write-Host "Completed query: " -NoNewline -ForegroundColor Green
    Write-Host $QueryToRun

    #Submitting queries first
    $submittedQueries = @{}
    
    $queryDetails = @{
        fromDate = $(Get-Date -Date $($(Get-Date -Date $fromDate).ToUniversalTime()) -Format O);
        toDate = $(Get-Date -Date $($(Get-Date -Date $toDate).ToUniversalTime()) -Format O);
        query = $QueryToRun;
        limit = $ResultSize;
        queryType = @("events");
        }
    $queryDetails = ConvertTo-Json -InputObject $queryDetails -Compress
    Write-Verbose $queryDetails
    Write-Host
    foreach ($Name in $APITokenName)
    {
        Write-Host "Submitting Deep Visibility query using API token $Name"
        $submittedQueries.Add($Name, $api.submitDVQuery($Name, $queryDetails))
    }

    #Getting status
    $FinishedStatus = @{}
    $submittedQueriesCount = $submittedQueries.Count
    $SuccessfulFetch = ""
    $Return = @()
    while ($submittedQueriesCount -ne 0)
    {
        foreach ($Key in $submittedQueries.Keys)
        {
            if ($FinishedStatus[$Key].responseState -ne "FINISHED")
            {
                $FinishedStatus[$Key] = $api.getQueryStatus($Key, $submittedQueries[$Key])
                if ($FinishedStatus[$Key].error.code -gt 1 )
                {
                    #DV query failed to execute.
                    #{"errors":[{"code":4000040,"detail":"Query execution failed, please re-run your query","title":"Bad Request"}]}
                    Write-Host "$($FinishedStatus[$Key].error.detail); Error code $($FinishedStatus[$Key].error.code)" -ForegroundColor Red
                    $submittedQueriesCount--
                    $SuccessfulFetch = $Key
                    Write-Host "Starting the same query again" -ForegroundColor Green
                    $Return += Get-S1DeepVisibility -APITokenName $Key -Query $QueryToRun -Earliest $Earliest -Latest $Latest
                }
                else {
                    write-host "Checking query with API token $Key. Completed $($FinishedStatus[$Key].progressStatus)%, status $($FinishedStatus[$Key].responseState)"    
                }
            }
            if ($FinishedStatus[$Key].responseState -eq "FINISHED")
            {
                $submittedQueriesCount--
                write-host "Query is ready for fetch with API token $Key" -ForegroundColor Green
                $Return += $API.GetQueryData($Key, $submittedQueries[$Key], $FetchSize)
                $SuccessfulFetch = $Key
            }
        }
        $submittedQueries.Remove($SuccessfulFetch)
    }
    return $Return
}

function Get-S1SitePolicy
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name", ValueFromPipelineByPropertyName)]
        [String] $APITokenName,

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $RetryIntervalSec = 5,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $MaximumRetryCount = 2,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias("id")]
        [String] $SiteId,

        [Parameter(ValueFromPipelineByPropertyName, DontShow)]
        [Alias("name")]
        [String] $SiteName
    )

    Begin
    {
        $API = [SentinelOne]::new($Path)
        $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)
        $SitePolicy = @()
    }
    Process
    {
        $API.CheckAPITokenName($APITokenName)
        if ($SitePolicy | Where-Object siteId -eq $SiteId)
        {
            #Site policy for this site has been already received
        }
        else
        {
            $SitePolicy += $api.GetS1SitePolicy($APITokenName, $SiteId, $SiteName)
        }
    }
    End
    {
        return $SitePolicy
    }
}

function Invoke-S1FileFetch
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name", ValueFromPipelineByPropertyName)]
        [String] $APITokenName,

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $RetryIntervalSec = 5,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $MaximumRetryCount = 2,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $DownloadTimeoutSec = 600,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias("id")]
        [String] $AgentID,

        [ValidateNotNullOrEmpty()]
        [String] $Password = "Password123",

        [Parameter(Mandatory)]
        [String[]] $File,

        [Switch] $SaveEmptyFetch
    )

    Begin
    {
        $API = [SentinelOne]::new($Path)
        $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)
        $FetchCollection = @()
    }
    Process
    {
        Write-Host "Requesting fetch from agent $AgentID using API token $APITokenName. " -NoNewline
        $API.CheckAPITokenName($APITokenName)
        $FetchTime = (Get-Date).ToUniversalTime()
        $FetchResult = $API.RequestFileFetch($APITokenName, $AgentID, $File, $Password)
        if ($FetchResult -eq $true)
        {
            Write-Host "Fetch submitted" -ForegroundColor Green
        }
        else
        {
            Write-Host "Fetch failed to submit" -ForegroundColor Red
        }
        $FetchRequest = @{AgentID = $AgentID; APITokenName = $APITokenName; FetchTime = $FetchTime; FetchResult = $FetchResult; Downloaded = $false; FetchID = ""; ComputerName = ""; ScopeName = ""; SiteName = ""; SavedAs = ""}
        $FetchCollection += $FetchRequest
    }
    End
    {
        if ($FetchCollection.Count -eq 0)
        {
            Write-Host "No agents to fetch from (pipe input is empty)" -ForegroundColor Red
            return
        }
        Start-Sleep 3
        #Getting submission activity page per APITokenName
        Write-Host "Getting activity fetch logs..."
        foreach ($SuccessfullAPIToken in ($FetchCollection | Where-Object FetchResult -eq $true | Select-Object APITokenName | Get-Unique -AsString))
        {
            $ActivityPage += $api.RequestFileFetchActivityPage($SuccessfullAPIToken.APITokenName, 81)
        }
        #Getting fetch ID for all
        foreach ($FetchRequest in ($FetchCollection | Where-Object FetchResult -eq $true))
        {
            $ActivityEvent = $ActivityPage | Where-Object {($_.agentId -eq $FetchRequest.AgentID) -and ($(Get-Date -Date $_.createdAt) -gt $FetchRequest.FetchTime) -and ($_.APITokenName -eq $FetchRequest.APITokenName)}
            if ($ActivityEvent.Count -eq 1)
            {
                $FetchRequest.FetchID = $ActivityEvent.data.commandBatchUuid
                $FetchRequest.ComputerName = $ActivityEvent.data.computerName
                $FetchRequest.ScopeName = $ActivityEvent.data.scopeName
                $FetchRequest.SiteName = $ActivityEvent.data.siteName
            }
            else
            {
                Write-host "Multiple fetch events found for computer $($ActivityEvent.data.computerName | Select-Object -First 1)" -ForegroundColor Red
            }
        }
        #Trying to download submissions
        $StopTime = (Get-Date).AddSeconds($DownloadTimeoutSec)
        $AllDownloaded = $false
        Write-Host "Getting activity download logs..."
        while ($(Get-Date) -le $StopTime -and $AllDownloaded -eq $false)
        {
            #Count remaining files to download
            $RemainToDownload = ($FetchCollection | Where-Object {($_.FetchID -ne "") -and ($_.Downloaded -eq $false)}) | Measure-Object
            if ($RemainToDownload.Count -eq 0)
            {
                $AllDownloaded = $true
                break
            }
            Write-Host "$($RemainToDownload.Count) file(s) left to download. Waiting for file(s) upload..."
            Start-Sleep 5
            $ActivityPage = @()
            #Getting submission activity page per APITokenName
            foreach ($SuccessfullAPIToken in ($FetchCollection | Where-Object FetchResult -eq $true | Select-Object APITokenName | Get-Unique -AsString))
            {
                $ActivityPage += $API.RequestFileFetchActivityPage($SuccessfullAPIToken.APITokenName, 80)
            }
            #Downloading avaiable fetches
            foreach ($FetchRequest in ($FetchCollection | Where-Object {($_.FetchID -ne "") -and ($_.Downloaded -eq $false)}))
            {
                $ActivityEvent = $ActivityPage.data | Where-Object {$_.commandBatchUuid -eq $FetchRequest.FetchID}
                if ($ActivityEvent.Count -eq 1)
                {
                    $FetchRequest.Downloaded = $true
                    if ($API.RequestFileFetchDownload($FetchRequest.APITokenName, $ActivityEvent.downloadUrl, $ActivityEvent.filename, $SaveEmptyFetch) -eq $true)
                    {
                        $FetchRequest.SavedAs = $ActivityEvent.filename + ".zip"
                    }
                    else
                    {
                        $FetchRequest.SavedAs = "Not saved"
                    }
                }
            }
        }
        $FetchCollection | Select-Object APITokenName, SiteName, ScopeName, ComputerName, Downloaded, SavedAs | Format-Table
        if($(Get-Date) -ge $StopTime)
        {
            Write-Host "Fetch timed out, most likely some agents are offline now." -ForegroundColor Red
        }
        Write-Host "Reminder: Password for fetched zip files: `"$Password`""
    }
}

function Get-S1Site
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")]
        [String[]] $APITokenName,

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $RetryIntervalSec = 5,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $MaximumRetryCount = 2,

        [Parameter()]
        [Switch] $IncludeDeletedSites,

        [Parameter()]
        [String] $SiteId
        )

    Begin
    {
        $API = [SentinelOne]::new($Path)
        $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)
        $Sites = @()
    }
    Process
    {
        foreach ($APIToken in $APITokenName)
        {
            $API.CheckAPITokenName($APIToken)
            $Sites += $api.GetS1Sites($APIToken)
        }
    }
    End
    {
        if ($IncludeDeletedSites)
        {
            if ($SiteId)
            {
                return $Sites | Where-Object id -eq $SiteId
            }
            else
            {
                return $Sites    
            }
        }
        else
        {
            if ($SiteId)
            {
                return $Sites | Where-Object state -ne deleted | Where-Object id -eq $SiteId
            }
            else
            {
                return $Sites | Where-Object state -ne deleted
            }
        }
    }
}

function Get-S1Exclusion
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")]
        [String[]] $APITokenName,

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $RetryIntervalSec = 5,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $MaximumRetryCount = 2,

        [Parameter(Mandatory)]
        [ValidateSet("path", "white_hash", "browser", "certificate", "file_type")]
        [String] $Type,

        [Parameter()]
        [Switch] $IncludeDeletedSites
    )

        $API = [SentinelOne]::new($Path)
        $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)
        $Exclusions = @()

        foreach ($APIToken in $APITokenName)
        {
            $API.CheckAPITokenName($APITokenName)

            #Get Global exclusions
            $Exclusions += $api.GetS1Exclusions($APIToken, $Type, "Global", "", "", "", "", "", "")

            if ($IncludeDeletedSites)
            {
                $Sites = Get-S1Site -APITokenName $APIToken -Path $Path -RetryIntervalSec $RetryIntervalSec -MaximumRetryCount $MaximumRetryCount -IncludeDeletedSites
            }
            else
            {
                $Sites = Get-S1Site -APITokenName $APIToken -Path $Path -RetryIntervalSec $RetryIntervalSec -MaximumRetryCount $MaximumRetryCount
            }
            #Get account exclusions
            foreach ($Account in $Sites | Select-Object accountId, accountName | Get-Unique)
            {
                $Exclusions += $api.GetS1Exclusions($APIToken, $Type, "Account", $Account.accountId, $Account.accountName, "", "", "", "")
            }

            #Get Site exclusions
            foreach ($Site in $Sites)
            {
                $Exclusions += $api.GetS1Exclusions($APIToken, $Type, "Site", $Site.accountId, $Site.accountName, $Site.id, $Site.name, "", "")
            }
            #Get Site exclusions
            $Groups = $Sites | Get-S1Group -Path $Path -RetryIntervalSec $RetryIntervalSec -MaximumRetryCount $MaximumRetryCount
            
            foreach ($Group in $Groups)
            {
                $Exclusions += $api.GetS1Exclusions($APIToken, $Type, "Group", $($sites | Where-Object id -eq $Group.siteId).accountId, $($sites | Where-Object id -eq $Group.siteName).accountId, $Group.siteId, $Group.siteName, $Group.id, $Group.name)
            }
            #Get Group exclusions
        }

        return $Exclusions | Select-Object -ExcludeProperty scope

}

function Get-S1Group
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name", ValueFromPipelineByPropertyName)]
        [String] $APITokenName,

        [Parameter(HelpMessage="Full path to encrypted file to load API token")]
        [ValidateNotNullOrEmpty()]
        [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $RetryIntervalSec = 5,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Int] $MaximumRetryCount = 2,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias("id")]
        [String] $SiteId,

        [Parameter(ValueFromPipelineByPropertyName, DontShow)]
        [String] $name
    )

    Begin
    {
        $API = [SentinelOne]::new($Path)
        $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount)
        $Groups = @()
    }
    Process
    {
        $API.CheckAPITokenName($APITokenName)
        if ($Groups | Where-Object siteId -eq $SiteId)
        {
            #Groups for this site has been already received
        }
        else
        {
            $Groups += $api.GetS1Groups($APITokenName, $SiteId, $name)
        }
    }
    End
    {
        return $Groups
    }
}

Export-ModuleMember -Function Add-S1APIToken, Get-S1APIToken, Remove-S1APIToken, Get-S1Agent, Get-S1DeepVisibility, Invoke-S1FileFetch, Get-S1SitePolicy, Get-S1Site, Get-S1Group, Get-S1Exclusion