SCENARIO
The SentinelOne Management Console REST API enables automation of administrative tasks, reporting, threat response, and integration with existing security orchestration platforms. PowerShell provides a powerful scripting environment for Windows-centric organizations to automate SentinelOne operations without writing complex applications.
Use this method when:
- Automating daily operational tasks (reporting, threat triage, agent management)
- Integrating SentinelOne with SIEM, ticketing systems, or security orchestration platforms
- Building custom dashboards or executive reports
- Performing bulk operations (agent upgrades, policy changes, threat remediation)
- Scheduled automation via Task Scheduler or Azure Automation
- Custom compliance reporting or audit workflows
REQUIREMENTS & ASSUMPTIONS
Prerequisites:
- SentinelOne Management Console with API access enabled
- API token with appropriate permissions (Admin, SOC, or custom role)
- PowerShell 5.1+ (Windows) or PowerShell 7+ (cross-platform)
- Network connectivity from script execution host to SentinelOne console (HTTPS/443)
- Understanding of REST API concepts (GET, POST, PUT, DELETE methods)
- Familiarity with JSON data structures
Assumed Environment:
- PowerShell execution policy allows script execution (
Set-ExecutionPolicy RemoteSigned) - Internet proxy configured if required (via
$PSDefaultParameterValues) - Secure credential storage (Azure Key Vault, Windows Credential Manager, encrypted files)
- Logging directory available for script output (
C:\BIN\LOGS)
Required Information:
- SentinelOne Console URL:
https://yourtenant.sentinelone.net - API Token: Generated from console with appropriate permissions
- API Documentation: https://yourtenant.sentinelone.net/api-doc/overview
PROCESS
Step 1: Generate API token
- Log in to the SentinelOne Management Console
- Navigate to Settings → Users
- Locate your user account and click on it
- Under API Token, click Generate (or Regenerate if token exists)
- Copy the token immediately (it's only shown once)
- Store securely in password manager or secure credential storage
Token format:
- Length: Variable (typically 64+ characters)
- Format: Alphanumeric string
- Example:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...(JWT format)
Security best practices:
- Never hardcode tokens in scripts (use secure retrieval methods)
- Generate service account-specific tokens (not personal user tokens)
- Rotate tokens quarterly or per security policy
- Limit token permissions to minimum required (principle of least privilege)
- Store in Azure Key Vault, AWS Secrets Manager, or encrypted configuration files
Step 2: Set up PowerShell environment
Install required modules (optional but recommended):
# Install PowerShell Gallery modules for enhanced API interaction
Install-Module -Name PowerShellGet -Force -AllowClobber
Install-Module -Name PSFramework -Scope CurrentUser # Enhanced loggingCreate reusable API connection module:
# Save as: C:\Scripts\SentinelOne-API-Module.psm1
$script:S1ApiToken = $null
$script:S1ConsoleUrl = $null
function Connect-S1Api {
<#
.SYNOPSIS
Establishes connection parameters for SentinelOne API
.PARAMETER ApiToken
SentinelOne API token (securely retrieved)
.PARAMETER ConsoleUrl
SentinelOne console URL (e.g., https://yourtenant.sentinelone.net)
.EXAMPLE
$token = Get-Secret -Name "SentinelOne-API-Token" -AsPlainText
Connect-S1Api -ApiToken $token -ConsoleUrl "https://yourtenant.sentinelone.net"
#>
param(
[Parameter(Mandatory=$true)]
[string]$ApiToken,
[Parameter(Mandatory=$true)]
[ValidatePattern('^https://.*\.sentinelone\.net$')]
[string]$ConsoleUrl
)
$script:S1ApiToken = $ApiToken
$script:S1ConsoleUrl = $ConsoleUrl.TrimEnd('/')
Write-Host "[SUCCESS] Connected to SentinelOne API: $ConsoleUrl" -ForegroundColor Green
}
function Invoke-S1ApiRequest {
<#
.SYNOPSIS
Generic wrapper for SentinelOne API requests
.PARAMETER Endpoint
API endpoint path (e.g., "/web/api/v2.1/agents")
.PARAMETER Method
HTTP method (GET, POST, PUT, DELETE)
.PARAMETER Body
Request body for POST/PUT requests (hashtable, auto-converted to JSON)
.EXAMPLE
Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents" -Method GET
#>
param(
[Parameter(Mandatory=$true)]
[string]$Endpoint,
[Parameter(Mandatory=$false)]
[ValidateSet('GET','POST','PUT','DELETE')]
[string]$Method = 'GET',
[Parameter(Mandatory=$false)]
[hashtable]$Body = @{}
)
if (-not $script:S1ApiToken -or -not $script:S1ConsoleUrl) {
throw "Not connected to SentinelOne API. Run Connect-S1Api first."
}
$headers = @{
"Authorization" = "ApiToken $script:S1ApiToken"
"Content-Type" = "application/json"
}
$uri = "$script:S1ConsoleUrl$Endpoint"
$params = @{
Uri = $uri
Method = $Method
Headers = $headers
ErrorAction = 'Stop'
}
if ($Method -in @('POST', 'PUT') -and $Body.Count -gt 0) {
$params.Body = ($Body | ConvertTo-Json -Depth 10)
}
try {
$response = Invoke-RestMethod @params
return $response
}
catch {
Write-Host "[ERROR] API request failed: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " URI: $uri" -ForegroundColor Yellow
Write-Host " Method: $Method" -ForegroundColor Yellow
if ($_.Exception.Response) {
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$responseBody = $reader.ReadToEnd()
Write-Host " Response: $responseBody" -ForegroundColor Yellow
}
throw
}
}
Export-ModuleMember -Function Connect-S1Api, Invoke-S1ApiRequestImport the module in your scripts:
# Import custom module
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
# Retrieve API token securely (example using Windows Credential Manager)
$token = (Get-StoredCredential -Target "SentinelOne-API").GetNetworkCredential().Password
# Connect to API
Connect-S1Api -ApiToken $token -ConsoleUrl "https://yourtenant.sentinelone.net"Step 3: Common API operations - Agent management
Retrieve all agents:
# Get all agents with basic filtering
$agents = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents" -Method GET
Write-Host "Total agents: $($agents.pagination.totalItems)"
Write-Host "Online agents: $(($agents.data | Where-Object {$_.isActive -eq $true}).Count)"
# Display summary
$agents.data | Select-Object computerName, osType, agentVersion, isActive, lastActiveDate | Format-Table -AutoSizeFilter agents by specific criteria:
# Build query parameters
$agentFilter = @{
isActive = $true
osTypes = "windows"
agentVersions = "23.4.2.487"
siteIds = "1234567890"
}
# Construct query string
$queryString = ($agentFilter.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join "&"
$endpoint = "/web/api/v2.1/agents?$queryString"
$filteredAgents = Invoke-S1ApiRequest -Endpoint $endpoint -Method GET
Write-Host "Filtered agents count: $($filteredAgents.pagination.totalItems)"Get detailed information for specific agent:
# Search by computer name
$computerName = "WKS-001"
$agents = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents?computerName=$computerName" -Method GET
if ($agents.data.Count -gt 0) {
$agent = $agents.data[0]
Write-Host "Agent Details:" -ForegroundColor Cyan
Write-Host " Computer Name: $($agent.computerName)"
Write-Host " Agent ID: $($agent.id)"
Write-Host " Agent Version: $($agent.agentVersion)"
Write-Host " OS: $($agent.osName) $($agent.osRevision)"
Write-Host " Domain: $($agent.domain)"
Write-Host " Last Active: $($agent.lastActiveDate)"
Write-Host " Network Status: $($agent.networkStatus)"
Write-Host " Infected: $($agent.infected)"
} else {
Write-Host "[WARN] Agent not found: $computerName" -ForegroundColor Yellow
}Initiate full disk scan on agent:
# Trigger full scan on specific agent
$agentId = "1234567890123456789"
$scanBody = @{
data = @{
ids = @($agentId)
}
}
$scanResult = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents/actions/initiate-scan" -Method POST -Body $scanBody
if ($scanResult.data.affected -gt 0) {
Write-Host "[SUCCESS] Full scan initiated on $($scanResult.data.affected) agent(s)" -ForegroundColor Green
} else {
Write-Host "[WARN] Scan not initiated - verify agent ID and connectivity" -ForegroundColor Yellow
}Bulk agent operations (restart, update, etc.):
# Example: Restart SentinelOne agent service on multiple computers
$computerNames = @("WKS-001", "WKS-002", "WKS-003")
# Get agent IDs for these computers
$agentIds = foreach ($computerName in $computerNames) {
$agent = (Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents?computerName=$computerName" -Method GET).data
if ($agent) { $agent[0].id }
}
# Restart agents
$restartBody = @{
data = @{
ids = $agentIds
}
}
$result = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents/actions/restart-machine" -Method POST -Body $restartBody
Write-Host "Restart initiated on $($result.data.affected) agent(s)"Step 4: Common API operations - Threat management
Retrieve unresolved threats:
# Get all unresolved threats from last 7 days
$dateFilter = (Get-Date).AddDays(-7).ToString("yyyy-MM-ddT00:00:00Z")
$endpoint = "/web/api/v2.1/threats?resolved=false&createdAt__gte=$dateFilter"
$threats = Invoke-S1ApiRequest -Endpoint $endpoint -Method GET
Write-Host "Unresolved threats: $($threats.pagination.totalItems)" -ForegroundColor Yellow
# Display summary
$threats.data | Select-Object @{Name="ThreatID";Expression={$_.id}}, @{Name="ThreatName";Expression={$_.threatInfo.threatName}}, @{Name="Computer";Expression={$_.agentRealtimeInfo.computerName}}, @{Name="DetectedAt";Expression={$_.threatInfo.createdAt}} | Format-Table -AutoSizeRetrieve threat details including storyline:
# Get specific threat details
$threatId = "1234567890"
$threat = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats/$threatId" -Method GET
Write-Host "Threat Details:" -ForegroundColor Cyan
Write-Host " Threat Name: $($threat.data.threatInfo.threatName)"
Write-Host " Classification: $($threat.data.threatInfo.classification)"
Write-Host " File Path: $($threat.data.threatInfo.filePath)"
Write-Host " SHA256: $($threat.data.threatInfo.sha256)"
Write-Host " Computer: $($threat.data.agentRealtimeInfo.computerName)"
Write-Host " Mitigation Status: $($threat.data.threatInfo.mitigationStatus)"
# Get threat timeline/storyline
$timeline = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats/$threatId/timeline" -Method GET
Write-Host "`nThreat Timeline Events: $($timeline.data.Count)" -ForegroundColor Cyan
$timeline.data | Select-Object -First 10 | ForEach-Object {
Write-Host " - $($_.eventType): $($_.processName) at $($_.createdAt)"
}Bulk resolve threats:
# Mark multiple threats as resolved
$threatIds = @("1234567890", "1234567891", "1234567892")
$resolveBody = @{
data = @{
threatIds = $threatIds
resolution = "confirmed_threat" # Options: confirmed_threat, false_positive, pua, suspicious
analystNote = "Bulk resolution - remediated via automated response playbook"
}
}
$result = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats/mark-as-resolved" -Method POST -Body $resolveBody
Write-Host "[SUCCESS] Resolved $($result.data.affected) threat(s)" -ForegroundColor GreenQuarantine or remediate threats:
# Quarantine specific threat
$threatId = "1234567890"
$quarantineBody = @{
data = @{
ids = @($threatId)
}
}
$result = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats/actions/quarantine" -Method POST -Body $quarantineBody
Write-Host "Quarantine action initiated on $($result.data.affected) threat(s)"Step 5: Common API operations - Reporting and analytics
Generate agent deployment status report:
# Retrieve all agents and generate CSV report
$allAgents = @()
$cursor = $null
do {
$endpoint = if ($cursor) {
"/web/api/v2.1/agents?cursor=$cursor&limit=1000"
} else {
"/web/api/v2.1/agents?limit=1000"
}
$response = Invoke-S1ApiRequest -Endpoint $endpoint -Method GET
$allAgents += $response.data
$cursor = $response.pagination.nextCursor
Write-Host "Retrieved $($allAgents.Count) agents so far..."
} while ($cursor)
# Generate report
$report = $allAgents | Select-Object `
computerName,
osType,
osName,
osRevision,
agentVersion,
siteName,
isActive,
lastActiveDate,
domain,
infected,
networkStatus
# Export to CSV
$reportPath = "C:\BIN\LOGS\SentinelOne-Agent-Report-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv"
$report | Export-Csv -Path $reportPath -NoTypeInformation
Write-Host "[SUCCESS] Agent report generated: $reportPath" -ForegroundColor Green
Write-Host "Total agents: $($allAgents.Count)"
Write-Host "Active agents: $(($allAgents | Where-Object {$_.isActive}).Count)"
Write-Host "Infected agents: $(($allAgents | Where-Object {$_.infected}).Count)"Generate daily threat summary report:
# Get threats from last 24 hours
$yesterday = (Get-Date).AddDays(-1).ToString("yyyy-MM-ddT00:00:00Z")
$threats = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats?createdAt__gte=$yesterday" -Method GET
# Analyze threat data
$totalThreats = $threats.pagination.totalItems
$resolvedThreats = ($threats.data | Where-Object {$_.threatInfo.resolved}).Count
$unresolvedThreats = $totalThreats - $resolvedThreats
$threatsByClassification = $threats.data | Group-Object -Property {$_.threatInfo.classification} | Select-Object Name, Count
# Generate HTML report
$htmlReport = @"
<!DOCTYPE html>
\<html\>
\<head\>
\<title\>SentinelOne Daily Threat Summary - $(Get-Date -Format 'yyyy-MM-dd')</title>
\<style\>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
.summary { background-color: #f0f0f0; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
</style>
</head>
\<body\>
<h1>SentinelOne Daily Threat Summary</h1>
<div class="summary">
\<p\>\<strong\>Report Date:</strong> $(Get-Date -Format 'yyyy-MM-dd HH:mm')</p>
\<p\>\<strong\>Total Threats (24h):</strong> $totalThreats</p>
\<p\>\<strong\>Resolved:</strong> $resolvedThreats</p>
\<p\>\<strong\>Unresolved:</strong> $unresolvedThreats</p>
</div>
<h2>Threats by Classification</h2>
\<table\>
\<tr\>\<th\>Classification</th>\<th\>Count</th></tr>
"@
foreach ($classification in $threatsByClassification) {
$htmlReport += "\<tr\>\<td\>$($classification.Name)</td>\<td\>$($classification.Count)</td></tr>"
}
$htmlReport += @"
</table>
<h2>Recent Unresolved Threats</h2>
\<table\>
\<tr\>\<th\>Threat Name</th>\<th\>Computer</th>\<th\>Detected At</th>\<th\>Confidence</th></tr>
"@
$unresolvedThreatList = $threats.data | Where-Object {-not $_.threatInfo.resolved} | Select-Object -First 20
foreach ($threat in $unresolvedThreatList) {
$htmlReport += "\<tr\>\<td\>$($threat.threatInfo.threatName)</td>\<td\>$($threat.agentRealtimeInfo.computerName)</td>\<td\>$($threat.threatInfo.createdAt)</td>\<td\>$($threat.threatInfo.confidenceLevel)</td></tr>"
}
$htmlReport += @"
</table>
</body>
</html>
"@
# Save report
$htmlPath = "C:\BIN\LOGS\SentinelOne-Daily-Threat-Report-$(Get-Date -Format 'yyyyMMdd').html"
$htmlReport | Out-File -FilePath $htmlPath -Encoding UTF8
Write-Host "[SUCCESS] Threat report generated: $htmlPath" -ForegroundColor Green
# Email report (optional - requires configured SMTP)
# Send-MailMessage -From "sentinelone@company.com" -To "security-team@company.com" -Subject "SentinelOne Daily Threat Summary" -BodyAsHtml -Body $htmlReport -SmtpServer "smtp.company.com"Step 6: Common API operations - Policy and exclusion management
Retrieve and export current policies:
# Get all sites
$sites = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/sites" -Method GET
# For each site, get policy configuration
$policyReport = foreach ($site in $sites.data) {
Write-Host "Retrieving policy for site: $($site.name)"
# Note: Policy retrieval varies by version, this is a generalized example
$policy = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/sites/$($site.id)" -Method GET
[PSCustomObject]@{
SiteName = $site.name
SiteId = $site.id
AgentCount = $site.agentCount
# Add specific policy settings as available in API response
}
}
$policyReport | Export-Csv -Path "C:\BIN\LOGS\SentinelOne-Policy-Audit-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformationCreate exclusion via API:
# Create path exclusion
$exclusionBody = @{
data = @{
type = "path"
value = "C:\Program Files\Custom App\**"
osType = "windows"
mode = @("static", "interoperability")
description = "Custom business application - approved by CISO ticket #12345"
scope = "site"
siteIds = @("1234567890")
}
}
$result = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/exclusions" -Method POST -Body $exclusionBody
if ($result.data.id) {
Write-Host "[SUCCESS] Exclusion created with ID: $($result.data.id)" -ForegroundColor Green
} else {
Write-Host "[ERROR] Failed to create exclusion" -ForegroundColor Red
}Bulk create exclusions from CSV:
# Import exclusions from CSV file
# CSV format: Type,Value,OSType,Mode,Description,Scope,SiteId
$exclusionsToCreate = Import-Csv -Path "C:\Temp\exclusions-to-create.csv"
foreach ($exclusion in $exclusionsToCreate) {
Write-Host "Creating exclusion: $($exclusion.Value)"
$exclusionBody = @{
data = @{
type = $exclusion.Type.ToLower()
value = $exclusion.Value
osType = $exclusion.OSType.ToLower()
mode = $exclusion.Mode.Split('|')
description = $exclusion.Description
}
}
if ($exclusion.Scope -eq "Site") {
$exclusionBody.data.scope = "site"
$exclusionBody.data.siteIds = @($exclusion.SiteId)
} else {
$exclusionBody.data.scope = "account"
}
try {
$result = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/exclusions" -Method POST -Body $exclusionBody
Write-Host " [SUCCESS] Exclusion ID: $($result.data.id)" -ForegroundColor Green
}
catch {
Write-Host " [FAILED] $($_.Exception.Message)" -ForegroundColor Red
}
}Step 7: Advanced automation - SIEM integration
Send SentinelOne alerts to Splunk/Syslog:
# Retrieve new threats and forward to Splunk HEC (HTTP Event Collector)
$splunkHecUrl = "https://splunk.company.com:8088/services/collector/event"
$splunkToken = "your-splunk-hec-token"
$lastCheckFile = "C:\BIN\LOGS\sentinelone-last-check.txt"
$lastCheckTime = if (Test-Path $lastCheckFile) {
Get-Content $lastCheckFile
} else {
(Get-Date).AddHours(-1).ToString("yyyy-MM-ddTHH:mm:ssZ")
}
# Get threats since last check
$threats = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats?createdAt__gte=$lastCheckTime" -Method GET
Write-Host "Found $($threats.pagination.totalItems) new threat(s) since $lastCheckTime"
# Send each threat to Splunk
foreach ($threat in $threats.data) {
$splunkEvent = @{
event = @{
threatId = $threat.id
threatName = $threat.threatInfo.threatName
classification = $threat.threatInfo.classification
confidenceLevel = $threat.threatInfo.confidenceLevel
computerName = $threat.agentRealtimeInfo.computerName
filePath = $threat.threatInfo.filePath
sha256 = $threat.threatInfo.sha256
detectedAt = $threat.threatInfo.createdAt
mitigationStatus = $threat.threatInfo.mitigationStatus
}
sourcetype = "sentinelone:threat"
index = "security"
} | ConvertTo-Json -Depth 10
$splunkHeaders = @{
"Authorization" = "Splunk $splunkToken"
"Content-Type" = "application/json"
}
try {
Invoke-RestMethod -Uri $splunkHecUrl -Method POST -Headers $splunkHeaders -Body $splunkEvent
Write-Host " [SUCCESS] Sent threat $($threat.id) to Splunk" -ForegroundColor Green
}
catch {
Write-Host " [FAILED] Failed to send threat to Splunk: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Update last check time
(Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ") | Out-File -FilePath $lastCheckFile -ForceStep 8: Scheduled automation with Task Scheduler
Create scheduled task to run daily threat report:
# Define the script to run
$scriptPath = "C:\Scripts\SentinelOne-Daily-Threat-Report.ps1"
$scriptContent = @'
# SentinelOne Daily Threat Report Script
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
$token = (Get-StoredCredential -Target "SentinelOne-API").GetNetworkCredential().Password
Connect-S1Api -ApiToken $token -ConsoleUrl "https://yourtenant.sentinelone.net"
# [Add report generation code from Step 5]
# Email report
$htmlPath = "C:\BIN\LOGS\SentinelOne-Daily-Threat-Report-$(Get-Date -Format 'yyyyMMdd').html"
Send-MailMessage -From "sentinelone@company.com" -To "security-team@company.com" -Subject "SentinelOne Daily Threat Summary - $(Get-Date -Format 'yyyy-MM-dd')" -Body (Get-Content $htmlPath -Raw) -BodyAsHtml -SmtpServer "smtp.company.com"
'@
$scriptContent | Out-File -FilePath $scriptPath -Encoding UTF8
# Create scheduled task
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -Daily -At "08:00AM"
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName "SentinelOne-Daily-Threat-Report" -Action $action -Trigger $trigger -Principal $principal -Description "Daily SentinelOne threat summary report"
Write-Host "[SUCCESS] Scheduled task created: SentinelOne-Daily-Threat-Report" -ForegroundColor GreenVERIFICATION
Test API connectivity:
# Simple API connectivity test
try {
$response = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/system/info" -Method GET
Write-Host "[SUCCESS] API connectivity verified" -ForegroundColor Green
Write-Host " Console Version: $($response.data.consoleVersion)"
Write-Host " API Version: $($response.data.apiVersion)"
}
catch {
Write-Host "[FAILED] API connectivity test failed" -ForegroundColor Red
Write-Host " Error: $($_.Exception.Message)"
}Verify API token permissions:
# Check token scope and permissions
$tokenInfo = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/users/api-token-details" -Method GET
Write-Host "API Token Details:" -ForegroundColor Cyan
Write-Host " User: $($tokenInfo.data.user.fullName)"
Write-Host " Role: $($tokenInfo.data.user.role)"
Write-Host " Expiration: $($tokenInfo.data.expirationDate)"
Write-Host " Scopes: $($tokenInfo.data.scopes -join ', ')"Test API rate limits:
# Monitor API rate limit headers
$response = Invoke-WebRequest -Uri "$ConsoleUrl/web/api/v2.1/agents?limit=1" -Headers @{"Authorization"="ApiToken $ApiToken"} -Method GET
Write-Host "API Rate Limit Status:" -ForegroundColor Cyan
Write-Host " Limit: $($response.Headers['X-RateLimit-Limit'])"
Write-Host " Remaining: $($response.Headers['X-RateLimit-Remaining'])"
Write-Host " Reset: $(([DateTimeOffset]::FromUnixTimeSeconds([int]$response.Headers['X-RateLimit-Reset'])).LocalDateTime)"TROUBLESHOOTING
Issue: Authentication failure (401 Unauthorized)
Solutions:
- Verify API token is correct and not expired
- Check token was copied completely (no truncation)
- Regenerate token from console if necessary
- Verify user account is not disabled
- Ensure
ApiTokenprefix is used in Authorization header
Issue: Permission denied (403 Forbidden)
Solutions:
- Verify user role has required API permissions
- Check endpoint requires Admin/SOC role vs. Analyst role
- Review API documentation for endpoint-specific permission requirements
- Generate new token from service account with higher privileges
Issue: Rate limit exceeded (429 Too Many Requests)
Solutions:
- Implement rate limit handling in scripts:
function Invoke-S1ApiRequestWithRetry {
param($Endpoint, $Method = 'GET', $Body = @{}, $MaxRetries = 3)
$retryCount = 0
while ($retryCount -lt $MaxRetries) {
try {
return Invoke-S1ApiRequest -Endpoint $Endpoint -Method $Method -Body $Body
}
catch {
if ($_.Exception.Response.StatusCode -eq 429) {
$retryAfter = $_.Exception.Response.Headers['Retry-After']
Write-Host "[WARN] Rate limit exceeded, retrying after $retryAfter seconds..." -ForegroundColor Yellow
Start-Sleep -Seconds ([int]$retryAfter + 1)
$retryCount++
} else {
throw
}
}
}
throw "Max retries exceeded due to rate limiting"
}- Reduce request frequency (add delays between calls)
- Use pagination with smaller page sizes instead of retrieving all data at once
Issue: Pagination handling for large datasets
Solutions:
# Proper pagination handling using cursor
function Get-AllS1Agents {
$allAgents = @()
$cursor = $null
do {
$endpoint = if ($cursor) {
"/web/api/v2.1/agents?cursor=$cursor&limit=1000"
} else {
"/web/api/v2.1/agents?limit=1000"
}
$response = Invoke-S1ApiRequest -Endpoint $endpoint -Method GET
$allAgents += $response.data
$cursor = $response.pagination.nextCursor
Write-Host "Retrieved $($allAgents.Count) agents so far..."
} while ($cursor)
return $allAgents
}Issue: Proxy configuration required
Solutions:
# Configure PowerShell to use corporate proxy
$proxy = "http://proxy.company.com:8080"
$proxyCredential = Get-Credential -Message "Enter proxy credentials"
$PSDefaultParameterValues = @{
'Invoke-RestMethod:Proxy' = $proxy
'Invoke-RestMethod:ProxyCredential' = $proxyCredential
'Invoke-WebRequest:Proxy' = $proxy
'Invoke-WebRequest:ProxyCredential' = $proxyCredential
}
# Or configure system-wide proxy
netsh winhttp set proxy proxy-server="proxy.company.com:8080"COMMANDS/SCRIPTS
Complete SentinelOne automation framework:
# Save as: C:\Scripts\SentinelOne-Automation-Framework.ps1
# Module import and connection (see Step 2 for module code)
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
# Secure token retrieval (Azure Key Vault example)
# Requires: Install-Module Az.KeyVault
$vaultName = "CompanySecrets"
$secretName = "SentinelOne-API-Token"
$token = (Get-AzKeyVaultSecret -VaultName $vaultName -Name $secretName -AsPlainText)
Connect-S1Api -ApiToken $token -ConsoleUrl "https://yourtenant.sentinelone.net"
# === Automation Functions ===
function Get-S1UnresolvedThreats {
<#
.SYNOPSIS
Retrieves all unresolved threats from last N days
#>
param([int]$DaysBack = 7)
$dateFilter = (Get-Date).AddDays(-$DaysBack).ToString("yyyy-MM-ddT00:00:00Z")
$endpoint = "/web/api/v2.1/threats?resolved=false&createdAt__gte=$dateFilter"
$threats = Invoke-S1ApiRequest -Endpoint $endpoint -Method GET
return $threats.data
}
function Invoke-S1ThreatTriage {
<#
.SYNOPSIS
Automated threat triage - marks low-severity PUA as resolved
#>
$puaThreats = Get-S1UnresolvedThreats -DaysBack 1 | Where-Object {$_.threatInfo.classification -eq "pua"}
Write-Host "Found $($puaThreats.Count) PUA threats for auto-resolution"
foreach ($threat in $puaThreats) {
$resolveBody = @{
data = @{
threatIds = @($threat.id)
resolution = "pua"
analystNote = "Auto-resolved PUA - low severity, automated triage $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
}
}
try {
Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats/mark-as-resolved" -Method POST -Body $resolveBody
Write-Host " [SUCCESS] Resolved threat: $($threat.id) - $($threat.threatInfo.threatName)" -ForegroundColor Green
}
catch {
Write-Host " [FAILED] $($threat.id): $($_.Exception.Message)" -ForegroundColor Red
}
}
}
function Send-S1DailyReport {
<#
.SYNOPSIS
Generates and emails daily operational report
#>
param(
[string]$EmailTo = "security-team@company.com",
[string]$EmailFrom = "sentinelone@company.com",
[string]$SmtpServer = "smtp.company.com"
)
# Get statistics
$allAgents = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents" -Method GET
$activeAgents = ($allAgents.data | Where-Object {$_.isActive}).Count
$infectedAgents = ($allAgents.data | Where-Object {$_.infected}).Count
$yesterday = (Get-Date).AddDays(-1).ToString("yyyy-MM-ddT00:00:00Z")
$threats = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats?createdAt__gte=$yesterday" -Method GET
$unresolvedThreats = ($threats.data | Where-Object {-not $_.threatInfo.resolved}).Count
# Build HTML report
$htmlBody = @"
\<html\>
\<body\>
<h2>SentinelOne Daily Report - $(Get-Date -Format 'yyyy-MM-dd')</h2>
<h3>Agent Status</h3>
\<ul\>
\<li\>Total Agents: $($allAgents.pagination.totalItems)</li>
\<li\>Active Agents: $activeAgents</li>
\<li\>Infected Agents: $infectedAgents</li>
</ul>
<h3>Threat Summary (24h)</h3>
\<ul\>
\<li\>Total Threats: $($threats.pagination.totalItems)</li>
\<li\>Unresolved: $unresolvedThreats</li>
</ul>
\<p\>\<em\>Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</em></p>
</body>
</html>
"@
# Send email
Send-MailMessage -From $EmailFrom -To $EmailTo -Subject "SentinelOne Daily Report - $(Get-Date -Format 'yyyy-MM-dd')" -Body $htmlBody -BodyAsHtml -SmtpServer $SmtpServer
Write-Host "[SUCCESS] Daily report sent to $EmailTo" -ForegroundColor Green
}
# === Main Execution ===
Write-Host "=== SentinelOne Daily Automation ===" -ForegroundColor Cyan
# Run automated triage
Invoke-S1ThreatTriage
# Generate and send daily report
Send-S1DailyReport
Write-Host "`n[COMPLETE] Automation tasks finished" -ForegroundColor Green