Skip to content
25 changes: 25 additions & 0 deletions prisma/migrations/20260708113434_add_dashboard_model/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "Dashboard" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"tags" TEXT[],
"ownerId" TEXT NOT NULL,
"panels" JSONB NOT NULL DEFAULT '[]',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Dashboard_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "Dashboard_ownerId_idx" ON "Dashboard"("ownerId");

-- CreateIndex
CREATE INDEX "Dashboard_createdAt_idx" ON "Dashboard"("createdAt");

-- CreateIndex
CREATE INDEX "Dashboard_title_idx" ON "Dashboard"("title");

-- AddForeignKey
ALTER TABLE "Dashboard" ADD CONSTRAINT "Dashboard_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "Analyst"("id") ON DELETE CASCADE ON UPDATE CASCADE;
37 changes: 28 additions & 9 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ enum Status {
IGNORED
OTHER
}

enum crdb_internal_region {
aws_eu_central_1 @map("aws-eu-central-1")
}

model Analyst {
id String @id @default(uuid())
firstName String
Expand All @@ -41,6 +43,7 @@ model Analyst {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
refreshTokens RefreshToken[]
dashboards Dashboard[]
}

model RefreshToken {
Expand Down Expand Up @@ -111,12 +114,12 @@ model Rule {
name String @unique
description String
type String
mitreTactics String[]
mitreTactics String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
logSourceId String?
logSource LogSource? @relation(fields: [logSourceId], references: [id], onDelete: SetNull)

logSourceId String?
logSource LogSource? @relation(fields: [logSourceId], references: [id], onDelete: SetNull)

alerts Alert[]

Expand All @@ -134,26 +137,42 @@ model Alert {
updatedAt DateTime @updatedAt
rule Rule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
ruleId String
device Device? @relation(fields: [deviceId], references: [id])
device Device? @relation(fields: [deviceId], references: [id])
deviceId String?
confidence Float
severity Severity
status Status
scope String?
source String?
mitre String[]
mitre String[]
}

model Dashboard {
id String @id @default(uuid())
title String
description String?
tags String[]
ownerId String
owner Analyst @relation(fields: [ownerId], references: [id], onDelete: Cascade)
panels Json @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([ownerId])
@@index([createdAt])
@@index([title])
}

model LogSource {
id String @id @default(uuid())
name String @unique
category String
category String
vendor String
product String
description String
dataset String
type String @default("logs")
index String // The Elasticsearch index pattern to query
index String // The Elasticsearch index pattern to query
agent String
agentVersion String @default("9.2.1")
pipeline String
Expand All @@ -167,5 +186,5 @@ model LogSource {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

rules Rule[]
rules Rule[]
}
116 changes: 116 additions & 0 deletions src/docs/openapi.docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,4 +809,120 @@
* 404: { $ref: '#/components/responses/NotFound' }
*/

/**
* @swagger
* components:
* schemas:
* DashboardPanel:
* type: object
* required: [id, type, title, spec]
* properties:
* id: { type: string, example: p1 }
* type: { type: string, enum: [metric, histogram, breakdown] }
* title: { type: string, example: Total events }
* spec:
* type: object
* description: "metric: { index, aggType: count|cardinality|ratio, field?, filter?, numeratorFilter? } | histogram: { index, interval?, filter? } | breakdown: { index, field, size?, filter? }"
* example: { index: "logs-auditbeat.auditd-*", aggType: cardinality, field: host.name }
* Dashboard:
* type: object
* properties:
* id: { type: string, format: uuid }
* title: { type: string, example: Auditd Overview }
* description: { type: string, nullable: true, example: Kernel audit events }
* tags: { type: array, items: { type: string }, example: [auditd] }
* ownerId: { type: string, format: uuid }
* panels: { type: array, items: { $ref: '#/components/schemas/DashboardPanel' } }
* createdAt: { type: string, format: date-time }
* updatedAt: { type: string, format: date-time }
* DashboardListItem:
* type: object
* properties:
* id: { type: string, format: uuid }
* title: { type: string }
* desc: { type: string }
* tags: { type: array, items: { type: string } }
* DashboardInput:
* type: object
* required: [title]
* properties:
* title: { type: string, example: Auditd Overview }
* description: { type: string, example: Kernel audit events }
* tags: { type: array, items: { type: string }, example: [auditd] }
* panels: { type: array, items: { $ref: '#/components/schemas/DashboardPanel' } }
*/

/**
* @swagger
* /api/v1/dashboards:
* get:
* tags: [Dashboards]
* summary: List dashboards
* security: [{ bearerAuth: [] }]
* parameters:
* - { in: query, name: search, schema: { type: string } }
* - { in: query, name: page, schema: { type: integer, default: 1 } }
* - { in: query, name: limit, schema: { type: integer, default: 10 } }
* responses:
* 200: { description: Paginated dashboard list }
* 401: { $ref: '#/components/responses/Unauthorized' }
* post:
* tags: [Dashboards]
* summary: Create dashboard
* security: [{ bearerAuth: [] }]
* requestBody:
* required: true
* content:
* application/json:
* schema: { $ref: '#/components/schemas/DashboardInput' }
* responses:
* 201: { description: Dashboard created }
* 401: { $ref: '#/components/responses/Unauthorized' }
* /api/v1/dashboards/{id}:
* get:
* tags: [Dashboards]
* summary: Get dashboard definition
* security: [{ bearerAuth: [] }]
* parameters:
* - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
* responses:
* 200: { description: Dashboard details }
* 404: { $ref: '#/components/responses/NotFound' }
* patch:
* tags: [Dashboards]
* summary: Update dashboard
* security: [{ bearerAuth: [] }]
* parameters:
* - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
* requestBody:
* required: true
* content:
* application/json:
* schema: { $ref: '#/components/schemas/DashboardInput' }
* responses:
* 200: { description: Dashboard updated }
* 404: { $ref: '#/components/responses/NotFound' }
* delete:
* tags: [Dashboards]
* summary: Delete dashboard
* security: [{ bearerAuth: [] }]
* parameters:
* - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
* responses:
* 204: { description: Dashboard deleted }
* 404: { $ref: '#/components/responses/NotFound' }
* /api/v1/dashboards/{id}/data:
* get:
* tags: [Dashboards]
* summary: Get live panel data over a time range
* security: [{ bearerAuth: [] }]
* parameters:
* - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
* - { in: query, name: from, schema: { type: string, format: date-time } }
* - { in: query, name: to, schema: { type: string, format: date-time } }
* responses:
* 200: { description: Dashboard panel data }
* 404: { $ref: '#/components/responses/NotFound' }
*/

export {};
4 changes: 4 additions & 0 deletions src/docs/swagger.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ const swaggerDefinition: SwaggerDefinition = {
name: 'Alerts',
description: 'Security alert queue management endpoints',
},
{
name: 'Dashboards',
description: 'Dashboard management endpoints',
},
{
name: 'LogSources',
description: 'Log source management endpoints',
Expand Down
83 changes: 83 additions & 0 deletions src/modules/Dashboards/controllers/dashboard.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Request, Response } from 'express';
import catchAsync from '../../../common/utils/catchAsync';
import { STATUS_CODE } from '../../../common/constants/responseCode';
import { STATUS } from '../../../common/constants/responseStatus';
import {
createDashboardData,
DashboardDataQuery,
ListDashboardsQuery,
updateDashboardData,
} from '../types/types';
import {
createDashboardService,
updateDashboardService,
deleteDashboardService,
getDashboardService,
getAllDashboardsService,
getDashboardDataService,
} from '../services/dashboard.service';
import { IRequest } from '../../../common/interfaces/types';

export const createDashboard = catchAsync(async (req: IRequest, res: Response) => {
const data: createDashboardData = req.body as createDashboardData;
const dashboard = await createDashboardService(req.user!.id, data);

res.status(STATUS_CODE.CREATED).json({
status: STATUS.SUCCESS,
data: dashboard,
message: 'Dashboard created successfully',
});
});

export const updateDashboard = catchAsync(async (req: Request, res: Response) => {
const id = req.params.id as string;
const data: updateDashboardData = req.body as updateDashboardData;
const dashboard = await updateDashboardService(id, data);

res.status(STATUS_CODE.SUCCESS).json({
status: STATUS.SUCCESS,
data: dashboard,
message: 'Dashboard updated successfully',
});
});

export const deleteDashboard = catchAsync(async (req: Request, res: Response) => {
const id = req.params.id as string;
await deleteDashboardService(id);

res.status(STATUS_CODE.NO_CONTENT).send();
});

export const getDashboardById = catchAsync(async (req: Request, res: Response) => {
const id = req.params.id as string;
const dashboard = await getDashboardService(id);

res.status(STATUS_CODE.SUCCESS).json({
status: STATUS.SUCCESS,
data: dashboard,
message: 'Dashboard retrieved successfully',
});
});

export const getAllDashboards = catchAsync(async (req: Request, res: Response) => {
const query: ListDashboardsQuery = req.query as unknown as ListDashboardsQuery;
const dashboards = await getAllDashboardsService(query);

res.status(STATUS_CODE.SUCCESS).json({
status: STATUS.SUCCESS,
data: dashboards.data,
meta: dashboards.meta,
});
});

export const getDashboardData = catchAsync(async (req: Request, res: Response) => {
const id = req.params.id as string;
const query: DashboardDataQuery = req.query as unknown as DashboardDataQuery;
const data = await getDashboardDataService(id, query);

res.status(STATUS_CODE.SUCCESS).json({
status: STATUS.SUCCESS,
data,
message: 'Dashboard data retrieved successfully',
});
});
Loading
Loading