feature: secret scanning pt2 and address initial feedback

This commit is contained in:
Scott Wilson
2025-05-29 20:40:48 -07:00
parent e6c97510ca
commit 4773336a04
78 changed files with 1038 additions and 346 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -135,7 +135,7 @@ export enum ProjectPermissionSecretScanningDataSourceActions {
export enum ProjectPermissionSecretScanningFindingActions {
Read = "read-findings",
Resolve = "resolve-findings"
Update = "update-findings"
}
export enum ProjectPermissionSub {

View File

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

View File

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

View File

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

View File

@@ -27,4 +27,6 @@ export type TQueueGitHubResourceDiffScan = {
dataSourceType: SecretScanningDataSource.GitHub;
payload: PushEvent;
dataSourceId: string;
resourceId: string;
scanId: string;
};

View File

@@ -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" });
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}`,

View File

@@ -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."
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => {

View 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.
![Secret Scanning Dashboard](/images/platform/secret-scanning/github/github-data-source-step-1.png)
2. Select the **GitHub** option.
![Select GitHub Option](/images/platform/secret-scanning/github/github-data-source-step-2.png)
3. Select the **GitHub Radar Connection** to use and configure which repositories you would like to scan. Then click **Next**.
![Data Source Configuration](/images/platform/secret-scanning/github/github-data-source-step-3.png)
- **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**.
![Data Source Details](/images/platform/secret-scanning/github/github-data-source-step-4.png)
- **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**.
![Data Source Review](/images/platform/secret-scanning/github/github-data-source-step-5.png)
6. Your **GitHub Data Source** is now available and will begin a full scan if **Auto-Scan** is enabled.
![Data Source Created](/images/platform/secret-scanning/github/github-data-source-step-6.png)
7. You can view repositories and scan results by clicking on your data source.
![Data Source Page](/images/platform/secret-scanning/github/github-data-source-step-7.png)
8. In addition, you can review any findings from the **Findings Page**.
![Findings Page](/images/platform/secret-scanning/github/github-data-source-step-8.png)
</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>

View File

@@ -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]
![data sources](/images/platform/secret-scanning/secret-scanning-data-sources.png)
### 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]
![resources](/images/platform/secret-scanning/secret-scanning-resources.png)
### 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]
![scans](/images/platform/secret-scanning/secret-scanning-scans.png)
## [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)
![Scanning Overview](/images/platform/secret-scanning/overview.png)
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>
![Exposed Secret](/images/platform/secret-scanning/exposed-secret.png)
## 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.
![Needs Attention](/images/platform/secret-scanning/needs-attention.png)
![findings](/images/platform/secret-scanning/secret-scanning-findings.png)
## 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -245,7 +245,8 @@
{
"group": "Secret Scanning",
"pages": [
"documentation/platform/secret-scanning/overview"
"documentation/platform/secret-scanning/overview",
"documentation/platform/secret-scanning/github"
]
}
]

File diff suppressed because one or more lines are too long

View File

@@ -125,7 +125,7 @@ export enum ProjectPermissionSecretScanningDataSourceActions {
export enum ProjectPermissionSecretScanningFindingActions {
Read = "read-findings",
Resolve = "resolve-findings"
Update = "update-findings"
}
export enum PermissionConditionOperators {

View File

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

View File

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

View File

@@ -14,4 +14,5 @@ export type TSecretScanningDataSourceBase = {
name: string;
};
isAutoScanEnabled: boolean;
isDisconnected: boolean;
};

View File

@@ -50,4 +50,5 @@ export type SubscriptionPlan = {
enforceMfa: boolean;
projectTemplates: boolean;
kmip: boolean;
secretScanning: boolean;
};

View File

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

View File

@@ -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}`;
}
}
);

View File

@@ -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}`;
}
}
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
</>
);
};

View File

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

View File

@@ -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."
/>
</>
);
};

View File

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

View File

@@ -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>
</>
);
};

View File

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

View File

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