Add cross-service alert correlation system FRE-4500

- Unified alert types (AlertSource, AlertCategory, CorrelationStatus, EntityType)
- NormalizedAlert and CorrelationGroup Prisma models
- AlertNormalizer for all 4 services (DarkWatch, SpamShield, VoicePrint, CallAnalysis)
- CorrelationEngine with temporal + entity-based correlation detection
- CorrelationService orchestrator with dashboard API
- Correlation API routes (/api/v1/correlation/*)
- Service emitters wired to DarkWatch, SpamShield, VoicePrint
- pnpm workspace config for monorepo
This commit is contained in:
Senior Engineer
2026-05-02 01:10:44 -04:00
committed by Michael Freno
parent 685fb57e53
commit 03276dde2d
35 changed files with 8072 additions and 31 deletions

View File

@@ -13,10 +13,11 @@
"@fastify/helmet": "^13.0.1",
"@fastify/rate-limit": "^9.0.0",
"@fastify/sensible": "^6.0.1",
"@shieldai/db": "0.1.0",
"@shieldai/types": "0.1.0",
"@shieldai/db": "workspace:*",
"@shieldai/types": "workspace:*",
"@shieldai/correlation": "workspace:*",
"fastify": "^5.2.0",
"@shieldai/darkwatch": "0.1.0",
"@shieldai/voiceprint": "0.1.0"
"@shieldai/darkwatch": "workspace:*",
"@shieldai/voiceprint": "workspace:*"
}
}

View File

@@ -0,0 +1,151 @@
import { FastifyInstance } from "fastify";
import { correlationService } from "@shieldai/correlation";
export function correlationRoutes(fastify: FastifyInstance) {
fastify.get("/dashboard", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const timeWindow = parseInt((request.query as any).timeWindow as string) || 60;
const data = await correlationService.getDashboardData(userId, timeWindow);
return reply.send(data);
});
fastify.get("/groups", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const query = request.query as Record<string, string>;
const result = await correlationService.getCorrelationGroups({
userId,
status: query.status || undefined,
timeWindowMinutes: query.timeWindow
? parseInt(query.timeWindow)
: 60,
limit: query.limit ? parseInt(query.limit) : 50,
offset: query.offset ? parseInt(query.offset) : 0,
});
return reply.send(result);
});
fastify.get("/groups/:groupId", async (request, reply) => {
const groupId = (request.params as any).groupId;
const group = await correlationService.getGroupById(groupId);
if (!group) {
return reply.code(404).send({ error: "Correlation group not found" });
}
return reply.send(group);
});
fastify.patch("/groups/:groupId/resolve", async (request, reply) => {
const groupId = (request.params as any).groupId;
const body = (request.body as any) || {};
const status = body.status || "RESOLVED";
const group = await correlationService.resolveGroup(groupId, status);
if (!group) {
return reply.code(404).send({ error: "Correlation group not found" });
}
return reply.send(group);
});
fastify.get("/alerts", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const query = request.query as Record<string, string>;
const result = await correlationService.getCorrelatedAlerts({
userId,
source: query.source || undefined,
category: query.category || undefined,
severity: query.severity || undefined,
timeWindowMinutes: query.timeWindow
? parseInt(query.timeWindow)
: 60,
limit: query.limit ? parseInt(query.limit) : 50,
offset: query.offset ? parseInt(query.offset) : 0,
});
return reply.send(result);
});
fastify.post("/ingest/darkwatch", async (request, reply) => {
const body = request.body as any;
const alert = await correlationService.ingestDarkWatchAlert(
body.userId,
body.sourceAlertId,
{
exposureId: body.exposureId,
breachName: body.breachName,
severity: body.severity,
channel: body.channel,
dataType: body.dataType,
dataSource: body.dataSource,
}
);
return reply.code(201).send(alert);
});
fastify.post("/ingest/spamshield", async (request, reply) => {
const body = request.body as any;
const alert = await correlationService.ingestSpamShieldAlert(
body.userId,
body.sourceAlertId,
{
phoneNumber: body.phoneNumber,
decision: body.decision,
confidence: body.confidence,
reasons: body.reasons,
channel: body.channel,
hiyaReputationScore: body.hiyaReputationScore,
truecallerSpamScore: body.truecallerSpamScore,
}
);
return reply.code(201).send(alert);
});
fastify.post("/ingest/voiceprint", async (request, reply) => {
const body = request.body as any;
const alert = await correlationService.ingestVoicePrintAlert(
body.userId,
body.sourceAlertId,
{
jobId: body.jobId,
verdict: body.verdict,
syntheticScore: body.syntheticScore,
confidence: body.confidence,
matchedEnrollmentId: body.matchedEnrollmentId,
matchedSimilarity: body.matchedSimilarity,
analysisType: body.analysisType,
}
);
return reply.code(201).send(alert);
});
fastify.post("/ingest/call-analysis", async (request, reply) => {
const body = request.body as any;
const alert = await correlationService.ingestCallAnalysisAlert(
body.userId,
body.sourceAlertId,
{
callId: body.callId,
eventType: body.eventType,
mosScore: body.mosScore,
anomaly: body.anomaly,
sentiment: body.sentiment,
}
);
return reply.code(201).send(alert);
});
}

View File

@@ -24,3 +24,10 @@ export function voiceprintRoutes(fastify: FastifyInstance) {
root.register(voiceprint);
}, { prefix: "/api/v1/voiceprint" });
}
export function correlationRoutes(fastify: FastifyInstance) {
fastify.register(async (root) => {
const correlation = (await import("./correlation.routes")).correlationRoutes;
root.register(correlation);
}, { prefix: "/api/v1/correlation" });
}

View File

@@ -3,7 +3,7 @@ import cors from "@fastify/cors";
import helmet from "@fastify/helmet";
import sensible from "@fastify/sensible";
import { extractOrGenerateRequestId } from "@shieldai/types";
import { darkwatchRoutes, voiceprintRoutes } from "./routes";
import { darkwatchRoutes, voiceprintRoutes, correlationRoutes } from "./routes";
const app = Fastify({
logger: {
@@ -27,6 +27,7 @@ async function bootstrap() {
await app.register(darkwatchRoutes);
await app.register(voiceprintRoutes);
await app.register(correlationRoutes);
app.get("/health", async () => ({ status: "ok", timestamp: new Date().toISOString() }));