wulf-pulse/app/api/sentinelone/sync/route.ts
lorentz ed6c4a8b65 feat: Add SentinelOne integration
- Add SentinelOne API client (lib/services/sentinelone-client.ts)
  - Paginated fetching for sites, agents, threats
  - JWT token auth via S1_API_URL / S1_API_TOKEN env vars

- Add SentinelOne sync service (lib/services/sentinelone-sync-service.ts)
  - Full sync: sites, agents, threats into s1_* tables
  - Sync history tracking with per-entity results

- Add DB migration 038: s1_sites, s1_agents, s1_threats,
  s1_company_mappings, s1_sync_history tables

- Add API routes:
  - POST/GET /api/sentinelone/sync
  - GET/POST/DELETE /api/sentinelone/company-mappings
  - GET /api/sentinelone/coverage (fixed Cartesian product bug)

- Add UI pages:
  - /admin/sync/sentinelone — sync admin with history + stats
  - /sentinelone/coverage — AV coverage report per site
  - /sentinelone/mappings — map S1 sites to Autotask companies

- Wire SentinelOne into admin sync overview card grid
- Add SentinelOne Sync to app navigation
- Fix docker-compose: remove explicit S1 env var entries that
  were overwriting env_file values with empty strings
2026-02-27 05:31:31 -05:00

53 lines
1.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getSentinelOneSyncService } from '@/lib/services/sentinelone-sync-service';
import { postgresClient } from '@/lib/services/postgres-client';
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}));
const triggeredBy = body.triggeredBy || 'manual';
const svc = getSentinelOneSyncService();
if (svc.isSyncInProgress()) {
return NextResponse.json({ error: 'Sync already in progress' }, { status: 409 });
}
svc.fullSync(triggeredBy).catch(err => console.error('[S1Sync] Background sync error:', err));
return NextResponse.json({ success: true, message: 'SentinelOne sync started' });
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: msg }, { status: 500 });
}
}
export async function GET() {
try {
const [historyRes, countsRes] = await Promise.all([
postgresClient.query(
`SELECT id, sync_type, status, triggered_by, started_at, completed_at,
duration_ms, total_upserted, error_message, entity_results
FROM s1_sync_history ORDER BY started_at DESC LIMIT 10`
),
postgresClient.query(
`SELECT
(SELECT COUNT(*) FROM s1_sites) AS sites,
(SELECT COUNT(*) FROM s1_agents) AS agents,
(SELECT COUNT(*) FROM s1_agents WHERE infected = true) AS infected,
(SELECT COUNT(*) FROM s1_agents WHERE is_active = true) AS active_agents,
(SELECT COUNT(*) FROM s1_threats) AS threats`
),
]);
const svc = getSentinelOneSyncService();
return NextResponse.json({
inProgress: svc.isSyncInProgress(),
history: historyRes.rows,
counts: countsRes.rows[0],
});
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: msg }, { status: 500 });
}
}