mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Security Solution] Adds basic test for rule data view (#136822)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
711d337820
commit
691e21ec2d
13 changed files with 386 additions and 211 deletions
|
@ -74,12 +74,6 @@ const DEFAULT_INDEX_PATTERNS = ['index-1-*', 'index-2-*'];
|
|||
const TAGS = ['cypress-tag-1', 'cypress-tag-2'];
|
||||
const OVERWRITE_INDEX_PATTERNS = ['overwrite-index-1-*', 'overwrite-index-2-*'];
|
||||
|
||||
const customRule = {
|
||||
...getNewRule(),
|
||||
index: DEFAULT_INDEX_PATTERNS,
|
||||
name: RULE_NAME,
|
||||
};
|
||||
|
||||
const expectedNumberOfCustomRulesToBeEdited = 6;
|
||||
const expectedNumberOfMachineLearningRulesToBeEdited = 1;
|
||||
const numberOfRulesPerPage = 5;
|
||||
|
@ -92,7 +86,14 @@ describe('Detection rules, bulk edit', () => {
|
|||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
esArchiverResetKibana();
|
||||
createCustomRule(customRule, '1');
|
||||
createCustomRule(
|
||||
{
|
||||
...getNewRule(),
|
||||
name: RULE_NAME,
|
||||
dataSource: { index: DEFAULT_INDEX_PATTERNS, type: 'indexPatterns' },
|
||||
},
|
||||
'1'
|
||||
);
|
||||
createCustomRule(getExistingRule(), '2');
|
||||
createCustomRule(getNewOverrideRule(), '3');
|
||||
createCustomRule(getNewThresholdRule(), '4');
|
||||
|
|
|
@ -295,10 +295,13 @@ describe('Custom query rules', () => {
|
|||
});
|
||||
|
||||
context('Edition', () => {
|
||||
const expectedEditedtags = getEditedRule().tags.join('');
|
||||
const rule = getEditedRule();
|
||||
const expectedEditedtags = rule.tags.join('');
|
||||
const expectedEditedIndexPatterns =
|
||||
getEditedRule().index && getEditedRule().index.length
|
||||
? getEditedRule().index
|
||||
rule.dataSource.type === 'indexPatterns' &&
|
||||
rule.dataSource.index &&
|
||||
rule.dataSource.index.length
|
||||
? rule.dataSource.index
|
||||
: getIndexPatterns();
|
||||
|
||||
before(() => {
|
||||
|
@ -325,27 +328,32 @@ describe('Custom query rules', () => {
|
|||
});
|
||||
|
||||
it('Allows a rule to be edited', () => {
|
||||
const existingRule = getExistingRule();
|
||||
|
||||
editFirstRule();
|
||||
|
||||
// expect define step to populate
|
||||
cy.get(CUSTOM_QUERY_INPUT).should('have.value', getExistingRule().customQuery);
|
||||
if (getExistingRule().index && getExistingRule().index.length > 0) {
|
||||
cy.get(DEFINE_INDEX_INPUT).should('have.text', getExistingRule().index.join(''));
|
||||
cy.get(CUSTOM_QUERY_INPUT).should('have.value', existingRule.customQuery);
|
||||
if (
|
||||
existingRule.dataSource.type === 'indexPatterns' &&
|
||||
existingRule.dataSource.index.length > 0
|
||||
) {
|
||||
cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.dataSource.index.join(''));
|
||||
}
|
||||
|
||||
goToAboutStepTab();
|
||||
|
||||
// expect about step to populate
|
||||
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', getExistingRule().name);
|
||||
cy.get(RULE_DESCRIPTION_INPUT).should('have.text', getExistingRule().description);
|
||||
cy.get(TAGS_FIELD).should('have.text', getExistingRule().tags.join(''));
|
||||
cy.get(SEVERITY_DROPDOWN).should('have.text', getExistingRule().severity);
|
||||
cy.get(DEFAULT_RISK_SCORE_INPUT).invoke('val').should('eql', getExistingRule().riskScore);
|
||||
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name);
|
||||
cy.get(RULE_DESCRIPTION_INPUT).should('have.text', existingRule.description);
|
||||
cy.get(TAGS_FIELD).should('have.text', existingRule.tags.join(''));
|
||||
cy.get(SEVERITY_DROPDOWN).should('have.text', existingRule.severity);
|
||||
cy.get(DEFAULT_RISK_SCORE_INPUT).invoke('val').should('eql', existingRule.riskScore);
|
||||
|
||||
goToScheduleStepTab();
|
||||
|
||||
// expect schedule step to populate
|
||||
const interval = getExistingRule().interval;
|
||||
const interval = existingRule.interval;
|
||||
const intervalParts = interval != null && interval.match(/[0-9]+|[a-zA-Z]+/g);
|
||||
if (intervalParts) {
|
||||
const [amount, unit] = intervalParts;
|
||||
|
@ -381,7 +389,7 @@ describe('Custom query rules', () => {
|
|||
cy.wait('@getRule').then(({ response }) => {
|
||||
cy.wrap(response?.statusCode).should('eql', 200);
|
||||
// ensure that editing rule does not modify max_signals
|
||||
cy.wrap(response?.body.max_signals).should('eql', getExistingRule().maxSignals);
|
||||
cy.wrap(response?.body.max_signals).should('eql', existingRule.maxSignals);
|
||||
});
|
||||
|
||||
cy.get(RULE_NAME_HEADER).should('contain', `${getEditedRule().name}`);
|
||||
|
@ -396,7 +404,7 @@ describe('Custom query rules', () => {
|
|||
cy.get(DEFINITION_DETAILS).within(() => {
|
||||
getDetails(INDEX_PATTERNS_DETAILS).should(
|
||||
'have.text',
|
||||
expectedEditedIndexPatterns.join('')
|
||||
expectedEditedIndexPatterns?.join('')
|
||||
);
|
||||
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', getEditedRule().customQuery);
|
||||
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { formatMitreAttackDescription } from '../../helpers/rules';
|
||||
import { getDataViewRule } from '../../objects/rule';
|
||||
import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts';
|
||||
|
||||
import {
|
||||
CUSTOM_RULES_BTN,
|
||||
RISK_SCORE,
|
||||
RULE_NAME,
|
||||
RULES_ROW,
|
||||
RULES_TABLE,
|
||||
RULE_SWITCH,
|
||||
SEVERITY,
|
||||
} from '../../screens/alerts_detection_rules';
|
||||
|
||||
import {
|
||||
ADDITIONAL_LOOK_BACK_DETAILS,
|
||||
ABOUT_DETAILS,
|
||||
ABOUT_INVESTIGATION_NOTES,
|
||||
ABOUT_RULE_DESCRIPTION,
|
||||
CUSTOM_QUERY_DETAILS,
|
||||
DEFINITION_DETAILS,
|
||||
FALSE_POSITIVES_DETAILS,
|
||||
removeExternalLinkText,
|
||||
INDEX_PATTERNS_DETAILS,
|
||||
INVESTIGATION_NOTES_MARKDOWN,
|
||||
INVESTIGATION_NOTES_TOGGLE,
|
||||
MITRE_ATTACK_DETAILS,
|
||||
REFERENCE_URLS_DETAILS,
|
||||
RISK_SCORE_DETAILS,
|
||||
RULE_NAME_HEADER,
|
||||
RULE_TYPE_DETAILS,
|
||||
RUNS_EVERY_DETAILS,
|
||||
SCHEDULE_DETAILS,
|
||||
SEVERITY_DETAILS,
|
||||
TAGS_DETAILS,
|
||||
TIMELINE_TEMPLATE_DETAILS,
|
||||
DATA_VIEW_DETAILS,
|
||||
} from '../../screens/rule_details';
|
||||
|
||||
import { goToRuleDetails } from '../../tasks/alerts_detection_rules';
|
||||
import { createTimeline } from '../../tasks/api_calls/timelines';
|
||||
import { postDataView } from '../../tasks/common';
|
||||
import {
|
||||
createAndEnableRule,
|
||||
fillAboutRuleAndContinue,
|
||||
fillDefineCustomRuleWithImportedQueryAndContinue,
|
||||
fillScheduleRuleAndContinue,
|
||||
waitForAlertsToPopulate,
|
||||
waitForTheRuleToBeExecuted,
|
||||
} from '../../tasks/create_new_rule';
|
||||
|
||||
import { esArchiverResetKibana } from '../../tasks/es_archiver';
|
||||
import { login, visit } from '../../tasks/login';
|
||||
import { getDetails } from '../../tasks/rule_details';
|
||||
|
||||
import { RULE_CREATION } from '../../urls/navigation';
|
||||
|
||||
describe('Custom query rules', () => {
|
||||
before(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
describe('Custom detection rules creation with data views', () => {
|
||||
const rule = getDataViewRule();
|
||||
const expectedUrls = rule.referenceUrls.join('');
|
||||
const expectedFalsePositives = rule.falsePositivesExamples.join('');
|
||||
const expectedTags = rule.tags.join('');
|
||||
const expectedMitre = formatMitreAttackDescription(rule.mitre);
|
||||
const expectedNumberOfRules = 1;
|
||||
|
||||
beforeEach(() => {
|
||||
/* We don't call cleanKibana method on the before hook, instead we call esArchiverReseKibana on the before each. This is because we
|
||||
are creating a data view we'll use after and cleanKibana does not delete all the data views created, esArchiverReseKibana does.
|
||||
We don't use esArchiverReseKibana in all the tests because is a time-consuming method and we don't need to perform an exhaustive
|
||||
cleaning in all the other tests. */
|
||||
esArchiverResetKibana();
|
||||
createTimeline(rule.timeline).then((response) => {
|
||||
cy.wrap({
|
||||
...rule,
|
||||
timeline: {
|
||||
...rule.timeline,
|
||||
id: response.body.data.persistTimeline.timeline.savedObjectId,
|
||||
},
|
||||
}).as('rule');
|
||||
});
|
||||
if (rule.dataSource.type === 'dataView') {
|
||||
postDataView(rule.dataSource.dataView);
|
||||
}
|
||||
});
|
||||
|
||||
it('Creates and enables a new rule', function () {
|
||||
visit(RULE_CREATION);
|
||||
fillDefineCustomRuleWithImportedQueryAndContinue(this.rule);
|
||||
fillAboutRuleAndContinue(this.rule);
|
||||
fillScheduleRuleAndContinue(this.rule);
|
||||
createAndEnableRule();
|
||||
|
||||
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
|
||||
|
||||
cy.get(RULES_TABLE).find(RULES_ROW).should('have.length', expectedNumberOfRules);
|
||||
cy.get(RULE_NAME).should('have.text', this.rule.name);
|
||||
cy.get(RISK_SCORE).should('have.text', this.rule.riskScore);
|
||||
cy.get(SEVERITY).should('have.text', this.rule.severity);
|
||||
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
goToRuleDetails();
|
||||
|
||||
cy.get(RULE_NAME_HEADER).should('contain', `${this.rule.name}`);
|
||||
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', this.rule.description);
|
||||
cy.get(ABOUT_DETAILS).within(() => {
|
||||
getDetails(SEVERITY_DETAILS).should('have.text', this.rule.severity);
|
||||
getDetails(RISK_SCORE_DETAILS).should('have.text', this.rule.riskScore);
|
||||
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
|
||||
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
|
||||
});
|
||||
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
|
||||
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
|
||||
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
|
||||
});
|
||||
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
|
||||
});
|
||||
cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
|
||||
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
|
||||
cy.get(DEFINITION_DETAILS).within(() => {
|
||||
getDetails(DATA_VIEW_DETAILS).should('have.text', this.rule.dataSource.dataView);
|
||||
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', this.rule.customQuery);
|
||||
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
|
||||
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||
});
|
||||
cy.get(DEFINITION_DETAILS).should('not.contain', INDEX_PATTERNS_DETAILS);
|
||||
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||
getDetails(RUNS_EVERY_DETAILS).should(
|
||||
'have.text',
|
||||
`${getDataViewRule().runsEvery.interval}${getDataViewRule().runsEvery.type}`
|
||||
);
|
||||
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
|
||||
'have.text',
|
||||
`${getDataViewRule().lookBack.interval}${getDataViewRule().lookBack.type}`
|
||||
);
|
||||
});
|
||||
|
||||
waitForTheRuleToBeExecuted();
|
||||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS)
|
||||
.invoke('text')
|
||||
.should('match', /^[1-9].+$/);
|
||||
cy.get(ALERT_GRID_CELL).contains(this.rule.name);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -205,12 +205,12 @@ describe('indicator match', () => {
|
|||
|
||||
describe('Indicator mapping', () => {
|
||||
beforeEach(() => {
|
||||
const rule = getNewThreatIndicatorRule();
|
||||
visitWithoutDateRange(RULE_CREATION);
|
||||
selectIndicatorMatchType();
|
||||
fillIndexAndIndicatorIndexPattern(
|
||||
getNewThreatIndicatorRule().index,
|
||||
getNewThreatIndicatorRule().indicatorIndexPattern
|
||||
);
|
||||
if (rule.dataSource.type === 'indexPatterns') {
|
||||
fillIndexAndIndicatorIndexPattern(rule.dataSource.index, rule.indicatorIndexPattern);
|
||||
}
|
||||
});
|
||||
|
||||
it('Does NOT show invalidation text on initial page load', () => {
|
||||
|
@ -419,11 +419,12 @@ describe('indicator match', () => {
|
|||
});
|
||||
|
||||
it('Creates and enables a new Indicator Match rule', () => {
|
||||
const rule = getNewThreatIndicatorRule();
|
||||
visitWithoutDateRange(RULE_CREATION);
|
||||
selectIndicatorMatchType();
|
||||
fillDefineIndicatorMatchRuleAndContinue(getNewThreatIndicatorRule());
|
||||
fillAboutRuleAndContinue(getNewThreatIndicatorRule());
|
||||
fillScheduleRuleAndContinue(getNewThreatIndicatorRule());
|
||||
fillDefineIndicatorMatchRuleAndContinue(rule);
|
||||
fillAboutRuleAndContinue(rule);
|
||||
fillScheduleRuleAndContinue(rule);
|
||||
createAndEnableRule();
|
||||
|
||||
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
|
||||
|
@ -432,22 +433,19 @@ describe('indicator match', () => {
|
|||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
|
||||
});
|
||||
|
||||
cy.get(RULE_NAME).should('have.text', getNewThreatIndicatorRule().name);
|
||||
cy.get(RISK_SCORE).should('have.text', getNewThreatIndicatorRule().riskScore);
|
||||
cy.get(SEVERITY).should('have.text', getNewThreatIndicatorRule().severity);
|
||||
cy.get(RULE_NAME).should('have.text', rule.name);
|
||||
cy.get(RISK_SCORE).should('have.text', rule.riskScore);
|
||||
cy.get(SEVERITY).should('have.text', rule.severity);
|
||||
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
goToRuleDetails();
|
||||
|
||||
cy.get(RULE_NAME_HEADER).should('contain', `${getNewThreatIndicatorRule().name}`);
|
||||
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', getNewThreatIndicatorRule().description);
|
||||
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
|
||||
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
|
||||
cy.get(ABOUT_DETAILS).within(() => {
|
||||
getDetails(SEVERITY_DETAILS).should('have.text', getNewThreatIndicatorRule().severity);
|
||||
getDetails(RISK_SCORE_DETAILS).should('have.text', getNewThreatIndicatorRule().riskScore);
|
||||
getDetails(INDICATOR_PREFIX_OVERRIDE).should(
|
||||
'have.text',
|
||||
getNewThreatIndicatorRule().threatIndicatorPath
|
||||
);
|
||||
getDetails(SEVERITY_DETAILS).should('have.text', rule.severity);
|
||||
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore);
|
||||
getDetails(INDICATOR_PREFIX_OVERRIDE).should('have.text', rule.threatIndicatorPath);
|
||||
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
|
||||
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
|
||||
});
|
||||
|
@ -461,22 +459,19 @@ describe('indicator match', () => {
|
|||
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
|
||||
|
||||
cy.get(DEFINITION_DETAILS).within(() => {
|
||||
getDetails(INDEX_PATTERNS_DETAILS).should(
|
||||
'have.text',
|
||||
getNewThreatIndicatorRule().index.join('')
|
||||
);
|
||||
if (rule.dataSource.type === 'indexPatterns') {
|
||||
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', rule.dataSource.index?.join(''));
|
||||
}
|
||||
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*');
|
||||
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match');
|
||||
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||
getDetails(INDICATOR_INDEX_PATTERNS).should(
|
||||
'have.text',
|
||||
getNewThreatIndicatorRule().indicatorIndexPattern.join('')
|
||||
rule.indicatorIndexPattern.join('')
|
||||
);
|
||||
getDetails(INDICATOR_MAPPING).should(
|
||||
'have.text',
|
||||
`${getNewThreatIndicatorRule().indicatorMappingField} MATCHES ${
|
||||
getNewThreatIndicatorRule().indicatorIndexField
|
||||
}`
|
||||
`${rule.indicatorMappingField} MATCHES ${rule.indicatorIndexField}`
|
||||
);
|
||||
getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*');
|
||||
});
|
||||
|
@ -484,15 +479,11 @@ describe('indicator match', () => {
|
|||
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||
getDetails(RUNS_EVERY_DETAILS).should(
|
||||
'have.text',
|
||||
`${getNewThreatIndicatorRule().runsEvery.interval}${
|
||||
getNewThreatIndicatorRule().runsEvery.type
|
||||
}`
|
||||
`${rule.runsEvery.interval}${rule.runsEvery.type}`
|
||||
);
|
||||
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
|
||||
'have.text',
|
||||
`${getNewThreatIndicatorRule().lookBack.interval}${
|
||||
getNewThreatIndicatorRule().lookBack.type
|
||||
}`
|
||||
`${rule.lookBack.interval}${rule.lookBack.type}`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -500,11 +491,9 @@ describe('indicator match', () => {
|
|||
waitForAlertsToPopulate();
|
||||
|
||||
cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
|
||||
cy.get(ALERT_RULE_NAME).first().should('have.text', getNewThreatIndicatorRule().name);
|
||||
cy.get(ALERT_SEVERITY)
|
||||
.first()
|
||||
.should('have.text', getNewThreatIndicatorRule().severity.toLowerCase());
|
||||
cy.get(ALERT_RISK_SCORE).first().should('have.text', getNewThreatIndicatorRule().riskScore);
|
||||
cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name);
|
||||
cy.get(ALERT_SEVERITY).first().should('have.text', rule.severity.toLowerCase());
|
||||
cy.get(ALERT_RISK_SCORE).first().should('have.text', rule.riskScore);
|
||||
});
|
||||
|
||||
it('Investigate alert in timeline', () => {
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
*/
|
||||
|
||||
import { formatMitreAttackDescription } from '../../helpers/rules';
|
||||
import type { ThresholdRule } from '../../objects/rule';
|
||||
import { getIndexPatterns, getNewRule, getNewThresholdRule } from '../../objects/rule';
|
||||
import { getIndexPatterns, getNewThresholdRule } from '../../objects/rule';
|
||||
|
||||
import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts';
|
||||
|
||||
|
@ -20,7 +19,6 @@ import {
|
|||
RULES_TABLE,
|
||||
SEVERITY,
|
||||
} from '../../screens/alerts_detection_rules';
|
||||
import { PREVIEW_HEADER_SUBTITLE } from '../../screens/create_new_rule';
|
||||
import {
|
||||
ABOUT_DETAILS,
|
||||
ABOUT_INVESTIGATION_NOTES,
|
||||
|
@ -47,22 +45,14 @@ import {
|
|||
} from '../../screens/rule_details';
|
||||
|
||||
import { getDetails } from '../../tasks/rule_details';
|
||||
import { goToManageAlertsDetectionRules } from '../../tasks/alerts';
|
||||
import {
|
||||
goToCreateNewRule,
|
||||
goToRuleDetails,
|
||||
waitForRulesTableToBeLoaded,
|
||||
} from '../../tasks/alerts_detection_rules';
|
||||
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
|
||||
import { goToRuleDetails } from '../../tasks/alerts_detection_rules';
|
||||
import { createTimeline } from '../../tasks/api_calls/timelines';
|
||||
import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common';
|
||||
import {
|
||||
createAndEnableRule,
|
||||
fillAboutRuleAndContinue,
|
||||
fillDefineThresholdRuleAndContinue,
|
||||
fillDefineThresholdRule,
|
||||
fillScheduleRuleAndContinue,
|
||||
previewResults,
|
||||
selectThresholdRuleType,
|
||||
waitForAlertsToPopulate,
|
||||
waitForTheRuleToBeExecuted,
|
||||
|
@ -156,37 +146,4 @@ describe('Detection rules, threshold', () => {
|
|||
cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text().split(' ')[0]).to.be.lt(100));
|
||||
cy.get(ALERT_GRID_CELL).contains(rule.name);
|
||||
});
|
||||
|
||||
it.skip('Preview results of keyword using "host.name"', () => {
|
||||
rule.index = [...rule.index, '.siem-signals*'];
|
||||
|
||||
createCustomRuleEnabled(getNewRule());
|
||||
goToManageAlertsDetectionRules();
|
||||
waitForRulesTableToBeLoaded();
|
||||
goToCreateNewRule();
|
||||
selectThresholdRuleType();
|
||||
fillDefineThresholdRule(rule);
|
||||
previewResults();
|
||||
|
||||
cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits');
|
||||
});
|
||||
|
||||
it.skip('Preview results of "ip" using "source.ip"', () => {
|
||||
const previewRule: ThresholdRule = {
|
||||
...rule,
|
||||
thresholdField: 'source.ip',
|
||||
threshold: '1',
|
||||
};
|
||||
previewRule.index = [...previewRule.index, '.siem-signals*'];
|
||||
|
||||
createCustomRuleEnabled(getNewRule());
|
||||
goToManageAlertsDetectionRules();
|
||||
waitForRulesTableToBeLoaded();
|
||||
goToCreateNewRule();
|
||||
selectThresholdRuleType();
|
||||
fillDefineThresholdRule(previewRule);
|
||||
previewResults();
|
||||
|
||||
cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '10 unique hits');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,7 +40,11 @@ describe('Adds rule exception', () => {
|
|||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
createCustomRuleEnabled(
|
||||
{ ...getNewRule(), customQuery: 'agent.name:*', index: ['exceptions*'] },
|
||||
{
|
||||
...getNewRule(),
|
||||
customQuery: 'agent.name:*',
|
||||
dataSource: { index: ['exceptions*'], type: 'indexPatterns' },
|
||||
},
|
||||
'rule_testing',
|
||||
'1s'
|
||||
);
|
||||
|
|
|
@ -70,7 +70,7 @@ describe('Exceptions flyout', () => {
|
|||
createExceptionList(getExceptionList(), getExceptionList().list_id).then((response) =>
|
||||
createCustomRule({
|
||||
...getNewRule(),
|
||||
index: ['exceptions-*'],
|
||||
dataSource: { index: ['exceptions-*'], type: 'indexPatterns' },
|
||||
exceptionLists: [
|
||||
{
|
||||
id: response.body.id,
|
||||
|
|
|
@ -37,11 +37,15 @@ interface Interval {
|
|||
type: string;
|
||||
}
|
||||
|
||||
export type RuleDataSource =
|
||||
| { type: 'indexPatterns'; index: string[] }
|
||||
| { type: 'dataView'; dataView: string };
|
||||
|
||||
export interface CustomRule {
|
||||
customQuery?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
index: string[];
|
||||
dataSource: RuleDataSource;
|
||||
interval?: string;
|
||||
severity: string;
|
||||
riskScore: string;
|
||||
|
@ -176,15 +180,39 @@ const getRunsEvery = (): Interval => ({
|
|||
type: 's',
|
||||
});
|
||||
|
||||
const getRunsEveryFiveMinutes = (): Interval => ({
|
||||
interval: '5',
|
||||
timeType: 'Minutes',
|
||||
type: 'm',
|
||||
});
|
||||
|
||||
const getLookBack = (): Interval => ({
|
||||
interval: '50000',
|
||||
timeType: 'Hours',
|
||||
type: 'h',
|
||||
});
|
||||
|
||||
export const getDataViewRule = (): CustomRule => ({
|
||||
customQuery: 'host.name: *',
|
||||
dataSource: { dataView: 'auditbeat-2022', type: 'dataView' },
|
||||
name: 'New Data View Rule',
|
||||
description: 'The new rule description.',
|
||||
severity: 'High',
|
||||
riskScore: '17',
|
||||
tags: ['test', 'newRule'],
|
||||
referenceUrls: ['http://example.com/', 'https://example.com/'],
|
||||
falsePositivesExamples: ['False1', 'False2'],
|
||||
mitre: [getMitre1(), getMitre2()],
|
||||
note: '# test markdown',
|
||||
runsEvery: getRunsEveryFiveMinutes(),
|
||||
lookBack: getLookBack(),
|
||||
timeline: getTimeline(),
|
||||
maxSignals: 100,
|
||||
});
|
||||
|
||||
export const getNewRule = (): CustomRule => ({
|
||||
customQuery: 'host.name: *',
|
||||
index: getIndexPatterns(),
|
||||
dataSource: { index: getIndexPatterns(), type: 'indexPatterns' },
|
||||
name: 'New Rule Test',
|
||||
description: 'The new rule description.',
|
||||
severity: 'High',
|
||||
|
@ -202,7 +230,7 @@ export const getNewRule = (): CustomRule => ({
|
|||
|
||||
export const getBuildingBlockRule = (): CustomRule => ({
|
||||
customQuery: 'host.name: *',
|
||||
index: getIndexPatterns(),
|
||||
dataSource: { index: getIndexPatterns(), type: 'indexPatterns' },
|
||||
name: 'Building Block Rule Test',
|
||||
description: 'The new rule description.',
|
||||
severity: 'High',
|
||||
|
@ -221,7 +249,7 @@ export const getBuildingBlockRule = (): CustomRule => ({
|
|||
|
||||
export const getUnmappedRule = (): CustomRule => ({
|
||||
customQuery: '*:*',
|
||||
index: ['unmapped*'],
|
||||
dataSource: { index: ['unmapped*'], type: 'indexPatterns' },
|
||||
name: 'Rule with unmapped fields',
|
||||
description: 'The new rule description.',
|
||||
severity: 'High',
|
||||
|
@ -239,7 +267,7 @@ export const getUnmappedRule = (): CustomRule => ({
|
|||
|
||||
export const getUnmappedCCSRule = (): CustomRule => ({
|
||||
customQuery: '*:*',
|
||||
index: [`${ccsRemoteName}:unmapped*`],
|
||||
dataSource: { index: [`${ccsRemoteName}:unmapped*`], type: 'indexPatterns' },
|
||||
name: 'Rule with unmapped fields',
|
||||
description: 'The new rule description.',
|
||||
severity: 'High',
|
||||
|
@ -259,7 +287,7 @@ export const getExistingRule = (): CustomRule => ({
|
|||
customQuery: 'host.name: *',
|
||||
name: 'Rule 1',
|
||||
description: 'Description for Rule 1',
|
||||
index: ['auditbeat-*'],
|
||||
dataSource: { index: ['auditbeat-*'], type: 'indexPatterns' },
|
||||
interval: '100m',
|
||||
severity: 'High',
|
||||
riskScore: '19',
|
||||
|
@ -278,7 +306,7 @@ export const getExistingRule = (): CustomRule => ({
|
|||
|
||||
export const getNewOverrideRule = (): OverrideRule => ({
|
||||
customQuery: 'host.name: *',
|
||||
index: getIndexPatterns(),
|
||||
dataSource: { index: getIndexPatterns(), type: 'indexPatterns' },
|
||||
name: 'Override Rule',
|
||||
description: 'The new rule description.',
|
||||
severity: 'High',
|
||||
|
@ -305,7 +333,7 @@ export const getNewOverrideRule = (): OverrideRule => ({
|
|||
|
||||
export const getNewThresholdRule = (): ThresholdRule => ({
|
||||
customQuery: 'host.name: *',
|
||||
index: getIndexPatterns(),
|
||||
dataSource: { index: getIndexPatterns(), type: 'indexPatterns' },
|
||||
name: 'Threshold Rule',
|
||||
description: 'The new rule description.',
|
||||
severity: 'High',
|
||||
|
@ -325,7 +353,7 @@ export const getNewThresholdRule = (): ThresholdRule => ({
|
|||
|
||||
export const getNewTermsRule = (): NewTermsRule => ({
|
||||
customQuery: 'host.name: *',
|
||||
index: getIndexPatterns(),
|
||||
dataSource: { index: getIndexPatterns(), type: 'indexPatterns' },
|
||||
name: 'New Terms Rule',
|
||||
description: 'The new rule description.',
|
||||
severity: 'High',
|
||||
|
@ -365,7 +393,7 @@ export const getMachineLearningRule = (): MachineLearningRule => ({
|
|||
export const getEqlRule = (): CustomRule => ({
|
||||
customQuery: 'any where process.name == "zsh"',
|
||||
name: 'New EQL Rule',
|
||||
index: getIndexPatterns(),
|
||||
dataSource: { index: getIndexPatterns(), type: 'indexPatterns' },
|
||||
description: 'New EQL rule description.',
|
||||
severity: 'High',
|
||||
riskScore: '17',
|
||||
|
@ -383,7 +411,7 @@ export const getEqlRule = (): CustomRule => ({
|
|||
export const getCCSEqlRule = (): CustomRule => ({
|
||||
customQuery: 'any where process.name == "run-parts"',
|
||||
name: 'New EQL Rule',
|
||||
index: [`${ccsRemoteName}:run-parts`],
|
||||
dataSource: { index: [`${ccsRemoteName}:run-parts`], type: 'indexPatterns' },
|
||||
description: 'New EQL rule description.',
|
||||
severity: 'High',
|
||||
riskScore: '17',
|
||||
|
@ -404,7 +432,7 @@ export const getEqlSequenceRule = (): CustomRule => ({
|
|||
[any where agent.name == "test.local"]\
|
||||
[any where host.name == "test.local"]',
|
||||
name: 'New EQL Sequence Rule',
|
||||
index: getIndexPatterns(),
|
||||
dataSource: { index: getIndexPatterns(), type: 'indexPatterns' },
|
||||
description: 'New EQL rule description.',
|
||||
severity: 'High',
|
||||
riskScore: '17',
|
||||
|
@ -422,7 +450,7 @@ export const getEqlSequenceRule = (): CustomRule => ({
|
|||
export const getNewThreatIndicatorRule = (): ThreatIndicatorRule => ({
|
||||
name: 'Threat Indicator Rule Test',
|
||||
description: 'The threat indicator rule description.',
|
||||
index: ['suspicious-*'],
|
||||
dataSource: { index: ['suspicious-*'], type: 'indexPatterns' },
|
||||
severity: 'Critical',
|
||||
riskScore: '20',
|
||||
tags: ['test', 'threat'],
|
||||
|
|
|
@ -93,6 +93,11 @@ export const AT_LEAST_ONE_INDEX_PATTERN = 'A minimum of one index pattern is req
|
|||
|
||||
export const CUSTOM_QUERY_REQUIRED = 'A custom query is required.';
|
||||
|
||||
export const DATA_VIEW_COMBO_BOX =
|
||||
'[data-test-subj="pick-rule-data-source"] [data-test-subj="comboBoxInput"]';
|
||||
|
||||
export const DATA_VIEW_OPTION = '[data-test-subj="rule-index-toggle-dataView"]';
|
||||
|
||||
export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]';
|
||||
|
||||
export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]';
|
||||
|
|
|
@ -22,6 +22,8 @@ export const ANOMALY_SCORE_DETAILS = 'Anomaly score';
|
|||
|
||||
export const CUSTOM_QUERY_DETAILS = 'Custom query';
|
||||
|
||||
export const DATA_VIEW_DETAILS = 'Data View';
|
||||
|
||||
export const DEFINITION_DETAILS =
|
||||
'[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"]';
|
||||
|
||||
|
|
|
@ -28,7 +28,11 @@ export const createMachineLearningRule = (rule: MachineLearningRule, ruleId = 'm
|
|||
failOnStatusCode: false,
|
||||
});
|
||||
|
||||
export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', interval = '100m') =>
|
||||
export const createCustomRule = (
|
||||
rule: CustomRule,
|
||||
ruleId = 'rule_testing',
|
||||
interval = '100m'
|
||||
): Cypress.Chainable<Cypress.Response<unknown>> =>
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/detection_engine/rules',
|
||||
|
@ -41,7 +45,7 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte
|
|||
severity: rule.severity.toLocaleLowerCase(),
|
||||
type: 'query',
|
||||
from: 'now-50000h',
|
||||
index: rule.index,
|
||||
index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : '',
|
||||
query: rule.customQuery,
|
||||
language: 'kuery',
|
||||
enabled: false,
|
||||
|
@ -51,98 +55,107 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte
|
|||
failOnStatusCode: false,
|
||||
});
|
||||
|
||||
export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_testing') =>
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/detection_engine/rules',
|
||||
body: {
|
||||
rule_id: ruleId,
|
||||
risk_score: parseInt(rule.riskScore, 10),
|
||||
description: rule.description,
|
||||
interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`,
|
||||
from: `now-${rule.lookBack.interval}${rule.lookBack.type}`,
|
||||
name: rule.name,
|
||||
severity: rule.severity.toLocaleLowerCase(),
|
||||
type: 'eql',
|
||||
index: rule.index,
|
||||
query: rule.customQuery,
|
||||
language: 'eql',
|
||||
enabled: true,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
});
|
||||
export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_testing') => {
|
||||
if (rule.dataSource.type === 'indexPatterns') {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/detection_engine/rules',
|
||||
body: {
|
||||
rule_id: ruleId,
|
||||
risk_score: parseInt(rule.riskScore, 10),
|
||||
description: rule.description,
|
||||
interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`,
|
||||
from: `now-${rule.lookBack.interval}${rule.lookBack.type}`,
|
||||
name: rule.name,
|
||||
severity: rule.severity.toLocaleLowerCase(),
|
||||
type: 'eql',
|
||||
index: rule.dataSource.index,
|
||||
query: rule.customQuery,
|
||||
language: 'eql',
|
||||
enabled: true,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') =>
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/detection_engine/rules',
|
||||
body: {
|
||||
rule_id: ruleId,
|
||||
risk_score: parseInt(rule.riskScore, 10),
|
||||
description: rule.description,
|
||||
// Default interval is 1m, our tests config overwrite this to 1s
|
||||
// See https://github.com/elastic/kibana/pull/125396 for details
|
||||
interval: '10s',
|
||||
name: rule.name,
|
||||
severity: rule.severity.toLocaleLowerCase(),
|
||||
type: 'threat_match',
|
||||
timeline_id: rule.timeline.templateTimelineId,
|
||||
timeline_title: rule.timeline.title,
|
||||
threat_mapping: [
|
||||
{
|
||||
entries: [
|
||||
{
|
||||
field: rule.indicatorMappingField,
|
||||
type: 'mapping',
|
||||
value: rule.indicatorIndexField,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
threat_query: '*:*',
|
||||
threat_language: 'kuery',
|
||||
threat_filters: [],
|
||||
threat_index: rule.indicatorIndexPattern,
|
||||
threat_indicator_path: rule.threatIndicatorPath,
|
||||
from: 'now-50000h',
|
||||
index: rule.index,
|
||||
query: rule.customQuery || '*:*',
|
||||
language: 'kuery',
|
||||
enabled: true,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') => {
|
||||
if (rule.dataSource.type === 'indexPatterns') {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/detection_engine/rules',
|
||||
body: {
|
||||
rule_id: ruleId,
|
||||
risk_score: parseInt(rule.riskScore, 10),
|
||||
description: rule.description,
|
||||
// Default interval is 1m, our tests config overwrite this to 1s
|
||||
// See https://github.com/elastic/kibana/pull/125396 for details
|
||||
interval: '10s',
|
||||
name: rule.name,
|
||||
severity: rule.severity.toLocaleLowerCase(),
|
||||
type: 'threat_match',
|
||||
timeline_id: rule.timeline.templateTimelineId,
|
||||
timeline_title: rule.timeline.title,
|
||||
threat_mapping: [
|
||||
{
|
||||
entries: [
|
||||
{
|
||||
field: rule.indicatorMappingField,
|
||||
type: 'mapping',
|
||||
value: rule.indicatorIndexField,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
threat_query: '*:*',
|
||||
threat_language: 'kuery',
|
||||
threat_filters: [],
|
||||
threat_index: rule.indicatorIndexPattern,
|
||||
threat_indicator_path: rule.threatIndicatorPath,
|
||||
from: 'now-50000h',
|
||||
index: rule.dataSource.index,
|
||||
query: rule.customQuery || '*:*',
|
||||
language: 'kuery',
|
||||
enabled: true,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createCustomRuleEnabled = (
|
||||
rule: CustomRule,
|
||||
ruleId = '1',
|
||||
interval = '100m',
|
||||
maxSignals = 500
|
||||
) =>
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/detection_engine/rules',
|
||||
body: {
|
||||
rule_id: ruleId,
|
||||
risk_score: parseInt(rule.riskScore, 10),
|
||||
description: rule.description,
|
||||
interval,
|
||||
name: rule.name,
|
||||
severity: rule.severity.toLocaleLowerCase(),
|
||||
type: 'query',
|
||||
from: 'now-50000h',
|
||||
index: rule.index,
|
||||
query: rule.customQuery,
|
||||
language: 'kuery',
|
||||
enabled: true,
|
||||
tags: ['rule1'],
|
||||
max_signals: maxSignals,
|
||||
building_block_type: rule.buildingBlockType,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
) => {
|
||||
if (rule.dataSource.type === 'indexPatterns') {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/detection_engine/rules',
|
||||
body: {
|
||||
rule_id: ruleId,
|
||||
risk_score: parseInt(rule.riskScore, 10),
|
||||
description: rule.description,
|
||||
interval,
|
||||
name: rule.name,
|
||||
severity: rule.severity.toLocaleLowerCase(),
|
||||
type: 'query',
|
||||
from: 'now-50000h',
|
||||
index: rule.dataSource.index,
|
||||
query: rule.customQuery,
|
||||
language: 'kuery',
|
||||
enabled: true,
|
||||
tags: ['rule1'],
|
||||
max_signals: maxSignals,
|
||||
building_block_type: rule.buildingBlockType,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCustomRule = (ruleId = '1') => {
|
||||
cy.request({
|
||||
|
|
|
@ -179,14 +179,14 @@ export const deleteCases = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const postDataView = (indexPattern: string) => {
|
||||
export const postDataView = (dataSource: string) => {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/index_patterns/index_pattern`,
|
||||
body: {
|
||||
index_pattern: {
|
||||
fieldAttrs: '{}',
|
||||
title: indexPattern,
|
||||
title: dataSource,
|
||||
timeFieldName: '@timestamp',
|
||||
fields: '{}',
|
||||
},
|
||||
|
|
|
@ -94,6 +94,8 @@ import {
|
|||
EMAIL_CONNECTOR_PASSWORD_INPUT,
|
||||
EMAIL_CONNECTOR_SERVICE_SELECTOR,
|
||||
PREVIEW_HISTOGRAM,
|
||||
DATA_VIEW_COMBO_BOX,
|
||||
DATA_VIEW_OPTION,
|
||||
NEW_TERMS_TYPE,
|
||||
NEW_TERMS_HISTORY_SIZE,
|
||||
NEW_TERMS_HISTORY_TIME_TYPE,
|
||||
|
@ -258,6 +260,10 @@ export const fillAboutRuleWithOverrideAndContinue = (rule: OverrideRule) => {
|
|||
export const fillDefineCustomRuleWithImportedQueryAndContinue = (
|
||||
rule: CustomRule | OverrideRule
|
||||
) => {
|
||||
if (rule.dataSource.type === 'dataView') {
|
||||
cy.get(DATA_VIEW_OPTION).click();
|
||||
cy.get(DATA_VIEW_COMBO_BOX).type(`${rule.dataSource.dataView}{enter}`);
|
||||
}
|
||||
cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click();
|
||||
cy.get(TIMELINE(rule.timeline.id)).click();
|
||||
cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery);
|
||||
|
@ -282,9 +288,11 @@ export const fillDefineThresholdRule = (rule: ThresholdRule) => {
|
|||
cy.get(TIMELINE(rule.timeline.id)).click();
|
||||
cy.get(COMBO_BOX_CLEAR_BTN).first().click();
|
||||
|
||||
rule.index.forEach((index) => {
|
||||
cy.get(COMBO_BOX_INPUT).first().type(`${index}{enter}`);
|
||||
});
|
||||
if (rule.dataSource.type === 'indexPatterns') {
|
||||
rule.dataSource.index.forEach((index) => {
|
||||
cy.get(COMBO_BOX_INPUT).first().type(`${index}{enter}`);
|
||||
});
|
||||
}
|
||||
|
||||
cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery);
|
||||
cy.get(THRESHOLD_INPUT_AREA)
|
||||
|
@ -494,7 +502,9 @@ export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQ
|
|||
* @param rule The rule to use to fill in everything
|
||||
*/
|
||||
export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRule) => {
|
||||
fillIndexAndIndicatorIndexPattern(rule.index, rule.indicatorIndexPattern);
|
||||
if (rule.dataSource.type === 'indexPatterns') {
|
||||
fillIndexAndIndicatorIndexPattern(rule.dataSource.index, rule.indicatorIndexPattern);
|
||||
}
|
||||
fillIndicatorMatchRow({
|
||||
indexField: rule.indicatorMappingField,
|
||||
indicatorIndexField: rule.indicatorIndexField,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue