[Security Solution][Exceptions] - Fix add exception to rule using data view from alert (#138856)

## Summary

Addresses issue #138231

There's a bit of a race condition happening between data views/index patterns. When a rule has a data view defined, we really shouldn't be making calls to fetch the default index patterns which is what had started happening here, however then once the data view id was defined, the fetch index was cut short but the `useFetchIndex` hook never reset to loading `false`, hence the perpetual loading state. That initial call should be avoided all together so that's what this change does. There's still a little funkiness in the code here that I'd like to look at during our work in 8.5.
This commit is contained in:
Yara Tercero 2022-08-16 13:04:33 -07:00 committed by GitHub
parent 8748cf80b5
commit cb8cf84720
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 189 additions and 4 deletions

View file

@ -0,0 +1,159 @@
/*
* 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 { getException } from '../../objects/exception';
import { getNewRule } from '../../objects/rule';
import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../screens/alerts';
import { addExceptionFromFirstAlert, goToClosedAlerts, goToOpenedAlerts } from '../../tasks/alerts';
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
import { goToRuleDetails } from '../../tasks/alerts_detection_rules';
import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload, esArchiverResetKibana } from '../../tasks/es_archiver';
import { login, visitWithoutDateRange } from '../../tasks/login';
import {
addsException,
addsExceptionFromRuleSettings,
editException,
goToAlertsTab,
goToExceptionsTab,
removeException,
waitForTheRuleToBeExecuted,
} from '../../tasks/rule_details';
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
import { deleteAlertsAndRules, postDataView } from '../../tasks/common';
import {
EXCEPTION_EDIT_FLYOUT_SAVE_BTN,
EXCEPTION_ITEM_CONTAINER,
FIELD_INPUT,
} from '../../screens/exceptions';
import {
addExceptionEntryFieldValueOfItemX,
addExceptionEntryFieldValueValue,
} from '../../tasks/exceptions';
describe('Adds rule exception using data views', () => {
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert';
before(() => {
esArchiverResetKibana();
esArchiverLoad('exceptions');
login();
postDataView('exceptions-*');
});
beforeEach(() => {
deleteAlertsAndRules();
createCustomRuleEnabled(
{
...getNewRule(),
customQuery: 'agent.name:*',
dataSource: { dataView: 'exceptions-*', type: 'dataView' },
},
'rule_testing',
'1s'
);
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
goToRuleDetails();
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
});
afterEach(() => {
esArchiverUnload('exceptions_2');
});
after(() => {
esArchiverUnload('exceptions');
});
it('Creates an exception from an alert and deletes it', () => {
cy.get(ALERTS_COUNT).should('exist');
cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS);
// Create an exception from the alerts actions menu that matches
// the existing alert
addExceptionFromFirstAlert();
addsException(getException());
// Alerts table should now be empty from having added exception and closed
// matching alert
cy.get(EMPTY_ALERT_TABLE).should('exist');
// Closed alert should appear in table
goToClosedAlerts();
cy.get(ALERTS_COUNT).should('exist');
cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
// Remove the exception and load an event that would have matched that exception
// to show that said exception now starts to show up again
goToExceptionsTab();
removeException();
esArchiverLoad('exceptions_2');
goToAlertsTab();
goToOpenedAlerts();
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT).should('exist');
cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
});
it('Creates an exception from a rule and deletes it', () => {
// Create an exception from the exception tab that matches
// the existing alert
goToExceptionsTab();
addsExceptionFromRuleSettings(getException());
// Alerts table should now be empty from having added exception and closed
// matching alert
goToAlertsTab();
cy.get(EMPTY_ALERT_TABLE).should('exist');
// Closed alert should appear in table
goToClosedAlerts();
cy.get(ALERTS_COUNT).should('exist');
cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
// Remove the exception and load an event that would have matched that exception
// to show that said exception now starts to show up again
goToExceptionsTab();
removeException();
esArchiverLoad('exceptions_2');
goToAlertsTab();
goToOpenedAlerts();
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT).should('exist');
cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`);
});
it('Edits an exception', () => {
goToExceptionsTab();
addsExceptionFromRuleSettings(getException());
editException();
// check that the existing item's field is being populated
cy.get(EXCEPTION_ITEM_CONTAINER)
.eq(0)
.find(FIELD_INPUT)
.eq(0)
.should('have.text', 'agent.name');
// check that you can select a different field
addExceptionEntryFieldValueOfItemX('user.name{downarrow}{enter}', 0, 0);
addExceptionEntryFieldValueValue('test', 0);
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click();
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled');
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist');
});
});

View file

@ -154,6 +154,31 @@ export const createCustomRuleEnabled = (
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
});
} else if (rule.dataSource.type === 'dataView') {
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: [],
data_view_id: rule.dataSource.dataView,
query: rule.customQuery,
language: 'kuery',
enabled: true,
tags: ['rule1'],
max_signals: maxSignals,
building_block_type: rule.buildingBlockType,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
});
}
};

View file

@ -185,6 +185,7 @@ export const postDataView = (dataSource: string) => {
url: `/api/index_patterns/index_pattern`,
body: {
index_pattern: {
id: dataSource,
fieldAttrs: '{}',
title: dataSource,
timeFieldName: '@timestamp',

View file

@ -331,7 +331,7 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps>
/**
* This should be re-visited after UEBA work is merged
*/
const useRuleIndices = useMemo(() => {
const memoRuleIndices = useMemo(() => {
if (enrichedAlert != null && enrichedAlert['kibana.alert.rule.parameters']?.index != null) {
return Array.isArray(enrichedAlert['kibana.alert.rule.parameters'].index)
? enrichedAlert['kibana.alert.rule.parameters'].index
@ -341,8 +341,8 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps>
? enrichedAlert.signal.rule.index
: [enrichedAlert.signal.rule.index];
}
return ruleIndices;
}, [enrichedAlert, ruleIndices]);
return [];
}, [enrichedAlert]);
const memoDataViewId = useMemo(() => {
if (
@ -359,7 +359,7 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps>
<AddExceptionFlyout
ruleName={ruleName}
ruleId={ruleId}
ruleIndices={useRuleIndices}
ruleIndices={memoRuleIndices}
dataViewId={memoDataViewId}
exceptionListType={exceptionListType}
alertData={enrichedAlert}