From 219aa3c6416a2afb2ed227aeace77233a5d469a3 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 6 Jun 2025 16:06:29 -0700 Subject: [PATCH 1/2] improvement: add webhook triggered audit log --- .../ee/services/audit-log/audit-log-types.ts | 11 ++++ backend/src/services/secret/secret-queue.ts | 1 + backend/src/services/webhook/webhook-fns.ts | 60 +++++++++++++++++-- docs/documentation/platform/webhooks.mdx | 2 +- .../src/hooks/api/auditLogs/constants.tsx | 1 + frontend/src/hooks/api/auditLogs/enums.tsx | 1 + .../components/WebhooksTab/WebhooksTab.tsx | 1 + 7 files changed, 70 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index cfc01741f..e43e02f37 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -44,6 +44,7 @@ import { TSecretSyncRaw, TUpdateSecretSyncDTO } from "@app/services/secret-sync/secret-sync-types"; +import { TWebhookPayloads } from "@app/services/webhook/webhook-types"; import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types"; import { KmipPermission } from "../kmip/kmip-enum"; @@ -206,6 +207,7 @@ export enum EventType { CREATE_WEBHOOK = "create-webhook", UPDATE_WEBHOOK_STATUS = "update-webhook-status", DELETE_WEBHOOK = "delete-webhook", + WEBHOOK_TRIGGERED = "webhook-triggered", GET_SECRET_IMPORTS = "get-secret-imports", GET_SECRET_IMPORT = "get-secret-import", CREATE_SECRET_IMPORT = "create-secret-import", @@ -1440,6 +1442,14 @@ interface DeleteWebhookEvent { }; } +export interface WebhookTriggeredEvent { + type: EventType.WEBHOOK_TRIGGERED; + metadata: { + webhookId: string; + status: string; + } & TWebhookPayloads; +} + interface GetSecretImportsEvent { type: EventType.GET_SECRET_IMPORTS; metadata: { @@ -3221,6 +3231,7 @@ export type Event = | CreateWebhookEvent | UpdateWebhookStatusEvent | DeleteWebhookEvent + | WebhookTriggeredEvent | GetSecretImportsEvent | GetSecretImportEvent | CreateSecretImportEvent diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 714df0d3f..6c9cf64d3 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1581,6 +1581,7 @@ export const secretQueueFactory = ({ projectDAL, webhookDAL, event: job.data, + auditLogService, secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() }); }); diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index a16158e14..6729e9a37 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -4,9 +4,12 @@ import { AxiosError } from "axios"; import picomatch from "picomatch"; import { TWebhooks } from "@app/db/schemas"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { EventType, WebhookTriggeredEvent } from "@app/ee/services/audit-log/audit-log-types"; import { request } from "@app/lib/config/request"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { ActorType } from "@app/services/auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -163,6 +166,7 @@ export type TFnTriggerWebhookDTO = { projectEnvDAL: Pick; projectDAL: Pick; secretManagerDecryptor: (value: Buffer) => string; + auditLogService: Pick; }; // this is reusable function @@ -175,7 +179,8 @@ export const fnTriggerWebhook = async ({ projectEnvDAL, event, secretManagerDecryptor, - projectDAL + projectDAL, + auditLogService }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -200,16 +205,43 @@ export const fnTriggerWebhook = async ({ }) ); + const eventPayloads: WebhookTriggeredEvent["metadata"][] = []; // filter hooks by status const successWebhooks = webhooksTriggered .filter(({ status }) => status === "fulfilled") - .map((_, i) => toBeTriggeredHooks[i].id); + .map((_, i) => { + eventPayloads.push({ + webhookId: toBeTriggeredHooks[i].id, + type: event.type, + payload: { + type: toBeTriggeredHooks[i].type!, + ...event.payload, + projectName + }, + status: "success" + } as WebhookTriggeredEvent["metadata"]); + + return toBeTriggeredHooks[i].id; + }); const failedWebhooks = webhooksTriggered .filter(({ status }) => status === "rejected") - .map((data, i) => ({ - id: toBeTriggeredHooks[i].id, - error: data.status === "rejected" ? (data.reason as AxiosError).message : "" - })); + .map((data, i) => { + eventPayloads.push({ + webhookId: toBeTriggeredHooks[i].id, + type: event.type, + payload: { + type: toBeTriggeredHooks[i].type!, + ...event.payload, + projectName + }, + status: "failed" + } as WebhookTriggeredEvent["metadata"]); + + return { + id: toBeTriggeredHooks[i].id, + error: data.status === "rejected" ? (data.reason as AxiosError).message : "" + }; + }); await webhookDAL.transaction(async (tx) => { const env = await projectEnvDAL.findOne({ projectId, slug: environment }, tx); @@ -236,5 +268,21 @@ export const fnTriggerWebhook = async ({ ); } }); + + for (const eventPayload of eventPayloads) { + // eslint-disable-next-line no-await-in-loop + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + projectId, + event: { + type: EventType.WEBHOOK_TRIGGERED, + metadata: eventPayload + } + }); + } + logger.info({ environment, secretPath, projectId }, "Secret webhook job ended"); }; diff --git a/docs/documentation/platform/webhooks.mdx b/docs/documentation/platform/webhooks.mdx index 92d3ff8b8..e6e71b9b0 100644 --- a/docs/documentation/platform/webhooks.mdx +++ b/docs/documentation/platform/webhooks.mdx @@ -27,7 +27,7 @@ If the signature in the header matches the signature that you generated, then yo ```json { - "event": "secret.modified", + "event": "secrets.modified", "project": { "workspaceId": "the workspace id", "environment": "project environment", diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index ca5a38e0b..696f5745f 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -52,6 +52,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_WEBHOOK]: "Create webhook", [EventType.UPDATE_WEBHOOK_STATUS]: "Update webhook status", [EventType.DELETE_WEBHOOK]: "Delete webhook", + [EventType.WEBHOOK_TRIGGERED]: "Webhook event", [EventType.GET_SECRET_IMPORTS]: "List secret imports", [EventType.CREATE_SECRET_IMPORT]: "Create secret import", [EventType.UPDATE_SECRET_IMPORT]: "Update secret import", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 67f0bac99..159a61808 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -65,6 +65,7 @@ export enum EventType { CREATE_WEBHOOK = "create-webhook", UPDATE_WEBHOOK_STATUS = "update-webhook-status", DELETE_WEBHOOK = "delete-webhook", + WEBHOOK_TRIGGERED = "webhook-triggered", GET_SECRET_IMPORTS = "get-secret-imports", CREATE_SECRET_IMPORT = "create-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import", diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx index 035a0eaa1..8c8d75f47 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WebhooksTab/WebhooksTab.tsx @@ -194,6 +194,7 @@ export const WebhooksTab = withProjectPermission( {url} +

