mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
Merge pull request #3754 from Infisical/add-webhook-trigger-audit-log
improvement(project-webhooks): Add webhook triggered audit log
This commit is contained in:
@@ -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()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user