- 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
76 lines
3.1 KiB
TypeScript
76 lines
3.1 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { postgresClient } from '@/lib/services/postgres-client';
|
|
|
|
export async function GET() {
|
|
try {
|
|
const result = await postgresClient.query(`
|
|
SELECT
|
|
s.id AS s1_site_id,
|
|
s.name AS s1_site_name,
|
|
s.state,
|
|
s.sku,
|
|
s.health_status,
|
|
s.active_licenses,
|
|
m.company_id,
|
|
m.company_name,
|
|
COALESCE(a.total_agents, 0) AS total_agents,
|
|
COALESCE(a.active_agents, 0) AS active_agents,
|
|
COALESCE(a.infected_agents, 0) AS infected_agents,
|
|
COALESCE(a.outdated_agents, 0) AS outdated_agents,
|
|
COALESCE(a.decommissioned_agents, 0) AS decommissioned_agents,
|
|
a.last_seen,
|
|
COALESCE(t.total_threats, 0) AS total_threats,
|
|
COALESCE(t.active_threats, 0) AS active_threats
|
|
FROM s1_sites s
|
|
LEFT JOIN s1_company_mappings m ON m.s1_site_id = s.id
|
|
LEFT JOIN (
|
|
SELECT
|
|
site_id,
|
|
COUNT(*) AS total_agents,
|
|
COUNT(*) FILTER (WHERE is_active = true) AS active_agents,
|
|
COUNT(*) FILTER (WHERE infected = true) AS infected_agents,
|
|
COUNT(*) FILTER (WHERE is_up_to_date = false) AS outdated_agents,
|
|
COUNT(*) FILTER (WHERE is_decommissioned = true) AS decommissioned_agents,
|
|
MAX(updated_at) AS last_seen
|
|
FROM s1_agents
|
|
WHERE is_decommissioned = false
|
|
GROUP BY site_id
|
|
) a ON a.site_id = s.id
|
|
LEFT JOIN (
|
|
SELECT
|
|
site_id,
|
|
COUNT(*) AS total_threats,
|
|
COUNT(*) FILTER (WHERE mitigation_status NOT IN
|
|
('mitigated','marked_as_benign','marked_as_false_positive')) AS active_threats
|
|
FROM s1_threats
|
|
GROUP BY site_id
|
|
) t ON t.site_id = s.id
|
|
ORDER BY s.name
|
|
`);
|
|
|
|
const rows = result.rows.map((r: any) => ({
|
|
...r,
|
|
total_agents: Number(r.total_agents),
|
|
active_agents: Number(r.active_agents),
|
|
infected_agents: Number(r.infected_agents),
|
|
outdated_agents: Number(r.outdated_agents),
|
|
decommissioned_agents: Number(r.decommissioned_agents),
|
|
total_threats: Number(r.total_threats),
|
|
active_threats: Number(r.active_threats),
|
|
}));
|
|
|
|
const summary = {
|
|
totalSites: rows.length,
|
|
mappedSites: rows.filter((r: any) => r.company_id).length,
|
|
totalAgents: rows.reduce((s: number, r: any) => s + r.total_agents, 0),
|
|
infectedAgents: rows.reduce((s: number, r: any) => s + r.infected_agents, 0),
|
|
activeThreats: rows.reduce((s: number, r: any) => s + r.active_threats, 0),
|
|
outdatedAgents: rows.reduce((s: number, r: any) => s + r.outdated_agents, 0),
|
|
};
|
|
|
|
return NextResponse.json({ sites: rows, summary });
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|