Skip to content
2 changes: 2 additions & 0 deletions backend/src/@types/fastify.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { TAiMcpServerServiceFactory } from "@app/ee/services/ai-mcp-server/ai-mc
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-types";
import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
import { TAuditReportServiceFactory } from "@app/ee/services/audit-report/audit-report-service";
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types";
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
Expand Down Expand Up @@ -405,6 +406,7 @@ declare module "fastify" {
microsoftTeams: TMicrosoftTeamsServiceFactory;
assumePrivileges: TAssumePrivilegeServiceFactory;
insights: TInsightsServiceFactory;
auditReport: TAuditReportServiceFactory;
pamInsights: TPamInsightsServiceFactory;
relay: TRelayServiceFactory;
gatewayV2: TGatewayV2ServiceFactory;
Expand Down
4 changes: 4 additions & 0 deletions backend/src/@types/knex.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ import {
TAuditLogStreamsInsert,
TAuditLogStreamsUpdate,
TAuditLogsUpdate,
TAuditReports,
TAuditReportsInsert,
TAuditReportsUpdate,
TAuthTokens,
TAuthTokenSessions,
TAuthTokenSessionsInsert,
Expand Down Expand Up @@ -1398,6 +1401,7 @@ declare module "knex/types/tables" {
TAuditLogStreamsInsert,
TAuditLogStreamsUpdate
>;
[TableName.AuditReport]: KnexOriginal.CompositeTableType<TAuditReports, TAuditReportsInsert, TAuditReportsUpdate>;
[TableName.GitAppInstallSession]: KnexOriginal.CompositeTableType<
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,
Expand Down
35 changes: 35 additions & 0 deletions backend/src/db/migrations/20260629120000_create-audit-reports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Knex } from "knex";

import { TableName } from "@app/db/schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
import { AuditReportStatus } from "@app/ee/services/audit-report/audit-report-types";

export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.AuditReport))) {
await knex.schema.createTable(TableName.AuditReport, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");

t.uuid("requestedByUserId");
t.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");

t.string("status").notNullable().defaultTo(AuditReportStatus.Pending);
t.jsonb("reportConfigs").notNullable();
t.specificType("emailRecipients", "text[]").notNullable();
t.jsonb("resultSummary");
t.text("errorMessage");
t.timestamps(true, true, true);

t.index(["projectId"]);
t.index(["requestedByUserId"]);
});

await createOnUpdateTrigger(knex, TableName.AuditReport);
}
}

export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.AuditReport);
await knex.schema.dropTableIfExists(TableName.AuditReport);
}
25 changes: 25 additions & 0 deletions backend/src/db/schemas/audit-reports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.

import { z } from "zod";

import { TImmutableDBKeys } from "./models";

export const AuditReportsSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
requestedByUserId: z.string().uuid().nullable().optional(),
status: z.string().default("pending"),
reportConfigs: z.unknown(),
emailRecipients: z.string().array(),
resultSummary: z.unknown().nullable().optional(),
errorMessage: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});

export type TAuditReports = z.infer<typeof AuditReportsSchema>;
export type TAuditReportsInsert = Omit<z.input<typeof AuditReportsSchema>, TImmutableDBKeys>;
export type TAuditReportsUpdate = Partial<Omit<z.input<typeof AuditReportsSchema>, TImmutableDBKeys>>;
4 changes: 2 additions & 2 deletions backend/src/db/schemas/certificate-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export const CertificateRequestsSchema = z.object({
state: z.string().nullable().optional(),
locality: z.string().nullable().optional(),
encryptedPrivateKey: zodBuffer.nullable().optional(),
pendingMessage: z.string().nullable().optional(),
applicationId: z.string().uuid().nullable().optional()
applicationId: z.string().uuid().nullable().optional(),
pendingMessage: z.string().nullable().optional()
});

