Compare commits

...

1 Commits

Author SHA1 Message Date
b3da70a993 wip 2025-05-28 11:45:48 -07:00
193 changed files with 11139 additions and 422 deletions

View File

@ -107,6 +107,14 @@ INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
INF_APP_CONNECTION_GITHUB_APP_SLUG= INF_APP_CONNECTION_GITHUB_APP_SLUG=
INF_APP_CONNECTION_GITHUB_APP_ID= INF_APP_CONNECTION_GITHUB_APP_ID=
#github radar app connection
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID=
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET=
INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY=
INF_APP_CONNECTION_GITHUB_RADAR_APP_SLUG=
INF_APP_CONNECTION_GITHUB_RADAR_APP_ID=
INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET=
#gcp app connection #gcp app connection
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=

View File

@ -37,6 +37,7 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service"; import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service"; import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
import { TSecretScanningV2ServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-service";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service"; import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
@ -268,6 +269,7 @@ declare module "fastify" {
microsoftTeams: TMicrosoftTeamsServiceFactory; microsoftTeams: TMicrosoftTeamsServiceFactory;
assumePrivileges: TAssumePrivilegeServiceFactory; assumePrivileges: TAssumePrivilegeServiceFactory;
githubOrgSync: TGithubOrgSyncServiceFactory; githubOrgSync: TGithubOrgSyncServiceFactory;
secretScanningV2: TSecretScanningV2ServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer

View File

@ -324,9 +324,21 @@ import {
TSecretRotationV2SecretMappingsInsert, TSecretRotationV2SecretMappingsInsert,
TSecretRotationV2SecretMappingsUpdate, TSecretRotationV2SecretMappingsUpdate,
TSecrets, TSecrets,
TSecretScanningDataSources,
TSecretScanningDataSourcesInsert,
TSecretScanningDataSourcesUpdate,
TSecretScanningFindings,
TSecretScanningFindingsInsert,
TSecretScanningFindingsUpdate,
TSecretScanningGitRisks, TSecretScanningGitRisks,
TSecretScanningGitRisksInsert, TSecretScanningGitRisksInsert,
TSecretScanningGitRisksUpdate, TSecretScanningGitRisksUpdate,
TSecretScanningResources,
TSecretScanningResourcesInsert,
TSecretScanningResourcesUpdate,
TSecretScanningScans,
TSecretScanningScansInsert,
TSecretScanningScansUpdate,
TSecretSharing, TSecretSharing,
TSecretSharingInsert, TSecretSharingInsert,
TSecretSharingUpdate, TSecretSharingUpdate,
@ -1074,5 +1086,25 @@ declare module "knex/types/tables" {
TGithubOrgSyncConfigsInsert, TGithubOrgSyncConfigsInsert,
TGithubOrgSyncConfigsUpdate TGithubOrgSyncConfigsUpdate
>; >;
[TableName.SecretScanningDataSource]: KnexOriginal.CompositeTableType<
TSecretScanningDataSources,
TSecretScanningDataSourcesInsert,
TSecretScanningDataSourcesUpdate
>;
[TableName.SecretScanningResource]: KnexOriginal.CompositeTableType<
TSecretScanningResources,
TSecretScanningResourcesInsert,
TSecretScanningResourcesUpdate
>;
[TableName.SecretScanningScan]: KnexOriginal.CompositeTableType<
TSecretScanningScans,
TSecretScanningScansInsert,
TSecretScanningScansUpdate
>;
[TableName.SecretScanningFinding]: KnexOriginal.CompositeTableType<
TSecretScanningFindings,
TSecretScanningFindingsInsert,
TSecretScanningFindingsUpdate
>;
} }
} }

View File

@ -0,0 +1,94 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
import {
SecretScanningFindingStatus,
SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.SecretScanningDataSource))) {
await knex.schema.createTable(TableName.SecretScanningDataSource, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("externalId").index(); // if we need a unique way of identifying this data source from an external resource
t.string("name", 48).notNullable();
t.string("description");
t.string("type").notNullable();
t.jsonb("config").notNullable();
t.binary("encryptedCredentials"); // webhook credentials, etc.
t.uuid("connectionId");
t.boolean("isAutoScanEnabled").defaultTo(true);
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.timestamps(true, true, true);
t.unique(["projectId", "name"]);
});
await createOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
}
if (!(await knex.schema.hasTable(TableName.SecretScanningResource))) {
await knex.schema.createTable(TableName.SecretScanningResource, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("externalId").notNullable();
t.string("name").notNullable();
t.string("type").notNullable();
t.uuid("dataSourceId").notNullable();
t.foreign("dataSourceId").references("id").inTable(TableName.SecretScanningDataSource).onDelete("CASCADE");
t.timestamps(true, true, true);
t.unique(["dataSourceId", "externalId"]);
});
await createOnUpdateTrigger(knex, TableName.SecretScanningResource);
}
if (!(await knex.schema.hasTable(TableName.SecretScanningScan))) {
await knex.schema.createTable(TableName.SecretScanningScan, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("status").notNullable().defaultTo(SecretScanningScanStatus.Queued);
t.string("statusMessage", 1024);
t.string("type").notNullable();
t.uuid("resourceId").notNullable();
t.foreign("resourceId").references("id").inTable(TableName.SecretScanningResource).onDelete("CASCADE");
t.timestamp("createdAt").defaultTo(knex.fn.now());
});
}
if (!(await knex.schema.hasTable(TableName.SecretScanningFinding))) {
await knex.schema.createTable(TableName.SecretScanningFinding, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("dataSourceName").notNullable();
t.string("dataSourceType").notNullable();
t.string("resourceName").notNullable();
t.string("resourceType").notNullable();
t.string("rule").notNullable();
t.string("severity").notNullable();
t.string("status").notNullable().defaultTo(SecretScanningFindingStatus.Unresolved);
t.string("remarks");
t.string("fingerprint").notNullable();
t.jsonb("details").notNullable();
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("scanId");
t.foreign("scanId").references("id").inTable(TableName.SecretScanningScan).onDelete("SET NULL");
t.timestamps(true, true, true);
t.unique(["projectId", "fingerprint"]);
});
await createOnUpdateTrigger(knex, TableName.SecretScanningFinding);
}
// TODO: Rules
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SecretScanningFinding);
await dropOnUpdateTrigger(knex, TableName.SecretScanningFinding);
await knex.schema.dropTableIfExists(TableName.SecretScanningScan);
await knex.schema.dropTableIfExists(TableName.SecretScanningResource);
await dropOnUpdateTrigger(knex, TableName.SecretScanningResource);
await knex.schema.dropTableIfExists(TableName.SecretScanningDataSource);
await dropOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
}

View File

@ -107,7 +107,11 @@ export * from "./secret-rotation-outputs";
export * from "./secret-rotation-v2-secret-mappings"; export * from "./secret-rotation-v2-secret-mappings";
export * from "./secret-rotations"; export * from "./secret-rotations";
export * from "./secret-rotations-v2"; export * from "./secret-rotations-v2";
export * from "./secret-scanning-data-sources";
export * from "./secret-scanning-findings";
export * from "./secret-scanning-git-risks"; export * from "./secret-scanning-git-risks";
export * from "./secret-scanning-resources";
export * from "./secret-scanning-scans";
export * from "./secret-sharing"; export * from "./secret-sharing";
export * from "./secret-snapshot-folders"; export * from "./secret-snapshot-folders";
export * from "./secret-snapshot-secrets"; export * from "./secret-snapshot-secrets";

View File

@ -155,7 +155,11 @@ export enum TableName {
MicrosoftTeamsIntegrations = "microsoft_teams_integrations", MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs", ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
SecretReminderRecipients = "secret_reminder_recipients", SecretReminderRecipients = "secret_reminder_recipients",
GithubOrgSyncConfig = "github_org_sync_configs" GithubOrgSyncConfig = "github_org_sync_configs",
SecretScanningDataSource = "secret_scanning_data_sources",
SecretScanningResource = "secret_scanning_resources",
SecretScanningScan = "secret_scanning_scans",
SecretScanningFinding = "secret_scanning_findings"
} }
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
@ -244,7 +248,8 @@ export enum ProjectType {
SecretManager = "secret-manager", SecretManager = "secret-manager",
CertificateManager = "cert-manager", CertificateManager = "cert-manager",
KMS = "kms", KMS = "kms",
SSH = "ssh" SSH = "ssh",
SecretScanning = "secret-scanning"
} }
export enum ActionProjectType { export enum ActionProjectType {
@ -252,6 +257,7 @@ export enum ActionProjectType {
CertificateManager = ProjectType.CertificateManager, CertificateManager = ProjectType.CertificateManager,
KMS = ProjectType.KMS, KMS = ProjectType.KMS,
SSH = ProjectType.SSH, SSH = ProjectType.SSH,
SecretScanning = ProjectType.SecretScanning,
// project operations that happen on all types // project operations that happen on all types
Any = "any" Any = "any"
} }

View File

@ -34,7 +34,9 @@ export const OrganizationsSchema = z.object({
kmsProductEnabled: z.boolean().default(true).nullable().optional(), kmsProductEnabled: z.boolean().default(true).nullable().optional(),
sshProductEnabled: z.boolean().default(true).nullable().optional(), sshProductEnabled: z.boolean().default(true).nullable().optional(),
scannerProductEnabled: z.boolean().default(true).nullable().optional(), scannerProductEnabled: z.boolean().default(true).nullable().optional(),
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional() shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
maxSharedSecretViewLimit: z.number().nullable().optional()
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@ -0,0 +1,31 @@
// 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 { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretScanningDataSourcesSchema = z.object({
id: z.string().uuid(),
externalId: z.string().nullable().optional(),
name: z.string(),
description: z.string().nullable().optional(),
type: z.string(),
config: z.unknown(),
encryptedCredentials: zodBuffer.nullable().optional(),
connectionId: z.string().uuid().nullable().optional(),
isAutoScanEnabled: z.boolean().default(true).nullable().optional(),
projectId: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretScanningDataSources = z.infer<typeof SecretScanningDataSourcesSchema>;
export type TSecretScanningDataSourcesInsert = Omit<z.input<typeof SecretScanningDataSourcesSchema>, TImmutableDBKeys>;
export type TSecretScanningDataSourcesUpdate = Partial<
Omit<z.input<typeof SecretScanningDataSourcesSchema>, TImmutableDBKeys>
>;

View File

@ -0,0 +1,32 @@
// 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 SecretScanningFindingsSchema = z.object({
id: z.string().uuid(),
dataSourceName: z.string(),
dataSourceType: z.string(),
resourceName: z.string(),
resourceType: z.string(),
rule: z.string(),
severity: z.string(),
status: z.string().default("unresolved"),
remarks: z.string().nullable().optional(),
fingerprint: z.string(),
details: z.unknown(),
projectId: z.string(),
scanId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretScanningFindings = z.infer<typeof SecretScanningFindingsSchema>;
export type TSecretScanningFindingsInsert = Omit<z.input<typeof SecretScanningFindingsSchema>, TImmutableDBKeys>;
export type TSecretScanningFindingsUpdate = Partial<
Omit<z.input<typeof SecretScanningFindingsSchema>, TImmutableDBKeys>
>;

View File

@ -0,0 +1,24 @@
// 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 SecretScanningResourcesSchema = z.object({
id: z.string().uuid(),
externalId: z.string(),
name: z.string(),
type: z.string(),
dataSourceId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretScanningResources = z.infer<typeof SecretScanningResourcesSchema>;
export type TSecretScanningResourcesInsert = Omit<z.input<typeof SecretScanningResourcesSchema>, TImmutableDBKeys>;
export type TSecretScanningResourcesUpdate = Partial<
Omit<z.input<typeof SecretScanningResourcesSchema>, TImmutableDBKeys>
>;

View File

@ -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 SecretScanningScansSchema = z.object({
id: z.string().uuid(),
status: z.string().default("queued"),
statusMessage: z.string().nullable().optional(),
type: z.string(),
resourceId: z.string().uuid(),
createdAt: z.date().nullable().optional()
});
export type TSecretScanningScans = z.infer<typeof SecretScanningScansSchema>;
export type TSecretScanningScansInsert = Omit<z.input<typeof SecretScanningScansSchema>, TImmutableDBKeys>;
export type TSecretScanningScansUpdate = Partial<Omit<z.input<typeof SecretScanningScansSchema>, TImmutableDBKeys>>;

View File

@ -2,6 +2,10 @@ import {
registerSecretRotationV2Router, registerSecretRotationV2Router,
SECRET_ROTATION_REGISTER_ROUTER_MAP SECRET_ROTATION_REGISTER_ROUTER_MAP
} from "@app/ee/routes/v2/secret-rotation-v2-routers"; } from "@app/ee/routes/v2/secret-rotation-v2-routers";
import {
registerSecretScanningV2Router,
SECRET_SCANNING_REGISTER_ROUTER_MAP
} from "@app/ee/routes/v2/secret-scanning-v2-routers";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRoleRouter } from "./project-role-router";
@ -31,4 +35,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
}, },
{ prefix: "/secret-rotations" } { prefix: "/secret-rotations" }
); );
await server.register(
async (secretScanningV2Router) => {
// register generic secret rotation endpoints
await secretScanningV2Router.register(registerSecretScanningV2Router);
// register service-specific secret rotation endpoints (gitlab/github, etc.)
for await (const [type, router] of Object.entries(SECRET_SCANNING_REGISTER_ROUTER_MAP)) {
await secretScanningV2Router.register(router, { prefix: `data-sources/${type}` });
}
},
{ prefix: "/secret-scanning" }
);
}; };

View File

