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.string("projectId").notNullable();
|
||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
|
t.boolean("isDisconnected").notNullable().defaultTo(false);
|
||||||
t.unique(["projectId", "name"]);
|
t.unique(["projectId", "name"]);
|
||||||
});
|
});
|
||||||
await createOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
|
await createOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export const SecretScanningDataSourcesSchema = z.object({
|
|||||||
isAutoScanEnabled: z.boolean().default(true).nullable().optional(),
|
isAutoScanEnabled: z.boolean().default(true).nullable().optional(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
isDisconnected: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretScanningDataSources = z.infer<typeof SecretScanningDataSourcesSchema>;
|
export type TSecretScanningDataSources = z.infer<typeof SecretScanningDataSourcesSchema>;
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ export const registerSecretScanningEndpoints = <
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { dataSourceId } = req.params;
|
const { dataSourceId } = req.params;
|
||||||
|
|
||||||
const dataSource = (await server.services.secretScanningV2.deleteSecretScanningResource(
|
const dataSource = (await server.services.secretScanningV2.deleteSecretScanningDataSource(
|
||||||
{ type, dataSourceId },
|
{ type, dataSourceId },
|
||||||
req.permission
|
req.permission
|
||||||
)) as T;
|
)) as T;
|
||||||
@@ -349,7 +349,7 @@ export const registerSecretScanningEndpoints = <
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: dataSource.projectId,
|
projectId: dataSource.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
|
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN,
|
||||||
metadata: {
|
metadata: {
|
||||||
type,
|
type,
|
||||||
dataSourceId
|
dataSourceId
|
||||||
@@ -392,7 +392,7 @@ export const registerSecretScanningEndpoints = <
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: dataSource.projectId,
|
projectId: dataSource.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
|
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN,
|
||||||
metadata: {
|
metadata: {
|
||||||
type,
|
type,
|
||||||
dataSourceId,
|
dataSourceId,
|
||||||
|
|||||||
@@ -140,12 +140,12 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.SecretScanning],
|
tags: [ApiDocsTags.SecretScanning],
|
||||||
description: "Update the resolve status of the specified Secret Scanning Finding.",
|
description: "Update the specified Secret Scanning Finding.",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
findingId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningFindings.UPDATE.findingId)
|
findingId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningFindings.UPDATE.findingId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
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)
|
remarks: z.string().nullish().describe(SecretScanningFindings.UPDATE.remarks)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
TSecretRotationV2Raw,
|
TSecretRotationV2Raw,
|
||||||
TUpdateSecretRotationV2DTO
|
TUpdateSecretRotationV2DTO
|
||||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
import {
|
||||||
|
SecretScanningDataSource,
|
||||||
|
SecretScanningScanStatus,
|
||||||
|
SecretScanningScanType
|
||||||
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||||
import {
|
import {
|
||||||
TCreateSecretScanningDataSourceDTO,
|
TCreateSecretScanningDataSourceDTO,
|
||||||
TDeleteSecretScanningDataSourceDTO,
|
TDeleteSecretScanningDataSourceDTO,
|
||||||
@@ -390,6 +394,7 @@ export enum EventType {
|
|||||||
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
|
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
|
||||||
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
|
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
|
||||||
SECRET_SCANNING_DATA_SOURCE_GET = "secret-scanning-data-source-get",
|
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_DATA_SOURCE_SCAN = "secret-scanning-data-source-scan",
|
||||||
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
|
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
|
||||||
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
|
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
|
||||||
@@ -2964,9 +2969,23 @@ interface SecretScanningDataSourceDeleteEvent {
|
|||||||
metadata: TDeleteSecretScanningDataSourceDTO;
|
metadata: TDeleteSecretScanningDataSourceDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecretScanningDataSourceTriggerScanEvent {
|
||||||
|
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN;
|
||||||
|
metadata: TTriggerSecretScanningDataSourceDTO;
|
||||||
|
}
|
||||||
|
|
||||||
interface SecretScanningDataSourceScanEvent {
|
interface SecretScanningDataSourceScanEvent {
|
||||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN;
|
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 {
|
interface SecretScanningResourceListEvent {
|
||||||
@@ -3273,6 +3292,7 @@ export type Event =
|
|||||||
| SecretScanningDataSourceCreateEvent
|
| SecretScanningDataSourceCreateEvent
|
||||||
| SecretScanningDataSourceUpdateEvent
|
| SecretScanningDataSourceUpdateEvent
|
||||||
| SecretScanningDataSourceDeleteEvent
|
| SecretScanningDataSourceDeleteEvent
|
||||||
|
| SecretScanningDataSourceTriggerScanEvent
|
||||||
| SecretScanningDataSourceScanEvent
|
| SecretScanningDataSourceScanEvent
|
||||||
| SecretScanningResourceListEvent
|
| SecretScanningResourceListEvent
|
||||||
| SecretScanningScanListEvent
|
| SecretScanningScanListEvent
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ const buildAdminPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Resolve],
|
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Update],
|
||||||
ProjectPermissionSub.SecretScanningFindings
|
ProjectPermissionSub.SecretScanningFindings
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -400,9 +400,6 @@ const buildMemberPermissionRules = () => {
|
|||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionSecretScanningDataSourceActions.Create,
|
|
||||||
ProjectPermissionSecretScanningDataSourceActions.Edit,
|
|
||||||
ProjectPermissionSecretScanningDataSourceActions.Delete,
|
|
||||||
ProjectPermissionSecretScanningDataSourceActions.Read,
|
ProjectPermissionSecretScanningDataSourceActions.Read,
|
||||||
ProjectPermissionSecretScanningDataSourceActions.TriggerScans,
|
ProjectPermissionSecretScanningDataSourceActions.TriggerScans,
|
||||||
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
|
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
|
||||||
@@ -412,7 +409,7 @@ const buildMemberPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Resolve],
|
[ProjectPermissionSecretScanningFindingActions.Read, ProjectPermissionSecretScanningFindingActions.Update],
|
||||||
ProjectPermissionSub.SecretScanningFindings
|
ProjectPermissionSub.SecretScanningFindings
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export enum ProjectPermissionSecretScanningDataSourceActions {
|
|||||||
|
|
||||||
export enum ProjectPermissionSecretScanningFindingActions {
|
export enum ProjectPermissionSecretScanningFindingActions {
|
||||||
Read = "read-findings",
|
Read = "read-findings",
|
||||||
Resolve = "resolve-findings"
|
Update = "update-findings"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
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 { 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 { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||||
import {
|
import {
|
||||||
|
SecretScanningDataSource,
|
||||||
SecretScanningFindingSeverity,
|
SecretScanningFindingSeverity,
|
||||||
SecretScanningResource
|
SecretScanningResource
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
} 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 {
|
import {
|
||||||
TSecretScanningFactoryGetDiffScanFindingsPayload,
|
TSecretScanningFactoryGetDiffScanFindingsPayload,
|
||||||
TSecretScanningFactoryGetDiffScanResourcePayload,
|
TSecretScanningFactoryGetDiffScanResourcePayload,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { titleCaseToCamelCase } from "@app/lib/fn";
|
||||||
import { listGitHubRadarRepositories, TGitHubRadarConnection } from "@app/services/app-connection/github-radar";
|
import { listGitHubRadarRepositories, TGitHubRadarConnection } from "@app/services/app-connection/github-radar";
|
||||||
|
|
||||||
import { TGitHubDataSourceWithConnection, TQueueGitHubResourceDiffScan } from "./github-secret-scanning-types";
|
import { TGitHubDataSourceWithConnection, TQueueGitHubResourceDiffScan } from "./github-secret-scanning-types";
|
||||||
@@ -30,7 +32,8 @@ export const GitHubSecretScanningFactory = () => {
|
|||||||
const externalId = connection.credentials.installationId;
|
const externalId = connection.credentials.installationId;
|
||||||
|
|
||||||
const existingDataSource = await secretScanningV2DAL.dataSources.findOne({
|
const existingDataSource = await secretScanningV2DAL.dataSources.findOne({
|
||||||
externalId
|
externalId,
|
||||||
|
type: SecretScanningDataSource.GitHub
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingDataSource)
|
if (existingDataSource)
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ import {
|
|||||||
BaseUpdateSecretScanningDataSourceSchema,
|
BaseUpdateSecretScanningDataSourceSchema,
|
||||||
GitRepositoryScanFindingDetailsSchema
|
GitRepositoryScanFindingDetailsSchema
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-schemas";
|
} 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";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
export const GitHubDataSourceConfigSchema = z.object({
|
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({
|
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 { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TGitHubDataSource } from "./github-secret-scanning-types";
|
||||||
|
|
||||||
export const githubSecretScanningService = (
|
export const githubSecretScanningService = (
|
||||||
secretScanningV2DAL: TSecretScanningV2DALFactory,
|
secretScanningV2DAL: TSecretScanningV2DALFactory,
|
||||||
secretScanningV2Queue: Pick<TSecretScanningV2QueueServiceFactory, "queueResourceDiffScan">
|
secretScanningV2Queue: Pick<TSecretScanningV2QueueServiceFactory, "queueResourceDiffScan">
|
||||||
@@ -21,9 +23,12 @@ export const githubSecretScanningService = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// scott: maybe add disabled col instead?
|
logger.info(
|
||||||
await secretScanningV2DAL.resources.delete({
|
`secretScanningV2RemoveEvent: GitHub - installation deleted [installationId=${installationId}] [dataSourceId=${dataSource.id}]`
|
||||||
dataSourceId: dataSource.id
|
);
|
||||||
|
|
||||||
|
await secretScanningV2DAL.dataSources.updateById(dataSource.id, {
|
||||||
|
isDisconnected: true
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,9 +42,10 @@ export const githubSecretScanningService = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSource = await secretScanningV2DAL.dataSources.findOne({
|
const dataSource = (await secretScanningV2DAL.dataSources.findOne({
|
||||||
externalId: String(installation.id)
|
externalId: String(installation.id),
|
||||||
});
|
type: SecretScanningDataSource.GitHub
|
||||||
|
})) as TGitHubDataSource | undefined;
|
||||||
|
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -48,15 +54,33 @@ export const githubSecretScanningService = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await secretScanningV2Queue.queueResourceDiffScan({
|
const {
|
||||||
dataSourceType: SecretScanningDataSource.GitHub,
|
isAutoScanEnabled,
|
||||||
payload,
|
config: { includeRepos }
|
||||||
dataSourceId: dataSource.id
|
} = 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 {
|
return {
|
||||||
handlePushEvent,
|
handlePushEvent,
|
||||||
handleInstallationDeleted: handleInstallationDeletedEvent
|
handleInstallationDeletedEvent
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,4 +27,6 @@ export type TQueueGitHubResourceDiffScan = {
|
|||||||
dataSourceType: SecretScanningDataSource.GitHub;
|
dataSourceType: SecretScanningDataSource.GitHub;
|
||||||
payload: PushEvent;
|
payload: PushEvent;
|
||||||
dataSourceId: string;
|
dataSourceId: string;
|
||||||
|
resourceId: string;
|
||||||
|
scanId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteDataSourceById = async (dataSourceId: string, tx?: Knex) => {
|
const deleteDataSourceById = async (dataSourceId: string, tx?: Knex) => {
|
||||||
const secretRotation = (await baseSecretScanningDataSourceQuery({
|
const dataSource = (await baseSecretScanningDataSourceQuery({
|
||||||
filter: { id: dataSourceId },
|
filter: { id: dataSourceId },
|
||||||
db,
|
db,
|
||||||
tx
|
tx
|
||||||
@@ -171,18 +171,18 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
await dataSourceOrm.deleteById(dataSourceId, tx);
|
await dataSourceOrm.deleteById(dataSourceId, tx);
|
||||||
|
|
||||||
return expandSecretScanningDataSource(secretRotation);
|
return expandSecretScanningDataSource(dataSource);
|
||||||
};
|
};
|
||||||
|
|
||||||
const findOneDataSource = async (filter: Parameters<(typeof dataSourceOrm)["findOne"]>[0], tx?: Knex) => {
|
const findOneDataSource = async (filter: Parameters<(typeof dataSourceOrm)["findOne"]>[0], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const secretRotation = await baseSecretScanningDataSourceQuery({ filter, db, tx }).first();
|
const dataSource = await baseSecretScanningDataSourceQuery({ filter, db, tx }).first();
|
||||||
|
|
||||||
if (secretRotation) {
|
if (dataSource) {
|
||||||
return expandSecretScanningDataSource(secretRotation);
|
return expandSecretScanningDataSource(dataSource);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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) {
|
} 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) {
|
} 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) {
|
} 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;
|
return scans;
|
||||||
} catch (error) {
|
} 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 { 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 { 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 { 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 { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums";
|
||||||
import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListItem } from "./secret-scanning-v2-types";
|
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 => {
|
export const scanGitRepositoryAndGetFindings = async (scanPath: string, findingsPath: string): TGetFindingsPayload => {
|
||||||
await scanDirectory(scanPath, findingsPath);
|
await scanDirectory(scanPath, findingsPath);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { join } from "path";
|
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 {
|
import {
|
||||||
createTempFolder,
|
createTempFolder,
|
||||||
deleteTempFolder
|
deleteTempFolder
|
||||||
@@ -8,12 +11,18 @@ import {
|
|||||||
parseScanErrorMessage,
|
parseScanErrorMessage,
|
||||||
scanGitRepositoryAndGetFindings
|
scanGitRepositoryAndGetFindings
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
|
} 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 { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||||
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
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 { 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 { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
|
||||||
import {
|
import {
|
||||||
@@ -28,16 +37,19 @@ import {
|
|||||||
TFindingsPayload,
|
TFindingsPayload,
|
||||||
TQueueSecretScanningDataSourceFullScan,
|
TQueueSecretScanningDataSourceFullScan,
|
||||||
TQueueSecretScanningResourceDiffScan,
|
TQueueSecretScanningResourceDiffScan,
|
||||||
|
TQueueSecretScanningSendNotification,
|
||||||
TSecretScanningDataSourceWithConnection
|
TSecretScanningDataSourceWithConnection
|
||||||
} from "./secret-scanning-v2-types";
|
} from "./secret-scanning-v2-types";
|
||||||
|
|
||||||
type TSecretRotationV2QueueServiceFactoryDep = {
|
type TSecretRotationV2QueueServiceFactoryDep = {
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
secretScanningV2DAL: TSecretScanningV2DALFactory;
|
secretScanningV2DAL: TSecretScanningV2DALFactory;
|
||||||
// smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
// projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||||
// projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
|
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
|
||||||
@@ -45,10 +57,12 @@ export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof sec
|
|||||||
export const secretScanningV2QueueServiceFactory = async ({
|
export const secretScanningV2QueueServiceFactory = async ({
|
||||||
queueService,
|
queueService,
|
||||||
secretScanningV2DAL,
|
secretScanningV2DAL,
|
||||||
// projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
// projectDAL,
|
projectDAL,
|
||||||
// smtpService,
|
smtpService,
|
||||||
kmsService
|
kmsService,
|
||||||
|
auditLogService,
|
||||||
|
keyStore
|
||||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||||
const queueDataSourceFullScan = async (
|
const queueDataSourceFullScan = async (
|
||||||
dataSource: TSecretScanningDataSourceWithConnection,
|
dataSource: TSecretScanningDataSourceWithConnection,
|
||||||
@@ -63,7 +77,7 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
|
|
||||||
let filteredRawResources = rawResources;
|
let filteredRawResources = rawResources;
|
||||||
|
|
||||||
// TODO: should add indivial resource fetch to factory
|
// TODO: should add individual resource fetch to factory
|
||||||
if (resourceExternalId) {
|
if (resourceExternalId) {
|
||||||
filteredRawResources = rawResources.filter((resource) => resource.externalId === 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) => {
|
await secretScanningV2DAL.resources.transaction(async (tx) => {
|
||||||
const resources = await secretScanningV2DAL.resources.upsert(
|
const resources = await secretScanningV2DAL.resources.upsert(
|
||||||
filteredRawResources.map((rawResource) => ({
|
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>(
|
await queueService.startPg<QueueName.SecretScanningV2>(
|
||||||
QueueJobs.SecretScanningV2FullScan,
|
QueueJobs.SecretScanningV2FullScan,
|
||||||
async ([job]) => {
|
async ([job]) => {
|
||||||
@@ -123,7 +141,26 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
|
|
||||||
const tempFolder = await createTempFolder();
|
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 {
|
||||||
|
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(
|
await secretScanningV2DAL.scans.update(
|
||||||
{ id: scanId },
|
{ 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;
|
let connection: TAppConnection | null = null;
|
||||||
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
|
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
|
||||||
|
|
||||||
@@ -165,110 +194,196 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
throw new Error("Unhandled resource type");
|
throw new Error("Unhandled resource type");
|
||||||
}
|
}
|
||||||
|
|
||||||
await secretScanningV2DAL.findings.transaction(async (tx) => {
|
if (findingsPayload.length) {
|
||||||
await secretScanningV2DAL.findings.upsert(
|
await secretScanningV2DAL.findings.transaction(async (tx) => {
|
||||||
findingsPayload.map((findings) => ({
|
await secretScanningV2DAL.findings.upsert(
|
||||||
...findings,
|
findingsPayload.map((findings) => ({
|
||||||
projectId: dataSource.projectId,
|
...findings,
|
||||||
dataSourceName: dataSource.name,
|
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,
|
dataSourceType: dataSource.type,
|
||||||
resourceName: resource.name,
|
resourceId: resource.id,
|
||||||
resourceType: resource.type,
|
resourceType: resource.type,
|
||||||
scanId,
|
scanId,
|
||||||
status: SecretScanningFindingStatus.Unresolved
|
scanStatus: SecretScanningScanStatus.Completed,
|
||||||
})),
|
scanType: SecretScanningScanType.FullScan,
|
||||||
["projectId", "fingerprint"],
|
numberOfSecretsDetected: findingsPayload.length
|
||||||
tx,
|
}
|
||||||
["resourceName", "dataSourceName", "status"]
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
|
logger.info(`secretScanningV2Queue: Full Scan Complete ${logDetails} findings=[${findingsPayload.length}]`);
|
||||||
|
} catch (error) {
|
||||||
|
if (retryCount === retryLimit) {
|
||||||
|
const errorMessage = parseScanErrorMessage(error);
|
||||||
|
|
||||||
await secretScanningV2DAL.scans.update(
|
await secretScanningV2DAL.scans.update(
|
||||||
{ id: scanId },
|
{ id: scanId },
|
||||||
{
|
{
|
||||||
status: SecretScanningScanStatus.Completed
|
status: SecretScanningScanStatus.Failed,
|
||||||
|
statusMessage: errorMessage
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: send notification
|
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
||||||
|
|
||||||
logger.info(`secretScanningV2Queue: Full Scan Complete ${logDetails}`);
|
|
||||||
} catch (error) {
|
|
||||||
await secretScanningV2DAL.scans.update(
|
|
||||||
{ id: scanId },
|
|
||||||
{
|
|
||||||
status: SecretScanningScanStatus.Failed,
|
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}`);
|
logger.error(error, `secretScanningV2Queue: Full Scan Failed ${logDetails}`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await deleteTempFolder(tempFolder);
|
await deleteTempFolder(tempFolder);
|
||||||
|
await lock?.release();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
batchSize: 1,
|
batchSize: 1,
|
||||||
workerCount: 2,
|
workerCount: 20,
|
||||||
pollingIntervalSeconds: 1
|
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>(
|
await queueService.startPg<QueueName.SecretScanningV2>(
|
||||||
QueueJobs.SecretScanningV2DiffScan,
|
QueueJobs.SecretScanningV2DiffScan,
|
||||||
async ([job]) => {
|
async ([job]) => {
|
||||||
const { payload, dataSourceId } = job.data as TQueueSecretScanningResourceDiffScan;
|
const { payload, dataSourceId, resourceId, scanId } = job.data as TQueueSecretScanningResourceDiffScan;
|
||||||
const { retryCount, retryLimit } = job;
|
const { retryCount, retryLimit } = job;
|
||||||
|
|
||||||
let scanId: string | undefined;
|
const logDetails = `[dataSourceId=${dataSourceId}] [scanId=${scanId}] [resourceId=${resourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
|
||||||
let logDetails = `[dataSourceId=${dataSourceId}] [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 {
|
try {
|
||||||
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
|
await secretScanningV2DAL.scans.update(
|
||||||
|
{ id: scanId },
|
||||||
if (!dataSource) throw new Error(`Data source with ID "${dataSourceId}" not found`);
|
{
|
||||||
|
status: SecretScanningScanStatus.Scanning
|
||||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
|
|
||||||
|
|
||||||
const resourcePayload = factory.getDiffScanResourcePayload(payload);
|
|
||||||
|
|
||||||
const { resourceId, resourceName, resourceType } = await secretScanningV2DAL.resources.transaction(
|
|
||||||
async (tx) => {
|
|
||||||
const [resource] = await secretScanningV2DAL.resources.upsert(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
...resourcePayload,
|
|
||||||
dataSourceId
|
|
||||||
}
|
|
||||||
],
|
|
||||||
["externalId", "dataSourceId"],
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
const scan = await secretScanningV2DAL.scans.create(
|
|
||||||
{
|
|
||||||
resourceId: resource.id,
|
|
||||||
type: SecretScanningScanType.DiffScan,
|
|
||||||
status: SecretScanningScanStatus.Scanning
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
scanId = scan.id;
|
|
||||||
|
|
||||||
return {
|
|
||||||
resourceId: resource.id,
|
|
||||||
resourceName: resource.name,
|
|
||||||
resourceType: resource.type
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
logDetails += ` [scanId=${scanId}] [resourceId=${resourceId}]`;
|
|
||||||
|
|
||||||
let connection: TAppConnection | null = null;
|
let connection: TAppConnection | null = null;
|
||||||
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
|
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
|
||||||
|
|
||||||
@@ -277,49 +392,107 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
...dataSource,
|
...dataSource,
|
||||||
connection
|
connection
|
||||||
} as TSecretScanningDataSourceWithConnection,
|
} as TSecretScanningDataSourceWithConnection,
|
||||||
resourceName,
|
resourceName: resource.name,
|
||||||
payload
|
payload
|
||||||
});
|
});
|
||||||
|
|
||||||
await secretScanningV2DAL.findings.transaction(async (tx) => {
|
if (findingsPayload.length) {
|
||||||
await secretScanningV2DAL.findings.upsert(
|
await secretScanningV2DAL.findings.transaction(async (tx) => {
|
||||||
findingsPayload.map((findings) => ({
|
await secretScanningV2DAL.findings.upsert(
|
||||||
...findings,
|
findingsPayload.map((findings) => ({
|
||||||
projectId: dataSource.projectId,
|
...findings,
|
||||||
dataSourceName: dataSource.name,
|
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,
|
dataSourceType: dataSource.type,
|
||||||
resourceName,
|
resourceId,
|
||||||
resourceType,
|
resourceType: resource.type,
|
||||||
scanId,
|
scanId,
|
||||||
status: SecretScanningFindingStatus.Unresolved
|
scanStatus: SecretScanningScanStatus.Completed,
|
||||||
})),
|
scanType: SecretScanningScanType.DiffScan,
|
||||||
["projectId", "fingerprint"],
|
numberOfSecretsDetected: findingsPayload.length
|
||||||
tx,
|
|
||||||
["resourceName", "dataSourceName", "status"]
|
|
||||||
);
|
|
||||||
|
|
||||||
await secretScanningV2DAL.scans.update(
|
|
||||||
{ id: scanId },
|
|
||||||
{
|
|
||||||
status: SecretScanningScanStatus.Completed
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: send notification
|
|
||||||
|
|
||||||
logger.info(`secretScanningV2Queue: Diff Scan Complete ${logDetails}`);
|
logger.info(`secretScanningV2Queue: Diff Scan Complete ${logDetails}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (scanId)
|
if (retryCount === retryLimit) {
|
||||||
|
const errorMessage = parseScanErrorMessage(error);
|
||||||
|
|
||||||
await secretScanningV2DAL.scans.update(
|
await secretScanningV2DAL.scans.update(
|
||||||
{ id: scanId },
|
{ id: scanId },
|
||||||
{
|
{
|
||||||
status: SecretScanningScanStatus.Failed,
|
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}`);
|
logger.error(error, `secretScanningV2Queue: Diff Scan Failed ${logDetails}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -327,67 +500,84 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
batchSize: 1,
|
batchSize: 1,
|
||||||
workerCount: 2,
|
workerCount: 20,
|
||||||
pollingIntervalSeconds: 1
|
pollingIntervalSeconds: 1
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// await queueService.startPg<QueueName.SecretRotationV2>(
|
await queueService.startPg<QueueName.SecretScanningV2>(
|
||||||
// QueueJobs.SecretRotationV2SendNotification,
|
QueueJobs.SecretScanningV2SendNotification,
|
||||||
// async ([job]) => {
|
async ([job]) => {
|
||||||
// const { secretRotation } = job.data as TSecretRotationSendNotificationJobPayload;
|
const { dataSource, resourceName, ...payload } = job.data as TQueueSecretScanningSendNotification;
|
||||||
// try {
|
|
||||||
// const {
|
const appCfg = getConfig();
|
||||||
// name: rotationName,
|
|
||||||
// type,
|
if (!appCfg.isSmtpConfigured) return;
|
||||||
// projectId,
|
|
||||||
// lastRotationAttemptedAt,
|
try {
|
||||||
// folder,
|
const { projectId } = dataSource;
|
||||||
// environment,
|
|
||||||
// id: dataSourceId
|
logger.info(
|
||||||
// } = secretRotation;
|
`secretScanningV2Queue: Sending Status Notification [dataSourceId=${dataSource.id}] [resourceName=${resourceName}] [status=${payload.status}]`
|
||||||
//
|
);
|
||||||
// logger.info(`secretRotationV2Queue: Sending Status Notification [dataSourceId=${dataSourceId}]`);
|
|
||||||
//
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
// const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
// const project = await projectDAL.findById(projectId);
|
|
||||||
//
|
const projectAdmins = projectMembers.filter((member) =>
|
||||||
// const projectAdmins = projectMembers.filter((member) =>
|
member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
|
||||||
// member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
|
);
|
||||||
// );
|
|
||||||
//
|
const timestamp = new Date().toISOString();
|
||||||
// const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
|
|
||||||
//
|
await smtpService.sendMail({
|
||||||
// await smtpService.sendMail({
|
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||||
// recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
template:
|
||||||
// template: SmtpTemplates.SecretRotationFailed,
|
payload.status === SecretScanningScanStatus.Completed
|
||||||
// subjectLine: `Secret Rotation Failed`,
|
? SmtpTemplates.SecretScanningV2SecretsDetected
|
||||||
// substitutions: {
|
: SmtpTemplates.SecretScanningV2ScanFailed,
|
||||||
// rotationName,
|
subjectLine:
|
||||||
// rotationType,
|
payload.status === SecretScanningScanStatus.Completed
|
||||||
// content: `Your ${rotationType} Rotation failed to rotate during it's scheduled rotation. The last rotation attempt occurred at ${new Date(
|
? "Incident Alert: Secret(s) Leaked"
|
||||||
// lastRotationAttemptedAt
|
: `Secret Scanning Failed`,
|
||||||
// ).toISOString()}. Please check the rotation status in Infisical for more details.`,
|
substitutions:
|
||||||
// secretPath: folder.path,
|
payload.status === SecretScanningScanStatus.Completed
|
||||||
// environment: environment.name,
|
? {
|
||||||
// projectName: project.name,
|
authorName: "Jim",
|
||||||
// rotationUrl: encodeURI(`${appCfg.SITE_URL}/secret-manager/${projectId}/secrets/${environment.slug}`)
|
authorEmail: "[email protected]",
|
||||||
// }
|
resourceName,
|
||||||
// });
|
numberOfSecrets: payload.numberOfSecrets,
|
||||||
// } catch (error) {
|
isDiffScan: payload.isDiffScan,
|
||||||
// logger.error(
|
url: encodeURI(
|
||||||
// error,
|
`${appCfg.SITE_URL}/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
|
||||||
// `secretRotationV2Queue: Failed to Send Status Notification [dataSourceId=${secretRotation.id}]`
|
),
|
||||||
// );
|
timestamp
|
||||||
// throw error;
|
}
|
||||||
// }
|
: {
|
||||||
// },
|
dataSourceName: dataSource.name,
|
||||||
// {
|
resourceName,
|
||||||
// batchSize: 1,
|
projectName: project.name,
|
||||||
// workerCount: 2,
|
timestamp,
|
||||||
// pollingIntervalSeconds: 1
|
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 {
|
return {
|
||||||
queueDataSourceFullScan,
|
queueDataSourceFullScan,
|
||||||
|
|||||||
@@ -49,10 +49,6 @@ export type TSecretScanningV2ServiceFactoryDep = {
|
|||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
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<
|
secretScanningV2Queue: Pick<
|
||||||
TSecretScanningV2QueueServiceFactory,
|
TSecretScanningV2QueueServiceFactory,
|
||||||
"queueDataSourceFullScan" | "queueResourceDiffScan"
|
"queueDataSourceFullScan" | "queueResourceDiffScan"
|
||||||
@@ -67,10 +63,6 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
// auditLogService,
|
|
||||||
// keyStore,
|
|
||||||
// queueService,
|
|
||||||
// appConnectionDAL,
|
|
||||||
secretScanningV2Queue,
|
secretScanningV2Queue,
|
||||||
kmsService
|
kmsService
|
||||||
}: TSecretScanningV2ServiceFactoryDep) => {
|
}: TSecretScanningV2ServiceFactoryDep) => {
|
||||||
@@ -374,7 +366,7 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSecretScanningResource = async (
|
const deleteSecretScanningDataSource = async (
|
||||||
{ type, dataSourceId }: TDeleteSecretScanningDataSourceDTO,
|
{ type, dataSourceId }: TDeleteSecretScanningDataSourceDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
@@ -750,7 +742,7 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionSecretScanningFindingActions.Resolve,
|
ProjectPermissionSecretScanningFindingActions.Update,
|
||||||
ProjectPermissionSub.SecretScanningFindings
|
ProjectPermissionSub.SecretScanningFindings
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -770,7 +762,7 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
findSecretScanningDataSourceByName,
|
findSecretScanningDataSourceByName,
|
||||||
createSecretScanningDataSource,
|
createSecretScanningDataSource,
|
||||||
updateSecretScanningDataSource,
|
updateSecretScanningDataSource,
|
||||||
deleteSecretScanningResource,
|
deleteSecretScanningDataSource,
|
||||||
triggerSecretScanningDataSourceScan,
|
triggerSecretScanningDataSourceScan,
|
||||||
listSecretScanningResourcesByDataSourceId,
|
listSecretScanningResourcesByDataSourceId,
|
||||||
listSecretScanningScansByDataSourceId,
|
listSecretScanningScansByDataSourceId,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { TSecretScanningFindingsInsert, TSecretScanningResources, TSecretScanningScans } from "@app/db/schemas";
|
import {
|
||||||
|
TSecretScanningDataSources,
|
||||||
|
TSecretScanningFindingsInsert,
|
||||||
|
TSecretScanningResources,
|
||||||
|
TSecretScanningScans
|
||||||
|
} from "@app/db/schemas";
|
||||||
import {
|
import {
|
||||||
TGitHubDataSource,
|
TGitHubDataSource,
|
||||||
TGitHubDataSourceInput,
|
TGitHubDataSourceInput,
|
||||||
@@ -96,6 +101,14 @@ export type TQueueSecretScanningDataSourceFullScan = {
|
|||||||
|
|
||||||
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan;
|
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 = {
|
export type TCloneRepository = {
|
||||||
cloneUrl: string;
|
cloneUrl: string;
|
||||||
repoPath: string;
|
repoPath: string;
|
||||||
@@ -163,7 +176,7 @@ export type TFindingsPayload = Pick<TSecretScanningFindingsInsert, "details" | "
|
|||||||
export type TGetFindingsPayload = Promise<TFindingsPayload>;
|
export type TGetFindingsPayload = Promise<TFindingsPayload>;
|
||||||
|
|
||||||
export type TUpdateSecretScanningFinding = {
|
export type TUpdateSecretScanningFinding = {
|
||||||
status: SecretScanningFindingStatus;
|
status?: SecretScanningFindingStatus;
|
||||||
remarks?: string | null;
|
remarks?: string | null;
|
||||||
findingId: string;
|
findingId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const KeyStorePrefixes = {
|
|||||||
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
||||||
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` 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,
|
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||||
`identity-access-token-status:${identityAccessTokenId}`,
|
`identity-access-token-status:${identityAccessTokenId}`,
|
||||||
|
|||||||
@@ -2408,7 +2408,12 @@ export const SecretScanningDataSources = {
|
|||||||
}),
|
}),
|
||||||
LIST_SCANS: (type: SecretScanningDataSource) => ({
|
LIST_SCANS: (type: SecretScanningDataSource) => ({
|
||||||
dataSourceId: `The ID of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to list scans for.`
|
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 = {
|
export const SecretScanningFindings = {
|
||||||
@@ -2416,8 +2421,8 @@ export const SecretScanningFindings = {
|
|||||||
projectId: `The ID of the project to list Secret Scanning Findings from.`
|
projectId: `The ID of the project to list Secret Scanning Findings from.`
|
||||||
},
|
},
|
||||||
UPDATE: {
|
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.",
|
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_GIT_APP_ID) &&
|
||||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
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:
|
isHsmConfigured:
|
||||||
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
|
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;
|
return acc;
|
||||||
}, {} as T);
|
}, {} 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";
|
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||||
import {
|
import {
|
||||||
TQueueSecretScanningDataSourceFullScan,
|
TQueueSecretScanningDataSourceFullScan,
|
||||||
TQueueSecretScanningResourceDiffScan
|
TQueueSecretScanningResourceDiffScan,
|
||||||
|
TQueueSecretScanningSendNotification
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@@ -91,7 +92,8 @@ export enum QueueJobs {
|
|||||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||||
InvalidateCache = "invalidate-cache",
|
InvalidateCache = "invalidate-cache",
|
||||||
SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
|
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 = {
|
export type TQueueJobTypes = {
|
||||||
@@ -260,6 +262,10 @@ export type TQueueJobTypes = {
|
|||||||
| {
|
| {
|
||||||
name: QueueJobs.SecretScanningV2DiffScan;
|
name: QueueJobs.SecretScanningV2DiffScan;
|
||||||
payload: TQueueSecretScanningResourceDiffScan;
|
payload: TQueueSecretScanningResourceDiffScan;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: QueueJobs.SecretScanningV2SendNotification;
|
||||||
|
payload: TQueueSecretScanningSendNotification;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
|
|||||||
const { payload } = context;
|
const { payload } = context;
|
||||||
const { installation } = payload;
|
const { installation } = payload;
|
||||||
|
|
||||||
await server.services.secretScanningV2.github.handleInstallationDeleted(installation.id);
|
await server.services.secretScanningV2.github.handleInstallationDeletedEvent(installation.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("installation", async (context) => {
|
app.on("installation", async (context) => {
|
||||||
@@ -28,6 +28,11 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
|
|||||||
|
|
||||||
const appCfg = getConfig();
|
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({
|
const probot = new Probot({
|
||||||
appId: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_ID as string,
|
appId: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_ID as string,
|
||||||
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY 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);
|
await probot.load(probotApp);
|
||||||
|
|
||||||
|
// github push event webhook
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/github",
|
url: "/github",
|
||||||
|
|||||||
@@ -1703,22 +1703,20 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
|
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
|
||||||
|
auditLogService,
|
||||||
secretScanningV2DAL,
|
secretScanningV2DAL,
|
||||||
queueService,
|
queueService,
|
||||||
// projectDAL,
|
projectDAL,
|
||||||
// projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
// smtpService,
|
smtpService,
|
||||||
kmsService
|
kmsService,
|
||||||
|
keyStore
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
||||||
// appConnectionDAL,
|
|
||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
// auditLogService,
|
|
||||||
// keyStore,
|
|
||||||
// queueService,
|
|
||||||
secretScanningV2DAL,
|
secretScanningV2DAL,
|
||||||
secretScanningV2Queue,
|
secretScanningV2Queue,
|
||||||
kmsService
|
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: "[email protected]",
|
||||||
|
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 "./SecretReminderTemplate";
|
||||||
export * from "./SecretRequestCompletedTemplate";
|
export * from "./SecretRequestCompletedTemplate";
|
||||||
export * from "./SecretRotationFailedTemplate";
|
export * from "./SecretRotationFailedTemplate";
|
||||||
|
export * from "./SecretScanningScanFailedTemplate";
|
||||||
|
export * from "./SecretScanningSecretsDetectedTemplate";
|
||||||
export * from "./SecretSyncFailedTemplate";
|
export * from "./SecretSyncFailedTemplate";
|
||||||
export * from "./ServiceTokenExpiryNoticeTemplate";
|
export * from "./ServiceTokenExpiryNoticeTemplate";
|
||||||
export * from "./SignupEmailVerificationTemplate";
|
export * from "./SignupEmailVerificationTemplate";
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import {
|
|||||||
SecretReminderTemplate,
|
SecretReminderTemplate,
|
||||||
SecretRequestCompletedTemplate,
|
SecretRequestCompletedTemplate,
|
||||||
SecretRotationFailedTemplate,
|
SecretRotationFailedTemplate,
|
||||||
|
SecretScanningScanFailedTemplate,
|
||||||
|
SecretScanningSecretsDetectedTemplate,
|
||||||
SecretSyncFailedTemplate,
|
SecretSyncFailedTemplate,
|
||||||
ServiceTokenExpiryNoticeTemplate,
|
ServiceTokenExpiryNoticeTemplate,
|
||||||
SignupEmailVerificationTemplate,
|
SignupEmailVerificationTemplate,
|
||||||
@@ -73,7 +75,9 @@ export enum SmtpTemplates {
|
|||||||
ProjectAccessRequest = "projectAccess",
|
ProjectAccessRequest = "projectAccess",
|
||||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess",
|
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess",
|
||||||
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess",
|
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess",
|
||||||
ServiceTokenExpired = "serviceTokenExpired"
|
ServiceTokenExpired = "serviceTokenExpired",
|
||||||
|
SecretScanningV2ScanFailed = "secretScanningV2ScanFailed",
|
||||||
|
SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
export enum SmtpHost {
|
||||||
@@ -113,7 +117,9 @@ const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
|
|||||||
[SmtpTemplates.SecretApprovalRequestNeedsReview]: SecretApprovalRequestNeedsReviewTemplate,
|
[SmtpTemplates.SecretApprovalRequestNeedsReview]: SecretApprovalRequestNeedsReviewTemplate,
|
||||||
[SmtpTemplates.ResetPassword]: PasswordResetTemplate,
|
[SmtpTemplates.ResetPassword]: PasswordResetTemplate,
|
||||||
[SmtpTemplates.SetupPassword]: PasswordSetupTemplate,
|
[SmtpTemplates.SetupPassword]: PasswordSetupTemplate,
|
||||||
[SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate
|
[SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate,
|
||||||
|
[SmtpTemplates.SecretScanningV2ScanFailed]: SecretScanningScanFailedTemplate,
|
||||||
|
[SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate
|
||||||
};
|
};
|
||||||
|
|
||||||
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||||
|
|||||||
@@ -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.
|
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
|
### 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.
|
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
|
||||||
|
|
||||||
Scans can be initiated in two ways:
|
Scans can be initiated in two ways:
|
||||||
|
|
||||||
1. **Full Scan** - Manually triggered scan that comprehensively checks either:
|
1. **Full Scan** - Manually triggered scan that comprehensively checks either all resources associated with a data source or a single selected resource.
|
||||||
- All resources associated with a data source
|
|
||||||
- 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.
|
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
|
- Resource(s) being scanned
|
||||||
- Detection results (whether any secrets were found)
|
- 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
|
## 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.
|
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 |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -245,7 +245,8 @@
|
|||||||
{
|
{
|
||||||
"group": "Secret Scanning",
|
"group": "Secret Scanning",
|
||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/secret-scanning/overview"
|
"documentation/platform/secret-scanning/overview",
|
||||||
|
"documentation/platform/secret-scanning/github"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export enum ProjectPermissionSecretScanningDataSourceActions {
|
|||||||
|
|
||||||
export enum ProjectPermissionSecretScanningFindingActions {
|
export enum ProjectPermissionSecretScanningFindingActions {
|
||||||
Read = "read-findings",
|
Read = "read-findings",
|
||||||
Resolve = "resolve-findings"
|
Update = "update-findings"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PermissionConditionOperators {
|
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_UPDATE]: "Update Secret Scanning Data Source",
|
||||||
[EventType.SECRET_SCANNING_DATA_SOURCE_DELETE]: "Delete 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_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_RESOURCE_LIST]: "List Secret Scanning Resources",
|
||||||
[EventType.SECRET_SCANNING_SCAN_LIST]: "List Secret Scanning Scans",
|
[EventType.SECRET_SCANNING_SCAN_LIST]: "List Secret Scanning Scans",
|
||||||
[EventType.SECRET_SCANNING_FINDING_LIST]: "List Secret Scanning Findings",
|
[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_UPDATE = "secret-scanning-data-source-update",
|
||||||
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
|
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
|
||||||
SECRET_SCANNING_DATA_SOURCE_GET = "secret-scanning-data-source-get",
|
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_DATA_SOURCE_SCAN = "secret-scanning-data-source-scan",
|
||||||
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
|
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
|
||||||
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
|
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ export type TSecretScanningDataSourceBase = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
isAutoScanEnabled: boolean;
|
isAutoScanEnabled: boolean;
|
||||||
|
isDisconnected: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,4 +50,5 @@ export type SubscriptionPlan = {
|
|||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
projectTemplates: boolean;
|
projectTemplates: boolean;
|
||||||
kmip: boolean;
|
kmip: boolean;
|
||||||
|
secretScanning: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export const ProjectLayout = () => {
|
|||||||
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(workspaceId, {
|
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(workspaceId, {
|
||||||
enabled:
|
enabled:
|
||||||
isSecretScanning &&
|
isSecretScanning &&
|
||||||
|
subscription.secretScanning &&
|
||||||
permission.can(
|
permission.can(
|
||||||
ProjectPermissionSecretScanningFindingActions.Read,
|
ProjectPermissionSecretScanningFindingActions.Read,
|
||||||
ProjectPermissionSub.SecretScanningFindings
|
ProjectPermissionSub.SecretScanningFindings
|
||||||
@@ -264,7 +265,7 @@ export const ProjectLayout = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
|
<MenuItem isSelected={isActive} icon="blocks">
|
||||||
Data Sources
|
Data Sources
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from "@app/components/v2";
|
|||||||
import { useProjectPermission, useWorkspace } from "@app/context";
|
import { useProjectPermission, useWorkspace } from "@app/context";
|
||||||
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
|
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
|
||||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
export const AssumePrivilegeModeBanner = () => {
|
export const AssumePrivilegeModeBanner = () => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
@@ -35,7 +36,20 @@ export const AssumePrivilegeModeBanner = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
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
|
useGetWorkspaceUserDetails
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
||||||
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||||
@@ -70,7 +71,21 @@ export const Page = () => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
text: "User privilege assumption has started"
|
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({
|
const SecretScanningFindingPolicyActionSchema = z.object({
|
||||||
[ProjectPermissionSecretScanningFindingActions.Read]: z.boolean().optional(),
|
[ProjectPermissionSecretScanningFindingActions.Read]: z.boolean().optional(),
|
||||||
[ProjectPermissionSecretScanningFindingActions.Resolve]: z.boolean().optional()
|
[ProjectPermissionSecretScanningFindingActions.Update]: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const KmipPolicyActionSchema = z.object({
|
const KmipPolicyActionSchema = z.object({
|
||||||
@@ -731,6 +731,52 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true;
|
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 (subject === ProjectPermissionSub.SshHosts) {
|
||||||
if (!formVal[subject]) formVal[subject] = [];
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
|
||||||
@@ -1313,8 +1359,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
value: ProjectPermissionSecretScanningFindingActions.Read
|
value: ProjectPermissionSecretScanningFindingActions.Read
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Resolve Finding Status",
|
label: "Update Findings",
|
||||||
value: ProjectPermissionSecretScanningFindingActions.Resolve
|
value: ProjectPermissionSecretScanningFindingActions.Update
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ const PageContent = () => {
|
|||||||
<p className="text-3xl font-semibold text-white">{dataSource.name}</p>
|
<p className="text-3xl font-semibold text-white">{dataSource.name}</p>
|
||||||
<p className="leading-3 text-bunker-300">{details.name} Data Source</p>
|
<p className="leading-3 text-bunker-300">{details.name} Data Source</p>
|
||||||
</div>
|
</div>
|
||||||
{/* <SecretSyncActionTriggers dataSource={dataSource} /> */}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
<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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { EditSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
import { EditSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
||||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
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 { ProjectPermissionSub } from "@app/context";
|
||||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
@@ -18,13 +18,31 @@ type Props = {
|
|||||||
export const SecretScanningDataSourceSection = ({ dataSource }: Props) => {
|
export const SecretScanningDataSourceSection = ({ dataSource }: Props) => {
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editDataSource"] as const);
|
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editDataSource"] as const);
|
||||||
|
|
||||||
const { name, description, connection, isAutoScanEnabled } = dataSource;
|
const { name, description, connection, isAutoScanEnabled, isDisconnected } = dataSource;
|
||||||
|
|
||||||
return (
|
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 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">
|
<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
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionSecretScanningDataSourceActions.Edit}
|
I={ProjectPermissionSecretScanningDataSourceActions.Edit}
|
||||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const SecretScanningScanRow = ({ scan }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="whitespace-nowrap">
|
<Td className="whitespace-nowrap">
|
||||||
{type === SecretScanningScanType.FullScan ? "Full scan" : "Diff scan"}
|
{type === SecretScanningScanType.FullScan ? "Full Scan" : "Diff Scan"}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionSecretScanningDataSourceActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
import { SecretScanningDataSourcesSection } from "./components";
|
import { SecretScanningDataSourcesSection } from "./components";
|
||||||
|
|
||||||
@@ -12,17 +17,23 @@ export const SecretScanningDataSourcesPage = () => {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
|
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="h-full bg-bunker-800">
|
<ProjectPermissionCan
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
renderGuardBanner
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
I={ProjectPermissionSecretScanningDataSourceActions.Read}
|
||||||
<PageHeader
|
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||||
title="Data Sources"
|
>
|
||||||
description="Manage your Secret Scanning data sources."
|
<div className="h-full bg-bunker-800">
|
||||||
/>
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
<SecretScanningDataSourcesSection />
|
<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>
|
</div>
|
||||||
</div>
|
</ProjectPermissionCan>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
faExpand,
|
faExpand,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
|
faPlugCircleXmark,
|
||||||
faSearch,
|
faSearch,
|
||||||
faToggleOff,
|
faToggleOff,
|
||||||
faToggleOn,
|
faToggleOn,
|
||||||
@@ -72,7 +73,8 @@ export const SecretScanningDataSourceRow = ({
|
|||||||
unresolvedFindings,
|
unresolvedFindings,
|
||||||
lastScannedAt,
|
lastScannedAt,
|
||||||
lastScanStatus,
|
lastScanStatus,
|
||||||
lastScanStatusMessage
|
lastScanStatusMessage,
|
||||||
|
isDisconnected
|
||||||
} = dataSource;
|
} = dataSource;
|
||||||
|
|
||||||
const sourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
const sourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||||
@@ -199,7 +201,7 @@ export const SecretScanningDataSourceRow = ({
|
|||||||
) : (
|
) : (
|
||||||
<span className="text-mineshaft-400">No scans</span>
|
<span className="text-mineshaft-400">No scans</span>
|
||||||
)}
|
)}
|
||||||
{!isAutoScanEnabled && (
|
{!isAutoScanEnabled && !isDisconnected && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="text-xs"
|
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`}
|
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>
|
</div>
|
||||||
</Tooltip>
|
</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>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { CreateSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
import { CreateSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
||||||
import { Button, Spinner } from "@app/components/v2";
|
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 { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useListSecretScanningDataSources } from "@app/hooks/api/secretScanningV2";
|
import { useListSecretScanningDataSources } from "@app/hooks/api/secretScanningV2";
|
||||||
@@ -12,7 +13,12 @@ import { useListSecretScanningDataSources } from "@app/hooks/api/secretScanningV
|
|||||||
import { SecretScanningDataSourcesTable } from "./SecretScanningDataSourcesTable";
|
import { SecretScanningDataSourcesTable } from "./SecretScanningDataSourcesTable";
|
||||||
|
|
||||||
export const SecretScanningDataSourcesSection = () => {
|
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();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
@@ -63,7 +69,14 @@ export const SecretScanningDataSourcesSection = () => {
|
|||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpOpen("addDataSource")}
|
onClick={() => {
|
||||||
|
if (!subscription.secretScanning) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("addDataSource");
|
||||||
|
}}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add Data Source
|
Add Data Source
|
||||||
@@ -77,6 +90,11 @@ export const SecretScanningDataSourcesSection = () => {
|
|||||||
isOpen={popUp.addDataSource.isOpen}
|
isOpen={popUp.addDataSource.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("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,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
|
faCubesStacked,
|
||||||
faFilter,
|
faFilter,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPuzzlePiece,
|
|
||||||
faSearch
|
faSearch
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -368,7 +368,7 @@ export const SecretScanningDataSourcesTable = ({ dataSources }: Props) => {
|
|||||||
? "No data sources match search..."
|
? "No data sources match search..."
|
||||||
: "This project has no data sources configured"
|
: "This project has no data sources configured"
|
||||||
}
|
}
|
||||||
icon={dataSources.length ? faSearch : faPuzzlePiece}
|
icon={dataSources.length ? faSearch : faCubesStacked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
import { SecretScanningFindingsSection } from "./components";
|
import { SecretScanningFindingsSection } from "./components";
|
||||||
|
|
||||||
@@ -12,14 +15,20 @@ export const SecretScanningFindingsPage = () => {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
|
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="h-full bg-bunker-800">
|
<ProjectPermissionCan
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
renderGuardBanner
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
I={ProjectPermissionSecretScanningFindingActions.Read}
|
||||||
<PageHeader title="Findings" description="View Secret Leaks across your project." />
|
a={ProjectPermissionSub.SecretScanningFindings}
|
||||||
<SecretScanningFindingsSection />
|
>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</ProjectPermissionCan>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { format } from "date-fns";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -18,6 +19,10 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionSecretScanningFindingActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import {
|
import {
|
||||||
SECRET_SCANNING_DATA_SOURCE_MAP,
|
SECRET_SCANNING_DATA_SOURCE_MAP,
|
||||||
SECRET_SCANNING_FINDING_STATUS_ICON_MAP
|
SECRET_SCANNING_FINDING_STATUS_ICON_MAP
|
||||||
@@ -191,19 +196,32 @@ export const SecretScanningFindingRow = ({ finding, onUpdate }: Props) => {
|
|||||||
{details.link}
|
{details.link}
|
||||||
</a>
|
</a>
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
|
<div className="col-span-full flex items-center border-t border-mineshaft-500" />
|
||||||
<Button
|
<ProjectPermissionCan
|
||||||
onClick={() => onUpdate(finding)}
|
I={ProjectPermissionSecretScanningFindingActions.Update}
|
||||||
colorSchema="secondary"
|
a={ProjectPermissionSub.SecretScanningFindings}
|
||||||
leftIcon={
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].className}
|
|
||||||
icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].icon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Update Status
|
{(isAllowed) => (
|
||||||
</Button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export const SecretScanningUpdateFindingModal = ({ finding, isOpen, onOpenChange
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<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)} />
|
<Content finding={finding} onComplete={() => onOpenChange(false)} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||