Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. HOWTOs
  3. SentinelOne PowerShell Automation Scripts
SentinelOne PowerShell Automation Scripts
HOWTOAdvanced

SentinelOne PowerShell Automation Scripts

This document provides a comprehensive library of production-ready PowerShell scripts for automating SentinelOne operations in an MSP environment. These...

Dylan H.

Security Operations

February 11, 2026
38 min read

SCENARIO

This document provides a comprehensive library of production-ready PowerShell scripts for automating SentinelOne operations in an MSP environment. These scripts leverage the SentinelOne REST API to automate agent management, threat response, reporting, and multi-tenant operations.

Use these scripts when:

  • Automating daily operational tasks across multiple client sites
  • Building scheduled threat hunting and reporting workflows
  • Managing agent deployments and health at scale
  • Integrating SentinelOne with PSA/RMM platforms
  • Generating compliance and executive reports
  • Performing bulk operations across the MSP tenant

Prerequisites:

  • SentinelOne Singularity Complete license
  • API token with appropriate permissions
  • PowerShell 5.1+ (Windows) or PowerShell 7+ (cross-platform)
  • Network connectivity to SentinelOne console (HTTPS/443)

1. API AUTHENTICATION MODULE

SentinelOne-API-Module.psm1

This core module provides secure authentication, rate limiting, and error handling for all API operations.

#Requires -Version 5.1
<#
.SYNOPSIS
    SentinelOne API Authentication and Request Module
.DESCRIPTION
    Provides secure credential storage, token management, rate limiting,
    and error handling wrapper functions for SentinelOne API operations.
.NOTES
    Version: 2.0
    Author: CosmicBytez
    Requires: PowerShell 5.1+
#>
 
# Module-scoped variables
$script:S1Config = @{
    ApiToken = $null
    ConsoleUrl = $null
    RateLimitRemaining = 1000
    RateLimitReset = $null
    LastRequestTime = $null
    MinRequestInterval = 100  # milliseconds between requests
}
 
$script:LogPath = "C:\BIN\LOGS"
 
#region Credential Management
 
function Set-S1CredentialStore {
    <#
    .SYNOPSIS
        Securely stores SentinelOne API credentials using Windows DPAPI
    .PARAMETER ConsoleUrl
        SentinelOne console URL (e.g., https://usea1-partners.sentinelone.net)
    .PARAMETER ApiToken
        API token (will prompt securely if not provided)
    .PARAMETER CredentialPath
        Path to store encrypted credentials (default: user profile)
    .EXAMPLE
        Set-S1CredentialStore -ConsoleUrl "https://usea1-partners.sentinelone.net"
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^https://.*\.sentinelone\.net$')]
        [string]$ConsoleUrl,
 
        [Parameter(Mandatory = $false)]
        [string]$ApiToken,
 
        [Parameter(Mandatory = $false)]
        [string]$CredentialPath = "$env:USERPROFILE\.sentinelone\credentials.xml"
    )
 
    # Create directory if needed
    $credDir = Split-Path $CredentialPath -Parent
    if (-not (Test-Path $credDir)) {
        New-Item -Path $credDir -ItemType Directory -Force | Out-Null
    }
 
    # Get token securely if not provided
    if (-not $ApiToken) {
        $secureToken = Read-Host -Prompt "Enter SentinelOne API Token" -AsSecureString
    } else {
        $secureToken = ConvertTo-SecureString $ApiToken -AsPlainText -Force
    }
 
    # Create credential object
    $credential = @{
        ConsoleUrl = $ConsoleUrl
        ApiToken = $secureToken | ConvertFrom-SecureString
        CreatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        CreatedBy = $env:USERNAME
    }
 
    # Export encrypted
    $credential | Export-Clixml -Path $CredentialPath -Force
 
    # Set restrictive permissions
    $acl = Get-Acl $CredentialPath
    $acl.SetAccessRuleProtection($true, $false)
    $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
        $env:USERNAME, "FullControl", "Allow"
    )
    $acl.SetAccessRule($rule)
    Set-Acl -Path $CredentialPath -AclObject $acl
 
    Write-Host "[SUCCESS] Credentials stored securely at: $CredentialPath" -ForegroundColor Green
}
 
function Get-S1StoredCredential {
    <#
    .SYNOPSIS
        Retrieves stored SentinelOne credentials
    .PARAMETER CredentialPath
        Path to credential file
    .EXAMPLE
        $creds = Get-S1StoredCredential
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$CredentialPath = "$env:USERPROFILE\.sentinelone\credentials.xml"
    )
 
    if (-not (Test-Path $CredentialPath)) {
        throw "Credential file not found. Run Set-S1CredentialStore first."
    }
 
    $stored = Import-Clixml -Path $CredentialPath
 
    return @{
        ConsoleUrl = $stored.ConsoleUrl
        ApiToken = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
            [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR(
                ($stored.ApiToken | ConvertTo-SecureString)
            )
        )
    }
}
 
#endregion
 
#region Connection Management
 
function Connect-S1Console {
    <#
    .SYNOPSIS
        Establishes connection to SentinelOne API
    .PARAMETER ConsoleUrl
        SentinelOne console URL
    .PARAMETER ApiToken
        API token (use stored credentials if not provided)
    .PARAMETER UseStoredCredentials
        Use credentials from secure store
    .EXAMPLE
        Connect-S1Console -UseStoredCredentials
    .EXAMPLE
        Connect-S1Console -ConsoleUrl "https://usea1-partners.sentinelone.net" -ApiToken $token
    #>
    [CmdletBinding(DefaultParameterSetName = 'Stored')]
    param(
        [Parameter(ParameterSetName = 'Manual', Mandatory = $true)]
        [string]$ConsoleUrl,
 
        [Parameter(ParameterSetName = 'Manual', Mandatory = $true)]
        [string]$ApiToken,
 
        [Parameter(ParameterSetName = 'Stored')]
        [switch]$UseStoredCredentials
    )
 
    if ($UseStoredCredentials -or $PSCmdlet.ParameterSetName -eq 'Stored') {
        $creds = Get-S1StoredCredential
        $ConsoleUrl = $creds.ConsoleUrl
        $ApiToken = $creds.ApiToken
    }
 
    $script:S1Config.ConsoleUrl = $ConsoleUrl.TrimEnd('/')
    $script:S1Config.ApiToken = $ApiToken
 
    # Validate connection
    try {
        $test = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/system/info" -Method GET
        Write-Host "[SUCCESS] Connected to SentinelOne" -ForegroundColor Green
        Write-Host "  Console: $($script:S1Config.ConsoleUrl)" -ForegroundColor Cyan
        Write-Host "  Version: $($test.data.consoleVersion)" -ForegroundColor Cyan
        return $true
    }
    catch {
        Write-Error "Failed to connect to SentinelOne: $($_.Exception.Message)"
        $script:S1Config.ApiToken = $null
        $script:S1Config.ConsoleUrl = $null
        return $false
    }
}
 
function Disconnect-S1Console {
    <#
    .SYNOPSIS
        Clears SentinelOne API session
    #>
    $script:S1Config.ApiToken = $null
    $script:S1Config.ConsoleUrl = $null
    Write-Host "[INFO] Disconnected from SentinelOne" -ForegroundColor Yellow
}
 
#endregion
 
#region API Request Handling
 
function Invoke-S1ApiRequest {
    <#
    .SYNOPSIS
        Generic wrapper for SentinelOne API requests with rate limiting and error handling
    .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
    .PARAMETER QueryParams
        Hashtable of query parameters
    .PARAMETER MaxRetries
        Maximum retry attempts for rate limiting (default: 3)
    .EXAMPLE
        Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents" -Method GET
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Endpoint,
 
        [Parameter(Mandatory = $false)]
        [ValidateSet('GET', 'POST', 'PUT', 'DELETE')]
        [string]$Method = 'GET',
 
        [Parameter(Mandatory = $false)]
        [hashtable]$Body = @{},
 
        [Parameter(Mandatory = $false)]
        [hashtable]$QueryParams = @{},
 
        [Parameter(Mandatory = $false)]
        [int]$MaxRetries = 3
    )
 
    if (-not $script:S1Config.ApiToken -or -not $script:S1Config.ConsoleUrl) {
        throw "Not connected to SentinelOne. Run Connect-S1Console first."
    }
 
    # Build query string
    if ($QueryParams.Count -gt 0) {
        $queryString = ($QueryParams.GetEnumerator() | ForEach-Object {
            "$($_.Key)=$([System.Web.HttpUtility]::UrlEncode($_.Value.ToString()))"
        }) -join "&"
        $Endpoint = "$Endpoint`?$queryString"
    }
 
    $uri = "$($script:S1Config.ConsoleUrl)$Endpoint"
 
    $headers = @{
        "Authorization" = "ApiToken $($script:S1Config.ApiToken)"
        "Content-Type"  = "application/json"
        "Accept"        = "application/json"
    }
 
    $requestParams = @{
        Uri                = $uri
        Method             = $Method
        Headers            = $headers
        ErrorAction        = 'Stop'
        UseBasicParsing    = $true
    }
 
    if ($Method -in @('POST', 'PUT') -and $Body.Count -gt 0) {
        $requestParams.Body = ($Body | ConvertTo-Json -Depth 20 -Compress)
    }
 
    # Rate limiting - ensure minimum interval between requests
    if ($script:S1Config.LastRequestTime) {
        $elapsed = (Get-Date) - $script:S1Config.LastRequestTime
        if ($elapsed.TotalMilliseconds -lt $script:S1Config.MinRequestInterval) {
            Start-Sleep -Milliseconds ($script:S1Config.MinRequestInterval - $elapsed.TotalMilliseconds)
        }
    }
 
    $retryCount = 0
    while ($retryCount -le $MaxRetries) {
        try {
            $script:S1Config.LastRequestTime = Get-Date
            $response = Invoke-RestMethod @requestParams
 
            # Update rate limit tracking from headers (if available)
            return $response
        }
        catch {
            $statusCode = $_.Exception.Response.StatusCode.value__
 
            # Handle rate limiting (429)
            if ($statusCode -eq 429) {
                $retryAfter = 60
                if ($_.Exception.Response.Headers['Retry-After']) {
                    $retryAfter = [int]$_.Exception.Response.Headers['Retry-After']
                }
                Write-Warning "Rate limited. Waiting $retryAfter seconds..."
                Start-Sleep -Seconds ($retryAfter + 1)
                $retryCount++
                continue
            }
 
            # Handle other errors
            $errorMessage = $_.Exception.Message
            try {
                $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
                $responseBody = $reader.ReadToEnd() | ConvertFrom-Json
                if ($responseBody.errors) {
                    $errorMessage = $responseBody.errors[0].detail
                }
            } catch {}
 
            Write-Error "API Error [$statusCode]: $errorMessage`nEndpoint: $Endpoint"
            throw
        }
    }
 
    throw "Max retries exceeded due to rate limiting"
}
 
function Get-S1AllPages {
    <#
    .SYNOPSIS
        Retrieves all pages of results using cursor pagination
    .PARAMETER Endpoint
        API endpoint path
    .PARAMETER QueryParams
        Initial query parameters
    .PARAMETER MaxItems
        Maximum items to retrieve (default: unlimited)
    .EXAMPLE
        $allAgents = Get-S1AllPages -Endpoint "/web/api/v2.1/agents"
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Endpoint,
 
        [Parameter(Mandatory = $false)]
        [hashtable]$QueryParams = @{},
 
        [Parameter(Mandatory = $false)]
        [int]$MaxItems = 0
    )
 
    $allData = @()
    $cursor = $null
    $QueryParams.limit = 1000
 
    do {
        if ($cursor) {
            $QueryParams.cursor = $cursor
        }
 
        $response = Invoke-S1ApiRequest -Endpoint $Endpoint -Method GET -QueryParams $QueryParams
 
        if ($response.data) {
            $allData += $response.data
        }
 
        $cursor = $response.pagination.nextCursor
 
        Write-Verbose "Retrieved $($allData.Count) items..."
 
        if ($MaxItems -gt 0 -and $allData.Count -ge $MaxItems) {
            $allData = $allData | Select-Object -First $MaxItems
            break
        }
 
    } while ($cursor)
 
    return $allData
}
 
