414 lines
12 KiB
TypeScript
414 lines
12 KiB
TypeScript
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
|
import { correlationService } from "@shieldai/correlation";
|
|
|
|
type AuthUser = { id?: string };
|
|
|
|
function getUserId(request: FastifyRequest): string | undefined {
|
|
return (request.user as AuthUser | undefined)?.id;
|
|
}
|
|
|
|
const timeWindowSchema = {
|
|
type: "object",
|
|
properties: {
|
|
timeWindow: { type: "integer", minimum: 1, maximum: 10080 },
|
|
},
|
|
};
|
|
|
|
const paginatedQuerySchema = {
|
|
type: "object",
|
|
properties: {
|
|
timeWindow: { type: "integer", minimum: 1, maximum: 10080 },
|
|
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
offset: { type: "integer", minimum: 0, maximum: 10000 },
|
|
},
|
|
};
|
|
|
|
export function correlationRoutes(fastify: FastifyInstance) {
|
|
fastify.get(
|
|
"/dashboard",
|
|
{
|
|
schema: {
|
|
...timeWindowSchema,
|
|
response: {
|
|
"400": {
|
|
type: "object",
|
|
properties: {
|
|
error: { type: "string" },
|
|
},
|
|
},
|
|
"401": {
|
|
type: "object",
|
|
properties: {
|
|
error: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const query = request.query as Record<string, string | number>;
|
|
const timeWindow =
|
|
typeof query.timeWindow === "number" ? query.timeWindow : 60;
|
|
const data = await correlationService.getDashboardData(userId, timeWindow);
|
|
return reply.send(data);
|
|
}
|
|
);
|
|
|
|
fastify.get(
|
|
"/groups",
|
|
{
|
|
schema: {
|
|
...paginatedQuerySchema,
|
|
response: {
|
|
"400": {
|
|
type: "object",
|
|
properties: {
|
|
error: { type: "string" },
|
|
},
|
|
},
|
|
"401": {
|
|
type: "object",
|
|
properties: {
|
|
error: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const query = request.query as Record<string, string | number>;
|
|
const result = await correlationService.getCorrelationGroups({
|
|
userId,
|
|
status: (query.status as any) || undefined,
|
|
timeWindowMinutes:
|
|
typeof query.timeWindow === "number" ? query.timeWindow : 60,
|
|
limit: typeof query.limit === "number" ? query.limit : 50,
|
|
offset: typeof query.offset === "number" ? query.offset : 0,
|
|
});
|
|
return reply.send(result);
|
|
}
|
|
);
|
|
|
|
fastify.get(
|
|
"/groups/:groupId",
|
|
{
|
|
schema: {
|
|
params: {
|
|
type: "object",
|
|
properties: {
|
|
groupId: { type: "string", format: "uuid" },
|
|
},
|
|
required: ["groupId"],
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const groupId = (request.params as Record<string, string>).groupId;
|
|
const group = await correlationService.getGroupById(groupId, userId);
|
|
|
|
if (!group) {
|
|
return reply.code(404).send({ error: "Correlation group not found" });
|
|
}
|
|
|
|
return reply.send(group);
|
|
}
|
|
);
|
|
|
|
fastify.patch(
|
|
"/groups/:groupId/resolve",
|
|
{
|
|
schema: {
|
|
params: {
|
|
type: "object",
|
|
properties: {
|
|
groupId: { type: "string", format: "uuid" },
|
|
},
|
|
required: ["groupId"],
|
|
},
|
|
body: {
|
|
type: "object",
|
|
properties: {
|
|
status: { type: "string", enum: ["RESOLVED", "ACTIVE"] },
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const groupId = (request.params as Record<string, string>).groupId;
|
|
const body = request.body as Record<string, string> | undefined;
|
|
const status = body?.status || "RESOLVED";
|
|
const group = await correlationService.resolveGroup(
|
|
groupId,
|
|
userId,
|
|
status
|
|
);
|
|
|
|
if (!group) {
|
|
return reply.code(404).send({ error: "Correlation group not found" });
|
|
}
|
|
|
|
return reply.send(group);
|
|
}
|
|
);
|
|
|
|
fastify.get(
|
|
"/alerts",
|
|
{
|
|
schema: {
|
|
...paginatedQuerySchema,
|
|
response: {
|
|
"400": {
|
|
type: "object",
|
|
properties: {
|
|
error: { type: "string" },
|
|
},
|
|
},
|
|
"401": {
|
|
type: "object",
|
|
properties: {
|
|
error: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const query = request.query as Record<string, string | number>;
|
|
const result = await correlationService.getCorrelatedAlerts({
|
|
userId,
|
|
source: (query.source as any) || undefined,
|
|
category: (query.category as any) || undefined,
|
|
severity: (query.severity as any) || undefined,
|
|
timeWindowMinutes:
|
|
typeof query.timeWindow === "number" ? query.timeWindow : 60,
|
|
limit: typeof query.limit === "number" ? query.limit : 50,
|
|
offset: typeof query.offset === "number" ? query.offset : 0,
|
|
});
|
|
return reply.send(result);
|
|
}
|
|
);
|
|
|
|
fastify.post(
|
|
"/ingest/darkwatch",
|
|
{
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
properties: {
|
|
sourceAlertId: { type: "string" },
|
|
exposureId: { type: "string" },
|
|
breachName: { type: "string", maxLength: 500 },
|
|
severity: { type: "string", maxLength: 20 },
|
|
channel: { type: "string", maxLength: 50 },
|
|
dataType: { type: "array", items: { type: "string" } },
|
|
dataSource: { type: "string", maxLength: 100 },
|
|
},
|
|
required: ["sourceAlertId", "breachName", "severity", "channel"],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const body = request.body as Record<string, unknown>;
|
|
const alert = await correlationService.ingestDarkWatchAlert(
|
|
userId,
|
|
body.sourceAlertId as string,
|
|
{
|
|
exposureId: body.exposureId as string,
|
|
breachName: body.breachName as string,
|
|
severity: body.severity as string,
|
|
channel: body.channel as string,
|
|
dataType: body.dataType as string[] | undefined,
|
|
dataSource: body.dataSource as string | undefined,
|
|
}
|
|
);
|
|
return reply.code(201).send(alert);
|
|
}
|
|
);
|
|
|
|
fastify.post(
|
|
"/ingest/spamshield",
|
|
{
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
properties: {
|
|
sourceAlertId: { type: "string" },
|
|
phoneNumber: { type: "string", maxLength: 20 },
|
|
decision: { type: "string", enum: ["BLOCK", "FLAG", "ALLOW"] },
|
|
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
reasons: { type: "array", items: { type: "string" } },
|
|
channel: { type: "string", enum: ["call", "sms"] },
|
|
hiyaReputationScore: { type: "number" },
|
|
truecallerSpamScore: { type: "number" },
|
|
},
|
|
required: ["sourceAlertId", "phoneNumber", "decision", "confidence"],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const body = request.body as Record<string, unknown>;
|
|
const alert = await correlationService.ingestSpamShieldAlert(
|
|
userId,
|
|
body.sourceAlertId as string,
|
|
{
|
|
phoneNumber: body.phoneNumber as string,
|
|
decision: body.decision as string,
|
|
confidence: body.confidence as number,
|
|
reasons: body.reasons as string[] | undefined,
|
|
channel: body.channel as "call" | "sms" | undefined,
|
|
hiyaReputationScore: body.hiyaReputationScore as
|
|
| number
|
|
| undefined,
|
|
truecallerSpamScore: body.truecallerSpamScore as
|
|
| number
|
|
| undefined,
|
|
}
|
|
);
|
|
return reply.code(201).send(alert);
|
|
}
|
|
);
|
|
|
|
fastify.post(
|
|
"/ingest/voiceprint",
|
|
{
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
properties: {
|
|
sourceAlertId: { type: "string" },
|
|
jobId: { type: "string" },
|
|
verdict: {
|
|
type: "string",
|
|
enum: ["SYNTHETIC", "NATURAL", "UNCERTAIN"],
|
|
},
|
|
syntheticScore: { type: "number", minimum: 0, maximum: 1 },
|
|
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
matchedEnrollmentId: { type: "string" },
|
|
matchedSimilarity: { type: "number" },
|
|
analysisType: { type: "string", maxLength: 50 },
|
|
},
|
|
required: [
|
|
"sourceAlertId",
|
|
"jobId",
|
|
"verdict",
|
|
"syntheticScore",
|
|
"confidence",
|
|
],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const body = request.body as Record<string, unknown>;
|
|
const alert = await correlationService.ingestVoicePrintAlert(
|
|
userId,
|
|
body.sourceAlertId as string,
|
|
{
|
|
jobId: body.jobId as string,
|
|
verdict: body.verdict as string,
|
|
syntheticScore: body.syntheticScore as number,
|
|
confidence: body.confidence as number,
|
|
matchedEnrollmentId: body.matchedEnrollmentId as
|
|
| string
|
|
| undefined,
|
|
matchedSimilarity: body.matchedSimilarity as number | undefined,
|
|
analysisType: body.analysisType as string | undefined,
|
|
}
|
|
);
|
|
return reply.code(201).send(alert);
|
|
}
|
|
);
|
|
|
|
fastify.post(
|
|
"/ingest/call-analysis",
|
|
{
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
properties: {
|
|
sourceAlertId: { type: "string" },
|
|
callId: { type: "string" },
|
|
eventType: { type: "string", maxLength: 100 },
|
|
mosScore: { type: "number", minimum: 1, maximum: 5 },
|
|
anomaly: { type: "string", maxLength: 500 },
|
|
sentiment: {
|
|
type: "object",
|
|
properties: {
|
|
label: { type: "string", maxLength: 50 },
|
|
score: { type: "number", minimum: 0, maximum: 1 },
|
|
},
|
|
},
|
|
},
|
|
required: ["sourceAlertId", "callId"],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
const userId = getUserId(request);
|
|
if (!userId || userId === "anonymous") {
|
|
return reply.code(401).send({ error: "User not authenticated" });
|
|
}
|
|
|
|
const body = request.body as Record<string, unknown>;
|
|
const alert = await correlationService.ingestCallAnalysisAlert(
|
|
userId,
|
|
body.sourceAlertId as string,
|
|
{
|
|
callId: body.callId as string,
|
|
eventType: body.eventType as string | undefined,
|
|
mosScore: body.mosScore as number | undefined,
|
|
anomaly: body.anomaly as string | undefined,
|
|
sentiment: body.sentiment as
|
|
| { label: string; score: number }
|
|
| undefined,
|
|
}
|
|
);
|
|
return reply.code(201).send(alert);
|
|
}
|
|
);
|
|
}
|