@ -126,7 +126,7 @@ export const registerSecretRotationEndpoints = <
const { rotationId } = req.params; const { rotationId } = req.params;
const secretRotation = (await server.services.secretRotationV2.findSecretRotationById( const secretRotation = (await server.services.secretRotationV2.findSecretRotationById(
{ rotationId, type }, { sourceId: rotationId, type },
req.permission req.permission
)) as T; )) as T;
@ -136,7 +136,7 @@ export const registerSecretRotationEndpoints = <
event: { event: {
type: EventType.GET_SECRET_ROTATION, type: EventType.GET_SECRET_ROTATION,
metadata: { metadata: {
rotationId, sourceId: rotationId,
type, type,
secretPath: secretRotation.folder.path, secretPath: secretRotation.folder.path,
environment: secretRotation.environment.slug environment: secretRotation.environment.slug
@ -202,7 +202,7 @@ export const registerSecretRotationEndpoints = <
event: { event: {
type: EventType.GET_SECRET_ROTATION, type: EventType.GET_SECRET_ROTATION,
metadata: { metadata: {
rotationId: secretRotation.id, sourceId: secretRotation.id,
type, type,
secretPath, secretPath,
environment environment
@ -244,7 +244,7 @@ export const registerSecretRotationEndpoints = <
event: { event: {
type: EventType.CREATE_SECRET_ROTATION, type: EventType.CREATE_SECRET_ROTATION,
metadata: { metadata: {
rotationId: secretRotation.id, sourceId: secretRotation.id,
type, type,
...req.body ...req.body
} }
@ -278,7 +278,7 @@ export const registerSecretRotationEndpoints = <
const { rotationId } = req.params; const { rotationId } = req.params;
const secretRotation = (await server.services.secretRotationV2.updateSecretRotation( const secretRotation = (await server.services.secretRotationV2.updateSecretRotation(
{ ...req.body, rotationId, type }, { ...req.body, sourceId: rotationId, type },
req.permission req.permission
)) as T; )) as T;
@ -288,7 +288,7 @@ export const registerSecretRotationEndpoints = <
event: { event: {
type: EventType.UPDATE_SECRET_ROTATION, type: EventType.UPDATE_SECRET_ROTATION,
metadata: { metadata: {
rotationId, sourceId: rotationId,
type, type,
...req.body ...req.body
} }
@ -332,7 +332,7 @@ export const registerSecretRotationEndpoints = <
const { deleteSecrets, revokeGeneratedCredentials } = req.query; const { deleteSecrets, revokeGeneratedCredentials } = req.query;
const secretRotation = (await server.services.secretRotationV2.deleteSecretRotation( const secretRotation = (await server.services.secretRotationV2.deleteSecretRotation(
{ type, rotationId, deleteSecrets, revokeGeneratedCredentials }, { type, sourceId: rotationId, deleteSecrets, revokeGeneratedCredentials },
req.permission req.permission
)) as T; )) as T;
@ -343,7 +343,7 @@ export const registerSecretRotationEndpoints = <
type: EventType.DELETE_SECRET_ROTATION, type: EventType.DELETE_SECRET_ROTATION,
metadata: { metadata: {
type, type,
rotationId, sourceId: rotationId,
deleteSecrets, deleteSecrets,
revokeGeneratedCredentials revokeGeneratedCredentials
} }
@ -385,7 +385,7 @@ export const registerSecretRotationEndpoints = <
secretRotation: { activeIndex, projectId, folder, environment } secretRotation: { activeIndex, projectId, folder, environment }
} = await server.services.secretRotationV2.findSecretRotationGeneratedCredentialsById( } = await server.services.secretRotationV2.findSecretRotationGeneratedCredentialsById(
{ {
rotationId, sourceId: rotationId,
type type
}, },
req.permission req.permission
@ -398,14 +398,14 @@ export const registerSecretRotationEndpoints = <
type: EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS, type: EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS,
metadata: { metadata: {
type, type,
rotationId, sourceId: rotationId,
secretPath: folder.path, secretPath: folder.path,
environment: environment.slug environment: environment.slug
} }
} }
}); });
return { generatedCredentials: generatedCredentials as C, activeIndex, rotationId, type }; return { generatedCredentials: generatedCredentials as C, activeIndex, sourceId: rotationId, type };
} }
}); });
@ -432,7 +432,7 @@ export const registerSecretRotationEndpoints = <
const secretRotation = (await server.services.secretRotationV2.rotateSecretRotation( const secretRotation = (await server.services.secretRotationV2.rotateSecretRotation(
{ {
rotationId, sourceId: rotationId,
type, type,
auditLogInfo: req.auditLogInfo auditLogInfo: req.auditLogInfo
}, },

View File

@ -0,0 +1,16 @@
import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints";
import {
CreateGitHubDataSourceSchema,
GitHubDataSourceSchema,
UpdateGitHubDataSourceSchema
} from "@app/ee/services/secret-scanning-v2/github";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
export const registerGitHubSecretScanningRouter = async (server: FastifyZodProvider) =>
registerSecretScanningEndpoints({
type: SecretScanningDataSource.GitHub,
server,
responseSchema: GitHubDataSourceSchema,
createSchema: CreateGitHubDataSourceSchema,
updateSchema: UpdateGitHubDataSourceSchema
});

View File

@ -0,0 +1,12 @@
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { registerGitHubSecretScanningRouter } from "./github-secret-scanning-router";
export * from "./secret-scanning-v2-router";
export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record<
SecretScanningDataSource,
(server: FastifyZodProvider) => Promise<void>
> = {
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter
};

View File

@ -0,0 +1,550 @@
import { z } from "zod";
import { SecretScanningResourcesSchema, SecretScanningScansSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
SecretScanningDataSource,
SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { SECRET_SCANNING_DATA_SOURCE_NAME_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps";
import {
TSecretScanningDataSource,
TSecretScanningDataSourceInput
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { ApiDocsTags, SecretScanningDataSources } from "@app/lib/api-docs";
import { startsWithVowel } from "@app/lib/fn";
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";
export const registerSecretScanningEndpoints = <
T extends TSecretScanningDataSource,
I extends TSecretScanningDataSourceInput
>({
server,
type,
createSchema,
updateSchema,
responseSchema
}: {
type: SecretScanningDataSource;
server: FastifyZodProvider;
createSchema: z.ZodType<{
name: string;
projectId: string;
connectionId?: string;
config: Partial<I["config"]>;
description?: string | null;
isAutoScanEnabled?: boolean;
}>;
updateSchema: z.ZodType<{
name?: string;
config?: Partial<I["config"]>;
description?: string | null;
isAutoScanEnabled?: boolean;
}>;
responseSchema: z.ZodTypeAny;
}) => {
const sourceType = SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type];
server.route({
method: "GET",
url: `/`,
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `List the ${sourceType} Data Sources for the specified project.`,
querystring: z.object({
projectId: z
.string()
.trim()
.min(1, "Project ID required")
.describe(SecretScanningDataSources.LIST(type).projectId)
}),
response: {
200: z.object({ dataSources: responseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId }
} = req;
const dataSources = (await server.services.secretScanningV2.listSecretScanningDataSourcesByProjectId(
{ projectId, type },
req.permission
)) as T[];
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST,
metadata: {
type,
count: dataSources.length,
dataSourceIds: dataSources.map((source) => source.id)
}
}
});
return { dataSources };
}
});
server.route({
method: "GET",
url: "/:dataSourceId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `Get the specified ${sourceType} Data Source by ID.`,
params: z.object({
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.GET_BY_ID(type).dataSourceId)
}),
response: {
200: z.object({ dataSource: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { dataSourceId } = req.params;
const dataSource = (await server.services.secretScanningV2.findSecretScanningDataSourceById(
{ dataSourceId, type },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: dataSource.projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_GET,
metadata: {
dataSourceId,
type
}
}
});
return { dataSource };
}
});
server.route({
method: "GET",
url: `/data-source-name/:sourceName`,
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `Get the specified ${sourceType} Data Source by name and project ID.`,
params: z.object({
sourceName: z
.string()
.trim()
.min(1, "Data Source name required")
.describe(SecretScanningDataSources.GET_BY_NAME(type).sourceName)
}),
querystring: z.object({
projectId: z
.string()
.trim()
.min(1, "Project ID required")
.describe(SecretScanningDataSources.GET_BY_NAME(type).projectId)
}),
response: {
200: z.object({ dataSource: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { sourceName } = req.params;
const { projectId } = req.query;
const dataSource = (await server.services.secretScanningV2.findSecretScanningDataSourceByName(
{ sourceName, projectId, type },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_GET,
metadata: {
dataSourceId: dataSource.id,
type
}
}
});
return { dataSource };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `Create ${
startsWithVowel(sourceType) ? "an" : "a"
} ${sourceType} Data Source for the specified project.`,
body: createSchema,
response: {
200: z.object({ dataSource: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const dataSource = (await server.services.secretScanningV2.createSecretScanningDataSource(
{ ...req.body, type },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: dataSource.projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_CREATE,
metadata: {
dataSourceId: dataSource.id,
type,
...req.body
}
}
});
return { dataSource };
}
});
server.route({
method: "PATCH",
url: "/:dataSourceId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `Update the specified ${sourceType} Data Source.`,
params: z.object({
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.UPDATE(type).dataSourceId)
}),
body: updateSchema,
response: {
200: z.object({ dataSource: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { dataSourceId } = req.params;
const dataSource = (await server.services.secretScanningV2.updateSecretScanningDataSource(
{ ...req.body, dataSourceId, type },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: dataSource.projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_UPDATE,
metadata: {
dataSourceId,
type,
...req.body
}
}
});
return { dataSource };
}
});
server.route({
method: "DELETE",
url: `/:dataSourceId`,
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `Delete the specified ${sourceType} Data Source.`,
params: z.object({
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.DELETE(type).dataSourceId)
}),
response: {
200: z.object({ dataSource: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { dataSourceId } = req.params;
const dataSource = (await server.services.secretScanningV2.deleteSecretScanningResource(
{ type, dataSourceId },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: dataSource.projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_DELETE,
metadata: {
type,
dataSourceId
}
}
});
return { dataSource };
}
});
server.route({
method: "POST",
url: `/:dataSourceId/scan`,
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `Trigger a scan for the specified ${sourceType} Data Source.`,
params: z.object({
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.SCAN(type).dataSourceId)
}),
response: {
200: z.object({ dataSource: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { dataSourceId } = req.params;
const dataSource = (await server.services.secretScanningV2.triggerSecretScanningDataSourceScan(
{ type, dataSourceId },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: dataSource.projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
metadata: {
type,
dataSourceId
}
}
});
return { dataSource };
}
});
server.route({
method: "POST",
url: `/:dataSourceId/resources/:resourceId/scan`,
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `Trigger a scan for the specified ${sourceType} Data Source resource.`,
params: z.object({
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.SCAN(type).dataSourceId),
resourceId: z.string().uuid().describe(SecretScanningDataSources.SCAN(type).resourceId)
}),
response: {
200: z.object({ dataSource: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { dataSourceId, resourceId } = req.params;
const dataSource = (await server.services.secretScanningV2.triggerSecretScanningDataSourceScan(
{ type, dataSourceId, resourceId },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: dataSource.projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
metadata: {
type,
dataSourceId,
resourceId
}
}
});
return { dataSource };
}
});
server.route({
method: "GET",
url: "/:dataSourceId/resources",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: `Get the resources associated with the specified ${sourceType} Data Source by ID.`,
params: z.object({
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.LIST_RESOURCES(type).dataSourceId)
}),
response: {
200: z.object({ resources: SecretScanningResourcesSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { dataSourceId } = req.params;
const { resources, projectId } = await server.services.secretScanningV2.listSecretScanningResourcesByDataSourceId(
{ dataSourceId, type },
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_RESOURCE_LIST,
metadata: {
dataSourceId,
type,
resourceIds: resources.map((resource) => resource.id),
count: resources.length
}
}
});
return { resources };
}
});
// not exposed, for UI only
server.route({
method: "GET",
url: "/:dataSourceId/resources-dashboard",
config: {
rateLimit: readLimit
},
schema: {
tags: [ApiDocsTags.SecretScanning],
params: z.object({
dataSourceId: z.string().uuid()
}),
response: {
200: z.object({
resources: SecretScanningResourcesSchema.extend({
lastScannedAt: z.date().nullish(),
lastScanStatus: z.nativeEnum(SecretScanningScanStatus).nullish(),
lastScanStatusMessage: z.string().nullish(),
unresolvedFindings: z.number()
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { dataSourceId } = req.params;
const { resources, projectId } =
await server.services.secretScanningV2.listSecretScanningResourcesWithDetailsByDataSourceId(
{ dataSourceId, type },
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_RESOURCE_LIST,
metadata: {
dataSourceId,
type,
resourceIds: resources.map((resource) => resource.id),
count: resources.length
}
}
});
return { resources };
}
});
server.route({
method: "GET",
url: "/:dataSourceId/scans-dashboard",
config: {
rateLimit: readLimit
},
schema: {
tags: [ApiDocsTags.SecretScanning],
params: z.object({
dataSourceId: z.string().uuid()
}),
response: {
200: z.object({
scans: SecretScanningScansSchema.extend({
unresolvedFindings: z.number(),
resolvedFindings: z.number(),
resourceName: z.string()
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { dataSourceId } = req.params;
const { scans, projectId } =
await server.services.secretScanningV2.listSecretScanningScansWithDetailsByDataSourceId(
{ dataSourceId, type },
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_SCAN_LIST,
metadata: {
dataSourceId,
type,
count: scans.length
}
}
});
return { scans };
}
});
};

View File

@ -0,0 +1,274 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
import { GitLabDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
import {
SecretScanningFindingStatus,
SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import {
SecretScanningDataSourceSchema,
SecretScanningFindingSchema
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas";
import { ApiDocsTags, SecretScanningDataSources, SecretScanningFindings } from "@app/lib/api-docs";
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 SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [
GitHubDataSourceListItemSchema,
GitLabDataSourceListItemSchema
]);
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/data-sources/options",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: "List the available Secret Scanning Data Source Options.",
response: {
200: z.object({
dataSourceOptions: SecretScanningDataSourceOptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => {
const dataSourceOptions = server.services.secretScanningV2.listSecretScanningDataSourceOptions();
return { dataSourceOptions };
}
});
server.route({
method: "GET",
url: "/data-sources",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: "List all the Secret Scanning Data Sources for the specified project.",
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretScanningDataSources.LIST().projectId)
}),
response: {
200: z.object({ dataSources: SecretScanningDataSourceSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const dataSources = await server.services.secretScanningV2.listSecretScanningDataSourcesByProjectId(
{ projectId },
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST,
metadata: {
dataSourceIds: dataSources.map((dataSource) => dataSource.id),
count: dataSources.length
}
}
});
return { dataSources };
}
});
server.route({
method: "GET",
url: "/findings",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: "List all the Secret Scanning Findings for the specified project.",
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretScanningFindings.LIST.projectId)
}),
response: {
200: z.object({ findings: SecretScanningFindingSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const findings = await server.services.secretScanningV2.listSecretScanningFindingsByProjectId(
projectId,
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_FINDING_LIST,
metadata: {
findingIds: findings.map((sync) => sync.id),
count: findings.length
}
}
});
return { findings };
}
});
server.route({
method: "PATCH",
url: "/findings/:findingId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: "Update the resolve status of the specified Secret Scanning Finding.",
params: z.object({
findingId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningFindings.UPDATE.findingId)
}),
body: z.object({
status: z.nativeEnum(SecretScanningFindingStatus).describe(SecretScanningFindings.UPDATE.status),
remarks: z.string().nullish().describe(SecretScanningFindings.UPDATE.remarks)
}),
response: {
200: z.object({ finding: SecretScanningFindingSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { findingId },
body,
permission
} = req;
const { finding, projectId } = await server.services.secretScanningV2.updateSecretScanningFindingById(
{ findingId, ...body },
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_FINDING_UPDATE,
metadata: {
findingId,
...body
}
}
});
return { finding };
}
});
/* DASHBOARD, NOT EXPOSED *********************************************************** */
server.route({
method: "GET",
url: "/data-sources-dashboard",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required")
}),
response: {
200: z.object({
dataSources: z
.intersection(
SecretScanningDataSourceSchema,
z.object({
lastScannedAt: z.date().nullish(),
lastScanStatus: z.nativeEnum(SecretScanningScanStatus).nullish(),
lastScanStatusMessage: z.string().nullish(),
unresolvedFindings: z.number().nullish()
})
)
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const dataSources = await server.services.secretScanningV2.listSecretScanningDataSourcesWithDetailsByProjectId(
{ projectId },
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST,
metadata: {
dataSourceIds: dataSources.map((dataSource) => dataSource.id),
count: dataSources.length
}
}
});
return { dataSources };
}
});
server.route({
method: "GET",
url: "/unresolved-findings-count",
config: {
rateLimit: readLimit
},
schema: {
tags: [ApiDocsTags.SecretScanning],
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretScanningFindings.LIST.projectId)
}),
response: {
200: z.object({ unresolvedFindings: z.number() })
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const unresolvedFindings =
await server.services.secretScanningV2.getSecretScanningUnresolvedFindingsCountByProjectId(
projectId,
permission
);
return { unresolvedFindings };
}
});
};

View File

@ -9,6 +9,14 @@ import {
TSecretRotationV2Raw, TSecretRotationV2Raw,
TUpdateSecretRotationV2DTO TUpdateSecretRotationV2DTO
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import {
TCreateSecretScanningDataSourceDTO,
TDeleteSecretScanningDataSourceDTO,
TTriggerSecretScanningDataSourceDTO,
TUpdateSecretScanningDataSourceDTO,
TUpdateSecretScanningFinding
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types"; import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types"; import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
@ -375,7 +383,18 @@ export enum EventType {
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list", MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start", PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end" PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end",
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
SECRET_SCANNING_DATA_SOURCE_GET = "secret-scanning-data-source-get",
SECRET_SCANNING_DATA_SOURCE_SCAN = "secret-scanning-data-source-scan",
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update"
} }
export const filterableSecretEvents: EventType[] = [ export const filterableSecretEvents: EventType[] = [
@ -2913,6 +2932,75 @@ interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
}; };
} }
interface SecretScanningDataSourceListEvent {
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST;
metadata: {
type?: SecretScanningDataSource;
count: number;
dataSourceIds: string[];
};
}
interface SecretScanningDataSourceGetEvent {
type: EventType.SECRET_SCANNING_DATA_SOURCE_GET;
metadata: {
type: SecretScanningDataSource;
dataSourceId: string;
};
}
interface SecretScanningDataSourceCreateEvent {
type: EventType.SECRET_SCANNING_DATA_SOURCE_CREATE;
metadata: Omit<TCreateSecretScanningDataSourceDTO, "projectId"> & { dataSourceId: string };
}
interface SecretScanningDataSourceUpdateEvent {
type: EventType.SECRET_SCANNING_DATA_SOURCE_UPDATE;
metadata: TUpdateSecretScanningDataSourceDTO;
}
interface SecretScanningDataSourceDeleteEvent {
type: EventType.SECRET_SCANNING_DATA_SOURCE_DELETE;
metadata: TDeleteSecretScanningDataSourceDTO;
}
interface SecretScanningDataSourceScanEvent {
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN;
metadata: TTriggerSecretScanningDataSourceDTO;
}
interface SecretScanningResourceListEvent {
type: EventType.SECRET_SCANNING_RESOURCE_LIST;
metadata: {
type: SecretScanningDataSource;
dataSourceId: string;
resourceIds: string[];
count: number;
};
}
interface SecretScanningScanListEvent {
type: EventType.SECRET_SCANNING_SCAN_LIST;
metadata: {
type: SecretScanningDataSource;
dataSourceId: string;
count: number;
};
}
interface SecretScanningFindingListEvent {
type: EventType.SECRET_SCANNING_FINDING_LIST;
metadata: {
findingIds: string[];
count: number;
};
}
interface SecretScanningFindingUpdateEvent {
type: EventType.SECRET_SCANNING_FINDING_UPDATE;
metadata: TUpdateSecretScanningFinding;
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@ -3179,4 +3267,14 @@ export type Event =
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent | MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
| MicrosoftTeamsWorkflowIntegrationGetEvent | MicrosoftTeamsWorkflowIntegrationGetEvent
| MicrosoftTeamsWorkflowIntegrationListEvent | MicrosoftTeamsWorkflowIntegrationListEvent
| MicrosoftTeamsWorkflowIntegrationUpdateEvent; | MicrosoftTeamsWorkflowIntegrationUpdateEvent
| SecretScanningDataSourceListEvent
| SecretScanningDataSourceGetEvent
| SecretScanningDataSourceCreateEvent
| SecretScanningDataSourceUpdateEvent
| SecretScanningDataSourceDeleteEvent
| SecretScanningDataSourceScanEvent
| SecretScanningResourceListEvent
| SecretScanningScanListEvent
| SecretScanningFindingListEvent
| SecretScanningFindingUpdateEvent;

View File

@ -54,7 +54,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
projectTemplates: false, projectTemplates: false,
kmip: false, kmip: false,
gateway: false, gateway: false,
sshHostGroups: false sshHostGroups: false,
secretScanning: true
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@ -72,6 +72,7 @@ export type TFeatureSet = {
kmip: false; kmip: false;
gateway: false; gateway: false;
sshHostGroups: false; sshHostGroups: false;
secretScanning: false;
}; };
export type TOrgPlansTableDTO = { export type TOrgPlansTableDTO = {

View File

@ -12,6 +12,8 @@ import {
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions, ProjectPermissionSecretRotationActions,
ProjectPermissionSecretScanningDataSourceActions,
ProjectPermissionSecretScanningFindingActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
ProjectPermissionSet, ProjectPermissionSet,
ProjectPermissionSshHostActions, ProjectPermissionSshHostActions,
@ -198,6 +200,24 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SecretRotation ProjectPermissionSub.SecretRotation
); );
can(
[
ProjectPermissionSecretScanningDataSourceActions.Create,
ProjectPermissionSecretScanningDataSourceActions.Edit,
ProjectPermissionSecretScanningDataSourceActions.Delete,
ProjectPermissionSecretScanningDataSourceActions.Read,
ProjectPermissionSecretScanningDataSourceActions.TriggerScans,
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
ProjectPermissionSecretScanningDataSourceActions.ReadResources
],
ProjectPermissionSub.SecretScanningDataSources
);
can(
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Resolve],
ProjectPermissionSub.SecretScanningFindings
);
return rules; return rules;
}; };
@ -378,6 +398,24 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.SecretSyncs ProjectPermissionSub.SecretSyncs
); );
can(
[
ProjectPermissionSecretScanningDataSourceActions.Create,
ProjectPermissionSecretScanningDataSourceActions.Edit,
ProjectPermissionSecretScanningDataSourceActions.Delete,
ProjectPermissionSecretScanningDataSourceActions.Read,
ProjectPermissionSecretScanningDataSourceActions.TriggerScans,
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
ProjectPermissionSecretScanningDataSourceActions.ReadResources
],
ProjectPermissionSub.SecretScanningDataSources
);
can(
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Resolve],
ProjectPermissionSub.SecretScanningFindings
);
return rules; return rules;
}; };
@ -413,6 +451,17 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
can(
[
ProjectPermissionSecretScanningDataSourceActions.Read,
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
ProjectPermissionSecretScanningDataSourceActions.ReadResources
],
ProjectPermissionSub.SecretScanningDataSources
);
can([ProjectPermissionSecretScanningFindingActions.Read], ProjectPermissionSub.SecretScanningFindings);
return rules; return rules;
}; };

