From 77c3404415a451493e01d97a63f11bb324294123 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 26 Dec 2024 16:32:36 +0000 Subject: [PATCH 1/2] feat(ui): Integration of audit logs with SIEM systems --- ui/cspell.json | 3 +- ui/packages/platform/src/actions/actions.js | 114 ++++- ui/packages/platform/src/api/api.js | 76 ++++ .../platform/src/components/Audit/Audit.tsx | 12 +- .../AuditSettingsForm/AuditSettingsForm.tsx | 423 ++++++++++++++++++ .../AuditSettingsFormWrapper.tsx | 32 ++ .../src/components/IndexPage/IndexPage.tsx | 44 +- .../components/IndexPage/IndexPageWrapper.tsx | 9 +- .../SIEMIntegrationForm.tsx | 181 ++++++++ ui/packages/platform/src/stores/store.js | 90 +++- ui/packages/shared/styles/icons.tsx | 8 + 11 files changed, 974 insertions(+), 18 deletions(-) create mode 100644 ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx create mode 100644 ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx create mode 100644 ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx diff --git a/ui/cspell.json b/ui/cspell.json index 62305d93..6cc3ab15 100644 --- a/ui/cspell.json +++ b/ui/cspell.json @@ -201,6 +201,7 @@ "sparql", "SPARQL", "subtransactions", - "mbox" + "mbox", + "SIEM" ] } diff --git a/ui/packages/platform/src/actions/actions.js b/ui/packages/platform/src/actions/actions.js index 507200e2..f16a8242 100644 --- a/ui/packages/platform/src/actions/actions.js +++ b/ui/packages/platform/src/actions/actions.js @@ -54,6 +54,7 @@ const Actions = Reflux.createActions([{ updateOrg: ASYNC_ACTION, createOrg: ASYNC_ACTION, updateAiBotSettings: ASYNC_ACTION, + updateAuditSettings: ASYNC_ACTION, inviteUser: ASYNC_ACTION, useDemoData: ASYNC_ACTION, setReportsProject: {}, @@ -114,7 +115,9 @@ const Actions = Reflux.createActions([{ downloadDblabSessionArtifact: ASYNC_ACTION, sendUserCode: ASYNC_ACTION, confirmUserEmail: ASYNC_ACTION, - confirmTosAgreement: ASYNC_ACTION + confirmTosAgreement: ASYNC_ACTION, + testSiemServiceConnection: ASYNC_ACTION, + getAuditEvents: ASYNC_ACTION }]); function timeoutPromise(ms, promise) { @@ -654,6 +657,42 @@ Actions.updateAiBotSettings.listen(function (token, orgId, orgData) { }); }); +Actions.updateAuditSettings.listen(function (token, orgId, orgData) { + let action = this; + + if (!api) { + settings.init(function () { + api = new Api(settings); + }); + } + + action.progressed({ orgId } + orgData); + timeoutPromise(REQUEST_TIMEOUT, api.updateAuditSettings(token, orgId, orgData)) + + .then(result => { + result.json() + .then(json => { + if (json) { + action.completed(json); + } else { + action.failed(new Error('wrong_reply')); + } + }) + .catch(err => { + console.error(err); + action.failed(new Error('wrong_reply')); + }); + }) + .catch(err => { + console.error(err); + if (err && err.message && err.message === 'timeout') { + action.failed(new Error('failed_fetch')); + } else { + action.failed(new Error('wrong_reply')); + } + }); +}); + Actions.createOrg.listen(function (token, orgData) { let action = this; @@ -1571,4 +1610,77 @@ Actions.confirmTosAgreement.listen(function (token) { ); }); + +Actions.testSiemServiceConnection.listen(function (token, data) { + let action = this; + + if (!api) { + settings.init(function () { + api = new Api(settings); + }); + } + + action.progressed(data); + timeoutPromise(REQUEST_TIMEOUT, api.testSiemServiceConnection(token, data)) + + .then(result => { + result.json() + .then(json => { + if (json) { + action.completed(json); + } else { + action.failed(new Error('wrong_reply')); + } + }) + .catch(err => { + console.error(err); + action.failed(new Error('wrong_reply')); + }); + }) + .catch(err => { + console.error(err); + if (err && err.message && err.message === 'timeout') { + action.failed(new Error('failed_fetch')); + } else { + action.failed(new Error('wrong_reply')); + } + }); +}); + +Actions.getAuditEvents.listen(function (token) { + let action = this; + + if (!api) { + settings.init(function () { + api = new Api(settings); + }); + } + + action.progressed(); + + timeoutPromise(REQUEST_TIMEOUT, api.getAuditEvents(token)) + .then(result => { + result.json() + .then(json => { + if (json) { + action.completed({ data: json }); + } else { + action.failed(new Error('wrong_reply')); + } + }) + .catch(err => { + console.error(err); + action.failed(new Error('wrong_reply')); + }); + }) + .catch(err => { + console.error(err); + if (err && err.message && err.message === 'timeout') { + action.failed(new Error('failed_fetch')); + } else { + action.failed(new Error('wrong_reply')); + } + }); +}); + export default Actions; diff --git a/ui/packages/platform/src/api/api.js b/ui/packages/platform/src/api/api.js index 308864b0..14d2d033 100644 --- a/ui/packages/platform/src/api/api.js +++ b/ui/packages/platform/src/api/api.js @@ -467,6 +467,71 @@ class Api { }); } + updateAuditSettings(token, orgId, orgData) { + let params = {}; + let headers = { + Authorization: 'Bearer ' + token, + prefer: 'return=representation' + }; + + if (typeof orgData.enableSiemIntegration !== 'undefined') { + params.siem_integration_enabled = orgData.enableSiemIntegration; + } + + if (typeof orgData.urlSchema !== 'undefined') { + params.siem_integration_url = orgData.urlSchema; + } + + if (typeof orgData.auditEvents !== "undefined") { + params.audit_events_to_log = orgData.auditEvents.map((item) => item.event_name) + } + + if (typeof orgData.headers !== 'undefined' && Array.isArray(orgData.headers)) { + orgData.headers = orgData.headers.filter(item => item.key && item.value); + if (Object.keys(orgData.headers).length > 0) { + params.siem_integration_request_headers = orgData.headers.reduce((acc, item) => { + acc[item.key] = item.value; + return acc; + }, {}); + } else { + params.siem_integration_request_headers = null + } + } + + return this.patch(`${this.apiServer}/orgs?id=eq.` + orgId, params, { + headers: headers + }); + } + + testSiemServiceConnection(token, data) { + let params = {}; + let headers = { + Accept: 'application/vnd.pgrst.object+json', + Authorization: 'Bearer ' + token, + prefer: 'return=representation' + }; + + if (typeof data.urlSchema !== 'undefined') { + params.api_url = data.urlSchema; + } + + if (typeof data.headers !== 'undefined' && Array.isArray(data.headers)) { + data.headers = data.headers.filter(item => item.key && item.value); + if (Object.keys(data.headers).length > 0) { + params.http_headers_extra = data.headers.reduce((acc, item) => { + acc[item.key] = item.value; + return acc; + }, {}); + } else { + params.http_headers_extra = null + } + } + + return this.post(`${this.apiServer}/rpc/test_siem_connection`, params, { + headers: headers + }); + } + inviteUser(token, orgId, email) { let headers = { Authorization: 'Bearer ' + token @@ -992,6 +1057,17 @@ class Api { { headers } ); } + + getAuditEvents(token) { + let params = {}; + let headers = { + Authorization: 'Bearer ' + token + }; + + return this.get(`${this.apiServer}/audit_events`, params, { + headers: headers + }); + } } export default Api; diff --git a/ui/packages/platform/src/components/Audit/Audit.tsx b/ui/packages/platform/src/components/Audit/Audit.tsx index 6704de25..7e6b337d 100644 --- a/ui/packages/platform/src/components/Audit/Audit.tsx +++ b/ui/packages/platform/src/components/Audit/Audit.tsx @@ -50,7 +50,7 @@ export interface AuditLogData { action: string actor: string action_data: { - processed_rows_count: number + processed_row_count: number } created_at: string table_name: string @@ -155,11 +155,11 @@ class Audit extends Component { actorSrc = ' (changed directly in database) ' } - if (r.action_data && r.action_data.processed_rows_count) { + if (r.action_data && r.action_data.processed_row_count) { rows = - r.action_data.processed_rows_count + + r.action_data.processed_row_count + ' ' + - (r.action_data.processed_rows_count > 1 ? 'rows' : 'row') + (r.action_data.processed_row_count > 1 ? 'rows' : 'row') } switch (r.action) { @@ -197,8 +197,8 @@ class Audit extends Component { ? r.data_before?.length : r.data_after?.length const objCount = - r.action_data && r.action_data.processed_rows_count - ? r.action_data.processed_rows_count + r.action_data && r.action_data.processed_row_count + ? r.action_data.processed_row_count : null if (displayedCount && (objCount as number) > displayedCount) { diff --git a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx new file mode 100644 index 00000000..a952f1bd --- /dev/null +++ b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx @@ -0,0 +1,423 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import React, { useEffect, useMemo, useState } from 'react' +import { Link } from '@postgres.ai/shared/components/Link2' +import { + Grid, + Button, + FormControl, + FormControlLabel, + makeStyles, + Typography +} from '@material-ui/core' +import * as Yup from 'yup'; +import Store from '../../stores/store' +import Actions from '../../actions/actions' +import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' +import ConsolePageTitle from '../ConsolePageTitle' +import { AuditSettingsFormProps } from './AuditSettingsFormWrapper' +import { styles } from "@postgres.ai/shared/styles/styles"; +import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; +import { WarningWrapper } from "../Warning/WarningWrapper"; +import { messages } from "../../assets/messages"; +import { ExternalIcon } from "@postgres.ai/shared/icons/External"; +import { useFormik } from "formik"; +import Checkbox from '@mui/material/Checkbox/Checkbox' +import { SIEMIntegrationForm } from "../SIEMIntegrationForm/SIEMIntegrationForm"; + +type AuditSettingState = { + data: { + auth: { + token: string | null + } | null + orgProfile: { + isUpdating: boolean + error: boolean + updateError: boolean + errorMessage: string | undefined + errorCode: number | undefined + updateErrorMessage: string | null + isProcessing: boolean + orgId: number | null + updateErrorFields: string[] + data: { + siem_integration_enabled: SiemSettings["enableSiemIntegration"] + siem_integration_url: SiemSettings["urlSchema"] + siem_integration_request_headers: SiemSettings["headers"] + audit_events_to_log: string[] + } + } | null + auditEvents: { + isProcessing: boolean + data: { + id: number + event_name: string + label: string + }[] | null + } | null + } | null +} + +interface SiemSettings { + enableSiemIntegration: boolean; + urlSchema?: string; + headers: { key: string; value: string }[]; + auditEvents: EventsToLog[]; +} + +interface EventsToLog { + id: number; + event_name: string; + label: string; +} + +export interface FormValues { + siemSettings: SiemSettings; +} + +const useStyles = makeStyles( + { + container: { + ...(styles.root as Object), + display: 'flex', + 'flex-wrap': 'wrap', + 'min-height': 0, + '&:not(:first-child)': { + 'margin-top': '20px', + }, + }, + textField: { + ...styles.inputField, + }, + instructionsField: { + ...styles.inputField, + }, + selectField: { + marginTop: 4, + + }, + label: { + color: '#000!important', + fontWeight: 'bold', + }, + updateButtonContainer: { + marginTop: 20, + textAlign: 'left', + }, + unlockNote: { + marginTop: 8, + '& ol': { + paddingLeft: 24, + marginTop: 6, + marginBottom: 0 + } + }, + externalIcon: { + width: 14, + height: 14, + marginLeft: 4, + transform: 'translateY(2px)', + }, + testConnectionButton: { + marginRight: 16 + }, + eventRow: { + display: 'flex', + alignItems: 'center', + marginBottom: '10px', + }, + }, + { index: 1 }, +) + +const validationSchema = Yup.object({ + siemSettings: Yup.object({ + urlSchema: Yup.string() + .url('Invalid URL format') // Validates that the input is a valid URL + .required('URL is required'), // Field is mandatory + headers: Yup.array().of( + Yup.object({ + key: Yup.string().optional(), + value: Yup.string().optional(), + }) + ), + auditEvents: Yup.array() + }), +}); + +const AuditSettingsForm: React.FC = (props) => { + const { orgPermissions, orgData, orgId, org, project } = props; + const classes = useStyles(); + const [data, setData] = useState(null); + + useEffect(() => { + const unsubscribe = Store.listen(function () { + const newStoreData = this.data; + + if (JSON.stringify(newStoreData) !== JSON.stringify(data)) { + const auth = newStoreData?.auth || null; + const orgProfile = newStoreData?.orgProfile || null; + const auditEvents = newStoreData?.auditEvents || null; + + if ( + auth?.token && + orgProfile && + orgProfile.orgId !== orgId && + !orgProfile.isProcessing + ) { + Actions.getOrgs(auth.token, orgId); + } + + if (auth?.token && auditEvents && !auditEvents.isProcessing) { + Actions.getAuditEvents(auth.token); + } + + setData(newStoreData); + } + }); + + Actions.refresh(); + + return () => { + unsubscribe(); + }; + }, [orgId, data, props.match.params.projectId]); + + const isAuditLogsSettingsAvailable = useMemo(() => { + const privileged_until = orgData?.priveleged_until; + return !!(orgData && privileged_until && new Date(privileged_until) > new Date() && orgData?.data?.plan === 'EE'); + + }, [orgData]) + + const formik = useFormik({ + enableReinitialize: true, + initialValues: { + siemSettings: { + enableSiemIntegration: Boolean(data?.orgProfile?.data?.siem_integration_enabled), + urlSchema: data?.orgProfile?.data?.siem_integration_url || '', + headers: data?.orgProfile?.data?.siem_integration_request_headers + ? Object.entries(data.orgProfile.data.siem_integration_request_headers).map(([key, value]) => ({ + key: key || '', + value: value || '', + })) as unknown as SiemSettings['headers'] + : [{ key: '', value: '' }], + auditEvents: data?.auditEvents?.data + ? data?.auditEvents?.data + ?.filter((event) => + data?.orgProfile?.data?.audit_events_to_log?.includes(event.event_name) + ) + ?.map((event) => ({ + id: event.id, + event_name: event.event_name, + label: event.label, + })) + : [], + }, + }, + validationSchema, + onSubmit: async (values, { setSubmitting }) => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + console.error('Validation errors:', errors); + setSubmitting(false); + return; // Stop submission if there are errors + } + + const currentOrgId = orgId || null; + const auth = data?.auth || null; + + if (auth) { + const params = formik.values.siemSettings; + try { + await Actions.updateAuditSettings(auth.token, currentOrgId, params); + } catch (error) { + const errorMessage = `Error updating audit settings: ${error}`; + Actions.showNotification(errorMessage, 'error'); + console.error('Error updating audit settings:', error); + } finally { + setSubmitting(false); + } + } + } + }); + + const isDisabled = useMemo(() => + !isAuditLogsSettingsAvailable || !formik.values.siemSettings.enableSiemIntegration, + [isAuditLogsSettingsAvailable, formik.values.siemSettings.enableSiemIntegration] + ); + + const testConnection = async () => { + try { + const auth = data?.auth || null; + + if (auth) { + const params = {...formik.values.siemSettings}; + if (formik.values.siemSettings.urlSchema) { + Actions.testSiemServiceConnection(auth.token, params); + } + } + } catch (error) { + console.error('Connection failed:', error); + } + }; + + const breadcrumbs = ( + + ); + + const pageTitle = ; + + if (orgPermissions && !orgPermissions.settingsOrganizationUpdate) { + return ( + <> + {breadcrumbs} + {pageTitle} + {messages.noPermissionPage} + + ); + } + + if (!data || (data && data.orgProfile && data.orgProfile.isProcessing) || (data && data.auditEvents && data.auditEvents.isProcessing)) { + return ( +
+ {breadcrumbs} + {pageTitle} + +
+ ); + } + + return ( + <> + {breadcrumbs} + {pageTitle} +
+ + + + {!isAuditLogsSettingsAvailable && + + Become an Enterprise customer + + +  to unlock audit settings + } + + + SIEM audit logs integration documentation + + + + + +

SIEM integration

+ + formik.setFieldValue( + 'siemSettings.enableSiemIntegration', + e.target.checked + ) + } + /> + } + label="Send audit events to SIEM system" + disabled={!isAuditLogsSettingsAvailable} + /> +

SIEM connection settings

+ +
+
+
+ + + + + + +

Select audit events to export

+ {data?.auditEvents?.data && + data?.auditEvents?.data?.map((event) => { + const isChecked = formik.values.siemSettings.auditEvents.some( + (e) => e.event_name === event.event_name + ); + + return ( +
+ { + const updatedAuditEvents = e.target.checked + ? [...formik.values.siemSettings.auditEvents, { ...event }] + : formik.values.siemSettings.auditEvents.filter( + (auditEvent) => auditEvent.event_name !== event.event_name + ); + + formik.setFieldValue('siemSettings.auditEvents', updatedAuditEvents); + }} + /> + } + label={event.label} + disabled={isDisabled} + /> +
+ ); + })} +
+
+
+ + + +
+
+
+ + ); +}; + +export default AuditSettingsForm diff --git a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx new file mode 100644 index 00000000..3ae26ec9 --- /dev/null +++ b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import AuditSettingsForm from "./AuditSettingsForm"; + +export interface AuditSettingsFormProps { + mode?: string | undefined + project?: string | undefined + org?: string | number + orgId?: number + orgPermissions?: { + settingsOrganizationUpdate?: boolean + } + orgData?: { + priveleged_until: Date + chats_private_allowed: boolean + data?: { + plan?: string + } | null + } + match: { + params: { + project?: string + projectId?: string | number | undefined + org?: string + } + } +} + + + +export const AuditSettingsFormWrapper = (props: AuditSettingsFormProps) => { + return +} diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index c5a20e98..c1a2ad7f 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -87,6 +87,7 @@ import { PostgresClusterInstallWrapper } from 'components/PostgresClusterInstall import { PostgresClustersWrapper } from 'components/PostgresClusters/PostgresClustersWrapper' import cn from "classnames"; import { BotSettingsFormWrapper } from "../BotSettingsForm/BotSettingsFormWrapper"; +import { AuditSettingsFormWrapper } from "../AuditSettingsForm/AuditSettingsFormWrapper"; import { ExpandLess, ExpandMore } from "@material-ui/icons"; @@ -397,6 +398,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { + {orgPermissions && orgPermissions.auditLogView && ( + + + {icons.auditLogIcon} + + Audit + + )} {icons.settingsIcon} - Settings + Manage {activeMenuItems.has('settings') ? : } @@ -639,7 +665,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { activeClassName={parentProps.classes.menuItemActiveLink} to={'/' + org + '/settings'} > - General + General settings )} @@ -655,7 +681,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { activeClassName={parentProps.classes.menuItemActiveLink} to={'/' + org + '/assistant-settings'} > - AI Assistant + AI Assistant settings )} @@ -702,7 +728,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { )} - {orgPermissions && orgPermissions.auditLogView && ( + {orgData !== null && orgPermissions && Permissions.isAdmin(orgData) && orgPermissions.auditLogView && ( - Audit + Audit settings )} @@ -985,6 +1011,12 @@ function OrganizationWrapper(parentProps: OrganizationWrapperProps) { )} /> + ( + + )} + /> ( diff --git a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx index 088cb735..47407451 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx @@ -206,7 +206,7 @@ export const IndexPageWrapper = (props: IndexPageProps) => { marginTop: '10px', }, menuSectionHeaderCollapsible: { - marginTop: 0 + //marginTop: 0 }, bottomFixedMenuItem: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', @@ -228,7 +228,12 @@ export const IndexPageWrapper = (props: IndexPageProps) => { color: '#000000', display: 'inline-flex', alignItems: 'center', - whiteSpace: 'nowrap' + whiteSpace: 'nowrap', + margin: 0 + }, + menuSectionHeaderLinkCollapsible: { + paddingTop: '10px!important', + paddingBottom: '10px!important', }, menuSectionHeaderActiveLink: { textDecoration: 'none', diff --git a/ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx b/ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx new file mode 100644 index 00000000..8f27fdc3 --- /dev/null +++ b/ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx @@ -0,0 +1,181 @@ +import React, { useMemo, useState } from 'react'; +import { TextField, Grid, IconButton } from '@mui/material'; +import { Button, makeStyles } from "@material-ui/core"; +import { styles } from "@postgres.ai/shared/styles/styles"; +import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline'; +import { FormikErrors, useFormik } from "formik"; +import { FormValues } from "../AuditSettingsForm/AuditSettingsForm"; + +const useStyles = makeStyles({ + textField: { + ...styles.inputField, + maxWidth: 450, + }, + requestHeadersContainer: { + paddingTop: '8px!important' + }, + label: { + color: '#000!important', + margin: 0 + }, + requestHeadersTextFieldContainer: { + flexBasis: 'calc(100% / 2 - 20px)!important', + width: 'calc(100% / 2 - 20px)!important', + }, + requestHeadersIconButtonContainer: { + width: '32px!important', + height: '32px!important', + padding: '0!important', + marginLeft: 'auto!important', + marginTop: '12px!important', + '& button': { + width: 'inherit', + height: 'inherit' + } + } +}) + +interface SIEMIntegrationFormProps { + formik: ReturnType>; + disabled: boolean +} + +export const SIEMIntegrationForm: React.FC = ({ formik, disabled }) => { + const classes = useStyles(); + const [isFocused, setIsFocused] = useState(false); + const [focusedHeaderIndex, setFocusedHeaderIndex] = useState(null); + + const getTruncatedUrl = (url: string) => { + const parts = url.split('/'); + return parts.length > 3 ? parts.slice(0, 3).join('/') + '/*****/' : url; + }; + + const handleHeaderValueDisplay = (index: number, value: string) => { + if (focusedHeaderIndex === index) { + return value; + } + if (value.length) { + return "*****"; + } else { + return '' + } + }; + + const handleFocusHeaderValue = (index: number) => setFocusedHeaderIndex(index); + const handleBlurHeaderValue = () => setFocusedHeaderIndex(null); + + const handleFocus = () => setIsFocused(true); + const handleBlur = () => setIsFocused(false); + + const handleHeaderChange = (index: number, field: 'key' | 'value', value: string) => { + const headers = formik.values.siemSettings.headers || []; + const updatedHeaders = [...headers]; + updatedHeaders[index] = { + ...updatedHeaders[index], + [field]: value, + }; + formik.setFieldValue('siemSettings.headers', updatedHeaders); + }; + + const addHeader = () => { + const headers = formik.values.siemSettings.headers || []; + const updatedHeaders = [...headers, { key: '', value: '' }]; + formik.setFieldValue('siemSettings.headers', updatedHeaders); + }; + + const removeHeader = (index: number) => { + const updatedHeaders = formik.values.siemSettings?.headers?.filter((_, i) => i !== index); + formik.setFieldValue('siemSettings.headers', updatedHeaders); + }; + + return ( + + + formik.setFieldValue('siemSettings.urlSchema', e.target.value)} + onFocus={handleFocus} + onBlur={(e) => { + formik.handleBlur(e); + handleBlur(); + }} + margin="normal" + fullWidth + placeholder="https://{siem-host}/{path}" + inputProps={{ + name: 'siemSettings.urlSchema', + id: 'urlSchemaTextField', + shrink: 'true', + }} + InputLabelProps={{ + shrink: true, + }} + disabled={disabled} + error={formik.touched.siemSettings?.urlSchema && !!formik.errors.siemSettings?.urlSchema} + helperText={formik.touched.siemSettings?.urlSchema && formik.errors.siemSettings?.urlSchema} + /> + + +

Request headers

+ {formik.values.siemSettings.headers.map((header, index) => ( + + + handleHeaderChange(index, 'key', e.target.value)} + placeholder="Authorization" + inputProps={{ + name: `siemSettings.headers[${index}].key`, + id: `requestHeaderKeyField${index}`, + shrink: 'true', + }} + InputLabelProps={{ + shrink: true, + }} + margin="normal" + disabled={disabled} + /> + + + handleHeaderChange(index, 'value', e.target.value)} + onFocus={() => handleFocusHeaderValue(index)} + onBlur={handleBlurHeaderValue} + placeholder="token" + inputProps={{ + name: `siemSettings.headers[${index}].value`, + id: `requestHeaderValueField${index}`, + shrink: 'true', + }} + InputLabelProps={{ + shrink: true, + }} + margin="normal" + disabled={disabled} + /> + + + removeHeader(index)} disabled={disabled}> + + + + + ))} + +
+
+ ); +}; \ No newline at end of file diff --git a/ui/packages/platform/src/stores/store.js b/ui/packages/platform/src/stores/store.js index 7ff47b6b..222f2d22 100644 --- a/ui/packages/platform/src/stores/store.js +++ b/ui/packages/platform/src/stores/store.js @@ -236,7 +236,8 @@ const initialState = { isLogDownloading: false, logs: {} }, - auditLog: storeItem + auditLog: storeItem, + auditEvents: {...storeItem} }; const Store = Reflux.createStore({ @@ -594,6 +595,58 @@ const Store = Reflux.createStore({ }, + onUpdateAuditSettingsFailed: function (error) { + this.data.orgProfile.isUpdating = false; + this.data.orgProfile.updateError = true; + this.data.orgProfile.updateErrorMessage = error.message; + this.trigger(this.data); + }, + + onUpdateAuditSettingsProgressed: function (data) { + this.data.orgProfile.updateErrorFields = null; + this.data.orgProfile.isUpdating = true; + + this.trigger(this.data); + }, + + onUpdateAuditSettingsCompleted: function (data) { + this.data.orgProfile.isUpdating = false; + this.data.orgProfile.updateErrorMessage = this.getError(data); + this.data.orgProfile.updateError = !!this.data.orgProfile.updateErrorMessage; + + if (!this.data.orgProfile.updateError && data.length > 0) { + this.data.orgProfile.updateErrorFields = null; + this.data.orgProfile.data = data[0]; + Actions.getUserProfile(this.data.auth.token); + Actions.getOrgs(this.data.auth.token, this.data.orgProfile.orgId); + Actions.showNotification('Audit settings successfully saved.', 'success'); + } + + this.trigger(this.data); + }, + + onTestSiemServiceConnectionFailed: function (error) { + this.data.orgProfile.isUpdating = false; + this.trigger(this.data); + }, + + onTestSiemServiceConnectionProgressed: function (data) { + this.data.orgProfile.isUpdating = true; + this.trigger(this.data); + }, + + onTestSiemServiceConnectionCompleted: function (data) { + this.data.orgProfile.isUpdating = false; + if (data && data.test_siem_connection && data.test_siem_connection.status && data.test_siem_connection.status < 300) { + Actions.showNotification('Connection successful', 'success'); + } else { + Actions.showNotification('Connection error', 'error'); + } + + this.trigger(this.data); + }, + + onCreateOrgFailed: function (error) { this.data.orgProfile.isUpdating = false; this.data.orgProfile.updateError = true; @@ -2961,7 +3014,40 @@ const Store = Reflux.createStore({ } this.trigger(this.data); - } + }, + + onGetAuditEventsFailed: function (error) { + this.data.auditEvents.isProcessing = false; + this.data.auditEvents.error = true; + this.data.auditEvents.errorMessage = error.message; + this.trigger(this.data); + }, + + onGetAuditEventsProgressed: function (data) { + this.data.auditEvents.isProcessing = true; + + this.trigger(this.data); + }, + + onGetAuditEventsCompleted: function (data) { + this.data.auditEvents.isProcessing = false; + this.data.auditEvents.errorMessage = this.getError(data.data); + this.data.auditEvents.error = this.data.orgProfile.errorMessage; + + if (!this.data.auditEvents.error) { + if (data.data.length > 0) { + this.data.auditEvents.isProcessed = true; + this.data.auditEvents = {...data}; + } else { + this.data.auditEvents.error = true; + this.data.auditEvents.errorMessage = + 'You do not have permission to view this page.'; + this.data.auditEvents.errorCode = 403; + } + } + + this.trigger(this.data); + }, }); diff --git a/ui/packages/shared/styles/icons.tsx b/ui/packages/shared/styles/icons.tsx index fd8da26c..d1d52fc5 100644 --- a/ui/packages/shared/styles/icons.tsx +++ b/ui/packages/shared/styles/icons.tsx @@ -1899,5 +1899,13 @@ export const icons = { + ), + auditLogIcon: ( + + + ) } From b8f572d423680ddd9d5c55f26c0f193b994dc729 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 26 Dec 2024 21:26:00 +0000 Subject: [PATCH 2/2] fix (ui): Audit logs page update --- .../platform/src/components/Audit/Audit.tsx | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/ui/packages/platform/src/components/Audit/Audit.tsx b/ui/packages/platform/src/components/Audit/Audit.tsx index 7e6b337d..692c09ff 100644 --- a/ui/packages/platform/src/components/Audit/Audit.tsx +++ b/ui/packages/platform/src/components/Audit/Audit.tsx @@ -45,12 +45,12 @@ interface AuditWithStylesProps extends AuditProps { export interface AuditLogData { id: number - data_before: string - data_after: string action: string actor: string action_data: { processed_row_count: number + data_before: Record[] + data_after: Record[] } created_at: string table_name: string @@ -193,9 +193,9 @@ class Audit extends Component { } getChangesTitle = (r: AuditLogData) => { - const displayedCount = r.data_before - ? r.data_before?.length - : r.data_after?.length + const displayedCount = r.action_data && r.action_data.data_before + ? r.action_data.data_before?.length + : r.action_data?.data_after?.length const objCount = r.action_data && r.action_data.processed_row_count ? r.action_data.processed_row_count @@ -243,15 +243,6 @@ class Audit extends Component { const pageTitle = ( 0 - ? { - filterValue: this.state.filterValue, - filterHandler: this.filterInputHandler, - placeholder: 'Search audit log', - } - : null - } /> ) @@ -310,7 +301,7 @@ class Audit extends Component { {this.formatAction(r)} - {(r.data_before || r.data_after) && ( + {((r.action_data && r.action_data.data_before) || (r.action_data && r.action_data.data_after)) && (
{ - {r.data_before && ( + {r.action_data && r.action_data.data_before && (
{this.getDataSectionTitle(r, true)} { multiline fullWidth value={JSON.stringify( - r.data_before, + r.action_data.data_before, null, 4, )} @@ -347,7 +338,7 @@ class Audit extends Component { />
)} - {r.data_after && ( + {r.action_data && r.action_data.data_after && (
{this.getDataSectionTitle(r, false)} { multiline fullWidth value={JSON.stringify( - r.data_after, + r.action_data.data_after, null, 4, )}