diff --git a/public/pages/Correlations/containers/CorrelationRules.tsx b/public/pages/Correlations/containers/CorrelationRules.tsx index 30c364295..5ffb88185 100644 --- a/public/pages/Correlations/containers/CorrelationRules.tsx +++ b/public/pages/Correlations/containers/CorrelationRules.tsx @@ -80,8 +80,8 @@ export const CorrelationRules: React.FC = (props: RouteComp const onRuleNameClick = useCallback((rule: CorrelationRule) => { props.history.push({ - pathname: ROUTES.CORRELATION_RULE_CREATE, - state: { rule, isReadOnly: true }, + pathname: `${ROUTES.CORRELATION_RULE_EDIT}/${rule.id}`, + state: { rule, isReadOnly: false }, }); }, []); diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index aa7ed603c..3b4231a90 100644 --- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx +++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx @@ -33,9 +33,9 @@ import { CorrelationRuleModel, CorrelationRuleQuery, } from '../../../../types'; -import { BREADCRUMBS, ROUTES, isDarkMode } from '../../../utils/constants'; +import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; import { CoreServicesContext } from '../../../components/core_services'; -import { RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, useParams } from 'react-router-dom'; import { CorrelationsExperimentalBanner } from '../components/ExperimentalBanner'; import { validateName } from '../../../utils/validation'; import { FieldMappingService, IndexService } from '../../../services'; @@ -99,41 +99,46 @@ export const CreateCorrelationRule: React.FC = ( return undefined; }, []); + const params = useParams<{ ruleId: string }>(); + const [initialValues, setInitialValues] = useState({ + ...correlationRuleStateDefaultValue, + }); + const [action, setAction] = useState('Create'); + + useEffect(() => { + if (props.history.location.state?.rule) { + setAction('Edit'); + setInitialValues(props.history.location.state?.rule); + } else if (params.ruleId) { + const setInitialRuleValues = async () => { + const ruleRes = await correlationStore.getCorrelationRule(params.ruleId); + if (ruleRes) { + setInitialValues(ruleRes); + } + }; + + setAction('Edit'); + setInitialRuleValues(); + } + }, []); const submit = async (values: any) => { let error; if ((error = validateCorrelationRule(values))) { - errorNotificationToast(props.notifications, 'Create', 'rule', error); + errorNotificationToast(props.notifications, action, 'rule', error); return; } - await correlationStore.createCorrelationRule(values); + if (action === 'Edit') { + await correlationStore.updateCorrelationRule(values); + } else { + await correlationStore.createCorrelationRule(values); + } props.history.push(ROUTES.CORRELATION_RULES); }; const context = useContext(CoreServicesContext); - let action: CorrelationRuleAction = 'Create'; - let initialValues = { - ...correlationRuleStateDefaultValue, - }; - - if (props.history.location.state?.rule) { - action = 'Edit'; - initialValues = props.history.location.state?.rule; - - if (props.history.location.state.isReadOnly) { - action = 'Readonly'; - } - } - - const disableForm = action === 'Readonly'; - const textClassName = disableForm - ? isDarkMode - ? 'readonly-text-color-dark-mode' - : 'readonly-text-color-light-mode' - : undefined; - const parseOptions = (indices: string[]) => { return indices.map( (index: string): CorrelationOptions => ({ @@ -206,9 +211,17 @@ export const CreateCorrelationRule: React.FC = ( } extraAction={ - queryIdx > 1 ? ( + correlationQueries.length > 2 ? ( - + { + const newQueries = [...correlationQueries]; + newQueries.splice(queryIdx, 1); + props.setFieldValue('queries', newQueries); + }} + /> ) : null } @@ -247,8 +260,6 @@ export const CreateCorrelationRule: React.FC = ( query.index ? [{ value: query.index, label: query.index }] : [] } isClearable={true} - isDisabled={disableForm} - className={textClassName} /> @@ -279,8 +290,6 @@ export const CreateCorrelationRule: React.FC = ( onCreateOption={(e) => { props.handleChange(`queries[${queryIdx}].logType`)(e); }} - isDisabled={disableForm} - className={textClassName} /> @@ -313,8 +322,6 @@ export const CreateCorrelationRule: React.FC = ( )(e); }} isClearable={true} - isDisabled={disableForm} - className={textClassName} /> ); @@ -332,8 +339,6 @@ export const CreateCorrelationRule: React.FC = ( `queries[${queryIdx}].conditions[${conditionIdx}].value` )} value={condition.value} - disabled={disableForm} - className={textClassName} /> ); @@ -352,7 +357,6 @@ export const CreateCorrelationRule: React.FC = ( )(e); }} className={'correlation_rule_field_condition'} - isDisabled={disableForm} /> ); @@ -387,7 +391,7 @@ export const CreateCorrelationRule: React.FC = ( initialIsOpen={true} buttonContent={`Field ${conditionIdx + 1}`} extraAction={ - query.conditions.length > 1 && !disableForm ? ( + query.conditions.length > 1 ? ( = ( newCases ); }} - disabled={disableForm} /> ) : null @@ -415,21 +418,18 @@ export const CreateCorrelationRule: React.FC = ( ); })} - {disableForm ? null : ( - { - props.setFieldValue(`queries[${queryIdx}].conditions`, [ - ...query.conditions, - ...correlationRuleStateDefaultValue.queries[0].conditions, - ]); - }} - iconType={'plusInCircle'} - disabled={disableForm} - > - Add field - - )} + { + props.setFieldValue(`queries[${queryIdx}].conditions`, [ + ...query.conditions, + ...correlationRuleStateDefaultValue.queries[0].conditions, + ]); + }} + iconType={'plusInCircle'} + > + Add field + @@ -437,21 +437,18 @@ export const CreateCorrelationRule: React.FC = ( ); })} - {disableForm ? null : ( - { - props.setFieldValue('queries', [ - ...correlationQueries, - { ...correlationRuleStateDefaultValue.queries[0] }, - ]); - }} - iconType={'plusInCircle'} - fullWidth={true} - disabled={disableForm} - > - Add query - - )} + { + props.setFieldValue('queries', [ + ...correlationQueries, + { ...correlationRuleStateDefaultValue.queries[0] }, + ]); + }} + iconType={'plusInCircle'} + fullWidth={true} + > + Add query + ); }; @@ -469,14 +466,12 @@ export const CreateCorrelationRule: React.FC = ( <> -

{action === 'Readonly' ? 'C' : `${action} c`}orrelation rule

+

{`${action} correlation rule`}

- {action === 'Readonly' ? null : ( - - {action === 'Create' ? 'Create a' : 'Edit'} correlation rule to define threat scenarios of - interest between different log sources. - - )} + + {action === 'Create' ? 'Create a' : 'Edit'} correlation rule to define threat scenarios of + interest between different log sources. + = ( setSubmitting(false); submit(values); }} + enableReinitialize={true} > {({ values: { name, queries }, touched, errors, ...props }) => { return ( @@ -514,9 +510,7 @@ export const CreateCorrelationRule: React.FC = ( isInvalid={touched.name && !!errors?.name} error={errors.name} helpText={ - disableForm - ? undefined - : 'Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores.' + 'Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores.' } > = ( }} onBlur={props.handleBlur('name')} value={name} - className={textClassName} - disabled={disableForm} /> @@ -538,9 +530,7 @@ export const CreateCorrelationRule: React.FC = ( diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index f8df933cf..a72847426 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -520,6 +520,17 @@ export default class Main extends Component { /> )} /> + ) => ( + + )} + /> ) => { diff --git a/public/pages/Rules/components/RuleEditor/components/SelectionExpField.tsx b/public/pages/Rules/components/RuleEditor/components/SelectionExpField.tsx index d7d555f22..bea0e6af5 100644 --- a/public/pages/Rules/components/RuleEditor/components/SelectionExpField.tsx +++ b/public/pages/Rules/components/RuleEditor/components/SelectionExpField.tsx @@ -8,7 +8,7 @@ import { EuiButtonIcon, EuiExpression, } from '@elastic/eui'; -import * as _ from 'lodash'; +import _ from 'lodash'; import { Selection } from '../DetectionVisualEditor'; export interface SelectionExpFieldProps { @@ -24,6 +24,19 @@ interface UsedSelection { description: string; } +const operationOptionsFirstExpression = [ + { value: '', text: '' }, + { value: 'not', text: 'NOT' }, +]; + +const operatorOptions = [ + { value: '', text: '' }, + { value: 'and', text: 'AND' }, + { value: 'or', text: 'OR' }, + { value: 'and not', text: 'AND NOT' }, + { value: 'or not', text: 'OR NOT' }, +]; + export const SelectionExpField: React.FC = ({ selections, dataTestSubj, @@ -31,13 +44,26 @@ export const SelectionExpField: React.FC = ({ value, }) => { const DEFAULT_DESCRIPTION = 'Select'; - const OPERATORS = ['and', 'or', 'not']; + const OPERATORS = ['and', 'or', 'and not', 'or not', 'not']; const [usedExpressions, setUsedExpressions] = useState([]); useEffect(() => { let expressions: UsedSelection[] = []; if (value?.length) { - let values = value.split(' '); + const temp = value.split('and not'); + let values = temp + .map((_) => { + return _.trim() + .split('or not') + .map((leaf) => leaf.split(' ')) + .reduce((prev, curr) => { + return [...prev, 'or not', ...curr]; + }); + }) + .reduce((prev, curr) => { + return [...prev, 'and not', ...curr]; + }); + if (OPERATORS.indexOf(values[0]) === -1) values = ['', ...values]; let counter = 0; @@ -110,12 +136,7 @@ export const SelectionExpField: React.FC = ({ compressed value={exp.description} onChange={(e) => changeExtDescription(e, exp, idx)} - options={[ - { value: '', text: '' }, - { value: 'and', text: 'AND' }, - { value: 'or', text: 'OR' }, - { value: 'not', text: 'NOT' }, - ]} + options={idx === 0 ? operationOptionsFirstExpression : operatorOptions} /> {selections.length > usedExpressions.length && ( @@ -212,7 +233,7 @@ export const SelectionExpField: React.FC = ({ description={exp.description} value={exp.name} isActive={exp.isOpen} - onClick={(e) => onSelectionPopup(e, idx)} + onClick={(e: any) => onSelectionPopup(e, idx)} /> } isOpen={exp.isOpen} diff --git a/public/services/CorrelationService.ts b/public/services/CorrelationService.ts index f409d6612..342185b62 100644 --- a/public/services/CorrelationService.ts +++ b/public/services/CorrelationService.ts @@ -13,6 +13,7 @@ import { GetCorrelationFindingsResponse, SearchCorrelationRulesResponse, ICorrelationsService, + UpdateCorrelationRuleResponse, } from '../../types'; export default class CorrelationService implements ICorrelationsService { @@ -34,15 +35,14 @@ export default class CorrelationService implements ICorrelationsService { }; getCorrelationRules = async ( - index?: string + index?: string, + query?: object ): Promise> => { const url = `..${API.CORRELATION_BASE}/_search`; - let query = { match_all: {} }; - return (await this.httpClient.post(url, { body: JSON.stringify({ - query, + query: query ?? { match_all: {} }, }), })) as ServerResponse; }; @@ -57,6 +57,17 @@ export default class CorrelationService implements ICorrelationsService { })) as ServerResponse; }; + updateCorrelationRule = async ( + id: string, + body: any + ): Promise> => { + const url = `..${API.CORRELATION_BASE}/${id}`; + + return (await this.httpClient.put(url, { + body: JSON.stringify(body), + })) as ServerResponse; + }; + deleteCorrelationRule = async ( ruleId: string ): Promise> => { diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts index 5d0ceaba2..f19e5402d 100644 --- a/public/store/CorrelationsStore.ts +++ b/public/store/CorrelationsStore.ts @@ -87,6 +87,57 @@ export class CorrelationsStore implements ICorrelationsStore { return response.ok; } + public async updateCorrelationRule(correlationRule: CorrelationRule): Promise { + const response = await this.invalidateCache().service.updateCorrelationRule( + correlationRule.id, + { + name: correlationRule.name, + correlate: correlationRule.queries?.map((query) => ({ + index: query.index, + category: query.logType, + query: query.conditions + .map((condition) => `${condition.name}:${condition.value}`) + .join(' AND '), + })), + } + ); + + if (!response.ok) { + errorNotificationToast(this.notifications, 'update', 'correlation rule', response.error); + return false; + } + + return response.ok; + } + + public async getCorrelationRule(id: string): Promise { + const response = await this.service.getCorrelationRules(undefined, { + terms: { + _id: [id], + }, + }); + + if (response?.ok && response.response.hits.hits[0]) { + const hit = response.response.hits.hits[0]; + + const queries: CorrelationRuleQuery[] = hit._source.correlate.map((queryData) => { + return { + index: queryData.index, + logType: queryData.category, + conditions: this.parseRuleQueryString(queryData.query), + }; + }); + + return { + id: hit._id, + name: hit._source.name, + queries, + }; + } + + return undefined; + } + public async getCorrelationRules(index?: string): Promise { const cacheKey: string = `getCorrelationRules:${JSON.stringify(arguments)}`; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index cac883c2d..fea6c072a 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -43,6 +43,7 @@ export const ROUTES = Object.freeze({ CORRELATIONS: '/correlations', CORRELATION_RULES: '/correlations/rules', CORRELATION_RULE_CREATE: '/correlations/create-rule', + CORRELATION_RULE_EDIT: '/correlations/rule', get LANDING_PAGE(): string { return this.OVERVIEW; diff --git a/public/utils/validation.ts b/public/utils/validation.ts index 35b70a9c5..83361e848 100644 --- a/public/utils/validation.ts +++ b/public/utils/validation.ts @@ -15,7 +15,7 @@ export const NAME_REGEX = new RegExp(/^[a-zA-Z0-9 _-]{5,50}$/); export const DETECTION_NAME_REGEX = new RegExp(/^[a-zA-Z0-9_.-]{5,50}$/); export const DETECTION_CONDITION_REGEX = new RegExp( - /^((not )?[a-zA-Z0-9_]+)?( (and|or|not) ?([a-zA-Z0-9_]+))*(?`, + req: { + ruleId: { + type: 'string', + required: true, + }, + }, + }, + needBody: true, + method: 'PUT', + }); + securityAnalytics[METHOD_NAMES.DELETE_CORRELATION_RULE] = createAction({ url: { fmt: `${API.CORRELATION_BASE}/<%=ruleId%>`, diff --git a/server/routes/CorrelationRoutes.ts b/server/routes/CorrelationRoutes.ts index 4f170df72..98a442fd6 100644 --- a/server/routes/CorrelationRoutes.ts +++ b/server/routes/CorrelationRoutes.ts @@ -31,6 +31,19 @@ export function setupCorrelationRoutes(services: NodeServices, router: IRouter) correlationService.createCorrelationRule ); + router.put( + { + path: `${API.CORRELATION_BASE}/{ruleId}`, + validate: { + body: schema.any(), + params: schema.object({ + ruleId: schema.string(), + }), + }, + }, + correlationService.updateCorrelationRule + ); + router.get( { path: `${API.FINDINGS_BASE}/correlate`, diff --git a/server/services/CorrelationService.ts b/server/services/CorrelationService.ts index eff26cb65..5dc2e150a 100644 --- a/server/services/CorrelationService.ts +++ b/server/services/CorrelationService.ts @@ -58,6 +58,38 @@ export default class CorrelationService { } }; + updateCorrelationRule = async ( + _context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory + ) => { + try { + const { ruleId } = request.params as { ruleId: string }; + const params: any = { body: request.body, ruleId }; + const { callAsCurrentUser: callWithRequest } = this.osDriver.asScoped(request); + const createRulesResponse = await callWithRequest( + CLIENT_CORRELATION_METHODS.UPDATE_CORRELATION_RULE, + params + ); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: createRulesResponse, + }, + }); + } catch (error: any) { + console.error('Security Analytics - CorrelationService - updateCorrelationRule:', error); + return response.custom({ + statusCode: 200, + body: { + ok: false, + error: error.message, + }, + }); + } + }; + /** * Calls backend GET correlation rules API. * URL /correlation/rules/_search diff --git a/server/utils/constants.ts b/server/utils/constants.ts index d22b0185c..9275dae9d 100644 --- a/server/utils/constants.ts +++ b/server/utils/constants.ts @@ -57,6 +57,7 @@ export const METHOD_NAMES = { // Correlation methods GET_CORRELATION_RULES: 'getCorrelationRules', CREATE_CORRELATION_RULE: 'createCorrelationRule', + UPDATE_CORRELATION_RULE: 'updateCorrelationRule', DELETE_CORRELATION_RULE: 'deleteCorrelationRule', GET_CORRELATED_FINDINGS: 'getCorrelatedFindings', GET_ALL_CORRELATIONS: 'getAllCorrelations', @@ -103,6 +104,7 @@ export const CLIENT_DETECTOR_METHODS = { export const CLIENT_CORRELATION_METHODS = { GET_CORRELATION_RULES: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_CORRELATION_RULES}`, CREATE_CORRELATION_RULE: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.CREATE_CORRELATION_RULE}`, + UPDATE_CORRELATION_RULE: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.UPDATE_CORRELATION_RULE}`, DELETE_CORRELATION_RULE: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.DELETE_CORRELATION_RULE}`, GET_CORRELATED_FINDINGS: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_CORRELATED_FINDINGS}`, GET_ALL_CORRELATIONS: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_ALL_CORRELATIONS}`, diff --git a/types/Correlations.ts b/types/Correlations.ts index 35481ddcc..4fea7d03e 100644 --- a/types/Correlations.ts +++ b/types/Correlations.ts @@ -107,6 +107,8 @@ export interface CreateCorrelationRuleResponse { _version: number; } +export interface UpdateCorrelationRuleResponse extends CreateCorrelationRuleResponse {} + export interface DeleteCorrelationRuleResponse {} export interface ICorrelationsStore { @@ -117,6 +119,7 @@ export interface ICorrelationsStore { nearby_findings?: number ): Promise<{ finding: CorrelationFinding; correlatedFindings: CorrelationFinding[] }>; createCorrelationRule(correlationRule: CorrelationRule): void; + updateCorrelationRule(correlationRule: CorrelationRule): void; deleteCorrelationRule(ruleId: string): Promise; getAllCorrelationsInWindow( start_time: string,