Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions backend/src/api/middleware/signedRequestVerification.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { FastifyReply, FastifyRequest } from "fastify";
import { signedRequestVerificationService } from "../../services/signedRequestVerification.service.js";

export async function verifySignedRequestHook(request: FastifyRequest, reply: FastifyReply) {
const keyId = request.headers["x-key-id"] as string;
const timestamp = request.headers["x-timestamp"] as string;
const signature = request.headers["x-signature"] as string;

if (!keyId || !timestamp || !signature) {
return reply.status(401).send({
error: "Unauthorized",
message: "Missing signature headers (X-Key-Id, X-Timestamp, X-Signature required)",
statusCode: 401,
});
}

const result = await signedRequestVerificationService.verifySignature({
keyId,
timestamp,
signature,
method: request.method,
path: request.url,
body: request.body,
clientIp: request.ip,
});

if (!result.valid) {
return reply.status(401).send({
error: "Unauthorized",
message: result.message || "Invalid request signature",
status: result.status,
statusCode: 401,
});
}
}
35 changes: 35 additions & 0 deletions backend/src/api/routes/route-groups/admin-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ import { parseQuarantineQueueRoutes } from "../parseQuarantineQueue.routes.js";
import { adminImpersonationRoutes } from "../adminImpersonation.routes.js";
// #1176 — Permission Change Notifications
import { permissionChangeNotificationRoutes } from "../permissionChangeNotification.routes.js";
// #1177 — Security Event Correlation View
import { securityEventCorrelationRoutes } from "../securityEventCorrelation.routes.js";
// #1178 — Webhook IP Allowlist Management
import { webhookIpAllowlistRoutes } from "../webhookIpAllowlist.routes.js";
// #1179 — Signed Request Verification Middleware
import { signedRequestVerificationRoutes } from "../signedRequestVerification.routes.js";
// #1180 — Sensitive Field Access Reports
import { sensitiveFieldAccessRoutes } from "../sensitiveFieldAccess.routes.js";

export async function registerAdminRoutes(server: FastifyInstance): Promise<void> {
server.register(apiKeysRoutes, { prefix: "/api/v1/admin/api-keys" });
Expand Down Expand Up @@ -136,6 +144,33 @@ export async function registerAdminRoutes(server: FastifyInstance): Promise<void
prefix: "/api/v1/notifications/permission-changes",
});

// #1177 — Security Event Correlation View
server.register(securityEventCorrelationRoutes, {
prefix: "/api/v1/security-correlations",
});

// #1178 — Webhook IP Allowlist Management
server.register(webhookIpAllowlistRoutes, {
prefix: "/api/v1/admin/webhooks/ip-allowlist",
});

// #1179 — Signed Request Verification Middleware
server.register(signedRequestVerificationRoutes, {
prefix: "/api/v1/admin/signed-requests",
});

// #1180 — Sensitive Field Access Reports
server.register(sensitiveFieldAccessRoutes, {
prefix: "/api/v1/admin/sensitive-fields",
});
prefix: "/api/v1/admin/impersonation",
});

// #1176 — Permission Change Notifications
server.register(permissionChangeNotificationRoutes, {
prefix: "/api/v1/notifications/permission-changes",
});