#endregion
 
#region Logging Functions
 
function Write-S1Log {
    <#
    .SYNOPSIS
        Writes log entry to file and console
    .PARAMETER Message
        Log message
    .PARAMETER Level
        Log level (INFO, WARN, ERROR, SUCCESS)
    .PARAMETER LogFile
        Log file name (auto-generated if not specified)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,
 
        [Parameter(Mandatory = $false)]
        [ValidateSet('INFO', 'WARN', 'ERROR', 'SUCCESS', 'DEBUG')]
        [string]$Level = 'INFO',
 
        [Parameter(Mandatory = $false)]
        [string]$LogFile
    )
 
    if (-not (Test-Path $script:LogPath)) {
        New-Item -Path $script:LogPath -ItemType Directory -Force | Out-Null
    }
 
    if (-not $LogFile) {
        $LogFile = "SentinelOne-$(Get-Date -Format 'yyyy-MM-dd').log"
    }
 
    $logFilePath = Join-Path $script:LogPath $LogFile
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "[$timestamp] [$Level] $Message"
 
    Add-Content -Path $logFilePath -Value $logEntry
 
    $color = switch ($Level) {
        'INFO'    { 'White' }
        'WARN'    { 'Yellow' }
        'ERROR'   { 'Red' }
        'SUCCESS' { 'Green' }
        'DEBUG'   { 'Gray' }
    }
 
    Write-Host $logEntry -ForegroundColor $color
}
 
#endregion
 
# Export module members
Export-ModuleMember -Function @(
    'Set-S1CredentialStore',
    'Get-S1StoredCredential',
    'Connect-S1Console',
    'Disconnect-S1Console',
    'Invoke-S1ApiRequest',
    'Get-S1AllPages',
    'Write-S1Log'
)

Usage Example:

# First-time setup - store credentials securely
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1"
Set-S1CredentialStore -ConsoleUrl "https://usea1-partners.sentinelone.net"
 
# In automation scripts - use stored credentials
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1"
Connect-S1Console -UseStoredCredentials
 
# Make API calls
$agents = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents" -Method GET

Scheduled Task Integration:

# Create scheduled task for daily automation
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\S1-DailyAutomation.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At "06:00AM"
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount
Register-ScheduledTask -TaskName "SentinelOne-DailyAutomation" `
    -Action $action -Trigger $trigger -Principal $principal

2. AGENT MANAGEMENT SCRIPTS

Get-S1Agents.ps1 - Agent Inventory with Filtering

#Requires -Version 5.1
<#
.SYNOPSIS
    Retrieves SentinelOne agents with flexible filtering options
.DESCRIPTION
    Queries all agents across sites with filtering by status, OS, version, site, etc.
    Supports export to CSV/JSON and integration with other automation.
.PARAMETER SiteId
    Filter by specific site ID
.PARAMETER SiteName
    Filter by site name (partial match)
.PARAMETER Status
    Filter by agent status (online, offline, all)
.PARAMETER OSType
    Filter by OS type (windows, linux, macos)
.PARAMETER MinVersion
    Filter agents below this version
.PARAMETER Infected
    Filter for infected agents only
.PARAMETER ExportPath
    Path to export results (CSV or JSON based on extension)
.EXAMPLE
    .\Get-S1Agents.ps1 -Status offline -ExportPath "C:\Reports\offline-agents.csv"
.EXAMPLE
    .\Get-S1Agents.ps1 -SiteName "Contoso" -OSType windows
#>
 
[CmdletBinding()]
param(
    [string]$SiteId,
    [string]$SiteName,
    [ValidateSet('online', 'offline', 'all')]
    [string]$Status = 'all',
    [ValidateSet('windows', 'linux', 'macos', 'all')]
    [string]$OSType = 'all',
    [string]$MinVersion,
    [switch]$Infected,
    [string]$ExportPath
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Starting agent inventory query" -Level INFO
 
    # Build query parameters
    $queryParams = @{
        limit = 1000
    }
 
    if ($SiteId) { $queryParams.siteIds = $SiteId }
    if ($Status -eq 'online') { $queryParams.isActive = $true }
    if ($Status -eq 'offline') { $queryParams.isActive = $false }
    if ($OSType -ne 'all') { $queryParams.osTypes = $OSType }
    if ($Infected) { $queryParams.infected = $true }
 
    # Get all agents
    $agents = Get-S1AllPages -Endpoint "/web/api/v2.1/agents" -QueryParams $queryParams
 
    # Filter by site name if specified
    if ($SiteName) {
        $agents = $agents | Where-Object { $_.siteName -like "*$SiteName*" }
    }
 
    # Filter by minimum version if specified
    if ($MinVersion) {
        $agents = $agents | Where-Object {
            [version]$_.agentVersion -lt [version]$MinVersion
        }
    }
 
    # Format output
    $report = $agents | Select-Object @(
        @{N='ComputerName'; E={$_.computerName}}
        @{N='SiteName'; E={$_.siteName}}
        @{N='GroupName'; E={$_.groupName}}
        @{N='AgentVersion'; E={$_.agentVersion}}
        @{N='OSType'; E={$_.osType}}
        @{N='OSName'; E={$_.osName}}
        @{N='IsActive'; E={$_.isActive}}
        @{N='LastActive'; E={$_.lastActiveDate}}
        @{N='Infected'; E={$_.infected}}
        @{N='NetworkStatus'; E={$_.networkStatus}}
        @{N='Domain'; E={$_.domain}}
        @{N='ExternalIP'; E={$_.externalIp}}
        @{N='AgentId'; E={$_.id}}
    )
 
    # Display summary
    Write-S1Log -Message "Found $($report.Count) agents" -Level SUCCESS
    Write-Host "`nAgent Summary:" -ForegroundColor Cyan
    Write-Host "  Total: $($report.Count)"
    Write-Host "  Online: $(($report | Where-Object IsActive).Count)"
    Write-Host "  Offline: $(($report | Where-Object {-not $_.IsActive}).Count)"
    Write-Host "  Infected: $(($report | Where-Object Infected).Count)"
 
    # Export if path specified
    if ($ExportPath) {
        $extension = [System.IO.Path]::GetExtension($ExportPath)
        if ($extension -eq '.json') {
            $report | ConvertTo-Json -Depth 5 | Out-File $ExportPath -Encoding UTF8
        } else {
            $report | Export-Csv -Path $ExportPath -NoTypeInformation
        }
        Write-S1Log -Message "Exported to: $ExportPath" -Level SUCCESS
    }
 
    return $report
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Get-S1AgentHealth.ps1 - Agent Health Check Automation

#Requires -Version 5.1
<#
.SYNOPSIS
    Performs comprehensive health check on SentinelOne agents
.DESCRIPTION
    Checks agent connectivity, version compliance, policy sync, and scan status.
    Generates health report and can send alerts for critical issues.
.PARAMETER SiteId
    Limit check to specific site
.PARAMETER AlertEmail
    Email address for critical alerts
.PARAMETER VersionThreshold
    Minimum acceptable agent version
.EXAMPLE
    .\Get-S1AgentHealth.ps1 -VersionThreshold "23.4.2.0" -AlertEmail "soc@company.com"
#>
 
