mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 22:02:57 +00:00
Compare commits
1 Commits
daniel/pat
...
sheen/hack
Author | SHA1 | Date | |
---|---|---|---|
0762d8b172 |
backend
package-lock.jsonpackage.json
src
@types
db
migrations
schemas
ee/services/license
lib/config
queue
server/routes
services/automated-security
frontend/src
hooks/api/automated-security
layouts/OrganizationLayout/components/MinimizedOrgSidebar
pages/organization/AutomatedSecurityPage
routeTree.gen.tsroutes.ts
106
backend/package-lock.json
generated
106
backend/package-lock.json
generated
@ -84,6 +84,7 @@
|
||||
"nanoid": "^3.3.8",
|
||||
"nodemailer": "^6.9.9",
|
||||
"odbc": "^2.4.9",
|
||||
"openai": "^4.85.4",
|
||||
"openid-client": "^5.6.5",
|
||||
"ora": "^7.0.1",
|
||||
"oracledb": "^6.4.0",
|
||||
@ -11747,6 +11748,17 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
||||
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aggregate-error": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||
@ -14897,6 +14909,23 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
"web-streams-polyfill": "4.0.0-beta.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -15951,6 +15980,14 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@ -17892,6 +17929,24 @@
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@ -18444,6 +18499,43 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.85.4",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.85.4.tgz",
|
||||
"integrity": "sha512-Nki51PBSu+Aryo7WKbdXvfm0X/iKkQS2fq3O0Uqb/O3b4exOZFid2te1BZ52bbO5UwxQZ5eeHJDCTqtrJLPw0w==",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/@types/node": {
|
||||
"version": "18.19.76",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz",
|
||||
"integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/openapi-types": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||
@ -23790,6 +23882,14 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@ -24282,9 +24382,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.22.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
||||
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
@ -192,6 +192,7 @@
|
||||
"nanoid": "^3.3.8",
|
||||
"nodemailer": "^6.9.9",
|
||||
"odbc": "^2.4.9",
|
||||
"openai": "^4.85.4",
|
||||
"openid-client": "^5.6.5",
|
||||
"ora": "^7.0.1",
|
||||
"oracledb": "^6.4.0",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -45,6 +45,7 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TAutomatedSecurityServiceFactory } from "@app/services/automated-security/automated-security-service";
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
@ -228,6 +229,7 @@ declare module "fastify" {
|
||||
secretSync: TSecretSyncServiceFactory;
|
||||
kmip: TKmipServiceFactory;
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
automatedSecurity: TAutomatedSecurityServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
16
backend/src/@types/knex.d.ts
vendored
16
backend/src/@types/knex.d.ts
vendored
@ -29,6 +29,9 @@ import {
|
||||
TAuthTokenSessionsUpdate,
|
||||
TAuthTokensInsert,
|
||||
TAuthTokensUpdate,
|
||||
TAutomatedSecurityReports,
|
||||
TAutomatedSecurityReportsInsert,
|
||||
TAutomatedSecurityReportsUpdate,
|
||||
TBackupPrivateKey,
|
||||
TBackupPrivateKeyInsert,
|
||||
TBackupPrivateKeyUpdate,
|
||||
@ -113,6 +116,9 @@ import {
|
||||
TIdentityOrgMemberships,
|
||||
TIdentityOrgMembershipsInsert,
|
||||
TIdentityOrgMembershipsUpdate,
|
||||
TIdentityProfile,
|
||||
TIdentityProfileInsert,
|
||||
TIdentityProfileUpdate,
|
||||
TIdentityProjectAdditionalPrivilege,
|
||||
TIdentityProjectAdditionalPrivilegeInsert,
|
||||
TIdentityProjectAdditionalPrivilegeUpdate,
|
||||
@ -930,5 +936,15 @@ declare module "knex/types/tables" {
|
||||
TKmipClientCertificatesInsert,
|
||||
TKmipClientCertificatesUpdate
|
||||
>;
|
||||
[TableName.IdentityProfile]: KnexOriginal.CompositeTableType<
|
||||
TIdentityProfile,
|
||||
TIdentityProfileInsert,
|
||||
TIdentityProfileUpdate
|
||||
>;
|
||||
[TableName.AutomatedSecurityReports]: KnexOriginal.CompositeTableType<
|
||||
TAutomatedSecurityReports,
|
||||
TAutomatedSecurityReportsInsert,
|
||||
TAutomatedSecurityReportsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
53
backend/src/db/migrations/20250224015833_identity-profile.ts
Normal file
53
backend/src/db/migrations/20250224015833_identity-profile.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IdentityProfile))) {
|
||||
await knex.schema.createTable(TableName.IdentityProfile, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("userId");
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
t.uuid("identityId");
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
|
||||
t.text("temporalProfile").notNullable(); // access pattern or frequency
|
||||
t.text("scopeProfile").notNullable(); // scope of usage - are they accessing development environment secrets. which paths? are they doing mainly admin work?
|
||||
t.text("usageProfile").notNullable(); // method of usage
|
||||
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityProfile);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.AutomatedSecurityReports))) {
|
||||
await knex.schema.createTable(TableName.AutomatedSecurityReports, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("profileId").notNullable();
|
||||
t.foreign("profileId").references("id").inTable(TableName.IdentityProfile).onDelete("CASCADE");
|
||||
|
||||
t.jsonb("event").notNullable();
|
||||
|
||||
t.string("remarks").notNullable();
|
||||
t.string("severity").notNullable();
|
||||
t.string("status").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.AutomatedSecurityReports);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityProfile);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.AutomatedSecurityReports);
|
||||
}
|
25
backend/src/db/schemas/automated-security-reports.ts
Normal file
25
backend/src/db/schemas/automated-security-reports.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// 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 AutomatedSecurityReportsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
profileId: z.string().uuid(),
|
||||
event: z.unknown(),
|
||||
remarks: z.string(),
|
||||
severity: z.string(),
|
||||
status: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAutomatedSecurityReports = z.infer<typeof AutomatedSecurityReportsSchema>;
|
||||
export type TAutomatedSecurityReportsInsert = Omit<z.input<typeof AutomatedSecurityReportsSchema>, TImmutableDBKeys>;
|
||||
export type TAutomatedSecurityReportsUpdate = Partial<
|
||||
Omit<z.input<typeof AutomatedSecurityReportsSchema>, TImmutableDBKeys>
|
||||
>;
|
24
backend/src/db/schemas/identity-profile.ts
Normal file
24
backend/src/db/schemas/identity-profile.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IdentityProfileSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
identityId: z.string().uuid().nullable().optional(),
|
||||
temporalProfile: z.string(),
|
||||
scopeProfile: z.string(),
|
||||
usageProfile: z.string(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TIdentityProfile = z.infer<typeof IdentityProfileSchema>;
|
||||
export type TIdentityProfileInsert = Omit<z.input<typeof IdentityProfileSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityProfileUpdate = Partial<Omit<z.input<typeof IdentityProfileSchema>, TImmutableDBKeys>>;
|
@ -7,6 +7,7 @@ export * from "./audit-log-streams";
|
||||
export * from "./audit-logs";
|
||||
export * from "./auth-token-sessions";
|
||||
export * from "./auth-tokens";
|
||||
export * from "./automated-security-reports";
|
||||
export * from "./backup-private-key";
|
||||
export * from "./certificate-authorities";
|
||||
export * from "./certificate-authority-certs";
|
||||
@ -35,6 +36,7 @@ export * from "./identity-kubernetes-auths";
|
||||
export * from "./identity-metadata";
|
||||
export * from "./identity-oidc-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-profile";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
export * from "./identity-project-membership-role";
|
||||
export * from "./identity-project-memberships";
|
||||
|
@ -80,6 +80,8 @@ export enum TableName {
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
// used by both identity and users
|
||||
IdentityMetadata = "identity_metadata",
|
||||
IdentityProfile = "identity_profile",
|
||||
AutomatedSecurityReports = "automated_security_reports",
|
||||
ResourceMetadata = "resource_metadata",
|
||||
ScimToken = "scim_tokens",
|
||||
AccessApprovalPolicy = "access_approval_policies",
|
||||
|
@ -25,8 +25,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
customRateLimits: false,
|
||||
customAlerts: false,
|
||||
secretAccessInsights: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
auditLogs: true,
|
||||
auditLogsRetentionDays: 3,
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
|
@ -228,7 +228,9 @@ const envSchema = z
|
||||
if (!val) return undefined;
|
||||
return JSON.parse(val) as string[];
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
AI_API_KEY: zpStr(z.string().optional())
|
||||
})
|
||||
// To ensure that basic encryption is always possible.
|
||||
.refine(
|
||||
|
@ -39,6 +39,7 @@ export enum QueueName {
|
||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||
CaCrlRotation = "ca-crl-rotation",
|
||||
SecretReplication = "secret-replication",
|
||||
AutomatedSecurity = "automated-security",
|
||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||
ProjectV3Migration = "project-v3-migration",
|
||||
AccessTokenStatusUpdate = "access-token-status-update",
|
||||
@ -72,7 +73,8 @@ export enum QueueJobs {
|
||||
SecretSyncSyncSecrets = "secret-sync-sync-secrets",
|
||||
SecretSyncImportSecrets = "secret-sync-import-secrets",
|
||||
SecretSyncRemoveSecrets = "secret-sync-remove-secrets",
|
||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications"
|
||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||
ProfileIdentity = "profile-identity"
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
@ -195,6 +197,10 @@ export type TQueueJobTypes = {
|
||||
};
|
||||
};
|
||||
};
|
||||
[QueueName.AutomatedSecurity]: {
|
||||
name: QueueJobs.ProfileIdentity;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.AppConnectionSecretSync]:
|
||||
| {
|
||||
name: QueueJobs.SecretSyncSyncSecrets;
|
||||
|
@ -105,6 +105,9 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
|
||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { automatedSecurityReportDALFactory } from "@app/services/automated-security/automated-security-report-dal";
|
||||
import { automatedSecurityServiceFactory } from "@app/services/automated-security/automated-security-service";
|
||||
import { identityProfileDALFactory } from "@app/services/automated-security/identity-profile-dal";
|
||||
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
@ -392,6 +395,8 @@ export const registerRoutes = async (
|
||||
const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db);
|
||||
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
|
||||
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
|
||||
const automatedSecurityReportDAL = automatedSecurityReportDALFactory(db);
|
||||
const identityProfileDAL = identityProfileDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
@ -1242,6 +1247,7 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const identityUaService = identityUaServiceFactory({
|
||||
identityOrgMembershipDAL,
|
||||
permissionService,
|
||||
@ -1250,6 +1256,7 @@ export const registerRoutes = async (
|
||||
identityUaDAL,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
|
||||
identityKubernetesAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
@ -1457,6 +1464,15 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const automatedSecurityService = automatedSecurityServiceFactory({
|
||||
auditLogDAL,
|
||||
automatedSecurityReportDAL,
|
||||
identityProfileDAL,
|
||||
orgDAL,
|
||||
queueService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
|
||||
// setup the communication with license key server
|
||||
@ -1557,7 +1573,8 @@ export const registerRoutes = async (
|
||||
appConnection: appConnectionService,
|
||||
secretSync: secretSyncService,
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService
|
||||
kmipOperation: kmipOperationService,
|
||||
automatedSecurity: automatedSecurityService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
72
backend/src/server/routes/v1/automated-security-router.ts
Normal file
72
backend/src/server/routes/v1/automated-security-router.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AutomatedSecurityReportsSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAutomatedSecurityRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/trigger",
|
||||
handler: async () => {
|
||||
return server.services.automatedSecurity.processSecurityJob();
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/reports",
|
||||
schema: {
|
||||
response: {
|
||||
200: AutomatedSecurityReportsSchema.extend({
|
||||
userId: z.string().nullish(),
|
||||
name: z.string().nullish()
|
||||
}).array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
return server.services.automatedSecurity.getReports(req.permission.orgId);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/reports/:id/status",
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: AutomatedSecurityReportsSchema.extend({
|
||||
userId: z.string().nullish(),
|
||||
name: z.string().nullish()
|
||||
}).array()
|
||||
},
|
||||
body: z.object({
|
||||
status: z.enum(["ignored", "resolved"])
|
||||
})
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
return server.services.automatedSecurity.patchSecurityReportStatus(req.params.id, req.body.status);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/project-permission/analyze",
|
||||
schema: {
|
||||
body: z.object({
|
||||
projectId: z.string(),
|
||||
userId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
return server.services.automatedSecurity.analyzeIdentityProjectPermission(req.body.userId, req.body.projectId);
|
||||
}
|
||||
});
|
||||
};
|
@ -8,6 +8,7 @@ import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/
|
||||
|
||||
import { registerAdminRouter } from "./admin-router";
|
||||
import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerAutomatedSecurityRouter } from "./automated-security-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
@ -141,4 +142,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ prefix: "/secret-syncs" }
|
||||
);
|
||||
|
||||
await server.register(registerAutomatedSecurityRouter, { prefix: "/automated-security" });
|
||||
};
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TAutomatedSecurityReportDALFactory = ReturnType<typeof automatedSecurityReportDALFactory>;
|
||||
|
||||
export const automatedSecurityReportDALFactory = (db: TDbClient) => {
|
||||
const automatedSecurityReportOrm = ormify(db, TableName.AutomatedSecurityReports);
|
||||
|
||||
const findByOrg = (orgId: string, status = "pending") => {
|
||||
return db(TableName.AutomatedSecurityReports)
|
||||
.join(
|
||||
TableName.IdentityProfile,
|
||||
`${TableName.IdentityProfile}.id`,
|
||||
`${TableName.AutomatedSecurityReports}.profileId`
|
||||
)
|
||||
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.IdentityProfile}.userId`)
|
||||
.where(`${TableName.IdentityProfile}.orgId`, "=", orgId)
|
||||
.where(`${TableName.AutomatedSecurityReports}.status`, "=", status)
|
||||
.select(
|
||||
selectAllTableCols(TableName.AutomatedSecurityReports),
|
||||
db.ref("userId").withSchema(TableName.IdentityProfile).as("userId"),
|
||||
db.ref("email").withSchema(TableName.Users).as("name")
|
||||
);
|
||||
};
|
||||
|
||||
const findById = (id: string) => {
|
||||
return db(TableName.AutomatedSecurityReports)
|
||||
.join(
|
||||
TableName.IdentityProfile,
|
||||
`${TableName.IdentityProfile}.id`,
|
||||
`${TableName.AutomatedSecurityReports}.profileId`
|
||||
)
|
||||
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.IdentityProfile}.userId`)
|
||||
.where(`${TableName.AutomatedSecurityReports}.id`, "=", id)
|
||||
.select(
|
||||
selectAllTableCols(TableName.AutomatedSecurityReports),
|
||||
selectAllTableCols(TableName.IdentityProfile),
|
||||
db.ref("email").withSchema(TableName.Users).as("name")
|
||||
)
|
||||
.first();
|
||||
};
|
||||
|
||||
return {
|
||||
...automatedSecurityReportOrm,
|
||||
findByOrg,
|
||||
findById
|
||||
};
|
||||
};
|
@ -0,0 +1,540 @@
|
||||
import { OpenAI } from "openai";
|
||||
import { zodResponseFormat } from "openai/helpers/zod";
|
||||
import z from "zod";
|
||||
|
||||
import { ActionProjectType, TAuditLogs } from "@app/db/schemas";
|
||||
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { AuthMethod } from "../auth/auth-type";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TAutomatedSecurityReportDALFactory } from "./automated-security-report-dal";
|
||||
import { TIdentityProfileDALFactory } from "./identity-profile-dal";
|
||||
|
||||
type TAutomatedSecurityServiceFactoryDep = {
|
||||
auditLogDAL: TAuditLogDALFactory;
|
||||
automatedSecurityReportDAL: TAutomatedSecurityReportDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
identityProfileDAL: TIdentityProfileDALFactory;
|
||||
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById" | "stopRepeatableJob">;
|
||||
orgDAL: TOrgDALFactory;
|
||||
};
|
||||
|
||||
export type TAutomatedSecurityServiceFactory = ReturnType<typeof automatedSecurityServiceFactory>;
|
||||
|
||||
export const ProfileIdentityResponseSchema = z.object({
|
||||
temporalProfile: z.string(),
|
||||
scopeProfile: z.string(),
|
||||
usageProfile: z.string()
|
||||
});
|
||||
|
||||
enum AnomalySeverity {
|
||||
LOW = "LOW",
|
||||
MEDIUM = "MEDIUM",
|
||||
HIGH = "HIGH",
|
||||
NONE = "NONE"
|
||||
}
|
||||
|
||||
export const AnomalyResponseSchema = z.object({
|
||||
results: z
|
||||
.object({
|
||||
auditLogId: z.string(),
|
||||
reason: z.string(),
|
||||
severity: z.nativeEnum(AnomalySeverity),
|
||||
isAnomalous: z.boolean()
|
||||
})
|
||||
.array()
|
||||
});
|
||||
|
||||
export const CaslRuleAnalysisSchema = z.object({
|
||||
results: z
|
||||
.object({
|
||||
rule: z.string(),
|
||||
classification: z.string(),
|
||||
justification: z.string()
|
||||
})
|
||||
.array()
|
||||
});
|
||||
|
||||
export const automatedSecurityServiceFactory = ({
|
||||
auditLogDAL,
|
||||
automatedSecurityReportDAL,
|
||||
identityProfileDAL,
|
||||
permissionService,
|
||||
queueService,
|
||||
orgDAL
|
||||
}: TAutomatedSecurityServiceFactoryDep) => {
|
||||
const getReports = async (orgId: string) => {
|
||||
const automatedReports = await automatedSecurityReportDAL.findByOrg(orgId, "pending");
|
||||
|
||||
return automatedReports;
|
||||
};
|
||||
|
||||
const patchSecurityReportStatus = async (id: string, status: "resolved" | "ignored") => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const securityReport = await automatedSecurityReportDAL.findById(id);
|
||||
if (!securityReport) {
|
||||
throw new NotFoundError({
|
||||
message: "Cannot find security report"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === "ignored") {
|
||||
const openAiClient = new OpenAI({
|
||||
apiKey: appCfg.AI_API_KEY
|
||||
});
|
||||
|
||||
const profileResponse = await openAiClient.beta.chat.completions.parse({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a security behavior analysis system that builds and maintains behavioral profiles.
|
||||
Your task is to analyze security events and prior profile information to generate cumulative profiles.
|
||||
Focus on three aspects:
|
||||
1. Temporal patterns (WHEN/HOW OFTEN)
|
||||
2. Scope patterns (WHAT/WHERE)
|
||||
3. Usage patterns (HOW)
|
||||
|
||||
Build comprehensive profiles:
|
||||
- Each profile should be 5-7 sentences to capture both established and emerging patterns
|
||||
- Core patterns get 1-2 sentences
|
||||
- Notable exceptions get 1-2 sentences
|
||||
- Emerging behaviors get 1-2 sentences
|
||||
- Key correlations get 1 sentence
|
||||
|
||||
Build profiles incrementally:
|
||||
- ALWAYS retain existing profile information unless directly contradicted
|
||||
- Add ANY new observed patterns, even from limited samples
|
||||
- Mark patterns as "emerging" if based on few observations
|
||||
- Use qualifiers like "occasionally" or "sometimes" for sparse patterns
|
||||
- Never dismiss patterns as insignificant due to sample size
|
||||
- Maintain history of both frequent and rare behaviors
|
||||
|
||||
When updating profiles:
|
||||
- Keep all historical patterns unless explicitly contradicted
|
||||
- Add new patterns with appropriate frequency qualifiers
|
||||
- Note both common and occasional behaviors
|
||||
- Use tentative language for new patterns ("appears to", "beginning to")
|
||||
- Highlight any changes or new observations
|
||||
|
||||
|
||||
Remember that dates being passed in are in ISO format`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Current behavioral profiles:
|
||||
${JSON.stringify(
|
||||
{
|
||||
temporalProfile: securityReport.temporalProfile,
|
||||
scopeProfile: securityReport.scopeProfile,
|
||||
usageProfile: securityReport.usageProfile
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
|
||||
New security event to analyze:
|
||||
${JSON.stringify(securityReport.event)}
|
||||
|
||||
Generate updated profiles that preserve ALL existing patterns and add new observations.
|
||||
Use appropriate qualifiers for frequency but include all behaviors.
|
||||
|
||||
Return exactly in this format:
|
||||
temporalProfile: "..."
|
||||
scopeProfile: "..."
|
||||
usageProfile: "..."`
|
||||
}
|
||||
],
|
||||
response_format: zodResponseFormat(ProfileIdentityResponseSchema, "profile_identity_response_schema")
|
||||
});
|
||||
|
||||
const parsedResponse = profileResponse.choices[0].message.parsed;
|
||||
|
||||
await identityProfileDAL.updateById(securityReport.profileId, {
|
||||
temporalProfile: parsedResponse?.temporalProfile ?? "",
|
||||
usageProfile: parsedResponse?.usageProfile ?? "",
|
||||
scopeProfile: parsedResponse?.scopeProfile ?? ""
|
||||
});
|
||||
}
|
||||
|
||||
await automatedSecurityReportDAL.updateById(id, {
|
||||
status
|
||||
});
|
||||
};
|
||||
|
||||
const processSecurityJob = async () => {
|
||||
const appCfg = getConfig();
|
||||
const orgs = await orgDAL.find({
|
||||
id: "1edc8c6e-8a39-499e-89a2-5d26149149cb"
|
||||
});
|
||||
|
||||
await Promise.allSettled(
|
||||
orgs.map(async (org) => {
|
||||
const orgUsers = await orgDAL.findAllOrgMembers(org.id);
|
||||
|
||||
const dateNow = new Date();
|
||||
const startDate = new Date(dateNow);
|
||||
|
||||
startDate.setMinutes(dateNow.getMinutes() - 30);
|
||||
|
||||
await Promise.all(
|
||||
orgUsers.map(async (orgUser) => {
|
||||
try {
|
||||
const openAiClient = new OpenAI({
|
||||
apiKey: appCfg.AI_API_KEY
|
||||
});
|
||||
|
||||
const auditLogEvents = await auditLogDAL.find({
|
||||
actorId: orgUser.user.id,
|
||||
orgId: org.id,
|
||||
startDate: startDate.toISOString(),
|
||||
limit: 100
|
||||
});
|
||||
|
||||
if (!auditLogEvents.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let normalEvents: TAuditLogs[] = auditLogEvents;
|
||||
|
||||
const identityProfile = await identityProfileDAL.transaction(async (tx) => {
|
||||
const profile = await identityProfileDAL.findOne(
|
||||
{
|
||||
userId: orgUser.user.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!profile) {
|
||||
return identityProfileDAL.create(
|
||||
{
|
||||
userId: orgUser.user.id,
|
||||
temporalProfile: "",
|
||||
usageProfile: "",
|
||||
scopeProfile: "",
|
||||
orgId: org.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return profile;
|
||||
});
|
||||
|
||||
if (identityProfile.usageProfile && identityProfile.scopeProfile && identityProfile.temporalProfile) {
|
||||
logger.info("Checking for anomalies...");
|
||||
const anomalyResponse = await openAiClient.beta.chat.completions.parse({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `
|
||||
You are a security analysis system that uses behavioral profiles to identify potentially suspicious activity for an identity.
|
||||
|
||||
Understanding Identity Profiles:
|
||||
- Temporal Profile: When and how frequently the identity normally operates
|
||||
- Scope Profile: What resources and permissions the identity typically uses
|
||||
- Usage Profile: How the identity normally interacts with systems
|
||||
|
||||
Security Analysis Rules:
|
||||
HIGH Severity - Clear security concerns:
|
||||
- Actions well outside the identity's normal scope of access
|
||||
- Resource access patterns suggesting compromise
|
||||
- Violation of critical security boundaries
|
||||
|
||||
MEDIUM Severity - Requires investigation:
|
||||
- Significant expansion of identity's normal access patterns
|
||||
- Unusual combination of valid permissions
|
||||
- Behavior suggesting possible credential misuse
|
||||
|
||||
NONE Severity (Default) - Expected behavior:
|
||||
- Actions within established identity patterns
|
||||
- Minor variations in normal behavior
|
||||
- Business-justified changes in access patterns
|
||||
- New but authorized behavior extensions
|
||||
|
||||
Note: LOW severity should not be used - an action either
|
||||
indicates a security concern (HIGH/MEDIUM) or it doesn't (NONE).
|
||||
|
||||
Analysis Process:
|
||||
1. Compare event against identity's established profiles
|
||||
2. Evaluate if deviations suggest potential security risks
|
||||
3. Consider business context and authorization
|
||||
4. Mark as anomalous ONLY if the deviation suggests security risk`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Given these established behavioral profiles for an identity:
|
||||
|
||||
temporalProfile: "${identityProfile.temporalProfile}"
|
||||
scopeProfile: "${identityProfile.scopeProfile}"
|
||||
usageProfile: "${identityProfile.usageProfile}"
|
||||
|
||||
Analyze these events for anomalies:
|
||||
${JSON.stringify(auditLogEvents, null, 2)}
|
||||
|
||||
Return response in this exact JSON format:
|
||||
{ results: [
|
||||
{
|
||||
"auditLogId": "event-123",
|
||||
"isAnomalous": false,
|
||||
"reason": "",
|
||||
"severity": "NONE"
|
||||
},
|
||||
{
|
||||
"auditLogId": "event-124",
|
||||
"isAnomalous": true,
|
||||
"reason": "Accessing production secrets outside business hours",
|
||||
"severity": "HIGH"
|
||||
}
|
||||
]}
|
||||
`
|
||||
}
|
||||
],
|
||||
response_format: zodResponseFormat(AnomalyResponseSchema, "anomaly_response_schema")
|
||||
});
|
||||
|
||||
const parsedAnomalyResponse = anomalyResponse.choices[0].message.parsed;
|
||||
const anomalyEvents = parsedAnomalyResponse?.results?.filter((val) => val.isAnomalous);
|
||||
console.log("ANOMALY EVENTS:", anomalyEvents);
|
||||
|
||||
const anomalyEventMap = anomalyEvents?.reduce((accum, item) => {
|
||||
return { ...accum, [item.auditLogId]: item };
|
||||
}, {}) as {
|
||||
[x: string]: {
|
||||
reason: string;
|
||||
severity: string;
|
||||
};
|
||||
};
|
||||
|
||||
const anomalousEventIds = anomalyEvents?.map((evt) => evt.auditLogId);
|
||||
normalEvents = auditLogEvents.filter((evt) => !anomalousEventIds?.includes(evt.id));
|
||||
|
||||
await Promise.all(
|
||||
(auditLogEvents ?? [])?.map(async (evt) => {
|
||||
const anomalyDetails = anomalyEventMap[evt.id];
|
||||
if (!anomalyDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
await automatedSecurityReportDAL.create({
|
||||
status: "pending",
|
||||
profileId: identityProfile.id,
|
||||
remarks: anomalyDetails.reason,
|
||||
severity: anomalyDetails.severity,
|
||||
event: JSON.stringify(evt, null, 2)
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Calibrating identity profile");
|
||||
// profile identity
|
||||
const profileResponse = await openAiClient.beta.chat.completions.parse({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a security behavior analysis system that builds and maintains behavioral profiles.
|
||||
Your task is to analyze security events and prior profile information to generate cumulative profiles.
|
||||
Focus on three aspects:
|
||||
1. Temporal patterns (WHEN/HOW OFTEN)
|
||||
2. Scope patterns (WHAT/WHERE)
|
||||
3. Usage patterns (HOW)
|
||||
|
||||
Build comprehensive profiles:
|
||||
- Each profile should be 5-7 sentences to capture both established and emerging patterns
|
||||
- Core patterns get 1-2 sentences
|
||||
- Notable exceptions get 1-2 sentences
|
||||
- Emerging behaviors get 1-2 sentences
|
||||
- Key correlations get 1 sentence
|
||||
|
||||
Build profiles incrementally:
|
||||
- ALWAYS retain existing profile information unless directly contradicted
|
||||
- Add ANY new observed patterns, even from limited samples
|
||||
- Mark patterns as "emerging" if based on few observations
|
||||
- Use qualifiers like "occasionally" or "sometimes" for sparse patterns
|
||||
- Never dismiss patterns as insignificant due to sample size
|
||||
- Maintain history of both frequent and rare behaviors
|
||||
|
||||
When updating profiles:
|
||||
- Keep all historical patterns unless explicitly contradicted
|
||||
- Add new patterns with appropriate frequency qualifiers
|
||||
- Note both common and occasional behaviors
|
||||
- Use tentative language for new patterns ("appears to", "beginning to")
|
||||
- Highlight any changes or new observations
|
||||
|
||||
|
||||
Remember that dates being passed in are in ISO format`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Current behavioral profiles:
|
||||
${JSON.stringify(
|
||||
{
|
||||
temporalProfile: identityProfile.temporalProfile,
|
||||
scopeProfile: identityProfile.scopeProfile,
|
||||
usageProfile: identityProfile.usageProfile
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
|
||||
New security events to analyze:
|
||||
${JSON.stringify(normalEvents, null, 2)}
|
||||
|
||||
Generate updated profiles that preserve ALL existing patterns and add new observations.
|
||||
Use appropriate qualifiers for frequency but include all behaviors.
|
||||
|
||||
Return exactly in this format:
|
||||
temporalProfile: "..."
|
||||
scopeProfile: "..."
|
||||
usageProfile: "..."`
|
||||
}
|
||||
],
|
||||
response_format: zodResponseFormat(ProfileIdentityResponseSchema, "profile_identity_response_schema")
|
||||
});
|
||||
|
||||
const parsedResponse = profileResponse.choices[0].message.parsed;
|
||||
|
||||
await identityProfileDAL.updateById(identityProfile.id, {
|
||||
temporalProfile: parsedResponse?.temporalProfile ?? "",
|
||||
usageProfile: parsedResponse?.usageProfile ?? "",
|
||||
scopeProfile: parsedResponse?.scopeProfile ?? ""
|
||||
});
|
||||
|
||||
console.log("FINISH");
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const analyzeIdentityProjectPermission = async (userId: string, projectId: string) => {
|
||||
const appCfg = getConfig();
|
||||
const userProjectPermission = await permissionService.getUserProjectPermission({
|
||||
userId,
|
||||
projectId,
|
||||
authMethod: AuthMethod.EMAIL,
|
||||
actionProjectType: ActionProjectType.Any,
|
||||
userOrgId: "1edc8c6e-8a39-499e-89a2-5d26149149cb"
|
||||
});
|
||||
|
||||
const identityProfile = await identityProfileDAL.findOne({
|
||||
userId
|
||||
});
|
||||
|
||||
const openAiClient = new OpenAI({
|
||||
apiKey: appCfg.AI_API_KEY
|
||||
});
|
||||
|
||||
const caslRuleAnalysisResponse = await openAiClient.beta.chat.completions.parse({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a security analysis system that evaluates CASL permission rules against behavioral profiles.
|
||||
|
||||
Your task is to identify CASL rules that appear unnecessary based on the identity's observed behavior.
|
||||
|
||||
Analysis process:
|
||||
1. Review the identity's behavioral profiles (temporal, scope, usage)
|
||||
2. Examine current CASL permission rules
|
||||
3. Identify rules that grant permissions never or rarely used based on the profiles
|
||||
4. Consider business context and security implications before marking rules as unnecessary
|
||||
|
||||
Classification criteria:
|
||||
- UNNECESSARY: Rule grants permissions never observed in the behavior profile
|
||||
- OVERPROVISIONED: Rule grants broader access than typically used
|
||||
- QUESTIONABLE: Rule grants permissions used very rarely (<5% of activity)
|
||||
|
||||
Provide justification for each classification based on specific patterns in the profiles.
|
||||
Consider both explicit patterns (directly mentioned) and implicit patterns (strongly implied).
|
||||
|
||||
Remember that security is the priority - when in doubt, mark a rule as QUESTIONABLE rather than UNNECESSARY.`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Identity Behavioral Profiles:
|
||||
${JSON.stringify(
|
||||
{
|
||||
temporalProfile: identityProfile.temporalProfile,
|
||||
scopeProfile: identityProfile.scopeProfile,
|
||||
usageProfile: identityProfile.usageProfile
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
|
||||
Current CASL Permission Rules:
|
||||
${JSON.stringify(userProjectPermission.permission.rules, null, 2)}
|
||||
|
||||
Analyze these rules against the behavioral profiles and identify which rules appear unnecessary.
|
||||
|
||||
Return in this exact format:
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"rule": "create secret",
|
||||
"classification": "UNNECESSARY",
|
||||
"justification": "No evidence in profile of needing this resource",
|
||||
},
|
||||
{
|
||||
"rule": "delete secret",
|
||||
"classification": "NECESSARY",
|
||||
"justification": "Regular access pattern in usage profile",
|
||||
}
|
||||
]
|
||||
}`
|
||||
}
|
||||
],
|
||||
response_format: zodResponseFormat(CaslRuleAnalysisSchema, "casl_rule_analysis_schema")
|
||||
});
|
||||
|
||||
return caslRuleAnalysisResponse.choices[0].message.parsed;
|
||||
};
|
||||
|
||||
const startJob = async () => {
|
||||
await queueService.stopRepeatableJob(
|
||||
QueueName.AutomatedSecurity,
|
||||
QueueJobs.ProfileIdentity,
|
||||
{ pattern: "0 0 * * *", utc: true },
|
||||
QueueName.AutomatedSecurity // just a job id
|
||||
);
|
||||
|
||||
await queueService.queue(QueueName.AutomatedSecurity, QueueJobs.ProfileIdentity, undefined, {
|
||||
delay: 5000,
|
||||
jobId: QueueName.AutomatedSecurity,
|
||||
repeat: { pattern: "0 0 * * *", utc: true }
|
||||
});
|
||||
};
|
||||
|
||||
queueService.start(QueueName.AutomatedSecurity, async (job) => {
|
||||
if (job.name === QueueJobs.ProfileIdentity) {
|
||||
await processSecurityJob();
|
||||
}
|
||||
});
|
||||
|
||||
queueService.listen(QueueName.AutomatedSecurity, "failed", (job, err) => {
|
||||
logger.error(err, "Failed to process job", job?.data);
|
||||
});
|
||||
|
||||
return {
|
||||
processSecurityJob,
|
||||
patchSecurityReportStatus,
|
||||
analyzeIdentityProjectPermission,
|
||||
startJob,
|
||||
getReports
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityProfileDALFactory = ReturnType<typeof identityProfileDALFactory>;
|
||||
|
||||
export const identityProfileDALFactory = (db: TDbClient) => {
|
||||
const identityProfileOrm = ormify(db, TableName.IdentityProfile);
|
||||
|
||||
return identityProfileOrm;
|
||||
};
|
2
frontend/src/hooks/api/automated-security/index.tsx
Normal file
2
frontend/src/hooks/api/automated-security/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
20
frontend/src/hooks/api/automated-security/mutations.ts
Normal file
20
frontend/src/hooks/api/automated-security/mutations.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { automatedSecurityKeys } from "./queries";
|
||||
|
||||
export const usePatchSecurityReportStatus = (orgId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, status }: { id: string; status: string }) => {
|
||||
await apiRequest.patch(`/api/v1/automated-security/reports/${id}/status`, {
|
||||
status
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: automatedSecurityKeys.getReports(orgId) });
|
||||
}
|
||||
});
|
||||
};
|
29
frontend/src/hooks/api/automated-security/queries.tsx
Normal file
29
frontend/src/hooks/api/automated-security/queries.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
export const automatedSecurityKeys = {
|
||||
getReports: (orgId: string) => [{ orgId }, "organization-security-reports"] as const
|
||||
};
|
||||
|
||||
export const useGetAutomatedSecurityReports = (orgId: string) => {
|
||||
return useQuery({
|
||||
queryKey: automatedSecurityKeys.getReports(orgId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<
|
||||
{
|
||||
id: string;
|
||||
profileId: string;
|
||||
event: string;
|
||||
remarks: string;
|
||||
severity: string;
|
||||
status: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
}[]
|
||||
>("/api/v1/automated-security/reports");
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
@ -348,6 +348,11 @@ export const MinimizedOrgSidebar = () => {
|
||||
Audit Logs
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link to="/organization/automated-security">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faCog} />}>
|
||||
Automated Security
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faCog} />}>
|
||||
Organization Settings
|
||||
|
@ -0,0 +1,103 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
PageHeader,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useGetAutomatedSecurityReports,
|
||||
usePatchSecurityReportStatus
|
||||
} from "@app/hooks/api/automated-security";
|
||||
|
||||
export const AutomatedSecurityPage = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: reportEntries } = useGetAutomatedSecurityReports(currentOrg.id);
|
||||
const { mutateAsync: patchSecurityReportStatus } = usePatchSecurityReportStatus(currentOrg.id);
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>Infisical | Automated Security</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Automated Security"
|
||||
description="Your organization's reliable AI security guard"
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Identity ID</Th>
|
||||
<Th>Name</Th>
|
||||
<Th>Event</Th>
|
||||
<Th>Remarks</Th>
|
||||
<Th>Severity</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{(reportEntries ?? []).map((entry) => (
|
||||
<Tr>
|
||||
<Td>{entry.userId}</Td>
|
||||
<Td>{entry.name}</Td>
|
||||
<Td>{JSON.stringify(entry.event, null, 2)}</Td>
|
||||
<Td>{entry.remarks}</Td>
|
||||
<Td>{entry.severity}</Td>
|
||||
<Td>
|
||||
<Button
|
||||
className="mb-4 w-[5rem]"
|
||||
onClick={async () => {
|
||||
await patchSecurityReportStatus({
|
||||
id: entry.id,
|
||||
status: "resolved"
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully resolved security report"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Resolve
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="w-[5rem]"
|
||||
onClick={async () => {
|
||||
await patchSecurityReportStatus({
|
||||
id: entry.id,
|
||||
status: "ignored"
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully ignored security report"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Ignore
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { AutomatedSecurityPage } from "./AutomatedSecurityPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/automated-security"
|
||||
)({
|
||||
component: AutomatedSecurityPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Home",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />,
|
||||
link: linkOptions({ to: "/organization/secret-manager/overview" })
|
||||
},
|
||||
{
|
||||
label: "Automated Security Page"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
@ -44,6 +44,7 @@ import { Route as organizationSettingsPageRouteImport } from './pages/organizati
|
||||
import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route'
|
||||
import { Route as organizationSecretScanningPageRouteImport } from './pages/organization/SecretScanningPage/route'
|
||||
import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route'
|
||||
import { Route as organizationAutomatedSecurityPageRouteImport } from './pages/organization/AutomatedSecurityPage/route'
|
||||
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
|
||||
import { Route as organizationAdminPageRouteImport } from './pages/organization/AdminPage/route'
|
||||
import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route'
|
||||
@ -508,6 +509,14 @@ const organizationBillingPageRouteRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const organizationAutomatedSecurityPageRouteRoute =
|
||||
organizationAutomatedSecurityPageRouteImport.update({
|
||||
id: '/automated-security',
|
||||
path: '/automated-security',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const organizationAuditLogsPageRouteRoute =
|
||||
organizationAuditLogsPageRouteImport.update({
|
||||
id: '/audit-logs',
|
||||
@ -1837,6 +1846,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof organizationAuditLogsPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/automated-security': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/automated-security'
|
||||
path: '/automated-security'
|
||||
fullPath: '/organization/automated-security'
|
||||
preLoaderRoute: typeof organizationAutomatedSecurityPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/billing': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/billing'
|
||||
path: '/billing'
|
||||
@ -2904,6 +2920,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
|
||||
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
|
||||
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
|
||||
organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute
|
||||
organizationAutomatedSecurityPageRouteRoute: typeof organizationAutomatedSecurityPageRouteRoute
|
||||
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
|
||||
organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute
|
||||
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
|
||||
@ -2925,6 +2942,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
|
||||
organizationAccessManagementPageRouteRoute,
|
||||
organizationAdminPageRouteRoute: organizationAdminPageRouteRoute,
|
||||
organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute,
|
||||
organizationAutomatedSecurityPageRouteRoute:
|
||||
organizationAutomatedSecurityPageRouteRoute,
|
||||
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
|
||||
organizationSecretScanningPageRouteRoute:
|
||||
organizationSecretScanningPageRouteRoute,
|
||||
@ -3601,6 +3620,7 @@ export interface FileRoutesByFullPath {
|
||||
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
|
||||
'/organization/admin': typeof organizationAdminPageRouteRoute
|
||||
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
|
||||
'/organization/automated-security': typeof organizationAutomatedSecurityPageRouteRoute
|
||||
'/organization/billing': typeof organizationBillingPageRouteRoute
|
||||
'/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
|
||||
'/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
|
||||
@ -3771,6 +3791,7 @@ export interface FileRoutesByTo {
|
||||
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
|
||||
'/organization/admin': typeof organizationAdminPageRouteRoute
|
||||
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
|
||||
'/organization/automated-security': typeof organizationAutomatedSecurityPageRouteRoute
|
||||
'/organization/billing': typeof organizationBillingPageRouteRoute
|
||||
'/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
|
||||
'/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
|
||||
@ -3949,6 +3970,7 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/access-management': typeof organizationAccessManagementPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/admin': typeof organizationAdminPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/automated-security': typeof organizationAutomatedSecurityPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
|
||||
@ -4129,6 +4151,7 @@ export interface FileRouteTypes {
|
||||
| '/organization/access-management'
|
||||
| '/organization/admin'
|
||||
| '/organization/audit-logs'
|
||||
| '/organization/automated-security'
|
||||
| '/organization/billing'
|
||||
| '/organization/secret-scanning'
|
||||
| '/organization/secret-sharing'
|
||||
@ -4298,6 +4321,7 @@ export interface FileRouteTypes {
|
||||
| '/organization/access-management'
|
||||
| '/organization/admin'
|
||||
| '/organization/audit-logs'
|
||||
| '/organization/automated-security'
|
||||
| '/organization/billing'
|
||||
| '/organization/secret-scanning'
|
||||
| '/organization/secret-sharing'
|
||||
@ -4474,6 +4498,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/access-management'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/admin'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/automated-security'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/billing'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
|
||||
@ -4843,6 +4868,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/access-management",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/admin",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/automated-security",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/billing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
|
||||
@ -4881,6 +4907,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "organization/AuditLogsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/automated-security": {
|
||||
"filePath": "organization/AutomatedSecurityPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/billing": {
|
||||
"filePath": "organization/BillingPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
|
||||
|
@ -15,6 +15,7 @@ const organizationRoutes = route("/organization", [
|
||||
route("/access-management", "organization/AccessManagementPage/route.tsx"),
|
||||
route("/admin", "organization/AdminPage/route.tsx"),
|
||||
route("/audit-logs", "organization/AuditLogsPage/route.tsx"),
|
||||
route("/automated-security", "organization/AutomatedSecurityPage/route.tsx"),
|
||||
route("/billing", "organization/BillingPage/route.tsx"),
|
||||
route("/secret-sharing", "organization/SecretSharingPage/route.tsx"),
|
||||
route("/settings", "organization/SettingsPage/route.tsx"),
|
||||
|
Reference in New Issue
Block a user