export type TCertificateRequests = z.infer<typeof CertificateRequestsSchema>;
Expand Down
1 change: 1 addition & 0 deletions backend/src/db/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from "./approval-requests";
export * from "./audit-log-stream-outbox";
export * from "./audit-log-streams";
export * from "./audit-logs";
export * from "./audit-reports";
export * from "./auth-token-sessions";
export * from "./auth-tokens";
export * from "./backup-private-key";
Expand Down
3 changes: 3 additions & 0 deletions backend/src/db/schemas/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ export enum TableName {
HoneyTokenEvent = "honey_token_events",
HoneyTokenSecretMapping = "honey_token_secret_mappings",

// Audit Reports (exportable compliance reports)
AuditReport = "audit_reports",

// Deprecated - Not used anymore now that Redis is persistent
DeprecatedDurableQueueJobs = "queue_jobs",
DeprecatedSecretRotationV1 = "secret_rotations",
Expand Down
3 changes: 2 additions & 1 deletion backend/src/db/schemas/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const OrganizationsSchema = z.object({
rootOrgId: z.string().uuid().nullable().optional(),
blockDuplicateSecretSyncDestinations: z.boolean().default(false),
secretShareBrandConfig: z.unknown().nullable().optional(),
defaultCertManagerProjectId: z.string().nullable().optional()
defaultCertManagerProjectId: z.string().nullable().optional(),
allowCrossProjectSecretSharing: z.boolean().default(false)
});

export type TOrganizations = z.infer<typeof OrganizationsSchema>;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/db/schemas/pam-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export const PamSessionsSchema = z.object({
aiInsightsStatus: z.string().nullable().optional(),
aiInsightsError: z.string().nullable().optional(),
reason: z.string().nullable().optional(),
selectedResourceId: z.string().uuid().nullable().optional(),
encryptedSessionKey: zodBuffer.nullable().optional(),
gatewayUploadTokenHash: zodBuffer.nullable().optional(),
selectedResourceId: z.string().uuid().nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
});

Expand Down
21 changes: 21 additions & 0 deletions backend/src/db/schemas/project-folder-grants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.

import { z } from "zod";

import { TImmutableDBKeys } from "./models";

export const ProjectFolderGrantsSchema = z.object({
id: z.string().uuid(),
sourceProjectId: z.string(),
sourceFolderId: z.string().uuid(),
targetProjectId: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});

export type TProjectFolderGrants = z.infer<typeof ProjectFolderGrantsSchema>;
export type TProjectFolderGrantsInsert = Omit<z.input<typeof ProjectFolderGrantsSchema>, TImmutableDBKeys>;
export type TProjectFolderGrantsUpdate = Partial<Omit<z.input<typeof ProjectFolderGrantsSchema>, TImmutableDBKeys>>;
176 changes: 176 additions & 0 deletions backend/src/ee/routes/v1/audit-report-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { z } from "zod";

import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
AuditReportResultEntrySchema,
AuditReportStatus,
AuditReportType
} from "@app/ee/services/audit-report/audit-report-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

const AuditReportConfigSchema = z.object({
type: z.nativeEnum(AuditReportType),
inputs: z.record(z.unknown())
});

const AuditReportSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
requestedByUserId: z.string().uuid().nullable(),
status: z.nativeEnum(AuditReportStatus),
reportConfigs: z.array(AuditReportConfigSchema),
emailRecipients: z.string().array(),
resultSummary: z.array(AuditReportResultEntrySchema).nullable(),
errorMessage: z.string().nullable(),
createdAt: z.date(),
updatedAt: z.date()
});

export const registerAuditReportRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: { rateLimit: writeLimit },
schema: {
operationId: "requestAuditReport",
description: "Request generation of one or more audit reports, delivered by email as CSV attachments",
security: [{ bearerAuth: [] }],
body: z.object({
projectId: z.string().trim(),
reports: z
.array(
z.object({
Comment thread
varonix0 marked this conversation as resolved.
Outdated
type: z.nativeEnum(AuditReportType),
inputs: z.record(z.unknown()).optional()
})
)
.min(1)
.describe("The report types to generate, each with optional type-specific inputs."),
emailRecipients: z
.array(z.string().email())
.optional()
.describe("Email addresses to deliver the reports to. Defaults to the requesting user's email.")
}),
response: {
200: AuditReportSchema
}
},
onRequest: verifyAuth([AuthMode.JWT]),
Comment thread
varonix0 marked this conversation as resolved.
Outdated
handler: async (req) => {
const report = await server.services.auditReport.requestReport(req.body, req.permission);
await server.services.auditLog.createAuditLog({
projectId: report.projectId,
event: {
type: EventType.CREATE_AUDIT_REPORT,
metadata: {
auditReportId: report.id,
projectId: report.projectId,
reportTypes: report.reportConfigs.map((config) => config.type),
recipientCount: report.emailRecipients.length
}
},
...req.auditLogInfo
});
return report;
}
});

