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 RMM Integration Guide
SentinelOne RMM Integration Guide
HOWTOAdvanced

SentinelOne RMM Integration Guide

This runbook provides comprehensive guidance for integrating SentinelOne Singularity Complete with NinjaRMM and other RMM platforms. Proper RMM...

Dylan H.

Security Operations

February 11, 2026
52 min read

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:

  • NinjaRMM SentinelOne Integration
  • SentinelOne API Documentation
  • SentinelOne Knowledge Base

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:

ComponentRequirement
NetworkHTTPS/443 outbound to *.sentinelone.net and *.ninjarmm.com
PowerShellVersion 5.1+ (7.x recommended)
PermissionsLocal admin for agent operations
Storage50MB 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:

LayerPurposeMethod
Console-to-ConsoleStatus sync, threat alertsREST API, Webhooks
Console-to-AgentDeployment, configurationRMM scripts
Agent-to-AgentHealth monitoringLocal queries via SentinelCtl
ReportingUnified dashboardsAPI 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:

PermissionUse 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

  1. Log into NinjaRMM Admin Portal
  2. Navigate to Administration > Apps & Services
  3. Select Third-Party Integrations
  4. Locate SentinelOne in the security tools section

Step 2: Configure API Connection

  1. Click Configure next to SentinelOne
  2. 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)
  3. Click Test Connection
  4. If successful, click Save

Step 3: Map Organizations

  1. After connection, go to Organization Mapping
  2. Map each NinjaRMM organization to corresponding SentinelOne Site
  3. Use the Site ID or Site Name for matching
  4. Enable auto-mapping for new organizations (optional)

2.2 Configuration Steps

Custom Field Configuration:

Create these custom fields in NinjaRMM for SentinelOne data:

Field NameTypeScopePurpose
s1_agent_statusTextDeviceHEALTHY, DEGRADED, OFFLINE, NOT_INSTALLED
s1_agent_versionTextDeviceCurrent agent version
s1_protection_statusTextDevicePROTECTED, DISABLED, UNKNOWN
s1_last_scanDate/TimeDeviceLast scan completion
s1_threat_countNumberDeviceActive unresolved threats
s1_console_connectedBooleanDeviceConsole connectivity status
s1_site_tokenText (Encrypted)OrganizationSite-specific deployment token
s1_site_idTextOrganizationSentinelOne Site ID

Creating Custom Fields via NinjaRMM:

  1. Navigate to Administration > Devices > Global Custom Fields
  2. Click Add for each field
  3. 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:

  1. Navigate to Dashboards > Create New Dashboard
  2. Name: "SentinelOne Security Overview"
  3. 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 = false AND s1_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

  1. Navigate to Administration > Policies > Conditions
  2. Create condition:
    • Name: "SentinelOne Agent Offline"
    • Type: Custom Field
    • Field: s1_agent_status
    • Operator: Equals
    • Value: "OFFLINE"
    • Duration: 30 minutes
  3. 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 -AutoSize

Storing Tokens in NinjaRMM:

  1. 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
  2. Tokens are encrypted at rest in NinjaRMM

3.3 Silent Installation Parameters

Windows MSI Parameters:

ParameterDescriptionExample
/iInstall/i "SentinelInstaller.msi"
/qnSilent install/qn
SITE_TOKENRegistration tokenSITE_TOKEN="eyJ..."
MANAGEMENT_TOKENAlternative authMANAGEMENT_TOKEN="..."
/l*vVerbose logging/l*v "C:\Temp\s1.log"
REBOOT=ReallySuppressPrevent rebootAdd 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=ReallySuppress

3.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-S1Installation

3.5 Deployment Scheduling Strategies

NinjaRMM Deployment Strategies:

StrategyUse CaseConfiguration
ImmediateEmergency deploymentRun once, now
ScheduledPlanned rolloutRun once at specific time
RecurringNew device onboardingRun on schedule, skip if installed
Condition-basedAuto-remediationTrigger 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 0

Linux 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 0

Linux 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 0

MONITORING 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 $exitCode

4.2 Alert Conditions and Thresholds

Recommended Alert Thresholds:

ConditionThresholdSeverityResponse Time
Agent Not InstalledAny device in protected orgHigh4 hours
Agent Offline> 30 minutesMedium4 hours
Agent Offline> 24 hoursHigh1 hour
Service StoppedAnyCritical15 minutes
Protection DisabledAnyCriticalImmediate
Console Disconnected> 1 hourMedium4 hours
Console Disconnected> 4 hoursHigh1 hour
Active Threats> 0CriticalImmediate
Version Out of Date> 2 versions behindLow1 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 team

4.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 1

5.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 1

REPORTING 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-Json

6.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 $OutputPath

6.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" -NoTypeInformation

OTHER 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 1

Datto 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 0

7.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 0

EXCLUSION MANAGEMENT

8.1 NinjaRMM Agent Exclusions for S1

SentinelOne should exclude NinjaRMM components to prevent interference:

Required SentinelOne Exclusions for NinjaRMM:

TypePath/ProcessReason
PathC:\ProgramData\NinjaRMMAgent\Agent data and scripts
PathC:\Program Files (x86)\NinjaRMMAgent\Agent binaries
ProcessNinjaRMMAgent.exeMain agent process
ProcessNinjaRMMAgentPatcher.exeUpdate process
Processninjarmm-cli.exeCLI tool
PathC:\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.exe

Bulk 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:

ComponentVerification MethodExpected Result
API ConnectivityTest API call from RMM200 OK response
Custom FieldsCheck field populationData present within 4 hours
Deployment ScriptTest on single endpointAgent installed and connected
Health MonitorTrigger test alertAlert generated and ticketed
ExclusionsRun RMM script on protected endpointNo S1 interference
ReportingGenerate test reportS1 data included
RemediationStop S1 service manuallyAuto-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 50

Issue: 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-List

Issue: 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).Hash

COMMANDS/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 $body

RELATED 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

VersionDateAuthorChanges
1.02026-01-08CosmicBytezInitial creation - NinjaRMM focus with multi-RMM support

Related Reading

  • SentinelOne Health Check: Agent Status Monitoring and
  • Deploy SentinelOne Policy
  • SentinelOne Control vs Complete Feature Comparison
#sentinelone#edr#Security#threat-hunting#deployment#policy#automation#api#detection-rules#firewall

Related Articles

SentinelOne Health Check: Agent Status Monitoring and

Organizations deploying SentinelOne endpoint protection require continuous monitoring of agent health to ensure comprehensive threat coverage across their...

17 min read

Deploy SentinelOne Policy

Deploy, manage, and validate SentinelOne security policies across your endpoint estate using the SentinelOne Management API. This automated workflow supports:

25 min read

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
Back to all HOWTOs