diff --git a/public/components/ContentPanel/ContentPanel.tsx b/public/components/ContentPanel/ContentPanel.tsx
index 57871bc65..c92ca6acc 100644
--- a/public/components/ContentPanel/ContentPanel.tsx
+++ b/public/components/ContentPanel/ContentPanel.tsx
@@ -38,7 +38,7 @@ const renderSubTitleText = (subTitleText: string | JSX.Element): JSX.Element | n
return subTitleText;
};
-const ContentPanel: React.SFC = ({
+const ContentPanel = ({
title = '',
titleSize = 'm',
subTitleText = '',
@@ -48,7 +48,7 @@ const ContentPanel: React.SFC = ({
children,
hideHeaderBorder = false,
className = '',
-}) => (
+}: ContentPanelProps): JSX.Element => (
= ({
+export const FormFieldHeader = ({
headerTitle = '',
optionalField = false,
toolTipIconType = 'questionInCircle',
toolTipPosition = 'top',
toolTipText = '',
-}) => {
+}: FormFieldHeaderProps): JSX.Element => {
return (
{headerTitle}
diff --git a/public/pages/Correlations/containers/CorrelationRuleFormModel.ts b/public/pages/Correlations/containers/CorrelationRuleFormModel.ts
index f244ae4c2..6bad7ef45 100644
--- a/public/pages/Correlations/containers/CorrelationRuleFormModel.ts
+++ b/public/pages/Correlations/containers/CorrelationRuleFormModel.ts
@@ -7,6 +7,7 @@ import { CorrelationRuleModel } from '../../../../types';
export const correlationRuleStateDefaultValue: CorrelationRuleModel = {
name: '',
+ time_window: 60000,
queries: [
{
logType: '',
@@ -18,6 +19,7 @@ export const correlationRuleStateDefaultValue: CorrelationRuleModel = {
},
],
index: '',
+ field: '',
},
{
logType: '',
@@ -29,6 +31,7 @@ export const correlationRuleStateDefaultValue: CorrelationRuleModel = {
},
],
index: '',
+ field: '',
},
],
};
diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx
index eaa6dd592..28423331f 100644
--- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx
+++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx
@@ -25,10 +25,15 @@ import {
EuiButtonIcon,
EuiToolTip,
EuiButtonGroup,
- EuiHorizontalRule,
+ EuiSelect,
+ EuiSelectOption,
+ EuiFieldNumber,
+ EuiCheckableCard,
+ htmlIdGenerator,
} from '@elastic/eui';
import { ruleTypes } from '../../Rules/utils/constants';
import {
+ CorrelationRule,
CorrelationRuleAction,
CorrelationRuleModel,
CorrelationRuleQuery,
@@ -56,6 +61,28 @@ export interface CorrelationOptions {
value: string;
}
+const parseTime = (time: number) => {
+ const minutes = Math.floor(time / 60000);
+ const hours = Math.floor(minutes / 60);
+
+ if (hours > 0 && minutes % 60 === 0) {
+ return {
+ interval: hours,
+ unit: 'HOURS',
+ };
+ } else {
+ return {
+ interval: minutes,
+ unit: 'MINUTES',
+ };
+ }
+};
+
+const unitOptions: EuiSelectOption[] = [
+ { value: 'MINUTES', text: 'Minutes' },
+ { value: 'HOURS', text: 'Hours' },
+];
+
export const CreateCorrelationRule: React.FC = (
props: CreateCorrelationRuleProps
) => {
@@ -64,48 +91,81 @@ export const CreateCorrelationRule: React.FC = (
const [logFieldsByIndex, setLogFieldsByIndex] = useState<{
[index: string]: CorrelationOptions[];
}>({});
- const validateCorrelationRule = useCallback((rule: CorrelationRuleModel) => {
- if (!rule.name) {
- return 'Invalid rule name';
- }
-
- let error = '';
- const invalidQuery = rule.queries.some((query, index) => {
- const invalidIndex = !query.index;
- if (invalidIndex) {
- error = `Invalid index for query ${index + 1}`;
- return true;
+ const params = useParams<{ ruleId: string }>();
+ const [initialValues, setInitialValues] = useState({
+ ...correlationRuleStateDefaultValue,
+ });
+ const [action, setAction] = useState('Create');
+ const [logTypeOptions, setLogTypeOptions] = useState([]);
+ const [period, setPeriod] = useState({ interval: 1, unit: 'MINUTES' });
+ const [dataFilterEnabled, setDataFilterEnabled] = useState(false);
+ const [groupByEnabled, setGroupByEnabled] = useState(false);
+
+ const validateCorrelationRule = useCallback(
+ (rule: CorrelationRuleModel) => {
+ if (!rule.name) {
+ return 'Invalid rule name';
}
- const invalidlogType = !query.logType;
- if (invalidlogType) {
- error = `Invalid log type for query ${index + 1}`;
- return true;
+ if (
+ Number.isNaN(rule.time_window) ||
+ rule.time_window > 86400000 ||
+ rule.time_window < 60000
+ ) {
+ return 'Invalid time window.';
}
- return query.conditions.some((cond) => {
- const invalid = !cond.name || !cond.value;
- if (invalid) {
- error = `Invalid fields for query ${index + 1}`;
+ let error = '';
+ const invalidQuery = rule.queries.some((query, index) => {
+ const invalidIndex = !query.index;
+ if (invalidIndex) {
+ error = `Invalid index for query ${index + 1}.`;
+ return true;
+ }
+
+ const invalidlogType = !query.logType;
+ if (invalidlogType) {
+ error = `Invalid log type for query ${index + 1}`;
+ return true;
+ }
+
+ if (!dataFilterEnabled && !groupByEnabled) {
+ error = 'Select at least one query type';
+ return true;
+ }
+
+ const invalidDataFilter =
+ dataFilterEnabled &&
+ query.conditions.some((cond) => {
+ const invalid = !cond.name || !cond.value;
+ if (invalid) {
+ error = `Invalid fields for query ${index + 1}`;
+ return true;
+ }
+
+ return false;
+ });
+
+ if (invalidDataFilter) {
+ return true;
+ }
+
+ if (groupByEnabled && rule.queries.some((q) => !q.field)) {
+ error = 'Select valid field for group by';
return true;
}
return false;
});
- });
- if (invalidQuery) {
- return error;
- }
+ if (invalidQuery) {
+ return error;
+ }
- return undefined;
- }, []);
- const params = useParams<{ ruleId: string }>();
- const [initialValues, setInitialValues] = useState({
- ...correlationRuleStateDefaultValue,
- });
- const [action, setAction] = useState('Create');
- const [logTypeOptions, setLogTypeOptions] = useState([]);
+ return undefined;
+ },
+ [dataFilterEnabled, groupByEnabled]
+ );
useEffect(() => {
if (props.history.location.state?.rule) {
@@ -131,22 +191,38 @@ export const CreateCorrelationRule: React.FC = (
}, []);
useEffect(() => {
+ setPeriod(parseTime(initialValues.time_window));
+ setGroupByEnabled(initialValues.queries.some((q) => !!q.field));
+ setDataFilterEnabled(initialValues.queries.some((q) => q.conditions.length > 0));
+
initialValues.queries.forEach(({ index }) => {
updateLogFieldsForIndex(index);
});
}, [initialValues]);
- const submit = async (values: any) => {
+ const submit = async (values: CorrelationRuleModel) => {
let error;
if ((error = validateCorrelationRule(values))) {
errorNotificationToast(props.notifications, action, 'rule', error);
return;
}
+ if (!dataFilterEnabled) {
+ values.queries.forEach((query) => {
+ query.conditions = [];
+ });
+ }
+
+ if (!groupByEnabled) {
+ values.queries.forEach((query) => {
+ query.field = '';
+ });
+ }
+
if (action === 'Edit') {
- await correlationStore.updateCorrelationRule(values);
+ await correlationStore.updateCorrelationRule(values as CorrelationRule);
} else {
- await correlationStore.createCorrelationRule(values);
+ await correlationStore.createCorrelationRule(values as CorrelationRule);
}
props.history.push(ROUTES.CORRELATION_RULES);
@@ -222,9 +298,70 @@ export const CreateCorrelationRule: React.FC = (
) => {
return (
<>
+
+ Query type
+
+
+
+
+
+
+ Data filter
+
+
+
+ A correlation will be created for the matching findings narrowed down with
+ data filter.
+
+
+ >
+ }
+ checkableType="checkbox"
+ checked={dataFilterEnabled}
+ onChange={() => {
+ setDataFilterEnabled(!dataFilterEnabled);
+ }}
+ />
+
+
+
+
+ Group by field values
+
+
+
+ A correlation will be created when the values for the field values for each
+ data source match between the findings.
+
+
+ >
+ }
+ checkableType="checkbox"
+ checked={groupByEnabled}
+ onChange={() => {
+ setGroupByEnabled(!groupByEnabled);
+ }}
+ />
+
+
+ {!dataFilterEnabled && !groupByEnabled && (
+
+ Select at least one query type
+
+ )}
+
+
{correlationQueries.map((query, queryIdx) => {
const fieldOptions = logFieldsByIndex[query.index] || [];
- const isInvalidInputForQuery = (field: 'logType' | 'index'): boolean => {
+ const isInvalidInputForQuery = (field: 'logType' | 'index' | 'field'): boolean => {
return (
!!touchedInputs.queries?.[queryIdx]?.[field] &&
!!(formikErrors.queries?.[queryIdx] as FormikErrors)?.[field]
@@ -238,7 +375,7 @@ export const CreateCorrelationRule: React.FC = (
id={`query-${queryIdx}`}
buttonContent={
- Query {queryIdx + 1}
+ Data source {queryIdx + 1}
}
extraAction={
@@ -334,107 +471,95 @@ export const CreateCorrelationRule: React.FC = (
}}
/>
-
-
- Fields
-
-
- {query.conditions.map((condition, conditionIdx) => {
- const fieldNameInput = (
- {
- props.handleChange(
- `queries[${queryIdx}].conditions[${conditionIdx}].name`
- )(e[0]?.value ? e[0].value : '');
- }}
- onBlur={props.handleBlur(
- `queries[${queryIdx}].conditions[${conditionIdx}].name`
- )}
- selectedOptions={
- condition.name ? [{ value: condition.name, label: condition.name }] : []
- }
- onCreateOption={(e) => {
- props.handleChange(
- `queries[${queryIdx}].conditions[${conditionIdx}].name`
- )(e);
- }}
- isClearable={true}
- />
- );
+ {!dataFilterEnabled && !groupByEnabled && (
+ <>
+
+
+ Select at least one query type
+
+ >
+ )}
+ {dataFilterEnabled && (
+ <>
+
+
+ Data filter
+
+
+ {query.conditions.map((condition, conditionIdx) => {
+ const fieldNameInput = (
+ {
+ props.handleChange(
+ `queries[${queryIdx}].conditions[${conditionIdx}].name`
+ )(e[0]?.value ? e[0].value : '');
+ }}
+ onBlur={props.handleBlur(
+ `queries[${queryIdx}].conditions[${conditionIdx}].name`
+ )}
+ selectedOptions={
+ condition.name
+ ? [{ value: condition.name, label: condition.name }]
+ : []
+ }
+ onCreateOption={(e) => {
+ props.handleChange(
+ `queries[${queryIdx}].conditions[${conditionIdx}].name`
+ )(e);
+ }}
+ isClearable={true}
+ />
+ );
- const fieldValueInput = (
- {
- props.handleChange(
- `queries[${queryIdx}].conditions[${conditionIdx}].value`
- )(e);
- }}
- onBlur={props.handleBlur(
- `queries[${queryIdx}].conditions[${conditionIdx}].value`
- )}
- value={condition.value}
- />
- );
-
- const conditionToggleButtons = [
- { id: 'AND', label: 'AND' },
- // { id: 'OR', label: 'OR' },
- ];
- const conditionButtonGroup = (
- {
- props.handleChange(
- `queries[${queryIdx}].conditions[${conditionIdx}].condition`
- )(e);
- }}
- className={'correlation_rule_field_condition'}
- />
- );
-
- const firstFieldRow = (
-
-
- Field}>
- {fieldNameInput}
-
-
-
- Field value}>
- {fieldValueInput}
-
-
-
- );
-
- const fieldRowWithCondition = (
-
-
- {conditionButtonGroup}
-
- {firstFieldRow}
-
- );
-
- return (
- <>
- 1 ? (
-
+ const fieldValueInput = (
+ {
+ props.handleChange(
+ `queries[${queryIdx}].conditions[${conditionIdx}].value`
+ )(e);
+ }}
+ onBlur={props.handleBlur(
+ `queries[${queryIdx}].conditions[${conditionIdx}].value`
+ )}
+ value={condition.value}
+ />
+ );
+
+ const conditionToggleButtons = [{ id: 'AND', label: 'AND' }];
+ const conditionButtonGroup = (
+ {
+ props.handleChange(
+ `queries[${queryIdx}].conditions[${conditionIdx}].condition`
+ )(e);
+ }}
+ className={'correlation_rule_field_condition'}
+ />
+ );
+
+ const firstFieldRow = (
+
+
+ Field}>
+ {fieldNameInput}
+
+
+
+ Field value}>
+ {fieldValueInput}
+
+
+
+ _
}>
= (
);
}}
/>
-
- ) : null
+
+
+
+ );
+
+ const fieldRowWithCondition = (
+
+
+ {conditionButtonGroup}
+
+ {firstFieldRow}
+
+ );
+
+ return (
+ <>
+
+ {conditionIdx === 0 ? firstFieldRow : fieldRowWithCondition}
+
+ >
+ );
+ })}
+ {
+ props.setFieldValue(`queries[${queryIdx}].conditions`, [
+ ...query.conditions,
+ ...correlationRuleStateDefaultValue.queries[0].conditions,
+ ]);
+ }}
+ iconType={'plusInCircle'}
+ >
+ Add field
+
+ >
+ )}
+
+ {groupByEnabled && (
+ <>
+
+
+ Group by field
+
+
+ Field}
+ isInvalid={isInvalidInputForQuery('field')}
+ error={
+ (formikErrors.queries?.[queryIdx] as FormikErrors)
+ ?.field
+ }
+ >
+ {
+ props.handleChange(`queries[${queryIdx}].field`)(
+ e[0]?.value ? e[0].value : ''
+ );
+ }}
+ onBlur={props.handleBlur(`queries[${queryIdx}].field`)}
+ selectedOptions={
+ query.field ? [{ value: query.field, label: query.field }] : []
}
- style={{ maxWidth: '500px' }}
- >
-
- {conditionIdx === 0 ? firstFieldRow : fieldRowWithCondition}
-
-
-
- >
- );
- })}
- {
- props.setFieldValue(`queries[${queryIdx}].conditions`, [
- ...query.conditions,
- ...correlationRuleStateDefaultValue.queries[0].conditions,
- ]);
- }}
- iconType={'plusInCircle'}
- >
- Add field
-
+ onCreateOption={(e) => {
+ props.handleChange(`queries[${queryIdx}].field`)(e);
+ }}
+ isClearable={true}
+ />
+
+ >
+ )}
@@ -527,6 +701,24 @@ export const CreateCorrelationRule: React.FC = (
}
}
+ if (
+ Number.isNaN(values.time_window) ||
+ values.time_window > 86400000 ||
+ values.time_window < 60000
+ ) {
+ errors.time_window = 'Invalid time window.';
+ }
+
+ values.queries.forEach((query, idx) => {
+ if (!query.field) {
+ if (!errors.queries) {
+ errors.queries = Array(values.queries.length).fill(null);
+ }
+
+ (errors.queries as Array<{ field: string }>)[idx] = { field: 'Field is required.' };
+ }
+ });
+
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
@@ -535,7 +727,7 @@ export const CreateCorrelationRule: React.FC = (
}}
enableReinitialize={true}
>
- {({ values: { name, queries }, touched, errors, ...props }) => {
+ {({ values: { name, queries, time_window }, touched, errors, ...props }) => {
return (