server.route({
method: "GET",
url: "/",
config: { rateLimit: readLimit },
schema: {
operationId: "listAuditReports",
description: "List audit reports for a project",
security: [{ bearerAuth: [] }],
querystring: z.object({
projectId: z.string().trim(),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(10)
}),
response: {
200: z.object({ reports: z.array(AuditReportSchema), totalCount: z.number() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { projectId, offset, limit } = req.query;
const { reports, totalCount } = await server.services.auditReport.listReports(
{ projectId, offset, limit },
req.permission
);
await server.services.auditLog.createAuditLog({
projectId,
event: { type: EventType.GET_AUDIT_REPORTS, metadata: { projectId } },
...req.auditLogInfo
});
return { reports, totalCount };
}
});

server.route({
method: "GET",
url: "/:auditReportId",
config: { rateLimit: readLimit },
schema: {
operationId: "getAuditReport",
description: "Get a single audit report by ID",
security: [{ bearerAuth: [] }],
params: z.object({ auditReportId: z.string().uuid() }),
querystring: z.object({ projectId: z.string().trim() }),
Comment thread
varonix0 marked this conversation as resolved.
Outdated
response: {
200: AuditReportSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const report = await server.services.auditReport.getReportById(
{ projectId: req.query.projectId, auditReportId: req.params.auditReportId },
req.permission
);
await server.services.auditLog.createAuditLog({
projectId: report.projectId,
event: {
type: EventType.GET_AUDIT_REPORT,
metadata: { auditReportId: report.id, projectId: report.projectId }
},
...req.auditLogInfo
});
return report;
}
});

server.route({
method: "DELETE",
url: "/:auditReportId",
config: { rateLimit: writeLimit },
schema: {
operationId: "deleteAuditReport",
description: "Delete an audit report",
security: [{ bearerAuth: [] }],
params: z.object({ auditReportId: z.string().uuid() }),
querystring: z.object({ projectId: z.string().trim() }),
response: {
200: AuditReportSchema
}
},
onRequest: verifyAuth([AuthMode.JWT]),
Comment thread
varonix0 marked this conversation as resolved.
Outdated
handler: async (req) => {
const report = await server.services.auditReport.deleteReport(
{ projectId: req.query.projectId, auditReportId: req.params.auditReportId },
req.permission
);
await server.services.auditLog.createAuditLog({
projectId: report.projectId,
event: {
type: EventType.DELETE_AUDIT_REPORT,
metadata: { auditReportId: report.id, projectId: report.projectId }
},
...req.auditLogInfo
});
return report;
}
});
};
2 changes: 2 additions & 0 deletions backend/src/ee/routes/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { registerAiMcpEndpointRouter } from "./ai-mcp-endpoint-router";
import { registerAiMcpServerRouter } from "./ai-mcp-server-router";
import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
import { AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP, registerAuditLogStreamRouter } from "./audit-log-stream-routers";
import { registerAuditReportRouter } from "./audit-report-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDeprecatedProjectRoleRouter } from "./deprecated-project-role-router";
import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
Expand Down Expand Up @@ -132,6 +133,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {

await server.register(registerInsightsRouter, { prefix: "/insights" });
await server.register(registerPamInsightsRouter, { prefix: "/insights/pam" });
await server.register(registerAuditReportRouter, { prefix: "/audit-reports" });

await server.register(
async (pkiRouter) => {
Expand Down
Loading
Loading