View File

@ -123,6 +123,21 @@ export enum ProjectPermissionKmipActions {
GenerateClientCertificates = "generate-client-certificates" GenerateClientCertificates = "generate-client-certificates"
} }
export enum ProjectPermissionSecretScanningDataSourceActions {
Read = "read-data-sources",
Create = "create-data-sources",
Edit = "edit-data-sources",
Delete = "delete-data-sources",
TriggerScans = "trigger-data-source-scans",
ReadScans = "read-data-source-scans",
ReadResources = "read-data-source-resources"
}
export enum ProjectPermissionSecretScanningFindingActions {
Read = "read-findings",
Resolve = "resolve-findings"
}
export enum ProjectPermissionSub { export enum ProjectPermissionSub {
Role = "role", Role = "role",
Member = "member", Member = "member",
@ -158,7 +173,9 @@ export enum ProjectPermissionSub {
Kms = "kms", Kms = "kms",
Cmek = "cmek", Cmek = "cmek",
SecretSyncs = "secret-syncs", SecretSyncs = "secret-syncs",
Kmip = "kmip" Kmip = "kmip",
SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@ -281,7 +298,9 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
| [ProjectPermissionSecretScanningDataSourceActions, ProjectPermissionSub.SecretScanningDataSources]
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings];
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
@ -602,6 +621,20 @@ const GeneralPermissionSchema = [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}),
z.object({
subject: z
.literal(ProjectPermissionSub.SecretScanningDataSources)
.describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningDataSourceActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretScanningFindings).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningFindingActions).describe(
"Describe what action an entity can take."
)
}) })
]; ];

View File

@ -0,0 +1,13 @@
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TSecretScanningDataSourceListItem } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = {
name: "GitHub",
type: SecretScanningDataSource.GitHub,
connection: AppConnection.GitHub
};
// TODO: figure out why I need to prefix /api
export const SECRET_SCANNING_WEBHOOK_PATH = "/secret-scanning/webhooks";
// https://bubblegloop-swamp.ngrok.dev/secret-scanning/webhooks/gitlab

View File

@ -0,0 +1,188 @@
import { join } from "path";
import { ProbotOctokit } from "probot";
import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import {
SecretScanningFindingSeverity,
SecretScanningResource
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { cloneRepository, titleCaseToCamelCase } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
import {
TSecretScanningFactoryGetDiffScanFindingsPayload,
TSecretScanningFactoryGetDiffScanResourcePayload,
TSecretScanningFactoryGetFullScanPath,
TSecretScanningFactoryInitialize,
TSecretScanningFactoryListRawResources,
TSecretScanningFactoryPostInitialization
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { getConfig } from "@app/lib/config/env";
import { listGitHubRadarRepositories, TGitHubRadarConnection } from "@app/services/app-connection/github-radar";
import { TGitHubDataSourceWithConnection, TQueueGitHubResourceDiffScan } from "./github-secret-scanning-types";
export const GitHubSecretScanningFactory = () => {
const initialize: TSecretScanningFactoryInitialize<TGitHubRadarConnection> = async ({ connection }, callback) => {
const externalId = connection.credentials.installationId;
// TODO: check if existing data source using installationId
return callback({
externalId
});
};
const postInitialization: TSecretScanningFactoryPostInitialization<TGitHubRadarConnection> = async () => {
// no post-initialization required
};
const listRawResources: TSecretScanningFactoryListRawResources<TGitHubDataSourceWithConnection> = async (
dataSource
) => {
const {
connection,
config: { includeRepos }
} = dataSource;
const repos = await listGitHubRadarRepositories(connection);
const filteredRepos: typeof repos = [];
if (includeRepos.includes("*")) {
filteredRepos.push(...repos);
} else {
filteredRepos.push(...repos.filter((repo) => includeRepos.includes(repo.full_name)));
}
return filteredRepos.map(({ id, full_name }) => ({
name: full_name,
externalId: id.toString(),
type: SecretScanningResource.Repository
}));
};
const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TGitHubDataSourceWithConnection> = async ({
dataSource,
resourceName,
tempFolder
}) => {
const appCfg = getConfig();
const {
connection: {
credentials: { installationId }
}
} = dataSource;
const octokit = new ProbotOctokit({
auth: {
appId: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_ID,
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY,
installationId
}
});
const {
data: { token }
} = await octokit.apps.createInstallationAccessToken({
installation_id: Number(installationId)
});
const repoPath = join(tempFolder, "repo.git");
await cloneRepository({
cloneUrl: `https://x-access-token:${token}@github.com/${resourceName}.git`,
repoPath
});
return repoPath;
};
const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<
TQueueGitHubResourceDiffScan["payload"]
> = ({ repository }) => {
return {
name: repository.full_name,
externalId: repository.id.toString(),
type: SecretScanningResource.Repository
};
};
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
TGitHubDataSourceWithConnection,
TQueueGitHubResourceDiffScan["payload"]
> = async ({ dataSource, payload, resourceName }) => {
const appCfg = getConfig();
const {
connection: {
credentials: { installationId }
}
} = dataSource;
const octokit = new ProbotOctokit({
auth: {
appId: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_ID,
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY,
installationId
}
});
const { commits, repository } = payload;
const [owner, repo] = repository.full_name.split("/");
const allFindings: SecretMatch[] = [];
for (const commit of commits) {
for (const filepath of [...commit.added, ...commit.modified]) {
// eslint-disable-next-line
const fileContentsResponse = await octokit.repos.getContent({
owner,
repo,
path: filepath
});
const { data } = fileContentsResponse;
const fileContent = Buffer.from((data as { content: string }).content, "base64").toString();
// eslint-disable-next-line
const findings = await scanContentAndGetFindings(`\n${fileContent}`); // extra line to count lines correctly
allFindings.push(
...findings.map((finding) => ({
...finding,
File: filepath,
Commit: commit.id,
Author: commit.author.name,
Email: commit.author.email ?? "",
Message: commit.message,
Fingerprint: `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`,
Date: commit.timestamp,
Link: `https://github.com/${resourceName}/blob/${commit.id}/${filepath}#L${finding.StartLine}`
}))
);
}
}
return allFindings.map(
({
// discard match and secret as we don't want to store
Match,
Secret,
...finding
}) => ({
details: titleCaseToCamelCase(finding),
fingerprint: finding.Fingerprint,
severity: SecretScanningFindingSeverity.High,
rule: finding.RuleID
})
);
};
return {
initialize,
postInitialization,
listRawResources,
getFullScanPath,
getDiffScanResourcePayload,
getDiffScanFindingsPayload
};
};

View File

@ -0,0 +1,72 @@
import { z } from "zod";
import {
SecretScanningDataSource,
SecretScanningResource
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import {
BaseCreateSecretScanningDataSourceSchema,
BaseSecretScanningDataSourceSchema,
BaseSecretScanningFindingSchema,
BaseUpdateSecretScanningDataSourceSchema,
GitRepositoryScanFindingDetailsSchema
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const GitHubDataSourceConfigSchema = z.object({
includeRepos: z.array(z.string()).nonempty("One or more repositories required").default(["*"])
});
export const GitHubDataSourceSchema = BaseSecretScanningDataSourceSchema({
type: SecretScanningDataSource.GitHub,
isConnectionRequired: true
})
.extend({
config: GitHubDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "GitHub"
})
);
export const CreateGitHubDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({
type: SecretScanningDataSource.GitHub,
isConnectionRequired: true
})
.extend({
config: GitHubDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "GitHub"
})
);
export const UpdateGitHubDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema(SecretScanningDataSource.GitHub)
.extend({
config: GitHubDataSourceConfigSchema.optional()
})
.describe(
JSON.stringify({
title: "GitHub"
})
);
export const GitHubDataSourceListItemSchema = z
.object({
name: z.literal("GitHub"),
connection: z.literal(AppConnection.GitHub),
type: z.literal(SecretScanningDataSource.GitHub)
})
.describe(
JSON.stringify({
title: "GitHub"
})
);
export const GitHubFindingSchema = BaseSecretScanningFindingSchema.extend({
resourceType: z.literal(SecretScanningResource.Repository),
dataSourceType: z.literal(SecretScanningDataSource.GitHub),
details: GitRepositoryScanFindingDetailsSchema
});

View File

@ -0,0 +1,43 @@
import { PushEvent } from "@octokit/webhooks-types";
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
import { logger } from "@app/lib/logger";
export const githubSecretScanningService = (
secretScanningV2DAL: TSecretScanningV2DALFactory,
secretScanningV2Queue: Pick<TSecretScanningV2QueueServiceFactory, "queueResourceDiffScan">
) => {
const handlePushEvent = async (payload: PushEvent) => {
const { commits, repository, installation } = payload;
if (!commits || !repository || !installation) {
logger.warn(
`secretScanningV2PushEvent: GitHub - Insufficient data [commits=${commits?.length ?? 0}] [repository=${repository.name}] [installationId=${installation?.id}]`
);
return;
}
const dataSource = await secretScanningV2DAL.dataSources.findOne({
externalId: String(installation.id)
});
if (!dataSource) {
logger.error(
`secretScanningV2PushEvent: GitHub - Could not find data source [installationId=${installation.id}]`
);
return;
}
await secretScanningV2Queue.queueResourceDiffScan({
dataSourceType: SecretScanningDataSource.GitHub,
payload,
dataSourceId: dataSource.id
});
};
return {
handlePushEvent
};
};

View File

@ -0,0 +1,30 @@
import { PushEvent } from "@octokit/webhooks-types";
import { z } from "zod";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TGitHubRadarConnection } from "@app/services/app-connection/github-radar";
import {
CreateGitHubDataSourceSchema,
GitHubDataSourceListItemSchema,
GitHubDataSourceSchema,
GitHubFindingSchema
} from "./github-secret-scanning-schemas";
export type TGitHubDataSource = z.infer<typeof GitHubDataSourceSchema>;
export type TGitHubDataSourceInput = z.infer<typeof CreateGitHubDataSourceSchema>;
export type TGitHubDataSourceListItem = z.infer<typeof GitHubDataSourceListItemSchema>;
export type TGitHubFinding = z.infer<typeof GitHubFindingSchema>;
export type TGitHubDataSourceWithConnection = TGitHubDataSource & {
connection: TGitHubRadarConnection;
};
export type TQueueGitHubResourceDiffScan = {
dataSourceType: SecretScanningDataSource.GitHub;
payload: PushEvent;
dataSourceId: string;
};

View File

@ -0,0 +1,3 @@
export * from "./github-secret-scanning-constants";
export * from "./github-secret-scanning-schemas";
export * from "./github-secret-scanning-types";

View File

@ -0,0 +1,439 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
SecretScanningResourcesSchema,
SecretScanningScansSchema,
TableName,
TSecretScanningDataSources
} from "@app/db/schemas";
import { SecretScanningFindingStatus } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { DatabaseError } from "@app/lib/errors";
import {
buildFindFilter,
ormify,
prependTableNameToFindFilter,
selectAllTableCols,
sqlNestRelationships,
TFindOpt
} from "@app/lib/knex";
export type TSecretScanningV2DALFactory = ReturnType<typeof secretScanningV2DALFactory>;
type TSecretScanningDataSourceFindFilter = Parameters<typeof buildFindFilter<TSecretScanningDataSources>>[0];
type TSecretScanningDataSourceFindOptions = TFindOpt<TSecretScanningDataSources, true, "name">;
const baseSecretScanningDataSourceQuery = ({
filter = {},
db,
tx
}: {
db: TDbClient;
filter?: TSecretScanningDataSourceFindFilter;
options?: TSecretScanningDataSourceFindOptions;
tx?: Knex;
}) => {
const query = (tx || db.replicaNode())(TableName.SecretScanningDataSource)
.join(
TableName.AppConnection,
`${TableName.SecretScanningDataSource}.connectionId`,
`${TableName.AppConnection}.id`
)
.select(selectAllTableCols(TableName.SecretScanningDataSource))
.select(
// entire connection
db.ref("name").withSchema(TableName.AppConnection).as("connectionName"),
db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"),
db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"),
db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"),
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db
.ref("isPlatformManagedCredentials")
.withSchema(TableName.AppConnection)
.as("connectionIsPlatformManagedCredentials")
);
if (filter) {
/* eslint-disable @typescript-eslint/no-misused-promises */
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretScanningDataSource, filter)));
}
return query;
};
const expandSecretScanningDataSource = <
T extends Awaited<ReturnType<typeof baseSecretScanningDataSourceQuery>>[number]
>(
dataSource: T
) => {
const {
connectionApp,
connectionName,
connectionId,
connectionOrgId,
connectionEncryptedCredentials,
connectionMethod,
connectionDescription,
connectionCreatedAt,
connectionUpdatedAt,
connectionVersion,
connectionIsPlatformManagedCredentials,
...el
} = dataSource;
return {
...el,
connectionId,
connection: connectionId
? {
app: connectionApp,
id: connectionId,
name: connectionName,
orgId: connectionOrgId,
encryptedCredentials: connectionEncryptedCredentials,
method: connectionMethod,
description: connectionDescription,
createdAt: connectionCreatedAt,
updatedAt: connectionUpdatedAt,
version: connectionVersion,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
}
: undefined
};
};
export const secretScanningV2DALFactory = (db: TDbClient) => {
const dataSourceOrm = ormify(db, TableName.SecretScanningDataSource);
const resourceOrm = ormify(db, TableName.SecretScanningResource);
const scanOrm = ormify(db, TableName.SecretScanningScan);
const findingOrm = ormify(db, TableName.SecretScanningFinding);
const findDataSource = async (filter: Parameters<(typeof dataSourceOrm)["find"]>[0], tx?: Knex) => {
try {
const dataSources = await baseSecretScanningDataSourceQuery({ filter, db, tx });
if (!dataSources.length) return [];
return dataSources.map(expandSecretScanningDataSource);
} catch (error) {
throw new DatabaseError({ error, name: "Find - Secret Scanning Data Source" });
}
};
const findDataSourceById = async (id: string, tx?: Knex) => {
try {
const dataSource = await baseSecretScanningDataSourceQuery({ filter: { id }, db, tx }).first();
if (dataSource) return expandSecretScanningDataSource(dataSource);
} catch (error) {
throw new DatabaseError({ error, name: "Find By ID - Secret Scanning Data Source" });
}
};
const createDataSource = async (data: Parameters<(typeof dataSourceOrm)["create"]>[0], tx?: Knex) => {
const source = await dataSourceOrm.create(data, tx);
const dataSource = (await baseSecretScanningDataSourceQuery({
filter: { id: source.id },
db,
tx
}).first())!;
return expandSecretScanningDataSource(dataSource);
};
const updateDataSourceById = async (
dataSourceId: string,
data: Parameters<(typeof dataSourceOrm)["updateById"]>[1],
tx?: Knex
) => {
const source = await dataSourceOrm.updateById(dataSourceId, data, tx);
const dataSource = (await baseSecretScanningDataSourceQuery({
filter: { id: source.id },
db,
tx
}).first())!;
return expandSecretScanningDataSource(dataSource);
};
const deleteDataSourceById = async (dataSourceId: string, tx?: Knex) => {
const secretRotation = (await baseSecretScanningDataSourceQuery({
filter: { id: dataSourceId },
db,
tx
}).first())!;
await dataSourceOrm.deleteById(dataSourceId, tx);
return expandSecretScanningDataSource(secretRotation);
};
const findOneDataSource = async (filter: Parameters<(typeof dataSourceOrm)["findOne"]>[0], tx?: Knex) => {
try {
const secretRotation = await baseSecretScanningDataSourceQuery({ filter, db, tx }).first();
if (secretRotation) {
return expandSecretScanningDataSource(secretRotation);
}
} catch (error) {
throw new DatabaseError({ error, name: "Find One - Secret Rotation V2" });
}
};
const findDataSourceWithDetails = async (filter: Parameters<(typeof dataSourceOrm)["find"]>[0], tx?: Knex) => {
try {
// TODO (scott): this query will probably need to be optimized
const dataSources = await baseSecretScanningDataSourceQuery({ filter, db, tx })
.leftJoin(
TableName.SecretScanningResource,
`${TableName.SecretScanningResource}.dataSourceId`,
`${TableName.SecretScanningDataSource}.id`
)
.leftJoin(
TableName.SecretScanningScan,
`${TableName.SecretScanningScan}.resourceId`,
`${TableName.SecretScanningResource}.id`
)
.leftJoin(
TableName.SecretScanningFinding,
`${TableName.SecretScanningFinding}.scanId`,
`${TableName.SecretScanningScan}.id`
)
.where((qb) => {
void qb
.where(`${TableName.SecretScanningFinding}.status`, SecretScanningFindingStatus.Unresolved)
.orWhereNull(`${TableName.SecretScanningFinding}.status`);
})
.select(
db.ref("id").withSchema(TableName.SecretScanningScan).as("scanId"),
db.ref("status").withSchema(TableName.SecretScanningScan).as("scanStatus"),
db.ref("statusMessage").withSchema(TableName.SecretScanningScan).as("scanStatusMessage"),
db.ref("createdAt").withSchema(TableName.SecretScanningScan).as("scanCreatedAt"),
db.ref("status").withSchema(TableName.SecretScanningFinding).as("findingStatus"),
db.ref("id").withSchema(TableName.SecretScanningFinding).as("findingId")
);
if (!dataSources.length) return [];
const results = sqlNestRelationships({
data: dataSources,
key: "id",
parentMapper: (dataSource) => expandSecretScanningDataSource(dataSource),
childrenMapper: [
{
key: "scanId",
label: "scans" as const,
mapper: ({ scanId, scanCreatedAt, scanStatus, scanStatusMessage }) => ({
id: scanId,
createdAt: scanCreatedAt,
status: scanStatus,
statusMessage: scanStatusMessage
})
},
{
key: "findingId",
label: "findings" as const,
mapper: ({ findingId }) => ({
id: findingId
})
}
]
});
return results.map(({ scans, findings, ...dataSource }) => {
const lastScan =
scans && scans.length
? scans.reduce((latest, current) => {
return new Date(current.createdAt) > new Date(latest.createdAt) ? current : latest;
})
: null;
return {
...dataSource,
lastScanStatus: lastScan?.status ?? null,
lastScanStatusMessage: lastScan?.statusMessage ?? null,
lastScannedAt: lastScan?.createdAt ?? null,
unresolvedFindings: scans.length ? findings.length : null
};
});
} catch (error) {
throw new DatabaseError({ error, name: "Find Data Source with Details - Secret Scanning V2" });
}
};
const findResourcesWithDetails = async (filter: Parameters<(typeof resourceOrm)["find"]>[0], tx?: Knex) => {
try {
// TODO (scott): this query will probably need to be optimized
const resources = await (tx || db.replicaNode())(TableName.SecretScanningResource)
.where((qb) => {
if (filter)
void qb.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretScanningResource, filter)));
})
.leftJoin(
TableName.SecretScanningScan,
`${TableName.SecretScanningScan}.resourceId`,
`${TableName.SecretScanningResource}.id`
)
.leftJoin(
TableName.SecretScanningFinding,
`${TableName.SecretScanningFinding}.scanId`,
`${TableName.SecretScanningScan}.id`
)
.where((qb) => {
void qb
.where(`${TableName.SecretScanningFinding}.status`, SecretScanningFindingStatus.Unresolved)
.orWhereNull(`${TableName.SecretScanningFinding}.status`);
})
.select(selectAllTableCols(TableName.SecretScanningResource))
.select(
db.ref("id").withSchema(TableName.SecretScanningScan).as("scanId"),
db.ref("status").withSchema(TableName.SecretScanningScan).as("scanStatus"),
db.ref("type").withSchema(TableName.SecretScanningScan).as("scanType"),
db.ref("statusMessage").withSchema(TableName.SecretScanningScan).as("scanStatusMessage"),
db.ref("createdAt").withSchema(TableName.SecretScanningScan).as("scanCreatedAt"),
db.ref("status").withSchema(TableName.SecretScanningFinding).as("findingStatus"),
db.ref("id").withSchema(TableName.SecretScanningFinding).as("findingId")
);
if (!resources.length) return [];
const results = sqlNestRelationships({
data: resources,
key: "id",
parentMapper: (resource) => SecretScanningResourcesSchema.parse(resource),
childrenMapper: [
{
key: "scanId",
label: "scans" as const,
mapper: ({ scanId, scanCreatedAt, scanStatus, scanStatusMessage, scanType }) => ({
id: scanId,
type: scanType,
createdAt: scanCreatedAt,
status: scanStatus,
statusMessage: scanStatusMessage
})
},
{
key: "findingId",
label: "findings" as const,
mapper: ({ findingId }) => ({
id: findingId
})
}
]
});
return results.map(({ scans, findings, ...resource }) => {
const lastScan =
scans && scans.length
? scans.reduce((latest, current) => {
return new Date(current.createdAt) > new Date(latest.createdAt) ? current : latest;
})
: null;
return {
...resource,
lastScanStatus: lastScan?.status ?? null,
lastScanStatusMessage: lastScan?.statusMessage ?? null,
lastScannedAt: lastScan?.createdAt ?? null,
unresolvedFindings: findings?.length ?? 0
};
});
} catch (error) {
throw new DatabaseError({ error, name: "Find Resource with Details - Secret Scanning V2" });
}
};
const findScansWithDetailsByDataSourceId = async (dataSourceId: string, tx?: Knex) => {
try {
// TODO (scott): this query will probably need to be optimized
const scans = await (tx || db.replicaNode())(TableName.SecretScanningScan)
.leftJoin(
TableName.SecretScanningResource,
`${TableName.SecretScanningResource}.id`,
`${TableName.SecretScanningScan}.resourceId`
)
.where(`${TableName.SecretScanningResource}.dataSourceId`, dataSourceId)
.leftJoin(
TableName.SecretScanningFinding,
`${TableName.SecretScanningFinding}.scanId`,
`${TableName.SecretScanningScan}.id`
)
.select(selectAllTableCols(TableName.SecretScanningScan))
.select(
db.ref("status").withSchema(TableName.SecretScanningFinding).as("findingStatus"),
db.ref("id").withSchema(TableName.SecretScanningFinding).as("findingId"),
db.ref("name").withSchema(TableName.SecretScanningResource).as("resourceName")
);
if (!scans.length) return [];
const results = sqlNestRelationships({
data: scans,
key: "id",
parentMapper: (scan) => SecretScanningScansSchema.parse(scan),
childrenMapper: [
{
key: "findingId",
label: "findings" as const,
mapper: ({ findingId, findingStatus }) => ({
id: findingId,
status: findingStatus
})
},
{
key: "resourceId",
label: "resources" as const,
mapper: ({ resourceName }) => ({
name: resourceName
})
}
]
});
return results.map(({ findings, resources, ...scan }) => {
return {
...scan,
unresolvedFindings:
findings?.filter((finding) => finding.status === SecretScanningFindingStatus.Unresolved).length ?? 0,
resolvedFindings:
findings?.filter((finding) => finding.status === SecretScanningFindingStatus.Resolved).length ?? 0,
resourceName: resources[0].name
};
});
} catch (error) {
throw new DatabaseError({ error, name: "Find Scan with Details - Secret Scanning V2" });
}
};
return {
dataSources: {
...dataSourceOrm,
find: findDataSource,
findById: findDataSourceById,
findOne: findOneDataSource,
create: createDataSource,
updateById: updateDataSourceById,
deleteById: deleteDataSourceById,
findWithDetails: findDataSourceWithDetails
},
resources: {
...resourceOrm,
findWithDetails: findResourcesWithDetails
},
scans: {
...scanOrm,
findWithDetailsByDataSourceId: findScansWithDetailsByDataSourceId
},
findings: findingOrm
};
};

