SCENARIO
This runbook provides comprehensive guidance for integrating SentinelOne Singularity Complete with NinjaRMM and other RMM platforms. Proper RMM integration enables centralized endpoint management, automated deployment, health monitoring, and coordinated threat response across your managed services environment.
Use this runbook when:
- Setting up SentinelOne integration with NinjaRMM for a new MSP practice
- Deploying SentinelOne agents via RMM to client environments
- Creating automated monitoring and alerting for SentinelOne health
- Building remediation scripts for common SentinelOne issues
- Integrating threat data into PSA platforms (ConnectWise, Autotask)
- Configuring proper exclusions between RMM and EDR platforms
Target Audience: MSP technicians, SOC analysts, RMM administrators
Reference Documentation:
REQUIREMENTS & ASSUMPTIONS
Prerequisites
SentinelOne Requirements:
- SentinelOne Singularity Complete SKU (for full feature integration)
- API Token with appropriate permissions (read/write for automation)
- Console Admin access for configuration
- Site tokens for each client deployment
NinjaRMM Requirements:
- NinjaRMM organization with Admin access
- Scripting enabled for PowerShell/Bash execution
- Custom fields capability
- Webhook/API access for PSA integration
Technical Requirements:
| Component | Requirement |
|---|---|
| Network | HTTPS/443 outbound to *.sentinelone.net and *.ninjarmm.com |
| PowerShell | Version 5.1+ (7.x recommended) |
| Permissions | Local admin for agent operations |
| Storage | 50MB for scripts and logs |
Assumed Environment
- MSP multi-tenant SentinelOne console with multiple client Sites
- NinjaRMM deployed across all managed endpoints
- Existing PSA integration (ConnectWise Manage or Datto Autotask)
- Standardized client onboarding process
INTEGRATION ARCHITECTURE
1.1 API-Based Integration Overview
The SentinelOne-RMM integration operates on multiple levels:
┌─────────────────────────────────────────────────────────────────┐
│ INTEGRATION ARCHITECTURE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ API Calls ┌───────────────┐ │
│ │ NinjaRMM │◄──────────────────────────►│ SentinelOne │ │
│ │ Console │ │ Console │ │
│ └──────┬───────┘ └───────┬───────┘ │
│ │ │ │
│ │ Agent Commands │ Mgmt │
│ │ Custom Fields │ Commands │
│ │ Monitoring Data │ Policies │
│ ▼ ▼ │
│ ┌──────────────┐ ┌───────────────┐ │
│ │ NinjaRMM │ Local IPC │ SentinelOne │ │
│ │ Agent │◄──────────────────────────►│ Agent │ │
│ └──────────────┘ └───────────────┘ │
│ │ │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Endpoint │ │
│ │ (Windows/ │ │
│ │ Mac/Linux) │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Integration Layers:
| Layer | Purpose | Method |
|---|---|---|
| Console-to-Console | Status sync, threat alerts | REST API, Webhooks |
| Console-to-Agent | Deployment, configuration | RMM scripts |
| Agent-to-Agent | Health monitoring | Local queries via SentinelCtl |
| Reporting | Unified dashboards | API data aggregation |
1.2 Data Flow Between Platforms
Outbound from SentinelOne to NinjaRMM:
- Agent health status (connected/disconnected)
- Protection status (enabled/disabled)
- Threat detection alerts
- Agent version information
- Scan status and results
- Deep Visibility events (Complete SKU)
Outbound from NinjaRMM to SentinelOne:
- Deployment commands (install/upgrade)
- Configuration changes via API
- Remediation actions (restart service, reconnect)
- Bulk operations (site-wide updates)
1.3 Authentication and Security
SentinelOne API Authentication:
# API Token Configuration
# Store securely - never hardcode in scripts
$s1Config = @{
ConsoleUrl = "https://usea1-partners.sentinelone.net"
ApiToken = $env:S1_API_TOKEN # From secure storage
}
# Headers for all API calls
$headers = @{
"Authorization" = "ApiToken $($s1Config.ApiToken)"
"Content-Type" = "application/json"
}
# Test authentication
try {
$response = Invoke-RestMethod -Uri "$($s1Config.ConsoleUrl)/web/api/v2.1/system/status" `
-Headers $headers -Method GET
Write-Host "API Authentication: SUCCESS" -ForegroundColor Green
Write-Host "Console Version: $($response.data.version)"
} catch {
Write-Error "API Authentication: FAILED - $($_.Exception.Message)"
}NinjaRMM Credential Storage:
For NinjaRMM scripts, use Script Variables (encrypted) or Custom Fields (secured):
# Retrieve from NinjaRMM Script Variables (set in script configuration)
$apiToken = $env:S1ApiToken # Configured as secure script variable
# Or retrieve from organization custom field (encrypted)
$apiToken = Ninja-Property-Get s1ApiToken
# Never log or expose tokens
if ($apiToken -like "eyJ*") {
Write-Host "Token loaded successfully (JWT format detected)"
}API Token Permission Scopes:
| Permission | Use Case |
|---|---|
| Accounts (Read) | List accounts, get account details |
| Sites (Read/Write) | Manage client sites, get site tokens |
| Agents (Read/Write) | Query agents, initiate actions |
| Threats (Read/Write) | Query threats, take remediation actions |
| Deep Visibility (Read) | Query DV events (Complete SKU) |
| Remote Shell (Execute) | Run remote commands (Complete SKU) |
1.4 Sync Considerations
Timing and Frequency:
- Agent status polling: Every 15-30 minutes recommended
- Threat alerts: Real-time via webhooks preferred
- Health checks: Every 4 hours for routine monitoring
- Full inventory sync: Daily during off-hours
Rate Limiting:
- SentinelOne API: 1000 requests per minute per token
- Batch operations: Use pagination with 1000 items per request
- Implement exponential backoff for 429 responses
# Rate limit handling example
function Invoke-S1ApiWithRetry {
param(
[string]$Uri,
[hashtable]$Headers,
[string]$Method = "GET",
[int]$MaxRetries = 3
)
$retryCount = 0
$baseDelay = 2
while ($retryCount -lt $MaxRetries) {
try {
return Invoke-RestMethod -Uri $Uri -Headers $Headers -Method $Method
} catch {
if ($_.Exception.Response.StatusCode -eq 429) {
$retryCount++
$delay = $baseDelay * [Math]::Pow(2, $retryCount)
Write-Warning "Rate limited. Waiting $delay seconds..."
Start-Sleep -Seconds $delay
} else {
throw
}
}
}
throw "Max retries exceeded for API call"
}NINJARMM NATIVE INTEGRATION
2.1 Enabling SentinelOne Integration in Ninja
NinjaRMM provides native SentinelOne integration for enhanced visibility.
Step 1: Access Integration Settings
- Log into NinjaRMM Admin Portal
- Navigate to Administration > Apps & Services
- Select Third-Party Integrations
- Locate SentinelOne in the security tools section
Step 2: Configure API Connection
- Click Configure next to SentinelOne
- Enter the following:
- Console URL:
https://usea1-partners.sentinelone.net(or your region) - API Token: Paste your SentinelOne API token
- Sync Interval: 15 minutes (recommended)
- Console URL:
- Click Test Connection
- If successful, click Save
Step 3: Map Organizations
- After connection, go to Organization Mapping
- Map each NinjaRMM organization to corresponding SentinelOne Site
- Use the Site ID or Site Name for matching
- Enable auto-mapping for new organizations (optional)
2.2 Configuration Steps
Custom Field Configuration:
Create these custom fields in NinjaRMM for SentinelOne data:
| Field Name | Type | Scope | Purpose |
|---|---|---|---|
s1_agent_status | Text | Device | HEALTHY, DEGRADED, OFFLINE, NOT_INSTALLED |
s1_agent_version | Text | Device | Current agent version |
s1_protection_status | Text | Device | PROTECTED, DISABLED, UNKNOWN |
s1_last_scan | Date/Time | Device | Last scan completion |
s1_threat_count | Number | Device | Active unresolved threats |
s1_console_connected | Boolean | Device | Console connectivity status |
s1_site_token | Text (Encrypted) | Organization | Site-specific deployment token |
s1_site_id | Text | Organization | SentinelOne Site ID |
Creating Custom Fields via NinjaRMM:
- Navigate to Administration > Devices > Global Custom Fields
- Click Add for each field
- Configure:
- Label: User-friendly name
- Name (API): Technical name (e.g.,
s1_agent_status) - Type: Appropriate data type
- Scope: Device or Organization
- Permissions: Technician-visible, script-writable
2.3 Custom Fields Mapping
Automated Field Population Script:
<#
.SYNOPSIS
Populates NinjaRMM custom fields with SentinelOne agent data
.DESCRIPTION
Queries local SentinelOne agent status and updates NinjaRMM custom fields
Run as scheduled task every 4 hours
#>
# Find SentinelCtl
$sentinelCtl = Get-ChildItem "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $sentinelCtl) {
Ninja-Property-Set s1_agent_status "NOT_INSTALLED"
Ninja-Property-Set s1_protection_status "UNKNOWN"
Write-Host "SentinelOne agent not found"
exit 0
}
# Get agent status
try {
$statusOutput = & $sentinelCtl.FullName status 2>&1 | Out-String
# Parse status output
$agentStatus = "UNKNOWN"
$protectionStatus = "UNKNOWN"
$connected = $false
if ($statusOutput -match "SentinelAgent service status: running") {
$agentStatus = "HEALTHY"
} elseif ($statusOutput -match "SentinelAgent service status: stopped") {
$agentStatus = "DEGRADED"
}
if ($statusOutput -match "Agent is connected to Management") {
$connected = $true
}
if ($statusOutput -match "Protection: enabled") {
$protectionStatus = "PROTECTED"
} elseif ($statusOutput -match "Protection: disabled") {
$protectionStatus = "DISABLED"
}
# Get agent version
$versionInfo = (Get-Item "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelAgent.exe" -ErrorAction SilentlyContinue).VersionInfo
$agentVersion = $versionInfo.FileVersion
# Update NinjaRMM custom fields
Ninja-Property-Set s1_agent_status $agentStatus
Ninja-Property-Set s1_protection_status $protectionStatus
Ninja-Property-Set s1_console_connected ([int]$connected)
Ninja-Property-Set s1_agent_version $agentVersion
Write-Host "Updated NinjaRMM fields:"
Write-Host " Status: $agentStatus"
Write-Host " Protection: $protectionStatus"
Write-Host " Connected: $connected"
Write-Host " Version: $agentVersion"
} catch {
Ninja-Property-Set s1_agent_status "ERROR"
Write-Error "Failed to query SentinelOne status: $($_.Exception.Message)"
exit 1
}2.4 Dashboard Widgets
Creating SentinelOne Dashboard in NinjaRMM:
- Navigate to Dashboards > Create New Dashboard
- Name: "SentinelOne Security Overview"
- Add the following widgets:
Widget 1: Agent Health Distribution (Pie Chart)
- Filter: All devices
- Group by: Custom Field
s1_agent_status - Colors: HEALTHY=Green, DEGRADED=Yellow, OFFLINE=Red, NOT_INSTALLED=Gray
Widget 2: Devices with Active Threats (Table)
- Filter:
s1_threat_count > 0 - Columns: Device Name, Organization, s1_threat_count, Last Seen
- Sort: s1_threat_count descending
Widget 3: Disconnected Agents (Table)
- Filter:
s1_console_connected = falseANDs1_agent_status != NOT_INSTALLED - Columns: Device Name, Organization, s1_last_seen
- Sort: Last Seen ascending
Widget 4: Version Compliance (Bar Chart)
- Filter: All devices where s1_agent_status = HEALTHY
- Group by: s1_agent_version
- Target version highlighted
2.5 Alert Integration
Creating SentinelOne Alerts in NinjaRMM:
Alert 1: Agent Offline
- Navigate to Administration > Policies > Conditions
- Create condition:
- Name: "SentinelOne Agent Offline"
- Type: Custom Field
- Field:
s1_agent_status - Operator: Equals
- Value: "OFFLINE"
- Duration: 30 minutes
- Actions:
- Create ticket (if PSA integrated)
- Send email notification
- Run remediation script (optional)
Alert 2: Protection Disabled
Condition: s1_protection_status = "DISABLED"
Severity: Critical
Actions: Create Priority ticket, Email SOC team
Alert 3: Active Threats
Condition: s1_threat_count > 0
Severity: Critical
Actions: Create ticket, Email SOC + Client contact, Run containment script
Alert 4: Agent Not Installed
Condition: s1_agent_status = "NOT_INSTALLED" for devices in protected organizations
Severity: High
Actions: Create ticket, Schedule deployment
AGENT DEPLOYMENT VIA RMM
3.1 NinjaRMM Deployment Script (PowerShell)
Complete Windows Deployment Script:
<#
.SYNOPSIS
SentinelOne Agent Deployment via NinjaRMM
.DESCRIPTION
Downloads and installs SentinelOne agent with proper configuration.
Handles existing installations, conflicting AV removal, and validation.
.REQUIREMENTS
- NinjaRMM agent installed
- Local administrator privileges
- Network access to SentinelOne and file storage
.NOTES
Version: 2.0
Configure Script Variables in NinjaRMM:
- S1_SITE_TOKEN (Required, Encrypted)
- S1_INSTALLER_URL (Required) - URL to hosted MSI
- S1_EXPECTED_VERSION (Optional) - For version validation
#>
#Requires -RunAsAdministrator
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# Configuration - Set via NinjaRMM Script Variables
$siteToken = $env:S1_SITE_TOKEN
$installerUrl = $env:S1_INSTALLER_URL
$expectedVersion = $env:S1_EXPECTED_VERSION
# Fallback: Get site token from organization custom field
if (-not $siteToken) {
$siteToken = Ninja-Property-Get s1_site_token
}
# Validate required parameters
if (-not $siteToken) {
Write-Error "Site token not configured. Set S1_SITE_TOKEN script variable or s1_site_token custom field."
Ninja-Property-Set s1_agent_status "DEPLOY_FAILED"
exit 1
}
if (-not $installerUrl) {
Write-Error "Installer URL not configured. Set S1_INSTALLER_URL script variable."
Ninja-Property-Set s1_agent_status "DEPLOY_FAILED"
exit 1
}
# Paths
$workDir = "C:\BIN"
$logPath = "$workDir\LOGS-$(Get-Date -Format 'yyyyMMdd')-SentinelOne-Deploy.log"
$installerPath = "$workDir\SentinelInstaller.msi"
# Ensure work directory exists
if (-not (Test-Path $workDir)) {
New-Item -Path $workDir -ItemType Directory -Force | Out-Null
}
# Logging function
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
Add-Content -Path $logPath -Value $logEntry
switch ($Level) {
"ERROR" { Write-Host $logEntry -ForegroundColor Red }
"WARNING" { Write-Host $logEntry -ForegroundColor Yellow }
"SUCCESS" { Write-Host $logEntry -ForegroundColor Green }
default { Write-Host $logEntry }
}
}
Write-Log "=== SentinelOne Deployment Started ===" "INFO"
Write-Log "Computer: $env:COMPUTERNAME" "INFO"
# Function: Check if SentinelOne is already installed
function Test-S1Installed {
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -eq "Running") {
$version = (Get-Item "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelAgent.exe" -ErrorAction SilentlyContinue).VersionInfo.FileVersion
return @{ Installed = $true; Version = $version; Running = $true }
} elseif ($service) {
return @{ Installed = $true; Version = "Unknown"; Running = $false }
}
return @{ Installed = $false; Version = $null; Running = $false }
}
# Check existing installation
$existingInstall = Test-S1Installed
if ($existingInstall.Installed -and $existingInstall.Running) {
Write-Log "SentinelOne already installed and running (v$($existingInstall.Version))" "SUCCESS"
Ninja-Property-Set s1_agent_status "HEALTHY"
Ninja-Property-Set s1_agent_version $existingInstall.Version
# Check if upgrade needed
if ($expectedVersion -and $existingInstall.Version -ne $expectedVersion) {
Write-Log "Version mismatch: Installed=$($existingInstall.Version), Expected=$expectedVersion" "WARNING"
Write-Log "Consider running upgrade script" "INFO"
}
exit 0
}
# Function: Remove conflicting security software
function Remove-ConflictingSoftware {
Write-Log "Checking for conflicting security software..." "INFO"
$conflicts = @(
@{ Name = "Windows Defender"; Service = "WinDefend"; Action = "Disable" },
@{ Name = "McAfee*"; Service = "McAfeeFramework"; Action = "Remove" },
@{ Name = "Symantec*"; Service = "SepMasterService"; Action = "Remove" },
@{ Name = "Trend Micro*"; Service = "ntrtscan"; Action = "Remove" },
@{ Name = "Webroot*"; Service = "WRSVC"; Action = "Remove" },
@{ Name = "Sophos*"; Service = "Sophos*"; Action = "Remove" }
)
foreach ($conflict in $conflicts) {
$service = Get-Service -Name $conflict.Service -ErrorAction SilentlyContinue
if ($service) {
Write-Log "Found conflicting software: $($conflict.Name)" "WARNING"
if ($conflict.Action -eq "Disable" -and $conflict.Name -eq "Windows Defender") {
# Disable Windows Defender real-time protection (S1 will manage this)
try {
Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction SilentlyContinue
Write-Log "Disabled Windows Defender real-time monitoring" "INFO"
} catch {
Write-Log "Could not disable Defender (may be managed by policy)" "WARNING"
}
}
}
}
}
# Remove conflicts
Remove-ConflictingSoftware
# Function: Test network connectivity
function Test-S1Connectivity {
Write-Log "Testing SentinelOne connectivity..." "INFO"
$endpoints = @(
"management-prod.sentinelone.net",
"usea1-partners.sentinelone.net"
)
foreach ($endpoint in $endpoints) {
$result = Test-NetConnection -ComputerName $endpoint -Port 443 -WarningAction SilentlyContinue
if ($result.TcpTestSucceeded) {
Write-Log "Connectivity to $endpoint : OK" "SUCCESS"
return $true
}
}
Write-Log "Cannot reach SentinelOne management servers" "ERROR"
return $false
}
# Test connectivity
if (-not (Test-S1Connectivity)) {
Write-Log "Network connectivity check failed" "ERROR"
Ninja-Property-Set s1_agent_status "DEPLOY_FAILED"
exit 1
}
# Download installer
Write-Log "Downloading SentinelOne installer..." "INFO"
try {
# Clean up any existing installer
if (Test-Path $installerPath) {
Remove-Item $installerPath -Force
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath -UseBasicParsing
if (Test-Path $installerPath) {
$fileSize = (Get-Item $installerPath).Length / 1MB
Write-Log "Download complete: $([math]::Round($fileSize, 2)) MB" "SUCCESS"
} else {
throw "Installer file not found after download"
}
} catch {
Write-Log "Download failed: $($_.Exception.Message)" "ERROR"
Ninja-Property-Set s1_agent_status "DEPLOY_FAILED"
exit 1
}
# Install SentinelOne
Write-Log "Installing SentinelOne agent..." "INFO"
try {
$msiLog = "$workDir\LOGS-$(Get-Date -Format 'yyyyMMdd')-S1-MSI.log"
# MSI installation arguments
$arguments = @(
"/i", "`"$installerPath`"",
"/qn", # Silent install
"SITE_TOKEN=`"$siteToken`"", # Site token
"/l*v", "`"$msiLog`"" # Verbose logging
)
Write-Log "Running: msiexec.exe $($arguments -join ' ')" "INFO"
$process = Start-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow
if ($process.ExitCode -eq 0) {
Write-Log "MSI installation completed successfully" "SUCCESS"
} elseif ($process.ExitCode -eq 3010) {
Write-Log "MSI installation completed - reboot required" "WARNING"
} else {
Write-Log "MSI installation failed with exit code: $($process.ExitCode)" "ERROR"
Write-Log "Check MSI log at: $msiLog" "INFO"
Ninja-Property-Set s1_agent_status "DEPLOY_FAILED"
exit 1
}
} catch {
Write-Log "Installation error: $($_.Exception.Message)" "ERROR"
Ninja-Property-Set s1_agent_status "DEPLOY_FAILED"
exit 1
}
# Wait for service to start
Write-Log "Waiting for SentinelOne service to start..." "INFO"
$maxWait = 180 # 3 minutes
$waited = 0
$serviceRunning = $false
while ($waited -lt $maxWait) {
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -eq "Running") {
$serviceRunning = $true
Write-Log "SentinelAgent service is running" "SUCCESS"
break
}
Start-Sleep -Seconds 5
$waited += 5
Write-Log "Waiting for service... ($waited/$maxWait seconds)" "INFO"
}
if (-not $serviceRunning) {
Write-Log "Service failed to start within timeout" "ERROR"
Ninja-Property-Set s1_agent_status "DEPLOY_FAILED"
exit 1
}
# Validate installation
Write-Log "Validating installation..." "INFO"
Start-Sleep -Seconds 10 # Allow agent to initialize
$finalCheck = Test-S1Installed
if ($finalCheck.Installed -and $finalCheck.Running) {
Write-Log "=== Deployment Successful ===" "SUCCESS"
Write-Log "Agent Version: $($finalCheck.Version)" "SUCCESS"
Ninja-Property-Set s1_agent_status "HEALTHY"
Ninja-Property-Set s1_agent_version $finalCheck.Version
Ninja-Property-Set s1_protection_status "PROTECTED"
# Cleanup
Remove-Item $installerPath -Force -ErrorAction SilentlyContinue
exit 0
} else {
Write-Log "Post-installation validation failed" "ERROR"
Ninja-Property-Set s1_agent_status "DEPLOY_FAILED"
exit 1
}3.2 Site Token Management
Retrieving Site Tokens via API:
<#
.SYNOPSIS
Retrieve SentinelOne site tokens for all client sites
.DESCRIPTION
Queries SentinelOne API and outputs site tokens for RMM configuration
#>
param(
[Parameter(Mandatory=$true)]
[string]$ConsoleUrl,
[Parameter(Mandatory=$true)]
[string]$ApiToken,
[string]$OutputPath = ".\SiteTokens.csv"
)
$headers = @{
"Authorization" = "ApiToken $ApiToken"
"Content-Type" = "application/json"
}
# Get all sites
$sites = @()
$cursor = $null
do {
$url = "$ConsoleUrl/web/api/v2.1/sites?limit=200"
if ($cursor) { $url += "&cursor=$cursor" }
$response = Invoke-RestMethod -Uri $url -Headers $headers -Method GET
$sites += $response.data.sites
$cursor = $response.pagination.nextCursor
} while ($cursor)
# Output site tokens
$siteData = $sites | Select-Object @{N='SiteName';E={$_.name}},
@{N='SiteId';E={$_.id}},
@{N='SiteToken';E={$_.registrationToken}},
@{N='AccountName';E={$_.accountName}},
@{N='SKU';E={$_.sku}},
@{N='TotalLicenses';E={$_.totalLicenses}},
@{N='ActiveLicenses';E={$_.activeLicenses}}
$siteData | Export-Csv -Path $OutputPath -NoTypeInformation
Write-Host "Exported $($sites.Count) site tokens to: $OutputPath"
# Display summary
$siteData | Format-Table SiteName, SiteId, SKU, ActiveLicenses -AutoSizeStoring Tokens in NinjaRMM:
- For each client organization in NinjaRMM:
- Navigate to Organization > Settings > Custom Fields
- Set
s1_site_token= Site registration token - Set
s1_site_id= Site ID
- Tokens are encrypted at rest in NinjaRMM
3.3 Silent Installation Parameters
Windows MSI Parameters:
| Parameter | Description | Example |
|---|---|---|
/i | Install | /i "SentinelInstaller.msi" |
/qn | Silent install | /qn |
SITE_TOKEN | Registration token | SITE_TOKEN="eyJ..." |
MANAGEMENT_TOKEN | Alternative auth | MANAGEMENT_TOKEN="..." |
/l*v | Verbose logging | /l*v "C:\Temp\s1.log" |
REBOOT=ReallySuppress | Prevent reboot | Add to suppress reboots |
Full Silent Install Command:
msiexec.exe /i "SentinelInstaller.msi" /qn SITE_TOKEN="YOUR_TOKEN" /l*v "C:\Temp\S1-Install.log" REBOOT=ReallySuppress3.4 Post-Install Verification
Verification Script:
<#
.SYNOPSIS
Post-installation verification for SentinelOne
.DESCRIPTION
Validates agent installation, connectivity, and protection status
#>
function Test-S1Installation {
$results = @{
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
ComputerName = $env:COMPUTERNAME
ServiceInstalled = $false
ServiceRunning = $false
AgentVersion = $null
ProtectionEnabled = $false
ConsoleConnected = $false
EicarTestPassed = $false
}
# Check service
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if ($service) {
$results.ServiceInstalled = $true
$results.ServiceRunning = $service.Status -eq "Running"
}
# Get version
$agentExe = Get-Item "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelAgent.exe" -ErrorAction SilentlyContinue
if ($agentExe) {
$results.AgentVersion = $agentExe.VersionInfo.FileVersion
}
# Query SentinelCtl
$sentinelCtl = Get-ChildItem "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($sentinelCtl) {
$status = & $sentinelCtl.FullName status 2>&1 | Out-String
$results.ProtectionEnabled = $status -match "Protection: enabled"
$results.ConsoleConnected = $status -match "Agent is connected to Management"
}
# EICAR test
$eicarString = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
$testPath = "$env:TEMP\eicar-test-$(Get-Random).txt"
try {
Set-Content -Path $testPath -Value $eicarString -ErrorAction Stop
Start-Sleep -Seconds 2
if (-not (Test-Path $testPath)) {
$results.EicarTestPassed = $true # File was blocked/removed
} else {
Remove-Item $testPath -Force -ErrorAction SilentlyContinue
}
} catch {
$results.EicarTestPassed = $true # Write was blocked
}
# Summary
Write-Host "`n=== SentinelOne Installation Verification ===" -ForegroundColor Cyan
Write-Host "Computer: $($results.ComputerName)"
Write-Host "Service Installed: $($results.ServiceInstalled)" -ForegroundColor $(if($results.ServiceInstalled){"Green"}else{"Red"})
Write-Host "Service Running: $($results.ServiceRunning)" -ForegroundColor $(if($results.ServiceRunning){"Green"}else{"Red"})
Write-Host "Agent Version: $($results.AgentVersion)"
Write-Host "Protection Enabled: $($results.ProtectionEnabled)" -ForegroundColor $(if($results.ProtectionEnabled){"Green"}else{"Red"})
Write-Host "Console Connected: $($results.ConsoleConnected)" -ForegroundColor $(if($results.ConsoleConnected){"Green"}else{"Red"})
Write-Host "EICAR Test Passed: $($results.EicarTestPassed)" -ForegroundColor $(if($results.EicarTestPassed){"Green"}else{"Red"})
# Overall result
$allPassed = $results.ServiceInstalled -and $results.ServiceRunning -and $results.ProtectionEnabled -and $results.ConsoleConnected
Write-Host "`nOverall Status: $(if($allPassed){'PASS'}else{'FAIL'})" -ForegroundColor $(if($allPassed){"Green"}else{"Red"})
return $results
}
Test-S1Installation3.5 Deployment Scheduling Strategies
NinjaRMM Deployment Strategies:
| Strategy | Use Case | Configuration |
|---|---|---|
| Immediate | Emergency deployment | Run once, now |
| Scheduled | Planned rollout | Run once at specific time |
| Recurring | New device onboarding | Run on schedule, skip if installed |
| Condition-based | Auto-remediation | Trigger on custom field value |
Recommended Approach - Phased Deployment:
Phase 1: Pilot (Day 1-3)
├── Target: 5% of devices per organization
├── Manual selection of test devices
└── Immediate execution, close monitoring
Phase 2: Standard Workstations (Day 4-7)
├── Target: All workstations
├── Schedule: After business hours
└── Wave size: 25% per night
Phase 3: Laptops/Remote (Day 8-10)
├── Target: Mobile devices
├── Schedule: Device check-in trigger
└── Retry on next check-in if offline
Phase 4: Servers (Day 11-14)
├── Target: Non-critical servers first
├── Schedule: Maintenance window
└── One-by-one with validation
3.6 Mac and Linux Deployment Scripts
macOS Deployment Script:
#!/bin/bash
#
# SentinelOne macOS Deployment Script
# For use with NinjaRMM or other RMM platforms
#
# Requirements:
# - Run as root
# - SITE_TOKEN environment variable set
# - Installer PKG accessible
set -e
# Configuration
SITE_TOKEN="${SITE_TOKEN:-}"
INSTALLER_URL="${S1_INSTALLER_URL:-}"
WORK_DIR="/tmp/s1_install"
LOG_FILE="/var/log/sentinelone_install.log"
PKG_PATH="$WORK_DIR/SentinelAgent.pkg"
# Logging function
log() {
local level="$1"
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}
log "INFO" "=== SentinelOne macOS Deployment Started ==="
log "INFO" "Hostname: $(hostname)"
# Check if running as root
if [[ $EUID -ne 0 ]]; then
log "ERROR" "This script must be run as root"
exit 1
fi
# Validate token
if [[ -z "$SITE_TOKEN" ]]; then
log "ERROR" "SITE_TOKEN not set"
exit 1
fi
# Check if already installed
if [[ -d "/Library/Sentinel/sentinel-agent.bundle" ]]; then
AGENT_VERSION=$(/Library/Sentinel/sentinel-agent.bundle/Contents/MacOS/sentinelctl version 2>/dev/null || echo "Unknown")
log "INFO" "SentinelOne already installed: $AGENT_VERSION"
# Check if agent is running
if launchctl list | grep -q "com.sentinelone.sentinel-agent"; then
log "SUCCESS" "Agent is running"
exit 0
else
log "WARNING" "Agent installed but not running. Attempting to start..."
launchctl load /Library/LaunchDaemons/com.sentinelone.sentinel-agent.plist 2>/dev/null || true
sleep 5
if launchctl list | grep -q "com.sentinelone.sentinel-agent"; then
log "SUCCESS" "Agent started successfully"
exit 0
fi
fi
fi
# Create work directory
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"
# Download installer
log "INFO" "Downloading installer..."
if [[ -n "$INSTALLER_URL" ]]; then
curl -sL "$INSTALLER_URL" -o "$PKG_PATH"
else
log "ERROR" "S1_INSTALLER_URL not set"
exit 1
fi
if [[ ! -f "$PKG_PATH" ]]; then
log "ERROR" "Failed to download installer"
exit 1
fi
log "INFO" "Download complete: $(ls -lh "$PKG_PATH" | awk '{print $5}')"
# Install the agent
log "INFO" "Installing SentinelOne agent..."
# Create registration token file
echo "$SITE_TOKEN" > "$WORK_DIR/com.sentinelone.registration-token"
# Install with token
installer -pkg "$PKG_PATH" -target / 2>&1 | tee -a "$LOG_FILE"
# Wait for installation to complete
sleep 10
# Verify installation
if [[ -d "/Library/Sentinel/sentinel-agent.bundle" ]]; then
log "SUCCESS" "Installation complete"
# Start the agent
launchctl load /Library/LaunchDaemons/com.sentinelone.sentinel-agent.plist 2>/dev/null || true
sleep 10
# Verify running
if launchctl list | grep -q "com.sentinelone.sentinel-agent"; then
AGENT_VERSION=$(/Library/Sentinel/sentinel-agent.bundle/Contents/MacOS/sentinelctl version 2>/dev/null || echo "Unknown")
log "SUCCESS" "Agent is running. Version: $AGENT_VERSION"
else
log "WARNING" "Agent may not be running. Check manually."
fi
else
log "ERROR" "Installation failed - agent not found"
exit 1
fi
# Cleanup
rm -rf "$WORK_DIR"
log "INFO" "=== Deployment Complete ==="
exit 0Linux Deployment Script (Debian/Ubuntu):
#!/bin/bash
#
# SentinelOne Linux Deployment Script (Debian/Ubuntu)
# For use with NinjaRMM or other RMM platforms
#
# Requirements:
# - Run as root
# - SITE_TOKEN environment variable set
# - dpkg available
set -e
# Configuration
SITE_TOKEN="${SITE_TOKEN:-}"
INSTALLER_URL="${S1_INSTALLER_URL:-}"
WORK_DIR="/tmp/s1_install"
LOG_FILE="/var/log/sentinelone_install.log"
DEB_PATH="$WORK_DIR/SentinelAgent.deb"
# Logging function
log() {
local level="$1"
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}
log "INFO" "=== SentinelOne Linux Deployment Started ==="
log "INFO" "Hostname: $(hostname)"
log "INFO" "OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
# Check if running as root
if [[ $EUID -ne 0 ]]; then
log "ERROR" "This script must be run as root"
exit 1
fi
# Validate token
if [[ -z "$SITE_TOKEN" ]]; then
log "ERROR" "SITE_TOKEN not set"
exit 1
fi
# Check if already installed
if command -v sentinelctl &> /dev/null; then
AGENT_VERSION=$(sentinelctl version 2>/dev/null || echo "Unknown")
log "INFO" "SentinelOne already installed: $AGENT_VERSION"
# Check if agent is running
if systemctl is-active --quiet sentinelone; then
log "SUCCESS" "Agent is running"
exit 0
else
log "WARNING" "Agent installed but not running. Attempting to start..."
systemctl start sentinelone
sleep 5
if systemctl is-active --quiet sentinelone; then
log "SUCCESS" "Agent started successfully"
exit 0
fi
fi
fi
# Create work directory
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"
# Download installer
log "INFO" "Downloading installer..."
if [[ -n "$INSTALLER_URL" ]]; then
curl -sL "$INSTALLER_URL" -o "$DEB_PATH"
else
log "ERROR" "S1_INSTALLER_URL not set"
exit 1
fi
if [[ ! -f "$DEB_PATH" ]]; then
log "ERROR" "Failed to download installer"
exit 1
fi
log "INFO" "Download complete: $(ls -lh "$DEB_PATH" | awk '{print $5}')"
# Install the agent
log "INFO" "Installing SentinelOne agent..."
# Export token for installer
export S1_AGENT_INSTALL_REGISTRATION_TOKEN="$SITE_TOKEN"
# Install package
dpkg -i "$DEB_PATH" 2>&1 | tee -a "$LOG_FILE"
# Fix any dependency issues
apt-get install -f -y 2>&1 | tee -a "$LOG_FILE"
# Wait for installation to complete
sleep 10
# Verify installation
if command -v sentinelctl &> /dev/null; then
log "SUCCESS" "Installation complete"
# Start and enable the service
systemctl enable sentinelone
systemctl start sentinelone
sleep 10
# Verify running
if systemctl is-active --quiet sentinelone; then
AGENT_VERSION=$(sentinelctl version 2>/dev/null || echo "Unknown")
log "SUCCESS" "Agent is running. Version: $AGENT_VERSION"
# Verify management connectivity
if sentinelctl management_status | grep -q "Connected"; then
log "SUCCESS" "Connected to management console"
else
log "WARNING" "Not connected to management. Check network connectivity."
fi
else
log "WARNING" "Agent may not be running. Check: systemctl status sentinelone"
fi
else
log "ERROR" "Installation failed - sentinelctl not found"
exit 1
fi
# Cleanup
rm -rf "$WORK_DIR"
log "INFO" "=== Deployment Complete ==="
exit 0Linux Deployment Script (RHEL/CentOS):
#!/bin/bash
#
# SentinelOne Linux Deployment Script (RHEL/CentOS)
#
# Similar to Debian but uses rpm/yum instead of dpkg/apt
set -e
SITE_TOKEN="${SITE_TOKEN:-}"
INSTALLER_URL="${S1_INSTALLER_URL:-}"
WORK_DIR="/tmp/s1_install"
LOG_FILE="/var/log/sentinelone_install.log"
RPM_PATH="$WORK_DIR/SentinelAgent.rpm"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$1] ${@:2}" | tee -a "$LOG_FILE"
}
log "INFO" "=== SentinelOne RHEL/CentOS Deployment Started ==="
# Validate
[[ $EUID -ne 0 ]] && { log "ERROR" "Must run as root"; exit 1; }
[[ -z "$SITE_TOKEN" ]] && { log "ERROR" "SITE_TOKEN not set"; exit 1; }
# Check if installed
if command -v sentinelctl &> /dev/null; then
if systemctl is-active --quiet sentinelone; then
log "SUCCESS" "Already installed and running"
exit 0
fi
fi
# Download and install
mkdir -p "$WORK_DIR" && cd "$WORK_DIR"
curl -sL "$INSTALLER_URL" -o "$RPM_PATH"
export S1_AGENT_INSTALL_REGISTRATION_TOKEN="$SITE_TOKEN"
rpm -i --nodigest "$RPM_PATH" 2>&1 | tee -a "$LOG_FILE"
# Start service
systemctl enable sentinelone
systemctl start sentinelone
sleep 10
# Verify
if systemctl is-active --quiet sentinelone; then
log "SUCCESS" "Deployment complete: $(sentinelctl version)"
else
log "ERROR" "Service not running"
exit 1
fi
rm -rf "$WORK_DIR"
exit 0MONITORING AND ALERTING
4.1 Creating Custom Monitors for S1 Health
NinjaRMM Script Monitor - SentinelOne Health Check:
<#
.SYNOPSIS
SentinelOne Health Monitor for NinjaRMM
.DESCRIPTION
Comprehensive health check that returns exit codes for monitoring
Exit 0 = Healthy, Exit 1 = Warning, Exit 2 = Critical
.NOTES
Schedule: Every 4 hours
Create alert condition on non-zero exit code
#>
$ErrorActionPreference = 'SilentlyContinue'
# Initialize status
$status = "HEALTHY"
$exitCode = 0
$issues = @()
# Check if agent is installed
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if (-not $service) {
$status = "NOT_INSTALLED"
Ninja-Property-Set s1_agent_status $status
Write-Host "CRITICAL: SentinelOne agent not installed"
exit 2
}
# Check service status
if ($service.Status -ne "Running") {
$issues += "Service not running (Status: $($service.Status))"
$status = "DEGRADED"
$exitCode = 1
# Attempt to start service
try {
Start-Service -Name "SentinelAgent" -ErrorAction Stop
Start-Sleep -Seconds 10
$service.Refresh()
if ($service.Status -eq "Running") {
$issues += "Service auto-recovered"
}
} catch {
$status = "CRITICAL"
$exitCode = 2
}
}
# Query SentinelCtl for detailed status
$sentinelCtl = Get-ChildItem "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($sentinelCtl) {
$statusOutput = & $sentinelCtl.FullName status 2>&1 | Out-String
# Check protection status
if ($statusOutput -notmatch "Protection: enabled") {
$issues += "Protection is disabled"
$status = "CRITICAL"
$exitCode = 2
}
# Check management connectivity
if ($statusOutput -notmatch "Agent is connected to Management") {
$issues += "Not connected to management console"
if ($exitCode -lt 1) { $exitCode = 1 }
if ($status -eq "HEALTHY") { $status = "DEGRADED" }
# Check last connection time
$lastConnected = $statusOutput | Select-String -Pattern "Last connected: (.+)" | ForEach-Object { $_.Matches.Groups[1].Value }
if ($lastConnected) {
$issues += "Last connected: $lastConnected"
}
}
# Check for pending actions
if ($statusOutput -match "pending reboot") {
$issues += "Pending reboot required"
if ($status -eq "HEALTHY") { $status = "DEGRADED" }
if ($exitCode -lt 1) { $exitCode = 1 }
}
# Get agent version
$version = (Get-Item "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelAgent.exe").VersionInfo.FileVersion
Ninja-Property-Set s1_agent_version $version
} else {
$issues += "SentinelCtl not found"
$status = "DEGRADED"
if ($exitCode -lt 1) { $exitCode = 1 }
}
# Check for active threats (via registry or log parsing)
try {
$threatCount = (Get-ItemProperty "HKLM:\SOFTWARE\SentinelOne\*" -Name "ThreatCount" -ErrorAction SilentlyContinue).ThreatCount
if ($threatCount -and $threatCount -gt 0) {
$issues += "$threatCount active threat(s) detected"
$status = "CRITICAL"
$exitCode = 2
Ninja-Property-Set s1_threat_count $threatCount
} else {
Ninja-Property-Set s1_threat_count 0
}
} catch {
# Threat count not available via registry
}
# Update NinjaRMM custom fields
Ninja-Property-Set s1_agent_status $status
Ninja-Property-Set s1_protection_status $(if($statusOutput -match "Protection: enabled"){"PROTECTED"}else{"DISABLED"})
Ninja-Property-Set s1_console_connected $(if($statusOutput -match "connected to Management"){1}else{0})
# Output summary
Write-Host "=== SentinelOne Health Check ==="
Write-Host "Status: $status"
Write-Host "Exit Code: $exitCode"
if ($issues.Count -gt 0) {
Write-Host "`nIssues Detected:"
$issues | ForEach-Object { Write-Host " - $_" }
}
exit $exitCode4.2 Alert Conditions and Thresholds
Recommended Alert Thresholds:
| Condition | Threshold | Severity | Response Time |
|---|---|---|---|
| Agent Not Installed | Any device in protected org | High | 4 hours |
| Agent Offline | > 30 minutes | Medium | 4 hours |
| Agent Offline | > 24 hours | High | 1 hour |
| Service Stopped | Any | Critical | 15 minutes |
| Protection Disabled | Any | Critical | Immediate |
| Console Disconnected | > 1 hour | Medium | 4 hours |
| Console Disconnected | > 4 hours | High | 1 hour |
| Active Threats | > 0 | Critical | Immediate |
| Version Out of Date | > 2 versions behind | Low | 1 week |
4.3 Ticket Creation Automation
ConnectWise Manage Integration Script:
<#
.SYNOPSIS
Creates ConnectWise Manage tickets for SentinelOne alerts
.DESCRIPTION
Called by NinjaRMM when S1 alert conditions are met
#>
param(
[Parameter(Mandatory=$true)]
[string]$DeviceName,
[Parameter(Mandatory=$true)]
[ValidateSet("AgentOffline", "ProtectionDisabled", "ThreatDetected", "ServiceStopped")]
[string]$AlertType,
[string]$AdditionalInfo = ""
)
# ConnectWise API Configuration
$cwConfig = @{
BaseUrl = "https://api-na.myconnectwise.net/v4_6_release/apis/3.0"
CompanyId = $env:CW_COMPANY_ID
PublicKey = $env:CW_PUBLIC_KEY
PrivateKey = $env:CW_PRIVATE_KEY
}
# Build authentication header
$authString = "$($cwConfig.CompanyId)+$($cwConfig.PublicKey):$($cwConfig.PrivateKey)"
$authBytes = [System.Text.Encoding]::UTF8.GetBytes($authString)
$authBase64 = [Convert]::ToBase64String($authBytes)
$headers = @{
"Authorization" = "Basic $authBase64"
"Content-Type" = "application/json"
"clientId" = $env:CW_CLIENT_ID
}
# Define ticket templates
$ticketTemplates = @{
"AgentOffline" = @{
Summary = "[SentinelOne] Agent Offline - $DeviceName"
Priority = 3
Status = "New"
Board = "Security Alerts"
InitialDescription = @"
SentinelOne agent on $DeviceName has gone offline.
Device: $DeviceName
Alert Type: Agent Offline
Time Detected: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Additional Info: $AdditionalInfo
Recommended Actions:
1. Verify device is powered on and connected to network
2. Check if device can reach SentinelOne management servers
3. Restart SentinelAgent service if device is accessible
4. Re-deploy agent if service cannot be recovered
"@
}
"ProtectionDisabled" = @{
Summary = "[SentinelOne] CRITICAL: Protection Disabled - $DeviceName"
Priority = 1
Status = "New"
Board = "Security Alerts"
InitialDescription = @"
CRITICAL: SentinelOne protection has been disabled on $DeviceName.
Device: $DeviceName
Alert Type: Protection Disabled
Time Detected: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Additional Info: $AdditionalInfo
IMMEDIATE ACTIONS REQUIRED:
1. Investigate why protection was disabled
2. Check for signs of tampering or compromise
3. Re-enable protection via console or locally
4. Review Deep Visibility logs for suspicious activity
5. Consider network isolation if compromise suspected
"@
}
"ThreatDetected" = @{
Summary = "[SentinelOne] THREAT DETECTED - $DeviceName"
Priority = 1
Status = "New"
Board = "Security Alerts"
InitialDescription = @"
SECURITY ALERT: SentinelOne has detected a threat on $DeviceName.
Device: $DeviceName
Alert Type: Threat Detected
Time Detected: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Additional Info: $AdditionalInfo
IMMEDIATE ACTIONS REQUIRED:
1. Log into SentinelOne console to review threat details
2. Assess threat severity and potential spread
3. Consider network isolation for the device
4. Follow incident response procedures
5. Document findings and actions taken
"@
}
"ServiceStopped" = @{
Summary = "[SentinelOne] Service Stopped - $DeviceName"
Priority = 2
Status = "New"
Board = "Security Alerts"
InitialDescription = @"
SentinelOne agent service has stopped on $DeviceName.
Device: $DeviceName
Alert Type: Service Stopped
Time Detected: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Additional Info: $AdditionalInfo
Recommended Actions:
1. Attempt to restart SentinelAgent service remotely
2. Check Windows Event Logs for service crash details
3. Verify no conflicting software was installed
4. Consider agent reinstallation if service won't start
"@
}
}
$template = $ticketTemplates[$AlertType]
# Create ticket
$ticketBody = @{
summary = $template.Summary
board = @{ name = $template.Board }
priority = @{ id = $template.Priority }
status = @{ name = $template.Status }
initialDescription = $template.InitialDescription
type = @{ name = "Security" }
} | ConvertTo-Json -Depth 5
try {
$response = Invoke-RestMethod -Uri "$($cwConfig.BaseUrl)/service/tickets" `
-Method POST -Headers $headers -Body $ticketBody
Write-Host "Ticket created: #$($response.id) - $($response.summary)"
# Return ticket ID to NinjaRMM
Ninja-Property-Set s1_last_ticket_id $response.id
} catch {
Write-Error "Failed to create ticket: $($_.Exception.Message)"
exit 1
}4.4 Escalation Workflows
NinjaRMM Alert Escalation Configuration:
# SentinelOne Alert Escalation Matrix
Alert: SentinelOne Agent Offline
- Initial:
- Wait: 30 minutes
- Action: Create low priority ticket
- Escalation 1:
- After: 4 hours unresolved
- Action: Upgrade to medium priority
- Notify: On-call technician
- Escalation 2:
- After: 24 hours unresolved
- Action: Upgrade to high priority
- Notify: Service manager
Alert: SentinelOne Protection Disabled
- Initial:
- Wait: 0 (immediate)
- Action: Create critical ticket
- Notify: SOC team + Service manager
- Escalation 1:
- After: 1 hour unresolved
- Action: Page on-call security
- Notify: Client security contact
Alert: Threat Detected
- Initial:
- Wait: 0 (immediate)
- Action: Create critical ticket
- Notify: SOC team
- Run: Containment assessment script
- Escalation 1:
- After: 30 minutes unresolved
- Action: Page security lead
- Notify: Client POC
- Escalation 2:
- After: 2 hours unresolved
- Action: Engage incident response team4.5 PSA Integration (ConnectWise, Autotask)
Autotask/Datto PSA Integration Script:
<#
.SYNOPSIS
Creates Autotask tickets for SentinelOne alerts
.DESCRIPTION
Autotask REST API integration for S1 alerting
#>
param(
[Parameter(Mandatory=$true)]
[string]$DeviceName,
[Parameter(Mandatory=$true)]
[string]$AlertType,
[Parameter(Mandatory=$true)]
[string]$CompanyId
)
# Autotask API Configuration
$atConfig = @{
BaseUrl = "https://webservices.autotask.net/atservicesrest/V1.0"
Username = $env:AT_API_USER
Secret = $env:AT_API_SECRET
IntegrationCode = $env:AT_INTEGRATION_CODE
}
$headers = @{
"ApiIntegrationCode" = $atConfig.IntegrationCode
"UserName" = $atConfig.Username
"Secret" = $atConfig.Secret
"Content-Type" = "application/json"
}
# Map alert to priority/queue
$alertConfig = @{
"ThreatDetected" = @{ Priority = 1; QueueId = 29683001; IssueType = 8 } # Critical, Security Queue
"ProtectionDisabled" = @{ Priority = 1; QueueId = 29683001; IssueType = 8 }
"AgentOffline" = @{ Priority = 3; QueueId = 29683002; IssueType = 7 } # Medium, Monitoring Queue
"ServiceStopped" = @{ Priority = 2; QueueId = 29683002; IssueType = 7 }
}
$config = $alertConfig[$AlertType]
$ticketBody = @{
companyId = [int]$CompanyId
title = "[SentinelOne] $AlertType - $DeviceName"
description = "SentinelOne alert generated for $DeviceName. Alert type: $AlertType. Time: $(Get-Date -Format 'o')"
priority = $config.Priority
queueId = $config.QueueId
issueType = $config.IssueType
status = 1 # New
} | ConvertTo-Json
try {
$response = Invoke-RestMethod -Uri "$($atConfig.BaseUrl)/Tickets" `
-Method POST -Headers $headers -Body $ticketBody
Write-Host "Autotask ticket created: $($response.item.id)"
} catch {
Write-Error "Failed to create Autotask ticket: $($_.Exception.Message)"
}AUTOMATED REMEDIATION VIA RMM
5.1 Scripted Response Actions
Service Recovery Script:
<#
.SYNOPSIS
Automated SentinelOne service recovery
.DESCRIPTION
Attempts to recover SentinelOne agent service with multiple strategies
#>
$ErrorActionPreference = 'Stop'
$logPath = "C:\BIN\LOGS-$(Get-Date -Format 'yyyyMMdd')-S1-Recovery.log"
function Write-Log {
param([string]$Message)
$entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
Add-Content -Path $logPath -Value $entry
Write-Host $entry
}
Write-Log "=== SentinelOne Service Recovery Started ==="
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if (-not $service) {
Write-Log "ERROR: SentinelAgent service not found - agent may need reinstallation"
Ninja-Property-Set s1_agent_status "NOT_INSTALLED"
exit 1
}
# Strategy 1: Simple restart
Write-Log "Attempting simple service restart..."
try {
Restart-Service -Name "SentinelAgent" -Force -ErrorAction Stop
Start-Sleep -Seconds 15
$service.Refresh()
if ($service.Status -eq "Running") {
Write-Log "SUCCESS: Service restarted successfully"
Ninja-Property-Set s1_agent_status "HEALTHY"
exit 0
}
} catch {
Write-Log "WARNING: Simple restart failed - $($_.Exception.Message)"
}
# Strategy 2: Stop dependent services, restart, start dependencies
Write-Log "Attempting restart with dependency handling..."
try {
$dependentServices = Get-Service -Name "SentinelAgent" -DependentServices -ErrorAction SilentlyContinue
foreach ($dep in $dependentServices) {
if ($dep.Status -eq "Running") {
Stop-Service -Name $dep.Name -Force -ErrorAction SilentlyContinue
}
}
Stop-Service -Name "SentinelAgent" -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 5
Start-Service -Name "SentinelAgent" -ErrorAction Stop
Start-Sleep -Seconds 15
foreach ($dep in $dependentServices) {
Start-Service -Name $dep.Name -ErrorAction SilentlyContinue
}
$service.Refresh()
if ($service.Status -eq "Running") {
Write-Log "SUCCESS: Service recovered with dependency handling"
Ninja-Property-Set s1_agent_status "HEALTHY"
exit 0
}
} catch {
Write-Log "WARNING: Dependency restart failed - $($_.Exception.Message)"
}
# Strategy 3: Kill process and restart
Write-Log "Attempting process kill and restart..."
try {
$processes = Get-Process -Name "SentinelAgent*" -ErrorAction SilentlyContinue
$processes | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 5
Start-Service -Name "SentinelAgent" -ErrorAction Stop
Start-Sleep -Seconds 15
$service.Refresh()
if ($service.Status -eq "Running") {
Write-Log "SUCCESS: Service recovered after process kill"
Ninja-Property-Set s1_agent_status "HEALTHY"
exit 0
}
} catch {
Write-Log "WARNING: Process kill approach failed - $($_.Exception.Message)"
}
# All strategies failed
Write-Log "ERROR: All recovery strategies failed - manual intervention required"
Ninja-Property-Set s1_agent_status "RECOVERY_FAILED"
exit 15.2 Isolation Triggers
Network Isolation Script (via S1 API):
<#
.SYNOPSIS
Isolates endpoint via SentinelOne API
.DESCRIPTION
Called when critical threat detected - isolates device from network
#>
param(
[Parameter(Mandatory=$true)]
[string]$AgentId,
[Parameter(Mandatory=$true)]
[string]$Reason
)
$consoleUrl = $env:S1_CONSOLE_URL
$apiToken = $env:S1_API_TOKEN
$headers = @{
"Authorization" = "ApiToken $apiToken"
"Content-Type" = "application/json"
}
# Disconnect from network
$body = @{
filter = @{
ids = @($AgentId)
}
} | ConvertTo-Json
try {
$response = Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/agents/actions/disconnect" `
-Method POST -Headers $headers -Body $body
Write-Host "Endpoint isolated successfully"
Write-Host "Affected: $($response.data.affected) agent(s)"
# Log isolation event
$logEntry = @{
Timestamp = Get-Date -Format "o"
AgentId = $AgentId
Action = "NetworkIsolation"
Reason = $Reason
InitiatedBy = "Automated-RMM"
}
# Store in NinjaRMM custom field for audit
Ninja-Property-Set s1_last_isolation_time (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
} catch {
Write-Error "Failed to isolate endpoint: $($_.Exception.Message)"
exit 1
}5.3 Threat Notification Scripts
Threat Alert Notification Script:
<#
.SYNOPSIS
Sends formatted threat notifications via multiple channels
.DESCRIPTION
Queries S1 API for threat details and sends notifications
#>
param(
[Parameter(Mandatory=$true)]
[string]$ThreatId
)
$consoleUrl = $env:S1_CONSOLE_URL
$apiToken = $env:S1_API_TOKEN
$teamsWebhook = $env:TEAMS_WEBHOOK_URL
$slackWebhook = $env:SLACK_WEBHOOK_URL
$headers = @{
"Authorization" = "ApiToken $apiToken"
"Content-Type" = "application/json"
}
# Get threat details
$threat = Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/threats?ids=$ThreatId" -Headers $headers
$threatData = $threat.data[0]
# Build notification message
$notification = @{
ComputerName = $threatData.agentComputerName
SiteName = $threatData.siteName
ThreatName = $threatData.threatInfo.threatName
Classification = $threatData.threatInfo.classification
FilePath = $threatData.threatInfo.filePath
SHA256 = $threatData.threatInfo.sha256
MitigationStatus = $threatData.mitigationStatus
DetectedAt = $threatData.threatInfo.createdAt
ConsoleLink = "$consoleUrl/analyze/threats/$ThreatId/overview"
}
# Teams notification
if ($teamsWebhook) {
$teamsCard = @{
"@type" = "MessageCard"
"@context" = "http://schema.org/extensions"
themeColor = "FF0000"
summary = "SentinelOne Threat Alert"
sections = @(
@{
activityTitle = "THREAT DETECTED: $($notification.ThreatName)"
activitySubtitle = "on $($notification.ComputerName)"
facts = @(
@{ name = "Site"; value = $notification.SiteName }
@{ name = "Classification"; value = $notification.Classification }
@{ name = "File Path"; value = $notification.FilePath }
@{ name = "Status"; value = $notification.MitigationStatus }
@{ name = "Detected"; value = $notification.DetectedAt }
)
markdown = $true
}
)
potentialAction = @(
@{
"@type" = "OpenUri"
name = "View in Console"
targets = @(@{ os = "default"; uri = $notification.ConsoleLink })
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $teamsWebhook -Method POST -Body $teamsCard -ContentType "application/json"
}
# Slack notification
if ($slackWebhook) {
$slackMessage = @{
attachments = @(
@{
color = "danger"
title = "SentinelOne Threat Alert"
text = "Threat detected on $($notification.ComputerName)"
fields = @(
@{ title = "Threat"; value = $notification.ThreatName; short = $true }
@{ title = "Classification"; value = $notification.Classification; short = $true }
@{ title = "Site"; value = $notification.SiteName; short = $true }
@{ title = "Status"; value = $notification.MitigationStatus; short = $true }
@{ title = "File Path"; value = $notification.FilePath; short = $false }
)
actions = @(
@{ type = "button"; text = "View in Console"; url = $notification.ConsoleLink }
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $slackWebhook -Method POST -Body $slackMessage -ContentType "application/json"
}
Write-Host "Threat notifications sent successfully"5.4 Health Check Remediation
Comprehensive Health Remediation Script:
<#
.SYNOPSIS
Comprehensive SentinelOne health check with auto-remediation
.DESCRIPTION
Checks multiple health aspects and attempts automatic fixes
#>
$ErrorActionPreference = 'SilentlyContinue'
$remediationPerformed = $false
$issues = @()
# Check 1: Service Status
$service = Get-Service -Name "SentinelAgent"
if ($service.Status -ne "Running") {
$issues += "Service not running"
try {
Start-Service -Name "SentinelAgent" -ErrorAction Stop
$remediationPerformed = $true
Write-Host "Remediation: Started SentinelAgent service"
} catch {
Write-Host "Failed to start service: $($_.Exception.Message)"
}
}
# Check 2: Agent executable exists
$agentPath = Get-ChildItem "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelAgent.exe" | Select-Object -First 1
if (-not $agentPath) {
$issues += "Agent executable not found"
# Cannot auto-remediate - flag for reinstall
Ninja-Property-Set s1_agent_status "REINSTALL_REQUIRED"
}
# Check 3: Management connectivity
$sentinelCtl = Get-ChildItem "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" | Select-Object -First 1
if ($sentinelCtl) {
$status = & $sentinelCtl.FullName status 2>&1 | Out-String
if ($status -notmatch "connected to Management") {
$issues += "Not connected to management"
# Check network connectivity
$mgmtServer = "usea1-partners.sentinelone.net"
if (-not (Test-NetConnection -ComputerName $mgmtServer -Port 443 -WarningAction SilentlyContinue).TcpTestSucceeded) {
$issues += "Cannot reach management server - check firewall/proxy"
} else {
# Network OK but not connected - restart may help
Restart-Service -Name "SentinelAgent" -Force -ErrorAction SilentlyContinue
$remediationPerformed = $true
Write-Host "Remediation: Restarted service to re-establish connection"
}
}
# Check 4: Protection status
if ($status -notmatch "Protection: enabled") {
$issues += "Protection is disabled"
# Cannot auto-enable - requires console action
Ninja-Property-Set s1_protection_status "DISABLED"
}
}
# Check 5: Disk space for agent operation
$diskFree = (Get-PSDrive C).Free / 1GB
if ($diskFree -lt 1) {
$issues += "Low disk space ($([math]::Round($diskFree, 2)) GB free)"
}
# Check 6: Agent log errors
$logPath = "C:\ProgramData\SentinelOne\Logs\Agent.log"
if (Test-Path $logPath) {
$recentErrors = Get-Content $logPath -Tail 100 | Select-String -Pattern "ERROR|CRITICAL" | Select-Object -Last 5
if ($recentErrors) {
$issues += "Recent errors in agent log"
}
}
# Summary
Write-Host "=== Health Check Summary ==="
Write-Host "Issues Found: $($issues.Count)"
Write-Host "Remediation Performed: $remediationPerformed"
if ($issues.Count -gt 0) {
Write-Host "`nIssues:"
$issues | ForEach-Object { Write-Host " - $_" }
}
# Update status
if ($issues.Count -eq 0) {
Ninja-Property-Set s1_agent_status "HEALTHY"
exit 0
} elseif ($remediationPerformed) {
Start-Sleep -Seconds 30
# Recheck after remediation
$service = Get-Service -Name "SentinelAgent"
if ($service.Status -eq "Running") {
Ninja-Property-Set s1_agent_status "HEALTHY"
exit 0
}
}
Ninja-Property-Set s1_agent_status "DEGRADED"
exit 1REPORTING INTEGRATION
6.1 Custom Report Templates
NinjaRMM Report Template - SentinelOne Security Summary:
# SentinelOne Security Report
Generated: {{report.date}}
Period: {{report.startDate}} to {{report.endDate}}
## Executive Summary
| Metric | Value |
|--------|-------|
| Total Protected Endpoints | {{s1.totalAgents}} |
| Agent Health Rate | {{s1.healthyPercentage}}% |
| Threats Detected | {{s1.threatsDetected}} |
| Threats Mitigated | {{s1.threatsMitigated}} |
| Protection Coverage | {{s1.protectionCoverage}}% |
## Agent Status Distribution
{{chart.agentStatusPie}}
- Healthy: {{s1.healthyCount}}
- Degraded: {{s1.degradedCount}}
- Offline: {{s1.offlineCount}}
- Not Installed: {{s1.notInstalledCount}}
## Threat Activity
### Threats by Classification
{{chart.threatsByClassification}}
### Threat Timeline
{{chart.threatTimeline}}
## Version Compliance
Current Recommended Version: {{s1.recommendedVersion}}
{{chart.versionDistribution}}
## Recommendations
1. {{recommendation.1}}
2. {{recommendation.2}}
3. {{recommendation.3}}6.2 S1 Data in Ninja Reports
Data Collection Script for Reporting:
<#
.SYNOPSIS
Collects SentinelOne data for NinjaRMM reporting
.DESCRIPTION
Queries S1 API and populates organization-level custom fields for reporting
#>
param(
[Parameter(Mandatory=$true)]
[string]$SiteId
)
$consoleUrl = $env:S1_CONSOLE_URL
$apiToken = $env:S1_API_TOKEN
$headers = @{
"Authorization" = "ApiToken $apiToken"
"Content-Type" = "application/json"
}
# Get agent statistics
$agentStats = Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/agents/count-by-filters?siteIds=$SiteId" -Headers $headers
# Get threat statistics (last 30 days)
$thirtyDaysAgo = (Get-Date).AddDays(-30).ToString("yyyy-MM-ddT00:00:00Z")
$threats = Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/threats?siteIds=$SiteId&createdAt__gte=$thirtyDaysAgo" -Headers $headers
# Get site info
$siteInfo = Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/sites?siteIds=$SiteId" -Headers $headers
# Calculate metrics
$metrics = @{
TotalAgents = $agentStats.data.total
HealthyAgents = $agentStats.data.online
OfflineAgents = $agentStats.data.total - $agentStats.data.online
ThreatsLast30Days = $threats.pagination.totalItems
ThreatsMitigated = ($threats.data | Where-Object { $_.mitigationStatus -in @("mitigated", "marked_as_benign") }).Count
ActiveThreats = ($threats.data | Where-Object { $_.mitigationStatus -notin @("mitigated", "marked_as_benign") }).Count
LicenseUsage = "$($siteInfo.data.sites[0].activeLicenses) / $($siteInfo.data.sites[0].totalLicenses)"
}
# Update organization custom fields
Ninja-Property-Set s1_report_total_agents $metrics.TotalAgents
Ninja-Property-Set s1_report_healthy_agents $metrics.HealthyAgents
Ninja-Property-Set s1_report_threats_30d $metrics.ThreatsLast30Days
Ninja-Property-Set s1_report_active_threats $metrics.ActiveThreats
Ninja-Property-Set s1_report_license_usage $metrics.LicenseUsage
Ninja-Property-Set s1_report_generated (Get-Date -Format "yyyy-MM-dd HH:mm")
Write-Host "Report data collected successfully"
$metrics | ConvertTo-Json6.3 Executive Dashboards
Power BI / Reporting Dashboard Query:
<#
.SYNOPSIS
Exports SentinelOne data for executive dashboards
.DESCRIPTION
Creates CSV/JSON exports suitable for Power BI or other BI tools
#>
param(
[string]$OutputPath = "C:\Reports\S1-Dashboard-Data"
)
$consoleUrl = $env:S1_CONSOLE_URL
$apiToken = $env:S1_API_TOKEN
$headers = @{
"Authorization" = "ApiToken $apiToken"
"Content-Type" = "application/json"
}
# Ensure output directory exists
New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
# 1. Agent Summary by Site
$allAgents = @()
$cursor = $null
do {
$url = "$consoleUrl/web/api/v2.1/agents?limit=1000"
if ($cursor) { $url += "&cursor=$cursor" }
$response = Invoke-RestMethod -Uri $url -Headers $headers
$allAgents += $response.data
$cursor = $response.pagination.nextCursor
} while ($cursor)
$agentSummary = $allAgents | Group-Object siteName | ForEach-Object {
@{
SiteName = $_.Name
TotalAgents = $_.Count
OnlineAgents = ($_.Group | Where-Object { $_.isActive }).Count
ProtectedAgents = ($_.Group | Where-Object { -not $_.infected }).Count
ThreatenedAgents = ($_.Group | Where-Object { $_.infected }).Count
}
}
$agentSummary | Export-Csv "$OutputPath\AgentSummary.csv" -NoTypeInformation
# 2. Threat Trends (Last 90 Days)
$ninetyDaysAgo = (Get-Date).AddDays(-90).ToString("yyyy-MM-ddT00:00:00Z")
$threats = @()
$cursor = $null
do {
$url = "$consoleUrl/web/api/v2.1/threats?createdAt__gte=$ninetyDaysAgo&limit=1000"
if ($cursor) { $url += "&cursor=$cursor" }
$response = Invoke-RestMethod -Uri $url -Headers $headers
$threats += $response.data
$cursor = $response.pagination.nextCursor
} while ($cursor)
$threatTrends = $threats | ForEach-Object {
@{
Date = ([DateTime]$_.threatInfo.createdAt).ToString("yyyy-MM-dd")
ThreatName = $_.threatInfo.threatName
Classification = $_.threatInfo.classification
SiteName = $_.siteName
ComputerName = $_.agentComputerName
MitigationStatus = $_.mitigationStatus
AnalystVerdict = $_.analystVerdict
}
}
$threatTrends | Export-Csv "$OutputPath\ThreatTrends.csv" -NoTypeInformation
# 3. Version Distribution
$versionDist = $allAgents | Group-Object agentVersion | ForEach-Object {
@{
Version = $_.Name
Count = $_.Count
Percentage = [math]::Round(($_.Count / $allAgents.Count) * 100, 2)
}
}
$versionDist | Export-Csv "$OutputPath\VersionDistribution.csv" -NoTypeInformation
# 4. OS Distribution
$osDist = $allAgents | Group-Object osName | ForEach-Object {
@{
OperatingSystem = $_.Name
Count = $_.Count
}
}
$osDist | Export-Csv "$OutputPath\OSDistribution.csv" -NoTypeInformation
Write-Host "Dashboard data exported to: $OutputPath"
Get-ChildItem $OutputPath6.4 Compliance Reporting
Compliance Status Report Script:
<#
.SYNOPSIS
Generates compliance status report for SentinelOne deployment
.DESCRIPTION
Checks deployment against compliance requirements and generates report
#>
param(
[string]$SiteId,
[string]$ComplianceFramework = "Generic" # HIPAA, PCI-DSS, SOC2, Generic
)
$consoleUrl = $env:S1_CONSOLE_URL
$apiToken = $env:S1_API_TOKEN
$headers = @{
"Authorization" = "ApiToken $apiToken"
"Content-Type" = "application/json"
}
# Get all agents for site
$agents = @()
$cursor = $null
do {
$url = "$consoleUrl/web/api/v2.1/agents?siteIds=$SiteId&limit=1000"
if ($cursor) { $url += "&cursor=$cursor" }
$response = Invoke-RestMethod -Uri $url -Headers $headers
$agents += $response.data
$cursor = $response.pagination.nextCursor
} while ($cursor)
# Define compliance checks
$complianceChecks = @(
@{
Control = "Endpoint Protection Coverage"
Requirement = "100% of endpoints must have EDR agent installed"
Check = { ($agents | Where-Object { $_.isActive }).Count -eq $agents.Count }
ActualValue = { "$($($agents | Where-Object { $_.isActive }).Count) / $($agents.Count)" }
},
@{
Control = "Real-time Protection"
Requirement = "All agents must have protection enabled"
Check = { ($agents | Where-Object { -not $_.infected }).Count -eq $agents.Count }
ActualValue = { "$($($agents | Where-Object { -not $_.infected }).Count) / $($agents.Count)" }
},
@{
Control = "Agent Currency"
Requirement = "All agents within 2 versions of current"
Check = {
$versions = $agents | Select-Object -ExpandProperty agentVersion -Unique | Sort-Object -Descending
$currentVersions = $versions | Select-Object -First 3
($agents | Where-Object { $_.agentVersion -in $currentVersions }).Count -eq $agents.Count
}
ActualValue = {
$versions = $agents | Group-Object agentVersion | Sort-Object Name -Descending
($versions | Select-Object -First 3 | ForEach-Object { "$($_.Name): $($_.Count)" }) -join ", "
}
},
@{
Control = "Management Connectivity"
Requirement = "All agents connected to management console"
Check = { ($agents | Where-Object { $_.isActive }).Count -ge ($agents.Count * 0.98) }
ActualValue = { "$([math]::Round(($agents | Where-Object { $_.isActive }).Count / $agents.Count * 100, 2))%" }
},
@{
Control = "Threat Response"
Requirement = "No unmitigated threats older than 24 hours"
Check = {
$oneDayAgo = (Get-Date).AddDays(-1).ToString("yyyy-MM-ddT00:00:00Z")
$oldThreats = Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/threats?siteIds=$SiteId&resolved=false&createdAt__lte=$oneDayAgo" -Headers $headers
$oldThreats.pagination.totalItems -eq 0
}
ActualValue = {
$oneDayAgo = (Get-Date).AddDays(-1).ToString("yyyy-MM-ddT00:00:00Z")
$oldThreats = Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/threats?siteIds=$SiteId&resolved=false&createdAt__lte=$oneDayAgo" -Headers $headers
"$($oldThreats.pagination.totalItems) unmitigated threats > 24h"
}
}
)
# Run compliance checks
$results = foreach ($check in $complianceChecks) {
$passed = & $check.Check
$actualValue = & $check.ActualValue
[PSCustomObject]@{
Control = $check.Control
Requirement = $check.Requirement
Status = if ($passed) { "COMPLIANT" } else { "NON-COMPLIANT" }
ActualValue = $actualValue
Framework = $ComplianceFramework
CheckDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
}
# Generate report
Write-Host "`n=== SentinelOne Compliance Report ===" -ForegroundColor Cyan
Write-Host "Site ID: $SiteId"
Write-Host "Framework: $ComplianceFramework"
Write-Host "Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "Total Endpoints: $($agents.Count)"
Write-Host ""
$compliantCount = ($results | Where-Object { $_.Status -eq "COMPLIANT" }).Count
$totalCount = $results.Count
Write-Host "Overall Compliance: $compliantCount / $totalCount ($([math]::Round($compliantCount/$totalCount*100))%)" -ForegroundColor $(if($compliantCount -eq $totalCount){"Green"}else{"Yellow"})
Write-Host ""
$results | Format-Table Control, Status, ActualValue -AutoSize
# Export results
$results | Export-Csv "C:\Reports\S1-Compliance-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformationOTHER RMM PLATFORMS
7.1 ConnectWise Automate Integration
ConnectWise Automate SentinelOne Monitor:
-- ConnectWise Automate Internal Monitor
-- SentinelOne Agent Health Check
SELECT
c.ComputerID,
c.Name AS ComputerName,
c.Domain,
CASE
WHEN s.ServiceName IS NOT NULL AND s.Running = 1 THEN 'Healthy'
WHEN s.ServiceName IS NOT NULL AND s.Running = 0 THEN 'Stopped'
ELSE 'Not Installed'
END AS S1Status
FROM computers c
LEFT JOIN services s ON c.ComputerID = s.ComputerID
AND s.ServiceName = 'SentinelAgent'
WHERE c.LastContact > DATE_SUB(NOW(), INTERVAL 1 DAY)Automate Deployment Script:
# ConnectWise Automate Script for SentinelOne Deployment
# Set as a Script in Automate, deploy via Group
# Parameters passed from Automate
$SiteToken = "@SiteToken@"
$InstallerPath = "@InstallerPath@"
# Logging
$logFile = "C:\Windows\LTSvc\Logs\S1-Install.log"
Start-Transcript -Path $logFile
try {
# Check if already installed
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -eq "Running") {
Write-Host "SentinelOne already installed and running"
Stop-Transcript
exit 0
}
# Install
$args = "/i `"$InstallerPath`" /qn SITE_TOKEN=`"$SiteToken`" /l*v `"C:\Windows\LTSvc\Logs\S1-MSI.log`""
Start-Process msiexec.exe -ArgumentList $args -Wait
# Verify
Start-Sleep -Seconds 60
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -eq "Running") {
Write-Host "Installation successful"
# Update EDF
# Use Automate's ExtraDataField update mechanism here
} else {
throw "Installation failed - service not running"
}
} catch {
Write-Error $_.Exception.Message
exit 1
} finally {
Stop-Transcript
}7.2 Datto RMM Integration
Datto RMM Component - SentinelOne Deployment:
# Datto RMM Component: SentinelOne Deployment
# Component Variables: SiteToken, InstallerURL
# Get component variables
$siteToken = $env:SiteToken
$installerUrl = $env:InstallerURL
$workDir = "C:\ProgramData\CentraStage\Packages"
$installerPath = "$workDir\SentinelInstaller.msi"
$logPath = "$workDir\S1-Install.log"
# Check existing installation
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -eq "Running") {
Write-Host "SentinelOne already installed"
exit 0
}
# Download installer
Write-Host "Downloading installer..."
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath -UseBasicParsing
# Install
Write-Host "Installing SentinelOne..."
$process = Start-Process msiexec.exe -ArgumentList "/i `"$installerPath`" /qn SITE_TOKEN=`"$siteToken`" /l*v `"$logPath`"" -Wait -PassThru
if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3010) {
Start-Sleep -Seconds 30
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -eq "Running") {
Write-Host "Installation successful"
# Update Datto UDF
Set-ItemProperty -Path "HKLM:\SOFTWARE\CentraStage" -Name "Custom1" -Value "S1_Installed"
exit 0
}
}
Write-Error "Installation failed"
exit 1Datto RMM Monitor - SentinelOne Health:
# Datto RMM Monitor Component: SentinelOne Health
# Returns exit codes for monitoring
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if (-not $service) {
Write-Host "ALERT: SentinelOne not installed"
exit 1
}
if ($service.Status -ne "Running") {
Write-Host "ALERT: SentinelOne service not running"
exit 1
}
# Check protection status
$sentinelCtl = Get-ChildItem "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" | Select-Object -First 1
if ($sentinelCtl) {
$status = & $sentinelCtl.FullName status 2>&1 | Out-String
if ($status -notmatch "Protection: enabled") {
Write-Host "ALERT: Protection disabled"
exit 1
}
if ($status -notmatch "connected to Management") {
Write-Host "WARNING: Not connected to console"
exit 1 # Or exit 0 with warning if preferred
}
}
Write-Host "OK: SentinelOne healthy"
exit 07.3 Generic API Integration Patterns
Universal RMM Integration Template:
<#
.SYNOPSIS
Universal SentinelOne RMM Integration Template
.DESCRIPTION
Adaptable script for any RMM platform
Customize the Output-RMMData function for your platform
#>
# Configuration - Set these for your environment
$script:S1Config = @{
ConsoleUrl = "https://your-console.sentinelone.net"
ApiToken = "YOUR_API_TOKEN" # Load from secure storage
}
# Platform-specific output function
# Customize this for your RMM
function Output-RMMData {
param(
[string]$FieldName,
[string]$Value
)
# NinjaRMM
# Ninja-Property-Set $FieldName $Value
# Datto RMM
# Set-ItemProperty -Path "HKLM:\SOFTWARE\CentraStage" -Name $FieldName -Value $Value
# ConnectWise Automate
# Update EDF via Automate functions
# Generic - Write to registry or file
$regPath = "HKLM:\SOFTWARE\MSP\SentinelOne"
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
Set-ItemProperty -Path $regPath -Name $FieldName -Value $Value
Write-Host "${FieldName}: $Value"
}
# Core functions
function Get-S1AgentStatus {
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
$sentinelCtl = Get-ChildItem "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
$result = @{
Installed = $null -ne $service
Running = $service.Status -eq "Running"
Version = $null
Connected = $false
Protected = $false
}
if ($sentinelCtl) {
$statusOutput = & $sentinelCtl.FullName status 2>&1 | Out-String
$result.Connected = $statusOutput -match "connected to Management"
$result.Protected = $statusOutput -match "Protection: enabled"
$result.Version = (Get-Item "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelAgent.exe" -ErrorAction SilentlyContinue).VersionInfo.FileVersion
}
return $result
}
function Install-S1Agent {
param(
[string]$InstallerPath,
[string]$SiteToken
)
$logPath = "C:\Temp\S1-Install.log"
$args = "/i `"$InstallerPath`" /qn SITE_TOKEN=`"$SiteToken`" /l*v `"$logPath`""
$process = Start-Process msiexec.exe -ArgumentList $args -Wait -PassThru
return $process.ExitCode -in @(0, 3010)
}
function Repair-S1Agent {
try {
Restart-Service -Name "SentinelAgent" -Force -ErrorAction Stop
return $true
} catch {
return $false
}
}
# Main execution
$status = Get-S1AgentStatus
if (-not $status.Installed) {
Output-RMMData "S1_Status" "NOT_INSTALLED"
exit 2
}
if (-not $status.Running) {
Output-RMMData "S1_Status" "SERVICE_STOPPED"
$repaired = Repair-S1Agent
if ($repaired) {
Output-RMMData "S1_Status" "RECOVERED"
}
exit 1
}
if (-not $status.Protected) {
Output-RMMData "S1_Status" "PROTECTION_DISABLED"
exit 1
}
if (-not $status.Connected) {
Output-RMMData "S1_Status" "DISCONNECTED"
exit 1
}
Output-RMMData "S1_Status" "HEALTHY"
Output-RMMData "S1_Version" $status.Version
exit 0EXCLUSION MANAGEMENT
8.1 NinjaRMM Agent Exclusions for S1
SentinelOne should exclude NinjaRMM components to prevent interference:
Required SentinelOne Exclusions for NinjaRMM:
| Type | Path/Process | Reason |
|---|---|---|
| Path | C:\ProgramData\NinjaRMMAgent\ | Agent data and scripts |
| Path | C:\Program Files (x86)\NinjaRMMAgent\ | Agent binaries |
| Process | NinjaRMMAgent.exe | Main agent process |
| Process | NinjaRMMAgentPatcher.exe | Update process |
| Process | ninjarmm-cli.exe | CLI tool |
| Path | C:\Windows\Temp\NinjaRMM* | Temporary files |
PowerShell Script to Add Exclusions via API:
<#
.SYNOPSIS
Adds NinjaRMM exclusions to SentinelOne
.DESCRIPTION
Creates path and process exclusions for NinjaRMM agent
#>
param(
[Parameter(Mandatory=$true)]
[string]$SiteId
)
$consoleUrl = $env:S1_CONSOLE_URL
$apiToken = $env:S1_API_TOKEN
$headers = @{
"Authorization" = "ApiToken $apiToken"
"Content-Type" = "application/json"
}
$exclusions = @(
@{
type = "path"
value = "C:\ProgramData\NinjaRMMAgent\"
osType = "windows"
description = "NinjaRMM Agent Data"
mode = "suppress"
},
@{
type = "path"
value = "C:\Program Files (x86)\NinjaRMMAgent\"
osType = "windows"
description = "NinjaRMM Agent Installation"
mode = "suppress"
},
@{
type = "process"
value = "NinjaRMMAgent.exe"
osType = "windows"
description = "NinjaRMM Main Process"
mode = "suppress"
},
@{
type = "process"
value = "NinjaRMMAgentPatcher.exe"
osType = "windows"
description = "NinjaRMM Patcher"
mode = "suppress"
}
)
foreach ($exclusion in $exclusions) {
$body = @{
data = @{
type = $exclusion.type
value = $exclusion.value
osType = $exclusion.osType
description = $exclusion.description
mode = $exclusion.mode
}
filter = @{
siteIds = @($SiteId)
}
} | ConvertTo-Json -Depth 5
try {
$response = Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/exclusions" `
-Method POST -Headers $headers -Body $body
Write-Host "Added exclusion: $($exclusion.value)" -ForegroundColor Green
} catch {
if ($_.Exception.Response.StatusCode -eq 409) {
Write-Host "Exclusion already exists: $($exclusion.value)" -ForegroundColor Yellow
} else {
Write-Error "Failed to add exclusion: $($_.Exception.Message)"
}
}
}8.2 S1 Exclusions for RMM Operations
NinjaRMM may need to exclude SentinelOne paths for certain operations:
NinjaRMM Antivirus Exclusions (if using built-in AV scanning):
C:\Program Files\SentinelOne\
C:\ProgramData\SentinelOne\
Script Execution Considerations:
SentinelOne may flag certain RMM scripts as suspicious. Create exclusions for:
- Signed scripts from your RMM vendor
- Known good script hash values
- Specific script paths used by your RMM
8.3 Common RMM Tool Exclusions
Comprehensive RMM Exclusion List for SentinelOne:
# Master RMM Exclusion Template
# Apply to SentinelOne sites based on RMM in use
NinjaRMM:
Paths:
- C:\ProgramData\NinjaRMMAgent\
- C:\Program Files (x86)\NinjaRMMAgent\
- C:\Windows\Temp\NinjaRMM*
Processes:
- NinjaRMMAgent.exe
- NinjaRMMAgentPatcher.exe
- ninjarmm-cli.exe
ConnectWise Automate:
Paths:
- C:\Windows\LTSvc\
- C:\Windows\LTSVC\
- C:\ProgramData\LabTech Client\
Processes:
- LTSvc.exe
- LTTray.exe
- ltsvcmon.exe
Datto RMM:
Paths:
- C:\ProgramData\CentraStage\
- C:\Program Files (x86)\CentraStage\
Processes:
- AEMAgent.exe
- CagService.exe
- Gui.exe
ConnectWise Control (ScreenConnect):
Paths:
- C:\Program Files (x86)\ScreenConnect Client*\
Processes:
- ScreenConnect.ClientService.exe
- ScreenConnect.WindowsClient.exe
Splashtop:
Paths:
- C:\Program Files (x86)\Splashtop\
Processes:
- SRService.exe
- SRManager.exe
TeamViewer:
Paths:
- C:\Program Files\TeamViewer\
- C:\Program Files (x86)\TeamViewer\
Processes:
- TeamViewer_Service.exe
- TeamViewer.exe
N-able N-central:
Paths:
- C:\Program Files (x86)\N-able Technologies\
- C:\Program Files (x86)\BeAnywhere Support Express\
Processes:
- BASupSrvc.exe
- BASupApp.exe
- winagent.exe
Kaseya VSA:
Paths:
- C:\Kaseya\
Processes:
- AgentMon.exe
- KaseyaD.exe
Atera:
Paths:
- C:\Program Files\ATERA Networks\
- C:\Program Files (x86)\ATERA Networks\
Processes:
- AteraAgent.exe
Syncro:
Paths:
- C:\ProgramData\Syncro\
- C:\Program Files\RepairTech\Syncro\
Processes:
- Syncro.App.Runner.exe
- SyncroLive.Agent.exeBulk Exclusion Import Script:
<#
.SYNOPSIS
Imports RMM exclusions from configuration file
.DESCRIPTION
Reads exclusion definitions and applies them to SentinelOne
#>
param(
[Parameter(Mandatory=$true)]
[string]$SiteId,
[Parameter(Mandatory=$true)]
[ValidateSet("NinjaRMM", "Automate", "Datto", "ScreenConnect")]
[string]$RMMPlatform
)
$consoleUrl = $env:S1_CONSOLE_URL
$apiToken = $env:S1_API_TOKEN
$headers = @{
"Authorization" = "ApiToken $apiToken"
"Content-Type" = "application/json"
}
# Define exclusions per platform
$platformExclusions = @{
"NinjaRMM" = @(
@{ type = "path"; value = "C:\ProgramData\NinjaRMMAgent\"; desc = "NinjaRMM Data" },
@{ type = "path"; value = "C:\Program Files (x86)\NinjaRMMAgent\"; desc = "NinjaRMM Install" },
@{ type = "process"; value = "NinjaRMMAgent.exe"; desc = "NinjaRMM Agent" },
@{ type = "process"; value = "NinjaRMMAgentPatcher.exe"; desc = "NinjaRMM Patcher" }
)
"Automate" = @(
@{ type = "path"; value = "C:\Windows\LTSvc\"; desc = "Automate Agent" },
@{ type = "process"; value = "LTSvc.exe"; desc = "Automate Service" },
@{ type = "process"; value = "LTTray.exe"; desc = "Automate Tray" }
)
"Datto" = @(
@{ type = "path"; value = "C:\ProgramData\CentraStage\"; desc = "Datto RMM Data" },
@{ type = "process"; value = "AEMAgent.exe"; desc = "Datto Agent" },
@{ type = "process"; value = "CagService.exe"; desc = "Datto Service" }
)
"ScreenConnect" = @(
@{ type = "path"; value = "C:\Program Files (x86)\ScreenConnect Client*\"; desc = "ScreenConnect Client" },
@{ type = "process"; value = "ScreenConnect.ClientService.exe"; desc = "ScreenConnect Service" }
)
}
$exclusions = $platformExclusions[$RMMPlatform]
$added = 0
$skipped = 0
foreach ($exclusion in $exclusions) {
$body = @{
data = @{
type = $exclusion.type
value = $exclusion.value
osType = "windows"
description = "$RMMPlatform - $($exclusion.desc)"
mode = "suppress"
}
filter = @{
siteIds = @($SiteId)
}
} | ConvertTo-Json -Depth 5
try {
Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/exclusions" -Method POST -Headers $headers -Body $body | Out-Null
Write-Host "[ADDED] $($exclusion.value)" -ForegroundColor Green
$added++
} catch {
if ($_.Exception.Response.StatusCode -eq 409) {
Write-Host "[EXISTS] $($exclusion.value)" -ForegroundColor Yellow
$skipped++
} else {
Write-Host "[FAILED] $($exclusion.value): $($_.Exception.Message)" -ForegroundColor Red
}
}
}
Write-Host "`nSummary: $added added, $skipped already existed"VERIFICATION
Integration Validation Checklist:
| Component | Verification Method | Expected Result |
|---|---|---|
| API Connectivity | Test API call from RMM | 200 OK response |
| Custom Fields | Check field population | Data present within 4 hours |
| Deployment Script | Test on single endpoint | Agent installed and connected |
| Health Monitor | Trigger test alert | Alert generated and ticketed |
| Exclusions | Run RMM script on protected endpoint | No S1 interference |
| Reporting | Generate test report | S1 data included |
| Remediation | Stop S1 service manually | Auto-recovery occurs |
Validation Script:
<#
.SYNOPSIS
Validates SentinelOne RMM integration
.DESCRIPTION
Comprehensive integration test script
#>
$results = @()
# Test 1: API Connectivity
Write-Host "Testing API connectivity..." -ForegroundColor Cyan
try {
$response = Invoke-RestMethod -Uri "$env:S1_CONSOLE_URL/web/api/v2.1/system/status" `
-Headers @{ "Authorization" = "ApiToken $env:S1_API_TOKEN" }
$results += @{ Test = "API Connectivity"; Status = "PASS"; Detail = "Connected to console" }
} catch {
$results += @{ Test = "API Connectivity"; Status = "FAIL"; Detail = $_.Exception.Message }
}
# Test 2: Agent Installation
Write-Host "Testing agent installation..." -ForegroundColor Cyan
$service = Get-Service -Name "SentinelAgent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -eq "Running") {
$results += @{ Test = "Agent Installation"; Status = "PASS"; Detail = "Service running" }
} else {
$results += @{ Test = "Agent Installation"; Status = "FAIL"; Detail = "Service not running" }
}
# Test 3: Custom Fields
Write-Host "Testing custom fields..." -ForegroundColor Cyan
$s1Status = Ninja-Property-Get s1_agent_status -ErrorAction SilentlyContinue
if ($s1Status) {
$results += @{ Test = "Custom Fields"; Status = "PASS"; Detail = "s1_agent_status = $s1Status" }
} else {
$results += @{ Test = "Custom Fields"; Status = "WARN"; Detail = "Custom field not populated" }
}
# Test 4: Protection Status
Write-Host "Testing protection status..." -ForegroundColor Cyan
$sentinelCtl = Get-ChildItem "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" -EA SilentlyContinue | Select-Object -First 1
if ($sentinelCtl) {
$status = & $sentinelCtl.FullName status 2>&1 | Out-String
if ($status -match "Protection: enabled") {
$results += @{ Test = "Protection Status"; Status = "PASS"; Detail = "Protection enabled" }
} else {
$results += @{ Test = "Protection Status"; Status = "FAIL"; Detail = "Protection not enabled" }
}
} else {
$results += @{ Test = "Protection Status"; Status = "FAIL"; Detail = "SentinelCtl not found" }
}
# Display results
Write-Host "`n=== Integration Validation Results ===" -ForegroundColor Cyan
$results | ForEach-Object {
$color = switch ($_.Status) {
"PASS" { "Green" }
"WARN" { "Yellow" }
"FAIL" { "Red" }
}
Write-Host "$($_.Test): $($_.Status) - $($_.Detail)" -ForegroundColor $color
}
$passCount = ($results | Where-Object { $_.Status -eq "PASS" }).Count
Write-Host "`nOverall: $passCount / $($results.Count) tests passed" -ForegroundColor $(if($passCount -eq $results.Count){"Green"}else{"Yellow"})TROUBLESHOOTING
Common Integration Issues
Issue: NinjaRMM custom fields not updating
# Verify Ninja-Property-Set is available
Get-Command Ninja-Property-Set -ErrorAction SilentlyContinue
# If not available, ensure script runs under NinjaRMM context
# Test with manual property set
Ninja-Property-Set test_field "test_value"
# Check NinjaRMM agent logs
Get-Content "C:\ProgramData\NinjaRMMAgent\logs\*.log" -Tail 50Issue: API rate limiting
# Implement rate limit handling
function Invoke-S1ApiSafe {
param($Uri, $Headers, $Method = "GET")
$maxRetries = 5
$retryDelay = 10
for ($i = 1; $i -le $maxRetries; $i++) {
try {
return Invoke-RestMethod -Uri $Uri -Headers $Headers -Method $Method
} catch {
if ($_.Exception.Response.StatusCode -eq 429) {
Write-Warning "Rate limited. Waiting $retryDelay seconds (attempt $i/$maxRetries)"
Start-Sleep -Seconds $retryDelay
$retryDelay *= 2
} else {
throw
}
}
}
throw "Max retries exceeded"
}Issue: Deployment script fails silently
# Enhanced logging for deployment troubleshooting
$DebugPreference = "Continue"
$VerbosePreference = "Continue"
# Check MSI log
$msiLog = Get-ChildItem "C:\Temp" -Filter "*S1*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($msiLog) {
Select-String -Path $msiLog.FullName -Pattern "error|failed|Return value 3" -Context 2,5
}
# Check Windows Event Log
Get-EventLog -LogName Application -Source MsiInstaller -Newest 10 | Format-ListIssue: SentinelOne blocking RMM scripts
# Check S1 Activity log for blocks
# In SentinelOne Console: Activity > Search for computer name
# Look for "Behavioral" or "Script" detections
# Temporary workaround (if needed for specific script)
# Add script hash to exclusions, or sign scripts with code signing certificate
# Get script hash for exclusion
$scriptPath = "C:\Path\To\Script.ps1"
(Get-FileHash $scriptPath -Algorithm SHA256).HashCOMMANDS/SCRIPTS
Quick Reference
# === SentinelOne Agent Commands ===
# Check agent status
& "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" status
# Get agent version
& "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" version
# Check protection status
& "C:\Program Files\SentinelOne\Sentinel Agent*\SentinelCtl.exe" protection_status
# Restart agent service
Restart-Service -Name "SentinelAgent" -Force
# === NinjaRMM Custom Field Commands ===
# Set custom field
Ninja-Property-Set s1_agent_status "HEALTHY"
# Get custom field
Ninja-Property-Get s1_site_token
# === SentinelOne API Quick Commands ===
# List all agents
Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/agents" -Headers $headers
# Get threats
Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/threats?resolved=false" -Headers $headers
# Initiate scan
$body = @{ filter = @{ ids = @("AGENT_ID") } } | ConvertTo-Json
Invoke-RestMethod -Uri "$consoleUrl/web/api/v2.1/agents/actions/initiate-scan" -Method POST -Headers $headers -Body $bodyRELATED DOCUMENTATION
- HOWTO- SentinelOne MSP Client Onboarding
- HOWTO- SentinelOne Deploy Agent Manual Installation
- HOWTO- SentinelOne Deploy Agent via Group Policy
- HOWTO- SentinelOne Create and Manage Exclusion Policies
- HOWTO- SentinelOne PowerShell API Automation
REVISION HISTORY
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-08 | CosmicBytez | Initial creation - NinjaRMM focus with multi-RMM support |