// #1187 — Graceful Shutdown Drain Protocol
const { drainProtocolRoutes } = await import("../drainProtocol.routes.js");
server.register(drainProtocolRoutes, {
Expand Down
100 changes: 100 additions & 0 deletions backend/src/api/routes/securityEventCorrelation.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { FastifyInstance } from "fastify";
import { securityEventCorrelationService } from "../../services/securityEventCorrelation.service.js";

export async function securityEventCorrelationRoutes(server: FastifyInstance) {
server.get("/", async (request, reply) => {
const { severity, status, search } = request.query as {
severity?: string;
status?: string;
search?: string;
};
const items = await securityEventCorrelationService.listCorrelations({ severity, status, search });
return reply.send({ data: items });
});

server.post("/", async (request, reply) => {
const body = request.body as {
title: string;
description?: string;
severity?: "low" | "medium" | "high" | "critical";
correlationRule?: Record<string, unknown>;
sourceSystems?: string[];
timeWindowMinutes?: number;
createdBy?: string;
};

try {
const created = await securityEventCorrelationService.createCorrelation(body);
return reply.status(201).send({ data: created });
} catch (err: any) {
return reply.status(400).send({ error: "Bad Request", message: err.message });
}
});

server.get("/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const correlation = await securityEventCorrelationService.getCorrelationById(id);
if (!correlation) {
return reply.status(404).send({ error: "Not Found", message: "Correlation view not found" });
}
return reply.send({ data: correlation });
});

server.patch("/:id/status", async (request, reply) => {
const { id } = request.params as { id: string };
const { status, updatedBy } = request.body as {
status: "active" | "investigating" | "resolved" | "archived";
updatedBy?: string;
};

if (!status) {
return reply.status(400).send({ error: "Bad Request", message: "status is required" });
}

const updated = await securityEventCorrelationService.updateCorrelationStatus(id, status, updatedBy);
if (!updated) {
return reply.status(404).send({ error: "Not Found", message: "Correlation view not found" });
}
return reply.send({ data: updated });
});

server.get("/events/raw", async (request, reply) => {
const { correlationId, eventType, source, severity, actor, limit } = request.query as {
correlationId?: string;
eventType?: string;
source?: string;
severity?: string;
actor?: string;
limit?: string;
};

const events = await securityEventCorrelationService.listSecurityEvents({
correlationId,
eventType,
source,
severity,
actor,
limit: limit ? parseInt(limit, 10) : undefined,
});
return reply.send({ data: events });
});

server.post("/events/ingest", async (request, reply) => {
const body = request.body as {
correlationId?: string;
eventType: string;
source: string;
severity?: "low" | "medium" | "high" | "critical";
actor?: string;
ipAddress?: string;
details?: Record<string, unknown>;
};

try {
const event = await securityEventCorrelationService.ingestSecurityEvent(body);
return reply.status(201).send({ data: event });
} catch (err: any) {
return reply.status(400).send({ error: "Bad Request", message: err.message });
}
});
}
97 changes: 97 additions & 0 deletions backend/src/api/routes/sensitiveFieldAccess.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { FastifyInstance } from "fastify";
import { sensitiveFieldAccessService } from "../../services/sensitiveFieldAccess.service.js";

export async function sensitiveFieldAccessRoutes(server: FastifyInstance) {
server.get("/definitions", async (request, reply) => {
const definitions = await sensitiveFieldAccessService.listDefinitions();
return reply.send({ data: definitions });
});

server.post("/definitions", async (request, reply) => {
const body = request.body as {
resourceName: string;
fieldName: string;
sensitivityLevel?: "low" | "medium" | "high" | "critical";
description?: string;
};

try {
const created = await sensitiveFieldAccessService.createDefinition(body);
return reply.status(201).send({ data: created });
} catch (err: any) {
return reply.status(400).send({ error: "Bad Request", message: err.message });
}
});

server.get("/access-logs", async (request, reply) => {
const { resourceName, fieldName, actorId, accessType, limit } = request.query as {
resourceName?: string;
fieldName?: string;
actorId?: string;
accessType?: string;
limit?: string;
};

const logs = await sensitiveFieldAccessService.queryLogs({
resourceName,
fieldName,
actorId,
accessType,
limit: limit ? parseInt(limit, 10) : undefined,
});
return reply.send({ data: logs });
});

server.post("/access-logs", async (request, reply) => {
const body = request.body as {
resourceName: string;
fieldName: string;
actorId: string;
actorRole?: string;
accessType?: "read" | "export" | "decrypted" | "modified";
reason?: string;
};

try {
const log = await sensitiveFieldAccessService.logAccess({
...body,
ipAddress: request.ip,
userAgent: request.headers["user-agent"],
});
return reply.status(201).send({ data: log });
} catch (err: any) {
return reply.status(400).send({ error: "Bad Request", message: err.message });
}
});

server.get("/reports", async (request, reply) => {
const reports = await sensitiveFieldAccessService.listReports();
return reply.send({ data: reports });
});

server.post("/reports/generate", async (request, reply) => {
const body = request.body as {
title: string;
timeRangeStart: string;
timeRangeEnd: string;
sensitivityFilter?: string;
generatedBy?: string;
};

try {
const report = await sensitiveFieldAccessService.generateReport(body);
return reply.status(201).send({ data: report });
} catch (err: any) {
return reply.status(400).send({ error: "Bad Request", message: err.message });
}
});

server.get("/reports/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const report = await sensitiveFieldAccessService.getReportById(id);
if (!report) {
return reply.status(404).send({ error: "Not Found", message: "Report not found" });
}
return reply.send({ data: report });
});
}
86 changes: 86 additions & 0 deletions backend/src/api/routes/signedRequestVerification.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { FastifyInstance } from "fastify";
import { signedRequestVerificationService } from "../../services/signedRequestVerification.service.js";

export async function signedRequestVerificationRoutes(server: FastifyInstance) {
server.get("/keys", async (request, reply) => {
const { activeOnly } = request.query as { activeOnly?: string };
const keys = await signedRequestVerificationService.listKeys(activeOnly === "true");
return reply.send({ data: keys });
});

server.post("/keys", async (request, reply) => {
const body = request.body as {
keyId?: string;
secret?: string;
algorithm?: string;
owner: string;
maxClockSkewSeconds?: number;
};

try {
const created = await signedRequestVerificationService.createKey(body);
return reply.status(201).send({ data: created });
} catch (err: any) {
return reply.status(400).send({ error: "Bad Request", message: err.message });
}
});

server.post("/keys/:id/rotate", async (request, reply) => {
const { id } = request.params as { id: string };
const rotated = await signedRequestVerificationService.rotateKeySecret(id);
if (!rotated) {
return reply.status(404).send({ error: "Not Found", message: "Signing key not found" });
}
return reply.send({ data: rotated });
});

server.post("/keys/:id/revoke", async (request, reply) => {
const { id } = request.params as { id: string };
const revoked = await signedRequestVerificationService.revokeKey(id);
if (!revoked) {
return reply.status(404).send({ error: "Not Found", message: "Signing key not found" });
}
return reply.send({ data: revoked });
});

server.get("/logs", async (request, reply) => {
const { keyId, status, limit } = request.query as {
keyId?: string;
status?: string;
limit?: string;
};

const logs = await signedRequestVerificationService.listLogs({
keyId,
status,
limit: limit ? parseInt(limit, 10) : undefined,
});
return reply.send({ data: logs });
});

server.post("/test-verify", async (request, reply) => {
const body = request.body as {
keyId: string;
method: string;
path: string;
timestamp: string | number;
signature: string;
payload?: any;
};

try {
const result = await signedRequestVerificationService.verifySignature({
keyId: body.keyId,
method: body.method,
path: body.path,
timestamp: body.timestamp,
signature: body.signature,
body: body.payload,
clientIp: request.ip,
});
return reply.send({ data: result });
} catch (err: any) {
return reply.status(400).send({ error: "Bad Request", message: err.message });
}
});
}
Loading
Loading