{id}

{environment.slug} {secretPath} From 977fd7a057e25099cf364b66c2797fdce82f60b7 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 9 Jun 2025 15:34:32 -0400 Subject: [PATCH 2/2] Small tweaks --- .../platform/audit-log-streams/audit-log-streams.mdx | 12 ++++++------ frontend/src/hooks/api/auditLogs/types.tsx | 11 +++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx b/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx index 2f38a3930..2ee4dff92 100644 --- a/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx +++ b/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx @@ -5,12 +5,12 @@ description: "Learn how to stream Infisical Audit Logs to external logging provi Audit log streams is a paid feature. - + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it. -Infisical Audit Log Streaming enables you to transmit your organization's Audit Logs to external logging providers for monitoring and analysis. +Infisical Audit Log Streaming enables you to transmit your organization's Audit Logs to external logging providers for monitoring and analysis. The logs are formatted in JSON, requiring your logging provider to support JSON-based log parsing. @@ -118,7 +118,7 @@ Each log entry sent to the external logging provider will follow the same struct ### Audit Logs Structure - The unique identifier for the log entry. + The unique identifier for the log entry. @@ -168,7 +168,7 @@ Each log entry sent to the external logging provider will follow the same struct If the `actor` field is set to `platform`, `scimClient`, or `unknownUser`, the `actorMetadata` field will be an empty object. - + @@ -178,7 +178,7 @@ Each log entry sent to the external logging provider will follow the same struct The type of event that occurred. Below you can see a list of possible event types. More event types will be added in the future as we expand our audit logs further. - `get-secrets`, `delete-secrets`, `get-secret`, `create-secret`, `update-secret`, `delete-secret`, `get-workspace-key`, `authorize-integration`, `update-integration-auth`, `unauthorize-integration`, `create-integration`, `delete-integration`, `add-trusted-ip`, `update-trusted-ip`, `delete-trusted-ip`, `create-service-token`, `delete-service-token`, `create-identity`, `update-identity`, `delete-identity`, `login-identity-universal-auth`, `add-identity-universal-auth`, `update-identity-universal-auth`, `get-identity-universal-auth`, `create-identity-universal-auth-client-secret`, `revoke-identity-universal-auth-client-secret`, `get-identity-universal-auth-client-secret`, `create-environment`, `update-environment`, `delete-environment`, `add-workspace-member`, `remove-workspace-member`, `create-folder`, `update-folder`, `delete-folder`, `create-webhook`, `update-webhook-status`, `delete-webhook`, `get-secret-imports`, `create-secret-import`, `update-secret-import`, `delete-secret-import`, `update-user-workspace-role`, `update-user-workspace-denied-permissions`, `create-certificate-authority`, `get-certificate-authority`, `update-certificate-authority`, `delete-certificate-authority`, `get-certificate-authority-csr`, `get-certificate-authority-cert`, `sign-intermediate`, `import-certificate-authority-cert`, `get-certificate-authority-crl`, `issue-cert`, `get-cert`, `delete-cert`, `revoke-cert`, `get-cert-body`, `create-pki-alert`, `get-pki-alert`, `update-pki-alert`, `delete-pki-alert`, `create-pki-collection`, `get-pki-collection`, `update-pki-collection`, `delete-pki-collection`, `get-pki-collection-items`, `add-pki-collection-item`, `delete-pki-collection-item`, `org-admin-accessed-project`, `create-certificate-template`, `update-certificate-template`, `delete-certificate-template`, `get-certificate-template`, `create-certificate-template-est-config`, `update-certificate-template-est-config`, `get-certificate-template-est-config`, `update-project-slack-config`, `get-project-slack-config`, `integration-synced`, `create-shared-secret`, `delete-shared-secret`, `read-shared-secret`. + `get-secrets`, `delete-secrets`, `get-secret`, `create-secret`, `update-secret`, `delete-secret`, `get-workspace-key`, `authorize-integration`, `update-integration-auth`, `unauthorize-integration`, `create-integration`, `delete-integration`, `add-trusted-ip`, `update-trusted-ip`, `delete-trusted-ip`, `create-service-token`, `delete-service-token`, `create-identity`, `update-identity`, `delete-identity`, `login-identity-universal-auth`, `add-identity-universal-auth`, `update-identity-universal-auth`, `get-identity-universal-auth`, `create-identity-universal-auth-client-secret`, `revoke-identity-universal-auth-client-secret`, `get-identity-universal-auth-client-secret`, `create-environment`, `update-environment`, `delete-environment`, `add-workspace-member`, `remove-workspace-member`, `create-folder`, `update-folder`, `delete-folder`, `create-webhook`, `update-webhook-status`, `delete-webhook`, `webhook-triggered`, `get-secret-imports`, `create-secret-import`, `update-secret-import`, `delete-secret-import`, `update-user-workspace-role`, `update-user-workspace-denied-permissions`, `create-certificate-authority`, `get-certificate-authority`, `update-certificate-authority`, `delete-certificate-authority`, `get-certificate-authority-csr`, `get-certificate-authority-cert`, `sign-intermediate`, `import-certificate-authority-cert`, `get-certificate-authority-crl`, `issue-cert`, `get-cert`, `delete-cert`, `revoke-cert`, `get-cert-body`, `create-pki-alert`, `get-pki-alert`, `update-pki-alert`, `delete-pki-alert`, `create-pki-collection`, `get-pki-collection`, `update-pki-collection`, `delete-pki-collection`, `get-pki-collection-items`, `add-pki-collection-item`, `delete-pki-collection-item`, `org-admin-accessed-project`, `create-certificate-template`, `update-certificate-template`, `delete-certificate-template`, `get-certificate-template`, `create-certificate-template-est-config`, `update-certificate-template-est-config`, `get-certificate-template-est-config`, `update-project-slack-config`, `get-project-slack-config`, `integration-synced`, `create-shared-secret`, `delete-shared-secret`, `read-shared-secret`. @@ -219,4 +219,4 @@ Each log entry sent to the external logging provider will follow the same struct The name of the project where the event occurred. The `projectName` field will only be present if the event occurred at the project level, not the organization level. - \ No newline at end of file + diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 8774e1a73..006a3d7ca 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -427,6 +427,16 @@ interface DeleteWebhookEvent { }; } +export interface WebhookTriggeredEvent { + type: EventType.WEBHOOK_TRIGGERED; + metadata: { + webhookId: string; + status: string; + type: string; + payload: { [k: string]: string | null }; + }; +} + interface GetSecretImportsEvent { type: EventType.GET_SECRET_IMPORTS; metadata: { @@ -891,6 +901,7 @@ export type Event = | CreateWebhookEvent | UpdateWebhookStatusEvent | DeleteWebhookEvent + | WebhookTriggeredEvent | GetSecretImportsEvent | CreateSecretImportEvent | UpdateSecretImportEvent