View File

@ -0,0 +1,33 @@
export enum SecretScanningDataSource {
GitHub = "github"
}
export enum SecretScanningScanStatus {
Completed = "completed",
Failed = "failed",
Queued = "queued",
Scanning = "scanning"
}
export enum SecretScanningScanType {
FullScan = "full-scan",
DiffScan = "diff-scan"
}
export enum SecretScanningFindingStatus {
Resolved = "resolved",
Unresolved = "unresolved",
FalsePositive = "false-positive",
Ignore = "ignore"
}
export enum SecretScanningResource {
Repository = "repository",
Project = "project"
}
export enum SecretScanningFindingSeverity {
High = "high",
Medium = "medium",
Low = "low"
}

View File

@ -0,0 +1,19 @@
import { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory";
import { SecretScanningDataSource } from "./secret-scanning-v2-enums";
import {
TQueueSecretScanningResourceDiffScan,
TSecretScanningDataSourceCredentials,
TSecretScanningDataSourceWithConnection,
TSecretScanningFactory
} from "./secret-scanning-v2-types";
type TSecretScanningFactoryImplementation = TSecretScanningFactory<
TSecretScanningDataSourceWithConnection,
TSecretScanningDataSourceCredentials,
TQueueSecretScanningResourceDiffScan["payload"]
>;
export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = {
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation
};

View File

@ -0,0 +1,102 @@
import { AxiosError } from "axios";
import { exec } from "child_process";
import { readFindingsFile } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/github";
import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums";
import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListItem } from "./secret-scanning-v2-types";
const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record<SecretScanningDataSource, TSecretScanningDataSourceListItem> = {
[SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
};
export const listSecretScanningDataSourceOptions = () => {
return Object.values(SECRET_SCANNING_SOURCE_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
};
export const cloneRepository = async ({ cloneUrl, repoPath }: TCloneRepository): Promise<void> => {
const command = `git clone ${cloneUrl} ${repoPath} --bare`;
return new Promise((resolve, reject) => {
exec(command, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
};
export function scanDirectory(inputPath: string, outputPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `cd ${inputPath} && infisical scan --exit-code=77 -r "${outputPath}"`;
exec(command, (error) => {
if (error && error.code !== 77) {
reject(error);
} else {
resolve();
}
});
});
}
export const titleCaseToCamelCase = (obj: unknown): unknown => {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item: object) => titleCaseToCamelCase(item));
}
const result: Record<string, unknown> = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const camelKey = key.charAt(0).toLowerCase() + key.slice(1);
result[camelKey] = titleCaseToCamelCase((obj as Record<string, unknown>)[key]);
}
}
return result;
};
export const scanGitRepositoryAndGetFindings = async (scanPath: string, findingsPath: string): TGetFindingsPayload => {
await scanDirectory(scanPath, findingsPath);
const findingsData = JSON.parse(await readFindingsFile(findingsPath)) as SecretMatch[];
return findingsData.map(
({
// discard match and secret as we don't want to store
Match,
Secret,
...finding
}) => ({
details: titleCaseToCamelCase(finding),
fingerprint: finding.Fingerprint,
severity: SecretScanningFindingSeverity.High,
rule: finding.RuleID
})
);
};
const MAX_MESSAGE_LENGTH = 1024;
export const parseScanErrorMessage = (err: unknown): string => {
let errorMessage: string;
if (err instanceof AxiosError) {
errorMessage = err?.response?.data
? JSON.stringify(err?.response?.data)
: (err?.message ?? "An unknown error occurred.");
} else {
errorMessage = (err as Error)?.message || "An unknown error occurred.";
}
return errorMessage.length <= MAX_MESSAGE_LENGTH
? errorMessage
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
};

View File

@ -0,0 +1,14 @@
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record<SecretScanningDataSource, string> = {
[SecretScanningDataSource.GitHub]: "GitHub"
};
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<SecretScanningDataSource, AppConnection> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar
};
export const AUTO_SYNC_DESCRIPTION_HELPER: Record<SecretScanningDataSource, { verb: string; noun: string }> = {
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" }
};

View File

