Merge pull request #3754 from Infisical/add-webhook-trigger-audit-log

improvement(project-webhooks): Add webhook triggered audit log
This commit is contained in:
x032205
2025-06-09 15:39:42 -04:00
committed by GitHub
9 changed files with 87 additions and 13 deletions
@@ -44,6 +44,7 @@ import {
TSecretSyncRaw, TSecretSyncRaw,
TUpdateSecretSyncDTO TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types"; } 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 { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
import { KmipPermission } from "../kmip/kmip-enum"; import { KmipPermission } from "../kmip/kmip-enum";
@@ -206,6 +207,7 @@ export enum EventType {
CREATE_WEBHOOK = "create-webhook", CREATE_WEBHOOK = "create-webhook",
UPDATE_WEBHOOK_STATUS = "update-webhook-status", UPDATE_WEBHOOK_STATUS = "update-webhook-status",
DELETE_WEBHOOK = "delete-webhook", DELETE_WEBHOOK = "delete-webhook",
WEBHOOK_TRIGGERED = "webhook-triggered",
GET_SECRET_IMPORTS = "get-secret-imports", GET_SECRET_IMPORTS = "get-secret-imports",
GET_SECRET_IMPORT = "get-secret-import", GET_SECRET_IMPORT = "get-secret-import",
CREATE_SECRET_IMPORT = "create-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 { interface GetSecretImportsEvent {
type: EventType.GET_SECRET_IMPORTS; type: EventType.GET_SECRET_IMPORTS;
metadata: { metadata: {
@@ -3221,6 +3231,7 @@ export type Event =
| CreateWebhookEvent | CreateWebhookEvent
| UpdateWebhookStatusEvent | UpdateWebhookStatusEvent
| DeleteWebhookEvent | DeleteWebhookEvent
| WebhookTriggeredEvent
| GetSecretImportsEvent | GetSecretImportsEvent
| GetSecretImportEvent | GetSecretImportEvent
| CreateSecretImportEvent | CreateSecretImportEvent
@@ -1581,6 +1581,7 @@ export const secretQueueFactory = ({
projectDAL, projectDAL,
webhookDAL, webhookDAL,
event: job.data, event: job.data,
auditLogService,
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
}); });
}); });
+54 -6
View File
@@ -4,9 +4,12 @@ import { AxiosError } from "axios";
import picomatch from "picomatch"; import picomatch from "picomatch";
import { TWebhooks } from "@app/db/schemas"; 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 { request } from "@app/lib/config/request";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -163,6 +166,7 @@ export type TFnTriggerWebhookDTO = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
secretManagerDecryptor: (value: Buffer) => string; secretManagerDecryptor: (value: Buffer) => string;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
}; };
// this is reusable function // this is reusable function
@@ -175,7 +179,8 @@ export const fnTriggerWebhook = async ({
projectEnvDAL, projectEnvDAL,
event, event,
secretManagerDecryptor, secretManagerDecryptor,
projectDAL projectDAL,
auditLogService
}: TFnTriggerWebhookDTO) => { }: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter( const toBeTriggeredHooks = webhooks.filter(
@@ -200,16 +205,43 @@ export const fnTriggerWebhook = async ({
}) })
); );
const eventPayloads: WebhookTriggeredEvent["metadata"][] = [];
// filter hooks by status // filter hooks by status
const successWebhooks = webhooksTriggered const successWebhooks = webhooksTriggered
.filter(({ status }) => status === "fulfilled") .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 const failedWebhooks = webhooksTriggered
.filter(({ status }) => status === "rejected") .filter(({ status }) => status === "rejected")
.map((data, i) => ({ .map((data, i) => {
id: toBeTriggeredHooks[i].id, eventPayloads.push({
error: data.status === "rejected" ? (data.reason as AxiosError).message : "" 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) => { await webhookDAL.transaction(async (tx) => {
const env = await projectEnvDAL.findOne({ projectId, slug: environment }, 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"); logger.info({ environment, secretPath, projectId }, "Secret webhook job ended");
}; };
@@ -5,12 +5,12 @@ description: "Learn how to stream Infisical Audit Logs to external logging provi
<Info> <Info>
Audit log streams is a paid feature. 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, If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
</Info> </Info>
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. 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 ### Audit Logs Structure
<ParamField path="id" type="string" required> <ParamField path="id" type="string" required>
The unique identifier for the log entry. The unique identifier for the log entry.
</ParamField> </ParamField>
<ParamField path="actor" type="platform | user | service | identity | scimClient | unknownUser" required> <ParamField path="actor" type="platform | user | service | identity | scimClient | unknownUser" required>
@@ -168,7 +168,7 @@ Each log entry sent to the external logging provider will follow the same struct
<Note> <Note>
If the `actor` field is set to `platform`, `scimClient`, or `unknownUser`, the `actorMetadata` field will be an empty object. If the `actor` field is set to `platform`, `scimClient`, or `unknownUser`, the `actorMetadata` field will be an empty object.
</Note> </Note>
</ParamField> </ParamField>
<ParamField path="ipAddress" type="string" required> <ParamField path="ipAddress" type="string" required>
@@ -178,7 +178,7 @@ Each log entry sent to the external logging provider will follow the same struct
<ParamField path="eventType" type="string" required> <ParamField path="eventType" type="string" required>
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. 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`.
</ParamField> </ParamField>
<ParamField path="eventMetadata" type="object" required> <ParamField path="eventMetadata" type="object" required>
@@ -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 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. The `projectName` field will only be present if the event occurred at the project level, not the organization level.
</ParamField> </ParamField>
+1 -1
View File
@@ -27,7 +27,7 @@ If the signature in the header matches the signature that you generated, then yo
```json ```json
{ {
"event": "secret.modified", "event": "secrets.modified",
"project": { "project": {
"workspaceId": "the workspace id", "workspaceId": "the workspace id",
"environment": "project environment", "environment": "project environment",
@@ -52,6 +52,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_WEBHOOK]: "Create webhook", [EventType.CREATE_WEBHOOK]: "Create webhook",
[EventType.UPDATE_WEBHOOK_STATUS]: "Update webhook status", [EventType.UPDATE_WEBHOOK_STATUS]: "Update webhook status",
[EventType.DELETE_WEBHOOK]: "Delete webhook", [EventType.DELETE_WEBHOOK]: "Delete webhook",
[EventType.WEBHOOK_TRIGGERED]: "Webhook event",
[EventType.GET_SECRET_IMPORTS]: "List secret imports", [EventType.GET_SECRET_IMPORTS]: "List secret imports",
[EventType.CREATE_SECRET_IMPORT]: "Create secret import", [EventType.CREATE_SECRET_IMPORT]: "Create secret import",
[EventType.UPDATE_SECRET_IMPORT]: "Update secret import", [EventType.UPDATE_SECRET_IMPORT]: "Update secret import",
@@ -65,6 +65,7 @@ export enum EventType {
CREATE_WEBHOOK = "create-webhook", CREATE_WEBHOOK = "create-webhook",
UPDATE_WEBHOOK_STATUS = "update-webhook-status", UPDATE_WEBHOOK_STATUS = "update-webhook-status",
DELETE_WEBHOOK = "delete-webhook", DELETE_WEBHOOK = "delete-webhook",
WEBHOOK_TRIGGERED = "webhook-triggered",
GET_SECRET_IMPORTS = "get-secret-imports", GET_SECRET_IMPORTS = "get-secret-imports",
CREATE_SECRET_IMPORT = "create-secret-import", CREATE_SECRET_IMPORT = "create-secret-import",
UPDATE_SECRET_IMPORT = "update-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import",
@@ -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 { interface GetSecretImportsEvent {
type: EventType.GET_SECRET_IMPORTS; type: EventType.GET_SECRET_IMPORTS;
metadata: { metadata: {
@@ -891,6 +901,7 @@ export type Event =
| CreateWebhookEvent | CreateWebhookEvent
| UpdateWebhookStatusEvent | UpdateWebhookStatusEvent
| DeleteWebhookEvent | DeleteWebhookEvent
| WebhookTriggeredEvent
| GetSecretImportsEvent | GetSecretImportsEvent
| CreateSecretImportEvent | CreateSecretImportEvent
| UpdateSecretImportEvent | UpdateSecretImportEvent
@@ -194,6 +194,7 @@ export const WebhooksTab = withProjectPermission(
<Tr key={id}> <Tr key={id}>
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all"> <Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{url} {url}
<p className="text-xs text-mineshaft-400">{id}</p>
</Td> </Td>
<Td>{environment.slug}</Td> <Td>{environment.slug}</Td>
<Td>{secretPath}</Td> <Td>{secretPath}</Td>