wulf-pulse/app/api/sentinelone/company-mappings/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

104 lines
3.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { postgresClient } from '@/lib/services/postgres-client';
export async function GET(request: NextRequest) {
try {
const { searchParams } = request.nextUrl;
const includeUnmapped = searchParams.get('includeUnmapped') === 'true';
const mappings = await postgresClient.query(
`SELECT m.id, m.s1_site_id, m.s1_site_name, m.company_id, m.company_name,
m.notes, m.created_at, m.updated_at,
s.state, s.active_licenses, s.health_status, s.sku
FROM s1_company_mappings m
LEFT JOIN s1_sites s ON s.id = m.s1_site_id
ORDER BY m.s1_site_name`
);
if (!includeUnmapped) {
return NextResponse.json({ mappings: mappings.rows });
}
const allSites = await postgresClient.query(
`SELECT id, name, state, active_licenses, health_status, sku FROM s1_sites ORDER BY name`
);
const mappedIds = new Set(mappings.rows.map((r: any) => r.s1_site_id));
const unmapped = allSites.rows
.filter((s: any) => !mappedIds.has(s.id))
.map((s: any) => ({
id: null,
s1_site_id: s.id,
s1_site_name: s.name,
company_id: null,
company_name: null,
notes: null,
state: s.state,
active_licenses: s.active_licenses,
health_status: s.health_status,
sku: s.sku,
}));
return NextResponse.json({
mappings: [...mappings.rows, ...unmapped],
stats: {
total: allSites.rows.length,
mapped: mappings.rows.length,
unmapped: unmapped.length,
},
});
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: msg }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { s1SiteId, s1SiteName, companyId, companyName, notes = null } = body;
if (!s1SiteId || !companyId) {
return NextResponse.json(
{ error: 'Missing required fields: s1SiteId and companyId' },
{ status: 400 }
);
}
const result = await postgresClient.query(
`INSERT INTO s1_company_mappings (s1_site_id, s1_site_name, company_id, company_name, notes)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (s1_site_id) DO UPDATE SET
s1_site_name = $2, company_id = $3, company_name = $4,
notes = $5, updated_at = NOW()
RETURNING *`,
[s1SiteId, s1SiteName, companyId, companyName || null, notes]
);
return NextResponse.json({ success: true, mapping: result.rows[0] });
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: msg }, { status: 500 });
}
}
export async function DELETE(request: NextRequest) {
try {
const id = request.nextUrl.searchParams.get('id');
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
const result = await postgresClient.query(
'DELETE FROM s1_company_mappings WHERE id = $1 RETURNING *',
[id]
);
if (result.rowCount === 0) {
return NextResponse.json({ error: 'Mapping not found' }, { status: 404 });
}
return NextResponse.json({ success: true });
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: msg }, { status: 500 });
}
}