feature: secret scanning pt2 and address initial feedback
@@ -23,6 +23,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
t.boolean("isDisconnected").notNullable().defaultTo(false);
|
||||
t.unique(["projectId", "name"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
|
||||
|
||||
@@ -21,7 +21,8 @@ export const SecretScanningDataSourcesSchema = z.object({
|
||||
isAutoScanEnabled: z.boolean().default(true).nullable().optional(),
|
||||
projectId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isDisconnected: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TSecretScanningDataSources = z.infer<typeof SecretScanningDataSourcesSchema>;
|
||||
|
||||
@@ -298,7 +298,7 @@ export const registerSecretScanningEndpoints = <
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const dataSource = (await server.services.secretScanningV2.deleteSecretScanningResource(
|
||||
const dataSource = (await server.services.secretScanningV2.deleteSecretScanningDataSource(
|
||||
{ type, dataSourceId },
|
||||
req.permission
|
||||
)) as T;
|
||||
@@ -349,7 +349,7 @@ export const registerSecretScanningEndpoints = <
|
||||
...req.auditLogInfo,
|
||||
projectId: dataSource.projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN,
|
||||
metadata: {
|
||||
type,
|
||||
dataSourceId
|
||||
@@ -392,7 +392,7 @@ export const registerSecretScanningEndpoints = <
|
||||
...req.auditLogInfo,
|
||||
projectId: dataSource.projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN,
|
||||
metadata: {
|
||||
type,
|
||||
dataSourceId,
|
||||
|
||||
@@ -140,12 +140,12 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: "Update the resolve status of the specified Secret Scanning Finding.",
|
||||
description: "Update 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),
|
||||
status: z.nativeEnum(SecretScanningFindingStatus).optional().describe(SecretScanningFindings.UPDATE.status),
|
||||
remarks: z.string().nullish().describe(SecretScanningFindings.UPDATE.remarks)
|
||||
}),
|
||||
response: {
|
||||
|
||||
@@ -9,7 +9,11 @@ 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 {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningScanStatus,
|
||||
SecretScanningScanType
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import {
|
||||
TCreateSecretScanningDataSourceDTO,
|
||||
TDeleteSecretScanningDataSourceDTO,
|
||||
@@ -390,6 +394,7 @@ export enum EventType {
|
||||
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_TRIGGER_SCAN = "secret-scanning-data-source-trigger-scan",
|
||||
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",
|
||||
@@ -2964,9 +2969,23 @@ interface SecretScanningDataSourceDeleteEvent {
|
||||
metadata: TDeleteSecretScanningDataSourceDTO;
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceTriggerScanEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN;
|
||||
metadata: TTriggerSecretScanningDataSourceDTO;
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceScanEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN;
|
||||
metadata: TTriggerSecretScanningDataSourceDTO;
|
||||
metadata: {
|
||||
scanId: string;
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
dataSourceId: string;
|
||||
dataSourceType: string;
|
||||
scanStatus: SecretScanningScanStatus;
|
||||
scanType: SecretScanningScanType;
|
||||
numberOfSecretsDetected?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningResourceListEvent {
|
||||
@@ -3273,6 +3292,7 @@ export type Event =
|
||||
| SecretScanningDataSourceCreateEvent
|
||||
| SecretScanningDataSourceUpdateEvent
|
||||
| SecretScanningDataSourceDeleteEvent
|
||||
| SecretScanningDataSourceTriggerScanEvent
|
||||
| SecretScanningDataSourceScanEvent
|
||||
| SecretScanningResourceListEvent
|
||||
| SecretScanningScanListEvent
|
||||
|
||||
@@ -214,7 +214,7 @@ const buildAdminPermissionRules = () => {
|
||||
);
|
||||
|
||||
can(
|
||||
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Resolve],
|
||||
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Update],
|
||||
ProjectPermissionSub.SecretScanningFindings
|
||||
);
|
||||
|
||||
@@ -400,9 +400,6 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretScanningDataSourceActions.Create,
|
||||
ProjectPermissionSecretScanningDataSourceActions.Edit,
|
||||
ProjectPermissionSecretScanningDataSourceActions.Delete,
|
||||
ProjectPermissionSecretScanningDataSourceActions.Read,
|
||||
ProjectPermissionSecretScanningDataSourceActions.TriggerScans,
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
|
||||
@@ -412,7 +409,7 @@ const buildMemberPermissionRules = () => {
|
||||
);
|
||||
|
||||
can(
|
||||
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Resolve],
|
||||
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Update],
|
||||
ProjectPermissionSub.SecretScanningFindings
|
||||
);
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ export enum ProjectPermissionSecretScanningDataSourceActions {
|
||||
|
||||
export enum ProjectPermissionSecretScanningFindingActions {
|
||||
Read = "read-findings",
|
||||
Resolve = "resolve-findings"
|
||||
Update = "update-findings"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSub {
|
||||
|
||||
@@ -4,10 +4,11 @@ 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 {
|
||||
SecretScanningDataSource,
|
||||
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 { cloneRepository } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
|
||||
import {
|
||||
TSecretScanningFactoryGetDiffScanFindingsPayload,
|
||||
TSecretScanningFactoryGetDiffScanResourcePayload,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { titleCaseToCamelCase } from "@app/lib/fn";
|
||||
import { listGitHubRadarRepositories, TGitHubRadarConnection } from "@app/services/app-connection/github-radar";
|
||||
|
||||
import { TGitHubDataSourceWithConnection, TQueueGitHubResourceDiffScan } from "./github-secret-scanning-types";
|
||||
@@ -30,7 +32,8 @@ export const GitHubSecretScanningFactory = () => {
|
||||
const externalId = connection.credentials.installationId;
|
||||
|
||||
const existingDataSource = await secretScanningV2DAL.dataSources.findOne({
|
||||
externalId
|
||||
externalId,
|
||||
type: SecretScanningDataSource.GitHub
|
||||
});
|
||||
|
||||
if (existingDataSource)
|
||||
|
||||
@@ -11,10 +11,15 @@ import {
|
||||
BaseUpdateSecretScanningDataSourceSchema,
|
||||
GitRepositoryScanFindingDetailsSchema
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-schemas";
|
||||
import { SecretScanningDataSources } from "@app/lib/api-docs";
|
||||
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(["*"])
|
||||
includeRepos: z
|
||||
.array(z.string())
|
||||
.nonempty("One or more repositories required")
|
||||
.default(["*"])
|
||||
.describe(SecretScanningDataSources.CONFIG.GITHUB.includeRepos)
|
||||
});
|
||||
|
||||
export const GitHubDataSourceSchema = BaseSecretScanningDataSourceSchema({
|
||||
|
||||
@@ -5,6 +5,8 @@ import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/se
|
||||
import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TGitHubDataSource } from "./github-secret-scanning-types";
|
||||
|
||||
export const githubSecretScanningService = (
|
||||
secretScanningV2DAL: TSecretScanningV2DALFactory,
|
||||
secretScanningV2Queue: Pick<TSecretScanningV2QueueServiceFactory, "queueResourceDiffScan">
|
||||
@@ -21,9 +23,12 @@ export const githubSecretScanningService = (
|
||||
return;
|
||||
}
|
||||
|
||||
// scott: maybe add disabled col instead?
|
||||
await secretScanningV2DAL.resources.delete({
|
||||
dataSourceId: dataSource.id
|
||||
logger.info(
|
||||
`secretScanningV2RemoveEvent: GitHub - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]`
|
||||
);
|
||||
|
||||
await secretScanningV2DAL.dataSources.updateById(dataSource.id, {
|
||||
isDisconnected: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -37,9 +42,10 @@ export const githubSecretScanningService = (
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSource = await secretScanningV2DAL.dataSources.findOne({
|
||||
externalId: String(installation.id)
|
||||
});
|
||||
const dataSource = (await secretScanningV2DAL.dataSources.findOne({
|
||||
externalId: String(installation.id),
|
||||
type: SecretScanningDataSource.GitHub
|
||||
})) as TGitHubDataSource | undefined;
|
||||
|
||||
if (!dataSource) {
|
||||
logger.error(
|
||||
@@ -48,15 +54,33 @@ export const githubSecretScanningService = (
|
||||
return;
|
||||
}
|
||||
|
||||
await secretScanningV2Queue.queueResourceDiffScan({
|
||||
dataSourceType: SecretScanningDataSource.GitHub,
|
||||
payload,
|
||||
dataSourceId: dataSource.id
|
||||
});
|
||||
const {
|
||||
isAutoScanEnabled,
|
||||
config: { includeRepos }
|
||||
} = dataSource;
|
||||
|
||||
if (!isAutoScanEnabled) {
|
||||
logger.info(
|
||||
`secretScanningV2PushEvent: GitHub - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [installationId=${installation.id}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (includeRepos.includes("*") || includeRepos.includes(repository.full_name)) {
|
||||
await secretScanningV2Queue.queueResourceDiffScan({
|
||||
dataSourceType: SecretScanningDataSource.GitHub,
|
||||
payload,
|
||||
dataSourceId: dataSource.id
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`secretScanningV2PushEvent: GitHub - ignoring due to repository not being present in config [installationId=${installation.id}] [dataSourceId=${dataSource.id}]`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handlePushEvent,
|
||||
handleInstallationDeleted: handleInstallationDeletedEvent
|
||||
handleInstallationDeletedEvent
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,4 +27,6 @@ export type TQueueGitHubResourceDiffScan = {
|
||||
dataSourceType: SecretScanningDataSource.GitHub;
|
||||
payload: PushEvent;
|
||||
dataSourceId: string;
|
||||
resourceId: string;
|
||||
scanId: string;
|
||||
};
|
||||
|
||||
@@ -163,7 +163,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
const deleteDataSourceById = async (dataSourceId: string, tx?: Knex) => {
|
||||
const secretRotation = (await baseSecretScanningDataSourceQuery({
|
||||
const dataSource = (await baseSecretScanningDataSourceQuery({
|
||||
filter: { id: dataSourceId },
|
||||
db,
|
||||
tx
|
||||
@@ -171,18 +171,18 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
||||
|
||||
await dataSourceOrm.deleteById(dataSourceId, tx);
|
||||
|
||||
return expandSecretScanningDataSource(secretRotation);
|
||||
return expandSecretScanningDataSource(dataSource);
|
||||
};
|
||||
|
||||
const findOneDataSource = async (filter: Parameters<(typeof dataSourceOrm)["findOne"]>[0], tx?: Knex) => {
|
||||
try {
|
||||
const secretRotation = await baseSecretScanningDataSourceQuery({ filter, db, tx }).first();
|
||||
const dataSource = await baseSecretScanningDataSourceQuery({ filter, db, tx }).first();
|
||||
|
||||
if (secretRotation) {
|
||||
return expandSecretScanningDataSource(secretRotation);
|
||||
if (dataSource) {
|
||||
return expandSecretScanningDataSource(dataSource);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find One - Secret Rotation V2" });
|
||||
throw new DatabaseError({ error, name: "Find One - Secret Scanning Data Source" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -264,7 +264,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find Data Source with Details - Secret Scanning V2" });
|
||||
throw new DatabaseError({ error, name: "Find with Details - Secret Scanning Data Source" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -348,7 +348,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find Resource with Details - Secret Scanning V2" });
|
||||
throw new DatabaseError({ error, name: "Find with Details - Secret Scanning Resource" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -411,7 +411,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find Scan with Details By Data Source ID - Secret Scanning V2" });
|
||||
throw new DatabaseError({ error, name: "Find with Details By Data Source ID - Secret Scanning Scan" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -429,7 +429,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
||||
|
||||
return scans;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find Scan By Data Source ID - Secret Scanning V2" });
|
||||
throw new DatabaseError({ error, name: "Find By Data Source ID - Secret Scanning Scan" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { titleCaseToCamelCase } from "@app/lib/fn";
|
||||
|
||||
import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums";
|
||||
import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListItem } from "./secret-scanning-v2-types";
|
||||
@@ -42,27 +43,6 @@ export function scanDirectory(inputPath: string, outputPath: string): Promise<vo
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { join } from "path";
|
||||
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
createTempFolder,
|
||||
deleteTempFolder
|
||||
@@ -8,12 +11,18 @@ import {
|
||||
parseScanErrorMessage,
|
||||
scanGitRepositoryAndGetFindings
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
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 { ActorType } from "@app/services/auth/auth-type";
|
||||
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 { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
|
||||
import {
|
||||
@@ -28,16 +37,19 @@ import {
|
||||
TFindingsPayload,
|
||||
TQueueSecretScanningDataSourceFullScan,
|
||||
TQueueSecretScanningResourceDiffScan,
|
||||
TQueueSecretScanningSendNotification,
|
||||
TSecretScanningDataSourceWithConnection
|
||||
} from "./secret-scanning-v2-types";
|
||||
|
||||
type TSecretRotationV2QueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
secretScanningV2DAL: TSecretScanningV2DALFactory;
|
||||
// smtpService: Pick<TSmtpService, "sendMail">;
|
||||
// projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
// projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
||||
};
|
||||
|
||||
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
|
||||
@@ -45,10 +57,12 @@ export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof sec
|
||||
export const secretScanningV2QueueServiceFactory = async ({
|
||||
queueService,
|
||||
secretScanningV2DAL,
|
||||
// projectMembershipDAL,
|
||||
// projectDAL,
|
||||
// smtpService,
|
||||
kmsService
|
||||
projectMembershipDAL,
|
||||
projectDAL,
|
||||
smtpService,
|
||||
kmsService,
|
||||
auditLogService,
|
||||
keyStore
|
||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||
const queueDataSourceFullScan = async (
|
||||
dataSource: TSecretScanningDataSourceWithConnection,
|
||||
@@ -63,7 +77,7 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
|
||||
let filteredRawResources = rawResources;
|
||||
|
||||
// TODO: should add indivial resource fetch to factory
|
||||
// TODO: should add individual resource fetch to factory
|
||||
if (resourceExternalId) {
|
||||
filteredRawResources = rawResources.filter((resource) => resource.externalId === resourceExternalId);
|
||||
}
|
||||
@@ -74,6 +88,13 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
});
|
||||
}
|
||||
|
||||
for (const resource of filteredRawResources) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (await keyStore.getItem(KeyStorePrefixes.SecretScanningLock(dataSource.id, resource.externalId))) {
|
||||
throw new BadRequestError({ message: `A scan is already in progress for resource "${resource.name}"` });
|
||||
}
|
||||
}
|
||||
|
||||
await secretScanningV2DAL.resources.transaction(async (tx) => {
|
||||
const resources = await secretScanningV2DAL.resources.upsert(
|
||||
filteredRawResources.map((rawResource) => ({
|
||||
@@ -110,9 +131,6 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
}
|
||||
};
|
||||
|
||||
const queueResourceDiffScan = async (payload: TQueueSecretScanningResourceDiffScan) =>
|
||||
queueService.queuePg(QueueJobs.SecretScanningV2DiffScan, payload);
|
||||
|
||||
await queueService.startPg<QueueName.SecretScanningV2>(
|
||||
QueueJobs.SecretScanningV2FullScan,
|
||||
async ([job]) => {
|
||||
@@ -123,7 +141,26 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
|
||||
const tempFolder = await createTempFolder();
|
||||
|
||||
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 lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
||||
|
||||
try {
|
||||
try {
|
||||
lock = await keyStore.acquireLock(
|
||||
[KeyStorePrefixes.SecretScanningLock(dataSource.id, resource.externalId)],
|
||||
60 * 1000 * 5
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error("Failed to acquire scanning lock.");
|
||||
}
|
||||
|
||||
await secretScanningV2DAL.scans.update(
|
||||
{ id: scanId },
|
||||
{
|
||||
@@ -131,14 +168,6 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
@@ -165,110 +194,196 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
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,
|
||||
if (findingsPayload.length) {
|
||||
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,
|
||||
statusMessage: null
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
||||
status: SecretScanningScanStatus.Completed,
|
||||
resourceName: resource.name,
|
||||
isDiffScan: false,
|
||||
dataSource,
|
||||
numberOfSecrets: findingsPayload.length,
|
||||
scanId
|
||||
});
|
||||
}
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: dataSource.projectId,
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
|
||||
metadata: {
|
||||
dataSourceId: dataSource.id,
|
||||
dataSourceType: dataSource.type,
|
||||
resourceName: resource.name,
|
||||
resourceId: resource.id,
|
||||
resourceType: resource.type,
|
||||
scanId,
|
||||
status: SecretScanningFindingStatus.Unresolved
|
||||
})),
|
||||
["projectId", "fingerprint"],
|
||||
tx,
|
||||
["resourceName", "dataSourceName", "status"]
|
||||
);
|
||||
scanStatus: SecretScanningScanStatus.Completed,
|
||||
scanType: SecretScanningScanType.FullScan,
|
||||
numberOfSecretsDetected: findingsPayload.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`secretScanningV2Queue: Full Scan Complete ${logDetails} findings=[${findingsPayload.length}]`);
|
||||
} catch (error) {
|
||||
if (retryCount === retryLimit) {
|
||||
const errorMessage = parseScanErrorMessage(error);
|
||||
|
||||
await secretScanningV2DAL.scans.update(
|
||||
{ id: scanId },
|
||||
{
|
||||
status: SecretScanningScanStatus.Completed
|
||||
status: SecretScanningScanStatus.Failed,
|
||||
statusMessage: errorMessage
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: send notification
|
||||
|
||||
logger.info(`secretScanningV2Queue: Full Scan Complete ${logDetails}`);
|
||||
} catch (error) {
|
||||
await secretScanningV2DAL.scans.update(
|
||||
{ id: scanId },
|
||||
{
|
||||
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
||||
status: SecretScanningScanStatus.Failed,
|
||||
statusMessage: parseScanErrorMessage(error)
|
||||
}
|
||||
);
|
||||
resourceName: resource.name,
|
||||
dataSource,
|
||||
errorMessage
|
||||
});
|
||||
|
||||
// TODO: send error notification
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: dataSource.projectId,
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
|
||||
metadata: {
|
||||
dataSourceId: dataSource.id,
|
||||
dataSourceType: dataSource.type,
|
||||
resourceId: resource.id,
|
||||
resourceType: resource.type,
|
||||
scanId,
|
||||
scanStatus: SecretScanningScanStatus.Failed,
|
||||
scanType: SecretScanningScanType.FullScan
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(error, `secretScanningV2Queue: Full Scan Failed ${logDetails}`);
|
||||
throw error;
|
||||
} finally {
|
||||
await deleteTempFolder(tempFolder);
|
||||
await lock?.release();
|
||||
}
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
workerCount: 2,
|
||||
workerCount: 20,
|
||||
pollingIntervalSeconds: 1
|
||||
}
|
||||
);
|
||||
|
||||
const queueResourceDiffScan = async ({
|
||||
payload,
|
||||
dataSourceId,
|
||||
dataSourceType
|
||||
}: Pick<TQueueSecretScanningResourceDiffScan, "payload" | "dataSourceId" | "dataSourceType">) => {
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]();
|
||||
|
||||
const resourcePayload = factory.getDiffScanResourcePayload(payload);
|
||||
|
||||
try {
|
||||
const { resourceId, scanId } = 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
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return {
|
||||
resourceId: resource.id,
|
||||
scanId: scan.id
|
||||
};
|
||||
});
|
||||
|
||||
await queueService.queuePg(QueueJobs.SecretScanningV2DiffScan, {
|
||||
payload,
|
||||
dataSourceId,
|
||||
dataSourceType,
|
||||
scanId,
|
||||
resourceId
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`secretScanningV2Queue: Failed to queue diff scan [dataSourceId=${dataSourceId}] [resourceExternalId=${resourcePayload.externalId}]`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await queueService.startPg<QueueName.SecretScanningV2>(
|
||||
QueueJobs.SecretScanningV2DiffScan,
|
||||
async ([job]) => {
|
||||
const { payload, dataSourceId } = job.data as TQueueSecretScanningResourceDiffScan;
|
||||
const { payload, dataSourceId, resourceId, scanId } = job.data as TQueueSecretScanningResourceDiffScan;
|
||||
const { retryCount, retryLimit } = job;
|
||||
|
||||
let scanId: string | undefined;
|
||||
let logDetails = `[dataSourceId=${dataSourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
|
||||
const logDetails = `[dataSourceId=${dataSourceId}] [scanId=${scanId}] [resourceId=${resourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
|
||||
|
||||
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`);
|
||||
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
|
||||
|
||||
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
|
||||
};
|
||||
await secretScanningV2DAL.scans.update(
|
||||
{ id: scanId },
|
||||
{
|
||||
status: SecretScanningScanStatus.Scanning
|
||||
}
|
||||
);
|
||||
|
||||
logDetails += ` [scanId=${scanId}] [resourceId=${resourceId}]`;
|
||||
|
||||
let connection: TAppConnection | null = null;
|
||||
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
|
||||
|
||||
@@ -277,49 +392,107 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
...dataSource,
|
||||
connection
|
||||
} as TSecretScanningDataSourceWithConnection,
|
||||
resourceName,
|
||||
resourceName: resource.name,
|
||||
payload
|
||||
});
|
||||
|
||||
await secretScanningV2DAL.findings.transaction(async (tx) => {
|
||||
await secretScanningV2DAL.findings.upsert(
|
||||
findingsPayload.map((findings) => ({
|
||||
...findings,
|
||||
projectId: dataSource.projectId,
|
||||
dataSourceName: dataSource.name,
|
||||
if (findingsPayload.length) {
|
||||
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
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
||||
status: SecretScanningScanStatus.Completed,
|
||||
resourceName: resource.name,
|
||||
isDiffScan: true,
|
||||
dataSource,
|
||||
numberOfSecrets: findingsPayload.length,
|
||||
scanId
|
||||
});
|
||||
}
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: dataSource.projectId,
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
|
||||
metadata: {
|
||||
dataSourceId: dataSource.id,
|
||||
dataSourceType: dataSource.type,
|
||||
resourceName,
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceType: resource.type,
|
||||
scanId,
|
||||
status: SecretScanningFindingStatus.Unresolved
|
||||
})),
|
||||
["projectId", "fingerprint"],
|
||||
tx,
|
||||
["resourceName", "dataSourceName", "status"]
|
||||
);
|
||||
|
||||
await secretScanningV2DAL.scans.update(
|
||||
{ id: scanId },
|
||||
{
|
||||
status: SecretScanningScanStatus.Completed
|
||||
scanStatus: SecretScanningScanStatus.Completed,
|
||||
scanType: SecretScanningScanType.DiffScan,
|
||||
numberOfSecretsDetected: findingsPayload.length
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: send notification
|
||||
|
||||
logger.info(`secretScanningV2Queue: Diff Scan Complete ${logDetails}`);
|
||||
} catch (error) {
|
||||
if (scanId)
|
||||
if (retryCount === retryLimit) {
|
||||
const errorMessage = parseScanErrorMessage(error);
|
||||
|
||||
await secretScanningV2DAL.scans.update(
|
||||
{ id: scanId },
|
||||
{
|
||||
status: SecretScanningScanStatus.Failed,
|
||||
statusMessage: parseScanErrorMessage(error)
|
||||
statusMessage: errorMessage
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: send error notification
|
||||
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
||||
status: SecretScanningScanStatus.Failed,
|
||||
resourceName: resource.name,
|
||||
dataSource,
|
||||
errorMessage
|
||||
});
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: dataSource.projectId,
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
|
||||
metadata: {
|
||||
dataSourceId: dataSource.id,
|
||||
dataSourceType: dataSource.type,
|
||||
resourceId: resource.id,
|
||||
resourceType: resource.type,
|
||||
scanId,
|
||||
scanStatus: SecretScanningScanStatus.Failed,
|
||||
scanType: SecretScanningScanType.DiffScan
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(error, `secretScanningV2Queue: Diff Scan Failed ${logDetails}`);
|
||||
throw error;
|
||||
@@ -327,67 +500,84 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
workerCount: 2,
|
||||
workerCount: 20,
|
||||
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
|
||||
// }
|
||||
// );
|
||||
await queueService.startPg<QueueName.SecretScanningV2>(
|
||||
QueueJobs.SecretScanningV2SendNotification,
|
||||
async ([job]) => {
|
||||
const { dataSource, resourceName, ...payload } = job.data as TQueueSecretScanningSendNotification;
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (!appCfg.isSmtpConfigured) return;
|
||||
|
||||
try {
|
||||
const { projectId } = dataSource;
|
||||
|
||||
logger.info(
|
||||
`secretScanningV2Queue: Sending Status Notification [dataSourceId=${dataSource.id}] [resourceName=${resourceName}] [status=${payload.status}]`
|
||||
);
|
||||
|
||||
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 timestamp = new Date().toISOString();
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? SmtpTemplates.SecretScanningV2SecretsDetected
|
||||
: SmtpTemplates.SecretScanningV2ScanFailed,
|
||||
subjectLine:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? "Incident Alert: Secret(s) Leaked"
|
||||
: `Secret Scanning Failed`,
|
||||
substitutions:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? {
|
||||
authorName: "Jim",
|
||||
authorEmail: "jim@infisical.com",
|
||||
resourceName,
|
||||
numberOfSecrets: payload.numberOfSecrets,
|
||||
isDiffScan: payload.isDiffScan,
|
||||
url: encodeURI(
|
||||
`${appCfg.SITE_URL}/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
|
||||
),
|
||||
timestamp
|
||||
}
|
||||
: {
|
||||
dataSourceName: dataSource.name,
|
||||
resourceName,
|
||||
projectName: project.name,
|
||||
timestamp,
|
||||
errorMessage: payload.errorMessage,
|
||||
url: encodeURI(
|
||||
`${appCfg.SITE_URL}/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
|
||||
)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`secretScanningV2Queue: Failed to Send Status Notification [dataSourceId=${dataSource.id}] [resourceName=${resourceName}] [status=${payload.status}]`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{
|
||||
batchSize: 1,
|
||||
workerCount: 5,
|
||||
pollingIntervalSeconds: 1
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
queueDataSourceFullScan,
|
||||
|
||||
@@ -49,10 +49,6 @@ export type TSecretScanningV2ServiceFactoryDep = {
|
||||
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"
|
||||
@@ -67,10 +63,6 @@ export const secretScanningV2ServiceFactory = ({
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
licenseService,
|
||||
// auditLogService,
|
||||
// keyStore,
|
||||
// queueService,
|
||||
// appConnectionDAL,
|
||||
secretScanningV2Queue,
|
||||
kmsService
|
||||
}: TSecretScanningV2ServiceFactoryDep) => {
|
||||
@@ -374,7 +366,7 @@ export const secretScanningV2ServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSecretScanningResource = async (
|
||||
const deleteSecretScanningDataSource = async (
|
||||
{ type, dataSourceId }: TDeleteSecretScanningDataSourceDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
@@ -750,7 +742,7 @@ export const secretScanningV2ServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretScanningFindingActions.Resolve,
|
||||
ProjectPermissionSecretScanningFindingActions.Update,
|
||||
ProjectPermissionSub.SecretScanningFindings
|
||||
);
|
||||
|
||||
@@ -770,7 +762,7 @@ export const secretScanningV2ServiceFactory = ({
|
||||
findSecretScanningDataSourceByName,
|
||||
createSecretScanningDataSource,
|
||||
updateSecretScanningDataSource,
|
||||
deleteSecretScanningResource,
|
||||
deleteSecretScanningDataSource,
|
||||
triggerSecretScanningDataSourceScan,
|
||||
listSecretScanningResourcesByDataSourceId,
|
||||
listSecretScanningScansByDataSourceId,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { TSecretScanningFindingsInsert, TSecretScanningResources, TSecretScanningScans } from "@app/db/schemas";
|
||||
import {
|
||||
TSecretScanningDataSources,
|
||||
TSecretScanningFindingsInsert,
|
||||
TSecretScanningResources,
|
||||
TSecretScanningScans
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TGitHubDataSource,
|
||||
TGitHubDataSourceInput,
|
||||
@@ -96,6 +101,14 @@ export type TQueueSecretScanningDataSourceFullScan = {
|
||||
|
||||
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan;
|
||||
|
||||
export type TQueueSecretScanningSendNotification = {
|
||||
dataSource: TSecretScanningDataSources;
|
||||
resourceName: string;
|
||||
} & (
|
||||
| { status: SecretScanningScanStatus.Failed; errorMessage: string }
|
||||
| { status: SecretScanningScanStatus.Completed; numberOfSecrets: number; scanId: string; isDiffScan: boolean }
|
||||
);
|
||||
|
||||
export type TCloneRepository = {
|
||||
cloneUrl: string;
|
||||
repoPath: string;
|
||||
@@ -163,7 +176,7 @@ export type TFindingsPayload = Pick<TSecretScanningFindingsInsert, "details" | "
|
||||
export type TGetFindingsPayload = Promise<TFindingsPayload>;
|
||||
|
||||
export type TUpdateSecretScanningFinding = {
|
||||
status: SecretScanningFindingStatus;
|
||||
status?: SecretScanningFindingStatus;
|
||||
remarks?: string | null;
|
||||
findingId: string;
|
||||
};
|
||||
|
||||
@@ -37,6 +37,8 @@ export const KeyStorePrefixes = {
|
||||
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
||||
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
|
||||
SecretScanningLock: (dataSourceId: string, resourceExternalId: string) =>
|
||||
`secret-scanning-v2-mutex-${dataSourceId}-${resourceExternalId}` as const,
|
||||
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||
`identity-access-token-status:${identityAccessTokenId}`,
|
||||
|
||||
@@ -2408,7 +2408,12 @@ export const SecretScanningDataSources = {
|
||||
}),
|
||||
LIST_SCANS: (type: SecretScanningDataSource) => ({
|
||||
dataSourceId: `The ID of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to list scans for.`
|
||||
})
|
||||
}),
|
||||
CONFIG: {
|
||||
GITHUB: {
|
||||
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const SecretScanningFindings = {
|
||||
@@ -2416,8 +2421,8 @@ export const SecretScanningFindings = {
|
||||
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.",
|
||||
findingId: "The ID of the Secret Scanning Finding to update.",
|
||||
status: "The updated status of the specified Secret Scanning Finding.",
|
||||
remarks: "Remarks pertaining to the resolve status of this finding."
|
||||
remarks: "Remarks pertaining to the status of this finding."
|
||||
}
|
||||
};
|
||||
|
||||
@@ -281,6 +281,13 @@ const envSchema = z
|
||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
||||
isSecretScanningV2Configured:
|
||||
Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_ID) &&
|
||||
Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY) &&
|
||||
Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_SLUG) &&
|
||||
Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID) &&
|
||||
Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET) &&
|
||||
Boolean(data.INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET),
|
||||
isHsmConfigured:
|
||||
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
|
||||
|
||||
|
||||
@@ -32,3 +32,24 @@ export const shake = <RemovedKeys extends string, T = object>(
|
||||
return acc;
|
||||
}, {} as T);
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||
import {
|
||||
TQueueSecretScanningDataSourceFullScan,
|
||||
TQueueSecretScanningResourceDiffScan
|
||||
TQueueSecretScanningResourceDiffScan,
|
||||
TQueueSecretScanningSendNotification
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -91,7 +92,8 @@ export enum QueueJobs {
|
||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||
InvalidateCache = "invalidate-cache",
|
||||
SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
|
||||
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan"
|
||||
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
||||
SecretScanningV2SendNotification = "secret-scanning-v2-notification"
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
@@ -260,6 +262,10 @@ export type TQueueJobTypes = {
|
||||
| {
|
||||
name: QueueJobs.SecretScanningV2DiffScan;
|
||||
payload: TQueueSecretScanningResourceDiffScan;
|
||||
}
|
||||
| {
|
||||
name: QueueJobs.SecretScanningV2SendNotification;
|
||||
payload: TQueueSecretScanningSendNotification;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
|
||||
const { payload } = context;
|
||||
const { installation } = payload;
|
||||
|
||||
await server.services.secretScanningV2.github.handleInstallationDeleted(installation.id);
|
||||
await server.services.secretScanningV2.github.handleInstallationDeletedEvent(installation.id);
|
||||
});
|
||||
|
||||
app.on("installation", async (context) => {
|
||||
@@ -28,6 +28,11 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (!appCfg.isSecretScanningV2Configured) {
|
||||
logger.info("Secret Scanning V2 is not configured. Skipping registration of secret scanning v2 webhooks.");
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -36,6 +41,7 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
|
||||
|
||||
await probot.load(probotApp);
|
||||
|
||||
// github push event webhook
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/github",
|
||||
|
||||
@@ -1703,22 +1703,20 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
|
||||
auditLogService,
|
||||
secretScanningV2DAL,
|
||||
queueService,
|
||||
// projectDAL,
|
||||
// projectMembershipDAL,
|
||||
// smtpService,
|
||||
kmsService
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
smtpService,
|
||||
kmsService,
|
||||
keyStore
|
||||
});
|
||||
|
||||
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
||||
// appConnectionDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
licenseService,
|
||||
// auditLogService,
|
||||
// keyStore,
|
||||
// queueService,
|
||||
secretScanningV2DAL,
|
||||
secretScanningV2Queue,
|
||||
kmsService
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Button, Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface SecretScanningScanFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
dataSourceName: string;
|
||||
resourceName: string;
|
||||
projectName: string;
|
||||
timestamp: string;
|
||||
url: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export const SecretScanningScanFailedTemplate = ({
|
||||
dataSourceName,
|
||||
resourceName,
|
||||
projectName,
|
||||
siteUrl,
|
||||
errorMessage,
|
||||
url,
|
||||
timestamp
|
||||
}: SecretScanningScanFailedTemplateProps) => {
|
||||
return (
|
||||
<BaseEmailWrapper
|
||||
title="Secret Scanning Failed"
|
||||
preview="Infisical encountered an error while attempting to scan for secret leaks."
|
||||
siteUrl={siteUrl}
|
||||
>
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
Infisical encountered an error while attempting to scan the resource <strong>{resourceName}</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<strong>Resource</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{resourceName}</Text>
|
||||
<strong>Data Source</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{dataSourceName}</Text>
|
||||
<strong>Project</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{projectName}</Text>
|
||||
<strong>Timestamp</strong>
|
||||
<Text className="text-[14px] mt-[4px]">{timestamp}</Text>
|
||||
<strong>Error</strong>
|
||||
<Text className="text-[14px] text-red-500 mt-[4px]">{errorMessage}</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={url}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
View in Infisical
|
||||
</Button>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecretScanningScanFailedTemplate;
|
||||
|
||||
SecretScanningScanFailedTemplate.PreviewProps = {
|
||||
dataSourceName: "my-data-source",
|
||||
resourceName: "my-resource",
|
||||
projectName: "my-project",
|
||||
timestamp: "May 3rd 2025, 5:42 pm",
|
||||
url: "https://infisical.com",
|
||||
errorMessage: "401 Unauthorized",
|
||||
siteUrl: "https://infisical.com"
|
||||
} as SecretScanningScanFailedTemplateProps;
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
|
||||
interface SecretScanningSecretsDetectedTemplateProps
|
||||
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||
numberOfSecrets: number;
|
||||
isDiffScan: boolean;
|
||||
authorName?: string;
|
||||
authorEmail?: string;
|
||||
resourceName: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const SecretScanningSecretsDetectedTemplate = ({
|
||||
numberOfSecrets,
|
||||
siteUrl,
|
||||
authorName,
|
||||
authorEmail,
|
||||
isDiffScan,
|
||||
resourceName,
|
||||
url
|
||||
}: SecretScanningSecretsDetectedTemplateProps) => {
|
||||
return (
|
||||
<BaseEmailWrapper
|
||||
title="Incident Alert: Secret(s) Leaked"
|
||||
preview="Infisical uncovered one or more leaked secrets."
|
||||
siteUrl={siteUrl}
|
||||
>
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
Infisical has uncovered <strong>{numberOfSecrets}</strong> secret(s)
|
||||
{isDiffScan ? " from a recent commit to" : " in"} <strong>{resourceName}</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mt-[36px] pt-[8px] pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-[14px]">
|
||||
You are receiving this notification because one or more leaked secrets have been detected
|
||||
{isDiffScan && " in a recent commit"}
|
||||
{isDiffScan ? (
|
||||
(authorName || authorEmail) && (
|
||||
<>
|
||||
{" "}
|
||||
pushed by <strong>{authorName ?? "Unknown Pusher"}</strong>{" "}
|
||||
{authorEmail && (
|
||||
<>
|
||||
(
|
||||
<Link href={`mailto:${authorEmail}`} className="text-slate-700 no-underline">
|
||||
{authorEmail}
|
||||
</Link>
|
||||
)
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
in your resource <strong>{resourceName}</strong>
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</Text>
|
||||
<Text className="text-[14px]">
|
||||
If these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as
|
||||
a comment in the given programming language. This will prevent future notifications from being sent out for
|
||||
these secrets.
|
||||
</Text>
|
||||
<Text className="text-[14px] text-red-500">
|
||||
If these are production secrets, please rotate them immediately.
|
||||
</Text>
|
||||
<Text className="text-[14px]">
|
||||
Once you have taken action, be sure to update the finding status in the{" "}
|
||||
<Link href={url} className="text-slate-700 no-underline">
|
||||
Infisical Dashboard
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center mt-[28px]">
|
||||
<Button
|
||||
href={url}
|
||||
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||
>
|
||||
View Leaked Secrets
|
||||
</Button>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecretScanningSecretsDetectedTemplate;
|
||||
|
||||
SecretScanningSecretsDetectedTemplate.PreviewProps = {
|
||||
authorName: "Jim",
|
||||
authorEmail: "jim@infisical.com",
|
||||
resourceName: "my-resource",
|
||||
numberOfSecrets: 3,
|
||||
url: "https://infisical.com",
|
||||
isDiffScan: true,
|
||||
siteUrl: "https://infisical.com"
|
||||
} as SecretScanningSecretsDetectedTemplateProps;
|
||||
@@ -21,6 +21,8 @@ export * from "./SecretLeakIncidentTemplate";
|
||||
export * from "./SecretReminderTemplate";
|
||||
export * from "./SecretRequestCompletedTemplate";
|
||||
export * from "./SecretRotationFailedTemplate";
|
||||
export * from "./SecretScanningScanFailedTemplate";
|
||||
export * from "./SecretScanningSecretsDetectedTemplate";
|
||||
export * from "./SecretSyncFailedTemplate";
|
||||
export * from "./ServiceTokenExpiryNoticeTemplate";
|
||||
export * from "./SignupEmailVerificationTemplate";
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
SecretReminderTemplate,
|
||||
SecretRequestCompletedTemplate,
|
||||
SecretRotationFailedTemplate,
|
||||
SecretScanningScanFailedTemplate,
|
||||
SecretScanningSecretsDetectedTemplate,
|
||||
SecretSyncFailedTemplate,
|
||||
ServiceTokenExpiryNoticeTemplate,
|
||||
SignupEmailVerificationTemplate,
|
||||
@@ -73,7 +75,9 @@ export enum SmtpTemplates {
|
||||
ProjectAccessRequest = "projectAccess",
|
||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess",
|
||||
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess",
|
||||
ServiceTokenExpired = "serviceTokenExpired"
|
||||
ServiceTokenExpired = "serviceTokenExpired",
|
||||
SecretScanningV2ScanFailed = "secretScanningV2ScanFailed",
|
||||
SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected"
|
||||
}
|
||||
|
||||
export enum SmtpHost {
|
||||
@@ -113,7 +117,9 @@ const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
|
||||
[SmtpTemplates.SecretApprovalRequestNeedsReview]: SecretApprovalRequestNeedsReviewTemplate,
|
||||
[SmtpTemplates.ResetPassword]: PasswordResetTemplate,
|
||||
[SmtpTemplates.SetupPassword]: PasswordSetupTemplate,
|
||||
[SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate
|
||||
[SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate,
|
||||
[SmtpTemplates.SecretScanningV2ScanFailed]: SecretScanningScanFailedTemplate,
|
||||
[SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate
|
||||
};
|
||||
|
||||
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||
|
||||
91
docs/documentation/platform/secret-scanning/github.mdx
Normal file
@@ -0,0 +1,91 @@
|
||||
import {Tabs} from "../../../../frontend/src/components/v2";## Prerequisites
|
||||
|
||||
- Create a [GitHub Radar Connection](/integrations/app-connections/github-radar)
|
||||
|
||||
## Create a GitHub Data Source in Infisical
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to your Secret Scanning Project's Dashboard and click the **Add Data Source** button.
|
||||

|
||||
|
||||
2. Select the **GitHub** option.
|
||||

|
||||
|
||||
3. Select the **GitHub Radar Connection** to use and configure which repositories you would like to scan. Then click **Next**.
|
||||

|
||||
|
||||
- **GitHub Radar Connection** - the connection that has access to the repositories you want to scan.
|
||||
- **Scan Repositories** - select which repositories you would like to scan.
|
||||
- **All Repositories** - Infisical will scan all repositories associated with your connection.
|
||||
- **Select Repositories** - Infisical will scan the selected repositories.
|
||||
- **Auto-Scan Enabled** - whether Infisical should automatically perform a scan when a push is made to configured repositories.
|
||||
|
||||
4. Give your data source a name and description (optional). Then click **Next**.
|
||||

|
||||
|
||||
- **Name** - the name of the data source. Must be slug-friendly.
|
||||
- **Description** (optional) - a description of this rotation configuration.
|
||||
|
||||
5. Review your data source, then click **Create Data Source**.
|
||||

|
||||
|
||||
6. Your **GitHub Data Source** is now available and will begin a full scan if **Auto-Scan** is enabled.
|
||||

|
||||
|
||||
7. You can view repositories and scan results by clicking on your data source.
|
||||

|
||||
|
||||
8. In addition, you can review any findings from the **Findings Page**.
|
||||

|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create a GitHub Data Source, make an API request to the [Create GitHub Data Source](/api-reference/endpoints/secret-scanning/data-sources/github/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://us.infisical.com/api/v2/secret-scanning/data-sources/github \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-github-source",
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"description": "my github data source",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"isAutoScanEnabled": true,
|
||||
"rotationInterval": 30,
|
||||
"config": {
|
||||
"includeRepos": ["*"],
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"dataSource": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"externalId": "1234567890",
|
||||
"name": "my-github-source",
|
||||
"description": "my github data source",
|
||||
"isAutoScanEnabled": true,
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"createdAt": "2023-11-07T05:31:56Z",
|
||||
"updatedAt": "2023-11-07T05:31:56Z",
|
||||
"type": "github",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connection": {
|
||||
"app": "github-radar",
|
||||
"name": "my-radar-app",
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
|
||||
},
|
||||
"config": {
|
||||
"includeRepos": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -29,7 +29,7 @@ Data sources are configured integrations with external platforms, such as a GitH
|
||||
|
||||
A data source acts as a secure intermediary between the external system and the scanner engine. It manages a collection of scannable resources (such as repositories) and handles the authentication and communication required for scanning operations.
|
||||
|
||||
[data source page image]
|
||||

|
||||
|
||||
### Resources
|
||||
|
||||
@@ -37,15 +37,13 @@ Resources are the atomic, scannable units, such as a repository, that can be mon
|
||||
|
||||
Each resource maintains its own scanning history and status, allowing for granular monitoring and management of secret scanning across your organization.
|
||||
|
||||
[resource table image]
|
||||

|
||||
|
||||
### Scans
|
||||
|
||||
Scans can be initiated in two ways:
|
||||
|
||||
1. **Full Scan** - Manually triggered scan that comprehensively checks either:
|
||||
- All resources associated with a data source
|
||||
- A single selected resource
|
||||
1. **Full Scan** - Manually triggered scan that comprehensively checks either all resources associated with a data source or a single selected resource.
|
||||
|
||||
2. **Diff Scan** - Automatically executed when **Auto-Scan** is enabled on a data source. This scan type specifically focuses on updates to existing resources.
|
||||
|
||||
@@ -55,42 +53,23 @@ All scan activities can be monitored in real-time through the Infisical UI, whic
|
||||
- Resource(s) being scanned
|
||||
- Detection results (whether any secrets were found)
|
||||
|
||||
[scan table image]
|
||||

|
||||
|
||||
## [In Progress - old below]
|
||||
### Findings
|
||||
|
||||
## Code Scanning
|
||||
Findings are automatically generated when secret leaks are detected during scanning operations. Each finding contains comprehensive information including:
|
||||
- The specific scanning rule that identified the leak
|
||||
- File location and line number where the secret was found
|
||||
- Resource-specific details (e.g., commit hash and author for Git repositories)
|
||||
|
||||

|
||||
Findings are initially marked as **Unresolved** and can be updated to one of the following statuses with additional remarks:
|
||||
- **Resolved** - The issue has been addressed
|
||||
- **False Positive** - The detection was incorrect
|
||||
- **Ignore** - The finding can be safely disregarded
|
||||
|
||||
Secret scans are built on event-driven architecture. This means that every time a push is made to one of your selected repositories, Infisical will scan the modified files for any exposed secrets.
|
||||
These status options help teams effectively track and manage the lifecycle of detected secret leaks.
|
||||
|
||||
If one or more exposed secrets are detected, it will be displayed in your Infisical dashboard. An exposed secret is known as a **"Risk"**. Each risk has the following data associated with it:
|
||||
- **Date**: When the risk was first detected.
|
||||
- **Secret Type**: Which type of secret was detected.
|
||||
- **Info**: Information about the secret, such as the repository, file name, and the committer who made the change.
|
||||
|
||||
Once an exposed secret is detected, all organization admins will be sent an e-mail notification containing details about the exposed secret.
|
||||
|
||||
<Tip>
|
||||
Each risk also contains a "View Exposed Secret" button, which will take you directly to the GitHub commit and to the line where the secret was exposed.
|
||||
</Tip>
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
## Responding to Exposed Secrets
|
||||
|
||||
After an exposed secret is detected, it will be marked as `Needs Attention`. When there are risks marked as needs attention, it's important to address them as soon as possible.
|
||||
|
||||
You can mark the risk as `Resolved` by changing the status to one of the following states:
|
||||
- **This Is a False Positive**: The secret was not exposed, but was detected by the scanner.
|
||||
- **I Have Rotated The Secret**: The secret was exposed, but it has now been removed.
|
||||
- **No Rotation Needed**: You are choosing to ignore this risk. You may choose to do this if the risk is non-sensitive or otherwise not a security risk.
|
||||
|
||||

|
||||

|
||||
|
||||
## Ignoring Known Secrets
|
||||
If you're intentionally committing a test secret that the secret scanner might flag, you can instruct Infisical to overlook that secret with the methods listed below.
|
||||
|
||||
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 694 KiB |
|
After Width: | Height: | Size: 760 KiB |
|
After Width: | Height: | Size: 733 KiB |
|
After Width: | Height: | Size: 728 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/images/platform/secret-scanning/secret-scanning-scans.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
@@ -245,7 +245,8 @@
|
||||
{
|
||||
"group": "Secret Scanning",
|
||||
"pages": [
|
||||
"documentation/platform/secret-scanning/overview"
|
||||
"documentation/platform/secret-scanning/overview",
|
||||
"documentation/platform/secret-scanning/github"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
1
frontend/public/lotties/blocks.json
Normal file
@@ -125,7 +125,7 @@ export enum ProjectPermissionSecretScanningDataSourceActions {
|
||||
|
||||
export enum ProjectPermissionSecretScanningFindingActions {
|
||||
Read = "read-findings",
|
||||
Resolve = "resolve-findings"
|
||||
Update = "update-findings"
|
||||
}
|
||||
|
||||
export enum PermissionConditionOperators {
|
||||
|
||||
@@ -196,7 +196,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_UPDATE]: "Update Secret Scanning Data Source",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_DELETE]: "Delete Secret Scanning Data Source",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_GET]: "Get Secret Scanning Data Source",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_SCAN]: "San Scanning Data Source",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN]: "Trigger Secret Scanning Data Source Scan",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_SCAN]: "Secret Scanning Data Source Scan",
|
||||
[EventType.SECRET_SCANNING_RESOURCE_LIST]: "List Secret Scanning Resources",
|
||||
[EventType.SECRET_SCANNING_SCAN_LIST]: "List Secret Scanning Scans",
|
||||
[EventType.SECRET_SCANNING_FINDING_LIST]: "List Secret Scanning Findings",
|
||||
|
||||
@@ -190,6 +190,7 @@ export enum EventType {
|
||||
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_TRIGGER_SCAN = "secret-scanning-data-source-trigger-scan",
|
||||
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",
|
||||
|
||||
@@ -14,4 +14,5 @@ export type TSecretScanningDataSourceBase = {
|
||||
name: string;
|
||||
};
|
||||
isAutoScanEnabled: boolean;
|
||||
isDisconnected: boolean;
|
||||
};
|
||||
|
||||
@@ -50,4 +50,5 @@ export type SubscriptionPlan = {
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: boolean;
|
||||
kmip: boolean;
|
||||
secretScanning: boolean;
|
||||
};
|
||||
|
||||
@@ -77,6 +77,7 @@ export const ProjectLayout = () => {
|
||||
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(workspaceId, {
|
||||
enabled:
|
||||
isSecretScanning &&
|
||||
subscription.secretScanning &&
|
||||
permission.can(
|
||||
ProjectPermissionSecretScanningFindingActions.Read,
|
||||
ProjectPermissionSub.SecretScanningFindings
|
||||
@@ -264,7 +265,7 @@ export const ProjectLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
|
||||
<MenuItem isSelected={isActive} icon="blocks">
|
||||
Data Sources
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@app/components/v2";
|
||||
import { useProjectPermission, useWorkspace } from "@app/context";
|
||||
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const AssumePrivilegeModeBanner = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -35,7 +36,20 @@ export const AssumePrivilegeModeBanner = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
|
||||
let page: string;
|
||||
|
||||
switch (currentWorkspace.type) {
|
||||
case ProjectType.SecretScanning:
|
||||
page = "data-sources";
|
||||
break;
|
||||
case ProjectType.CertificateManager:
|
||||
page = "subscribers";
|
||||
break;
|
||||
default:
|
||||
page = "overview";
|
||||
}
|
||||
|
||||
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/${page}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
useGetWorkspaceUserDetails
|
||||
} from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
||||
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||
@@ -70,7 +71,21 @@ export const Page = () => {
|
||||
type: "success",
|
||||
text: "User privilege assumption has started"
|
||||
});
|
||||
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
|
||||
|
||||
let page: string;
|
||||
|
||||
switch (currentWorkspace.type) {
|
||||
case ProjectType.SecretScanning:
|
||||
page = "data-sources";
|
||||
break;
|
||||
case ProjectType.CertificateManager:
|
||||
page = "subscribers";
|
||||
break;
|
||||
default:
|
||||
page = "overview";
|
||||
}
|
||||
|
||||
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/${page}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -104,7 +104,7 @@ const SecretScanningDataSourcePolicyActionSchema = z.object({
|
||||
|
||||
const SecretScanningFindingPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretScanningFindingActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretScanningFindingActions.Resolve]: z.boolean().optional()
|
||||
[ProjectPermissionSecretScanningFindingActions.Update]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const KmipPolicyActionSchema = z.object({
|
||||
@@ -731,6 +731,52 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretScanningDataSources) {
|
||||
const canRead = action.includes(ProjectPermissionSecretScanningDataSourceActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionSecretScanningDataSourceActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionSecretScanningDataSourceActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionSecretScanningDataSourceActions.Create);
|
||||
const canReadScans = action.includes(
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadScans
|
||||
);
|
||||
const canReadResources = action.includes(
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadResources
|
||||
);
|
||||
const canTriggerScans = action.includes(
|
||||
ProjectPermissionSecretScanningDataSourceActions.TriggerScans
|
||||
);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
if (canRead)
|
||||
formVal[subject]![0][ProjectPermissionSecretScanningDataSourceActions.Read] = true;
|
||||
if (canEdit)
|
||||
formVal[subject]![0][ProjectPermissionSecretScanningDataSourceActions.Edit] = true;
|
||||
if (canCreate)
|
||||
formVal[subject]![0][ProjectPermissionSecretScanningDataSourceActions.Create] = true;
|
||||
if (canDelete)
|
||||
formVal[subject]![0][ProjectPermissionSecretScanningDataSourceActions.Delete] = true;
|
||||
if (canReadScans)
|
||||
formVal[subject]![0][ProjectPermissionSecretScanningDataSourceActions.ReadScans] = true;
|
||||
if (canReadResources)
|
||||
formVal[subject]![0][ProjectPermissionSecretScanningDataSourceActions.ReadResources] = true;
|
||||
if (canTriggerScans)
|
||||
formVal[subject]![0][ProjectPermissionSecretScanningDataSourceActions.TriggerScans] = true;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretScanningFindings) {
|
||||
const canRead = action.includes(ProjectPermissionSecretScanningFindingActions.Read);
|
||||
const canUpdate = action.includes(ProjectPermissionSecretScanningFindingActions.Update);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
if (canRead) formVal[subject]![0][ProjectPermissionSecretScanningFindingActions.Read] = true;
|
||||
if (canUpdate)
|
||||
formVal[subject]![0][ProjectPermissionSecretScanningFindingActions.Update] = true;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SshHosts) {
|
||||
if (!formVal[subject]) formVal[subject] = [];
|
||||
|
||||
@@ -1313,8 +1359,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
value: ProjectPermissionSecretScanningFindingActions.Read
|
||||
},
|
||||
{
|
||||
label: "Resolve Finding Status",
|
||||
value: ProjectPermissionSecretScanningFindingActions.Resolve
|
||||
label: "Update Findings",
|
||||
value: ProjectPermissionSecretScanningFindingActions.Update
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ const PageContent = () => {
|
||||
<p className="text-3xl font-semibold text-white">{dataSource.name}</p>
|
||||
<p className="leading-3 text-bunker-300">{details.name} Data Source</p>
|
||||
</div>
|
||||
{/* <SecretSyncActionTriggers dataSource={dataSource} /> */}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { faBan, faCheck, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBan, faCheck, faEdit, faPlugCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { EditSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { Badge, IconButton } from "@app/components/v2";
|
||||
import { Badge, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
@@ -18,13 +18,31 @@ type Props = {
|
||||
export const SecretScanningDataSourceSection = ({ dataSource }: Props) => {
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editDataSource"] as const);
|
||||
|
||||
const { name, description, connection, isAutoScanEnabled } = dataSource;
|
||||
const { name, description, connection, isAutoScanEnabled, isDisconnected } = dataSource;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Details</h3>
|
||||
<div className="mr-2 flex flex-1 items-center justify-between">
|
||||
<h3 className="font-semibold text-mineshaft-100">Details</h3>
|
||||
{isDisconnected && (
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content="The external data source has been removed and can no longer be scanned. Delete this data source and re-initialize the connection."
|
||||
>
|
||||
<div className="ml-auto">
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlugCircleXmark} />
|
||||
<span>Disconnected</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.Edit}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
|
||||
@@ -82,7 +82,7 @@ export const SecretScanningScanRow = ({ scan }: Props) => {
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="whitespace-nowrap">
|
||||
{type === SecretScanningScanType.FullScan ? "Full scan" : "Diff scan"}
|
||||
{type === SecretScanningScanType.FullScan ? "Full Scan" : "Diff Scan"}
|
||||
</Td>
|
||||
<Td>
|
||||
{
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionSecretScanningDataSourceActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { SecretScanningDataSourcesSection } from "./components";
|
||||
|
||||
@@ -12,17 +17,23 @@ export const SecretScanningDataSourcesPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Data Sources"
|
||||
description="Manage your Secret Scanning data sources."
|
||||
/>
|
||||
<SecretScanningDataSourcesSection />
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.Read}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Data Sources"
|
||||
description="Manage your Secret Scanning data sources."
|
||||
/>
|
||||
<SecretScanningDataSourcesSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
faEllipsisV,
|
||||
faExpand,
|
||||
faInfoCircle,
|
||||
faPlugCircleXmark,
|
||||
faSearch,
|
||||
faToggleOff,
|
||||
faToggleOn,
|
||||
@@ -72,7 +73,8 @@ export const SecretScanningDataSourceRow = ({
|
||||
unresolvedFindings,
|
||||
lastScannedAt,
|
||||
lastScanStatus,
|
||||
lastScanStatusMessage
|
||||
lastScanStatusMessage,
|
||||
isDisconnected
|
||||
} = dataSource;
|
||||
|
||||
const sourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
@@ -199,7 +201,7 @@ export const SecretScanningDataSourceRow = ({
|
||||
) : (
|
||||
<span className="text-mineshaft-400">No scans</span>
|
||||
)}
|
||||
{!isAutoScanEnabled && (
|
||||
{!isAutoScanEnabled && !isDisconnected && (
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content={`Auto-Scan is disabled. Scans will not be automatically triggered when a ${autoScanDescription.verb} occurs to ${autoScanDescription.pluralNoun} associated with this data source`}
|
||||
@@ -212,6 +214,22 @@ export const SecretScanningDataSourceRow = ({
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isDisconnected && (
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content="The external data source has been removed and can no longer be scanned. Delete this data source and re-initialize the connection."
|
||||
>
|
||||
<div className="ml-auto">
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlugCircleXmark} />
|
||||
<span>Disconnected</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { CreateSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
||||
import { Button, Spinner } from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSub, useSubscription, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useListSecretScanningDataSources } from "@app/hooks/api/secretScanningV2";
|
||||
@@ -12,7 +13,12 @@ import { useListSecretScanningDataSources } from "@app/hooks/api/secretScanningV
|
||||
import { SecretScanningDataSourcesTable } from "./SecretScanningDataSourcesTable";
|
||||
|
||||
export const SecretScanningDataSourcesSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addDataSource"] as const);
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"addDataSource",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
@@ -63,7 +69,14 @@ export const SecretScanningDataSourcesSection = () => {
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addDataSource")}
|
||||
onClick={() => {
|
||||
if (!subscription.secretScanning) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("addDataSource");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Data Source
|
||||
@@ -77,6 +90,11 @@ export const SecretScanningDataSourcesSection = () => {
|
||||
isOpen={popUp.addDataSource.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addDataSource", isOpen)}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can create Data Sources by upgrading to Infisical's Enterprise plan."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCheckCircle,
|
||||
faCubesStacked,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faPuzzlePiece,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -368,7 +368,7 @@ export const SecretScanningDataSourcesTable = ({ dataSources }: Props) => {
|
||||
? "No data sources match search..."
|
||||
: "This project has no data sources configured"
|
||||
}
|
||||
icon={dataSources.length ? faSearch : faPuzzlePiece}
|
||||
icon={dataSources.length ? faSearch : faCubesStacked}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { SecretScanningFindingsSection } from "./components";
|
||||
|
||||
@@ -12,14 +15,20 @@ export const SecretScanningFindingsPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Findings" description="View Secret Leaks across your project." />
|
||||
<SecretScanningFindingsSection />
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionSecretScanningFindingActions.Read}
|
||||
a={ProjectPermissionSub.SecretScanningFindings}
|
||||
>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Findings" description="View Secret Leaks across your project." />
|
||||
<SecretScanningFindingsSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -18,6 +19,10 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionSecretScanningFindingActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import {
|
||||
SECRET_SCANNING_DATA_SOURCE_MAP,
|
||||
SECRET_SCANNING_FINDING_STATUS_ICON_MAP
|
||||
@@ -191,19 +196,32 @@ export const SecretScanningFindingRow = ({ finding, onUpdate }: Props) => {
|
||||
{details.link}
|
||||
</a>
|
||||
</GenericFieldLabel>
|
||||
|
||||
<Button
|
||||
onClick={() => onUpdate(finding)}
|
||||
colorSchema="secondary"
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
className={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].className}
|
||||
icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].icon}
|
||||
/>
|
||||
}
|
||||
<div className="col-span-full flex items-center border-t border-mineshaft-500" />
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningFindingActions.Update}
|
||||
a={ProjectPermissionSub.SecretScanningFindings}
|
||||
>
|
||||
Update Status
|
||||
</Button>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => onUpdate(finding)}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
className={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].className}
|
||||
icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].icon}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Update Status
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{Boolean(remarks) && (
|
||||
<GenericFieldLabel className="col-span-3" label="Remarks">
|
||||
{remarks}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
|
||||
@@ -133,7 +133,7 @@ export const SecretScanningUpdateFindingModal = ({ finding, isOpen, onOpenChange
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent title="Resolve Finding" subTitle="Mark this finding as resolved">
|
||||
<ModalContent title="Update Finding" subTitle="Update the status or leave remarks">
|
||||
<Content finding={finding} onComplete={() => onOpenChange(false)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||