From 3fb90214d679a8e7f50dbbdb170b95ce2fcbbf81 Mon Sep 17 00:00:00 2001
From: Amardeepsingh Siglani
Date: Wed, 14 Feb 2024 20:47:49 +0530
Subject: [PATCH] Add missing backports 2.x (#887)
* Make notifications optional (#796)
* Make notifications optional 2.11 (#770)
* Threat intel feed support for detector creation (#762)
* added threat intel feed support for detector creation
Signed-off-by: Amardeepsingh Siglani
* updated cypress workflow file
Signed-off-by: Amardeepsingh Siglani
* updated alerts; findings UX
Signed-off-by: Amardeepsingh Siglani
* refactored alert condition panel; update detector for intel feeds
Signed-off-by: Amardeepsingh Siglani
* updated snapshots, mocks
Signed-off-by: Amardeepsingh Siglani
* updated workflow
Signed-off-by: Amardeepsingh Siglani
* updated tests
Signed-off-by: Amardeepsingh Siglani
* updated snapshot
Signed-off-by: Amardeepsingh Siglani
* updated UI; tests
Signed-off-by: Amardeepsingh Siglani
---------
Signed-off-by: Amardeepsingh Siglani
* fixed tests; make notification optional
Signed-off-by: Amardeepsingh Siglani
---------
Signed-off-by: Amardeepsingh Siglani
* revert notification change
Signed-off-by: Amardeepsingh Siglani
---------
Signed-off-by: Amardeepsingh Siglani
* supporting array of key/value under selection (#803)
Signed-off-by: Amardeepsingh Siglani
* Correlation rules field matching and time window support (#804)
* added ux for group by field and time window
Signed-off-by: Amardeepsingh Siglani
* improved validation
Signed-off-by: Amardeepsingh Siglani
* check for null values
Signed-off-by: Amardeepsingh Siglani
* addressed PR comments
Signed-off-by: Amardeepsingh Siglani
---------
Signed-off-by: Amardeepsingh Siglani
* validate fields only when group by enabled; update default window to 5 min (#810)
Signed-off-by: Amardeepsingh Siglani
* do not send fields if empty (#817)
Signed-off-by: Amardeepsingh Siglani
* Allow empty field name for supporting timeframe and keywords when validating rule during creation/update (#823)
* allow empty field name for supporting timeframe and keywords
Signed-off-by: Amardeepsingh Siglani
* fixed cypress test
Signed-off-by: Amardeepsingh Siglani
---------
Signed-off-by: Amardeepsingh Siglani
* fixed update payload (#838)
Signed-off-by: Amardeepsingh Siglani
---------
Signed-off-by: Amardeepsingh Siglani
---
cypress/integration/1_detectors.spec.js | 291 ++++-----
cypress/integration/2_rules.spec.js | 12 +-
.../components/ContentPanel/ContentPanel.tsx | 4 +-
.../FormFieldHeader/FormFieldHeader.tsx | 4 +-
.../containers/CorrelationRuleFormModel.ts | 3 +
.../containers/CreateCorrelationRule.tsx | 571 +++++++++++++-----
.../AlertCondition/AlertConditionPanel.tsx | 15 +-
.../AlertConditionPanel.test.tsx.snap | 2 -
.../containers/ConfigureAlerts.tsx | 4 +-
.../RuleEditor/DetectionVisualEditor.tsx | 51 +-
public/store/CorrelationsStore.ts | 59 +-
types/Correlations.ts | 4 +
12 files changed, 650 insertions(+), 370 deletions(-)
diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index 25a4a4c99..bbf1ecf8d 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -16,7 +16,6 @@ const cypressIndexDns = 'cypress-index-dns';
const cypressIndexWindows = 'cypress-index-windows';
const detectorName = 'test detector';
const cypressLogTypeDns = 'dns';
-const sampleNotificationChannel = 'sample_chime_channel';
const creationFailedMessage = 'Create detector failed.';
const cypressDNSRule = dns_name_rule_data.title;
@@ -43,10 +42,6 @@ const logTypeLabel = 'Log type';
const getLogTypeField = () => cy.getFieldByLabel(logTypeLabel);
-const notificationLabel = 'Notification channel';
-
-const getNotificationField = () => cy.getFieldByLabel(notificationLabel);
-
const openDetectorDetails = (detectorName) => {
cy.getInputByPlaceholder('Search threat detectors').type(`${detectorName}`).pressEnterKey();
cy.getElementByText('.euiTableCellContent button', detectorName).click();
@@ -162,8 +157,6 @@ const createDetector = (detectorName, dataSource, expectFailure) => {
.focus()
.blur();
- getNotificationField().selectComboboxItem(`[Channel] ${sampleNotificationChannel}`);
-
cy.intercept('POST', '/_plugins/_security_analytics/mappings').as('createMappingsRequest');
cy.intercept('POST', '/_plugins/_security_analytics/detectors').as('createDetectorRequest');
@@ -227,155 +220,141 @@ describe('Detectors', () => {
cy.createRule(dns_name_rule_data);
cy.createRule(dns_type_rule_data);
-
- cy.request('POST', 'http://localhost:9200/_plugins/_notifications/configs/', {
- config_id: 'sa_notification-channel_id',
- name: sampleNotificationChannel,
- config: {
- name: sampleNotificationChannel,
- description: 'This is a sample chime channel',
- config_type: 'chime',
- is_enabled: true,
- chime: {
- url: 'https://sample-chime-webhook',
- },
- },
- }).should('have.property', 'status', 200);
});
- // describe('...should validate form fields', () => {
- // beforeEach(() => {
- // cy.intercept('/_plugins/_security_analytics/detectors/_search').as('detectorsSearch');
-
- // // Visit Detectors page before any test
- // cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`);
- // cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete');
-
- // openCreateForm();
- // });
-
- // it('...should validate name field', () => {
- // getNameField().should('be.empty');
- // getNameField().focus().blur();
- // getNameField().parentsUntil('.euiFormRow__fieldWrapper').siblings().contains('Enter a name.');
-
- // getNameField().type('text').focus().blur();
-
- // getNameField()
- // .parents('.euiFormRow__fieldWrapper')
- // .find('.euiFormErrorText')
- // .contains(
- // 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.'
- // );
-
- // getNameField().type('{selectall}').type('{backspace}').type('tex&').focus().blur();
-
- // getNameField()
- // .parents('.euiFormRow__fieldWrapper')
- // .find('.euiFormErrorText')
- // .contains(
- // 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.'
- // );
-
- // getNameField()
- // .type('{selectall}')
- // .type('{backspace}')
- // .type('Detector name')
- // .focus()
- // .blur()
- // .parents('.euiFormRow__fieldWrapper')
- // .find('.euiFormErrorText')
- // .should('not.exist');
- // });
-
- // it('...should validate description field', () => {
- // const longDescriptionText =
- // 'This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.';
-
- // getDescriptionField().should('be.empty');
-
- // getDescriptionField().type(longDescriptionText).focus().blur();
-
- // getDescriptionField()
- // .parents('.euiFormRow__fieldWrapper')
- // .find('.euiFormErrorText')
- // .contains(
- // 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.'
- // );
-
- // getDescriptionField()
- // .type('{selectall}')
- // .type('{backspace}')
- // .type('Detector description...')
- // .focus()
- // .blur();
-
- // getDescriptionField()
- // .type('{selectall}')
- // .type('{backspace}')
- // .type('Detector name')
- // .focus()
- // .blur()
- // .parents('.euiFormRow__fieldWrapper')
- // .find('.euiFormErrorText')
- // .should('not.exist');
- // });
-
- // it('...should validate data source field', () => {
- // getDataSourceField()
- // .focus()
- // .blur()
- // .parentsUntil('.euiFormRow__fieldWrapper')
- // .siblings()
- // .contains('Select an input source.');
-
- // getDataSourceField().selectComboboxItem(cypressIndexDns);
- // getDataSourceField()
- // .focus()
- // .blur()
- // .parentsUntil('.euiFormRow__fieldWrapper')
- // .find('.euiFormErrorText')
- // .should('not.exist');
- // });
-
- // it('...should validate next button', () => {
- // getNextButton().should('be.disabled');
-
- // fillDetailsForm(detectorName, cypressIndexDns);
- // getNextButton().should('be.enabled');
- // });
-
- // it('...should validate alerts page', () => {
- // fillDetailsForm(detectorName, cypressIndexDns);
- // getNextButton().click({ force: true });
- // // Open the trigger details accordion
- // cy.get('[data-test-subj="trigger-details-btn"]').click({ force: true });
- // getTriggerNameField().should('have.value', 'Trigger 1');
- // getTriggerNameField()
- // .parents('.euiFormRow__fieldWrapper')
- // .find('.euiFormErrorText')
- // .should('not.exist');
-
- // getTriggerNameField().type('{selectall}').type('{backspace}').focus().blur();
- // getCreateDetectorButton().should('be.disabled');
-
- // cy.getButtonByText('Remove').click({ force: true });
- // getCreateDetectorButton().should('be.enabled');
- // });
-
- // it('...should show mappings warning', () => {
- // fillDetailsForm(detectorName, cypressIndexDns);
-
- // getDataSourceField().selectComboboxItem(cypressIndexWindows);
- // getDataSourceField().focus().blur();
-
- // cy.get('[data-test-subj="define-detector-diff-log-types-warning"]')
- // .should('be.visible')
- // .contains(
- // 'To avoid issues with field mappings, we recommend creating separate detectors for different log types.'
- // );
- // });
- // });
+ describe('...should validate form fields', () => {
+ beforeEach(() => {
+ cy.intercept('/_plugins/_security_analytics/detectors/_search').as('detectorsSearch');
+
+ // Visit Detectors page before any test
+ cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`);
+ cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete');
+
+ openCreateForm();
+ });
+
+ it('...should validate name field', () => {
+ getNameField().should('be.empty');
+ getNameField().focus().blur();
+ getNameField().parentsUntil('.euiFormRow__fieldWrapper').siblings().contains('Enter a name.');
+
+ getNameField().type('text').focus().blur();
+
+ getNameField()
+ .parents('.euiFormRow__fieldWrapper')
+ .find('.euiFormErrorText')
+ .contains(
+ 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.'
+ );
+
+ getNameField().type('{selectall}').type('{backspace}').type('tex&').focus().blur();
+
+ getNameField()
+ .parents('.euiFormRow__fieldWrapper')
+ .find('.euiFormErrorText')
+ .contains(
+ 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.'
+ );
+
+ getNameField()
+ .type('{selectall}')
+ .type('{backspace}')
+ .type('Detector name')
+ .focus()
+ .blur()
+ .parents('.euiFormRow__fieldWrapper')
+ .find('.euiFormErrorText')
+ .should('not.exist');
+ });
+
+ it('...should validate description field', () => {
+ const longDescriptionText =
+ 'This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.';
+
+ getDescriptionField().should('be.empty');
+
+ getDescriptionField().type(longDescriptionText).focus().blur();
+
+ getDescriptionField()
+ .parents('.euiFormRow__fieldWrapper')
+ .find('.euiFormErrorText')
+ .contains(
+ 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.'
+ );
+
+ getDescriptionField()
+ .type('{selectall}')
+ .type('{backspace}')
+ .type('Detector description...')
+ .focus()
+ .blur();
+
+ getDescriptionField()
+ .type('{selectall}')
+ .type('{backspace}')
+ .type('Detector name')
+ .focus()
+ .blur()
+ .parents('.euiFormRow__fieldWrapper')
+ .find('.euiFormErrorText')
+ .should('not.exist');
+ });
+
+ it('...should validate data source field', () => {
+ getDataSourceField()
+ .focus()
+ .blur()
+ .parentsUntil('.euiFormRow__fieldWrapper')
+ .siblings()
+ .contains('Select an input source.');
+
+ getDataSourceField().selectComboboxItem(cypressIndexDns);
+ getDataSourceField()
+ .focus()
+ .blur()
+ .parentsUntil('.euiFormRow__fieldWrapper')
+ .find('.euiFormErrorText')
+ .should('not.exist');
+ });
+
+ it('...should validate next button', () => {
+ getNextButton().should('be.disabled');
+
+ fillDetailsForm(detectorName, cypressIndexDns);
+ getNextButton().should('be.enabled');
+ });
+
+ it('...should validate alerts page', () => {
+ fillDetailsForm(detectorName, cypressIndexDns);
+ getNextButton().click({ force: true });
+ // Open the trigger details accordion
+ cy.get('[data-test-subj="trigger-details-btn"]').click({ force: true });
+ getTriggerNameField().should('have.value', 'Trigger 1');
+ getTriggerNameField()
+ .parents('.euiFormRow__fieldWrapper')
+ .find('.euiFormErrorText')
+ .should('not.exist');
+
+ getTriggerNameField().type('{selectall}').type('{backspace}').focus().blur();
+ getCreateDetectorButton().should('be.disabled');
+
+ cy.getButtonByText('Remove').click({ force: true });
+ getCreateDetectorButton().should('be.enabled');
+ });
+
+ it('...should show mappings warning', () => {
+ fillDetailsForm(detectorName, cypressIndexDns);
+
+ getDataSourceField().selectComboboxItem(cypressIndexWindows);
+ getDataSourceField().focus().blur();
+
+ cy.get('[data-test-subj="define-detector-diff-log-types-warning"]')
+ .should('be.visible')
+ .contains(
+ 'To avoid issues with field mappings, we recommend creating separate detectors for different log types.'
+ );
+ });
+ });
describe('...validate create detector flow', () => {
beforeEach(() => {
@@ -521,9 +500,5 @@ describe('Detectors', () => {
after(() => {
cy.cleanUpTests();
- cy.request(
- 'DELETE',
- 'http://localhost:9200/_plugins/_notifications/configs/sa_notification-channel_id'
- );
});
});
diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js
index 18b18f6ca..3e90ed212 100644
--- a/cypress/integration/2_rules.spec.js
+++ b/cypress/integration/2_rules.spec.js
@@ -330,13 +330,20 @@ describe('Rules', () => {
it('...should validate selection map key field', () => {
getSelectionPanelByIndex(0).within(() => {
getMapKeyField().should('be.empty');
- getMapKeyField().focus().blur();
+ getMapKeyField()
+ .focus()
+ .blur()
+ .parents('.euiFormRow__fieldWrapper')
+ .find('.euiFormErrorText')
+ .should('not.exist');
+
+ getMapKeyField().type('hello@');
getMapKeyField()
.parentsUntil('.euiFormRow__fieldWrapper')
.siblings()
.contains('Invalid key name');
- getMapKeyField().type('FieldKey');
+ getMapKeyField().focus().type('{selectall}').type('FieldKey');
getMapKeyField()
.focus()
.blur()
@@ -446,7 +453,6 @@ describe('Rules', () => {
getSelectionPanelByIndex(0).within(() =>
getMapKeyField().type('{selectall}').type('{backspace}')
);
- toastShouldExist();
getSelectionPanelByIndex(0).within(() => getMapKeyField().type('FieldKey'));
// selection map value field
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..775b07523 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: 300000,
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 221619d13..f6823a7dc 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,
@@ -58,6 +63,28 @@ export interface CorrelationOption {
options?: CorrelationOption[];
}
+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
) => {
@@ -66,48 +93,81 @@ export const CreateCorrelationRule: React.FC = (
const [logFieldsByIndex, setLogFieldsByIndex] = useState<{
[index: string]: CorrelationOption[];
}>({});
- 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) {
@@ -133,22 +193,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);
@@ -212,9 +288,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]
@@ -228,7 +365,7 @@ export const CreateCorrelationRule: React.FC = (
id={`query-${queryIdx}`}
buttonContent={
- Query {queryIdx + 1}
+ Data source {queryIdx + 1}
}
extraAction={
@@ -327,107 +464,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}
+ />
+
+ >
+ )}
@@ -520,6 +694,26 @@ 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.';
+ }
+
+ if (groupByEnabled) {
+ 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 }) => {
@@ -528,7 +722,7 @@ export const CreateCorrelationRule: React.FC = (
}}
enableReinitialize={true}
>
- {({ values: { name, queries }, touched, errors, ...props }) => {
+ {({ values: { name, queries, time_window }, touched, errors, ...props }) => {
return (