mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-13 09:35:39 +00:00
Compare commits
1 Commits
commit-ui-
...
secret-sca
Author | SHA1 | Date | |
---|---|---|---|
b3da70a993 |
@ -107,6 +107,14 @@ INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||
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
|
||||
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
||||
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -37,6 +37,7 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
|
||||
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 { 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 { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||
@ -268,6 +269,7 @@ declare module "fastify" {
|
||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||
secretScanningV2: TSecretScanningV2ServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
32
backend/src/@types/knex.d.ts
vendored
32
backend/src/@types/knex.d.ts
vendored
@ -324,9 +324,21 @@ import {
|
||||
TSecretRotationV2SecretMappingsInsert,
|
||||
TSecretRotationV2SecretMappingsUpdate,
|
||||
TSecrets,
|
||||
TSecretScanningDataSources,
|
||||
TSecretScanningDataSourcesInsert,
|
||||
TSecretScanningDataSourcesUpdate,
|
||||
TSecretScanningFindings,
|
||||
TSecretScanningFindingsInsert,
|
||||
TSecretScanningFindingsUpdate,
|
||||
TSecretScanningGitRisks,
|
||||
TSecretScanningGitRisksInsert,
|
||||
TSecretScanningGitRisksUpdate,
|
||||
TSecretScanningResources,
|
||||
TSecretScanningResourcesInsert,
|
||||
TSecretScanningResourcesUpdate,
|
||||
TSecretScanningScans,
|
||||
TSecretScanningScansInsert,
|
||||
TSecretScanningScansUpdate,
|
||||
TSecretSharing,
|
||||
TSecretSharingInsert,
|
||||
TSecretSharingUpdate,
|
||||
@ -1074,5 +1086,25 @@ declare module "knex/types/tables" {
|
||||
TGithubOrgSyncConfigsInsert,
|
||||
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
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -107,7 +107,11 @@ export * from "./secret-rotation-outputs";
|
||||
export * from "./secret-rotation-v2-secret-mappings";
|
||||
export * from "./secret-rotations";
|
||||
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-resources";
|
||||
export * from "./secret-scanning-scans";
|
||||
export * from "./secret-sharing";
|
||||
export * from "./secret-snapshot-folders";
|
||||
export * from "./secret-snapshot-secrets";
|
||||
|
@ -155,7 +155,11 @@ export enum TableName {
|
||||
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
|
||||
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
|
||||
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";
|
||||
@ -244,7 +248,8 @@ export enum ProjectType {
|
||||
SecretManager = "secret-manager",
|
||||
CertificateManager = "cert-manager",
|
||||
KMS = "kms",
|
||||
SSH = "ssh"
|
||||
SSH = "ssh",
|
||||
SecretScanning = "secret-scanning"
|
||||
}
|
||||
|
||||
export enum ActionProjectType {
|
||||
@ -252,6 +257,7 @@ export enum ActionProjectType {
|
||||
CertificateManager = ProjectType.CertificateManager,
|
||||
KMS = ProjectType.KMS,
|
||||
SSH = ProjectType.SSH,
|
||||
SecretScanning = ProjectType.SecretScanning,
|
||||
// project operations that happen on all types
|
||||
Any = "any"
|
||||
}
|
||||
|
@ -34,7 +34,9 @@ export const OrganizationsSchema = z.object({
|
||||
kmsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
sshProductEnabled: 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>;
|
||||
|
31
backend/src/db/schemas/secret-scanning-data-sources.ts
Normal file
31
backend/src/db/schemas/secret-scanning-data-sources.ts
Normal 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>
|
||||
>;
|
32
backend/src/db/schemas/secret-scanning-findings.ts
Normal file
32
backend/src/db/schemas/secret-scanning-findings.ts
Normal 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>
|
||||
>;
|
24
backend/src/db/schemas/secret-scanning-resources.ts
Normal file
24
backend/src/db/schemas/secret-scanning-resources.ts
Normal 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>
|
||||
>;
|
21
backend/src/db/schemas/secret-scanning-scans.ts
Normal file
21
backend/src/db/schemas/secret-scanning-scans.ts
Normal 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>>;
|
@ -2,6 +2,10 @@ import {
|
||||
registerSecretRotationV2Router,
|
||||
SECRET_ROTATION_REGISTER_ROUTER_MAP
|
||||
} 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 { registerProjectRoleRouter } from "./project-role-router";
|
||||
@ -31,4 +35,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ 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" }
|
||||
);
|
||||
};
|
||||
|
@ -126,7 +126,7 @@ export const registerSecretRotationEndpoints = <
|
||||
const { rotationId } = req.params;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.findSecretRotationById(
|
||||
{ rotationId, type },
|
||||
{ sourceId: rotationId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
@ -136,7 +136,7 @@ export const registerSecretRotationEndpoints = <
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId,
|
||||
sourceId: rotationId,
|
||||
type,
|
||||
secretPath: secretRotation.folder.path,
|
||||
environment: secretRotation.environment.slug
|
||||
@ -202,7 +202,7 @@ export const registerSecretRotationEndpoints = <
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId: secretRotation.id,
|
||||
sourceId: secretRotation.id,
|
||||
type,
|
||||
secretPath,
|
||||
environment
|
||||
@ -244,7 +244,7 @@ export const registerSecretRotationEndpoints = <
|
||||
event: {
|
||||
type: EventType.CREATE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId: secretRotation.id,
|
||||
sourceId: secretRotation.id,
|
||||
type,
|
||||
...req.body
|
||||
}
|
||||
@ -278,7 +278,7 @@ export const registerSecretRotationEndpoints = <
|
||||
const { rotationId } = req.params;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.updateSecretRotation(
|
||||
{ ...req.body, rotationId, type },
|
||||
{ ...req.body, sourceId: rotationId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
@ -288,7 +288,7 @@ export const registerSecretRotationEndpoints = <
|
||||
event: {
|
||||
type: EventType.UPDATE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId,
|
||||
sourceId: rotationId,
|
||||
type,
|
||||
...req.body
|
||||
}
|
||||
@ -332,7 +332,7 @@ export const registerSecretRotationEndpoints = <
|
||||
const { deleteSecrets, revokeGeneratedCredentials } = req.query;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.deleteSecretRotation(
|
||||
{ type, rotationId, deleteSecrets, revokeGeneratedCredentials },
|
||||
{ type, sourceId: rotationId, deleteSecrets, revokeGeneratedCredentials },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
@ -343,7 +343,7 @@ export const registerSecretRotationEndpoints = <
|
||||
type: EventType.DELETE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
type,
|
||||
rotationId,
|
||||
sourceId: rotationId,
|
||||
deleteSecrets,
|
||||
revokeGeneratedCredentials
|
||||
}
|
||||
@ -385,7 +385,7 @@ export const registerSecretRotationEndpoints = <
|
||||
secretRotation: { activeIndex, projectId, folder, environment }
|
||||
} = await server.services.secretRotationV2.findSecretRotationGeneratedCredentialsById(
|
||||
{
|
||||
rotationId,
|
||||
sourceId: rotationId,
|
||||
type
|
||||
},
|
||||
req.permission
|
||||
@ -398,14 +398,14 @@ export const registerSecretRotationEndpoints = <
|
||||
type: EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS,
|
||||
metadata: {
|
||||
type,
|
||||
rotationId,
|
||||
sourceId: rotationId,
|
||||
secretPath: folder.path,
|
||||
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(
|
||||
{
|
||||
rotationId,
|
||||
sourceId: rotationId,
|
||||
type,
|
||||
auditLogInfo: req.auditLogInfo
|
||||
},
|
||||
|
@ -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
|
||||
});
|
12
backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts
Normal file
12
backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts
Normal 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
|
||||
};
|
@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
@ -9,6 +9,14 @@ import {
|
||||
TSecretRotationV2Raw,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} 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 { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-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",
|
||||
|
||||
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[] = [
|
||||
@ -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 =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -3179,4 +3267,14 @@ export type Event =
|
||||
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
|
||||
| SecretScanningDataSourceListEvent
|
||||
| SecretScanningDataSourceGetEvent
|
||||
| SecretScanningDataSourceCreateEvent
|
||||
| SecretScanningDataSourceUpdateEvent
|
||||
| SecretScanningDataSourceDeleteEvent
|
||||
| SecretScanningDataSourceScanEvent
|
||||
| SecretScanningResourceListEvent
|
||||
| SecretScanningScanListEvent
|
||||
| SecretScanningFindingListEvent
|
||||
| SecretScanningFindingUpdateEvent;
|
||||
|
@ -54,7 +54,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false,
|
||||
sshHostGroups: false
|
||||
sshHostGroups: false,
|
||||
secretScanning: true
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -72,6 +72,7 @@ export type TFeatureSet = {
|
||||
kmip: false;
|
||||
gateway: false;
|
||||
sshHostGroups: false;
|
||||
secretScanning: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -12,6 +12,8 @@ import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretScanningDataSourceActions,
|
||||
ProjectPermissionSecretScanningFindingActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSshHostActions,
|
||||
@ -198,6 +200,24 @@ const buildAdminPermissionRules = () => {
|
||||
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;
|
||||
};
|
||||
|
||||
@ -378,6 +398,24 @@ const buildMemberPermissionRules = () => {
|
||||
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;
|
||||
};
|
||||
|
||||
@ -413,6 +451,17 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretScanningDataSourceActions.Read,
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadResources
|
||||
],
|
||||
ProjectPermissionSub.SecretScanningDataSources
|
||||
);
|
||||
|
||||
can([ProjectPermissionSecretScanningFindingActions.Read], ProjectPermissionSub.SecretScanningFindings);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@ -123,6 +123,21 @@ export enum ProjectPermissionKmipActions {
|
||||
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 {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
@ -158,7 +173,9 @@ export enum ProjectPermissionSub {
|
||||
Kms = "kms",
|
||||
Cmek = "cmek",
|
||||
SecretSyncs = "secret-syncs",
|
||||
Kmip = "kmip"
|
||||
Kmip = "kmip",
|
||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||
SecretScanningFindings = "secret-scanning-findings"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@ -281,7 +298,9 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, 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_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
@ -602,6 +621,20 @@ const GeneralPermissionSchema = [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
|
||||
"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."
|
||||
)
|
||||
})
|
||||
];
|
||||
|
||||
|
@ -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
|
@ -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
|
||||
};
|
||||
};
|
@ -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
|
||||
});
|
@ -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
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export * from "./github-secret-scanning-constants";
|
||||
export * from "./github-secret-scanning-schemas";
|
||||
export * from "./github-secret-scanning-types";
|
@ -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
|
||||
};
|
||||
};
|
@ -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"
|
||||
}
|
@ -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
|
||||
};
|
@ -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)}...`;
|
||||
};
|
@ -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" }
|
||||
};
|
@ -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
|
||||
};
|
||||
};
|
@ -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
|
||||
});
|
@ -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)
|
||||
};
|
||||
};
|
@ -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;
|
@ -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
|
||||
]);
|
@ -9,6 +9,7 @@ export type SecretMatch = {
|
||||
Match: string;
|
||||
Secret: string;
|
||||
File: string;
|
||||
Link: string;
|
||||
SymlinkFile: string;
|
||||
Commit: string;
|
||||
Entropy: number;
|
||||
|
@ -3,6 +3,12 @@ import {
|
||||
SECRET_ROTATION_CONNECTION_MAP,
|
||||
SECRET_ROTATION_NAME_MAP
|
||||
} 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 { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@ -55,7 +61,8 @@ export enum ApiDocsTags {
|
||||
SshHostGroups = "SSH Host Groups",
|
||||
KmsKeys = "KMS Keys",
|
||||
KmsEncryption = "KMS Encryption",
|
||||
KmsSigning = "KMS Signing"
|
||||
KmsSigning = "KMS Signing",
|
||||
SecretScanning = "Secret Scanning"
|
||||
}
|
||||
|
||||
export const GROUPS = {
|
||||
@ -2084,6 +2091,10 @@ export const AppConnections = {
|
||||
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.",
|
||||
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."
|
||||
}
|
||||
};
|
||||
|
@ -215,6 +215,14 @@ const envSchema = z
|
||||
INF_APP_CONNECTION_GITHUB_APP_SLUG: 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
|
||||
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
|
||||
|
||||
|
@ -179,13 +179,18 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
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 {
|
||||
if (!data.length) return [];
|
||||
const res = await (tx || db)(tableName)
|
||||
.insert(data as never)
|
||||
.onConflict(onConflictField as never)
|
||||
.merge()
|
||||
.merge(mergeColumns)
|
||||
.returning("*");
|
||||
return res;
|
||||
} catch (error) {
|
||||
|
@ -12,6 +12,10 @@ import {
|
||||
TScanFullRepoEventPayload,
|
||||
TScanPushEventPayload
|
||||
} 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 { logger } from "@app/lib/logger";
|
||||
import {
|
||||
@ -51,7 +55,8 @@ export enum QueueName {
|
||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||
SecretRotationV2 = "secret-rotation-v2",
|
||||
InvalidateCache = "invalidate-cache"
|
||||
InvalidateCache = "invalidate-cache",
|
||||
SecretScanningV2 = "secret-scanning-v2"
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@ -84,7 +89,9 @@ export enum QueueJobs {
|
||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||
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 = {
|
||||
@ -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>;
|
||||
|
63
backend/src/server/plugins/secret-scanner-v2.ts
Normal file
63
backend/src/server/plugins/secret-scanner-v2.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
};
|
@ -86,6 +86,10 @@ import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning
|
||||
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
|
||||
import { secretScanningQueueFactory } from "@app/ee/services/secret-scanning/secret-scanning-queue";
|
||||
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 { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-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 { TQueueServiceFactory } from "@app/queue";
|
||||
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 { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
@ -299,6 +304,9 @@ export const registerRoutes = async (
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
|
||||
await server.register(registerSecretScanningV2Webhooks, {
|
||||
prefix: SECRET_SCANNING_WEBHOOK_PATH
|
||||
});
|
||||
|
||||
// db layers
|
||||
const userDAL = userDALFactory(db);
|
||||
@ -444,6 +452,7 @@ export const registerRoutes = async (
|
||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||
const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db);
|
||||
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
|
||||
const secretScanningV2DAL = secretScanningV2DALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
@ -1694,6 +1703,28 @@ export const registerRoutes = async (
|
||||
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();
|
||||
|
||||
// setup the communication with license key server
|
||||
@ -1805,7 +1836,8 @@ export const registerRoutes = async (
|
||||
secretRotationV2: secretRotationV2Service,
|
||||
microsoftTeams: microsoftTeamsService,
|
||||
assumePrivileges: assumePrivilegeService,
|
||||
githubOrgSync: githubOrgSyncConfigService
|
||||
githubOrgSync: githubOrgSyncConfigService,
|
||||
secretScanningV2: secretScanningV2Service
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@ -28,6 +28,10 @@ import {
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||
import {
|
||||
GitHubRadarConnectionListItemSchema,
|
||||
SanitizedGitHubRadarConnectionSchema
|
||||
} from "@app/services/app-connection/github-radar";
|
||||
import {
|
||||
HCVaultConnectionListItemSchema,
|
||||
SanitizedHCVaultConnectionSchema
|
||||
@ -62,6 +66,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAwsConnectionSchema.options,
|
||||
...SanitizedGitHubConnectionSchema.options,
|
||||
...SanitizedGitHubRadarConnectionSchema.options,
|
||||
...SanitizedGcpConnectionSchema.options,
|
||||
...SanitizedAzureKeyVaultConnectionSchema.options,
|
||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||
@ -84,6 +89,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AwsConnectionListItemSchema,
|
||||
GitHubConnectionListItemSchema,
|
||||
GitHubRadarConnectionListItemSchema,
|
||||
GcpConnectionListItemSchema,
|
||||
AzureKeyVaultConnectionListItemSchema,
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
|
@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
@ -9,6 +9,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router";
|
||||
import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-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.GitHub]: registerGitHubConnectionRouter,
|
||||
[AppConnection.GitHubRadar]: registerGitHubRadarConnectionRouter,
|
||||
[AppConnection.GCP]: registerGcpConnectionRouter,
|
||||
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
|
||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||
|
@ -160,7 +160,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
.default("false")
|
||||
.transform((value) => value === "true"),
|
||||
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()
|
||||
}),
|
||||
response: {
|
||||
|
@ -1,5 +1,6 @@
|
||||
export enum AppConnection {
|
||||
GitHub = "github",
|
||||
GitHubRadar = "github-radar",
|
||||
AWS = "aws",
|
||||
Databricks = "databricks",
|
||||
GCP = "gcp",
|
||||
|
@ -2,6 +2,11 @@ import { TAppConnections } from "@app/db/schemas/app-connections";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
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 {
|
||||
transferSqlConnectionCredentialsToPlatform,
|
||||
validateSqlConnectionCredentials
|
||||
@ -77,6 +82,7 @@ export const listAppConnectionOptions = () => {
|
||||
return [
|
||||
getAwsConnectionListItem(),
|
||||
getGitHubConnectionListItem(),
|
||||
getGitHubRadarConnectionListItem(),
|
||||
getGcpConnectionListItem(),
|
||||
getAzureKeyVaultConnectionListItem(),
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
@ -146,6 +152,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitHubRadar]: validateGitHubRadarConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureAppConfiguration]:
|
||||
@ -173,6 +180,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
return "GitHub App";
|
||||
case GitHubRadarConnectionMethod.App:
|
||||
return "GitHub App";
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
@ -240,6 +249,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.AWS]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Databricks]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitHub]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitHubRadar]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||
|
@ -3,6 +3,7 @@ import { AppConnection } from "./app-connection-enums";
|
||||
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AWS]: "AWS",
|
||||
[AppConnection.GitHub]: "GitHub",
|
||||
[AppConnection.GitHubRadar]: "GitHub Radar",
|
||||
[AppConnection.GCP]: "GCP",
|
||||
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
@ -19,5 +20,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.HCVault]: "Hashicorp Vault",
|
||||
[AppConnection.LDAP]: "LDAP",
|
||||
[AppConnection.TeamCity]: "TeamCity",
|
||||
[AppConnection.OCI]: "OCI"
|
||||
[AppConnection.OCI]: "OCI",
|
||||
[AppConnection.GitLab]: "GitLab"
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
validateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-fns";
|
||||
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 { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
@ -43,6 +44,7 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar";
|
||||
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
|
||||
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
|
||||
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
@ -72,6 +74,7 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentialsSchema> = {
|
||||
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
|
||||
[AppConnection.GitHubRadar]: ValidateGitHubRadarConnectionCredentialsSchema,
|
||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
||||
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
@ -456,6 +459,7 @@ export const appConnectionServiceFactory = ({
|
||||
connectAppConnectionById,
|
||||
listAvailableAppConnectionsForUser,
|
||||
github: githubConnectionService(connectAppConnectionById),
|
||||
githubRadar: githubRadarConnectionService(connectAppConnectionById),
|
||||
gcp: gcpConnectionService(connectAppConnectionById),
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
aws: awsConnectionService(connectAppConnectionById),
|
||||
|
@ -57,6 +57,12 @@ import {
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentialsSchema
|
||||
} from "./github";
|
||||
import {
|
||||
TGitHubRadarConnection,
|
||||
TGitHubRadarConnectionConfig,
|
||||
TGitHubRadarConnectionInput,
|
||||
TValidateGitHubRadarConnectionCredentialsSchema
|
||||
} from "./github-radar";
|
||||
import {
|
||||
THCVaultConnection,
|
||||
THCVaultConnectionConfig,
|
||||
@ -115,6 +121,7 @@ import {
|
||||
export type TAppConnection = { id: string } & (
|
||||
| TAwsConnection
|
||||
| TGitHubConnection
|
||||
| TGitHubRadarConnection
|
||||
| TGcpConnection
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
@ -141,6 +148,7 @@ export type TSqlConnection = TPostgresConnection | TMsSqlConnection;
|
||||
export type TAppConnectionInput = { id: string } & (
|
||||
| TAwsConnectionInput
|
||||
| TGitHubConnectionInput
|
||||
| TGitHubRadarConnectionInput
|
||||
| TGcpConnectionInput
|
||||
| TAzureKeyVaultConnectionInput
|
||||
| TAzureAppConfigurationConnectionInput
|
||||
@ -174,6 +182,7 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
|
||||
export type TAppConnectionConfig =
|
||||
| TAwsConnectionConfig
|
||||
| TGitHubConnectionConfig
|
||||
| TGitHubRadarConnectionConfig
|
||||
| TGcpConnectionConfig
|
||||
| TAzureKeyVaultConnectionConfig
|
||||
| TAzureAppConfigurationConnectionConfig
|
||||
@ -194,6 +203,7 @@ export type TAppConnectionConfig =
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
| TValidateGitHubConnectionCredentialsSchema
|
||||
| TValidateGitHubRadarConnectionCredentialsSchema
|
||||
| TValidateGcpConnectionCredentialsSchema
|
||||
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
|
@ -0,0 +1,3 @@
|
||||
export enum GitHubRadarConnectionMethod {
|
||||
App = "github-app"
|
||||
}
|
@ -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}`
|
||||
});
|
||||
}
|
||||
};
|
@ -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()
|
||||
});
|
@ -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
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
@ -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";
|
@ -8,6 +8,7 @@ import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
@ -29,7 +30,6 @@ import {
|
||||
TGetCertPrivateKeyDTO,
|
||||
TRevokeCertDTO
|
||||
} from "./certificate-types";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
|
||||
type TCertificateServiceFactoryDep = {
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/github-radar/available"
|
||||
---
|
@ -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>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/github-radar/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/github-radar/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/github-radar/connection-name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/github-radar"
|
||||
---
|
@ -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>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/secret-scanning/data-sources"
|
||||
---
|
@ -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 |
BIN
docs/images/app-connections/gitlab/gitlab-add-access-token.png
Normal file
BIN
docs/images/app-connections/gitlab/gitlab-add-access-token.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 497 KiB |
BIN
docs/images/app-connections/gitlab/gitlab-copy-token.png
Normal file
BIN
docs/images/app-connections/gitlab/gitlab-copy-token.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 540 KiB |
Binary file not shown.
After Width: | Height: | Size: 592 KiB |
BIN
docs/images/app-connections/gitlab/select-gitlab-connection.png
Normal file
BIN
docs/images/app-connections/gitlab/select-gitlab-connection.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 863 KiB |
@ -3,7 +3,7 @@ title: "TeamCity Connection"
|
||||
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
|
||||
|
||||
|
@ -3,7 +3,7 @@ title: "Vercel Connection"
|
||||
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
|
||||
|
||||
|
@ -3,7 +3,7 @@ title: "Windmill Connection"
|
||||
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
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Infisical",
|
||||
"openapi": "https://app.infisical.com/api/docs/json",
|
||||
"openapi": "http://localhost:8080/api/docs/json",
|
||||
"logo": {
|
||||
"dark": "/logo/dark.svg",
|
||||
"light": "/logo/light.svg",
|
||||
@ -478,6 +478,7 @@
|
||||
"integrations/app-connections/databricks",
|
||||
"integrations/app-connections/gcp",
|
||||
"integrations/app-connections/github",
|
||||
"integrations/app-connections/github-radar",
|
||||
"integrations/app-connections/hashicorp-vault",
|
||||
"integrations/app-connections/humanitec",
|
||||
"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",
|
||||
"pages": [
|
||||
@ -1148,6 +1156,18 @@
|
||||
"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",
|
||||
"pages": [
|
||||
|
1
frontend/public/lotties/search.json
Normal file
1
frontend/public/lotties/search.json
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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't see the repository you'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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceConfigFields";
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceReviewFields";
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceConfigReviewSection";
|
1
frontend/src/components/secret-scanning/forms/index.ts
Normal file
1
frontend/src/components/secret-scanning/forms/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceForm";
|
@ -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
Reference in New Issue
Block a user