Compare commits

...

2 Commits

Author SHA1 Message Date
x032205
977fd7a057 Small tweaks 2025-06-09 15:34:32 -04:00
Scott Wilson
219aa3c641 improvement: add webhook triggered audit log 2025-06-06 16:06:29 -07:00
9 changed files with 87 additions and 13 deletions

View File

@@ -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

View File

@@ -1581,6 +1581,7 @@ export const secretQueueFactory = ({
projectDAL,
webhookDAL,
event: job.data,
auditLogService,
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
});
});

View File

@@ -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<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">;
secretManagerDecryptor: (value: Buffer) => string;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
// 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");
};

View File

@@ -5,12 +5,12 @@ description: "Learn how to stream Infisical Audit Logs to external logging provi
<Info>
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.
</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.
@@ -118,7 +118,7 @@ Each log entry sent to the external logging provider will follow the same struct
### Audit Logs Structure
<ParamField path="id" type="string" required>
The unique identifier for the log entry.
The unique identifier for the log entry.
</ParamField>
<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>
If the `actor` field is set to `platform`, `scimClient`, or `unknownUser`, the `actorMetadata` field will be an empty object.
</Note>
</ParamField>
<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>
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 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 `projectName` field will only be present if the event occurred at the project level, not the organization level.
</ParamField>
</ParamField>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -194,6 +194,7 @@ export const WebhooksTab = withProjectPermission(
<Tr key={id}>
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{url}
<p className="text-xs text-mineshaft-400">{id}</p>
</Td>
<Td>{environment.slug}</Td>
<Td>{secretPath}</Td>