/** * SentinelOne Sync Service * Syncs sites, agents, and threats to s1_* PostgreSQL tables */ import { postgresClient } from './postgres-client'; import { getSentinelOneClient, S1Site, S1Agent, S1Threat } from './sentinelone-client'; export interface S1SyncEntityResult { entity: string; success: boolean; recordsUpserted: number; duration: number; error?: string; } export interface S1SyncResult { syncId: number; status: 'completed' | 'failed'; startedAt: Date; completedAt: Date; duration: number; entities: S1SyncEntityResult[]; totalUpserted: number; errors: string[]; } export class SentinelOneSyncService { private isSyncing = false; isSyncInProgress(): boolean { return this.isSyncing; } async fullSync(triggeredBy = 'system'): Promise { if (this.isSyncing) throw new Error('SentinelOne sync already in progress'); this.isSyncing = true; const startedAt = new Date(); const entities: S1SyncEntityResult[] = []; const errors: string[] = []; const { rows } = await postgresClient.query( `INSERT INTO s1_sync_history (sync_type, status, triggered_by, started_at) VALUES ('full', 'running', $1, NOW()) RETURNING id`, [triggeredBy] ); const syncId = Number(rows[0].id); const run = async (name: string, fn: () => Promise) => { const t = Date.now(); try { const count = await fn(); entities.push({ entity: name, success: true, recordsUpserted: count, duration: Date.now() - t }); console.log(`[S1Sync] ${name}: ${count} records`); } catch (err: any) { errors.push(`${name}: ${err.message}`); entities.push({ entity: name, success: false, recordsUpserted: 0, duration: Date.now() - t, error: err.message }); console.error(`[S1Sync] ${name} FAILED:`, err.message); } }; try { await run('sites', () => this.syncSites()); await run('agents', () => this.syncAgents()); await run('threats', () => this.syncThreats()); const completedAt = new Date(); const totalUpserted = entities.reduce((s, e) => s + e.recordsUpserted, 0); const status = errors.length === 0 ? 'completed' : 'failed'; await postgresClient.query( `UPDATE s1_sync_history SET status=$1, completed_at=NOW(), duration_ms=$2, total_upserted=$3, entity_results=$4, error_message=$5 WHERE id=$6`, [status, completedAt.getTime() - startedAt.getTime(), totalUpserted, JSON.stringify(entities), errors.length ? errors.join('; ') : null, syncId] ); return { syncId, status, startedAt, completedAt, duration: completedAt.getTime() - startedAt.getTime(), entities, totalUpserted, errors, }; } finally { this.isSyncing = false; } } private async syncSites(): Promise { const client = getSentinelOneClient(); const sites = await client.getSites(); let count = 0; for (const s of sites) { await postgresClient.query( `INSERT INTO s1_sites ( id, account_id, account_name, name, site_type, state, sku, suite, health_status, active_licenses, total_licenses, unlimited_licenses, unlimited_expiration, expiration, is_default, usage_type, external_id, registration_token, description, created_at, updated_at, synced_at ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,NOW()) ON CONFLICT (id) DO UPDATE SET account_name=$3, name=$4, state=$6, health_status=$9, active_licenses=$10, total_licenses=$11, unlimited_licenses=$12, unlimited_expiration=$13, expiration=$14, usage_type=$16, updated_at=$21, synced_at=NOW()`, [ s.id, s.accountId, s.accountName, s.name, s.siteType, s.state, s.sku, s.suite, s.healthStatus, s.activeLicenses, s.totalLicenses, s.unlimitedLicenses, s.unlimitedExpiration, s.expiration || null, s.isDefault, s.usageType, s.externalId || null, s.registrationToken || null, s.description || null, s.createdAt || null, s.updatedAt || null, ] ); count++; } return count; } private async syncAgents(): Promise { const client = getSentinelOneClient(); const agents = await client.getAgents(); let count = 0; for (const a of agents) { await postgresClient.query( `INSERT INTO s1_agents ( id, site_id, site_name, account_id, account_name, group_id, group_name, computer_name, domain, os_type, os_name, os_revision, agent_version, machine_type, is_active, is_decommissioned, is_up_to_date, is_pending_uninstall, is_uninstalled, infected, active_threats, network_status, mitigation_mode, detection_state, apps_vulnerability_status, firewall_enabled, external_ip, last_active_date, last_logged_in_user_name, cpu_id, core_count, cpu_count, total_memory, uuid, external_id, installer_type, scan_status, scan_started_at, scan_finished_at, created_at, updated_at, synced_at ) VALUES ( $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20, $21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,NOW() ) ON CONFLICT (id) DO UPDATE SET site_id=$2, site_name=$3, group_id=$6, group_name=$7, is_active=$15, is_decommissioned=$16, is_up_to_date=$17, is_pending_uninstall=$18, is_uninstalled=$19, infected=$20, active_threats=$21, network_status=$22, mitigation_mode=$23, detection_state=$24, apps_vulnerability_status=$25, firewall_enabled=$26, external_ip=$27, last_active_date=$28, last_logged_in_user_name=$29, agent_version=$13, scan_status=$37, scan_started_at=$38, scan_finished_at=$39, updated_at=$41, synced_at=NOW()`, [ a.id, a.siteId, a.siteName, a.accountId, a.accountName, a.groupId, a.groupName, a.computerName, a.domain || null, a.osType, a.osName, a.osRevision, a.agentVersion, a.machineType, a.isActive, a.isDecommissioned, a.isUpToDate, a.isPendingUninstall, a.isUninstalled, a.infected, a.activeThreats, a.networkStatus, a.mitigationMode, a.detectionState, a.appsVulnerabilityStatus, a.firewallEnabled ?? null, a.externalIp || null, a.lastActiveDate || null, a.lastLoggedInUserName || null, a.cpuId || null, a.coreCount ?? null, a.cpuCount ?? null, a.totalMemory ?? null, a.uuid, a.externalId || null, a.installerType || null, a.scanStatus || null, a.scanStartedAt || null, a.scanFinishedAt || null, a.createdAt || null, a.updatedAt || null, ] ); count++; } return count; } private async syncThreats(): Promise { const client = getSentinelOneClient(); const threats = await client.getThreats(); let count = 0; for (const t of threats) { const ti = t.threatInfo; const adi = t.agentDetectionInfo; const ari = t.agentRealtimeInfo; await postgresClient.query( `INSERT INTO s1_threats ( id, site_id, site_name, account_id, agent_id, agent_computer_name, agent_os_name, agent_version, agent_is_active, agent_is_decommissioned, threat_name, threat_file_path, threat_file_sha256, classification, classification_source, confidence_level, mitigation_status, mitigation_report, analyst_verdict, incident_status, detection_engines, indicators, created_at, updated_at, synced_at ) VALUES ( $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,NOW() ) ON CONFLICT (id) DO UPDATE SET agent_is_active=$9, agent_is_decommissioned=$10, mitigation_status=$17, mitigation_report=$18, analyst_verdict=$19, incident_status=$20, updated_at=$24, synced_at=NOW()`, [ t.id, adi.siteId, adi.siteName, adi.accountId, ari.agentId, ari.agentComputerName, ari.agentOsName, ari.agentVersion, ari.agentIsActive, ari.agentIsDecommissioned, ti.threatName || null, ti.filePath || null, ti.sha256 || null, ti.classification || null, ti.classificationSource || null, ti.confidenceLevel || null, ti.mitigationStatus || null, JSON.stringify(t.mitigationStatus ?? []), ti.analystVerdict || null, ti.incidentStatus || null, JSON.stringify(ti.detectionEngines ?? []), JSON.stringify(t.indicators ?? []), ti.createdAt || null, ti.updatedAt || null, ] ); count++; } return count; } } let _instance: SentinelOneSyncService | null = null; export function getSentinelOneSyncService(): SentinelOneSyncService { if (!_instance) _instance = new SentinelOneSyncService(); return _instance; }