@ -0,0 +1,401 @@
import { join } from "path";
import {
createTempFolder,
deleteTempFolder
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import {
parseScanErrorMessage,
scanGitRepositoryAndGetFindings
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
import {
SecretScanningDataSource,
SecretScanningFindingStatus,
SecretScanningResource,
SecretScanningScanStatus,
SecretScanningScanType
} from "./secret-scanning-v2-enums";
import { SECRET_SCANNING_FACTORY_MAP } from "./secret-scanning-v2-factory";
import {
TFindingsPayload,
TQueueSecretScanningDataSourceFullScan,
TQueueSecretScanningResourceDiffScan,
TSecretScanningDataSourceWithConnection
} from "./secret-scanning-v2-types";
type TSecretRotationV2QueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
secretScanningV2DAL: TSecretScanningV2DALFactory;
smtpService: Pick<TSmtpService, "sendMail">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
export const secretScanningV2QueueServiceFactory = async ({
queueService,
secretScanningV2DAL,
projectMembershipDAL,
projectDAL,
smtpService,
kmsService
}: TSecretRotationV2QueueServiceFactoryDep) => {
const queueDataSourceFullScan = async (
dataSource: TSecretScanningDataSourceWithConnection,
resourceExternalId?: string
) => {
try {
const { type } = dataSource;
const factory = SECRET_SCANNING_FACTORY_MAP[type]();
const rawResources = await factory.listRawResources(dataSource);
let filteredRawResources = rawResources;
// TODO: should add indivial resource fetch to factory
if (resourceExternalId) {
filteredRawResources = rawResources.filter((resource) => resource.externalId === resourceExternalId);
}
if (!filteredRawResources.length) {
throw new BadRequestError({
message: `${resourceExternalId ? `Resource with "ID" ${resourceExternalId} could not be found.` : "Data source has no resources to scan"}. Ensure your data source config is correct and not filtering out scanning resources.`
});
}
await secretScanningV2DAL.resources.transaction(async (tx) => {
const resources = await secretScanningV2DAL.resources.upsert(
filteredRawResources.map((rawResource) => ({
...rawResource,
dataSourceId: dataSource.id
})),
["externalId", "dataSourceId"],
tx
);
const scans = await secretScanningV2DAL.scans.insertMany(
resources.map((resource) => ({
resourceId: resource.id,
type: SecretScanningScanType.FullScan
})),
tx
);
for (const scan of scans) {
// eslint-disable-next-line no-await-in-loop
await queueService.queuePg(QueueJobs.SecretScanningV2FullScan, {
scanId: scan.id,
resourceId: scan.resourceId,
dataSourceId: dataSource.id
});
}
});
} catch (error) {
logger.error(error, `Failed to queue full-scan for data source with ID "${dataSource.id}"`);
if (error instanceof BadRequestError) throw error;
throw new InternalServerError({ message: `Failed to queue scan: ${(error as Error).message}` });
}
};
const queueResourceDiffScan = async (payload: TQueueSecretScanningResourceDiffScan) =>
queueService.queuePg(QueueJobs.SecretScanningV2DiffScan, payload);
await queueService.startPg<QueueName.SecretScanningV2>(
QueueJobs.SecretScanningV2FullScan,
async ([job]) => {
const { scanId, resourceId, dataSourceId } = job.data as TQueueSecretScanningDataSourceFullScan;
const { retryCount, retryLimit } = job;
const logDetails = `[scanId=${scanId}] [resourceId=${resourceId}] [dataSourceId=${dataSourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
const tempFolder = await createTempFolder();
try {
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Scanning
}
);
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource) throw new Error(`Data source with ID "${dataSourceId}" not found`);
const resource = await secretScanningV2DAL.resources.findById(resourceId);
if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`);
let connection: TAppConnection | null = null;
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
const findingsPath = join(tempFolder, "findings.json");
const scanPath = await factory.getFullScanPath({
dataSource: {
...dataSource,
connection
} as TSecretScanningDataSourceWithConnection,
resourceName: resource.name,
tempFolder
});
let findingsPayload: TFindingsPayload;
switch (resource.type) {
case SecretScanningResource.Repository:
case SecretScanningResource.Project:
findingsPayload = await scanGitRepositoryAndGetFindings(scanPath, findingsPath);
break;
default:
throw new Error("Unhandled resource type");
}
await secretScanningV2DAL.findings.transaction(async (tx) => {
await secretScanningV2DAL.findings.upsert(
findingsPayload.map((findings) => ({
...findings,
projectId: dataSource.projectId,
dataSourceName: dataSource.name,
dataSourceType: dataSource.type,
resourceName: resource.name,
resourceType: resource.type,
scanId,
status: SecretScanningFindingStatus.Unresolved
})),
["projectId", "fingerprint"],
tx,
["resourceName", "dataSourceName", "status"]
);
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Completed
}
);
});
// TODO: send notification
logger.info(`secretScanningV2Queue: Full Scan Complete ${logDetails}`);
} catch (error) {
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Failed,
statusMessage: parseScanErrorMessage(error)
}
);
// TODO: send error notification
logger.error(error, `secretScanningV2Queue: Full Scan Failed ${logDetails}`);
throw error;
} finally {
await deleteTempFolder(tempFolder);
}
},
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
await queueService.startPg<QueueName.SecretScanningV2>(
QueueJobs.SecretScanningV2DiffScan,
async ([job]) => {
const { payload, dataSourceId } = job.data as TQueueSecretScanningResourceDiffScan;
const { retryCount, retryLimit } = job;
let scanId: string | undefined;
let logDetails = `[dataSourceId=${dataSourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
try {
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource) throw new Error(`Data source with ID "${dataSourceId}" not found`);
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
const resourcePayload = factory.getDiffScanResourcePayload(payload);
const { resourceId, resourceName, resourceType } = await secretScanningV2DAL.resources.transaction(
async (tx) => {
const [resource] = await secretScanningV2DAL.resources.upsert(
[
{
...resourcePayload,
dataSourceId
}
],
["externalId", "dataSourceId"],
tx
);
const scan = await secretScanningV2DAL.scans.create(
{
resourceId: resource.id,
type: SecretScanningScanType.DiffScan,
status: SecretScanningScanStatus.Scanning
},
tx
);
scanId = scan.id;
return {
resourceId: resource.id,
resourceName: resource.name,
resourceType: resource.type
};
}
);
logDetails += ` [scanId=${scanId}] [resourceId=${resourceId}]`;
let connection: TAppConnection | null = null;
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
const findingsPayload = await factory.getDiffScanFindingsPayload({
dataSource: {
...dataSource,
connection
} as TSecretScanningDataSourceWithConnection,
resourceName,
payload
});
logger.warn(findingsPayload, "findingsPayload");
await secretScanningV2DAL.findings.transaction(async (tx) => {
await secretScanningV2DAL.findings.upsert(
findingsPayload.map((findings) => ({
...findings,
projectId: dataSource.projectId,
dataSourceName: dataSource.name,
dataSourceType: dataSource.type,
resourceName,
resourceType,
scanId,
status: SecretScanningFindingStatus.Unresolved
})),
["projectId", "fingerprint"],
tx,
["resourceName", "dataSourceName", "status"]
);
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Completed
}
);
});
// TODO: send notification
logger.info(`secretScanningV2Queue: Diff Scan Complete ${logDetails}`);
} catch (error) {
if (scanId)
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Failed,
statusMessage: parseScanErrorMessage(error)
}
);
// TODO: send error notification
logger.error(error, `secretScanningV2Queue: Diff Scan Failed ${logDetails}`);
throw error;
}
},
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
// await queueService.startPg<QueueName.SecretRotationV2>(
// QueueJobs.SecretRotationV2SendNotification,
// async ([job]) => {
// const { secretRotation } = job.data as TSecretRotationSendNotificationJobPayload;
// try {
// const {
// name: rotationName,
// type,
// projectId,
// lastRotationAttemptedAt,
// folder,
// environment,
// id: dataSourceId
// } = secretRotation;
//
// logger.info(`secretRotationV2Queue: Sending Status Notification [dataSourceId=${dataSourceId}]`);
//
// const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
// const project = await projectDAL.findById(projectId);
//
// const projectAdmins = projectMembers.filter((member) =>
// member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
// );
//
// const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
//
// await smtpService.sendMail({
// recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
// template: SmtpTemplates.SecretRotationFailed,
// subjectLine: `Secret Rotation Failed`,
// substitutions: {
// rotationName,
// rotationType,
// content: `Your ${rotationType} Rotation failed to rotate during it's scheduled rotation. The last rotation attempt occurred at ${new Date(
// lastRotationAttemptedAt
// ).toISOString()}. Please check the rotation status in Infisical for more details.`,
// secretPath: folder.path,
// environment: environment.name,
// projectName: project.name,
// rotationUrl: encodeURI(`${appCfg.SITE_URL}/secret-manager/${projectId}/secrets/${environment.slug}`)
// }
// });
// } catch (error) {
// logger.error(
// error,
// `secretRotationV2Queue: Failed to Send Status Notification [dataSourceId=${secretRotation.id}]`
// );
// throw error;
// }
// },
// {
// batchSize: 1,
// workerCount: 2,
// pollingIntervalSeconds: 1
// }
// );
return {
queueDataSourceFullScan,
queueResourceDiffScan
};
};

View File

@ -0,0 +1,100 @@
import { z } from "zod";
import { SecretScanningDataSourcesSchema, SecretScanningFindingsSchema } from "@app/db/schemas";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps";
import { SecretScanningDataSources } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
type SecretScanningDataSourceSchemaOpts = {
type: SecretScanningDataSource;
isConnectionRequired: boolean;
};
// TODO: check if need type support for is connection required
export const BaseSecretScanningDataSourceSchema = ({
type,
isConnectionRequired
}: SecretScanningDataSourceSchemaOpts) =>
SecretScanningDataSourcesSchema.omit({
// unique to provider
type: true,
connectionId: true,
config: true,
encryptedCredentials: true
}).extend({
type: z.literal(type),
connectionId: isConnectionRequired ? z.string().uuid() : z.null(),
connection: isConnectionRequired
? z.object({
app: z.literal(SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[type]),
name: z.string(),
id: z.string().uuid()
})
: z.null()
});
export const BaseCreateSecretScanningDataSourceSchema = ({
type,
isConnectionRequired
}: SecretScanningDataSourceSchemaOpts) =>
z.object({
name: slugSchema({ field: "name" }).describe(SecretScanningDataSources.CREATE(type).name),
projectId: z
.string()
.trim()
.min(1, "Project ID required")
.describe(SecretScanningDataSources.CREATE(type).projectId),
description: z
.string()
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(SecretScanningDataSources.CREATE(type).description),
connectionId: isConnectionRequired
? z.string().uuid().describe(SecretScanningDataSources.CREATE(type).connectionId)
: z.undefined(),
isAutoScanEnabled: z
.boolean()
.optional()
.default(true)
.describe(SecretScanningDataSources.CREATE(type).isAutoScanEnabled)
});
export const BaseUpdateSecretScanningDataSourceSchema = (type: SecretScanningDataSource) =>
z.object({
name: slugSchema({ field: "name" }).describe(SecretScanningDataSources.UPDATE(type).name).optional(),
description: z
.string()
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(SecretScanningDataSources.UPDATE(type).description),
isAutoScanEnabled: z.boolean().optional().describe(SecretScanningDataSources.UPDATE(type).isAutoScanEnabled)
});
export const GitRepositoryScanFindingDetailsSchema = z.object({
description: z.string(),
startLine: z.number(),
endLine: z.number(),
startColumn: z.number(),
endColumn: z.number(),
file: z.string(),
link: z.string(),
symlinkFile: z.string(),
commit: z.string(),
entropy: z.number(),
author: z.string(),
email: z.string(),
date: z.string(),
message: z.string(),
tags: z.string().array(),
ruleID: z.string(),
fingerprint: z.string()
});
export const BaseSecretScanningFindingSchema = SecretScanningFindingsSchema.omit({
dataSourceType: true,
resourceType: true,
details: true
});

View File

@ -0,0 +1,740 @@
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionSecretScanningDataSourceActions,
ProjectPermissionSecretScanningFindingActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { githubSecretScanningService } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-service";
import { SecretScanningFindingStatus } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { SECRET_SCANNING_FACTORY_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-factory";
import { listSecretScanningDataSourceOptions } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
import {
SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP,
SECRET_SCANNING_DATA_SOURCE_NAME_MAP
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps";
import {
TCreateSecretScanningDataSourceDTO,
TDeleteSecretScanningDataSourceDTO,
TFindSecretScanningDataSourceByIdDTO,
TFindSecretScanningDataSourceByNameDTO,
TListSecretScanningDataSourcesByProjectId,
TSecretScanningDataSource,
TSecretScanningDataSourceWithConnection,
TSecretScanningDataSourceWithDetails,
TSecretScanningFinding,
TSecretScanningResourceWithDetails,
TSecretScanningScanWithDetails,
TTriggerSecretScanningDataSourceDTO,
TUpdateSecretScanningDataSourceDTO,
TUpdateSecretScanningFinding
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TQueueServiceFactory } from "@app/queue";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue";
export type TSecretScanningV2ServiceFactoryDep = {
secretScanningV2DAL: TSecretScanningV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
queueService: Pick<TQueueServiceFactory, "queuePg">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
secretScanningV2Queue: Pick<
TSecretScanningV2QueueServiceFactory,
"queueDataSourceFullScan" | "queueResourceDiffScan"
>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TSecretScanningV2ServiceFactory = ReturnType<typeof secretScanningV2ServiceFactory>;
export const secretScanningV2ServiceFactory = ({
secretScanningV2DAL,
permissionService,
appConnectionService,
licenseService,
auditLogService,
keyStore,
queueService,
appConnectionDAL,
secretScanningV2Queue,
kmsService
}: TSecretScanningV2ServiceFactoryDep) => {
const $checkListSecretScanningDataSourcesByProjectIdPermissions = async (
projectId: string,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Data Sources due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.Read,
ProjectPermissionSub.SecretScanningDataSources
);
};
const listSecretScanningDataSourcesByProjectId = async (
{ projectId, type }: TListSecretScanningDataSourcesByProjectId,
actor: OrgServiceActor
) => {
await $checkListSecretScanningDataSourcesByProjectIdPermissions(projectId, actor);
const dataSources = await secretScanningV2DAL.dataSources.find({
...(type && { type }),
projectId
});
return dataSources as TSecretScanningDataSource[];
};
const listSecretScanningDataSourcesWithDetailsByProjectId = async (
{ projectId, type }: TListSecretScanningDataSourcesByProjectId,
actor: OrgServiceActor
) => {
await $checkListSecretScanningDataSourcesByProjectIdPermissions(projectId, actor);
const dataSources = await secretScanningV2DAL.dataSources.findWithDetails({
...(type && { type }),
projectId
});
return dataSources as TSecretScanningDataSourceWithDetails[];
};
const findSecretScanningDataSourceById = async (
{ type, dataSourceId }: TFindSecretScanningDataSourceByIdDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Data Source due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource)
throw new NotFoundError({
message: `Could not find ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source with ID "${dataSourceId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: dataSource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.Read,
ProjectPermissionSub.SecretScanningDataSources
);
if (type !== dataSource.type)
throw new BadRequestError({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
return dataSource as TSecretScanningDataSource;
};
const findSecretScanningDataSourceByName = async (
{ type, sourceName, projectId }: TFindSecretScanningDataSourceByNameDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Data Source due to plan restriction. Upgrade plan to enable Secret Scanning."
});
// we prevent conflicting names within a folder
const dataSource = await secretScanningV2DAL.dataSources.findOne({
name: sourceName,
projectId
});
if (!dataSource)
throw new NotFoundError({
message: `Could not find ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source with name "${sourceName}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.Read,
ProjectPermissionSub.SecretScanningDataSources
);
if (type !== dataSource.type)
throw new BadRequestError({
message: `Secret Scanning Data Source with ID "${dataSource.id}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
return dataSource as TSecretScanningDataSource;
};
const createSecretScanningDataSource = async (
payload: TCreateSecretScanningDataSourceDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to create Secret Scanning Data Source due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: payload.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.Create,
ProjectPermissionSub.SecretScanningDataSources
);
let connection: TAppConnection | null = null;
if (payload.connectionId) {
// validates permission to connect and app is valid for data source
connection = await appConnectionService.connectAppConnectionById(
SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[payload.type],
payload.connectionId,
actor
);
}
const factory = SECRET_SCANNING_FACTORY_MAP[payload.type]();
try {
const createdDataSource = await factory.initialize(
{ payload, connection: connection as TSecretScanningDataSourceWithConnection["connection"] },
async ({ credentials, externalId }) => {
let encryptedCredentials: Buffer | null = null;
if (credentials) {
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: payload.projectId
});
const { cipherTextBlob } = encryptor({
plainText: Buffer.from(JSON.stringify(credentials))
});
encryptedCredentials = cipherTextBlob;
}
return secretScanningV2DAL.dataSources.transaction(async (tx) => {
const dataSource = await secretScanningV2DAL.dataSources.create(
{
encryptedCredentials,
externalId,
...payload
},
tx
);
await factory.postInitialization({
payload,
connection: connection as TSecretScanningDataSourceWithConnection["connection"],
dataSourceId: dataSource.id,
credentials
});
return dataSource;
});
}
);
if (payload.isAutoScanEnabled) {
try {
await secretScanningV2Queue.queueDataSourceFullScan({
...createdDataSource,
connection
} as TSecretScanningDataSourceWithConnection);
} catch {
// silently fail, don't want to block creation, they'll try scanning when they don't see anything and get the error
}
}
return createdDataSource as TSecretScanningDataSource;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `A Secret Scanning Data Source with the name "${payload.name}" already exists for the project with ID "${payload.projectId}"`
});
}
throw err;
}
};
const updateSecretScanningDataSource = async (
{ type, dataSourceId, ...payload }: TUpdateSecretScanningDataSourceDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to update Secret Scanning Data Source due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource)
throw new NotFoundError({
message: `Could not find ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source with ID "${dataSourceId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: dataSource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.Edit,
ProjectPermissionSub.SecretScanningDataSources
);
if (type !== dataSource.type)
throw new BadRequestError({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
try {
const updatedDataSource = await secretScanningV2DAL.dataSources.updateById(dataSourceId, payload);
return updatedDataSource as TSecretScanningDataSource;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `A Secret Scanning Data Source with the name "${payload.name}" already exists for the project with ID "${dataSource.projectId}"`
});
}
throw err;
}
};
const deleteSecretScanningResource = async (
{ type, dataSourceId }: TDeleteSecretScanningDataSourceDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to delete Secret Scanning Data Source due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource)
throw new NotFoundError({
message: `Could not find ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source with ID "${dataSourceId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: dataSource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.Delete,
ProjectPermissionSub.SecretScanningDataSources
);
if (type !== dataSource.type)
throw new BadRequestError({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
// TODO: clean up webhooks
await secretScanningV2DAL.dataSources.deleteById(dataSourceId);
return dataSource as TSecretScanningDataSource;
};
const triggerSecretScanningDataSourceScan = async (
{ type, dataSourceId, resourceId }: TTriggerSecretScanningDataSourceDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to trigger scan for Secret Scanning Data Source due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource)
throw new NotFoundError({
message: `Could not find ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source with ID "${dataSourceId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: dataSource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.TriggerScans,
ProjectPermissionSub.SecretScanningDataSources
);
if (type !== dataSource.type)
throw new BadRequestError({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
let connection: TAppConnection | null = null;
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
let resourceExternalId: string | undefined;
if (resourceId) {
const resource = await secretScanningV2DAL.resources.findOne({ id: resourceId, dataSourceId });
if (!resource) {
throw new NotFoundError({
message: `Could not find Secret Scanning Resource with ID "${resourceId}" for Data Source with ID "${dataSourceId}"`
});
}
resourceExternalId = resource.externalId;
}
await secretScanningV2Queue.queueDataSourceFullScan(
{
...dataSource,
connection
} as TSecretScanningDataSourceWithConnection,
resourceExternalId
);
return dataSource as TSecretScanningDataSource;
};
const listSecretScanningResourcesByDataSourceId = async (
{ type, dataSourceId }: TFindSecretScanningDataSourceByIdDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Resources due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource)
throw new NotFoundError({
message: `Could not find ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source with ID "${dataSourceId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: dataSource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.ReadResources,
ProjectPermissionSub.SecretScanningDataSources
);
if (type !== dataSource.type)
throw new BadRequestError({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
const resources = await secretScanningV2DAL.resources.find({
dataSourceId
});
return { resources, projectId: dataSource.projectId };
};
const listSecretScanningResourcesWithDetailsByDataSourceId = async (
{ type, dataSourceId }: TFindSecretScanningDataSourceByIdDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Resources due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource)
throw new NotFoundError({
message: `Could not find ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source with ID "${dataSourceId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: dataSource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.ReadResources,
ProjectPermissionSub.SecretScanningDataSources
);
if (type !== dataSource.type)
throw new BadRequestError({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
const resources = await secretScanningV2DAL.resources.findWithDetails({ dataSourceId });
return { resources: resources as TSecretScanningResourceWithDetails[], projectId: dataSource.projectId };
};
const listSecretScanningScansWithDetailsByDataSourceId = async (
{ type, dataSourceId }: TFindSecretScanningDataSourceByIdDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Scans due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource)
throw new NotFoundError({
message: `Could not find ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source with ID "${dataSourceId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: dataSource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
ProjectPermissionSub.SecretScanningDataSources
);
if (type !== dataSource.type)
throw new BadRequestError({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
const scans = await secretScanningV2DAL.scans.findWithDetailsByDataSourceId(dataSourceId);
return { scans: scans as TSecretScanningScanWithDetails[], projectId: dataSource.projectId };
};
const getSecretScanningUnresolvedFindingsCountByProjectId = async (projectId: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Findings due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningFindingActions.Read,
ProjectPermissionSub.SecretScanningFindings
);
const [finding] = await secretScanningV2DAL.findings.find(
{
projectId,
status: SecretScanningFindingStatus.Unresolved
},
{ count: true }
);
return Number(finding?.count ?? 0);
};
const listSecretScanningFindingsByProjectId = async (projectId: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Findings due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningFindingActions.Read,
ProjectPermissionSub.SecretScanningFindings
);
const findings = await secretScanningV2DAL.findings.find({
projectId
});
return findings as TSecretScanningFinding[];
};
const updateSecretScanningFindingById = async (
{ findingId, remarks, status }: TUpdateSecretScanningFinding,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretScanning)
throw new BadRequestError({
message:
"Failed to access Secret Scanning Findings due to plan restriction. Upgrade plan to enable Secret Scanning."
});
const finding = await secretScanningV2DAL.findings.findById(findingId);
if (!finding)
throw new NotFoundError({
message: `Could not find Secret Scanning Finding with ID "${findingId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretScanning,
projectId: finding.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretScanningFindingActions.Resolve,
ProjectPermissionSub.SecretScanningFindings
);
const updatedFinding = await secretScanningV2DAL.findings.updateById(findingId, {
remarks,
status
});
return { finding: updatedFinding as TSecretScanningFinding, projectId: finding.projectId };
};
return {
listSecretScanningDataSourceOptions,
listSecretScanningDataSourcesByProjectId,
listSecretScanningDataSourcesWithDetailsByProjectId,
findSecretScanningDataSourceById,
findSecretScanningDataSourceByName,
createSecretScanningDataSource,
updateSecretScanningDataSource,
deleteSecretScanningResource,
triggerSecretScanningDataSourceScan,
listSecretScanningResourcesByDataSourceId,
listSecretScanningResourcesWithDetailsByDataSourceId,
listSecretScanningScansWithDetailsByDataSourceId,
getSecretScanningUnresolvedFindingsCountByProjectId,
listSecretScanningFindingsByProjectId,
updateSecretScanningFindingById,
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue)
};
};

View File

@ -0,0 +1,167 @@
import { TSecretScanningFindingsInsert, TSecretScanningResources, TSecretScanningScans } from "@app/db/schemas";
import {
TGitHubDataSource,
TGitHubDataSourceInput,
TGitHubDataSourceListItem,
TGitHubDataSourceWithConnection,
TGitHubFinding,
TQueueGitHubResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/github";
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
import {
SecretScanningDataSource,
SecretScanningFindingStatus,
SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
export type TSecretScanningDataSource = TGitHubDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt?: Date | null;
lastScanStatus?: SecretScanningScanStatus | null;
lastScanStatusMessage?: string | null;
unresolvedFindings: number;
};
export type TSecretScanningResourceWithDetails = TSecretScanningResources & {
lastScannedAt?: Date | null;
lastScanStatus?: SecretScanningScanStatus | null;
lastScanStatusMessage?: string | null;
unresolvedFindings: number;
};
export type TSecretScanningScanWithDetails = TSecretScanningScans & {
unresolvedFindings: number;
resolvedFindings: number;
resourceName: string;
};
export type TSecretScanningDataSourceWithConnection = TGitHubDataSourceWithConnection;
export type TSecretScanningDataSourceInput = TGitHubDataSourceInput;
export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem;
export type TSecretScanningFinding = TGitHubFinding;
export type TListSecretScanningDataSourcesByProjectId = {
projectId: string;
type?: SecretScanningDataSource;
};
export type TFindSecretScanningDataSourceByIdDTO = {
dataSourceId: string;
type: SecretScanningDataSource;
};
export type TFindSecretScanningDataSourceByNameDTO = {
sourceName: string;
projectId: string;
type: SecretScanningDataSource;
};
export type TCreateSecretScanningDataSourceDTO = Pick<
TSecretScanningDataSource,
"description" | "name" | "projectId"
> & {
connectionId?: string;
type: SecretScanningDataSource;
isAutoScanEnabled?: boolean;
config: Partial<TSecretScanningDataSourceInput["config"]>;
};
export type TUpdateSecretScanningDataSourceDTO = Partial<
Omit<TCreateSecretScanningDataSourceDTO, "projectId" | "connectionId">
> & {
dataSourceId: string;
type: SecretScanningDataSource;
};
export type TDeleteSecretScanningDataSourceDTO = {
type: SecretScanningDataSource;
dataSourceId: string;
};
export type TTriggerSecretScanningDataSourceDTO = {
type: SecretScanningDataSource;
dataSourceId: string;
resourceId?: string;
};
export type TQueueSecretScanningDataSourceFullScan = {
dataSourceId: string;
resourceId: string;
scanId: string;
};
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan;
export type TCloneRepository = {
cloneUrl: string;
repoPath: string;
};
export type TSecretScanningFactoryListRawResources<T extends TSecretScanningDataSourceWithConnection> = (
dataSource: T
) => Promise<Pick<TSecretScanningResources, "externalId" | "name" | "type">[]>;
export type TSecretScanningFactoryGetDiffScanResourcePayload<
P extends TQueueSecretScanningResourceDiffScan["payload"]
> = (payload: P) => Pick<TSecretScanningResources, "externalId" | "name" | "type">;
export type TSecretScanningFactoryGetFullScanPath<T extends TSecretScanningDataSourceWithConnection> = (parameters: {
dataSource: T;
resourceName: string;
tempFolder: string;
}) => Promise<string>;
export type TSecretScanningFactoryGetDiffScanFindingsPayload<
T extends TSecretScanningDataSourceWithConnection,
P extends TQueueSecretScanningResourceDiffScan["payload"]
> = (parameters: { dataSource: T; resourceName: string; payload: P }) => Promise<TFindingsPayload>;
export type TSecretScanningDataSourceRaw = NonNullable<
Awaited<ReturnType<TSecretScanningV2DALFactory["dataSources"]["findById"]>>
>;
export type TSecretScanningFactoryInitialize<
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
C extends TSecretScanningDataSourceCredentials = undefined
> = (
params: { payload: TCreateSecretScanningDataSourceDTO; connection: T },
callback: (parameters: { credentials?: C; externalId?: string }) => Promise<TSecretScanningDataSourceRaw>
) => Promise<TSecretScanningDataSourceRaw>;
export type TSecretScanningFactoryPostInitialization<
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
C extends TSecretScanningDataSourceCredentials = undefined
> = (params: {
payload: TCreateSecretScanningDataSourceDTO;
connection: T;
credentials: C;
dataSourceId: string;
}) => Promise<void>;
export type TSecretScanningFactory<
T extends TSecretScanningDataSourceWithConnection,
C extends TSecretScanningDataSourceCredentials,
P extends TQueueSecretScanningResourceDiffScan["payload"]
> = () => {
listRawResources: TSecretScanningFactoryListRawResources<T>;
getFullScanPath: TSecretScanningFactoryGetFullScanPath<T>;
initialize: TSecretScanningFactoryInitialize<T["connection"] | undefined, C>;
postInitialization: TSecretScanningFactoryPostInitialization<T["connection"] | undefined, C>;
getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<P>;
getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<T, P>;
};
export type TFindingsPayload = Pick<TSecretScanningFindingsInsert, "details" | "fingerprint" | "severity" | "rule">[];
export type TGetFindingsPayload = Promise<TFindingsPayload>;
export type TUpdateSecretScanningFinding = {
status: SecretScanningFindingStatus;
remarks?: string | null;
findingId: string;
};
export type TSecretScanningDataSourceCredentials = undefined;

View File

@ -0,0 +1,14 @@
import { z } from "zod";
import { GitHubDataSourceSchema, GitHubFindingSchema } from "@app/ee/services/secret-scanning-v2/github";
import { GitLabDataSourceSchema, GitLabFindingSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema,
GitLabDataSourceSchema
]);
export const SecretScanningFindingSchema = z.discriminatedUnion("resourceType", [
GitHubFindingSchema,
GitLabFindingSchema
]);

View File

@ -9,6 +9,7 @@ export type SecretMatch = {
Match: string; Match: string;
Secret: string; Secret: string;
File: string; File: string;
Link: string;
SymlinkFile: string; SymlinkFile: string;
Commit: string; Commit: string;
Entropy: number; Entropy: number;

View File

@ -3,6 +3,12 @@ import {
SECRET_ROTATION_CONNECTION_MAP, SECRET_ROTATION_CONNECTION_MAP,
SECRET_ROTATION_NAME_MAP SECRET_ROTATION_NAME_MAP
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import {
AUTO_SYNC_DESCRIPTION_HELPER,
SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP,
SECRET_SCANNING_DATA_SOURCE_NAME_MAP
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@ -55,7 +61,8 @@ export enum ApiDocsTags {
SshHostGroups = "SSH Host Groups", SshHostGroups = "SSH Host Groups",
KmsKeys = "KMS Keys", KmsKeys = "KMS Keys",
KmsEncryption = "KMS Encryption", KmsEncryption = "KMS Encryption",
KmsSigning = "KMS Signing" KmsSigning = "KMS Signing",
SecretScanning = "Secret Scanning"
} }
export const GROUPS = { export const GROUPS = {
@ -2084,6 +2091,10 @@ export const AppConnections = {
region: "The region identifier in Oracle Cloud Infrastructure where the vault is located.", region: "The region identifier in Oracle Cloud Infrastructure where the vault is located.",
fingerprint: "The fingerprint of the public key uploaded to the user's API keys.", fingerprint: "The fingerprint of the public key uploaded to the user's API keys.",
privateKey: "The private key content in PEM format used to sign API requests." privateKey: "The private key content in PEM format used to sign API requests."
},
GITLAB: {
instanceUrl: "The GitLab instance URL to connect with (defaults to https://gitlab.com).",
accessToken: "The access token to use to connect with GitLab."
} }
} }
}; };
@ -2351,3 +2362,63 @@ export const SecretRotations = {
} }
} }
}; };
export const SecretScanningDataSources = {
LIST: (type?: SecretScanningDataSource) => ({
projectId: `The ID of the project to list ${type ? SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type] : "Scanning"} Data Sources from.`
}),
GET_BY_ID: (type: SecretScanningDataSource) => ({
dataSourceId: `The ID of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to retrieve.`
}),
GET_BY_NAME: (type: SecretScanningDataSource) => ({
sourceName: `The name of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to retrieve.`,
projectId: `The ID of the project the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source is located in.`
}),
CREATE: (type: SecretScanningDataSource) => {
const sourceType = SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type];
const autoScanDescription = AUTO_SYNC_DESCRIPTION_HELPER[type];
return {
name: `The name of the ${sourceType} Data Source to create. Must be slug-friendly.`,
description: `An optional description for the ${sourceType} Data Source.`,
projectId: `The ID of the project to create the ${sourceType} Data Source in.`,
connectionId: `The ID of the ${
APP_CONNECTION_NAME_MAP[SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[type]]
} Connection to use for this Data Source.`,
isAutoScanEnabled: `Whether scans should be automatically performed when a ${autoScanDescription.verb} occurs to ${autoScanDescription.noun} associated with this Data Source.`,
config: `The configuration parameters to use for this Data Source.`
};
},
UPDATE: (type: SecretScanningDataSource) => {
const typeName = SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type];
const autoScanDescription = AUTO_SYNC_DESCRIPTION_HELPER[type];
return {
dataSourceId: `The ID of the ${typeName} Data Source to be updated.`,
name: `The updated name of the ${typeName} Data Source. Must be slug-friendly.`,
description: `The updated description of the ${typeName} Data Source.`,
isAutoScanEnabled: `Whether scans should be automatically performed when a ${autoScanDescription.verb} occurs to ${autoScanDescription.noun} associated with this Data Source.`,
config: `The updated configuration parameters to use for this Data Source.`
};
},
DELETE: (type: SecretScanningDataSource) => ({
dataSourceId: `The ID of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to be deleted.`
}),
SCAN: (type: SecretScanningDataSource) => ({
dataSourceId: `The ID of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to trigger a scan for.`,
resourceId: `The ID of the individual Data Source resource to trigger a scan for.`
}),
LIST_RESOURCES: (type: SecretScanningDataSource) => ({
dataSourceId: `The ID of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to list resources from.`
})
};
export const SecretScanningFindings = {
LIST: {
projectId: `The ID of the project to list Secret Scanning Findings from.`
},
UPDATE: {
findingId: "The ID of the Secret Scanning Finding to update the resolve status for.",
status: "The updated status of the specified Secret Scanning Finding.",
remarks: "Remarks pertaining to the resolve status of this finding."
}
};

