admin / Synapse-Sonar
publicAttack Surface Simulation
Synapse-Sonar / synapse-sonar / lib / vulnIngest.ts
3021 B · main
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | import { z } from "zod"; import { prisma } from "@/lib/prisma"; /** * Shared NetscanXi -> Synapse Sonar vulnerability ingest. * * Used by BOTH integration directions so they behave identically: * - PUSH app/api/ingest/vulnerabilities (NetscanXi POSTs to us) * - PULL app/api/integrations/netscan/sync (we GET from NetscanXi) * * Assets are upserted by (tenant, IP); vulnerabilities by (asset, CVE). */ export const FindingSchema = z.object({ ip: z.string().ip(), hostname: z.string().optional(), cveId: z.string().min(3).max(64), cvssScore: z.number().min(0).max(10), severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional(), title: z.string().max(500).optional(), source: z.string().max(120).optional(), discoveredAt: z.string().datetime().optional(), }); export const PayloadSchema = z.object({ findings: z.array(FindingSchema).min(1).max(1000), }); export type Finding = z.infer<typeof FindingSchema>; export function severityFromScore( score: number ): "LOW" | "MEDIUM" | "HIGH" | "CRITICAL" { if (score >= 9.0) return "CRITICAL"; if (score >= 7.0) return "HIGH"; if (score >= 4.0) return "MEDIUM"; return "LOW"; } export interface IngestSummary { assetsTouched: number; vulnsUpserted: number; } /** Upsert a batch of findings for a tenant. Returns the touched counts. */ export async function ingestFindings( tenantId: string, findings: Finding[] ): Promise<IngestSummary> { let assetsTouched = 0; let vulnsUpserted = 0; for (const f of findings) { // Ensure the asset exists for this tenant (create a minimal node if new). // IMPORTANT: vulnerability ingestion must NOT modify an existing node's // hostname. The hostname is owned by flow discovery (the agent); a scan's // hostname value (often absent, an IP, or a scan label) must never overwrite // it. We therefore only set hostname when creating a brand-new node. const asset = await prisma.asset.upsert({ where: { tenantId_ipAddress: { tenantId, ipAddress: f.ip } }, create: { tenantId, ipAddress: f.ip, hostname: f.hostname ?? null, }, update: {}, // never touch hostname (or any asset field) on ingest select: { id: true }, }); assetsTouched++; const severity = f.severity ?? severityFromScore(f.cvssScore); const discoveredAt = f.discoveredAt ? new Date(f.discoveredAt) : new Date(); await prisma.vulnerability.upsert({ where: { assetId_cveId: { assetId: asset.id, cveId: f.cveId } }, create: { tenantId, assetId: asset.id, cveId: f.cveId, title: f.title ?? null, cvssScore: f.cvssScore, severity, source: f.source ?? "NETSCAN_XI", discoveredAt, }, update: { title: f.title ?? null, cvssScore: f.cvssScore, severity, source: f.source ?? "NETSCAN_XI", discoveredAt, }, }); vulnsUpserted++; } return { assetsTouched, vulnsUpserted }; } |