[CmdletBinding()]
param(
    [string]$SiteId,
    [string]$AlertEmail,
    [string]$VersionThreshold = "23.3.0.0",
    [int]$OfflineThresholdHours = 24,
    [string]$ReportPath = "C:\BIN\LOGS\S1-HealthReport-$(Get-Date -Format 'yyyyMMdd').html"
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
function Get-AgentHealthStatus {
    param($Agent)
 
    $issues = @()
    $severity = 'Healthy'
 
    # Check online status
    if (-not $Agent.isActive) {
        $lastActive = [datetime]$Agent.lastActiveDate
        $hoursOffline = (Get-Date) - $lastActive | Select-Object -ExpandProperty TotalHours
        if ($hoursOffline -gt $OfflineThresholdHours) {
            $issues += "Offline for $([math]::Round($hoursOffline, 1)) hours"
            $severity = 'Warning'
        }
        if ($hoursOffline -gt ($OfflineThresholdHours * 7)) {
            $severity = 'Critical'
        }
    }
 
    # Check version
    if ([version]$Agent.agentVersion -lt [version]$VersionThreshold) {
        $issues += "Outdated version: $($Agent.agentVersion)"
        if ($severity -ne 'Critical') { $severity = 'Warning' }
    }
 
    # Check infected status
    if ($Agent.infected) {
        $issues += "INFECTED - Active threat detected"
        $severity = 'Critical'
    }
 
    # Check network quarantine
    if ($Agent.networkStatus -eq 'quarantined') {
        $issues += "Network quarantined"
        $severity = 'Critical'
    }
 
    # Check scan status
    if ($Agent.scanStatus -eq 'aborted' -or $Agent.scanStatus -eq 'failed') {
        $issues += "Scan status: $($Agent.scanStatus)"
        if ($severity -ne 'Critical') { $severity = 'Warning' }
    }
 
    return @{
        Severity = $severity
        Issues = $issues -join "; "
        IssueCount = $issues.Count
    }
}
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Starting agent health check" -Level INFO
 
    $queryParams = @{}
    if ($SiteId) { $queryParams.siteIds = $SiteId }
 
    $agents = Get-S1AllPages -Endpoint "/web/api/v2.1/agents" -QueryParams $queryParams
 
    $healthReport = foreach ($agent in $agents) {
        $health = Get-AgentHealthStatus -Agent $agent
 
        [PSCustomObject]@{
            ComputerName = $agent.computerName
            SiteName = $agent.siteName
            AgentVersion = $agent.agentVersion
            IsActive = $agent.isActive
            LastActive = $agent.lastActiveDate
            Infected = $agent.infected
            NetworkStatus = $agent.networkStatus
            Severity = $health.Severity
            Issues = $health.Issues
            AgentId = $agent.id
        }
    }
 
    # Categorize results
    $critical = $healthReport | Where-Object { $_.Severity -eq 'Critical' }
    $warning = $healthReport | Where-Object { $_.Severity -eq 'Warning' }
    $healthy = $healthReport | Where-Object { $_.Severity -eq 'Healthy' }
 
    # Generate HTML report
    $html = @"
<!DOCTYPE html>
\<html\>
\<head\>
    \<title\>SentinelOne Agent Health Report</title>
    \<style\>
        body { font-family: 'Segoe UI', Arial, sans-serif; margin: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        h1 { color: #333; border-bottom: 2px solid #6B2D9E; padding-bottom: 10px; }
        .summary { display: flex; gap: 20px; margin: 20px 0; }
        .stat-box { flex: 1; padding: 20px; border-radius: 8px; text-align: center; }
        .critical { background: #FFE5E5; border: 2px solid #FF4444; }
        .warning { background: #FFF3E0; border: 2px solid #FF9800; }
        .healthy { background: #E8F5E9; border: 2px solid #4CAF50; }
        .stat-number { font-size: 36px; font-weight: bold; }
        .stat-label { font-size: 14px; color: #666; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th { background: #6B2D9E; color: white; padding: 12px; text-align: left; }
        td { padding: 10px; border-bottom: 1px solid #ddd; }
        tr:hover { background: #f9f9f9; }
        .severity-critical { color: #FF4444; font-weight: bold; }
        .severity-warning { color: #FF9800; font-weight: bold; }
        .severity-healthy { color: #4CAF50; }
        .timestamp { color: #999; font-size: 12px; margin-top: 20px; }
    </style>
</head>
\<body\>
    <div class="container">
        <h1>SentinelOne Agent Health Report</h1>
 
        <div class="summary">
            <div class="stat-box critical">
                <div class="stat-number">$($critical.Count)</div>
                <div class="stat-label">Critical Issues</div>
            </div>
            <div class="stat-box warning">
                <div class="stat-number">$($warning.Count)</div>
                <div class="stat-label">Warnings</div>
            </div>
            <div class="stat-box healthy">
                <div class="stat-number">$($healthy.Count)</div>
                <div class="stat-label">Healthy</div>
            </div>
        </div>
 
        <h2>Critical Issues</h2>
        \<table\>
            \<tr\>\<th\>Computer</th>\<th\>Site</th>\<th\>Version</th>\<th\>Status</th>\<th\>Issues</th></tr>
            $(foreach ($item in $critical) {
                "\<tr\>\<td\>$($item.ComputerName)</td>\<td\>$($item.SiteName)</td>\<td\>$($item.AgentVersion)</td>\<td\>$(if($item.IsActive){'Online'}else{'Offline'})</td>\<td\>$($item.Issues)</td></tr>"
            })
        </table>
 
        <h2>Warnings</h2>
        \<table\>
            \<tr\>\<th\>Computer</th>\<th\>Site</th>\<th\>Version</th>\<th\>Status</th>\<th\>Issues</th></tr>
            $(foreach ($item in ($warning | Select-Object -First 50)) {
                "\<tr\>\<td\>$($item.ComputerName)</td>\<td\>$($item.SiteName)</td>\<td\>$($item.AgentVersion)</td>\<td\>$(if($item.IsActive){'Online'}else{'Offline'})</td>\<td\>$($item.Issues)</td></tr>"
            })
        </table>
 
        <p class="timestamp">Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Total Agents: $($healthReport.Count)</p>
    </div>
</body>
</html>
"@
 
    $html | Out-File $ReportPath -Encoding UTF8
    Write-S1Log -Message "Health report saved to: $ReportPath" -Level SUCCESS
 
    # Send alert if critical issues found
    if ($AlertEmail -and $critical.Count -gt 0) {
        $subject = "ALERT: $($critical.Count) SentinelOne Agents with Critical Issues"
        $body = "Critical issues detected on the following agents:`n`n"
        $body += ($critical | ForEach-Object { "$($_.ComputerName) - $($_.Issues)" }) -join "`n"
 
        # Uncomment to enable email alerts
        # Send-MailMessage -To $AlertEmail -From "sentinelone@company.com" -Subject $subject -Body $body -SmtpServer "smtp.company.com"
        Write-S1Log -Message "Alert would be sent to $AlertEmail" -Level WARN
    }
 
    # Console summary
    Write-Host "`nHealth Check Summary:" -ForegroundColor Cyan
    Write-Host "  Critical: $($critical.Count)" -ForegroundColor Red
    Write-Host "  Warning: $($warning.Count)" -ForegroundColor Yellow
    Write-Host "  Healthy: $($healthy.Count)" -ForegroundColor Green
 
    return $healthReport
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Update-S1Agents.ps1 - Bulk Agent Upgrade Script

#Requires -Version 5.1
<#
.SYNOPSIS
    Initiates bulk agent upgrades across sites
.DESCRIPTION
    Upgrades agents to specified version with scheduling and rollback options.
    Supports filtering by site, group, OS type, and current version.
.PARAMETER TargetVersion
    Target agent version (or 'latest' for newest GA)
.PARAMETER SiteIds
    Array of site IDs to upgrade
.PARAMETER MaxConcurrent
    Maximum concurrent upgrades per site
.PARAMETER ScheduleTime
    Schedule upgrade for specific time (UTC)
.EXAMPLE
    .\Update-S1Agents.ps1 -TargetVersion "latest" -SiteIds @("site1","site2")
#>
 
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory = $true)]
    [string]$TargetVersion,
 
    [Parameter(Mandatory = $false)]
    [string[]]$SiteIds,
 
    [Parameter(Mandatory = $false)]
    [string[]]$GroupIds,
 
    [Parameter(Mandatory = $false)]
    [ValidateSet('windows', 'linux', 'macos')]
    [string]$OSType = 'windows',
 
    [Parameter(Mandatory = $false)]
    [int]$MaxConcurrent = 50,
 
    [Parameter(Mandatory = $false)]
    [datetime]$ScheduleTime,
 
    [Parameter(Mandatory = $false)]
    [switch]$Force
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Starting bulk agent upgrade process" -Level INFO
 
    # Get available packages if 'latest' specified
    if ($TargetVersion -eq 'latest') {
        $packages = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/update/agent/packages" -QueryParams @{
            osTypes = $OSType
            status = 'ga'
            sortBy = 'version'
            sortOrder = 'desc'
            limit = 1
        }
 
        if ($packages.data.Count -eq 0) {
            throw "No GA packages found for $OSType"
        }
 
        $TargetVersion = $packages.data[0].version
        $packageId = $packages.data[0].id
        Write-S1Log -Message "Latest GA version for $OSType`: $TargetVersion" -Level INFO
    } else {
        # Find specific package
        $packages = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/update/agent/packages" -QueryParams @{
            osTypes = $OSType
            version = $TargetVersion
        }
 
        if ($packages.data.Count -eq 0) {
            throw "Package version $TargetVersion not found for $OSType"
        }
        $packageId = $packages.data[0].id
    }
 
    # Build agent filter
    $agentQuery = @{
        osTypes = $OSType
        isActive = $true
    }
 
    if ($SiteIds) { $agentQuery.siteIds = $SiteIds -join ',' }
    if ($GroupIds) { $agentQuery.groupIds = $GroupIds -join ',' }
 
    # Get agents needing upgrade
    $agents = Get-S1AllPages -Endpoint "/web/api/v2.1/agents" -QueryParams $agentQuery
 
    $agentsToUpgrade = $agents | Where-Object {
        [version]$_.agentVersion -lt [version]$TargetVersion
    }
 
    if ($agentsToUpgrade.Count -eq 0) {
        Write-S1Log -Message "All agents are already at version $TargetVersion or higher" -Level SUCCESS
        return
    }
 
    Write-S1Log -Message "Found $($agentsToUpgrade.Count) agents requiring upgrade to $TargetVersion" -Level INFO
 
    # Confirm upgrade
    if (-not $Force -and -not $PSCmdlet.ShouldProcess("$($agentsToUpgrade.Count) agents", "Upgrade to $TargetVersion")) {
        Write-S1Log -Message "Upgrade cancelled by user" -Level WARN
        return
    }
 
    # Process in batches
    $batches = [System.Collections.ArrayList]@()
    for ($i = 0; $i -lt $agentsToUpgrade.Count; $i += $MaxConcurrent) {
        $batch = $agentsToUpgrade | Select-Object -Skip $i -First $MaxConcurrent
        [void]$batches.Add($batch)
    }
 
    Write-S1Log -Message "Processing $($batches.Count) batches of up to $MaxConcurrent agents" -Level INFO
 
    $results = @()
    $batchNum = 0
 
    foreach ($batch in $batches) {
        $batchNum++
        Write-S1Log -Message "Processing batch $batchNum of $($batches.Count)" -Level INFO
 
        $agentIds = $batch | Select-Object -ExpandProperty id
 
        $upgradeBody = @{
            data = @{
                packageId = $packageId
            }
            filter = @{
                ids = $agentIds
            }
        }
 
        if ($ScheduleTime) {
            $upgradeBody.data.scheduleTime = $ScheduleTime.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
        }
 
        try {
            $response = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents/actions/update-software" `
                -Method POST -Body $upgradeBody
 
            $results += [PSCustomObject]@{
                Batch = $batchNum
                AgentCount = $agentIds.Count
                Affected = $response.data.affected
                Status = 'Initiated'
            }
 
            Write-S1Log -Message "Batch $batchNum`: Upgrade initiated for $($response.data.affected) agents" -Level SUCCESS
        }
        catch {
            $results += [PSCustomObject]@{
                Batch = $batchNum
                AgentCount = $agentIds.Count
                Affected = 0
                Status = "Failed: $($_.Exception.Message)"
            }
            Write-S1Log -Message "Batch $batchNum failed: $($_.Exception.Message)" -Level ERROR
        }
 
        # Rate limiting between batches
        if ($batchNum -lt $batches.Count) {
            Start-Sleep -Seconds 2
        }
    }
 
    # Summary
    $totalAffected = ($results | Measure-Object -Property Affected -Sum).Sum
    Write-Host "`nUpgrade Summary:" -ForegroundColor Cyan
    Write-Host "  Target Version: $TargetVersion"
    Write-Host "  Total Agents Upgraded: $totalAffected"
    Write-Host "  Batches Processed: $($batches.Count)"
 
    return $results
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Get-S1UninstallPassphrase.ps1 - Uninstall Passphrase Retrieval

#Requires -Version 5.1
<#
.SYNOPSIS
    Retrieves agent uninstall passphrases
.DESCRIPTION
    Gets uninstall passphrase for specific agents or exports all for a site.
    Useful for agent removal and reimaging scenarios.
.PARAMETER ComputerName
    Computer name to retrieve passphrase for
.PARAMETER AgentId
    Agent ID to retrieve passphrase for
.PARAMETER SiteId
    Site ID to export all passphrases for
.PARAMETER ExportPath
    Path to export passphrases (encrypted CSV)
.EXAMPLE
    .\Get-S1UninstallPassphrase.ps1 -ComputerName "WKS-001"
.EXAMPLE
    .\Get-S1UninstallPassphrase.ps1 -SiteId "12345" -ExportPath "C:\Secure\passphrases.csv"
#>
 
[CmdletBinding()]
param(
    [Parameter(ParameterSetName = 'ByName')]
    [string]$ComputerName,
 
    [Parameter(ParameterSetName = 'ById')]
    [string]$AgentId,
 
    [Parameter(ParameterSetName = 'BySite')]
    [string]$SiteId,
 
    [Parameter(ParameterSetName = 'BySite')]
    [string]$ExportPath
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Retrieving uninstall passphrase(s)" -Level INFO
 
    $results = @()
 
    if ($ComputerName) {
        # Find agent by computer name
        $agents = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents" -QueryParams @{
            computerName = $ComputerName
        }
 
        if ($agents.data.Count -eq 0) {
            throw "Agent not found: $ComputerName"
        }
 
        $AgentId = $agents.data[0].id
    }
 
    if ($AgentId) {
        # Get passphrase for single agent
        $passphrase = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents/passphrases" -QueryParams @{
            ids = $AgentId
        }
 
        if ($passphrase.data.Count -gt 0) {
            $agentInfo = $agents.data[0]
            $results += [PSCustomObject]@{
                ComputerName = $agentInfo.computerName
                SiteName = $agentInfo.siteName
                Passphrase = $passphrase.data[0].passphrase
                AgentId = $AgentId
            }
 
            Write-Host "`nUninstall Passphrase for $($agentInfo.computerName):" -ForegroundColor Cyan
            Write-Host "  Passphrase: $($passphrase.data[0].passphrase)" -ForegroundColor Yellow
            Write-Host "  Site: $($agentInfo.siteName)"
        }
    }
    elseif ($SiteId) {
        # Get all agents for site
        $agents = Get-S1AllPages -Endpoint "/web/api/v2.1/agents" -QueryParams @{
            siteIds = $SiteId
        }
 
        Write-S1Log -Message "Found $($agents.Count) agents in site" -Level INFO
 
        # Get passphrases in batches
        for ($i = 0; $i -lt $agents.Count; $i += 100) {
            $batch = $agents | Select-Object -Skip $i -First 100
            $agentIds = ($batch | Select-Object -ExpandProperty id) -join ','
 
            $passphrases = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents/passphrases" -QueryParams @{
                ids = $agentIds
            }
 
            foreach ($pp in $passphrases.data) {
                $agent = $batch | Where-Object { $_.id -eq $pp.id }
                $results += [PSCustomObject]@{
                    ComputerName = $agent.computerName
                    SiteName = $agent.siteName
                    Passphrase = $pp.passphrase
                    AgentId = $pp.id
                }
            }
 
            Write-Verbose "Processed $([math]::Min($i + 100, $agents.Count)) of $($agents.Count) agents"
        }
 
        if ($ExportPath) {
            # Export with warning about sensitivity
            Write-Warning "Exporting uninstall passphrases - ensure secure storage!"
            $results | Export-Csv -Path $ExportPath -NoTypeInformation
            Write-S1Log -Message "Passphrases exported to: $ExportPath" -Level SUCCESS
        }
    }
 
    return $results
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Remove-S1Agent.ps1 - Agent Decommission Script

#Requires -Version 5.1
<#
.SYNOPSIS
    Decommissions SentinelOne agents
.DESCRIPTION
    Removes agents from management, optionally triggering uninstall.
    Use for endpoint retirement, reimaging, or cleanup scenarios.
.PARAMETER ComputerName
    Computer name(s) to decommission
.PARAMETER AgentId
    Agent ID(s) to decommission
.PARAMETER Uninstall
    Send uninstall command (requires agent online)
.PARAMETER Force
    Skip confirmation prompt
.EXAMPLE
    .\Remove-S1Agent.ps1 -ComputerName "WKS-001","WKS-002" -Uninstall
#>
 
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
    [Parameter(ParameterSetName = 'ByName')]
    [string[]]$ComputerName,
 
    [Parameter(ParameterSetName = 'ById')]
    [string[]]$AgentId,
 
    [switch]$Uninstall,
 
    [switch]$Force
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Starting agent decommission process" -Level INFO
 
    $agentIds = @()
 
    if ($ComputerName) {
        foreach ($name in $ComputerName) {
            $agents = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents" -QueryParams @{
                computerName = $name
            }
 
            if ($agents.data.Count -eq 0) {
                Write-S1Log -Message "Agent not found: $name" -Level WARN
                continue
            }
 
            $agentIds += $agents.data[0].id
            Write-S1Log -Message "Found agent: $name (ID: $($agents.data[0].id))" -Level INFO
        }
    } else {
        $agentIds = $AgentId
    }
 
    if ($agentIds.Count -eq 0) {
        Write-S1Log -Message "No agents to process" -Level WARN
        return
    }
 
    # Confirmation
    if (-not $Force) {
        Write-Host "`nAgents to decommission: $($agentIds.Count)" -ForegroundColor Yellow
        if (-not $PSCmdlet.ShouldProcess("$($agentIds.Count) agents", "Decommission")) {
            return
        }
    }
 
    $results = @()
 
    # Initiate uninstall if requested
    if ($Uninstall) {
        Write-S1Log -Message "Initiating remote uninstall for $($agentIds.Count) agents" -Level INFO
 
        $uninstallBody = @{
            filter = @{
                ids = $agentIds
            }
        }
 
        try {
            $response = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents/actions/uninstall" `
                -Method POST -Body $uninstallBody
 
            Write-S1Log -Message "Uninstall initiated for $($response.data.affected) agents" -Level SUCCESS
 
            $results += [PSCustomObject]@{
                Action = 'Uninstall'
                AgentCount = $agentIds.Count
                Affected = $response.data.affected
                Status = 'Initiated'
            }
        }
        catch {
            Write-S1Log -Message "Uninstall failed: $($_.Exception.Message)" -Level ERROR
            $results += [PSCustomObject]@{
                Action = 'Uninstall'
                AgentCount = $agentIds.Count
                Affected = 0
                Status = "Failed: $($_.Exception.Message)"
            }
        }
    }
 
    # Decommission (remove from console)
    Write-S1Log -Message "Decommissioning $($agentIds.Count) agents from console" -Level INFO
 
    $decommBody = @{
        filter = @{
            ids = $agentIds
        }
    }
 
    try {
        $response = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents/actions/decommission" `
            -Method POST -Body $decommBody
 
        Write-S1Log -Message "Decommissioned $($response.data.affected) agents" -Level SUCCESS
 
        $results += [PSCustomObject]@{
            Action = 'Decommission'
            AgentCount = $agentIds.Count
            Affected = $response.data.affected
            Status = 'Completed'
        }
    }
    catch {
        Write-S1Log -Message "Decommission failed: $($_.Exception.Message)" -Level ERROR
        $results += [PSCustomObject]@{
            Action = 'Decommission'
            AgentCount = $agentIds.Count
            Affected = 0
            Status = "Failed: $($_.Exception.Message)"
        }
    }
 
    return $results
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

3. THREAT MANAGEMENT SCRIPTS

Get-S1Threats.ps1 - Active Threat Retrieval

#Requires -Version 5.1
<#
.SYNOPSIS
    Retrieves threats across all sites with filtering options
.DESCRIPTION
    Queries active and historical threats with filtering by status,
    severity, date range, and site.
.PARAMETER Status
    Filter by resolution status (unresolved, resolved, all)
.PARAMETER Severity
    Filter by AI confidence level
.PARAMETER DaysBack
    Number of days to look back (default: 7)
.PARAMETER SiteId
    Filter by specific site
.PARAMETER ExportPath
    Path to export threat data
.EXAMPLE
    .\Get-S1Threats.ps1 -Status unresolved -DaysBack 1
#>
 
[CmdletBinding()]
param(
    [ValidateSet('unresolved', 'resolved', 'all')]
    [string]$Status = 'unresolved',
 
    [ValidateSet('malicious', 'suspicious', 'all')]
    [string]$Severity = 'all',
 
    [int]$DaysBack = 7,
 
    [string]$SiteId,
 
    [string]$ExportPath
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Querying threats (Status: $Status, Days: $DaysBack)" -Level INFO
 
    $queryParams = @{
        createdAt__gte = (Get-Date).AddDays(-$DaysBack).ToString("yyyy-MM-ddT00:00:00Z")
    }
 
    if ($Status -ne 'all') {
        $queryParams.resolved = ($Status -eq 'resolved')
    }
 
    if ($Severity -ne 'all') {
        $queryParams.confidenceLevels = $Severity
    }
 
    if ($SiteId) {
        $queryParams.siteIds = $SiteId
    }
 
    $threats = Get-S1AllPages -Endpoint "/web/api/v2.1/threats" -QueryParams $queryParams
 
    $report = $threats | Select-Object @(
        @{N='ThreatId'; E={$_.id}}
        @{N='ThreatName'; E={$_.threatInfo.threatName}}
        @{N='Classification'; E={$_.threatInfo.classification}}
        @{N='Confidence'; E={$_.threatInfo.confidenceLevel}}
        @{N='ComputerName'; E={$_.agentRealtimeInfo.agentComputerName}}
        @{N='SiteName'; E={$_.agentRealtimeInfo.siteName}}
        @{N='FilePath'; E={$_.threatInfo.filePath}}
        @{N='SHA256'; E={$_.threatInfo.sha256}}
        @{N='MitigationStatus'; E={$_.threatInfo.mitigationStatus}}
        @{N='AnalystVerdict'; E={$_.threatInfo.analystVerdict}}
        @{N='Resolved'; E={$_.threatInfo.resolved}}
        @{N='CreatedAt'; E={$_.threatInfo.createdAt}}
        @{N='Username'; E={$_.threatInfo.processUser}}
    )
 
    # Summary by classification
    Write-Host "`nThreat Summary:" -ForegroundColor Cyan
    Write-Host "  Total: $($report.Count)"
 
    $byClass = $report | Group-Object Classification
    foreach ($group in $byClass) {
        Write-Host "  $($group.Name): $($group.Count)"
    }
 
    # Export if requested
    if ($ExportPath) {
        $report | Export-Csv -Path $ExportPath -NoTypeInformation
        Write-S1Log -Message "Threats exported to: $ExportPath" -Level SUCCESS
    }
 
    return $report
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Invoke-S1ThreatMitigation.ps1 - Automated Threat Response

#Requires -Version 5.1
<#
.SYNOPSIS
    Automates threat mitigation actions
.DESCRIPTION
    Performs bulk threat mitigation including kill, quarantine, remediate,
    and rollback actions. Supports automatic resolution based on rules.
.PARAMETER ThreatIds
    Specific threat IDs to mitigate
.PARAMETER Action
    Mitigation action to perform
.PARAMETER AutoResolve
    Automatically resolve after successful mitigation
.PARAMETER Classification
    Filter threats by classification for bulk action
.EXAMPLE
    .\Invoke-S1ThreatMitigation.ps1 -ThreatIds @("123","456") -Action quarantine
.EXAMPLE
    .\Invoke-S1ThreatMitigation.ps1 -Classification "pup" -Action remediate -AutoResolve
#>
 
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(ParameterSetName = 'ById')]
    [string[]]$ThreatIds,
 
    [Parameter(ParameterSetName = 'ByFilter')]
    [ValidateSet('malware', 'pup', 'suspicious', 'trojan', 'ransomware')]
    [string]$Classification,
 
    [Parameter(ParameterSetName = 'ByFilter')]
    [int]$DaysBack = 1,
 
    [Parameter(Mandatory = $true)]
    [ValidateSet('kill', 'quarantine', 'remediate', 'rollback', 'disconnect-from-network')]
    [string]$Action,
 
    [switch]$AutoResolve,
 
    [ValidateSet('true_positive', 'false_positive', 'suspicious', 'undefined')]
    [string]$Verdict = 'true_positive'
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Starting threat mitigation process" -Level INFO
 
    # Get threats if filtering
    if ($Classification) {
        $queryParams = @{
            resolved = $false
            classification = $Classification
            createdAt__gte = (Get-Date).AddDays(-$DaysBack).ToString("yyyy-MM-ddT00:00:00Z")
        }
 
        $threats = Get-S1AllPages -Endpoint "/web/api/v2.1/threats" -QueryParams $queryParams
        $ThreatIds = $threats | Select-Object -ExpandProperty id
    }
 
    if ($ThreatIds.Count -eq 0) {
        Write-S1Log -Message "No threats to process" -Level WARN
        return
    }
 
    Write-S1Log -Message "Processing $($ThreatIds.Count) threats with action: $Action" -Level INFO
 
    # Map action to endpoint
    $actionEndpoint = switch ($Action) {
        'kill' { 'kill' }
        'quarantine' { 'quarantine' }
        'remediate' { 'remediate' }
        'rollback' { 'rollback' }
        'disconnect-from-network' { 'disconnect-from-network' }
    }
 
    if (-not $PSCmdlet.ShouldProcess("$($ThreatIds.Count) threats", $Action)) {
        return
    }
 
    # Perform mitigation action
    $mitigationBody = @{
        filter = @{
            ids = $ThreatIds
        }
    }
 
    try {
        $response = Invoke-S1ApiRequest `
            -Endpoint "/web/api/v2.1/threats/actions/$actionEndpoint" `
            -Method POST -Body $mitigationBody
 
        Write-S1Log -Message "$Action action applied to $($response.data.affected) threats" -Level SUCCESS
    }
    catch {
        Write-S1Log -Message "Mitigation action failed: $($_.Exception.Message)" -Level ERROR
        throw
    }
 
    # Auto-resolve if requested
    if ($AutoResolve) {
        Start-Sleep -Seconds 2  # Allow mitigation to process
 
        $resolveBody = @{
            filter = @{
                ids = $ThreatIds
            }
            data = @{
                analystVerdict = $Verdict
            }
        }
 
        try {
            $response = Invoke-S1ApiRequest `
                -Endpoint "/web/api/v2.1/threats/analyst-verdict" `
                -Method POST -Body $resolveBody
 
            Write-S1Log -Message "Resolved $($response.data.affected) threats as $Verdict" -Level SUCCESS
        }
        catch {
            Write-S1Log -Message "Resolution failed: $($_.Exception.Message)" -Level ERROR
        }
    }
 
    return @{
        Action = $Action
        ThreatsProcessed = $ThreatIds.Count
        AutoResolved = $AutoResolve
    }
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Export-S1ThreatIntelligence.ps1 - Threat Intelligence Export

#Requires -Version 5.1
<#
.SYNOPSIS
    Exports threat intelligence data including IOCs
.DESCRIPTION
    Extracts threat indicators (hashes, IPs, domains) from threats
    for use in other security tools or threat intel platforms.
.PARAMETER DaysBack
    Number of days to analyze
.PARAMETER Format
    Output format (csv, json, stix)
.PARAMETER ExportPath
    Path to save export
.EXAMPLE
    .\Export-S1ThreatIntelligence.ps1 -DaysBack 30 -Format json -ExportPath "C:\Intel\iocs.json"
#>
 
[CmdletBinding()]
param(
    [int]$DaysBack = 30,
 
    [ValidateSet('csv', 'json', 'stix')]
    [string]$Format = 'csv',
 
    [Parameter(Mandatory = $true)]
    [string]$ExportPath
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Extracting threat intelligence for last $DaysBack days" -Level INFO
 
    $threats = Get-S1AllPages -Endpoint "/web/api/v2.1/threats" -QueryParams @{
        createdAt__gte = (Get-Date).AddDays(-$DaysBack).ToString("yyyy-MM-ddT00:00:00Z")
    }
 
    Write-S1Log -Message "Processing $($threats.Count) threats" -Level INFO
 
    $iocs = @()
 
    foreach ($threat in $threats) {
        # File hash IOCs
        if ($threat.threatInfo.sha256) {
            $iocs += [PSCustomObject]@{
                Type = 'sha256'
                Value = $threat.threatInfo.sha256
                ThreatName = $threat.threatInfo.threatName
                Classification = $threat.threatInfo.classification
                Confidence = $threat.threatInfo.confidenceLevel
                FirstSeen = $threat.threatInfo.createdAt
                Source = 'SentinelOne'
            }
        }
 
        if ($threat.threatInfo.sha1) {
            $iocs += [PSCustomObject]@{
                Type = 'sha1'
                Value = $threat.threatInfo.sha1
                ThreatName = $threat.threatInfo.threatName
                Classification = $threat.threatInfo.classification
                Confidence = $threat.threatInfo.confidenceLevel
                FirstSeen = $threat.threatInfo.createdAt
                Source = 'SentinelOne'
            }
        }
 
        if ($threat.threatInfo.md5) {
            $iocs += [PSCustomObject]@{
                Type = 'md5'
                Value = $threat.threatInfo.md5
                ThreatName = $threat.threatInfo.threatName
                Classification = $threat.threatInfo.classification
                Confidence = $threat.threatInfo.confidenceLevel
                FirstSeen = $threat.threatInfo.createdAt
                Source = 'SentinelOne'
            }
        }
    }
 
    # Deduplicate
    $uniqueIOCs = $iocs | Sort-Object Type, Value -Unique
 
    Write-S1Log -Message "Extracted $($uniqueIOCs.Count) unique IOCs" -Level SUCCESS
 
    # Export based on format
    switch ($Format) {
        'csv' {
            $uniqueIOCs | Export-Csv -Path $ExportPath -NoTypeInformation
        }
        'json' {
            $uniqueIOCs | ConvertTo-Json -Depth 5 | Out-File $ExportPath -Encoding UTF8
        }
        'stix' {
            # Basic STIX 2.1 format
            $stixBundle = @{
                type = 'bundle'
                id = "bundle--$(New-Guid)"
                objects = @()
            }
 
            foreach ($ioc in $uniqueIOCs) {
                $stixBundle.objects += @{
                    type = 'indicator'
                    id = "indicator--$(New-Guid)"
                    created = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
                    modified = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
                    name = $ioc.ThreatName
                    pattern = "[file:hashes.'$($ioc.Type.ToUpper())' = '$($ioc.Value)']"
                    pattern_type = 'stix'
                    valid_from = $ioc.FirstSeen
                }
            }
 
            $stixBundle | ConvertTo-Json -Depth 10 | Out-File $ExportPath -Encoding UTF8
        }
    }
 
    Write-S1Log -Message "IOCs exported to: $ExportPath" -Level SUCCESS
 
    # Summary
    Write-Host "`nIOC Summary:" -ForegroundColor Cyan
    $uniqueIOCs | Group-Object Type | ForEach-Object {
        Write-Host "  $($_.Name): $($_.Count)"
    }
 
    return $uniqueIOCs
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

4. DEEP VISIBILITY AUTOMATION

Invoke-S1ThreatHunt.ps1 - Scheduled Threat Hunting

#Requires -Version 5.1
<#
.SYNOPSIS
    Executes scheduled threat hunting queries via Deep Visibility
.DESCRIPTION
    Runs predefined or custom hunting queries, exports results,
    and can trigger alerts for findings.
.PARAMETER HuntName
    Name of predefined hunt (or 'custom' for custom query)
.PARAMETER CustomQuery
    Custom S1QL query string
.PARAMETER SiteId
    Limit hunt to specific site
.PARAMETER AlertOnFindings
    Send alert if results found
.EXAMPLE
    .\Invoke-S1ThreatHunt.ps1 -HuntName "EncodedPowerShell"
.EXAMPLE
    .\Invoke-S1ThreatHunt.ps1 -CustomQuery 'SrcProcName = "mimikatz.exe"' -AlertOnFindings
#>
 
[CmdletBinding()]
param(
    [Parameter(ParameterSetName = 'Predefined')]
    [ValidateSet('EncodedPowerShell', 'LSASSAccess', 'ScheduledTaskCreation',
                 'EventLogClearing', 'WebShellCreation', 'CertutilDownload',
                 'WMIPersistence', 'SuspiciousParentChild', 'All')]
    [string]$HuntName,
 
    [Parameter(ParameterSetName = 'Custom')]
    [string]$CustomQuery,
 
    [string]$SiteId,
    [int]$HoursBack = 24,
    [switch]$AlertOnFindings,
    [string]$AlertEmail,
    [string]$OutputPath = "C:\BIN\LOGS\ThreatHunting"
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
# Predefined hunting queries
$HuntingQueries = @{
    EncodedPowerShell = @{
        Name = "Encoded PowerShell Execution"
        Query = 'EventType = "Process Creation" AND SrcProcName = "powershell.exe" AND (SrcProcCmdLine ContainsCIS "-enc" OR SrcProcCmdLine ContainsCIS "-encodedcommand" OR SrcProcCmdLine ContainsCIS "frombase64string")'
        MITRE = "T1059.001"
        Severity = "High"
    }
    LSASSAccess = @{
        Name = "LSASS Memory Access"
        Query = 'TgtProcName = "lsass.exe" AND EventType = "Open Remote Process Handle" AND SrcProcName Not In ("svchost.exe","lsm.exe","wmiprvse.exe","csrss.exe","MsMpEng.exe")'
        MITRE = "T1003.001"
        Severity = "Critical"
    }
    ScheduledTaskCreation = @{
        Name = "Suspicious Scheduled Task Creation"
        Query = '(TgtProcName = "schtasks.exe" AND TgtProcCmdLine ContainsCIS "/create") AND SrcProcParentName Not In ("services.exe","svchost.exe","msiexec.exe")'
        MITRE = "T1053.005"
        Severity = "Medium"
    }
    EventLogClearing = @{
        Name = "Windows Event Log Clearing"
        Query = '(TgtProcName = "wevtutil.exe" AND TgtProcCmdLine ContainsCIS "cl ") OR (SrcProcCmdLine ContainsCIS "Clear-EventLog")'
        MITRE = "T1070.001"
        Severity = "High"
    }
    WebShellCreation = @{
        Name = "Web Shell File Creation"
        Query = 'EventType = "File Creation" AND FileFullName ContainsCIS "inetpub\wwwroot" AND TgtFileExtension In Contains Anycase ("jsp","aspx","php","asp")'
        MITRE = "T1505.003"
        Severity = "Critical"
    }
    CertutilDownload = @{
        Name = "Certutil File Download"
        Query = 'TgtProcName = "certutil.exe" AND (TgtProcCmdLine ContainsCIS "urlcache" OR TgtProcCmdLine ContainsCIS "verifyctl")'
        MITRE = "T1105"
        Severity = "High"
    }
    WMIPersistence = @{
        Name = "WMI Event Subscription Persistence"
        Query = 'SrcProcCmdLine ContainsCIS "New-CimInstance" AND SrcProcCmdLine ContainsCIS "root/subscription"'
        MITRE = "T1546.003"
        Severity = "High"
    }
    SuspiciousParentChild = @{
        Name = "Office Spawning Shell"
        Query = 'SrcProcName In ("winword.exe","excel.exe","powerpnt.exe","outlook.exe") AND TgtProcName In ("cmd.exe","powershell.exe","wscript.exe","cscript.exe","mshta.exe")'
        MITRE = "T1566.001"
        Severity = "High"
    }
}
 
function Invoke-DVQuery {
    param(
        [string]$Query,
        [string]$FromDate,
        [string]$ToDate,
        [string]$SiteId
    )
 
    $body = @{
        query = $Query
        fromDate = $FromDate
        toDate = $ToDate
        limit = 10000
    }
 
    if ($SiteId) {
        $body.siteIds = @($SiteId)
    }
 
    $response = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/dv/query" `
        -Method POST -Body $body
 
    return $response.data
}
 
try {
    Connect-S1Console -UseStoredCredentials
 
    # Ensure output directory exists
    if (-not (Test-Path $OutputPath)) {
        New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
    }
 
    $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
    $fromDate = (Get-Date).AddHours(-$HoursBack).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
    $toDate = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
 
    $huntsToRun = @()
 
    if ($CustomQuery) {
        $huntsToRun += @{
            Name = "CustomQuery"
            Query = $CustomQuery
            MITRE = "Custom"
            Severity = "Medium"
        }
    }
    elseif ($HuntName -eq 'All') {
        $huntsToRun = $HuntingQueries.Values
    }
    else {
        $huntsToRun += $HuntingQueries[$HuntName]
    }
 
    Write-S1Log -Message "Starting threat hunt - $($huntsToRun.Count) queries" -Level INFO
 
    $summaryResults = @()
    $allFindings = @()
 
    foreach ($hunt in $huntsToRun) {
        Write-S1Log -Message "Running hunt: $($hunt.Name)" -Level INFO
 
        try {
            $results = Invoke-DVQuery -Query $hunt.Query -FromDate $fromDate -ToDate $toDate -SiteId $SiteId
 
            $resultCount = if ($results) { $results.Count } else { 0 }
 
            $summaryResults += [PSCustomObject]@{
                HuntName = $hunt.Name
                MITRE = $hunt.MITRE
                Severity = $hunt.Severity
                ResultCount = $resultCount
                Status = if ($resultCount -gt 0) { "FINDINGS" } else { "Clear" }
            }
 
            if ($resultCount -gt 0) {
                Write-S1Log -Message "  [!] Found $resultCount results" -Level WARN
 
                # Export individual hunt results
                $huntFile = Join-Path $OutputPath "$($hunt.Name -replace ' ','_')_$timestamp.csv"
                $results | Export-Csv -Path $huntFile -NoTypeInformation
 
                $allFindings += $results | ForEach-Object {
                    $_ | Add-Member -NotePropertyName 'HuntName' -NotePropertyValue $hunt.Name -PassThru
                }
            }
            else {
                Write-S1Log -Message "  [OK] No findings" -Level SUCCESS
            }
        }
        catch {
            Write-S1Log -Message "  Hunt failed: $($_.Exception.Message)" -Level ERROR
            $summaryResults += [PSCustomObject]@{
                HuntName = $hunt.Name
                MITRE = $hunt.MITRE
                Severity = $hunt.Severity
                ResultCount = -1
                Status = "ERROR"
            }
        }
 
        Start-Sleep -Seconds 2  # Rate limiting
    }
 
    # Export summary
    $summaryFile = Join-Path $OutputPath "HuntingSummary_$timestamp.csv"
    $summaryResults | Export-Csv -Path $summaryFile -NoTypeInformation
 
    # Display summary
    Write-Host "`n=== Threat Hunting Summary ===" -ForegroundColor Cyan
    $summaryResults | Format-Table -AutoSize
 
    # Alert if findings
    $findingsCount = ($summaryResults | Where-Object { $_.Status -eq "FINDINGS" }).Count
    if ($findingsCount -gt 0) {
        Write-S1Log -Message "$findingsCount hunts returned findings - review required!" -Level WARN
 
        if ($AlertOnFindings -and $AlertEmail) {
            $subject = "[SentinelOne] Threat Hunt Findings - $findingsCount Detections"
            $body = "Threat hunting completed with findings:`n`n"
            $body += ($summaryResults | Where-Object { $_.Status -eq "FINDINGS" } |
                      ForEach-Object { "$($_.HuntName): $($_.ResultCount) results" }) -join "`n"
 
            # Send-MailMessage -To $AlertEmail -From "sentinelone@company.com" -Subject $subject -Body $body -SmtpServer "smtp.company.com"
            Write-S1Log -Message "Alert sent to $AlertEmail" -Level INFO
        }
    }
 
    return $summaryResults
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Search-S1IOC.ps1 - IOC Hunting Across Fleet

#Requires -Version 5.1
<#
.SYNOPSIS
    Searches for IOCs across the entire fleet using Deep Visibility
.DESCRIPTION
    Hunts for file hashes, IPs, domains, and file names across all endpoints.
    Supports bulk IOC import from file.
.PARAMETER IOCType
    Type of IOC to search
.PARAMETER IOCValue
    Single IOC value to search
.PARAMETER IOCFile
    Path to file containing IOCs (one per line)
.PARAMETER DaysBack
    Days to search back in telemetry
.EXAMPLE
    .\Search-S1IOC.ps1 -IOCType sha256 -IOCValue "abc123..."
.EXAMPLE
    .\Search-S1IOC.ps1 -IOCType domain -IOCFile "C:\Intel\bad-domains.txt"
#>
 
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [ValidateSet('sha256', 'sha1', 'md5', 'ip', 'domain', 'filename')]
    [string]$IOCType,
 
    [Parameter(ParameterSetName = 'Single')]
    [string]$IOCValue,
 
    [Parameter(ParameterSetName = 'Bulk')]
    [string]$IOCFile,
 
    [int]$DaysBack = 14,
    [string]$SiteId,
    [string]$ExportPath
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
function Build-IOCQuery {
    param($Type, $Value)
 
    switch ($Type) {
        'sha256' { return "FileSha256 = `"$Value`"" }
        'sha1' { return "FileSha1 = `"$Value`"" }
        'md5' { return "FileMD5 = `"$Value`"" }
        'ip' { return "DstIp = `"$Value`"" }
        'domain' { return "DnsRequest ContainsCIS `"$Value`"" }
        'filename' { return "FileName ContainsCIS `"$Value`"" }
    }
}
 
try {
    Connect-S1Console -UseStoredCredentials
 
    # Get IOC list
    $iocs = @()
    if ($IOCFile) {
        $iocs = Get-Content $IOCFile | Where-Object { $_ -and $_ -notmatch '^#' }
    } else {
        $iocs = @($IOCValue)
    }
 
    Write-S1Log -Message "Searching for $($iocs.Count) IOCs of type: $IOCType" -Level INFO
 
    $fromDate = (Get-Date).AddDays(-$DaysBack).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
    $toDate = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
 
    $allResults = @()
 
    foreach ($ioc in $iocs) {
        $query = Build-IOCQuery -Type $IOCType -Value $ioc
 
        try {
            $body = @{
                query = $query
                fromDate = $fromDate
                toDate = $toDate
                limit = 1000
            }
 
            if ($SiteId) { $body.siteIds = @($SiteId) }
 
            $response = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/dv/query" `
                -Method POST -Body $body
 
            $resultCount = if ($response.data) { $response.data.Count } else { 0 }
 
            if ($resultCount -gt 0) {
                Write-S1Log -Message "[!] Found $resultCount hits for: $ioc" -Level WARN
 
                foreach ($hit in $response.data) {
                    $allResults += [PSCustomObject]@{
                        IOC = $ioc
                        IOCType = $IOCType
                        AgentName = $hit.agentName
                        SiteName = $hit.siteName
                        EventType = $hit.eventType
                        ProcessName = $hit.srcProcName
                        CommandLine = $hit.srcProcCmdLine
                        FilePath = $hit.fileFullName
                        EventTime = $hit.eventTime
                    }
                }
            } else {
                Write-Verbose "No hits for: $ioc"
            }
        }
        catch {
            Write-S1Log -Message "Query failed for $ioc`: $($_.Exception.Message)" -Level ERROR
        }
 
        Start-Sleep -Milliseconds 500  # Rate limiting
    }
 
    Write-S1Log -Message "Total IOC hits: $($allResults.Count)" -Level SUCCESS
 
    if ($ExportPath -and $allResults.Count -gt 0) {
        $allResults | Export-Csv -Path $ExportPath -NoTypeInformation
        Write-S1Log -Message "Results exported to: $ExportPath" -Level SUCCESS
    }
 
    # Summary by IOC
    if ($allResults.Count -gt 0) {
        Write-Host "`nIOC Hit Summary:" -ForegroundColor Cyan
        $allResults | Group-Object IOC | ForEach-Object {
            Write-Host "  $($_.Name): $($_.Count) hits" -ForegroundColor Yellow
        }
    }
 
    return $allResults
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

5. STAR RULE MANAGEMENT

Export-S1STARRules.ps1 - STAR Rule Export

#Requires -Version 5.1
<#
.SYNOPSIS
    Exports all STAR custom detection rules for backup/migration
.DESCRIPTION
    Exports STAR rules to JSON for backup, version control, or
    deployment to other sites/accounts.
.PARAMETER SiteId
    Filter rules by site
.PARAMETER ExportPath
    Path for export file
.PARAMETER IncludeDisabled
    Include disabled rules in export
.EXAMPLE
    .\Export-S1STARRules.ps1 -ExportPath "C:\Backup\star-rules.json"
#>
 
[CmdletBinding()]
param(
    [string]$SiteId,
    [string]$ExportPath = "C:\BIN\LOGS\S1-STARRules-$(Get-Date -Format 'yyyyMMdd').json",
    [switch]$IncludeDisabled
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Exporting STAR rules" -Level INFO
 
    $queryParams = @{}
    if ($SiteId) { $queryParams.siteIds = $SiteId }
 
    $rules = Get-S1AllPages -Endpoint "/web/api/v2.1/cloud-detection/rules" -QueryParams $queryParams
 
    if (-not $IncludeDisabled) {
        $rules = $rules | Where-Object { $_.status -eq 'Active' }
    }
 
    Write-S1Log -Message "Found $($rules.Count) rules to export" -Level INFO
 
    # Format for export (remove runtime fields)
    $exportRules = $rules | ForEach-Object {
        [PSCustomObject]@{
            name = $_.name
            description = $_.description
            query = $_.query
            severity = $_.severity
            status = $_.status
            treatAsThreat = $_.treatAsThreat
            networkQuarantine = $_.networkQuarantine
            siteIds = $_.siteIds
            groupIds = $_.groupIds
            createdAt = $_.createdAt
            updatedAt = $_.updatedAt
        }
    }
 
    $export = @{
        exportDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
        exportedBy = $env:USERNAME
        ruleCount = $exportRules.Count
        rules = $exportRules
    }
 
    $export | ConvertTo-Json -Depth 10 | Out-File $ExportPath -Encoding UTF8
 
    Write-S1Log -Message "Exported $($rules.Count) rules to: $ExportPath" -Level SUCCESS
 
    # Summary
    Write-Host "`nExport Summary:" -ForegroundColor Cyan
    $rules | Group-Object severity | ForEach-Object {
        Write-Host "  $($_.Name): $($_.Count) rules"
    }
 
    return $exportRules
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Import-S1STARRules.ps1 - STAR Rule Import

#Requires -Version 5.1
<#
.SYNOPSIS
    Imports STAR rules from backup file
.DESCRIPTION
    Restores or deploys STAR rules from JSON export.
    Can target different sites than original.
.PARAMETER ImportPath
    Path to rules JSON file
.PARAMETER TargetSiteId
    Site ID to deploy rules to (overrides original)
.PARAMETER ImportAsDisabled
    Import rules as disabled for review
.EXAMPLE
    .\Import-S1STARRules.ps1 -ImportPath "C:\Backup\star-rules.json" -TargetSiteId "12345"
#>
 
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory = $true)]
    [string]$ImportPath,
 
    [string]$TargetSiteId,
    [switch]$ImportAsDisabled,
    [switch]$SkipExisting
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    if (-not (Test-Path $ImportPath)) {
        throw "Import file not found: $ImportPath"
    }
 
    $import = Get-Content $ImportPath -Raw | ConvertFrom-Json
 
    Write-S1Log -Message "Importing $($import.ruleCount) STAR rules" -Level INFO
    Write-Host "Export date: $($import.exportDate)"
 
    # Get existing rules to check for duplicates
    $existingRules = @()
    if ($SkipExisting) {
        $existingRules = Get-S1AllPages -Endpoint "/web/api/v2.1/cloud-detection/rules"
        $existingNames = $existingRules | Select-Object -ExpandProperty name
    }
 
    $results = @()
 
    foreach ($rule in $import.rules) {
        # Skip if exists
        if ($SkipExisting -and $existingNames -contains $rule.name) {
            Write-S1Log -Message "Skipping existing rule: $($rule.name)" -Level INFO
            $results += [PSCustomObject]@{
                RuleName = $rule.name
                Status = 'Skipped'
                Reason = 'Already exists'
            }
            continue
        }
 
        if (-not $PSCmdlet.ShouldProcess($rule.name, "Import STAR rule")) {
            continue
        }
 
        $body = @{
            data = @{
                name = $rule.name
                description = $rule.description
                query = $rule.query
                queryType = "events"
                severity = $rule.severity
                status = if ($ImportAsDisabled) { "Disabled" } else { $rule.status }
                treatAsThreat = $rule.treatAsThreat
            }
        }
 
        if ($TargetSiteId) {
            $body.data.siteIds = @($TargetSiteId)
        } elseif ($rule.siteIds) {
            $body.data.siteIds = $rule.siteIds
        }
 
        try {
            $response = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/cloud-detection/rules" `
                -Method POST -Body $body
 
            Write-S1Log -Message "Imported: $($rule.name)" -Level SUCCESS
            $results += [PSCustomObject]@{
                RuleName = $rule.name
                Status = 'Imported'
                RuleId = $response.data.id
            }
        }
        catch {
            Write-S1Log -Message "Failed to import $($rule.name): $($_.Exception.Message)" -Level ERROR
            $results += [PSCustomObject]@{
                RuleName = $rule.name
                Status = 'Failed'
                Reason = $_.Exception.Message
            }
        }
 
        Start-Sleep -Milliseconds 500
    }
 
    # Summary
    Write-Host "`nImport Summary:" -ForegroundColor Cyan
    $results | Group-Object Status | ForEach-Object {
        Write-Host "  $($_.Name): $($_.Count)"
    }
 
    return $results
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

6. REPORTING AND COMPLIANCE

New-S1ExecutiveReport.ps1 - Executive Summary Generator

#Requires -Version 5.1
<#
.SYNOPSIS
    Generates executive summary report for leadership
.DESCRIPTION
    Creates HTML report with threat trends, agent coverage,
    and key security metrics for executive consumption.
.PARAMETER ReportPeriod
    Period for report (daily, weekly, monthly)
.PARAMETER SiteId
    Limit to specific site
.PARAMETER OutputPath
    Path for HTML report
.EXAMPLE
    .\New-S1ExecutiveReport.ps1 -ReportPeriod weekly
#>
 
[CmdletBinding()]
param(
    [ValidateSet('daily', 'weekly', 'monthly')]
    [string]$ReportPeriod = 'weekly',
 
    [string]$SiteId,
    [string]$OutputPath = "C:\BIN\LOGS\S1-ExecutiveReport-$(Get-Date -Format 'yyyyMMdd').html",
    [string]$CompanyName = "Client"
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    $daysBack = switch ($ReportPeriod) {
        'daily' { 1 }
        'weekly' { 7 }
        'monthly' { 30 }
    }
 
    $startDate = (Get-Date).AddDays(-$daysBack).ToString("yyyy-MM-ddT00:00:00Z")
 
    Write-S1Log -Message "Generating $ReportPeriod executive report" -Level INFO
 
    # Gather metrics
    $queryParams = @{}
    if ($SiteId) { $queryParams.siteIds = $SiteId }
 
    # Agent statistics
    $agents = Get-S1AllPages -Endpoint "/web/api/v2.1/agents" -QueryParams $queryParams
    $totalAgents = $agents.Count
    $onlineAgents = ($agents | Where-Object { $_.isActive }).Count
    $infectedAgents = ($agents | Where-Object { $_.infected }).Count
    $outdatedAgents = ($agents | Where-Object {
        [version]$_.agentVersion -lt [version]"23.3.0.0"
    }).Count
 
    # Threat statistics
    $threatParams = $queryParams.Clone()
    $threatParams.createdAt__gte = $startDate
 
    $threats = Get-S1AllPages -Endpoint "/web/api/v2.1/threats" -QueryParams $threatParams
    $totalThreats = $threats.Count
    $resolvedThreats = ($threats | Where-Object { $_.threatInfo.resolved }).Count
    $malwareThreats = ($threats | Where-Object { $_.threatInfo.classification -eq 'malware' }).Count
 
    # Group by classification
    $threatsByClass = $threats | Group-Object { $_.threatInfo.classification }
 
    # Calculate trends (compare to previous period)
    $prevStartDate = (Get-Date).AddDays(-($daysBack * 2)).ToString("yyyy-MM-ddT00:00:00Z")
    $prevEndDate = $startDate
 
    $prevThreatParams = $queryParams.Clone()
    $prevThreatParams.createdAt__gte = $prevStartDate
    $prevThreatParams.createdAt__lt = $prevEndDate
 
    $prevThreats = Get-S1AllPages -Endpoint "/web/api/v2.1/threats" -QueryParams $prevThreatParams
    $threatTrend = if ($prevThreats.Count -gt 0) {
        [math]::Round((($totalThreats - $prevThreats.Count) / $prevThreats.Count) * 100, 1)
    } else { 0 }
 
    # Generate HTML
    $html = @"
<!DOCTYPE html>
\<html\>
\<head\>
    \<title\>SentinelOne Executive Report - $CompanyName</title>
    \<style\>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Arial, sans-serif; background: #f0f2f5; color: #333; }
        .header { background: linear-gradient(135deg, #6B2D9E 0%, #4A1F6F 100%); color: white; padding: 30px 40px; }
        .header h1 { font-size: 28px; margin-bottom: 5px; }
        .header .subtitle { opacity: 0.9; }
        .container { max-width: 1200px; margin: 0 auto; padding: 30px; }
        .metrics-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; }
        .metric-card { background: white; border-radius: 12px; padding: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .metric-value { font-size: 36px; font-weight: bold; color: #6B2D9E; }
        .metric-label { color: #666; font-size: 14px; margin-top: 5px; }
        .metric-trend { font-size: 12px; margin-top: 8px; }
        .trend-up { color: #e74c3c; }
        .trend-down { color: #27ae60; }
        .section { background: white; border-radius: 12px; padding: 25px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .section h2 { color: #333; margin-bottom: 20px; font-size: 18px; border-bottom: 2px solid #6B2D9E; padding-bottom: 10px; }
        table { width: 100%; border-collapse: collapse; }
        th { background: #f8f9fa; padding: 12px; text-align: left; font-weight: 600; }
        td { padding: 12px; border-bottom: 1px solid #eee; }
        .status-healthy { color: #27ae60; }
        .status-warning { color: #f39c12; }
        .status-critical { color: #e74c3c; }
        .progress-bar { background: #e9ecef; border-radius: 10px; height: 20px; overflow: hidden; }
        .progress-fill { background: #27ae60; height: 100%; transition: width 0.3s; }
        .footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
    </style>
</head>
\<body\>
    <div class="header">
        <h1>SentinelOne Security Report</h1>
        <div class="subtitle">$CompanyName | $(Get-Date -Format 'MMMM dd, yyyy') | $($ReportPeriod.ToUpper()) Summary</div>
    </div>
 
    <div class="container">
        <div class="metrics-grid">
            <div class="metric-card">
                <div class="metric-value">$totalAgents</div>
                <div class="metric-label">Total Endpoints</div>
                <div class="metric-trend">$onlineAgents online ($([math]::Round($onlineAgents/$totalAgents*100,0))%)</div>
            </div>
            <div class="metric-card">
                <div class="metric-value">$totalThreats</div>
                <div class="metric-label">Threats Detected</div>
                <div class="metric-trend $(if($threatTrend -gt 0){'trend-up'}else{'trend-down'})">
                    $(if($threatTrend -gt 0){"+$threatTrend%"}else{"$threatTrend%"}) vs previous period
                </div>
            </div>
            <div class="metric-card">
                <div class="metric-value">$resolvedThreats</div>
                <div class="metric-label">Threats Resolved</div>
                <div class="metric-trend">$([math]::Round($resolvedThreats/$totalThreats*100,0))% resolution rate</div>
            </div>
            <div class="metric-card">
                <div class="metric-value" style="color: $(if($infectedAgents -gt 0){'#e74c3c'}else{'#27ae60'})">$infectedAgents</div>
                <div class="metric-label">Currently Infected</div>
                <div class="metric-trend">Endpoints with active threats</div>
            </div>
        </div>
 
        <div class="section">
            <h2>Endpoint Protection Coverage</h2>
            <div class="progress-bar">
                <div class="progress-fill" style="width: $([math]::Round($onlineAgents/$totalAgents*100,0))%"></div>
            </div>
            <table style="margin-top: 20px;">
                \<tr\>
                    \<th\>Metric</th>
                    \<th\>Count</th>
                    \<th\>Status</th>
                </tr>
                \<tr\>
                    \<td\>Online Agents</td>
                    \<td\>$onlineAgents / $totalAgents</td>
                    <td class="$(if($onlineAgents/$totalAgents -gt 0.95){'status-healthy'}elseif($onlineAgents/$totalAgents -gt 0.85){'status-warning'}else{'status-critical'})">
                        $(if($onlineAgents/$totalAgents -gt 0.95){'Healthy'}elseif($onlineAgents/$totalAgents -gt 0.85){'Warning'}else{'Critical'})
                    </td>
                </tr>
                \<tr\>
                    \<td\>Agents Needing Update</td>
                    \<td\>$outdatedAgents</td>
                    <td class="$(if($outdatedAgents -eq 0){'status-healthy'}elseif($outdatedAgents -lt 10){'status-warning'}else{'status-critical'})">
                        $(if($outdatedAgents -eq 0){'Healthy'}elseif($outdatedAgents -lt 10){'Warning'}else{'Needs Attention'})
                    </td>
                </tr>
            </table>
        </div>
 
        <div class="section">
            <h2>Threat Breakdown</h2>
            \<table\>
                \<tr\>
                    \<th\>Classification</th>
                    \<th\>Count</th>
                    \<th\>Percentage</th>
                </tr>
                $(foreach ($class in $threatsByClass) {
                    $pct = [math]::Round($class.Count / $totalThreats * 100, 1)
                    "\<tr\>\<td\>$($class.Name)</td>\<td\>$($class.Count)</td>\<td\>$pct%</td></tr>"
                })
            </table>
        </div>
    </div>
 
    <div class="footer">
        Generated by SentinelOne Automation | $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
    </div>
</body>
</html>
"@
 
    $html | Out-File $OutputPath -Encoding UTF8
    Write-S1Log -Message "Executive report saved to: $OutputPath" -Level SUCCESS
 
    return @{
        TotalAgents = $totalAgents
        OnlineAgents = $onlineAgents
        TotalThreats = $totalThreats
        ResolvedThreats = $resolvedThreats
        ReportPath = $OutputPath
    }
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

7. MSP OPERATIONS SCRIPTS

Get-S1MSPDashboard.ps1 - Multi-Site Health Dashboard

#Requires -Version 5.1
<#
.SYNOPSIS
    Generates MSP dashboard data for all client sites
.DESCRIPTION
    Aggregates health metrics across all managed sites for
    MSP operations dashboard and reporting.
.PARAMETER ExportPath
    Path to export dashboard data
.PARAMETER IncludeDetails
    Include per-endpoint details
.EXAMPLE
    .\Get-S1MSPDashboard.ps1 -ExportPath "C:\Dashboard\msp-data.json"
#>
 
[CmdletBinding()]
param(
    [string]$ExportPath,
    [switch]$IncludeDetails
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Generating MSP dashboard data" -Level INFO
 
    # Get all sites
    $sites = Get-S1AllPages -Endpoint "/web/api/v2.1/sites"
 
    Write-S1Log -Message "Processing $($sites.Count) client sites" -Level INFO
 
    $dashboardData = @()
 
    foreach ($site in $sites) {
        Write-Verbose "Processing site: $($site.name)"
 
        # Get agents for this site
        $agents = Get-S1AllPages -Endpoint "/web/api/v2.1/agents" -QueryParams @{
            siteIds = $site.id
        }
 
        # Get threats for last 24h
        $threats = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/threats" -QueryParams @{
            siteIds = $site.id
            createdAt__gte = (Get-Date).AddDays(-1).ToString("yyyy-MM-ddT00:00:00Z")
            limit = 100
        }
 
        # Calculate metrics
        $totalAgents = $agents.Count
        $onlineAgents = ($agents | Where-Object { $_.isActive }).Count
        $offlineAgents = $totalAgents - $onlineAgents
        $infectedAgents = ($agents | Where-Object { $_.infected }).Count
        $outdatedAgents = ($agents | Where-Object {
            [version]$_.agentVersion -lt [version]"23.3.0.0"
        }).Count
 
        $threatCount24h = $threats.pagination.totalItems
        $unresolvedThreats = ($threats.data | Where-Object { -not $_.threatInfo.resolved }).Count
 
        # Determine health status
        $healthScore = 100
        if ($offlineAgents / [math]::Max($totalAgents, 1) -gt 0.1) { $healthScore -= 20 }
        if ($infectedAgents -gt 0) { $healthScore -= 30 }
        if ($outdatedAgents / [math]::Max($totalAgents, 1) -gt 0.2) { $healthScore -= 15 }
        if ($unresolvedThreats -gt 0) { $healthScore -= 20 }
 
        $healthStatus = switch ($healthScore) {
            { $_ -ge 90 } { 'Healthy' }
            { $_ -ge 70 } { 'Warning' }
            default { 'Critical' }
        }
 
        $siteData = [PSCustomObject]@{
            SiteId = $site.id
            SiteName = $site.name
            TotalAgents = $totalAgents
            OnlineAgents = $onlineAgents
            OfflineAgents = $offlineAgents
            OnlinePercentage = [math]::Round($onlineAgents / [math]::Max($totalAgents, 1) * 100, 1)
            InfectedAgents = $infectedAgents
            OutdatedAgents = $outdatedAgents
            Threats24h = $threatCount24h
            UnresolvedThreats = $unresolvedThreats
            HealthScore = $healthScore
            HealthStatus = $healthStatus
            LicenseUsed = $site.totalLicenses
            LastUpdated = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
        }
 
        if ($IncludeDetails) {
            $siteData | Add-Member -NotePropertyName 'Agents' -NotePropertyValue ($agents | Select-Object computerName, agentVersion, isActive, infected, lastActiveDate)
        }
 
        $dashboardData += $siteData
    }
 
    # Sort by health status
    $dashboardData = $dashboardData | Sort-Object HealthScore
 
    # Summary output
    Write-Host "`n=== MSP Dashboard Summary ===" -ForegroundColor Cyan
    Write-Host "Total Sites: $($dashboardData.Count)"
    Write-Host "Total Agents: $(($dashboardData | Measure-Object -Property TotalAgents -Sum).Sum)"
    Write-Host ""
 
    $dashboardData | Group-Object HealthStatus | ForEach-Object {
        $color = switch ($_.Name) {
            'Healthy' { 'Green' }
            'Warning' { 'Yellow' }
            'Critical' { 'Red' }
        }
        Write-Host "$($_.Name): $($_.Count) sites" -ForegroundColor $color
    }
 
    # Export if requested
    if ($ExportPath) {
        $export = @{
            generatedAt = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
            siteCount = $dashboardData.Count
            totalAgents = ($dashboardData | Measure-Object -Property TotalAgents -Sum).Sum
            sites = $dashboardData
        }
 
        $export | ConvertTo-Json -Depth 5 | Out-File $ExportPath -Encoding UTF8
        Write-S1Log -Message "Dashboard data exported to: $ExportPath" -Level SUCCESS
    }
 
    return $dashboardData
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Find-S1OrphanedEndpoints.ps1 - Orphaned Endpoint Detection

#Requires -Version 5.1
<#
.SYNOPSIS
    Identifies orphaned/stale endpoints across all sites
.DESCRIPTION
    Finds endpoints that haven't checked in for extended periods,
    helping identify decommissioned systems still consuming licenses.
.PARAMETER DaysOffline
    Threshold for considering endpoint orphaned (default: 30)
.PARAMETER ExportPath
    Path to export orphaned endpoints
.PARAMETER AutoDecommission
    Automatically decommission identified orphans
.EXAMPLE
    .\Find-S1OrphanedEndpoints.ps1 -DaysOffline 60 -ExportPath "C:\Reports\orphans.csv"
#>
 
[CmdletBinding(SupportsShouldProcess)]
param(
    [int]$DaysOffline = 30,
    [string]$ExportPath,
    [switch]$AutoDecommission
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Searching for endpoints offline > $DaysOffline days" -Level INFO
 
    $cutoffDate = (Get-Date).AddDays(-$DaysOffline)
 
    # Get all offline agents
    $agents = Get-S1AllPages -Endpoint "/web/api/v2.1/agents" -QueryParams @{
        isActive = $false
    }
 
    # Filter to orphaned (beyond threshold)
    $orphaned = $agents | Where-Object {
        [datetime]$_.lastActiveDate -lt $cutoffDate
    }
 
    Write-S1Log -Message "Found $($orphaned.Count) orphaned endpoints" -Level WARN
 
    $report = $orphaned | Select-Object @(
        @{N='ComputerName'; E={$_.computerName}}
        @{N='SiteName'; E={$_.siteName}}
        @{N='LastActive'; E={$_.lastActiveDate}}
        @{N='DaysOffline'; E={[math]::Round(((Get-Date) - [datetime]$_.lastActiveDate).TotalDays, 0)}}
        @{N='AgentVersion'; E={$_.agentVersion}}
        @{N='OSName'; E={$_.osName}}
        @{N='Domain'; E={$_.domain}}
        @{N='AgentId'; E={$_.id}}
    ) | Sort-Object DaysOffline -Descending
 
    # Group by site
    Write-Host "`nOrphaned Endpoints by Site:" -ForegroundColor Cyan
    $report | Group-Object SiteName | Sort-Object Count -Descending | ForEach-Object {
        Write-Host "  $($_.Name): $($_.Count) endpoints"
    }
 
    # Export if requested
    if ($ExportPath) {
        $report | Export-Csv -Path $ExportPath -NoTypeInformation
        Write-S1Log -Message "Orphaned endpoints exported to: $ExportPath" -Level SUCCESS
    }
 
    # Auto-decommission if requested
    if ($AutoDecommission -and $orphaned.Count -gt 0) {
        Write-S1Log -Message "Auto-decommissioning $($orphaned.Count) orphaned endpoints" -Level WARN
 
        if ($PSCmdlet.ShouldProcess("$($orphaned.Count) endpoints", "Decommission")) {
            $agentIds = $orphaned | Select-Object -ExpandProperty id
 
            # Process in batches
            for ($i = 0; $i -lt $agentIds.Count; $i += 100) {
                $batch = $agentIds | Select-Object -Skip $i -First 100
 
                $body = @{
                    filter = @{
                        ids = $batch
                    }
                }
 
                try {
                    $response = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents/actions/decommission" `
                        -Method POST -Body $body
                    Write-S1Log -Message "Decommissioned batch: $($response.data.affected) agents" -Level SUCCESS
                }
                catch {
                    Write-S1Log -Message "Batch decommission failed: $($_.Exception.Message)" -Level ERROR
                }
 
                Start-Sleep -Seconds 1
            }
        }
    }
 
    return $report
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

Get-S1LicenseUsage.ps1 - License Usage Tracking

#Requires -Version 5.1
<#
.SYNOPSIS
    Tracks license usage across all sites
.DESCRIPTION
    Reports on license allocation and usage per site to help
    with capacity planning and billing.
.PARAMETER ExportPath
    Path to export license report
.EXAMPLE
    .\Get-S1LicenseUsage.ps1 -ExportPath "C:\Reports\licenses.csv"
#>
 
[CmdletBinding()]
param(
    [string]$ExportPath
)
 
$ErrorActionPreference = 'Stop'
Import-Module "C:\Scripts\SentinelOne-API-Module.psm1" -Force
 
try {
    Connect-S1Console -UseStoredCredentials
 
    Write-S1Log -Message "Gathering license usage data" -Level INFO
 
    # Get account info
    $account = Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/accounts"
 
    # Get all sites with license info
    $sites = Get-S1AllPages -Endpoint "/web/api/v2.1/sites"
 
    $licenseReport = foreach ($site in $sites) {
        # Count actual agents
        $agentCount = (Invoke-S1ApiRequest -Endpoint "/web/api/v2.1/agents" -QueryParams @{
            siteIds = $site.id
            countOnly = $true
        }).pagination.totalItems
 
        [PSCustomObject]@{
            SiteName = $site.name
            SiteId = $site.id
            LicensesAllocated = $site.totalLicenses
            LicensesUsed = $agentCount
            LicensesAvailable = $site.totalLicenses - $agentCount
            UtilizationPercent = [math]::Round($agentCount / [math]::Max($site.totalLicenses, 1) * 100, 1)
            SiteState = $site.state
            Expiration = $site.expiration
        }
    }
 
    # Summary
    $totalAllocated = ($licenseReport | Measure-Object -Property LicensesAllocated -Sum).Sum
    $totalUsed = ($licenseReport | Measure-Object -Property LicensesUsed -Sum).Sum
 
    Write-Host "`n=== License Usage Summary ===" -ForegroundColor Cyan
    Write-Host "Total Licenses Allocated: $totalAllocated"
    Write-Host "Total Licenses Used: $totalUsed"
    Write-Host "Overall Utilization: $([math]::Round($totalUsed / [math]::Max($totalAllocated, 1) * 100, 1))%"
    Write-Host ""
 
    # Sites over 90% utilization
    $highUtil = $licenseReport | Where-Object { $_.UtilizationPercent -gt 90 }
    if ($highUtil) {
        Write-Host "Sites at >90% capacity:" -ForegroundColor Yellow
        $highUtil | ForEach-Object {
            Write-Host "  $($_.SiteName): $($_.UtilizationPercent)%"
        }
    }
 
    # Export if requested
    if ($ExportPath) {
        $licenseReport | Export-Csv -Path $ExportPath -NoTypeInformation
        Write-S1Log -Message "License report exported to: $ExportPath" -Level SUCCESS
    }
 
    return $licenseReport
 
} catch {
    Write-S1Log -Message "Error: $($_.Exception.Message)" -Level ERROR
    throw
} finally {
    Disconnect-S1Console
}

SCHEDULED TASK SETUP

Create-S1ScheduledTasks.ps1 - Automation Task Setup

#Requires -Version 5.1
<#
.SYNOPSIS
    Creates scheduled tasks for SentinelOne automation
.DESCRIPTION
    Sets up Windows Task Scheduler jobs for daily operations,
    threat hunting, and reporting.
.PARAMETER ScriptsPath
    Path to SentinelOne scripts
.PARAMETER LogPath
    Path for script logs
.EXAMPLE
    .\Create-S1ScheduledTasks.ps1 -ScriptsPath "C:\Scripts\SentinelOne"
#>
 
[CmdletBinding()]
param(
    [string]$ScriptsPath = "C:\Scripts\SentinelOne",
    [string]$LogPath = "C:\BIN\LOGS"
)
 
$ErrorActionPreference = 'Stop'
 
$tasks = @(
    @{
        Name = "S1-DailyHealthCheck"
        Script = "Get-S1AgentHealth.ps1"
        Schedule = "06:00"
        Description = "Daily agent health check with email alerts"
    },
    @{
        Name = "S1-ThreatHunting"
        Script = "Invoke-S1ThreatHunt.ps1 -HuntName All -AlertOnFindings"
        Schedule = "08:00"
        Description = "Daily threat hunting sweep"
    },
    @{
        Name = "S1-WeeklyReport"
        Script = "New-S1ExecutiveReport.ps1 -ReportPeriod weekly"
        Schedule = "08:00"
        Frequency = "Weekly"
        DayOfWeek = "Monday"
        Description = "Weekly executive security report"
    },
    @{
        Name = "S1-OrphanDetection"
        Script = "Find-S1OrphanedEndpoints.ps1 -DaysOffline 30"
        Schedule = "02:00"
        Frequency = "Weekly"
        DayOfWeek = "Sunday"
        Description = "Weekly orphaned endpoint detection"
    },
    @{
        Name = "S1-LicenseReport"
        Script = "Get-S1LicenseUsage.ps1"
        Schedule = "07:00"
        Frequency = "Monthly"
        Description = "Monthly license usage report"
    }
)
 
foreach ($task in $tasks) {
    Write-Host "Creating task: $($task.Name)" -ForegroundColor Cyan
 
    $scriptPath = Join-Path $ScriptsPath $task.Script.Split(' ')[0]
    $arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
 
    if ($task.Script -match ' (.+)$') {
        $arguments += " $($Matches[1])"
    }
 
    $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $arguments
 
    $trigger = switch ($task.Frequency) {
        'Weekly' {
            New-ScheduledTaskTrigger -Weekly -DaysOfWeek $task.DayOfWeek -At $task.Schedule
        }
        'Monthly' {
            New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -WeeksInterval 4 -At $task.Schedule
        }
        default {
            New-ScheduledTaskTrigger -Daily -At $task.Schedule
        }
    }
 
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
 
    $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
        -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5)
 
    try {
        Register-ScheduledTask -TaskName $task.Name -Action $action -Trigger $trigger `
            -Principal $principal -Settings $settings -Description $task.Description -Force
 
        Write-Host "  [OK] Created: $($task.Name)" -ForegroundColor Green
    }
    catch {
        Write-Host "  [FAIL] $($_.Exception.Message)" -ForegroundColor Red
    }
}
 
Write-Host "`nScheduled tasks created successfully!" -ForegroundColor Green
Write-Host "View tasks in Task Scheduler under 'S1-*'" -ForegroundColor Cyan

RELATED DOCUMENTATION

  • HOWTO- SentinelOne PowerShell API Automation
  • HOWTO- SentinelOne Deep Visibility Threat Hunting
  • HOWTO- SentinelOne STAR Custom Detection Rules
  • HOWTO- SentinelOne MSP Client Onboarding

SOURCES

  • SentinelOne API Documentation
  • SentinelOne PowerShell Examples (GitHub)
  • MITRE ATT&CK Framework

REVISION HISTORY

VersionDateAuthorChanges
1.02026-01-08CosmicBytezInitial creation

Related Reading

  • SentinelOne Control vs Complete Feature Comparison
  • SentinelOne Deep Visibility Threat Hunting
  • SentinelOne Forensics Rollback and Remediation
#sentinelone#edr#Security#threat-hunting#deployment#policy#automation#api#mitre-attack#detection-rules

Related Articles

SentinelOne Control vs Complete Feature Comparison

This document provides a comprehensive comparison between SentinelOne Singularity Control and Singularity Complete SKUs to help MSP teams understand the...

17 min read

SentinelOne Deep Visibility Threat Hunting

Deep Visibility is SentinelOne's EDR telemetry engine that provides comprehensive endpoint data collection for threat hunting, incident investigation, and...

22 min read

SentinelOne Forensics Rollback and Remediation

This document provides comprehensive procedures for forensic evidence collection, ransomware rollback, and threat remediation using SentinelOne Complete...

39 min read
Back to all HOWTOs