View File

@ -215,6 +215,14 @@ const envSchema = z
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()), INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
// github radar app
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_RADAR_APP_SLUG: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_RADAR_APP_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET: zpStr(z.string().optional()),
// gcp app // gcp app
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()), INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),

View File

@ -179,13 +179,18 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "batchInsert" }); throw new DatabaseError({ error, name: "batchInsert" });
} }
}, },
upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => { upsert: async (
data: readonly Tables[Tname]["insert"][],
onConflictField: keyof Tables[Tname]["base"] | Array<keyof Tables[Tname]["base"]>,
tx?: Knex,
mergeColumns?: (keyof Knex.ResolveTableType<Knex.TableType<Tname>, "update">)[] | undefined
) => {
try { try {
if (!data.length) return []; if (!data.length) return [];
const res = await (tx || db)(tableName) const res = await (tx || db)(tableName)
.insert(data as never) .insert(data as never)
.onConflict(onConflictField as never) .onConflict(onConflictField as never)
.merge() .merge(mergeColumns)
.returning("*"); .returning("*");
return res; return res;
} catch (error) { } catch (error) {

View File

@ -12,6 +12,10 @@ import {
TScanFullRepoEventPayload, TScanFullRepoEventPayload,
TScanPushEventPayload TScanPushEventPayload
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import {
TQueueSecretScanningDataSourceFullScan,
TQueueSecretScanningResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { import {
@ -51,7 +55,8 @@ export enum QueueName {
ImportSecretsFromExternalSource = "import-secrets-from-external-source", ImportSecretsFromExternalSource = "import-secrets-from-external-source",
AppConnectionSecretSync = "app-connection-secret-sync", AppConnectionSecretSync = "app-connection-secret-sync",
SecretRotationV2 = "secret-rotation-v2", SecretRotationV2 = "secret-rotation-v2",
InvalidateCache = "invalidate-cache" InvalidateCache = "invalidate-cache",
SecretScanningV2 = "secret-scanning-v2"
} }
export enum QueueJobs { export enum QueueJobs {
@ -84,7 +89,9 @@ export enum QueueJobs {
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations", SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
InvalidateCache = "invalidate-cache" InvalidateCache = "invalidate-cache",
SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@ -245,6 +252,15 @@ export type TQueueJobTypes = {
}; };
}; };
}; };
[QueueName.SecretScanningV2]:
| {
name: QueueJobs.SecretScanningV2FullScan;
payload: TQueueSecretScanningDataSourceFullScan;
}
| {
name: QueueJobs.SecretScanningV2DiffScan;
payload: TQueueSecretScanningResourceDiffScan;
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@ -0,0 +1,63 @@
import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types";
import { PushEvent } from "@octokit/webhooks-types";
import { Probot } from "probot";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { writeLimit } from "@app/server/config/rateLimiter";
export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvider) => {
const probotApp = (app: Probot) => {
app.on("installation.deleted", async (context) => {
const { payload } = context;
// const { installation, repositories } = payload;
// TODO: delete data resources
// await server.services.secretScanning.handleRepoDeleteEvent(
// String(installation.id),
// (repositories || [])?.map(({ id }) => String(id))
// );
});
app.on("installation", async (context) => {
const { payload } = context;
logger.info({ repositories: payload.repositories }, "Installed secret scanner to");
});
app.on("push", async (context) => {
const { payload } = context;
await server.services.secretScanningV2.github.handlePushEvent(payload as PushEvent);
});
};
const appCfg = getConfig();
const probot = new Probot({
appId: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_ID as string,
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY as string,
secret: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET as string
});
await probot.load(probotApp);
server.route({
method: "POST",
url: "/github",
config: {
rateLimit: writeLimit
},
handler: async (req, res) => {
const eventName = req.headers["x-github-event"] as EmitterWebhookEventName;
const signatureSHA256 = req.headers["x-hub-signature-256"] as string;
const id = req.headers["x-github-delivery"] as string;
await probot.webhooks.verifyAndReceive({
id,
name: eventName,
payload: JSON.stringify(req.body),
signature: signatureSHA256
});
return res.send("ok");
}
});
};

View File

@ -86,6 +86,10 @@ import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal"; import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
import { secretScanningQueueFactory } from "@app/ee/services/secret-scanning/secret-scanning-queue"; import { secretScanningQueueFactory } from "@app/ee/services/secret-scanning/secret-scanning-queue";
import { secretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service"; import { secretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
import { SECRET_SCANNING_WEBHOOK_PATH } from "@app/ee/services/secret-scanning-v2/github";
import { secretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
import { secretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
import { secretScanningV2ServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-service";
import { secretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { secretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal"; import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
@ -112,6 +116,7 @@ import { getConfig, TEnvConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TQueueServiceFactory } from "@app/queue"; import { TQueueServiceFactory } from "@app/queue";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { registerSecretScanningV2Webhooks } from "@app/server/plugins/secret-scanner-v2";
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue"; import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
@ -299,6 +304,9 @@ export const registerRoutes = async (
) => { ) => {
const appCfg = getConfig(); const appCfg = getConfig();
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" }); await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
await server.register(registerSecretScanningV2Webhooks, {
prefix: SECRET_SCANNING_WEBHOOK_PATH
});
// db layers // db layers
const userDAL = userDALFactory(db); const userDAL = userDALFactory(db);
@ -444,6 +452,7 @@ export const registerRoutes = async (
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL); const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db); const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db);
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db); const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
const secretScanningV2DAL = secretScanningV2DALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
@ -1694,6 +1703,28 @@ export const registerRoutes = async (
smtpService smtpService
}); });
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
secretScanningV2DAL,
queueService,
projectDAL,
projectMembershipDAL,
smtpService,
kmsService
});
const secretScanningV2Service = secretScanningV2ServiceFactory({
appConnectionDAL,
permissionService,
appConnectionService,
licenseService,
auditLogService,
keyStore,
queueService,
secretScanningV2DAL,
secretScanningV2Queue,
kmsService
});
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
// setup the communication with license key server // setup the communication with license key server
@ -1805,7 +1836,8 @@ export const registerRoutes = async (
secretRotationV2: secretRotationV2Service, secretRotationV2: secretRotationV2Service,
microsoftTeams: microsoftTeamsService, microsoftTeams: microsoftTeamsService,
assumePrivileges: assumePrivilegeService, assumePrivileges: assumePrivilegeService,
githubOrgSync: githubOrgSyncConfigService githubOrgSync: githubOrgSyncConfigService,
secretScanningV2: secretScanningV2Service
}); });
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];

View File

