├── LICENSE ├── README.md └── SentinelOne.ps1 /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vladimir Radchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell module for SentinelOne 2 | 3 | This module provides basic PowerShell cmdlets to work with [SentinelOne](https://www.sentinelone.com/) API functions 4 | 5 | ## Installation 6 | 7 | ### Prerequisites 8 | SentinelOne module for PowerShell requires PowerShell version > 7.0. Check your Powershell version (`$PSVersionTable.PSVersion`) and download > 7.0 from [PowerShell](https://github.com/PowerShell/PowerShell) GitHub page if your Major is < 7. 9 | 10 | ### Installation 11 | Install SentinelOne module from Powershell: `Install-Module -Name SentinelOne` 12 | Alternatively, download the script `Invoke-WebRequest -Uri https://raw.githubusercontent.com/vradchenko/PowerShell-SentinelOne/main/SentinelOne.ps1 -OutFile SentinelOne.ps1` file and import into PowerShell session `. ./SentinelOne.ps1` 13 | 14 | ## Supported cmdlets 15 | - [Add-S1APIToken](#Add-S1APIToken) 16 | - [Invoke-S1FileFetch](#Invoke-S1FileFetch) 17 | - [Get-S1Agent](#Get-S1Agent) 18 | - [Get-S1APIToken](#Get-S1APIToken) 19 | - [Get-S1DeepVisibility](#Get-S1DeepVisibility) 20 | - [Get-S1Site](#Get-S1Site) 21 | - [Get-S1SitePolicy](#Get-S1SitePolicy) 22 | - [Get-S1Group](#Get-S1Group) 23 | - [Get-S1Exclusion](#Get-S1Exclusion) 24 | - [Remove-S1APIToken](#Remove-S1APIToken) 25 | 26 | ### Add-S1APIToken 27 | Prerequisites for all other cmdlets to function is to add at least one API token. Token(s) will be stored by default in a user's profile folder (`$env:APPDATA`) in SentinelOneAPI.token file. Before saving API token is encrypted using .NET [System.Security.Cryptography.ProtectedData](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=dotnet-plat-ext-5.0) class using [CurrentUser](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotectionscope?view=dotnet-plat-ext-5.0) data protection scope which means that only threads running under the current user context can unprotect the data. API token is never written to a disk in an unprotected format. 28 | #### Parameters 29 | |Parameter|Required|Description| 30 | |--|--|--| 31 | |APIToken|Yes|Secret API token generated with SentinelOne console, a string of 80 chars| 32 | |Endpoint|Yes|SentinelOne console URL, e.g. https://contoso.sentinelone.net| 33 | |APITokenName|Yes|Shortcut to the API token, will be referenced in all other cmdlets, e.g MyKey1| 34 | |Description|No|Any text you'd like to save along with the token, if not provided a current date will be used| 35 | |Path|No|A full path to the encrypted file where a token will be saved. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 36 | |DoNotValidateToken|No|Switch to disable token validation before saving. If not provided, validation happens by executing /web/api/v2.1/users/api-token-details| 37 | #### Examples 38 | `Add-S1APIToken -APIToken FeA1KIEfKZE3nYog4dafQfcMg7kTqwktKRrRjKUt99U4rkLz1KZrW7dOLFjsLUgOprzT2bsCc41qbRPv -APITokenName MyKey1 -Endpoint https://contoso.sentinelone.net` 39 | #### Output 40 | A console message indicating whenever a token was added succesfully or not. 41 | #### Final notes 42 | You can add as many tokens as you want (e.g. tokens with different scope or tokens from different consoles). 43 | You cannot modify existing tokens or add tokens with the same name. If any changes are necessary to the existing tokens you need to delete it with [`Remove-S1APIKey`](#Remove-S1APIToken) first. 44 | 45 | ### Invoke-S1FileFetch 46 | Fetches files from an agent. This cmdlet accepts pipe from [Get-S1Agent](#Get-S1Agent) and will fetch same file(S) from all agents returned by Get-S1Agent. 47 | #### Parameters 48 | |Parameter|Required|Description| 49 | |--|--|--| 50 | |APITokenName|Yes|Name of the API token(s) to perform API request| 51 | |Path|No|A full path to the encrypted file from where a token will be read. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 52 | |RetryIntervalSec|No|Specifies the interval between retries for the connection when a failure code between 400 and 599, inclusive or 304 is received; default is 5| 53 | |MaximumRetryCount|No|Specifies how many times PowerShell retries a connection when a failure code between 400 and 599, inclusive or 304 is received; default is 2| 54 | |AgentID|Yes|Agent ID to fetch file from| 55 | |File|Yes|Comma-separated files to fetch from the agent. Example: "C:\Windows\notepad.exe", "C:\Users\Public\Documents\flag.txt"| 56 | |Password|No|SentinelOne will encrypt ZIP file with this password. If not provided default password is "Password123"| 57 | |SaveEmptyFetch|No|If requested file(s) are not available on the agent, SentinelOne returns empty ZIP archive and it will not be saved on a disk. Use this switch to force file saving even when it is empty| 58 | |DownloadTimeoutSec|No|File fetch requires agent to be online with the console. This parameter speficies for how many seconds to wait for the fetch upload. Default value is 300 (5 min) and it should be enough to fetch a file from an online agent. Cmdlet stops waiting for file fetch upload once this timer expires (however this does not cancel the fetch request and eventually fetch file will be uploaded to the console when agent gets online). Increase this parameter on slow networks or when fetching files from a big number of agents (using pipe from Get-S1Agent)| 59 | #### Examples 60 | `Invoke-S1FileFetch -APITokenName MyKey1 -AgentID 987623279592853912 -File "C:\windows\UpdateLog.txt", "C:\Program Files\Microsoft\config.xml"` 61 | 62 | `Get-S1Agent -APITokenName MyKey1 -ResultSize All -ComputerNameContains DESKTOP | Invoke-S1FileFetch -File "C:\windows\UpdateLog.txt", "C:\Program Files\Microsoft\config.xml" -SaveEmptyFetch` 63 | 64 | `Get-S1Agent -APITokenName MyKey1 -ResultSize 10 -OSTypes linux | Invoke-S1FileFetch -File "/etc/passwd"` - Gets /etc/passwd file from up to 10 Linux agents 65 | 66 | #### Output 67 | Console messages showing fetching progress. Once fetching is finished or expired, an object with a fetch summary is returned (filenames, agent names, status). Fetched files are always saved in the current PoweShell script folder. 68 | 69 | ### Get-S1Agent 70 | Get the agents and their data, that match the filter. This command also returns the Agent ID, which is a required attribute for other cmdlets (e.g. for [`Invoke-S1FileFetch`](#Invoke-S1FileFetch)). 71 | #### Parameters 72 | |Parameter|Required|Description| 73 | |--|--|--| 74 | |APITokenName|Yes|Name of the API token(s) to perform API request| 75 | |Path|No|A full path to the encrypted file from where a token will be read. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 76 | |RetryIntervalSec|No|Specifies the interval between retries for the connection when a failure code between 400 and 599, inclusive or 304 is received; default is 5| 77 | |MaximumRetryCount|No|Specifies how many times PowerShell retries a connection when a failure code between 400 and 599, inclusive or 304 is received; default is 2| 78 | |ResultSize|No|Number of agents to return, default is 1000, use -ResultSize All to get all agents| 79 | #### Filter parameters 80 | All filter parameters are optional, if nothing is provided Get-S1Agent gets all registered agents. 81 | |Filter parameter|Description| 82 | |--|--| 83 | |ComputerNameContains|Comma-separated hostnames, e.g. DESKTOP, HOST| 84 | |OSTypes|Comma-separated OS types from a set of 4: windows, linux, macos, windows_legacy, e.g. windows, macos| 85 | |AgentVersions|Agent versions to include, e.g. 2.0.0.0,2.1.5.144| 86 | |IsActive|Include only active Agents, $true or $false| 87 | |IsInfected|Include only agents with at least one active threat, $true of $false| 88 | |IsUpToDate|Include only agents with updated software, $true of $false| 89 | |IsPendingUninstall|Include agents with pending uninstall requests, $true or $false| 90 | |NumberOfActiveThreatsEqualTo|Include Agents with this number of active threats| 91 | |NumberOfActiveThreatsGreaterThan|Include Agents with at least this number of active threats| 92 | |ScanStatus|Scan status, one from 4 : finished, aborted, started, none| 93 | |MachineTypes|Comma-separated machine types from a set of 5: "kubernetes node", desktop, laptop, server, unknown| 94 | |NetworkStatuses|Comma-separated agents network statutes from a set of 4: connected, connecting, disconnected, disconnecting| 95 | |UserActionsNeeded|Include agents with pending user actions, press 'Tab' to list possible values. Example: reboot_needed, upgrade_needed| 96 | |AgentDomains|Comma-separated agent domain names. Example: contoso.org,lab.dev, workgroup| 97 | #### Examples 98 | `Get-S1Agent -APITokenName MyKey1` returns first 1000 agents from the console 99 | 100 | `Get-S1Agent -APITokenName MyKey1 -ResultSize All -ComputerNameContains DESKTOP` 101 | 102 | `Get-S1Agent -APITokenName MyKey1 -ResultSize 500 -ScanStatus finished -IsInfected $true -OSTypes windows, linux` 103 | 104 | `Get-S1Agent -APITokenName MyKey1 -ResultSize All -MachineTypes server -AgentDomains contoso.org` 105 | 106 | #### Output 107 | Array of objects representing agents that match the filter. 108 | #### Final notes 109 | There are more agent filters available from the SentinelOne API, however they are not so common so I decided not to implement them. Make an issue if you need other filters! 110 | 111 | ### Get-S1APIToken 112 | Lists and gets details of all currently saved API tokens 113 | #### Parameters 114 | |Parameter|Required|Description| 115 | |--|--|--| 116 | |APITokenName|No|API token name to get details for, e.g. MyKey1. If not provided all tokens will be displayed (equals to `-APITokenName *`)| 117 | |Path|No|A full path to the encrypted file from where a token will be read. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 118 | |ValidateAPIToken|No|Switch to validate saved API key. Validation happens by executing /web/api/v2.1/users/api-token-details. Default is not to validate| 119 | |UnmaskAPIToken|No|Switch to show full API token on the screen. If not provided displayed token(s) will be masked| 120 | #### Examples 121 | `Get-S1APIToken -APITokenName MyKey1 -ValidateAPIToken` 122 | 123 | `Get-S1APIToken -APITokenName *` 124 | 125 | `Get-S1APIToken -UnmaskAPIToken` 126 | #### Output 127 | An object showing details of the saved token(s). If ValidateAPIToken is set, a token expiration date is shown. 128 | 129 | ### Get-S1DeepVisibility 130 | Submits Deep Visibility query and fetches results. 131 | #### Parameters 132 | |Parameter|Required|Description| 133 | |--|--|--| 134 | |APITokenName|Yes|Name of the API token(s) to perform API request| 135 | |Path|No|A full path to the encrypted file from where a token will be read. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 136 | |RetryIntervalSec|No|Specifies the interval between retries for the connection when a failure code between 400 and 599, inclusive or 304 is received; default is 5| 137 | |MaximumRetryCount|No|Specifies how many times PowerShell retries a connection when a failure code between 400 and 599, inclusive or 304 is received; default is 2| 138 | |ResultSize|No|Number of events to retrieve, default is 1000, maximum is 20000| 139 | |FetchSize|No|Number of events to retrieve per API call, default is 500, maximum is 1000. Higher number can get results quicker, however timeouts from the API are possible| 140 | |Earliest|Yes|Specifies date of the earliest event to retrieve. Time can be relative or fixed. Relative modifiers are "d" - days, "h" - hours, "m" - minutes, e.g. "-3d" - 3 days ago, "-12h" - 12 hours ago. Fixed time can be specified in flexible formats like "2021-01-10 13:18:21.500", "1/19/2021 3:59:13.000 PM". Keep in mind this is your local time and it will be converted to UTC before submitting the query. To explicilty specify UTC time you need to add "Z" at the end e.g. "2021-01-10 13:18:21.500 Z". 141 | |Latest|No|Specifies data of the latest event to retrieve. If not provided then current time is used. Same as with Earliest time, Latest can be relative or fixed, for more details about the format see Earliest description. Latest should be greater or equal to Earliest.| 142 | |Query|Yes*|Specifies full Deep Visibility query, the same way as it looks in the SentinelOne console e.g. -Query 'SrcProcCmdLine RegExp "schtasks" AND SrcProcParentName != "Manages scheduled tasks"'. Always use ' for string determination since " are used in the query itself. Use Query parameter for advanced, manually created and verified queries.| 143 | |EndpointName, Sha256, Sha1, Md5, FilePath, IP, DstPort, DNS, Name, CmdLine, UserName|Yes*|These are simplified query parameters. Either Query or at least one simplified parameter must be provided. You cannot combine Query with simplified parameters. All simplified parameters will be combined using "AND", and evaluated as "ContainsCIS", e.g. -CmdLine "svchost" -DstPort 445 will be submitted as CmdLine ContainsCIS "svchost" AND DstPort ContainsCIS "445"| 144 | |ObjectType|Yes*|Additional filter to narrow down event to a certain event type, one from "ip", "dns", "process", "cross_process", "indicators", "file", "registry", "scheduled_task", "url", "command_script", "logins".| 145 | |EventType|Yes*|Additional filter to narrow down the search even further, by specifying certain evnts like "File Creation", "Registry Value Modified" or "Task Register". Full list is availalbe with auto completion in the script| 146 | #### Examples 147 | `Get-S1DeepVisibility -APITokenName MyKey1 -Earliest -24h -Query 'SrcProcCmdLine RegExp "schtasks" AND SrcProcParentName != "Manages scheduled tasks"` 148 | 149 | `Get-S1DeepVisibility -APITokenName MyKey1 -Earliest -7d -Latest -6d -EndpointName DESKTOP-RC4DWK -ObjectType dns` 150 | 151 | `Get-S1DeepVisibility -APITokenName MyKey1 -Earliest "2021-01-10 13:18:21.500" -Latest -180m -EventType "Task Start"` 152 | 153 | `Get-S1DeepVisibility -APITokenName MyKey1 -Earliest -90d IP "192.168.0.1" -DstPort 80 -CmdLine chrome` 154 | #### Output 155 | Console messages showing progress of request and an array of objects containing Deep Visibility events. 156 | #### Final notes 157 | Overall cmdlet process is: first query is submitted, then script waits for query to finish and then fetches the results. Sometimes submitted queries cannot be completed (Deep Visibility timeout) - in this case script will throw an error when all http retries are used. Fetching the maximum set (20000 events) can take a while, always save cmdlete results to a variable like $events = Get-S1DeepVisibility ... unless you're expecting only a few results (or no results). 158 | 159 | ### Get-S1Site 160 | Get all sites. 161 | #### Parameters 162 | |Parameter|Required|Description| 163 | |--|--|--| 164 | |APITokenName|Yes|Name of the API token(s) to perform API request| 165 | |Path|No|A full path to the encrypted file from where a token will be read. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 166 | |RetryIntervalSec|No|Specifies the interval between retries for the connection when a failure code between 400 and 599, inclusive or 304 is received; default is 5| 167 | |MaximumRetryCount|No|Specifies how many times PowerShell retries a connection when a failure code between 400 and 599, inclusive or 304 is received; default is 2| 168 | |SiteId|No|Numerical site ID, if not provided then all sites are returned| 169 | |IncludeDeletedSites|No|Switch to get deleted sites as well| 170 | #### Examples 171 | `Get-S1Site -APITokenName MyKey1 -SiteId 987654321123456789` 172 | 173 | `Get-S1Site -APITokenName MyKey1` 174 | #### Output 175 | Array of objects representing site setting for a given site ID, or for all sites. 176 | 177 | ### Get-S1SitePolicy 178 | Get site policy settings from a siteID. This cmdlet accepts pipe from [Get-S1Agent](#Get-S1Agent). 179 | #### Parameters 180 | |Parameter|Required|Description| 181 | |--|--|--| 182 | |APITokenName|Yes|Name of the API token(s) to perform API request| 183 | |Path|No|A full path to the encrypted file from where a token will be read. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 184 | |RetryIntervalSec|No|Specifies the interval between retries for the connection when a failure code between 400 and 599, inclusive or 304 is received; default is 5| 185 | |MaximumRetryCount|No|Specifies how many times PowerShell retries a connection when a failure code between 400 and 599, inclusive or 304 is received; default is 2| 186 | |SiteId|Yes|Unique site ID to get policy settings for| 187 | #### Examples 188 | `Get-S1SitePolicy -APITokenName MyKey1 -SiteId 987654321123456789` 189 | 190 | `Get-S1Agent -APITokenName MyKey1 -ResultSize 250 -ComputerNameContains DESKTOP | Get-S1SitePolicy` This will get policy settings for all sites where all agents from the first cmdlet are located. 191 | 192 | `Get-S1Agent -APITokenName MyKey1 -ResultSize All | Get-S1SitePolicy | Select-Object mitigationMode` This will show mitigationMode settings from all policies applies to all agents. 193 | #### Output 194 | Array of objects representing policy settings for a given site ID. 195 | #### Final notes 196 | Piping from Get-S1Agent or Get-S1Site is the easiest way to use this cmdlet, else you need to provide a numerical siteID. 197 | 198 | ### Get-S1Group 199 | Get all groups from a given site. 200 | #### Parameters 201 | |Parameter|Required|Description| 202 | |--|--|--| 203 | |APITokenName|Yes|Name of the API token(s) to perform API request| 204 | |Path|No|A full path to the encrypted file from where a token will be read. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 205 | |RetryIntervalSec|No|Specifies the interval between retries for the connection when a failure code between 400 and 599, inclusive or 304 is received; default is 5| 206 | |MaximumRetryCount|No|Specifies how many times PowerShell retries a connection when a failure code between 400 and 599, inclusive or 304 is received; default is 2| 207 | |SiteId|Yes|Numerical site ID to get groups from| 208 | #### Examples 209 | `Get-S1Group -APITokenName MyKey1 -SiteId 987654321123456789` 210 | 211 | `Get-S1Site -APITokenName MyKey1 | Get-S1Group` 212 | 213 | `Get-S1Agent -APITokenName MyKey1 -ComputerNameContains XYZ -ResultSize 100 | Get-S1Group` 214 | #### Output 215 | Array of objects representing group settings. 216 | #### Final notes 217 | Piping from Get-S1Agent or Get-S1Site is the easiest way to use this cmdlet, else you need to provide a numerical siteID. 218 | 219 | ### Get-S1Exclusion 220 | Get all exclusions of given type from all scopes (Gloval, Account, Site, Group). 221 | #### Parameters 222 | |Parameter|Required|Description| 223 | |--|--|--| 224 | |APITokenName|Yes|Name of the API token(s) to perform API request| 225 | |Path|No|A full path to the encrypted file from where a token will be read. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 226 | |RetryIntervalSec|No|Specifies the interval between retries for the connection when a failure code between 400 and 599, inclusive or 304 is received; default is 5| 227 | |MaximumRetryCount|No|Specifies how many times PowerShell retries a connection when a failure code between 400 and 599, inclusive or 304 is received; default is 2| 228 | |IncludeDeletedSites|No|Switch to get exclusions from deleted sites as well| 229 | |Type|Yes|Type of the exclusion, one of: path, white_hash, browser, certificate, file_type| 230 | #### Examples 231 | `Get-S1Exclusion -APITokenName MyKey1 -Type path` 232 | 233 | `Get-S1Exclusion -APITokenName MyKey1 -Type white_hash -IncludeDeletedSites | Select-Object exceptionScope, value, createdAt` 234 | #### Output 235 | Array of objects representing all exclusions from all scopes. ExceptionScope field identifies scope of the exception. AccountName, siteName and groupName field shows exception exact location. 236 | 237 | 238 | ### Remove-S1APIToken 239 | Removes currently saved API tokens 240 | #### Parameters 241 | |Parameter|Required|Description| 242 | |--|--|--| 243 | |APITokenName|Yes|API token name to remove, e.g. MyKey1)| 244 | |Path|No|A full path to the encrypted file from where a token will be removed. If not provided, a default AppData folder and SentinelOneAPI.token filename will be used| 245 | #### Examples 246 | `Remove-S1APIToken -APITokenName MyKey1` 247 | #### Output 248 | No output when removed successfully. 249 | -------------------------------------------------------------------------------- /SentinelOne.ps1: -------------------------------------------------------------------------------- 1 | #This module requires Powershell 7 or higher 2 | #Requires -Version 7.0 3 | 4 | class SentinelOne 5 | { 6 | [Hashtable]$APITokens = @{} 7 | [String]$Path 8 | [Datetime]$GetDate 9 | [Int]$RetryIntervalSec = 2 10 | [Int]$MaximumRetryCount = 2 11 | 12 | #API endpoints 13 | [Hashtable]$APIEndpoints = @{ 14 | ApiTokenDetails = @{Method = "POST"; URI = "web/api/v2.1/users/api-token-details"}; 15 | GetAgents = @{Method = "GET"; URI = "web/api/v2.1/agents"}; 16 | CreateQueryAndGetQueryid = @{Method = "POST"; URI = "web/api/v2.1/dv/init-query"}; 17 | GetQueryStatus = @{Method = "GET"; URI = "web/api/v2.1/dv/query-status?queryId="}; 18 | GetEvents = @{Method = "GET"; URI = "web/api/v2.1/dv/events?sortBy=createdAt&queryId="}; 19 | GetActivities = @{Method = "GET"; URI = "web/api/v2.1/activities?sortBy=createdAt&sortOrder=desc&limit=1000&activityTypes="}; 20 | FetchFiles = @{Method = "POST"; URI = "web/api/v2.1/agents/{agent_id}/actions/fetch-files"}; 21 | GetSites = @{Method = "GET"; URI = "/web/api/v2.1/sites?limit=1000"}; 22 | GetGroups = @{Method = "GET"; URI = "/web/api/v2.1/groups?limit=200&siteIds="}; 23 | GetExclusions = @{Method = "GET"; URI = "/web/api/v2.1/exclusions?limit=1000&type="}; 24 | SitePolicy = @{Method = "GET"; URI = "web/api/v2.1/sites/{site_id}/policy"}} 25 | 26 | SentinelOne($Path) 27 | { 28 | $this.Path = $Path 29 | $this.ReadAPITokens() 30 | $this.GetDate = Get-Date 31 | } 32 | 33 | [PSObject] MakeHTTPRequest($APITokenName, $RequestName, $Parameters) 34 | { 35 | $Headers = @{Authorization = "APIToken $($this.APITokens.$APITokenName.APIToken)"} 36 | $URI = $this.APITokens.$APITokenName.Endpoint + $this.APIEndpoints.$RequestName.URI 37 | switch ($RequestName) 38 | { 39 | "GetAgents" { $URI += $Parameters[0]; break} 40 | "GetQueryStatus" { $URI += $Parameters[0]; break} 41 | "GetEvents" { $URI += $Parameters[0] + "&cursor=" + $Parameters[1] + "&limit=" + $Parameters[2]; break} 42 | "FetchFiles" { $URI = $URI.Replace("{agent_id}", $Parameters[1]); break} 43 | "GetActivities" { $URI += $Parameters[0]; break} 44 | "GetGroups" { $URI += $Parameters[0]; break} 45 | "SitePolicy" { $URI = $URI.Replace("{site_id}", $Parameters[0]); break} 46 | "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} 47 | Default {} 48 | } 49 | 50 | if ($this.APIEndpoints.$RequestName.Method -eq "GET") 51 | { 52 | $httpError = "" 53 | try 54 | { 55 | $return = Invoke-RestMethod -Uri $URI -Method GET -Headers $Headers -RetryIntervalSec $this.RetryIntervalSec -MaximumRetryCount $this.MaximumRetryCount -ContentType "application/json" 56 | } 57 | catch 58 | { 59 | try 60 | { 61 | $httpError = $_ 62 | $return = $httpError.ErrorDetails.Message | ConvertFrom-Json 63 | } 64 | catch 65 | { 66 | $return = $httpError 67 | } 68 | } 69 | } 70 | else 71 | { 72 | $httpError = "" 73 | try 74 | { 75 | #$Parameters[0] should be a POST body, JSON formatted 76 | $return = Invoke-RestMethod -Uri $URI -Method POST -Headers $Headers -RetryIntervalSec $this.RetryIntervalSec -MaximumRetryCount $this.MaximumRetryCount -ContentType "application/json" -Body $Parameters[0] 77 | } 78 | catch 79 | { 80 | try 81 | { 82 | $httpError = $_ 83 | $return = $httpError.ErrorDetails.Message | ConvertFrom-Json 84 | } 85 | catch 86 | { 87 | $return = $httpError 88 | } 89 | } 90 | } 91 | return $return 92 | } 93 | 94 | [String] ValidateAPIToken($APITokenName, $ThrowIfInvalid) 95 | { 96 | $Body = ConvertTo-Json -Compress -InputObject $(@{data = @{apiToken = $this.APITokens.$APITokenName.APIToken}}) 97 | $Http = $this.MakeHTTPRequest($APITokenName, "ApiTokenDetails", @($Body)) 98 | if ($Http.data.expiresAt) 99 | { 100 | $this.APITokens.$APITokenName.ExpiresAt = $Http.data.expiresAt 101 | return "True" 102 | } 103 | else 104 | { 105 | if ($ThrowIfInvalid) 106 | { 107 | throw "Failed to verify API token. Please check Endpoint, APIToken and network connection to the console." 108 | } 109 | else 110 | { 111 | return $Http 112 | } 113 | } 114 | } 115 | 116 | [Void] SaveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 117 | { 118 | $this.RetryIntervalSec = $RetryIntervalSec 119 | $this.MaximumRetryCount = $MaximumRetryCount 120 | } 121 | 122 | [Bool] Hidden ReadAPITokens() 123 | { 124 | try 125 | { 126 | $read = [System.IO.File]::ReadAllBytes($this.Path) 127 | $read = [System.Security.Cryptography.ProtectedData]::Unprotect($read, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser) 128 | $read = [System.Text.Encoding]::Unicode.GetString($read) 129 | $read = ConvertFrom-Json -InputObject $read -AsHashtable 130 | } 131 | catch 132 | { 133 | return $false 134 | } 135 | $this.APITokens = $read 136 | return $true 137 | } 138 | 139 | [Bool] Hidden WriteAPITokens() 140 | { 141 | try 142 | { 143 | $write = ConvertTo-Json -InputObject $this.APITokens -Compress 144 | $write = [System.Text.Encoding]::Unicode.GetBytes($write) 145 | $write = [System.Security.Cryptography.ProtectedData]::Protect($write, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser) 146 | [System.IO.File]::WriteAllBytes($this.Path, $write) 147 | } 148 | catch 149 | { 150 | throw "Cannot encrypt and/or save API tokens to $($this.Path)" 151 | } 152 | return $true 153 | } 154 | 155 | [Bool] AddAPIToken($APIToken, $Endpoint, $APITokenName, $Description, $DoNotValidateToken) 156 | { 157 | #Ensuring API token name is not * 158 | if ($APITokenName -eq "*") 159 | { 160 | throw "Name cannot be equal to *" 161 | } 162 | #Ensuring Endpoint URL contains / at the end 163 | if ($Endpoint -notmatch "/$") 164 | { 165 | $Endpoint += "/" 166 | } 167 | #Ensuring endpoing looks like a SentineOne URL 168 | if ($Endpoint -notmatch "^https://[\w\S]+\.sentinelone.net/$") 169 | { 170 | throw "Wrong Endpoint provided. Proper format is e.g. https://contoso.sentinelone.net/" 171 | } 172 | 173 | if ($this.APITokens.ContainsKey($APITokenName)) 174 | { 175 | throw "Saved API tokens already contain a token with name $APITokenName. Remove existing token with `"Remove-S1APIToken -Name $APITokenName`"" 176 | } 177 | 178 | $this.APITokens.Add($APITokenName, @{"APIToken" = $APIToken; "Endpoint" = $Endpoint; "Description" = $Description}) 179 | 180 | if ($DoNotValidateToken -eq $false) 181 | { 182 | $this.ValidateAPIToken($APITokenName, $true) 183 | } 184 | 185 | $this.WriteAPITokens() 186 | return $true 187 | } 188 | 189 | [Void] RemoveAPIToken($APITokenName) 190 | { 191 | if ($APITokenName -eq "*") 192 | { 193 | throw "API token name cannot be equal to *" 194 | } 195 | if (!$this.APITokens.ContainsKey($APITokenName)) 196 | { 197 | throw "No saved API token with name $APITokenName." 198 | } 199 | $this.APITokens.Remove($APITokenName) 200 | $this.writeAPITokens() 201 | } 202 | 203 | [String] Hidden PrepareGetFilter($Parameters) 204 | { 205 | $filter = "" 206 | 207 | foreach ($Key in $Parameters.Keys) 208 | { 209 | switch -Exact ($Key) 210 | { 211 | "ComputerNameContains" { $filter += "&computerName__contains="+$($Parameters[$Key] -join ","); Break } 212 | "OSTypes" { $filter += "&osTypes="+$($Parameters[$Key] -join ","); Break } 213 | "AgentVersions" { $filter += "&agentVersions="+$($Parameters[$Key] -join ","); Break } 214 | "IsActive" { $filter += "&isActive="+$($Parameters[$Key]); Break } 215 | "IsInfected" { $filter += "&infected="+$($Parameters[$Key]); Break } 216 | "IsUpToDate" { $filter += "&isUpToDate="+$($Parameters[$Key]); Break } 217 | "NumberOfActiveThreatsEqualTo" { $filter += "&activeThreats="+$($Parameters[$Key]); Break } 218 | "NumberOfActiveThreatsGreaterThan" { $filter += "&activeThreats__gt="+$($Parameters[$Key]); Break } 219 | "ScanStatus" { $filter += "&scanStatus="+$($Parameters[$Key]); Break } 220 | "MachineTypes" { $filter += "&machineTypes="+$($Parameters[$Key] -join ","); Break } 221 | "UserActionsNeeded" { $filter += "&userActionsNeeded="+$($Parameters[$Key] -join ","); Break } 222 | "NetworkStatuses" { $filter += "&networkStatuses="+$($Parameters[$Key] -join ","); Break } 223 | "AgentDomains" { $filter += "&domains="+$($Parameters[$Key] -join ","); Break } 224 | "IsPendingUninstall" { $filter += "&isPendingUninstall="+$($Parameters[$Key]); Break } 225 | "IsDecommissioned" { $filter += "&isDecommissioned="+$($Parameters[$Key]); Break } 226 | Default {} 227 | } 228 | } 229 | return $filter 230 | } 231 | 232 | [PSObject] GetAgents($APITokenName, $ResultSize, $Parameters) 233 | { 234 | $Return = @() 235 | $GetAll = $false 236 | if ($ResultSize -eq "All") 237 | { 238 | $ResultSize = 1000 239 | $GetAll = $true 240 | } 241 | $Filter = "?$($this.PrepareGetFilter($Parameters))&limit=$ResultSize" 242 | $FilterCursor = $Filter 243 | $TotalAgents = 0 244 | Do 245 | { 246 | $Http = $this.MakeHTTPRequest($APITokenName, "GetAgents", @($FilterCursor)) 247 | if ($Http.errors) 248 | { 249 | Write-Host "Error code: $($Http.errors.code)" 250 | Write-Host "Error detail: $($Http.errors.detail)" 251 | Write-Host "Error title: $($Http.errors.title)" 252 | throw "Error while running Get-S1Agent" 253 | } 254 | $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty 255 | $Return += $Http.data 256 | $FilterCursor = $Filter + "&cursor=$($Http.pagination.nextCursor)" 257 | if ($Http.pagination.totalItems -gt 0) 258 | { 259 | $TotalAgents = $Http.pagination.totalItems 260 | } 261 | if ($TotalAgents -gt 1) 262 | { 263 | Write-Host "Completed $($Return.Count) agents from $TotalAgents using API token $APITokenName..." 264 | } 265 | } While ($GetAll -and $null -ne $Http.pagination.nextCursor) 266 | return $Return 267 | } 268 | 269 | [Void] CheckAPITokenName($APITokenNames) 270 | { 271 | foreach ($APITokenName in $APITokenNames) 272 | { 273 | if(!$this.APITokens.ContainsKey($APITokenName)) 274 | { 275 | throw "No saved API token with name $APITokenName" 276 | } 277 | } 278 | } 279 | 280 | [Datetime] parseRange($range) 281 | { 282 | #Relative range 283 | if ($range -match "^-\d+[hmd]$") 284 | { 285 | $number = [int]((Select-String -InputObject $range -Pattern "\d+").Matches.Value) 286 | switch ((Select-String -InputObject $range -Pattern "[mhd]").Matches.Value) 287 | { 288 | "m" { return $this.getDate.AddMinutes($number*-1) } 289 | "h" { return $this.getDate.AddMinutes($number*60*-1) } 290 | "d" { return $this.getDate.AddMinutes($number*60*24*-1) } 291 | Default {throw "Error parsing range"} 292 | } 293 | } 294 | else 295 | { 296 | try 297 | { 298 | $date = Get-Date -Date $range 299 | } 300 | catch 301 | { 302 | throw "Cannot parse date" 303 | } 304 | return $date 305 | } 306 | return $this.getDate 307 | } 308 | 309 | [String] submitDVQuery($APITokenName, $Query) 310 | { 311 | $Http = $this.MakeHTTPRequest($APITokenName, "CreateQueryAndGetQueryid", @($Query)) 312 | if ($Http.errors) 313 | { 314 | Write-Host "Error code: $($Http.errors.code)" 315 | Write-Host "Error detail: $($Http.errors.detail)" 316 | Write-Host "Error title: $($Http.errors.title)" 317 | throw "Error while running Get-S1Agent" 318 | } 319 | $QueryId = $Http.data.queryId 320 | if ($QueryId -match "q[a-f0-9]{32}") 321 | { 322 | return $QueryId 323 | } 324 | else 325 | { 326 | throw "DeepVisibility query submission failed." 327 | } 328 | } 329 | 330 | [Hashtable] getQueryStatus($APITokenName, $QueryID) 331 | { 332 | Start-Sleep 3 333 | $Http = $this.MakeHTTPRequest($APITokenName, "GetQueryStatus", @($QueryID)) 334 | return @{progressStatus = $Http.data.progressStatus; responseState = $Http.data.responseState; error = $Http.errors} 335 | } 336 | 337 | [Bool] RequestFileFetch($APITokenName, $AgentID, $File, $Password) 338 | { 339 | $PostBody = @{data = @{files = $File; password = $Password}} 340 | $PostBody = ConvertTo-Json -Compress -InputObject $PostBody 341 | $Http = $this.MakeHTTPRequest($APITokenName, "FetchFiles", @($PostBody, $AgentID)) 342 | if ($Http.data.success -eq $true) 343 | { 344 | return $true 345 | } 346 | return $false 347 | } 348 | 349 | [PSObject] RequestFileFetchActivityPage($APITokenName, $Code) 350 | { 351 | $Http = $this.MakeHTTPRequest($APITokenName, "GetActivities", @($Code)) 352 | $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty 353 | return $Http.data 354 | } 355 | 356 | [Bool] RequestFileFetchDownload($APITokenName, $DownloadUrl, $Filename, $SaveEmptyFetch) 357 | { 358 | $URI = $this.APITokens[$APITokenName].Endpoint + "web/api/v2.1" + $DownloadUrl 359 | $OutFile = $(Get-Location).Path + "\" + $Filename + ".zip" 360 | $ZipFileFetch = Invoke-WebRequest -Uri $URI -Method GET -Headers @{Authorization = "APIToken "+$this.APITokens[$APITokenName].APIToken} -RetryIntervalSec $this.RetryIntervalSec -MaximumRetryCount $this.MaximumRetryCount 361 | if ($ZipFileFetch.RawContentLength -gt 5000 -or $SaveEmptyFetch) 362 | { 363 | #ZIP file is not empty because of its size - no need to unpack in memory. Just saving. 364 | #SaveEmptyFetch was set. Just saving. 365 | [System.IO.File]::WriteAllBytes($OutFile, $ZipFileFetch.Content) 366 | Write-Host "File saved to $OutFile" -ForegroundColor Green 367 | return $true 368 | } 369 | else 370 | { 371 | [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression") 372 | $ZipStream = New-Object System.IO.Memorystream 373 | $ZipStream.Write($ZipFileFetch.Content,0,$ZipFileFetch.Content.Length) 374 | $ZipFile = [System.IO.Compression.ZipArchive]::new($ZipStream) 375 | if ($ZipFile.Entries.Count -gt 1) 376 | { 377 | [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression") 378 | $ZipFileFetch.Content | Set-Content -Path $OutFile -AsByteStream 379 | Write-Host "File saved to $OutFile" -ForegroundColor Green 380 | return $true 381 | } 382 | else 383 | { 384 | Write-Host "$Filename.zip was fetched, but appear to be empty. Not saving." -ForegroundColor Red 385 | return $false 386 | } 387 | } 388 | } 389 | 390 | [PSObject] GetS1SitePolicy($APITokenName, $SiteId, $SiteName) 391 | { 392 | $Http = $this.MakeHTTPRequest($APITokenName, "SitePolicy", @($SiteId)) 393 | $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty 394 | $Http.data | Add-Member -Value $SiteName -Name "siteName" -MemberType NoteProperty 395 | $Http.data | Add-Member -Value $SiteId -Name "siteId" -MemberType NoteProperty 396 | return $Http.data 397 | } 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | [PSObject] GetS1Groups($APITokenName, $SiteId, $SiteName) 411 | { 412 | $Http = $this.MakeHTTPRequest($APITokenName, "GetGroups", @($SiteId)) 413 | $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty 414 | $Http.data | Add-Member -Value $SiteName -Name "siteName" -MemberType NoteProperty 415 | $Http.data | Add-Member -Value $SiteId -Name "siteId" -MemberType NoteProperty 416 | return $Http.data 417 | } 418 | 419 | 420 | [PSObject] GetS1Exclusions($APITokenName, $Type, $Scope, $AccountId, $AccountName, $SiteId, $SiteName, $GroupId, $GroupName) 421 | { 422 | $Return = @() 423 | $NextCursor = "" 424 | if($Scope -eq "Account") 425 | { 426 | $SiteId = $AccountId 427 | } 428 | Do 429 | { 430 | $Http = $this.MakeHTTPRequest($APITokenName, "GetExclusions", @($Scope, $Type, $SiteId, $GroupId, $NextCursor)) 431 | $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty 432 | $Return += $Http.data 433 | $NextCursor = $Http.pagination.nextCursor 434 | 435 | if ($Scope -eq "Global") 436 | { 437 | $Http.data | Add-Member -Value "" -Name "accountName" -MemberType NoteProperty 438 | $Http.data | Add-Member -Value "" -Name "accountId" -MemberType NoteProperty 439 | } 440 | else 441 | { 442 | $Http.data | Add-Member -Value $AccountName -Name "accountName" -MemberType NoteProperty 443 | $Http.data | Add-Member -Value $AccountId -Name "accountId" -MemberType NoteProperty 444 | } 445 | if ($Scope -eq "Account") 446 | { 447 | $Http.data | Add-Member -Value "Account" -Name "exceptionScope" -MemberType NoteProperty 448 | } 449 | elseif ($Scope -eq "Site") 450 | { 451 | $Http.data | Add-Member -Value $SiteName -Name "siteName" -MemberType NoteProperty 452 | $Http.data | Add-Member -Value $SiteId -Name "siteId" -MemberType NoteProperty 453 | $Http.data | Add-Member -Value "" -Name "groupName" -MemberType NoteProperty 454 | $Http.data | Add-Member -Value "" -Name "groupId" -MemberType NoteProperty 455 | $Http.data | Add-Member -Value "Site" -Name "exceptionScope" -MemberType NoteProperty 456 | } 457 | elseif ($Scope -eq "Group") 458 | { 459 | $Http.data | Add-Member -Value $SiteName -Name "siteName" -MemberType NoteProperty 460 | $Http.data | Add-Member -Value $SiteId -Name "siteId" -MemberType NoteProperty 461 | $Http.data | Add-Member -Value $groupName -Name "groupName" -MemberType NoteProperty 462 | $Http.data | Add-Member -Value $groupId -Name "groupId" -MemberType NoteProperty 463 | $Http.data | Add-Member -Value "Group" -Name "exceptionScope" -MemberType NoteProperty 464 | } 465 | elseif ($Scope -eq "Global") 466 | { 467 | $Http.data | Add-Member -Value "" -Name "siteName" -MemberType NoteProperty 468 | $Http.data | Add-Member -Value "" -Name "siteId" -MemberType NoteProperty 469 | $Http.data | Add-Member -Value "" -Name "groupName" -MemberType NoteProperty 470 | $Http.data | Add-Member -Value "" -Name "groupId" -MemberType NoteProperty 471 | $Http.data | Add-Member -Value "Global" -Name "exceptionScope" -MemberType NoteProperty 472 | } 473 | } While ($null -ne $Http.pagination.nextCursor) 474 | return $Return 475 | } 476 | 477 | [PSObject] GetS1Sites($APITokenName) 478 | { 479 | $Http = $this.MakeHTTPRequest($APITokenName, "GetSites", @()) 480 | $Http.data.sites | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty 481 | return $Http.data.sites 482 | } 483 | 484 | [PSObject] GetQueryData($APITokenName, $QueryID, $FetchSize) 485 | { 486 | $Return = @() 487 | $NextCursor = "" 488 | $TotalItems = 0 489 | 490 | Do 491 | { 492 | $Http = $this.MakeHTTPRequest($APITokenName, "GetEvents", @($QueryID, $NextCursor, $FetchSize)) 493 | $Http.data | Add-Member -Value $APITokenName -Name "APITokenName" -MemberType NoteProperty 494 | $Return += $Http.data | Select-Object -ExcludeProperty attributes 495 | $NextCursor = $Http.pagination.nextCursor 496 | if ($Http.pagination.totalItems -gt 0 -and $TotalItems -eq 0) 497 | { 498 | $TotalItems = $Http.pagination.totalItems 499 | } 500 | if ($TotalItems -gt 0) 501 | { 502 | Write-Host "Fetched $($Return.Count) Deep Visibility events from total $TotalItems using API token $APITokenName..." 503 | } 504 | } While ($null -ne $Http.pagination.nextCursor) 505 | return $Return 506 | } 507 | } 508 | 509 | function Add-S1APIToken 510 | { 511 | [CmdletBinding()] 512 | Param( 513 | 514 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")] 515 | [String] $APITokenName, 516 | 517 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token")] 518 | [ValidateLength(80,80)] 519 | [String] $APIToken, 520 | 521 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token endpoint URL (e.g. https://contoso.sentinelone.net/)")] 522 | [String] $Endpoint, 523 | 524 | [Parameter(HelpMessage="You can provide and save comments to the API token")] 525 | [String] $Description = $("API token added $(Get-Date)"), 526 | 527 | [Parameter(HelpMessage="Full path to encrypted file to save API token")] 528 | [ValidateNotNullOrEmpty()] 529 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 530 | 531 | [Switch] $DoNotValidateToken 532 | ) 533 | 534 | $API = [SentinelOne]::new($Path) 535 | 536 | if ($API.AddAPIToken($APIToken, $Endpoint, $APITokenName, $Description, $DoNotValidateToken) -eq $true) 537 | { 538 | Write-Host "API token `"$APITokenName`" added successfully." -ForegroundColor Green 539 | } 540 | else 541 | { 542 | Write-Host "Failed to add API token `"$APITokenName`"." -ForegroundColor Red 543 | } 544 | } 545 | 546 | function Get-S1APIToken 547 | { 548 | [CmdletBinding()] 549 | Param( 550 | [Parameter(HelpMessage="Enter SentinelOne API token name")] 551 | [ValidateNotNullOrEmpty()] 552 | [String] $APITokenName = "*", 553 | 554 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 555 | [ValidateNotNullOrEmpty()] 556 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 557 | 558 | [Parameter()] 559 | [ValidateRange(1,10)] 560 | [Int] $RetryIntervalSec = 1, 561 | 562 | [Parameter()] 563 | [ValidateRange(1,10)] 564 | [Int] $MaximumRetryCount = 2, 565 | 566 | [Switch] $ValidateAPIToken, 567 | [Switch] $UnmaskAPIToken 568 | ) 569 | 570 | $API = [SentinelOne]::new($Path) 571 | $API.SaveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 572 | $Tokens = @() 573 | 574 | foreach ($Name in $API.APITokens.Keys) 575 | { 576 | $APITokenHashTable = [Ordered]@{APITokenName = $Name; Endpoint = $API.APITokens.$Name.Endpoint; Description = $API.APITokens.$Name.Description; APIToken = ($API.APITokens.$Name.APIToken.Substring(0,5)+"*"*75)} 577 | if ($UnmaskAPIToken) 578 | { 579 | $APITokenHashTable.APIToken = $API.APITokens.$Name.APIToken 580 | } 581 | if ($ValidateAPIToken -and ($Name -eq $APITokenName -or $APITokenName -eq "*")) 582 | { 583 | $APITokenHashTable.IsValid = $API.ValidateAPIToken($Name, $false) 584 | $APITokenHashTable.ExpiresAt = $API.APITokens.$Name.ExpiresAt 585 | } 586 | $Tokens += [PSCustomObject]$APITokenHashTable 587 | } 588 | if ($APITokenName -eq "*") 589 | { 590 | return $Tokens 591 | } 592 | else 593 | { 594 | return ($Tokens | Where-Object APITokenName -eq $APITokenName) 595 | } 596 | 597 | } 598 | 599 | function Remove-S1APIToken 600 | { 601 | [CmdletBinding()] 602 | Param( 603 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")] 604 | [String] $APITokenName, 605 | 606 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 607 | [ValidateNotNullOrEmpty()] 608 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token") 609 | ) 610 | 611 | $API = [SentinelOne]::new($Path) 612 | $API.RemoveAPIToken($APITokenName) 613 | } 614 | 615 | function Get-S1Agent 616 | { 617 | [CmdletBinding(PositionalBinding = $false)] 618 | Param( 619 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")] 620 | [String[]] $APITokenName, 621 | 622 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 623 | [ValidateNotNullOrEmpty()] 624 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 625 | 626 | [Parameter()] 627 | [ValidateNotNullOrEmpty()] 628 | [ValidateScript({$_ -eq "All" -or ([Int]::Parse($_) -ge 1 -and [int]::Parse($_) -le 1000)})] 629 | [String] $ResultSize = "1000", 630 | 631 | [Parameter()] 632 | [ValidateNotNullOrEmpty()] 633 | [Int] $RetryIntervalSec = 5, 634 | 635 | [Parameter()] 636 | [ValidateNotNullOrEmpty()] 637 | [Int] $MaximumRetryCount = 2, 638 | 639 | #Get-S1Agents filters 640 | [Parameter()] 641 | [ValidateNotNullOrEmpty()] 642 | [String[]] $ComputerNameContains, 643 | 644 | [Parameter()] 645 | [ValidateSet("linux","macos","windows", "windows_legacy")] 646 | [String[]] $OSTypes, 647 | 648 | [Parameter()] 649 | [ValidateNotNullOrEmpty()] 650 | [String[]] $AgentVersions, 651 | 652 | [Parameter()] 653 | [ValidateNotNullOrEmpty()] 654 | [Bool] $IsActive, 655 | 656 | [Parameter()] 657 | [ValidateNotNullOrEmpty()] 658 | [Bool] $IsInfected, 659 | 660 | [Parameter()] 661 | [ValidateNotNullOrEmpty()] 662 | [Bool] $IsUpToDate, 663 | 664 | [Parameter()] 665 | [ValidateNotNullOrEmpty()] 666 | [Int] $NumberOfActiveThreatsEqualTo, 667 | 668 | [Parameter()] 669 | [ValidateNotNullOrEmpty()] 670 | [Int] $NumberOfActiveThreatsGreaterThan, 671 | 672 | [Parameter()] 673 | [ValidateSet("finished","aborted","started", "none")] 674 | [String] $ScanStatus, 675 | 676 | [Parameter()] 677 | [ValidateSet("kubernetes node","desktop","laptop", "server", "unknown")] 678 | [String[]] $MachineTypes, 679 | 680 | [Parameter()] 681 | [ValidateSet("agent_suppressed_category", "incompatible_os", "incompatible_os_category", "missing_permissions_category", "none", "reboot_category", "reboot_needed", 682 | "unprotected", "unprotected_category", "upgrade_needed", "user_action_needed", "user_action_needed_fda", "user_action_needed_network", "user_action_needed_rs_fda")] 683 | [String[]] $UserActionsNeeded, 684 | 685 | [Parameter()] 686 | [ValidateSet("connected", "connecting", "disconnected", "disconnecting")] 687 | [String[]] $NetworkStatuses, 688 | 689 | [Parameter()] 690 | [ValidateNotNullOrEmpty()] 691 | [String[]] $AgentDomains, 692 | 693 | [Parameter()] 694 | [ValidateNotNullOrEmpty()] 695 | [Bool] $IsPendingUninstall, 696 | 697 | [Parameter()] 698 | [ValidateNotNullOrEmpty()] 699 | [Bool] $IsDecommissioned 700 | ) 701 | 702 | $API = [SentinelOne]::new($Path) 703 | $API.CheckAPITokenName($APITokenName) 704 | $API.SaveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 705 | $Return = @() 706 | foreach ($Name in $APITokenName) 707 | { 708 | $Return += $API.GetAgents($Name, $ResultSize, $PSBoundParameters) 709 | } 710 | return $Return 711 | } 712 | 713 | function Get-S1DeepVisibility 714 | { 715 | [CmdletBinding()] 716 | Param( 717 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")] 718 | [String[]] $APITokenName, 719 | 720 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 721 | [ValidateNotNullOrEmpty()] 722 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 723 | 724 | [Parameter()] 725 | [ValidateNotNullOrEmpty()] 726 | [ValidateScript({[Int]::Parse($_) -ge 1 -and [int]::Parse($_) -le 20000})] 727 | [String] $ResultSize = "1000", 728 | 729 | [ValidateNotNullOrEmpty()] 730 | [ValidateRange(1,1000)] 731 | [Int] $FetchSize = 500, 732 | 733 | [ValidateNotNullOrEmpty()] 734 | [Int] $RetryIntervalSec = 5, 735 | 736 | [ValidateNotNullOrEmpty()] 737 | [Int] $MaximumRetryCount = 36, 738 | 739 | [Parameter(HelpMessage="Enter Deep Visibility search query", ParameterSetName="Advanced")] 740 | [ValidateNotNullOrEmpty()] 741 | [String] $Query, 742 | 743 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $EndpointName, 744 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $Sha256, 745 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $Sha1, 746 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $Md5, 747 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $FilePath, 748 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $IP, 749 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $DNS, 750 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $Name, 751 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $CmdLine, 752 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $UserName, 753 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()][String] $DstPort, 754 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()] 755 | [ValidateSet("ip", "dns", "process", "cross_process", "indicators", "file", "registry", "scheduled_task", "url", "command_script", "logins")] 756 | [String] $ObjectType, 757 | 758 | [Parameter(ParameterSetName="Simple")][ValidateNotNullOrEmpty()] 759 | [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")] 760 | [String] $EventType, 761 | 762 | [Parameter(Mandatory, HelpMessage="Enter Deep Visibility search range")] 763 | [String] $Earliest, 764 | 765 | [Parameter(HelpMessage="Enter Deep Visibility search range")] 766 | [ValidateNotNullOrEmpty()] 767 | [String] $Latest 768 | ) 769 | 770 | $API = [SentinelOne]::new($Path) 771 | $API.CheckAPITokenName($APITokenName) 772 | $API.SaveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 773 | 774 | $fromDate = $API.ParseRange($Earliest) 775 | $fromDate = $(Get-Date -Date $($(Get-Date -Date $fromDate).ToUniversalTime()) -Format O) 776 | 777 | if($PSBoundParameters.ContainsKey("Latest")) 778 | { 779 | $toDate = $API.parseRange($Latest) 780 | $toDate = $(Get-Date -Date $($(Get-Date -Date $toDate).ToUniversalTime()) -Format O) 781 | if ($fromDate -gt $toDate) 782 | { 783 | throw "Latest is before Earliest!" 784 | } 785 | } 786 | else 787 | { 788 | $Latest = $(Get-Date) 789 | $toDate = $(Get-Date -Date $($(Get-Date -Date $Latest).ToUniversalTime()) -Format O) 790 | } 791 | 792 | Write-Host "Search time:" -ForegroundColor Green 793 | Write-Host " From: $(Get-Date -Date $($(Get-Date -Date $fromDate).ToUniversalTime()) -Format "dddd, MMMM dd, yyyy HH:mm:ss.fff")" 794 | Write-Host " To: $(Get-Date -Date $($(Get-Date -Date $toDate).ToUniversalTime()) -Format "dddd, MMMM dd, yyyy HH:mm:ss.fff")" 795 | 796 | #Building DV query 797 | $QueryToRun = "" 798 | foreach ($Key in $PSBoundParameters.Keys) 799 | { 800 | switch -Exact ($Key) 801 | { 802 | "Query" { $QueryToRun = " AND "+$query; Break } 803 | "Sha256" {$QueryToRun += " AND Sha256 ContainsCIS `""+$Sha256+"`""; Break } 804 | "Sha1" {$QueryToRun += " AND Sha1 ContainsCIS `""+$Sha1+"`""; Break } 805 | "Md5" {$QueryToRun += " AND Md5 ContainsCIS `""+$Md5+"`""; Break } 806 | "FilePath" {$QueryToRun += " AND FilePath ContainsCIS `""+$FilePath+"`""; Break } 807 | "IP" {$QueryToRun += " AND IP ContainsCIS `""+$IP+"`""; Break } 808 | "DNS" {$QueryToRun += " AND DNS ContainsCIS `""+$DNS+"`""; Break } 809 | "Name" {$QueryToRun += " AND Name ContainsCIS `""+$Name+"`""; Break } 810 | "CmdLine" {$QueryToRun += " AND CmdLine ContainsCIS `""+$CmdLine+"`""; Break } 811 | "UserName" {$QueryToRun += " AND UserName ContainsCIS `""+$UserName+"`""; Break } 812 | "EndpointName" {$QueryToRun += " AND EndpointName ContainsCIS `""+$EndpointName+"`""; Break } 813 | "ObjectType" {$QueryToRun += " AND ObjectType = `""+$ObjectType+"`""; Break } 814 | "EventType" {$QueryToRun += " AND EventType = `""+$EventType+"`""; Break } 815 | "DstPort" {$QueryToRun += " AND DstPort = `""+$DstPort+"`""; Break } 816 | Default {} 817 | } 818 | } 819 | $QueryToRun = $QueryToRun.Substring(5, $QueryToRun.Length-5) 820 | Write-Host "Completed query: " -NoNewline -ForegroundColor Green 821 | Write-Host $QueryToRun 822 | 823 | #Submitting queries first 824 | $submittedQueries = @{} 825 | 826 | $queryDetails = @{ 827 | fromDate = $(Get-Date -Date $($(Get-Date -Date $fromDate).ToUniversalTime()) -Format O); 828 | toDate = $(Get-Date -Date $($(Get-Date -Date $toDate).ToUniversalTime()) -Format O); 829 | query = $QueryToRun; 830 | limit = $ResultSize; 831 | queryType = @("events"); 832 | } 833 | $queryDetails = ConvertTo-Json -InputObject $queryDetails -Compress 834 | Write-Verbose $queryDetails 835 | Write-Host 836 | foreach ($Name in $APITokenName) 837 | { 838 | Write-Host "Submitting Deep Visibility query using API token $Name" 839 | $submittedQueries.Add($Name, $api.submitDVQuery($Name, $queryDetails)) 840 | } 841 | 842 | #Getting status 843 | $FinishedStatus = @{} 844 | $submittedQueriesCount = $submittedQueries.Count 845 | $SuccessfulFetch = "" 846 | $Return = @() 847 | while ($submittedQueriesCount -ne 0) 848 | { 849 | foreach ($Key in $submittedQueries.Keys) 850 | { 851 | if ($FinishedStatus[$Key].responseState -ne "FINISHED") 852 | { 853 | $FinishedStatus[$Key] = $api.getQueryStatus($Key, $submittedQueries[$Key]) 854 | if ($FinishedStatus[$Key].error.code -gt 1 ) 855 | { 856 | #DV query failed to execute. 857 | #{"errors":[{"code":4000040,"detail":"Query execution failed, please re-run your query","title":"Bad Request"}]} 858 | Write-Host "$($FinishedStatus[$Key].error.detail); Error code $($FinishedStatus[$Key].error.code)" -ForegroundColor Red 859 | $submittedQueriesCount-- 860 | $SuccessfulFetch = $Key 861 | Write-Host "Starting the same query again" -ForegroundColor Green 862 | $Return += Get-S1DeepVisibility -APITokenName $Key -Query $QueryToRun -Earliest $Earliest -Latest $Latest 863 | } 864 | else { 865 | write-host "Checking query with API token $Key. Completed $($FinishedStatus[$Key].progressStatus)%, status $($FinishedStatus[$Key].responseState)" 866 | } 867 | } 868 | if ($FinishedStatus[$Key].responseState -eq "FINISHED") 869 | { 870 | $submittedQueriesCount-- 871 | write-host "Query is ready for fetch with API token $Key" -ForegroundColor Green 872 | $Return += $API.GetQueryData($Key, $submittedQueries[$Key], $FetchSize) 873 | $SuccessfulFetch = $Key 874 | } 875 | } 876 | $submittedQueries.Remove($SuccessfulFetch) 877 | } 878 | return $Return 879 | } 880 | 881 | function Get-S1SitePolicy 882 | { 883 | [CmdletBinding()] 884 | Param( 885 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name", ValueFromPipelineByPropertyName)] 886 | [String] $APITokenName, 887 | 888 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 889 | [ValidateNotNullOrEmpty()] 890 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 891 | 892 | [Parameter()] 893 | [ValidateNotNullOrEmpty()] 894 | [Int] $RetryIntervalSec = 5, 895 | 896 | [Parameter()] 897 | [ValidateNotNullOrEmpty()] 898 | [Int] $MaximumRetryCount = 2, 899 | 900 | [Parameter(Mandatory, ValueFromPipelineByPropertyName)] 901 | [Alias("id")] 902 | [String] $SiteId, 903 | 904 | [Parameter(ValueFromPipelineByPropertyName, DontShow)] 905 | [Alias("name")] 906 | [String] $SiteName 907 | ) 908 | 909 | Begin 910 | { 911 | $API = [SentinelOne]::new($Path) 912 | $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 913 | $SitePolicy = @() 914 | } 915 | Process 916 | { 917 | $API.CheckAPITokenName($APITokenName) 918 | if ($SitePolicy | Where-Object siteId -eq $SiteId) 919 | { 920 | #Site policy for this site has been already received 921 | } 922 | else 923 | { 924 | $SitePolicy += $api.GetS1SitePolicy($APITokenName, $SiteId, $SiteName) 925 | } 926 | } 927 | End 928 | { 929 | return $SitePolicy 930 | } 931 | } 932 | 933 | function Invoke-S1FileFetch 934 | { 935 | [CmdletBinding()] 936 | Param( 937 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name", ValueFromPipelineByPropertyName)] 938 | [String] $APITokenName, 939 | 940 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 941 | [ValidateNotNullOrEmpty()] 942 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 943 | 944 | [Parameter()] 945 | [ValidateNotNullOrEmpty()] 946 | [Int] $RetryIntervalSec = 5, 947 | 948 | [Parameter()] 949 | [ValidateNotNullOrEmpty()] 950 | [Int] $MaximumRetryCount = 2, 951 | 952 | [Parameter()] 953 | [ValidateNotNullOrEmpty()] 954 | [Int] $DownloadTimeoutSec = 600, 955 | 956 | [Parameter(Mandatory, ValueFromPipelineByPropertyName)] 957 | [Alias("id")] 958 | [String] $AgentID, 959 | 960 | [ValidateNotNullOrEmpty()] 961 | [String] $Password = "Password123", 962 | 963 | [Parameter(Mandatory)] 964 | [String[]] $File, 965 | 966 | [Switch] $SaveEmptyFetch 967 | ) 968 | 969 | Begin 970 | { 971 | $API = [SentinelOne]::new($Path) 972 | $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 973 | $FetchCollection = @() 974 | } 975 | Process 976 | { 977 | Write-Host "Requesting fetch from agent $AgentID using API token $APITokenName. " -NoNewline 978 | $API.CheckAPITokenName($APITokenName) 979 | $FetchTime = (Get-Date).ToUniversalTime() 980 | $FetchResult = $API.RequestFileFetch($APITokenName, $AgentID, $File, $Password) 981 | if ($FetchResult -eq $true) 982 | { 983 | Write-Host "Fetch submitted" -ForegroundColor Green 984 | } 985 | else 986 | { 987 | Write-Host "Fetch failed to submit" -ForegroundColor Red 988 | } 989 | $FetchRequest = @{AgentID = $AgentID; APITokenName = $APITokenName; FetchTime = $FetchTime; FetchResult = $FetchResult; Downloaded = $false; FetchID = ""; ComputerName = ""; ScopeName = ""; SiteName = ""; SavedAs = ""} 990 | $FetchCollection += $FetchRequest 991 | } 992 | End 993 | { 994 | if ($FetchCollection.Count -eq 0) 995 | { 996 | Write-Host "No agents to fetch from (pipe input is empty)" -ForegroundColor Red 997 | return 998 | } 999 | Start-Sleep 3 1000 | #Getting submission activity page per APITokenName 1001 | Write-Host "Getting activity fetch logs..." 1002 | foreach ($SuccessfullAPIToken in ($FetchCollection | Where-Object FetchResult -eq $true | Select-Object APITokenName | Get-Unique -AsString)) 1003 | { 1004 | $ActivityPage += $api.RequestFileFetchActivityPage($SuccessfullAPIToken.APITokenName, 81) 1005 | } 1006 | #Getting fetch ID for all 1007 | foreach ($FetchRequest in ($FetchCollection | Where-Object FetchResult -eq $true)) 1008 | { 1009 | $ActivityEvent = $ActivityPage | Where-Object {($_.agentId -eq $FetchRequest.AgentID) -and ($(Get-Date -Date $_.createdAt) -gt $FetchRequest.FetchTime) -and ($_.APITokenName -eq $FetchRequest.APITokenName)} 1010 | if ($ActivityEvent.Count -eq 1) 1011 | { 1012 | $FetchRequest.FetchID = $ActivityEvent.data.commandBatchUuid 1013 | $FetchRequest.ComputerName = $ActivityEvent.data.computerName 1014 | $FetchRequest.ScopeName = $ActivityEvent.data.scopeName 1015 | $FetchRequest.SiteName = $ActivityEvent.data.siteName 1016 | } 1017 | else 1018 | { 1019 | Write-host "Multiple fetch events found for computer $($ActivityEvent.data.computerName | Select-Object -First 1)" -ForegroundColor Red 1020 | } 1021 | } 1022 | #Trying to download submissions 1023 | $StopTime = (Get-Date).AddSeconds($DownloadTimeoutSec) 1024 | $AllDownloaded = $false 1025 | Write-Host "Getting activity download logs..." 1026 | while ($(Get-Date) -le $StopTime -and $AllDownloaded -eq $false) 1027 | { 1028 | #Count remaining files to download 1029 | $RemainToDownload = ($FetchCollection | Where-Object {($_.FetchID -ne "") -and ($_.Downloaded -eq $false)}) | Measure-Object 1030 | if ($RemainToDownload.Count -eq 0) 1031 | { 1032 | $AllDownloaded = $true 1033 | break 1034 | } 1035 | Write-Host "$($RemainToDownload.Count) file(s) left to download. Waiting for file(s) upload..." 1036 | Start-Sleep 5 1037 | $ActivityPage = @() 1038 | #Getting submission activity page per APITokenName 1039 | foreach ($SuccessfullAPIToken in ($FetchCollection | Where-Object FetchResult -eq $true | Select-Object APITokenName | Get-Unique -AsString)) 1040 | { 1041 | $ActivityPage += $API.RequestFileFetchActivityPage($SuccessfullAPIToken.APITokenName, 80) 1042 | } 1043 | #Downloading avaiable fetches 1044 | foreach ($FetchRequest in ($FetchCollection | Where-Object {($_.FetchID -ne "") -and ($_.Downloaded -eq $false)})) 1045 | { 1046 | $ActivityEvent = $ActivityPage.data | Where-Object {$_.commandBatchUuid -eq $FetchRequest.FetchID} 1047 | if ($ActivityEvent.Count -eq 1) 1048 | { 1049 | $FetchRequest.Downloaded = $true 1050 | if ($API.RequestFileFetchDownload($FetchRequest.APITokenName, $ActivityEvent.downloadUrl, $ActivityEvent.filename, $SaveEmptyFetch) -eq $true) 1051 | { 1052 | $FetchRequest.SavedAs = $ActivityEvent.filename + ".zip" 1053 | } 1054 | else 1055 | { 1056 | $FetchRequest.SavedAs = "Not saved" 1057 | } 1058 | } 1059 | } 1060 | } 1061 | $FetchCollection | Select-Object APITokenName, SiteName, ScopeName, ComputerName, Downloaded, SavedAs | Format-Table 1062 | if($(Get-Date) -ge $StopTime) 1063 | { 1064 | Write-Host "Fetch timed out, most likely some agents are offline now." -ForegroundColor Red 1065 | } 1066 | Write-Host "Reminder: Password for fetched zip files: `"$Password`"" 1067 | } 1068 | } 1069 | 1070 | function Get-S1Site 1071 | { 1072 | [CmdletBinding()] 1073 | Param( 1074 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")] 1075 | [String[]] $APITokenName, 1076 | 1077 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 1078 | [ValidateNotNullOrEmpty()] 1079 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 1080 | 1081 | [Parameter()] 1082 | [ValidateNotNullOrEmpty()] 1083 | [Int] $RetryIntervalSec = 5, 1084 | 1085 | [Parameter()] 1086 | [ValidateNotNullOrEmpty()] 1087 | [Int] $MaximumRetryCount = 2, 1088 | 1089 | [Parameter()] 1090 | [Switch] $IncludeDeletedSites, 1091 | 1092 | [Parameter()] 1093 | [String] $SiteId 1094 | ) 1095 | 1096 | Begin 1097 | { 1098 | $API = [SentinelOne]::new($Path) 1099 | $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 1100 | $Sites = @() 1101 | } 1102 | Process 1103 | { 1104 | foreach ($APIToken in $APITokenName) 1105 | { 1106 | $API.CheckAPITokenName($APIToken) 1107 | $Sites += $api.GetS1Sites($APIToken) 1108 | } 1109 | } 1110 | End 1111 | { 1112 | if ($IncludeDeletedSites) 1113 | { 1114 | if ($SiteId) 1115 | { 1116 | return $Sites | Where-Object id -eq $SiteId 1117 | } 1118 | else 1119 | { 1120 | return $Sites 1121 | } 1122 | } 1123 | else 1124 | { 1125 | if ($SiteId) 1126 | { 1127 | return $Sites | Where-Object state -ne deleted | Where-Object id -eq $SiteId 1128 | } 1129 | else 1130 | { 1131 | return $Sites | Where-Object state -ne deleted 1132 | } 1133 | } 1134 | } 1135 | } 1136 | 1137 | function Get-S1Exclusion 1138 | { 1139 | [CmdletBinding()] 1140 | Param( 1141 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name")] 1142 | [String[]] $APITokenName, 1143 | 1144 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 1145 | [ValidateNotNullOrEmpty()] 1146 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 1147 | 1148 | [Parameter()] 1149 | [ValidateNotNullOrEmpty()] 1150 | [Int] $RetryIntervalSec = 5, 1151 | 1152 | [Parameter()] 1153 | [ValidateNotNullOrEmpty()] 1154 | [Int] $MaximumRetryCount = 2, 1155 | 1156 | [Parameter(Mandatory)] 1157 | [ValidateSet("path", "white_hash", "browser", "certificate", "file_type")] 1158 | [String] $Type, 1159 | 1160 | [Parameter()] 1161 | [Switch] $IncludeDeletedSites 1162 | ) 1163 | 1164 | $API = [SentinelOne]::new($Path) 1165 | $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 1166 | $Exclusions = @() 1167 | 1168 | foreach ($APIToken in $APITokenName) 1169 | { 1170 | $API.CheckAPITokenName($APITokenName) 1171 | 1172 | #Get Global exclusions 1173 | $Exclusions += $api.GetS1Exclusions($APIToken, $Type, "Global", "", "", "", "", "", "") 1174 | 1175 | if ($IncludeDeletedSites) 1176 | { 1177 | $Sites = Get-S1Site -APITokenName $APIToken -Path $Path -RetryIntervalSec $RetryIntervalSec -MaximumRetryCount $MaximumRetryCount -IncludeDeletedSites 1178 | } 1179 | else 1180 | { 1181 | $Sites = Get-S1Site -APITokenName $APIToken -Path $Path -RetryIntervalSec $RetryIntervalSec -MaximumRetryCount $MaximumRetryCount 1182 | } 1183 | #Get account exclusions 1184 | foreach ($Account in $Sites | Select-Object accountId, accountName | Get-Unique) 1185 | { 1186 | $Exclusions += $api.GetS1Exclusions($APIToken, $Type, "Account", $Account.accountId, $Account.accountName, "", "", "", "") 1187 | } 1188 | 1189 | #Get Site exclusions 1190 | foreach ($Site in $Sites) 1191 | { 1192 | $Exclusions += $api.GetS1Exclusions($APIToken, $Type, "Site", $Site.accountId, $Site.accountName, $Site.id, $Site.name, "", "") 1193 | } 1194 | #Get Site exclusions 1195 | $Groups = $Sites | Get-S1Group -Path $Path -RetryIntervalSec $RetryIntervalSec -MaximumRetryCount $MaximumRetryCount 1196 | 1197 | foreach ($Group in $Groups) 1198 | { 1199 | $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) 1200 | } 1201 | #Get Group exclusions 1202 | } 1203 | 1204 | return $Exclusions | Select-Object -ExcludeProperty scope 1205 | 1206 | } 1207 | 1208 | function Get-S1Group 1209 | { 1210 | [CmdletBinding()] 1211 | Param( 1212 | [Parameter(Mandatory, HelpMessage="Enter SentinelOne API token name", ValueFromPipelineByPropertyName)] 1213 | [String] $APITokenName, 1214 | 1215 | [Parameter(HelpMessage="Full path to encrypted file to load API token")] 1216 | [ValidateNotNullOrEmpty()] 1217 | [String] $Path = $(Join-Path $env:APPDATA "SentinelOneAPI.token"), 1218 | 1219 | [Parameter()] 1220 | [ValidateNotNullOrEmpty()] 1221 | [Int] $RetryIntervalSec = 5, 1222 | 1223 | [Parameter()] 1224 | [ValidateNotNullOrEmpty()] 1225 | [Int] $MaximumRetryCount = 2, 1226 | 1227 | [Parameter(Mandatory, ValueFromPipelineByPropertyName)] 1228 | [Alias("id")] 1229 | [String] $SiteId, 1230 | 1231 | [Parameter(ValueFromPipelineByPropertyName, DontShow)] 1232 | [String] $name 1233 | ) 1234 | 1235 | Begin 1236 | { 1237 | $API = [SentinelOne]::new($Path) 1238 | $API.saveHTTPRetryParameters($RetryIntervalSec, $MaximumRetryCount) 1239 | $Groups = @() 1240 | } 1241 | Process 1242 | { 1243 | $API.CheckAPITokenName($APITokenName) 1244 | if ($Groups | Where-Object siteId -eq $SiteId) 1245 | { 1246 | #Groups for this site has been already received 1247 | } 1248 | else 1249 | { 1250 | $Groups += $api.GetS1Groups($APITokenName, $SiteId, $name) 1251 | } 1252 | } 1253 | End 1254 | { 1255 | return $Groups 1256 | } 1257 | } 1258 | 1259 | --------------------------------------------------------------------------------