mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 21:27:31 +00:00
feature: secret scanning pt 3
This commit is contained in:
Vendored
+8
@@ -324,6 +324,9 @@ import {
|
|||||||
TSecretRotationV2SecretMappingsInsert,
|
TSecretRotationV2SecretMappingsInsert,
|
||||||
TSecretRotationV2SecretMappingsUpdate,
|
TSecretRotationV2SecretMappingsUpdate,
|
||||||
TSecrets,
|
TSecrets,
|
||||||
|
TSecretScanningConfigs,
|
||||||
|
TSecretScanningConfigsInsert,
|
||||||
|
TSecretScanningConfigsUpdate,
|
||||||
TSecretScanningDataSources,
|
TSecretScanningDataSources,
|
||||||
TSecretScanningDataSourcesInsert,
|
TSecretScanningDataSourcesInsert,
|
||||||
TSecretScanningDataSourcesUpdate,
|
TSecretScanningDataSourcesUpdate,
|
||||||
@@ -1106,5 +1109,10 @@ declare module "knex/types/tables" {
|
|||||||
TSecretScanningFindingsInsert,
|
TSecretScanningFindingsInsert,
|
||||||
TSecretScanningFindingsUpdate
|
TSecretScanningFindingsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.SecretScanningConfig]: KnexOriginal.CompositeTableType<
|
||||||
|
TSecretScanningConfigs,
|
||||||
|
TSecretScanningConfigsInsert,
|
||||||
|
TSecretScanningConfigsUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,16 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
await createOnUpdateTrigger(knex, TableName.SecretScanningFinding);
|
await createOnUpdateTrigger(knex, TableName.SecretScanningFinding);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rules
|
if (!(await knex.schema.hasTable(TableName.SecretScanningConfig))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretScanningConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("projectId").notNullable().unique();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.string("content", 5000);
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretScanningConfig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
@@ -92,4 +101,7 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
|
|
||||||
await knex.schema.dropTableIfExists(TableName.SecretScanningDataSource);
|
await knex.schema.dropTableIfExists(TableName.SecretScanningDataSource);
|
||||||
await dropOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
|
await dropOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretScanningConfig);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SecretScanningConfig);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export * from "./secret-rotation-outputs";
|
|||||||
export * from "./secret-rotation-v2-secret-mappings";
|
export * from "./secret-rotation-v2-secret-mappings";
|
||||||
export * from "./secret-rotations";
|
export * from "./secret-rotations";
|
||||||
export * from "./secret-rotations-v2";
|
export * from "./secret-rotations-v2";
|
||||||
|
export * from "./secret-scanning-configs";
|
||||||
export * from "./secret-scanning-data-sources";
|
export * from "./secret-scanning-data-sources";
|
||||||
export * from "./secret-scanning-findings";
|
export * from "./secret-scanning-findings";
|
||||||
export * from "./secret-scanning-git-risks";
|
export * from "./secret-scanning-git-risks";
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ export enum TableName {
|
|||||||
SecretScanningDataSource = "secret_scanning_data_sources",
|
SecretScanningDataSource = "secret_scanning_data_sources",
|
||||||
SecretScanningResource = "secret_scanning_resources",
|
SecretScanningResource = "secret_scanning_resources",
|
||||||
SecretScanningScan = "secret_scanning_scans",
|
SecretScanningScan = "secret_scanning_scans",
|
||||||
SecretScanningFinding = "secret_scanning_findings"
|
SecretScanningFinding = "secret_scanning_findings",
|
||||||
|
SecretScanningConfig = "secret_scanning_configs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretScanningConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
projectId: z.string(),
|
||||||
|
content: z.string().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretScanningConfigs = z.infer<typeof SecretScanningConfigsSchema>;
|
||||||
|
export type TSecretScanningConfigsInsert = Omit<z.input<typeof SecretScanningConfigsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSecretScanningConfigsUpdate = Partial<Omit<z.input<typeof SecretScanningConfigsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretScanningConfigsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
|
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +11,12 @@ import {
|
|||||||
SecretScanningDataSourceSchema,
|
SecretScanningDataSourceSchema,
|
||||||
SecretScanningFindingSchema
|
SecretScanningFindingSchema
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas";
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas";
|
||||||
import { ApiDocsTags, SecretScanningDataSources, SecretScanningFindings } from "@app/lib/api-docs";
|
import {
|
||||||
|
ApiDocsTags,
|
||||||
|
SecretScanningConfigs,
|
||||||
|
SecretScanningDataSources,
|
||||||
|
SecretScanningFindings
|
||||||
|
} from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -181,6 +187,96 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/configs/:projectId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SecretScanning],
|
||||||
|
description: "Get the Secret Scanning Config for the specified project.",
|
||||||
|
params: z.object({
|
||||||
|
projectId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Project ID required")
|
||||||
|
.describe(SecretScanningConfigs.GET_BY_PROJECT_ID.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
config: z.object({ content: z.string().nullish(), projectId: z.string(), updatedAt: z.date().nullish() })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { projectId },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const config = await server.services.secretScanningV2.findSecretScanningConfigByProjectId(projectId, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SECRET_SCANNING_CONFIG_GET
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { config };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/configs/:projectId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SecretScanning],
|
||||||
|
description: "Update the specified Secret Scanning Configuration.",
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningConfigs.UPDATE.projectId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
content: z.string().nullable().describe(SecretScanningConfigs.UPDATE.content)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ config: SecretScanningConfigsSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { projectId },
|
||||||
|
body,
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const config = await server.services.secretScanningV2.upsertSecretScanningConfig(
|
||||||
|
{ projectId, ...body },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SECRET_SCANNING_CONFIG_UPDATE,
|
||||||
|
metadata: body
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { config };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// not exposed, for UI only
|
// not exposed, for UI only
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -399,7 +399,9 @@ export enum EventType {
|
|||||||
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",
|
||||||
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
|
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
|
||||||
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update"
|
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update",
|
||||||
|
SECRET_SCANNING_CONFIG_GET = "secret-scanning-config-get",
|
||||||
|
SECRET_SCANNING_CONFIG_UPDATE = "secret-scanning-config-update"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterableSecretEvents: EventType[] = [
|
export const filterableSecretEvents: EventType[] = [
|
||||||
@@ -3020,6 +3022,18 @@ interface SecretScanningFindingUpdateEvent {
|
|||||||
metadata: TUpdateSecretScanningFinding;
|
metadata: TUpdateSecretScanningFinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecretScanningConfigUpdateEvent {
|
||||||
|
type: EventType.SECRET_SCANNING_CONFIG_UPDATE;
|
||||||
|
metadata: {
|
||||||
|
content: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecretScanningConfigReadEvent {
|
||||||
|
type: EventType.SECRET_SCANNING_CONFIG_GET;
|
||||||
|
metadata?: Record<string, never>; // not needed, based off projectId
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -3297,4 +3311,6 @@ export type Event =
|
|||||||
| SecretScanningResourceListEvent
|
| SecretScanningResourceListEvent
|
||||||
| SecretScanningScanListEvent
|
| SecretScanningScanListEvent
|
||||||
| SecretScanningFindingListEvent
|
| SecretScanningFindingListEvent
|
||||||
| SecretScanningFindingUpdateEvent;
|
| SecretScanningFindingUpdateEvent
|
||||||
|
| SecretScanningConfigUpdateEvent
|
||||||
|
| SecretScanningConfigReadEvent;
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
customRateLimits: false,
|
customRateLimits: false,
|
||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
secretAccessInsights: false,
|
secretAccessInsights: false,
|
||||||
auditLogs: false,
|
auditLogs: true,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 30,
|
||||||
auditLogStreams: false,
|
auditLogStreams: false,
|
||||||
auditLogStreamLimit: 3,
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
@@ -55,7 +55,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
kmip: false,
|
kmip: false,
|
||||||
gateway: false,
|
gateway: false,
|
||||||
sshHostGroups: false,
|
sshHostGroups: false,
|
||||||
secretScanning: false
|
secretScanning: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ProjectPermissionPkiSubscriberActions,
|
ProjectPermissionPkiSubscriberActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSecretRotationActions,
|
ProjectPermissionSecretRotationActions,
|
||||||
|
ProjectPermissionSecretScanningConfigActions,
|
||||||
ProjectPermissionSecretScanningDataSourceActions,
|
ProjectPermissionSecretScanningDataSourceActions,
|
||||||
ProjectPermissionSecretScanningFindingActions,
|
ProjectPermissionSecretScanningFindingActions,
|
||||||
ProjectPermissionSecretSyncActions,
|
ProjectPermissionSecretSyncActions,
|
||||||
@@ -218,6 +219,11 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.SecretScanningFindings
|
ProjectPermissionSub.SecretScanningFindings
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[ProjectPermissionSecretScanningConfigActions.Read, ProjectPermissionSecretScanningConfigActions.Update],
|
||||||
|
ProjectPermissionSub.SecretScanningConfigs
|
||||||
|
);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -413,6 +419,8 @@ const buildMemberPermissionRules = () => {
|
|||||||
ProjectPermissionSub.SecretScanningFindings
|
ProjectPermissionSub.SecretScanningFindings
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -459,6 +467,8 @@ const buildViewerPermissionRules = () => {
|
|||||||
|
|
||||||
can([ProjectPermissionSecretScanningFindingActions.Read], ProjectPermissionSub.SecretScanningFindings);
|
can([ProjectPermissionSecretScanningFindingActions.Read], ProjectPermissionSub.SecretScanningFindings);
|
||||||
|
|
||||||
|
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,11 @@ export enum ProjectPermissionSecretScanningFindingActions {
|
|||||||
Update = "update-findings"
|
Update = "update-findings"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSecretScanningConfigActions {
|
||||||
|
Read = "read-configs",
|
||||||
|
Update = "update-configs"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
@@ -175,7 +180,8 @@ export enum ProjectPermissionSub {
|
|||||||
SecretSyncs = "secret-syncs",
|
SecretSyncs = "secret-syncs",
|
||||||
Kmip = "kmip",
|
Kmip = "kmip",
|
||||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||||
SecretScanningFindings = "secret-scanning-findings"
|
SecretScanningFindings = "secret-scanning-findings",
|
||||||
|
SecretScanningConfigs = "secret-scanning-configs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SecretSubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
@@ -300,7 +306,8 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||||
| [ProjectPermissionSecretScanningDataSourceActions, ProjectPermissionSub.SecretScanningDataSources]
|
| [ProjectPermissionSecretScanningDataSourceActions, ProjectPermissionSub.SecretScanningDataSources]
|
||||||
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings];
|
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
|
||||||
|
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
|
||||||
|
|
||||||
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||||
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||||
@@ -635,6 +642,12 @@ const GeneralPermissionSchema = [
|
|||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningFindingActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningFindingActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SecretScanningConfigs).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningConfigActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+49
-26
@@ -8,7 +8,11 @@ import {
|
|||||||
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 } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
|
import {
|
||||||
|
cloneRepository,
|
||||||
|
convertPatchLineToFileLineNumber,
|
||||||
|
replaceNonChangesWithNewlines
|
||||||
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
|
||||||
import {
|
import {
|
||||||
TSecretScanningFactoryGetDiffScanFindingsPayload,
|
TSecretScanningFactoryGetDiffScanFindingsPayload,
|
||||||
TSecretScanningFactoryGetDiffScanResourcePayload,
|
TSecretScanningFactoryGetDiffScanResourcePayload,
|
||||||
@@ -121,7 +125,7 @@ export const GitHubSecretScanningFactory = () => {
|
|||||||
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
|
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
|
||||||
TGitHubDataSourceWithConnection,
|
TGitHubDataSourceWithConnection,
|
||||||
TQueueGitHubResourceDiffScan["payload"]
|
TQueueGitHubResourceDiffScan["payload"]
|
||||||
> = async ({ dataSource, payload, resourceName }) => {
|
> = async ({ dataSource, payload, resourceName, configPath }) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const {
|
const {
|
||||||
connection: {
|
connection: {
|
||||||
@@ -144,33 +148,52 @@ export const GitHubSecretScanningFactory = () => {
|
|||||||
const allFindings: SecretMatch[] = [];
|
const allFindings: SecretMatch[] = [];
|
||||||
|
|
||||||
for (const commit of commits) {
|
for (const commit of commits) {
|
||||||
for (const filepath of [...commit.added, ...commit.modified]) {
|
// eslint-disable-next-line no-await-in-loop
|
||||||
// eslint-disable-next-line
|
const commitData = await octokit.repos.getCommit({
|
||||||
const fileContentsResponse = await octokit.repos.getContent({
|
owner,
|
||||||
owner,
|
repo,
|
||||||
repo,
|
ref: commit.id
|
||||||
path: filepath
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = fileContentsResponse;
|
// eslint-disable-next-line no-continue
|
||||||
const fileContent = Buffer.from((data as { content: string }).content, "base64").toString();
|
if (!commitData.data.files) continue;
|
||||||
|
|
||||||
// eslint-disable-next-line
|
for (const file of commitData.data.files) {
|
||||||
const findings = await scanContentAndGetFindings(`\n${fileContent}`); // extra line to count lines correctly
|
if ((file.status === "added" || file.status === "modified") && file.patch) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const findings = await scanContentAndGetFindings(
|
||||||
|
replaceNonChangesWithNewlines(`\n${file.patch}`),
|
||||||
|
configPath
|
||||||
|
);
|
||||||
|
|
||||||
allFindings.push(
|
const adjustedFindings = findings.map((finding) => {
|
||||||
...findings.map((finding) => ({
|
const startLine = convertPatchLineToFileLineNumber(file.patch!, finding.StartLine);
|
||||||
...finding,
|
const endLine =
|
||||||
File: filepath,
|
finding.StartLine === finding.EndLine
|
||||||
Commit: commit.id,
|
? startLine
|
||||||
Author: commit.author.name,
|
: convertPatchLineToFileLineNumber(file.patch!, finding.EndLine);
|
||||||
Email: commit.author.email ?? "",
|
const startColumn = finding.StartColumn - 1; // subtract 1 for +
|
||||||
Message: commit.message,
|
const endColumn = finding.EndColumn - 1; // subtract 1 for +
|
||||||
Fingerprint: `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`,
|
|
||||||
Date: commit.timestamp,
|
return {
|
||||||
Link: `https://github.com/${resourceName}/blob/${commit.id}/${filepath}#L${finding.StartLine}`
|
...finding,
|
||||||
}))
|
StartLine: startLine,
|
||||||
);
|
EndLine: endLine,
|
||||||
|
StartColumn: startColumn,
|
||||||
|
EndColumn: endColumn,
|
||||||
|
File: file.filename,
|
||||||
|
Commit: commit.id,
|
||||||
|
Author: commit.author.name,
|
||||||
|
Email: commit.author.email ?? "",
|
||||||
|
Message: commit.message,
|
||||||
|
Fingerprint: `${commit.id}:${file.filename}:${finding.RuleID}:${startLine}:${startColumn}`,
|
||||||
|
Date: commit.timestamp,
|
||||||
|
Link: `https://github.com/${resourceName}/blob/${commit.id}/${file.filename}#L${startLine}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
allFindings.push(...adjustedFindings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
|||||||
|
|
||||||
export const GitHubDataSourceConfigSchema = z.object({
|
export const GitHubDataSourceConfigSchema = z.object({
|
||||||
includeRepos: z
|
includeRepos: z
|
||||||
.array(z.string())
|
.array(z.string().min(1).max(256))
|
||||||
.nonempty("One or more repositories required")
|
.nonempty("One or more repositories required")
|
||||||
|
.max(25)
|
||||||
.default(["*"])
|
.default(["*"])
|
||||||
.describe(SecretScanningDataSources.CONFIG.GITHUB.includeRepos)
|
.describe(SecretScanningDataSources.CONFIG.GITHUB.includeRepos)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
|||||||
const resourceOrm = ormify(db, TableName.SecretScanningResource);
|
const resourceOrm = ormify(db, TableName.SecretScanningResource);
|
||||||
const scanOrm = ormify(db, TableName.SecretScanningScan);
|
const scanOrm = ormify(db, TableName.SecretScanningScan);
|
||||||
const findingOrm = ormify(db, TableName.SecretScanningFinding);
|
const findingOrm = ormify(db, TableName.SecretScanningFinding);
|
||||||
|
const configOrm = ormify(db, TableName.SecretScanningConfig);
|
||||||
|
|
||||||
const findDataSource = async (filter: Parameters<(typeof dataSourceOrm)["find"]>[0], tx?: Knex) => {
|
const findDataSource = async (filter: Parameters<(typeof dataSourceOrm)["find"]>[0], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
@@ -406,7 +407,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
|||||||
unresolvedFindings:
|
unresolvedFindings:
|
||||||
findings?.filter((finding) => finding.status === SecretScanningFindingStatus.Unresolved).length ?? 0,
|
findings?.filter((finding) => finding.status === SecretScanningFindingStatus.Unresolved).length ?? 0,
|
||||||
resolvedFindings:
|
resolvedFindings:
|
||||||
findings?.filter((finding) => finding.status === SecretScanningFindingStatus.Resolved).length ?? 0,
|
findings?.filter((finding) => finding.status !== SecretScanningFindingStatus.Unresolved).length ?? 0,
|
||||||
resourceName: resources[0].name
|
resourceName: resources[0].name
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -453,6 +454,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
|||||||
findWithDetailsByDataSourceId: findScansWithDetailsByDataSourceId,
|
findWithDetailsByDataSourceId: findScansWithDetailsByDataSourceId,
|
||||||
findByDataSourceId: findScansByDataSourceId
|
findByDataSourceId: findScansByDataSourceId
|
||||||
},
|
},
|
||||||
findings: findingOrm
|
findings: findingOrm,
|
||||||
|
configs: configOrm
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ export const cloneRepository = async ({ cloneUrl, repoPath }: TCloneRepository):
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function scanDirectory(inputPath: string, outputPath: string): Promise<void> {
|
export function scanDirectory(inputPath: string, outputPath: string, configPath?: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const command = `cd ${inputPath} && infisical scan --exit-code=77 -r "${outputPath}"`;
|
const command = `cd ${inputPath} && infisical scan --exit-code=77 -r "${outputPath}" ${configPath ? `-c ${configPath}` : ""}`;
|
||||||
exec(command, (error) => {
|
exec(command, (error) => {
|
||||||
if (error && error.code !== 77) {
|
if (error && error.code !== 77) {
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -43,8 +43,12 @@ export function scanDirectory(inputPath: string, outputPath: string): Promise<vo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const scanGitRepositoryAndGetFindings = async (scanPath: string, findingsPath: string): TGetFindingsPayload => {
|
export const scanGitRepositoryAndGetFindings = async (
|
||||||
await scanDirectory(scanPath, findingsPath);
|
scanPath: string,
|
||||||
|
findingsPath: string,
|
||||||
|
configPath?: string
|
||||||
|
): TGetFindingsPayload => {
|
||||||
|
await scanDirectory(scanPath, findingsPath, configPath);
|
||||||
|
|
||||||
const findingsData = JSON.parse(await readFindingsFile(findingsPath)) as SecretMatch[];
|
const findingsData = JSON.parse(await readFindingsFile(findingsPath)) as SecretMatch[];
|
||||||
|
|
||||||
@@ -56,13 +60,64 @@ export const scanGitRepositoryAndGetFindings = async (scanPath: string, findings
|
|||||||
...finding
|
...finding
|
||||||
}) => ({
|
}) => ({
|
||||||
details: titleCaseToCamelCase(finding),
|
details: titleCaseToCamelCase(finding),
|
||||||
fingerprint: finding.Fingerprint,
|
fingerprint: `${finding.Fingerprint}:${finding.StartColumn}`,
|
||||||
severity: SecretScanningFindingSeverity.High,
|
severity: SecretScanningFindingSeverity.High,
|
||||||
rule: finding.RuleID
|
rule: finding.RuleID
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const replaceNonChangesWithNewlines = (patch: string) => {
|
||||||
|
return patch
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
// Keep added lines (remove the + prefix)
|
||||||
|
if (line.startsWith("+") && !line.startsWith("+++")) {
|
||||||
|
return line.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace everything else with newlines to maintain line positioning
|
||||||
|
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertPatchLineToFileLineNumber = (patch: string, patchLineNumber: number) => {
|
||||||
|
const lines = patch.split("\n");
|
||||||
|
let currentPatchLine = 0;
|
||||||
|
let currentNewLine = 0;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
currentPatchLine += 1;
|
||||||
|
|
||||||
|
// Hunk header: @@ -a,b +c,d @@
|
||||||
|
const hunkHeaderMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
||||||
|
if (hunkHeaderMatch) {
|
||||||
|
const startLine = parseInt(hunkHeaderMatch[1], 10);
|
||||||
|
currentNewLine = startLine;
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPatchLine === patchLineNumber) {
|
||||||
|
return currentNewLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("+++")) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue; // skip file metadata lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance only if the line exists in the new file
|
||||||
|
if (line.startsWith("+") || line.startsWith(" ")) {
|
||||||
|
currentNewLine += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentNewLine;
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_MESSAGE_LENGTH = 1024;
|
const MAX_MESSAGE_LENGTH = 1024;
|
||||||
|
|
||||||
export const parseScanErrorMessage = (err: unknown): string => {
|
export const parseScanErrorMessage = (err: unknown): string => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
import { ProjectMembershipRole, TSecretScanningFindings } from "@app/db/schemas";
|
||||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import {
|
||||||
createTempFolder,
|
createTempFolder,
|
||||||
deleteTempFolder
|
deleteTempFolder,
|
||||||
|
writeTextToFile
|
||||||
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
|
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
|
||||||
import {
|
import {
|
||||||
parseScanErrorMessage,
|
parseScanErrorMessage,
|
||||||
@@ -184,21 +185,33 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
tempFolder
|
tempFolder
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const config = await secretScanningV2DAL.configs.findOne({
|
||||||
|
projectId: dataSource.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
let configPath: string | undefined;
|
||||||
|
|
||||||
|
if (config && config.content) {
|
||||||
|
configPath = join(tempFolder, "infisical-scan.toml");
|
||||||
|
await writeTextToFile(configPath, config.content);
|
||||||
|
}
|
||||||
|
|
||||||
let findingsPayload: TFindingsPayload;
|
let findingsPayload: TFindingsPayload;
|
||||||
switch (resource.type) {
|
switch (resource.type) {
|
||||||
case SecretScanningResource.Repository:
|
case SecretScanningResource.Repository:
|
||||||
case SecretScanningResource.Project:
|
case SecretScanningResource.Project:
|
||||||
findingsPayload = await scanGitRepositoryAndGetFindings(scanPath, findingsPath);
|
findingsPayload = await scanGitRepositoryAndGetFindings(scanPath, findingsPath, configPath);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Unhandled resource type");
|
throw new Error("Unhandled resource type");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (findingsPayload.length) {
|
const allFindings = await secretScanningV2DAL.findings.transaction(async (tx) => {
|
||||||
await secretScanningV2DAL.findings.transaction(async (tx) => {
|
let findings: TSecretScanningFindings[] = [];
|
||||||
await secretScanningV2DAL.findings.upsert(
|
if (findingsPayload.length) {
|
||||||
findingsPayload.map((findings) => ({
|
findings = await secretScanningV2DAL.findings.upsert(
|
||||||
...findings,
|
findingsPayload.map((finding) => ({
|
||||||
|
...finding,
|
||||||
projectId: dataSource.projectId,
|
projectId: dataSource.projectId,
|
||||||
dataSourceName: dataSource.name,
|
dataSourceName: dataSource.name,
|
||||||
dataSourceType: dataSource.type,
|
dataSourceType: dataSource.type,
|
||||||
@@ -211,22 +224,28 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
tx,
|
tx,
|
||||||
["resourceName", "dataSourceName", "status"]
|
["resourceName", "dataSourceName", "status"]
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await secretScanningV2DAL.scans.update(
|
await secretScanningV2DAL.scans.update(
|
||||||
{ id: scanId },
|
{ id: scanId },
|
||||||
{
|
{
|
||||||
status: SecretScanningScanStatus.Completed,
|
status: SecretScanningScanStatus.Completed,
|
||||||
statusMessage: null
|
statusMessage: null
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
|
return findings;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newFindings = allFindings.filter((finding) => finding.scanId === scanId);
|
||||||
|
|
||||||
|
if (newFindings.length) {
|
||||||
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
||||||
status: SecretScanningScanStatus.Completed,
|
status: SecretScanningScanStatus.Completed,
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
isDiffScan: false,
|
isDiffScan: false,
|
||||||
dataSource,
|
dataSource,
|
||||||
numberOfSecrets: findingsPayload.length,
|
numberOfSecrets: newFindings.length,
|
||||||
scanId
|
scanId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -376,6 +395,8 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
|
|
||||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
|
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
|
||||||
|
|
||||||
|
const tempFolder = await createTempFolder();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await secretScanningV2DAL.scans.update(
|
await secretScanningV2DAL.scans.update(
|
||||||
{ id: scanId },
|
{ id: scanId },
|
||||||
@@ -387,20 +408,34 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
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);
|
||||||
|
|
||||||
|
const config = await secretScanningV2DAL.configs.findOne({
|
||||||
|
projectId: dataSource.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
let configPath: string | undefined;
|
||||||
|
|
||||||
|
if (config && config.content) {
|
||||||
|
configPath = join(tempFolder, "infisical-scan.toml");
|
||||||
|
await writeTextToFile(configPath, config.content);
|
||||||
|
}
|
||||||
|
|
||||||
const findingsPayload = await factory.getDiffScanFindingsPayload({
|
const findingsPayload = await factory.getDiffScanFindingsPayload({
|
||||||
dataSource: {
|
dataSource: {
|
||||||
...dataSource,
|
...dataSource,
|
||||||
connection
|
connection
|
||||||
} as TSecretScanningDataSourceWithConnection,
|
} as TSecretScanningDataSourceWithConnection,
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
payload
|
payload,
|
||||||
|
configPath
|
||||||
});
|
});
|
||||||
|
|
||||||
if (findingsPayload.length) {
|
const allFindings = await secretScanningV2DAL.findings.transaction(async (tx) => {
|
||||||
await secretScanningV2DAL.findings.transaction(async (tx) => {
|
let findings: TSecretScanningFindings[] = [];
|
||||||
await secretScanningV2DAL.findings.upsert(
|
|
||||||
findingsPayload.map((findings) => ({
|
if (findingsPayload.length) {
|
||||||
...findings,
|
findings = await secretScanningV2DAL.findings.upsert(
|
||||||
|
findingsPayload.map((finding) => ({
|
||||||
|
...finding,
|
||||||
projectId: dataSource.projectId,
|
projectId: dataSource.projectId,
|
||||||
dataSourceName: dataSource.name,
|
dataSourceName: dataSource.name,
|
||||||
dataSourceType: dataSource.type,
|
dataSourceType: dataSource.type,
|
||||||
@@ -413,21 +448,27 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
tx,
|
tx,
|
||||||
["resourceName", "dataSourceName", "status"]
|
["resourceName", "dataSourceName", "status"]
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await secretScanningV2DAL.scans.update(
|
await secretScanningV2DAL.scans.update(
|
||||||
{ id: scanId },
|
{ id: scanId },
|
||||||
{
|
{
|
||||||
status: SecretScanningScanStatus.Completed
|
status: SecretScanningScanStatus.Completed
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
|
return findings;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newFindings = allFindings.filter((finding) => finding.scanId === scanId);
|
||||||
|
|
||||||
|
if (newFindings.length) {
|
||||||
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
|
||||||
status: SecretScanningScanStatus.Completed,
|
status: SecretScanningScanStatus.Completed,
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
isDiffScan: true,
|
isDiffScan: true,
|
||||||
dataSource,
|
dataSource,
|
||||||
numberOfSecrets: findingsPayload.length,
|
numberOfSecrets: newFindings.length,
|
||||||
scanId
|
scanId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -496,6 +537,8 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
|
|
||||||
logger.error(error, `secretScanningV2Queue: Diff Scan Failed ${logDetails}`);
|
logger.error(error, `secretScanningV2Queue: Diff Scan Failed ${logDetails}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await deleteTempFolder(tempFolder);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ActionProjectType } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
|
ProjectPermissionSecretScanningConfigActions,
|
||||||
ProjectPermissionSecretScanningDataSourceActions,
|
ProjectPermissionSecretScanningDataSourceActions,
|
||||||
ProjectPermissionSecretScanningFindingActions,
|
ProjectPermissionSecretScanningFindingActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
@@ -30,7 +31,8 @@ import {
|
|||||||
TSecretScanningScanWithDetails,
|
TSecretScanningScanWithDetails,
|
||||||
TTriggerSecretScanningDataSourceDTO,
|
TTriggerSecretScanningDataSourceDTO,
|
||||||
TUpdateSecretScanningDataSourceDTO,
|
TUpdateSecretScanningDataSourceDTO,
|
||||||
TUpdateSecretScanningFinding
|
TUpdateSecretScanningFindingDTO,
|
||||||
|
TUpsertSecretScanningConfigDTO
|
||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
@@ -714,7 +716,7 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateSecretScanningFindingById = async (
|
const updateSecretScanningFindingById = async (
|
||||||
{ findingId, remarks, status }: TUpdateSecretScanningFinding,
|
{ findingId, remarks, status }: TUpdateSecretScanningFindingDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const plan = await licenseService.getPlan(actor.orgId);
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
@@ -754,6 +756,77 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
return { finding: updatedFinding as TSecretScanningFinding, projectId: finding.projectId };
|
return { finding: updatedFinding as TSecretScanningFinding, projectId: finding.projectId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findSecretScanningConfigByProjectId = async (projectId: string, actor: OrgServiceActor) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.secretScanning)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to access Secret Scanning Configuration due to plan restriction. Upgrade plan to enable Secret Scanning."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretScanning,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretScanningConfigActions.Read,
|
||||||
|
ProjectPermissionSub.SecretScanningConfigs
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = await secretScanningV2DAL.configs.findOne({
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
config ?? { content: null, projectId, updatedAt: null } // using default config
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const upsertSecretScanningConfig = async (
|
||||||
|
{ projectId, content }: TUpsertSecretScanningConfigDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.secretScanning)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to access Secret Scanning Configuration due to plan restriction. Upgrade plan to enable Secret Scanning."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretScanning,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretScanningConfigActions.Read,
|
||||||
|
ProjectPermissionSub.SecretScanningConfigs
|
||||||
|
);
|
||||||
|
|
||||||
|
const [config] = await secretScanningV2DAL.configs.upsert(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
content
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"projectId"
|
||||||
|
);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listSecretScanningDataSourceOptions,
|
listSecretScanningDataSourceOptions,
|
||||||
listSecretScanningDataSourcesByProjectId,
|
listSecretScanningDataSourcesByProjectId,
|
||||||
@@ -771,6 +844,8 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
getSecretScanningUnresolvedFindingsCountByProjectId,
|
getSecretScanningUnresolvedFindingsCountByProjectId,
|
||||||
listSecretScanningFindingsByProjectId,
|
listSecretScanningFindingsByProjectId,
|
||||||
updateSecretScanningFindingById,
|
updateSecretScanningFindingById,
|
||||||
|
findSecretScanningConfigByProjectId,
|
||||||
|
upsertSecretScanningConfig,
|
||||||
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue)
|
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export type TSecretScanningFactoryGetFullScanPath<T extends TSecretScanningDataS
|
|||||||
export type TSecretScanningFactoryGetDiffScanFindingsPayload<
|
export type TSecretScanningFactoryGetDiffScanFindingsPayload<
|
||||||
T extends TSecretScanningDataSourceWithConnection,
|
T extends TSecretScanningDataSourceWithConnection,
|
||||||
P extends TQueueSecretScanningResourceDiffScan["payload"]
|
P extends TQueueSecretScanningResourceDiffScan["payload"]
|
||||||
> = (parameters: { dataSource: T; resourceName: string; payload: P }) => Promise<TFindingsPayload>;
|
> = (parameters: { dataSource: T; resourceName: string; payload: P; configPath?: string }) => Promise<TFindingsPayload>;
|
||||||
|
|
||||||
export type TSecretScanningDataSourceRaw = NonNullable<
|
export type TSecretScanningDataSourceRaw = NonNullable<
|
||||||
Awaited<ReturnType<TSecretScanningV2DALFactory["dataSources"]["findById"]>>
|
Awaited<ReturnType<TSecretScanningV2DALFactory["dataSources"]["findById"]>>
|
||||||
@@ -175,10 +175,15 @@ export type TSecretScanningFactory<
|
|||||||
export type TFindingsPayload = Pick<TSecretScanningFindingsInsert, "details" | "fingerprint" | "severity" | "rule">[];
|
export type TFindingsPayload = Pick<TSecretScanningFindingsInsert, "details" | "fingerprint" | "severity" | "rule">[];
|
||||||
export type TGetFindingsPayload = Promise<TFindingsPayload>;
|
export type TGetFindingsPayload = Promise<TFindingsPayload>;
|
||||||
|
|
||||||
export type TUpdateSecretScanningFinding = {
|
export type TUpdateSecretScanningFindingDTO = {
|
||||||
status?: SecretScanningFindingStatus;
|
status?: SecretScanningFindingStatus;
|
||||||
remarks?: string | null;
|
remarks?: string | null;
|
||||||
findingId: string;
|
findingId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TUpsertSecretScanningConfigDTO = {
|
||||||
|
projectId: string;
|
||||||
|
content: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type TSecretScanningDataSourceCredentials = undefined;
|
export type TSecretScanningDataSourceCredentials = undefined;
|
||||||
|
|||||||
+4
-4
@@ -65,9 +65,9 @@ export function runInfisicalScanOnRepo(repoPath: string, outputPath: string): Pr
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runInfisicalScan(inputPath: string, outputPath: string): Promise<void> {
|
export function runInfisicalScan(inputPath: string, outputPath: string, configPath?: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const command = `cat "${inputPath}" | infisical scan --exit-code=77 --pipe -r "${outputPath}"`;
|
const command = `cat "${inputPath}" | infisical scan --exit-code=77 --pipe -r "${outputPath}" ${configPath ? `-c "${configPath}"` : ""}`;
|
||||||
exec(command, (error) => {
|
exec(command, (error) => {
|
||||||
if (error && error.code !== 77) {
|
if (error && error.code !== 77) {
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -138,14 +138,14 @@ export async function scanFullRepoContentAndGetFindings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function scanContentAndGetFindings(textContent: string): Promise<SecretMatch[]> {
|
export async function scanContentAndGetFindings(textContent: string, configPath?: string): Promise<SecretMatch[]> {
|
||||||
const tempFolder = await createTempFolder();
|
const tempFolder = await createTempFolder();
|
||||||
const filePath = join(tempFolder, "content.txt");
|
const filePath = join(tempFolder, "content.txt");
|
||||||
const findingsPath = join(tempFolder, "findings.json");
|
const findingsPath = join(tempFolder, "findings.json");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await writeTextToFile(filePath, textContent);
|
await writeTextToFile(filePath, textContent);
|
||||||
await runInfisicalScan(filePath, findingsPath);
|
await runInfisicalScan(filePath, findingsPath, configPath);
|
||||||
const findingsData = await readFindingsFile(findingsPath);
|
const findingsData = await readFindingsFile(findingsPath);
|
||||||
return JSON.parse(findingsData) as SecretMatch[];
|
return JSON.parse(findingsData) as SecretMatch[];
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -2426,3 +2426,13 @@ export const SecretScanningFindings = {
|
|||||||
remarks: "Remarks pertaining to the status of this finding."
|
remarks: "Remarks pertaining to the status of this finding."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SecretScanningConfigs = {
|
||||||
|
GET_BY_PROJECT_ID: {
|
||||||
|
projectId: `The ID of the project to retrieve the Secret Scanning Configuration for.`
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
projectId: "The ID of the project to update the Secret Scanning Configuration for.",
|
||||||
|
content: "The contents of the Secret Scanning Configuration file."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Project ID"
|
||||||
|
openapi: "GET /api/v2/secret-scanning/configs/{projectId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v2/secret-scanning/configs/{projectId}"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Check out the [Configuration Docs](/documentation/platform/secret-scanning/overview#configuration) for an in-depth guide on custom configurations.
|
||||||
|
</Note>
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import {Tabs} from "../../../../frontend/src/components/v2";## Prerequisites
|
---
|
||||||
|
title: "GitHub Secret Scanning"
|
||||||
|
sidebarTitle: "GitHub"
|
||||||
|
description: "Learn how to configure secret scanning for GitHub."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
- Create a [GitHub Radar Connection](/integrations/app-connections/github-radar)
|
- Create a [GitHub Radar Connection](/integrations/app-connections/github-radar)
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,134 @@ These status options help teams effectively track and manage the lifecycle of de
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
You can configure custom scanning rules and exceptions by updating your project's scanning configuration via the UI or API.
|
||||||
|
|
||||||
|
The configuration options allow you to:
|
||||||
|
- Define custom scanning patterns and rules
|
||||||
|
- Set up ignore patterns to reduce false positives
|
||||||
|
- Specify file path exclusions
|
||||||
|
- Configure entropy thresholds for secret detection
|
||||||
|
- Add allowlists for known safe patterns
|
||||||
|
|
||||||
|
For detailed configuration options, expand the example configuration below.
|
||||||
|
|
||||||
|
<Accordion title="Example Configuration">
|
||||||
|
```toml
|
||||||
|
# Title for the configuration file
|
||||||
|
title = "Some title"
|
||||||
|
|
||||||
|
|
||||||
|
# This configuration is the foundation that can be expanded. If there are any overlapping rules
|
||||||
|
# between this base and the expanded configuration, the rules in this base will take priority.
|
||||||
|
# Another aspect of extending configurations is the ability to link multiple files, up to a depth of 2.
|
||||||
|
# "Allowlist" arrays get appended and may have repeated elements.
|
||||||
|
# "useDefault" and "path" cannot be used simultaneously. Please choose one.
|
||||||
|
[extend]
|
||||||
|
# useDefault will extend the base configuration with the default config:
|
||||||
|
# https://raw.githubusercontent.com/Infisical/infisical/main/cli/config/infisical-scan.toml
|
||||||
|
useDefault = true
|
||||||
|
# or you can supply a path to a configuration. Path is relative to where infisical cli
|
||||||
|
# was invoked, not the location of the base config.
|
||||||
|
path = "common_config.toml"
|
||||||
|
|
||||||
|
# An array of tables that contain information that define instructions
|
||||||
|
# on how to detect secrets
|
||||||
|
[[rules]]
|
||||||
|
|
||||||
|
# Unique identifier for this rule
|
||||||
|
id = "some-identifier-for-rule"
|
||||||
|
|
||||||
|
# Short human readable description of the rule.
|
||||||
|
description = "awesome rule 1"
|
||||||
|
|
||||||
|
# Golang regular expression used to detect secrets. Note Golang's regex engine
|
||||||
|
# does not support lookaheads.
|
||||||
|
regex = '''one-go-style-regex-for-this-rule'''
|
||||||
|
|
||||||
|
# Golang regular expression used to match paths. This can be used as a standalone rule or it can be used
|
||||||
|
# in conjunction with a valid `regex` entry.
|
||||||
|
path = '''a-file-path-regex'''
|
||||||
|
|
||||||
|
# Array of strings used for metadata and reporting purposes.
|
||||||
|
tags = ["tag","another tag"]
|
||||||
|
|
||||||
|
# A regex match may have many groups, this allows you to specify the group that should be used as (which group the secret is contained in)
|
||||||
|
# its entropy checked if `entropy` is set.
|
||||||
|
secretGroup = 3
|
||||||
|
|
||||||
|
# Float representing the minimum shannon entropy a regex group must have to be considered a secret.
|
||||||
|
# Shannon entropy measures how random a data is. Since secrets are usually composed of many random characters, they typically have high entropy
|
||||||
|
entropy = 3.5
|
||||||
|
|
||||||
|
# Keywords are used for pre-regex check filtering.
|
||||||
|
# If rule has keywords but the text fragment being scanned doesn't have at least one of it's keywords, it will be skipped for processing further.
|
||||||
|
# Ideally these values should either be part of the identifier or unique strings specific to the rule's regex
|
||||||
|
# (introduced in v8.6.0)
|
||||||
|
keywords = [
|
||||||
|
"auth",
|
||||||
|
"password",
|
||||||
|
"token",
|
||||||
|
]
|
||||||
|
|
||||||
|
# You can include an allowlist table for a single rule to reduce false positives or ignore commits
|
||||||
|
# with known/rotated secrets
|
||||||
|
[rules.allowlist]
|
||||||
|
description = "ignore commit A"
|
||||||
|
commits = [ "commit-A", "commit-B"]
|
||||||
|
paths = [
|
||||||
|
'''go\.mod''',
|
||||||
|
'''go\.sum'''
|
||||||
|
]
|
||||||
|
# note: (rule) regexTarget defaults to check the _Secret_ in the finding.
|
||||||
|
# if regexTarget is not specified then _Secret_ will be used.
|
||||||
|
# Acceptable values for regexTarget are "match" and "line"
|
||||||
|
regexTarget = "match"
|
||||||
|
regexes = [
|
||||||
|
'''process''',
|
||||||
|
'''getenv''',
|
||||||
|
]
|
||||||
|
# note: stopwords targets the extracted secret, not the entire regex match
|
||||||
|
# if the extracted secret is found in the stopwords list, the finding will be skipped (i.e not included in report)
|
||||||
|
stopwords = [
|
||||||
|
'''client''',
|
||||||
|
'''endpoint''',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# This is a global allowlist which has a higher order of precedence than rule-specific allowlists.
|
||||||
|
# If a commit listed in the `commits` field below is encountered then that commit will be skipped and no
|
||||||
|
# secrets will be detected for said commit. The same logic applies for regexes and paths.
|
||||||
|
[allowlist]
|
||||||
|
description = "global allow list"
|
||||||
|
commits = [ "commit-A", "commit-B", "commit-C"]
|
||||||
|
paths = [
|
||||||
|
'''gitleaks\.toml''',
|
||||||
|
'''(.*?)(jpg|gif|doc)'''
|
||||||
|
]
|
||||||
|
|
||||||
|
# note: (global) regexTarget defaults to check the _Secret_ in the finding.
|
||||||
|
# if regexTarget is not specified then _Secret_ will be used.
|
||||||
|
# Acceptable values for regexTarget are "match" and "line"
|
||||||
|
regexTarget = "match"
|
||||||
|
|
||||||
|
regexes = [
|
||||||
|
'''219-09-9999''',
|
||||||
|
'''078-05-1120''',
|
||||||
|
'''(9[0-9]{2}|666)-\d{2}-\d{4}''',
|
||||||
|
]
|
||||||
|
# note: stopwords targets the extracted secret, not the entire regex match
|
||||||
|
# if the extracted secret is found in the stopwords list, the finding will be skipped (i.e not included in report)
|
||||||
|
stopwords = [
|
||||||
|
'''client''',
|
||||||
|
'''endpoint''',
|
||||||
|
]
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -1046,6 +1046,13 @@
|
|||||||
"api-reference/endpoints/secret-scanning/findings/list",
|
"api-reference/endpoints/secret-scanning/findings/list",
|
||||||
"api-reference/endpoints/secret-scanning/findings/update"
|
"api-reference/endpoints/secret-scanning/findings/update"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Configuration",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/secret-scanning/config/get-by-project-id",
|
||||||
|
"api-reference/endpoints/secret-scanning/config/update"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ export enum ProjectPermissionSecretScanningFindingActions {
|
|||||||
Update = "update-findings"
|
Update = "update-findings"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSecretScanningConfigActions {
|
||||||
|
Read = "read-configs",
|
||||||
|
Update = "update-configs"
|
||||||
|
}
|
||||||
|
|
||||||
export enum PermissionConditionOperators {
|
export enum PermissionConditionOperators {
|
||||||
$IN = "$in",
|
$IN = "$in",
|
||||||
$ALL = "$all",
|
$ALL = "$all",
|
||||||
@@ -216,7 +221,8 @@ export enum ProjectPermissionSub {
|
|||||||
SecretSyncs = "secret-syncs",
|
SecretSyncs = "secret-syncs",
|
||||||
Kmip = "kmip",
|
Kmip = "kmip",
|
||||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||||
SecretScanningFindings = "secret-scanning-findings"
|
SecretScanningFindings = "secret-scanning-findings",
|
||||||
|
SecretScanningConfigs = "secret-scanning-configs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SecretSubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
@@ -345,6 +351,7 @@ export type ProjectPermissionSet =
|
|||||||
ProjectPermissionSecretScanningDataSourceActions,
|
ProjectPermissionSecretScanningDataSourceActions,
|
||||||
ProjectPermissionSub.SecretScanningDataSources
|
ProjectPermissionSub.SecretScanningDataSources
|
||||||
]
|
]
|
||||||
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings];
|
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
|
||||||
|
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
|
||||||
|
|
||||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||||
|
|||||||
@@ -201,7 +201,9 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[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",
|
||||||
[EventType.SECRET_SCANNING_FINDING_UPDATE]: "Update Secret Scanning Finding Status"
|
[EventType.SECRET_SCANNING_FINDING_UPDATE]: "Update Secret Scanning Finding Status",
|
||||||
|
[EventType.SECRET_SCANNING_CONFIG_GET]: "Get Secret Scanning Configuration",
|
||||||
|
[EventType.SECRET_SCANNING_CONFIG_UPDATE]: "Update Secret Scanning Configuration"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
|
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
|
||||||
|
|||||||
@@ -195,5 +195,7 @@ export enum EventType {
|
|||||||
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",
|
||||||
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
|
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
|
||||||
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update"
|
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update",
|
||||||
|
SECRET_SCANNING_CONFIG_GET = "secret-scanning-config-get",
|
||||||
|
SECRET_SCANNING_CONFIG_UPDATE = "secret-scanning-config-update"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { secretScanningV2Keys } from "./queries";
|
|||||||
import {
|
import {
|
||||||
TCreateSecretScanningDataSourceDTO,
|
TCreateSecretScanningDataSourceDTO,
|
||||||
TDeleteSecretScanningDataSourceDTO,
|
TDeleteSecretScanningDataSourceDTO,
|
||||||
|
TGetSecretScanningConfigResponse,
|
||||||
|
TSecretScanningConfig,
|
||||||
TSecretScanningDataSourceResponse,
|
TSecretScanningDataSourceResponse,
|
||||||
TSecretScanningFindingResponse,
|
TSecretScanningFindingResponse,
|
||||||
TTriggerSecretScanningDataSourceDTO,
|
TTriggerSecretScanningDataSourceDTO,
|
||||||
@@ -128,7 +130,31 @@ export const useUpdateSecretScanningFinding = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretScanningV2Keys.listFindings(projectId)
|
queryKey: secretScanningV2Keys.listFindings(projectId)
|
||||||
});
|
});
|
||||||
// TODO: data source queries
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: secretScanningV2Keys.findingCount(projectId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: secretScanningV2Keys.dataSource()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSecretScanningConfig = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ projectId, ...params }: TSecretScanningConfig) => {
|
||||||
|
const { data } = await apiRequest.patch<TGetSecretScanningConfigResponse>(
|
||||||
|
`/api/v2/secret-scanning/configs/${projectId}`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.config;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: secretScanningV2Keys.configByProjectId(projectId)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
TGetSecretScanningConfigResponse,
|
||||||
TGetSecretScanningDataSource,
|
TGetSecretScanningDataSource,
|
||||||
TGetSecretScanningUnresolvedFindingsResponse,
|
TGetSecretScanningUnresolvedFindingsResponse,
|
||||||
TListSecretScanningDataSourceOptions,
|
TListSecretScanningDataSourceOptions,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
TListSecretScanningFindingsResponse,
|
TListSecretScanningFindingsResponse,
|
||||||
TListSecretScanningResourcesResponse,
|
TListSecretScanningResourcesResponse,
|
||||||
TListSecretScanningScansResponse,
|
TListSecretScanningScansResponse,
|
||||||
|
TSecretScanningConfig,
|
||||||
TSecretScanningDataSource,
|
TSecretScanningDataSource,
|
||||||
TSecretScanningDataSourceOption,
|
TSecretScanningDataSourceOption,
|
||||||
TSecretScanningDataSourceResponse,
|
TSecretScanningDataSourceResponse,
|
||||||
@@ -42,7 +44,9 @@ export const secretScanningV2Keys = {
|
|||||||
finding: () => [...secretScanningV2Keys.all, "finding"] as const,
|
finding: () => [...secretScanningV2Keys.all, "finding"] as const,
|
||||||
findingCount: (projectId: string) =>
|
findingCount: (projectId: string) =>
|
||||||
[...secretScanningV2Keys.finding(), "count", projectId] as const,
|
[...secretScanningV2Keys.finding(), "count", projectId] as const,
|
||||||
listFindings: (projectId: string) => [...secretScanningV2Keys.finding(), "list", projectId]
|
listFindings: (projectId: string) => [...secretScanningV2Keys.finding(), "list", projectId],
|
||||||
|
configByProjectId: (projectId: string) =>
|
||||||
|
[...secretScanningV2Keys.all, "config", projectId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSecretScanningDataSourceOptions = (
|
export const useSecretScanningDataSourceOptions = (
|
||||||
@@ -216,3 +220,28 @@ export const useListSecretScanningFindings = (
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetSecretScanningConfig = (
|
||||||
|
projectId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TSecretScanningConfig,
|
||||||
|
unknown,
|
||||||
|
TSecretScanningConfig,
|
||||||
|
ReturnType<typeof secretScanningV2Keys.configByProjectId>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: secretScanningV2Keys.configByProjectId(projectId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TGetSecretScanningConfigResponse>(
|
||||||
|
`/api/v2/secret-scanning/configs/${projectId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.config;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -115,6 +115,15 @@ export type TGetSecretScanningUnresolvedFindingsResponse = {
|
|||||||
unresolvedFindings: number;
|
unresolvedFindings: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TSecretScanningConfig = {
|
||||||
|
projectId: string;
|
||||||
|
content: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetSecretScanningConfigResponse = {
|
||||||
|
config: TSecretScanningConfig;
|
||||||
|
};
|
||||||
|
|
||||||
export type TSecretScanningFinding = {
|
export type TSecretScanningFinding = {
|
||||||
id: string;
|
id: string;
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
|
|||||||
+50
-3
@@ -20,6 +20,7 @@ import {
|
|||||||
ProjectPermissionPkiSubscriberActions,
|
ProjectPermissionPkiSubscriberActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSecretRotationActions,
|
ProjectPermissionSecretRotationActions,
|
||||||
|
ProjectPermissionSecretScanningConfigActions,
|
||||||
ProjectPermissionSecretScanningDataSourceActions,
|
ProjectPermissionSecretScanningDataSourceActions,
|
||||||
ProjectPermissionSecretScanningFindingActions,
|
ProjectPermissionSecretScanningFindingActions,
|
||||||
ProjectPermissionSecretSyncActions,
|
ProjectPermissionSecretSyncActions,
|
||||||
@@ -107,6 +108,11 @@ const SecretScanningFindingPolicyActionSchema = z.object({
|
|||||||
[ProjectPermissionSecretScanningFindingActions.Update]: z.boolean().optional()
|
[ProjectPermissionSecretScanningFindingActions.Update]: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SecretScanningConfigPolicyActionSchema = z.object({
|
||||||
|
[ProjectPermissionSecretScanningConfigActions.Read]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionSecretScanningConfigActions.Update]: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
const KmipPolicyActionSchema = z.object({
|
const KmipPolicyActionSchema = z.object({
|
||||||
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
|
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
|
||||||
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
|
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
|
||||||
@@ -295,7 +301,9 @@ export const projectRoleFormSchema = z.object({
|
|||||||
[ProjectPermissionSub.SecretScanningDataSources]:
|
[ProjectPermissionSub.SecretScanningDataSources]:
|
||||||
SecretScanningDataSourcePolicyActionSchema.array().default([]),
|
SecretScanningDataSourcePolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.SecretScanningFindings]:
|
[ProjectPermissionSub.SecretScanningFindings]:
|
||||||
SecretScanningFindingPolicyActionSchema.array().default([])
|
SecretScanningFindingPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.SecretScanningConfigs]:
|
||||||
|
SecretScanningConfigPolicyActionSchema.array().default([])
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -777,6 +785,18 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
formVal[subject]![0][ProjectPermissionSecretScanningFindingActions.Update] = true;
|
formVal[subject]![0][ProjectPermissionSecretScanningFindingActions.Update] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subject === ProjectPermissionSub.SecretScanningConfigs) {
|
||||||
|
const canRead = action.includes(ProjectPermissionSecretScanningConfigActions.Read);
|
||||||
|
const canUpdate = action.includes(ProjectPermissionSecretScanningConfigActions.Update);
|
||||||
|
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
|
// from above statement we are sure it won't be undefined
|
||||||
|
if (canRead) formVal[subject]![0][ProjectPermissionSecretScanningConfigActions.Read] = true;
|
||||||
|
if (canUpdate)
|
||||||
|
formVal[subject]![0][ProjectPermissionSecretScanningConfigActions.Update] = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (subject === ProjectPermissionSub.SshHosts) {
|
if (subject === ProjectPermissionSub.SshHosts) {
|
||||||
if (!formVal[subject]) formVal[subject] = [];
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
|
||||||
@@ -1363,6 +1383,19 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
value: ProjectPermissionSecretScanningFindingActions.Update
|
value: ProjectPermissionSecretScanningFindingActions.Update
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretScanningConfigs]: {
|
||||||
|
title: "Secret Scanning Config",
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: "Read Config",
|
||||||
|
value: ProjectPermissionSecretScanningConfigActions.Read
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Update Config",
|
||||||
|
value: ProjectPermissionSecretScanningConfigActions.Update
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1418,7 +1451,8 @@ const SshPermissionSubjects = (enabled = false) => ({
|
|||||||
|
|
||||||
const SecretScanningSubject = (enabled = false) => ({
|
const SecretScanningSubject = (enabled = false) => ({
|
||||||
[ProjectPermissionSub.SecretScanningDataSources]: enabled,
|
[ProjectPermissionSub.SecretScanningDataSources]: enabled,
|
||||||
[ProjectPermissionSub.SecretScanningFindings]: enabled
|
[ProjectPermissionSub.SecretScanningFindings]: enabled,
|
||||||
|
[ProjectPermissionSub.SecretScanningConfigs]: enabled
|
||||||
});
|
});
|
||||||
|
|
||||||
// scott: this structure ensures we don't forget to add project permissions to their relevant project type
|
// scott: this structure ensures we don't forget to add project permissions to their relevant project type
|
||||||
@@ -1693,6 +1727,10 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
|||||||
{
|
{
|
||||||
subject: ProjectPermissionSub.SecretScanningFindings,
|
subject: ProjectPermissionSub.SecretScanningFindings,
|
||||||
actions: [ProjectPermissionSecretScanningFindingActions.Read]
|
actions: [ProjectPermissionSecretScanningFindingActions.Read]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: ProjectPermissionSub.SecretScanningConfigs,
|
||||||
|
actions: [ProjectPermissionSecretScanningConfigActions.Read]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1708,10 +1746,19 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
|||||||
{
|
{
|
||||||
subject: ProjectPermissionSub.SecretScanningFindings,
|
subject: ProjectPermissionSub.SecretScanningFindings,
|
||||||
actions: Object.values(ProjectPermissionSecretScanningFindingActions)
|
actions: Object.values(ProjectPermissionSecretScanningFindingActions)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: ProjectPermissionSub.SecretScanningConfigs,
|
||||||
|
actions: [ProjectPermissionSecretScanningConfigActions.Read]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
projectManagerTemplate()
|
projectManagerTemplate([
|
||||||
|
{
|
||||||
|
subject: ProjectPermissionSub.SecretScanningConfigs,
|
||||||
|
actions: Object.values(ProjectPermissionSecretScanningConfigActions)
|
||||||
|
}
|
||||||
|
])
|
||||||
],
|
],
|
||||||
[ProjectType.SecretManager]: [
|
[ProjectType.SecretManager]: [
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -114,7 +114,7 @@ export const SecretScanningScanRow = ({ scan }: Props) => {
|
|||||||
<span className="text-xs">
|
<span className="text-xs">
|
||||||
{totalFindings}{" "}
|
{totalFindings}{" "}
|
||||||
{unresolvedFindings
|
{unresolvedFindings
|
||||||
? `Secret ${totalFindings > 1 ? "s" : ""} Detected`
|
? `Secret${totalFindings > 1 ? "s" : ""} Detected`
|
||||||
: "Leak Resolved"}
|
: "Leak Resolved"}
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||||
|
import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab";
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -21,26 +23,29 @@ export const SettingsPage = () => {
|
|||||||
<TabList>
|
<TabList>
|
||||||
<Tab value="tab-project-general">General</Tab>
|
<Tab value="tab-project-general">General</Tab>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionSecretScanningConfigActions.Read}
|
||||||
a={ProjectPermissionSub.Project}
|
a={ProjectPermissionSub.SecretScanningConfigs}
|
||||||
>
|
>
|
||||||
{(isAllowed) =>
|
{(isAllowed) =>
|
||||||
isAllowed && <Tab value="tab-project-secret-scannings">Rule Settings</Tab>
|
isAllowed && <Tab value="tab-project-scanning-config">Scanning Configuration</Tab>
|
||||||
}
|
}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value="tab-project-general">
|
<TabPanel value="tab-project-general">
|
||||||
<ProjectGeneralTab />
|
<ProjectGeneralTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
{/* <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionSecretScanningConfigActions.Read}
|
||||||
|
a={ProjectPermissionSub.SecretScanningConfigs}
|
||||||
|
>
|
||||||
{(isAllowed) =>
|
{(isAllowed) =>
|
||||||
isAllowed && (
|
isAllowed && (
|
||||||
<TabPanel value="tab-project-ssh">
|
<TabPanel value="tab-project-scanning-config">
|
||||||
<ProjectSshTab />
|
<ProjectScanningConfigTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</ProjectPermissionCan> */}
|
</ProjectPermissionCan>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { ContentLoader, EmptyState } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useGetSecretScanningConfig } from "@app/hooks/api/secretScanningV2";
|
||||||
|
|
||||||
|
import { SecretScanningConfigForm } from "./SecretScanningConfigForm";
|
||||||
|
|
||||||
|
export const ProjectScanningConfigTab = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: config, isPending: isConfigPending } = useGetSecretScanningConfig(
|
||||||
|
currentWorkspace.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConfigPending) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<ContentLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center px-20">
|
||||||
|
<EmptyState
|
||||||
|
className="max-w-2xl rounded-md text-center"
|
||||||
|
icon={faBan}
|
||||||
|
title="Could not find Project Configuration"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SecretScanningConfigForm config={config} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, TextArea } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import {
|
||||||
|
TSecretScanningConfig,
|
||||||
|
useUpdateSecretScanningConfig
|
||||||
|
} from "@app/hooks/api/secretScanningV2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
config: TSecretScanningConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
content: z.string().max(5000, "Configuration cannot exceed 5000 characters").optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
export const SecretScanningConfigForm = ({ config }: Props) => {
|
||||||
|
const updateConfig = useUpdateSecretScanningConfig();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { isDirty }
|
||||||
|
} = useForm<FormType>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
content: config.content ?? ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async ({ content }: FormType) => {
|
||||||
|
try {
|
||||||
|
await updateConfig.mutateAsync({
|
||||||
|
projectId: config.projectId,
|
||||||
|
content: content || null
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Configuration successfully updated"
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update Configuration"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-xl font-semibold text-mineshaft-100">Project Configuration</h2>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview#configuration"
|
||||||
|
className="mt-[0.02rem]"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="leading-5 text-mineshaft-400">
|
||||||
|
Configure rules and exceptions to customize scanning
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="flex w-full flex-col">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionSecretScanningConfigActions.Update}
|
||||||
|
a={ProjectPermissionSub.SecretScanningConfigs}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<TextArea
|
||||||
|
placeholder={
|
||||||
|
"[extend]\n\nuseDefault = true\n\n# See docs for configuration guide"
|
||||||
|
}
|
||||||
|
{...field}
|
||||||
|
rows={3}
|
||||||
|
className="thin-scrollbar min-h-[36rem] w-full !resize-none !resize-y"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="content"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionSecretScanningConfigActions.Update}
|
||||||
|
a={ProjectPermissionSub.SecretScanningConfigs}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
className="w-min"
|
||||||
|
isLoading={updateConfig.isPending}
|
||||||
|
isDisabled={updateConfig.isPending || !isAllowed || !isDirty}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectScanningConfigTab } from "./ProjectScanningConfigTab";
|
||||||
Reference in New Issue
Block a user