@ -28,6 +28,10 @@ import {
} from "@app/services/app-connection/databricks"; } from "@app/services/app-connection/databricks";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import {
GitHubRadarConnectionListItemSchema,
SanitizedGitHubRadarConnectionSchema
} from "@app/services/app-connection/github-radar";
import { import {
HCVaultConnectionListItemSchema, HCVaultConnectionListItemSchema,
SanitizedHCVaultConnectionSchema SanitizedHCVaultConnectionSchema
@ -62,6 +66,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SanitizedAppConnectionSchema = z.union([ const SanitizedAppConnectionSchema = z.union([
...SanitizedAwsConnectionSchema.options, ...SanitizedAwsConnectionSchema.options,
...SanitizedGitHubConnectionSchema.options, ...SanitizedGitHubConnectionSchema.options,
...SanitizedGitHubRadarConnectionSchema.options,
...SanitizedGcpConnectionSchema.options, ...SanitizedGcpConnectionSchema.options,
...SanitizedAzureKeyVaultConnectionSchema.options, ...SanitizedAzureKeyVaultConnectionSchema.options,
...SanitizedAzureAppConfigurationConnectionSchema.options, ...SanitizedAzureAppConfigurationConnectionSchema.options,
@ -84,6 +89,7 @@ const SanitizedAppConnectionSchema = z.union([
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AwsConnectionListItemSchema, AwsConnectionListItemSchema,
GitHubConnectionListItemSchema, GitHubConnectionListItemSchema,
GitHubRadarConnectionListItemSchema,
GcpConnectionListItemSchema, GcpConnectionListItemSchema,
AzureKeyVaultConnectionListItemSchema, AzureKeyVaultConnectionListItemSchema,
AzureAppConfigurationConnectionListItemSchema, AzureAppConfigurationConnectionListItemSchema,

View File

@ -0,0 +1,54 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateGitHubRadarConnectionSchema,
SanitizedGitHubRadarConnectionSchema,
UpdateGitHubRadarConnectionSchema
} from "@app/services/app-connection/github-radar";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerGitHubRadarConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.GitHubRadar,
server,
sanitizedResponseSchema: SanitizedGitHubRadarConnectionSchema,
createSchema: CreateGitHubRadarConnectionSchema,
updateSchema: UpdateGitHubRadarConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/repositories`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
repositories: z.object({ id: z.number(), name: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const repositories = await server.services.appConnection.githubRadar.listRepositories(
connectionId,
req.permission
);
return { repositories };
}
});
};

View File

@ -9,6 +9,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router";
import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router";
@ -26,6 +27,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
{ {
[AppConnection.AWS]: registerAwsConnectionRouter, [AppConnection.AWS]: registerAwsConnectionRouter,
[AppConnection.GitHub]: registerGitHubConnectionRouter, [AppConnection.GitHub]: registerGitHubConnectionRouter,
[AppConnection.GitHubRadar]: registerGitHubRadarConnectionRouter,
[AppConnection.GCP]: registerGcpConnectionRouter, [AppConnection.GCP]: registerGcpConnectionRouter,
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter, [AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter, [AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,

View File

@ -160,7 +160,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.default("false") .default("false")
.transform((value) => value === "true"), .transform((value) => value === "true"),
type: z type: z
.enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, ProjectType.SSH, "all"]) .enum([
ProjectType.SecretManager,
ProjectType.KMS,
ProjectType.CertificateManager,
ProjectType.SSH,
ProjectType.SecretScanning,
"all"
])
.optional() .optional()
}), }),
response: { response: {

View File

@ -1,5 +1,6 @@
export enum AppConnection { export enum AppConnection {
GitHub = "github", GitHub = "github",
GitHubRadar = "github-radar",
AWS = "aws", AWS = "aws",
Databricks = "databricks", Databricks = "databricks",
GCP = "gcp", GCP = "gcp",

View File

@ -2,6 +2,11 @@ import { TAppConnections } from "@app/db/schemas/app-connections";
import { generateHash } from "@app/lib/crypto/encryption"; import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import {
getGitHubRadarConnectionListItem,
GitHubRadarConnectionMethod,
validateGitHubRadarConnectionCredentials
} from "@app/services/app-connection/github-radar";
import { import {
transferSqlConnectionCredentialsToPlatform, transferSqlConnectionCredentialsToPlatform,
validateSqlConnectionCredentials validateSqlConnectionCredentials
@ -77,6 +82,7 @@ export const listAppConnectionOptions = () => {
return [ return [
getAwsConnectionListItem(), getAwsConnectionListItem(),
getGitHubConnectionListItem(), getGitHubConnectionListItem(),
getGitHubRadarConnectionListItem(),
getGcpConnectionListItem(), getGcpConnectionListItem(),
getAzureKeyVaultConnectionListItem(), getAzureKeyVaultConnectionListItem(),
getAzureAppConfigurationConnectionListItem(), getAzureAppConfigurationConnectionListItem(),
@ -146,6 +152,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitHubRadar]: validateGitHubRadarConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureAppConfiguration]: [AppConnection.AzureAppConfiguration]:
@ -173,6 +180,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
switch (method) { switch (method) {
case GitHubConnectionMethod.App: case GitHubConnectionMethod.App:
return "GitHub App"; return "GitHub App";
case GitHubRadarConnectionMethod.App:
return "GitHub App";
case AzureKeyVaultConnectionMethod.OAuth: case AzureKeyVaultConnectionMethod.OAuth:
case AzureAppConfigurationConnectionMethod.OAuth: case AzureAppConfigurationConnectionMethod.OAuth:
case AzureClientSecretsConnectionMethod.OAuth: case AzureClientSecretsConnectionMethod.OAuth:
@ -240,6 +249,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.AWS]: platformManagedCredentialsNotSupported, [AppConnection.AWS]: platformManagedCredentialsNotSupported,
[AppConnection.Databricks]: platformManagedCredentialsNotSupported, [AppConnection.Databricks]: platformManagedCredentialsNotSupported,
[AppConnection.GitHub]: platformManagedCredentialsNotSupported, [AppConnection.GitHub]: platformManagedCredentialsNotSupported,
[AppConnection.GitHubRadar]: platformManagedCredentialsNotSupported,
[AppConnection.GCP]: platformManagedCredentialsNotSupported, [AppConnection.GCP]: platformManagedCredentialsNotSupported,
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported, [AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported, [AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,

View File

@ -3,6 +3,7 @@ import { AppConnection } from "./app-connection-enums";
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = { export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AWS]: "AWS", [AppConnection.AWS]: "AWS",
[AppConnection.GitHub]: "GitHub", [AppConnection.GitHub]: "GitHub",
[AppConnection.GitHubRadar]: "GitHub Radar",
[AppConnection.GCP]: "GCP", [AppConnection.GCP]: "GCP",
[AppConnection.AzureKeyVault]: "Azure Key Vault", [AppConnection.AzureKeyVault]: "Azure Key Vault",
[AppConnection.AzureAppConfiguration]: "Azure App Configuration", [AppConnection.AzureAppConfiguration]: "Azure App Configuration",
@ -19,5 +20,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.HCVault]: "Hashicorp Vault", [AppConnection.HCVault]: "Hashicorp Vault",
[AppConnection.LDAP]: "LDAP", [AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity", [AppConnection.TeamCity]: "TeamCity",
[AppConnection.OCI]: "OCI" [AppConnection.OCI]: "OCI",
[AppConnection.GitLab]: "GitLab"
}; };

View File

@ -15,6 +15,7 @@ import {
validateAppConnectionCredentials validateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-fns"; } from "@app/services/app-connection/app-connection-fns";
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service"; import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service";
import { githubRadarConnectionService } from "@app/services/app-connection/github-radar/github-radar-connection-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal"; import { TAppConnectionDALFactory } from "./app-connection-dal";
@ -43,6 +44,7 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service"; import { gcpConnectionService } from "./gcp/gcp-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service"; import { githubConnectionService } from "./github/github-connection-service";
import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar";
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault"; import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service"; import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
@ -72,6 +74,7 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentialsSchema> = { const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentialsSchema> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema, [AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema, [AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
[AppConnection.GitHubRadar]: ValidateGitHubRadarConnectionCredentialsSchema,
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema, [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema, [AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema, [AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
@ -456,6 +459,7 @@ export const appConnectionServiceFactory = ({
connectAppConnectionById, connectAppConnectionById,
listAvailableAppConnectionsForUser, listAvailableAppConnectionsForUser,
github: githubConnectionService(connectAppConnectionById), github: githubConnectionService(connectAppConnectionById),
githubRadar: githubRadarConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById), gcp: gcpConnectionService(connectAppConnectionById),
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
aws: awsConnectionService(connectAppConnectionById), aws: awsConnectionService(connectAppConnectionById),

View File

@ -57,6 +57,12 @@ import {
TGitHubConnectionInput, TGitHubConnectionInput,
TValidateGitHubConnectionCredentialsSchema TValidateGitHubConnectionCredentialsSchema
} from "./github"; } from "./github";
import {
TGitHubRadarConnection,
TGitHubRadarConnectionConfig,
TGitHubRadarConnectionInput,
TValidateGitHubRadarConnectionCredentialsSchema
} from "./github-radar";
import { import {
THCVaultConnection, THCVaultConnection,
THCVaultConnectionConfig, THCVaultConnectionConfig,
@ -115,6 +121,7 @@ import {
export type TAppConnection = { id: string } & ( export type TAppConnection = { id: string } & (
| TAwsConnection | TAwsConnection
| TGitHubConnection | TGitHubConnection
| TGitHubRadarConnection
| TGcpConnection | TGcpConnection
| TAzureKeyVaultConnection | TAzureKeyVaultConnection
| TAzureAppConfigurationConnection | TAzureAppConfigurationConnection
@ -141,6 +148,7 @@ export type TSqlConnection = TPostgresConnection | TMsSqlConnection;
export type TAppConnectionInput = { id: string } & ( export type TAppConnectionInput = { id: string } & (
| TAwsConnectionInput | TAwsConnectionInput
| TGitHubConnectionInput | TGitHubConnectionInput
| TGitHubRadarConnectionInput
| TGcpConnectionInput | TGcpConnectionInput
| TAzureKeyVaultConnectionInput | TAzureKeyVaultConnectionInput
| TAzureAppConfigurationConnectionInput | TAzureAppConfigurationConnectionInput
@ -174,6 +182,7 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
export type TAppConnectionConfig = export type TAppConnectionConfig =
| TAwsConnectionConfig | TAwsConnectionConfig
| TGitHubConnectionConfig | TGitHubConnectionConfig
| TGitHubRadarConnectionConfig
| TGcpConnectionConfig | TGcpConnectionConfig
| TAzureKeyVaultConnectionConfig | TAzureKeyVaultConnectionConfig
| TAzureAppConfigurationConnectionConfig | TAzureAppConfigurationConnectionConfig
@ -194,6 +203,7 @@ export type TAppConnectionConfig =
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
| TValidateGitHubConnectionCredentialsSchema | TValidateGitHubConnectionCredentialsSchema
| TValidateGitHubRadarConnectionCredentialsSchema
| TValidateGcpConnectionCredentialsSchema | TValidateGcpConnectionCredentialsSchema
| TValidateAzureKeyVaultConnectionCredentialsSchema | TValidateAzureKeyVaultConnectionCredentialsSchema
| TValidateAzureAppConfigurationConnectionCredentialsSchema | TValidateAzureAppConfigurationConnectionCredentialsSchema

View File

@ -0,0 +1,3 @@
export enum GitHubRadarConnectionMethod {
App = "github-app"
}

View File

@ -0,0 +1,166 @@
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import { AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
import {
TGitHubRadarConnection,
TGitHubRadarConnectionConfig,
TGitHubRadarRepository
} from "./github-radar-connection-types";
export const getGitHubRadarConnectionListItem = () => {
const { INF_APP_CONNECTION_GITHUB_RADAR_APP_SLUG } = getConfig();
return {
name: "GitHub Radar" as const,
app: AppConnection.GitHubRadar as const,
methods: Object.values(GitHubRadarConnectionMethod) as [GitHubRadarConnectionMethod.App],
appClientSlug: INF_APP_CONNECTION_GITHUB_RADAR_APP_SLUG
};
};
export const getGitHubRadarClient = (appConnection: TGitHubRadarConnection) => {
const appCfg = getConfig();
const { method, credentials } = appConnection;
let client: Octokit;
switch (method) {
case GitHubRadarConnectionMethod.App:
if (!appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY) {
throw new InternalServerError({
message: `GitHub ${getAppConnectionMethodName(method).replace(
"GitHub",
""
)} environment variables have not been configured`
});
}
client = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_ID,
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY,
installationId: credentials.installationId
}
});
break;
default:
throw new InternalServerError({
message: `Unhandled GitHub Radar connection method: ${method as GitHubRadarConnectionMethod}`
});
}
return client;
};
export const listGitHubRadarRepositories = async (appConnection: TGitHubRadarConnection) => {
const client = getGitHubRadarClient(appConnection);
const repositories: TGitHubRadarRepository[] = await client.paginate("GET /installation/repositories");
return repositories;
};
type TokenRespData = {
access_token: string;
scope: string;
token_type: string;
error?: string;
};
export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRadarConnectionConfig) => {
const { credentials, method } = config;
const { INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID, INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET, SITE_URL } =
getConfig();
if (!INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID || !INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET) {
throw new InternalServerError({
message: `GitHub ${getAppConnectionMethodName(method).replace(
"GitHub",
""
)} environment variables have not been configured`
});
}
let tokenResp: AxiosResponse<TokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID,
client_secret: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET,
code: credentials.code,
redirect_uri: `${SITE_URL}/organization/app-connections/github-radar/oauth/callback`
},
headers: {
Accept: "application/json",
"Accept-Encoding": "application/json"
}
});
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
}
if (tokenResp.status !== 200) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
});
}
if (method === GitHubRadarConnectionMethod.App) {
const installationsResp = await request.get<{
installations: {
id: number;
account: {
login: string;
type: string;
id: number;
};
}[];
}>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${tokenResp.data.access_token}`,
"Accept-Encoding": "application/json"
}
});
const matchingInstallation = installationsResp.data.installations.find(
(installation) => installation.id === +credentials.installationId
);
if (!matchingInstallation) {
throw new ForbiddenRequestError({
message: "User does not have access to the provided installation"
});
}
}
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
switch (method) {
case GitHubRadarConnectionMethod.App:
return {
installationId: credentials.installationId
};
default:
throw new InternalServerError({
message: `Unhandled GitHub connection method: ${method as GitHubRadarConnectionMethod}`
});
}
};

View File

@ -0,0 +1,71 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
export const GitHubRadarConnectionInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "GitHub Radar App code required"),
installationId: z.string().min(1, "GitHub Radar App Installation ID required")
});
export const GitHubRadarConnectionOutputCredentialsSchema = z.object({
installationId: z.string()
});
export const ValidateGitHubRadarConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(GitHubRadarConnectionMethod.App)
.describe(AppConnections.CREATE(AppConnection.GitHubRadar).method),
credentials: GitHubRadarConnectionInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GitHubRadar).credentials
)
})
]);
export const CreateGitHubRadarConnectionSchema = ValidateGitHubRadarConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHubRadar)
);
export const UpdateGitHubRadarConnectionSchema = z
.object({
credentials: GitHubRadarConnectionInputCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.GitHubRadar).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHubRadar));
const BaseGitHubRadarConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHubRadar) });
export const GitHubRadarConnectionSchema = z.intersection(
BaseGitHubRadarConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(GitHubRadarConnectionMethod.App),
credentials: GitHubRadarConnectionOutputCredentialsSchema
})
])
);
export const SanitizedGitHubRadarConnectionSchema = z.discriminatedUnion("method", [
BaseGitHubRadarConnectionSchema.extend({
method: z.literal(GitHubRadarConnectionMethod.App),
credentials: GitHubRadarConnectionOutputCredentialsSchema.pick({})
})
]);
export const GitHubRadarConnectionListItemSchema = z.object({
name: z.literal("GitHub Radar"),
app: z.literal(AppConnection.GitHubRadar),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GitHubRadarConnectionMethod).array(),
appClientSlug: z.string().optional()
});

View File

@ -0,0 +1,24 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { listGitHubRadarRepositories } from "@app/services/app-connection/github-radar/github-radar-connection-fns";
import { TGitHubRadarConnection } from "@app/services/app-connection/github-radar/github-radar-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TGitHubRadarConnection>;
export const githubRadarConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listRepositories = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.GitHubRadar, connectionId, actor);
const repositories = await listGitHubRadarRepositories(appConnection);
return repositories.map((repo) => ({ id: repo.id, name: repo.full_name }));
};
return {
listRepositories
};
};

View File

@ -0,0 +1,28 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateGitHubRadarConnectionSchema,
GitHubRadarConnectionSchema,
ValidateGitHubRadarConnectionCredentialsSchema
} from "./github-radar-connection-schemas";
export type TGitHubRadarConnection = z.infer<typeof GitHubRadarConnectionSchema>;
export type TGitHubRadarConnectionInput = z.infer<typeof CreateGitHubRadarConnectionSchema> & {
app: AppConnection.GitHub;
};
export type TValidateGitHubRadarConnectionCredentialsSchema = typeof ValidateGitHubRadarConnectionCredentialsSchema;
export type TGitHubRadarConnectionConfig = DiscriminativePick<
TGitHubRadarConnectionInput,
"method" | "app" | "credentials"
>;
export type TGitHubRadarRepository = {
id: number;
full_name: string;
};

View File

@ -0,0 +1,4 @@
export * from "./github-radar-connection-enums";
export * from "./github-radar-connection-fns";
export * from "./github-radar-connection-schemas";
export * from "./github-radar-connection-types";

View File

