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:
@@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
151
packages/api/src/routes/correlation.routes.ts
Normal file
151
packages/api/src/routes/correlation.routes.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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() }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user