@ -8,6 +8,7 @@ import {
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
@ -29,7 +30,6 @@ import {
TGetCertPrivateKeyDTO, TGetCertPrivateKeyDTO,
TRevokeCertDTO TRevokeCertDTO
} from "./certificate-types"; } from "./certificate-types";
import { NotFoundError } from "@app/lib/errors";
type TCertificateServiceFactoryDep = { type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;

View File

@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/github-radar/available"
---

View File

@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/github-radar"
---
<Note>
GitHub Radar Connections must be created through the Infisical UI.
Check out the configuration docs for [GitHub Radar Connections](/integrations/app-connections/github-radar) for a step-by-step
guide.
</Note>

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/github-radar/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/github-radar/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/github-radar/connection-name/{connectionName}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/github-radar"
---

View File

@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/github-radar/{connectionId}"
---
<Note>
GitHub Radar Connections must be updated through the Infisical UI.
Check out the configuration docs for [GitHub Radar Connections](/integrations/app-connections/github-radar) for a step-by-step
guide.
</Note>

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-scanning/data-sources"
---

View File

@ -0,0 +1,4 @@
---
title: "Options"
openapi: "GET /api/v2/secret-scanning/data-sources/options"
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

View File

@ -3,7 +3,7 @@ title: "TeamCity Connection"
description: "Learn how to configure a TeamCity Connection for Infisical." description: "Learn how to configure a TeamCity Connection for Infisical."
--- ---
Infisical supports connecting to TeamCity using an Access Token to securely sync your secrets to TeamCity. Infisical supports connecting to TeamCity using Access Tokens.
## Setup TeamCity Connection in Infisical ## Setup TeamCity Connection in Infisical

View File

@ -3,7 +3,7 @@ title: "Vercel Connection"
description: "Learn how to configure a Vercel Connection for Infisical." description: "Learn how to configure a Vercel Connection for Infisical."
--- ---
Infisical supports connecting to Vercel using an API Token to securely sync your secrets to Vercel. Infisical supports connecting to Vercel using API Tokens.
## Setup Vercel Connection in Infisical ## Setup Vercel Connection in Infisical

View File

@ -3,7 +3,7 @@ title: "Windmill Connection"
description: "Learn how to configure a Windmill Connection for Infisical." description: "Learn how to configure a Windmill Connection for Infisical."
--- ---
Infisical supports connecting to Windmill using an **Access Token** to securely sync your secrets to Windmill. Infisical supports connecting to Windmill using Access Tokens.
## Get a Windmill Access Token ## Get a Windmill Access Token

View File

@ -1,6 +1,6 @@
{ {
"name": "Infisical", "name": "Infisical",
"openapi": "https://app.infisical.com/api/docs/json", "openapi": "http://localhost:8080/api/docs/json",
"logo": { "logo": {
"dark": "/logo/dark.svg", "dark": "/logo/dark.svg",
"light": "/logo/light.svg", "light": "/logo/light.svg",
@ -478,6 +478,7 @@
"integrations/app-connections/databricks", "integrations/app-connections/databricks",
"integrations/app-connections/gcp", "integrations/app-connections/gcp",
"integrations/app-connections/github", "integrations/app-connections/github",
"integrations/app-connections/github-radar",
"integrations/app-connections/hashicorp-vault", "integrations/app-connections/hashicorp-vault",
"integrations/app-connections/humanitec", "integrations/app-connections/humanitec",
"integrations/app-connections/ldap", "integrations/app-connections/ldap",
@ -1008,6 +1009,13 @@
} }
] ]
}, },
{
"group": "Secret Scanning",
"pages": [
"api-reference/endpoints/secret-scanning/data-sources/list",
"api-reference/endpoints/secret-scanning/data-sources/options"
]
},
{ {
"group": "Identity Specific Privilege", "group": "Identity Specific Privilege",
"pages": [ "pages": [
@ -1148,6 +1156,18 @@
"api-reference/endpoints/app-connections/github/delete" "api-reference/endpoints/app-connections/github/delete"
] ]
}, },
{
"group": "GitHub Radar",
"pages": [
"api-reference/endpoints/app-connections/github-radar/list",
"api-reference/endpoints/app-connections/github-radar/available",
"api-reference/endpoints/app-connections/github-radar/get-by-id",
"api-reference/endpoints/app-connections/github-radar/get-by-name",
"api-reference/endpoints/app-connections/github-radar/create",
"api-reference/endpoints/app-connections/github-radar/update",
"api-reference/endpoints/app-connections/github-radar/delete"
]
},
{ {
"group": "Hashicorp Vault", "group": "Hashicorp Vault",
"pages": [ "pages": [

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,97 @@
import { useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Modal, ModalContent } from "@app/components/v2";
import {
SecretScanningDataSource,
TSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import { SecretScanningDataSourceForm } from "./forms";
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
import { SecretScanningDataSourceSelect } from "./SecretScanningDataSourceSelect";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
selectedDataSource: SecretScanningDataSource | null;
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
};
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
if (selectedDataSource) {
return (
<SecretScanningDataSourceForm
onCancel={() => setSelectedDataSource(null)}
type={selectedDataSource}
{...props}
/>
);
}
return <SecretScanningDataSourceSelect onSelect={setSelectedDataSource} />;
};
export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...props }: Props) => {
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
null
);
return (
<Modal
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) setSelectedDataSource(null);
onOpenChange(open);
}}
>
<ModalContent
title={
selectedDataSource ? (
<SecretScanningDataSourceModalHeader isConfigured={false} type={selectedDataSource} />
) : (
<div className="flex items-center text-mineshaft-300">
Add Data Source
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
)
}
onPointerDownOutside={(e) => e.preventDefault()}
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
subTitle={
selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
}
bodyClassName="overflow-visible"
>
<Content
onComplete={() => {
setSelectedDataSource(null);
onOpenChange(false);
}}
selectedDataSource={selectedDataSource}
setSelectedDataSource={setSelectedDataSource}
{...props}
/>
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,66 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
TSecretScanningDataSource,
useDeleteSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
type Props = {
dataSource?: TSecretScanningDataSource;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onComplete?: () => void;
};
export const DeleteSecretScanningDataSourceModal = ({
isOpen,
onOpenChange,
dataSource,
onComplete
}: Props) => {
const deleteSecretRotation = useDeleteSecretScanningDataSource();
if (!dataSource) return null;
const { id: dataSourceId, name, type, projectId } = dataSource;
const handleDeleteDataSource = async () => {
const dataSourceType = SECRET_SCANNING_DATA_SOURCE_MAP[type].name;
try {
await deleteSecretRotation.mutateAsync({
dataSourceId,
type,
projectId
});
createNotification({
text: `Successfully deleted ${dataSourceType} Data Source`,
type: "success"
});
if (onComplete) onComplete();
onOpenChange(false);
} catch {
createNotification({
text: `Failed to delete ${dataSourceType} Data Source`,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteDataSource}
>
<p className="mt-1 font-inter text-sm text-mineshaft-400">
Findings associated with this data source will be preserved.
</p>
</DeleteActionModal>
);
};

View File

@ -0,0 +1,36 @@
import { Modal, ModalContent } from "@app/components/v2";
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { SecretScanningDataSourceForm } from "./forms";
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
dataSource?: TSecretScanningDataSource;
};
export const EditSecretScanningDataSourceModal = ({
dataSource,
onOpenChange,
...props
}: Props) => {
if (!dataSource) return null;
return (
<Modal {...props} onOpenChange={onOpenChange}>
<ModalContent
title={<SecretScanningDataSourceModalHeader isConfigured type={dataSource.type} />}
className="max-w-2xl"
bodyClassName="overflow-visible"
>
<SecretScanningDataSourceForm
onComplete={() => onOpenChange(false)}
onCancel={() => onOpenChange(false)}
dataSource={dataSource}
type={dataSource.type}
/>
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,47 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
type Props = {
type: SecretScanningDataSource;
isConfigured: boolean;
};
export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Props) => {
const dataSourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
return (
<div className="flex w-full items-start gap-2">
<img
alt={`${dataSourceDetails.name} logo`}
src={`/images/integrations/${dataSourceDetails.image}`}
className="h-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
{dataSourceDetails.name} Data Source
<a
target="_blank"
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source
</p>
</div>
</div>
);
};

View File

@ -0,0 +1,91 @@
import { faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Spinner, Tooltip } from "@app/components/v2";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
SecretScanningDataSource,
useSecretScanningDataSourceOptions
} from "@app/hooks/api/secretScanningV2";
type Props = {
onSelect: (type: SecretScanningDataSource) => void;
};
export const SecretScanningDataSourceSelect = ({ onSelect }: Props) => {
const { isPending, data: dataSourceOptions } = useSecretScanningDataSourceOptions();
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
</div>
);
}
return (
<div className="grid grid-cols-3 gap-2">
{dataSourceOptions?.map(({ type }) => {
const { image, name, size } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
return (
<button
type="button"
key={type}
onClick={() => onSelect(type)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
width={size}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more services.</p>
<p>
{`If you don't see the third-party
service you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4 hover:bg-mineshaft-900/50">
<FontAwesomeIcon className="mt-auto text-3xl" icon={faWrench} />
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
Coming Soon
</div>
</div>
</Tooltip>
</div>
);
};

View File

@ -0,0 +1,90 @@
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatDistance } from "date-fns";
import { twMerge } from "tailwind-merge";
import { Badge, Tooltip } from "@app/components/v2";
import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
type Props = {
status: SecretScanningScanStatus;
statusMessage?: string | null;
className?: string;
scannedAt?: string | null;
};
export const SecretScanningScanStatusBadge = ({
status,
statusMessage,
className,
scannedAt
}: Props) => {
if (status === SecretScanningScanStatus.Failed) {
let errorMessage = statusMessage;
if (statusMessage) {
try {
errorMessage = JSON.stringify(JSON.parse(statusMessage), null, 2);
} catch {
errorMessage = statusMessage;
}
}
return (
<Tooltip
position="left"
className="max-w-sm select-text"
content={
<div className="flex flex-col gap-2 whitespace-normal py-1">
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<div className="text-xs">Failure Reason</div>
</div>
<div className="break-words rounded bg-mineshaft-600 p-2 text-xs">{errorMessage}</div>
{scannedAt && (
<div className="mt-1 text-xs text-mineshaft-400">
Attempted {formatDistance(new Date(scannedAt), new Date(), { addSuffix: true })}
</div>
)}
</div>
</div>
}
>
<div>
<Badge
variant="danger"
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
>
<FontAwesomeIcon icon={faXmark} />
Scan Error
</Badge>
</div>
</Tooltip>
);
}
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
return (
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant="primary"
>
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
<span>Scanning</span>
</Badge>
);
}
return (
<Badge
variant="success"
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faCheck} />
<span>Complete</span>
</Badge>
);
};

View File

@ -0,0 +1,126 @@
import { useEffect } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { MultiValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import {
TGitHubRadarConnectionRepository,
useGitHubRadarConnectionListRepositories
} from "@app/hooks/api/appConnections/github-radar";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
enum ScanMethod {
AllRepositories = "all-repositories",
SelectRepositories = "select-repositories"
}
export const GitHubDataSourceConfigFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitHub;
}
>();
const connectionId = useWatch({ control, name: "connection.id" });
const isUpdate = Boolean(watch("id"));
const { data: repositories, isPending: areRepositoriesLoading } =
useGitHubRadarConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) });
const includeRepos = watch("config.includeRepos");
const scanMethod =
!includeRepos || includeRepos[0] === "*"
? ScanMethod.AllRepositories
: ScanMethod.SelectRepositories;
useEffect(() => {
if (!includeRepos) {
setValue("config.includeRepos", ["*"]);
}
}, [includeRepos]);
return (
<>
<SecretScanningDataSourceConnectionField
isUpdate={isUpdate}
onChange={() => {
if (scanMethod === ScanMethod.SelectRepositories) {
setValue("config.includeRepos", []);
}
}}
/>
<FormControl label="Scan Projects">
<Select
value={scanMethod}
onValueChange={(val) => {
setValue("config.includeRepos", val === ScanMethod.AllRepositories ? ["*"] : []);
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
isDisabled={!connectionId}
>
{Object.values(ScanMethod).map((method) => {
return (
<SelectItem className="capitalize" value={method} key={method}>
{method.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</FormControl>
{scanMethod === ScanMethod.SelectRepositories && (
<Controller
name="config.includeRepos"
defaultValue={["*"]}
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Include Repositories"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the repository you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={areRepositoriesLoading && Boolean(connectionId)}
isDisabled={!connectionId}
isMulti
value={repositories?.find((project) => value.includes(project.name))}
onChange={(newValue) => {
onChange(
newValue
? (newValue as MultiValue<TGitHubRadarConnectionRepository>).map(
(p) => p.name
)
: null
);
}}
options={repositories}
placeholder="Select repositories..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
)}
</>
);
};

View File

@ -0,0 +1,55 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Switch } from "@app/components/v2";
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields
};
export const SecretScanningDataSourceConfigFields = () => {
const { watch, control } = useFormContext<TSecretScanningDataSourceForm>();
const type = watch("type");
const Component = COMPONENT_MAP[type];
const autoScanDescription = RESOURCE_DESCRIPTION_HELPER[type];
return (
<>
<p className="mb-4 text-sm text-bunker-300">Connect and configure your Data Source.</p>
<Component />
<Controller
control={control}
name="isAutoScanEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl
helperText={
value
? `Scans will automatically be triggered when a ${autoScanDescription.verb} occurs to ${autoScanDescription.pluralNoun} associated with this data source.`
: "Manually trigger scans to detect secret leaks."
}
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="auto-scan-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p className="w-[9.6rem]">Auto-Scan {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
);
}}
/>
</>
);
};

View File

@ -0,0 +1 @@
export * from "./SecretScanningDataSourceConfigFields";

View File

@ -0,0 +1,105 @@
import { Controller, useFormContext } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { TSecretScanningDataSourceForm } from "./schemas";
type Props = {
onChange?: VoidFunction;
isUpdate?: boolean;
};
export const SecretScanningDataSourceConnectionField = ({
onChange: callback,
isUpdate
}: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretScanningDataSourceForm>();
const dataSourceType = watch("type");
const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
);
const appName = APP_CONNECTION_MAP[app].name;
return (
<>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="App Connections can be created from the Organization Settings page."
isError={Boolean(error)}
errorText={error?.message}
label={`${connectionName} Connection`}
helperText={
isUpdate ? (
"Cannot be updated"
) : (
<p>
Check out{" "}
<a
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
target="_blank"
className="underline"
rel="noopener noreferrer"
>
our docs
</a>{" "}
to ensure your connection has the required permissions for secret scanning.
</p>
)
}
>
<FilterableSelect
value={value}
onChange={(newValue) => {
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
isDisabled={isUpdate}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
)}
</p>
)}
</>
);
};

View File

@ -0,0 +1,51 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input, TextArea } from "@app/components/v2";
import { TSecretScanningDataSourceForm } from "./schemas";
export const SecretScanningDataSourceDetailsFields = () => {
const { control } = useFormContext<TSecretScanningDataSourceForm>();
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Provide a name and description for this Data Source.
</p>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
helperText="Must be slug-friendly"
isError={Boolean(error)}
errorText={error?.message}
label="Name"
>
<Input autoFocus value={value} onChange={onChange} placeholder="my-data-source" />
</FormControl>
)}
control={control}
name="name"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Description"
>
<TextArea
value={value ?? ""}
onChange={onChange}
placeholder="Provide a description for this data source..."
className="!resize-none"
rows={4}
/>
</FormControl>
)}
control={control}
name="description"
/>
</>
);
};

View File

@ -0,0 +1,180 @@
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
SecretScanningDataSource,
TSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import {
useCreateSecretScanningDataSource,
useUpdateSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2/mutations";
import { SecretScanningDataSourceSchema, TSecretScanningDataSourceForm } from "./schemas";
import { SecretScanningDataSourceConfigFields } from "./SecretScanningDataSourceConfigFields";
import { SecretScanningDataSourceDetailsFields } from "./SecretScanningDataSourceDetailsFields";
import { SecretScanningDataSourceReviewFields } from "./SecretScanningDataSourceReviewFields";
type Props = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
type: SecretScanningDataSource;
onCancel: () => void;
dataSource?: TSecretScanningDataSource;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningDataSourceForm)[] }[] =
[
{ name: "Configuration", key: "config", fields: ["config", "isAutoScanEnabled", "connection"] },
{ name: "Details", key: "details", fields: ["name", "description"] },
{ name: "Review", key: "review", fields: [] }
];
export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataSource }: Props) => {
const createDataSource = useCreateSecretScanningDataSource();
const updateDataSource = useUpdateSecretScanningDataSource();
const { currentWorkspace } = useWorkspace();
const { name: sourceType } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const formMethods = useForm<TSecretScanningDataSourceForm>({
resolver: zodResolver(SecretScanningDataSourceSchema),
defaultValues: dataSource ?? {
type,
isAutoScanEnabled: true // scott: this may need to be derived from type in the future
},
reValidateMode: "onChange"
});
const onSubmit = async ({ connection, ...formData }: TSecretScanningDataSourceForm) => {
const mutation = dataSource
? updateDataSource.mutateAsync({
dataSourceId: dataSource.id,
projectId: dataSource.projectId,
...formData
})
: createDataSource.mutateAsync({
...formData,
connectionId: connection?.id,
projectId: currentWorkspace.id
});
try {
const rotation = await mutation;
createNotification({
text: `Successfully ${dataSource ? "updated" : "created"} ${sourceType} Data Source`,
type: "success"
});
onComplete(rotation);
} catch (err: any) {
createNotification({
title: `Failed to ${dataSource ? "update" : "create"} ${sourceType} Data Source`,
text: err.message,
type: "error"
});
}
};
const handlePrev = () => {
if (selectedTabIndex === 0) {
onCancel();
return;
}
setSelectedTabIndex((prev) => prev - 1);
};
const {
handleSubmit,
trigger,
formState: { isSubmitting }
} = formMethods;
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
const handleNext = async () => {
if (isFinalStep) {
handleSubmit(onSubmit)();
return;
}
const isValid = await isStepValid(selectedTabIndex);
if (!isValid) return;
setSelectedTabIndex((prev) => prev + 1);
};
const isTabEnabled = async (index: number) => {
let isEnabled = true;
for (let i = index - 1; i >= 0; i -= 1) {
// eslint-disable-next-line no-await-in-loop
isEnabled = isEnabled && (await isStepValid(i));
}
return isEnabled;
};
return (
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
<FormProvider {...formMethods}>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
{FORM_TABS.map((tab, index) => (
<Tab
onClick={async (e) => {
e.preventDefault();
const isEnabled = await isTabEnabled(index);
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
}}
className={({ selected }) =>
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
key={tab.key}
>
{index + 1}. {tab.name}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<SecretScanningDataSourceConfigFields />
</Tab.Panel>
<Tab.Panel>
<SecretScanningDataSourceDetailsFields />
</Tab.Panel>
<Tab.Panel>
<SecretScanningDataSourceReviewFields />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button
onClick={handleNext}
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema="secondary"
>
{isFinalStep ? `${dataSource ? "Update" : "Create"} Data Source` : "Next"}
</Button>
<Button onClick={handlePrev} colorSchema="secondary">
Back
</Button>
</div>
</form>
);
};

View File

@ -0,0 +1,27 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConfigReviewSection } from "./shared";
export const GitHubDataSourceReviewFields = () => {
const { watch } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitHub;
}
>();
const [{ includeRepos }, connection] = watch(["config", "connection"]);
const shouldScanAll = includeRepos[0] === "*";
return (
<SecretScanningDataSourceConfigReviewSection>
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
<GenericFieldLabel label="Scan Projects">
{shouldScanAll ? "All" : includeRepos.join(", ")}
</GenericFieldLabel>
</SecretScanningDataSourceConfigReviewSection>
);
};

View File

@ -0,0 +1,34 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields
};
export const SecretScanningDataSourceReviewFields = () => {
const { watch } = useFormContext<TSecretScanningDataSourceForm>();
const { type, name, description } = watch();
const Component = COMPONENT_MAP[type];
return (
<div className="mb-4 flex flex-col gap-6">
<Component />
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Details</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./SecretScanningDataSourceReviewFields";

View File

@ -0,0 +1,16 @@
import { ReactNode } from "react";
type Props = {
children: ReactNode;
};
export const SecretScanningDataSourceConfigReviewSection = ({ children }: Props) => {
return (
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Configuration</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">{children}</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./SecretScanningDataSourceConfigReviewSection";

View File

@ -0,0 +1 @@
export * from "./SecretScanningDataSourceForm";

View File

@ -0,0 +1,18 @@
import { z } from "zod";
import { slugSchema } from "@app/lib/schemas";
type SchemaOptions = {
isConnectionRequired: boolean;
};
export const BaseSecretScanningDataSourceSchema = ({ isConnectionRequired }: SchemaOptions) =>
z.object({
name: slugSchema({ field: "Name" }),
description: z.string().trim().max(256, "Cannot exceed 256 characters").nullish(),
connection: isConnectionRequired
? z.object({ name: z.string(), id: z.string().uuid() })
: z.null().or(z.undefined()),
isAutoScanEnabled: z.boolean(),
id: z.string().uuid().optional()
});

Some files were not shown because too many files have changed in this diff Show More