Skip to content

Commit

Permalink
[Security Solutions][Detection Engine] Adds a warning banner when the…
Browse files Browse the repository at this point in the history
… alerts data has not been migrated yet. (#90258)

## Summary

Adds a warning banner for when the alerting/signals data has not been migrated to the new structure. Although we are planning on supporting some backwards compatibility where the rules don't completely blow up, this support of backwards compatibility is going to be best effort and not have explicit tests and checks against backwards compatibility. Hence the reason we need to alert any users of the system when we can that they should have an administrator visit the detections page to start a migration.

From previous reasons why we don't migrate on startup of Kibana is that there are multiple instances running and it might be a worse situation so we migrate on page visit by an administrator to reduce chances of issues. In the future we might revisit this decision but for now this is what we have moved forward with.

If the user does not have sufficient privileges such as t1 analyst to see if they have should upgrade, no message is shown to those users.

This PR adds the following banner which is non-dismissible to:
* Main detections page
* Manage rules page
* View/Edit rules page
<img width="2259" alt="Screen Shot 2021-02-03 at 4 16 00 PM" src="https://user-images.githubusercontent.com/1151048/106926989-eb2fb300-66ce-11eb-877c-1210357af108.png">

If other dismissible alerts are on the page then you will get a stacked effect until you dismiss those messages. Again, this message cannot be dismissed intentionally to let the user know that they should contact an administrator to update/upgrade the alerting/signal data:
<img width="1526" alt="Screen Shot 2021-02-03 at 5 41 57 PM" src="https://user-images.githubusercontent.com/1151048/106927465-6b561880-66cf-11eb-8c0f-dfdfa624c24b.png">

Other items of note:
* Added ability to remove the button from the callouts
* Consolidated in one area some types
* Removed one part of the callout that has branching logic we never activate. We can re-add that later if we do have a need for it
* e2e Cypress tests added to detect when the banner should be present
* Backfilled unit tests for enzyme for some of the callout code

Manual testing:
Bump this number in your dev env:
https://github.com/elastic/kibana/blob/master/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts#L11

Give yourself these permissions (or use one of the scripts for creating these roles):
<img width="1243" alt="Screen Shot 2021-02-05 at 1 49 02 PM" src="https://user-images.githubusercontent.com/1151048/107087773-30301400-67b9-11eb-9ac9-0a67fafd8231.png">

Visit the page.

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
  • Loading branch information
FrankHassanabad committed Feb 17, 2021
1 parent 06580cd commit 1e66526
Show file tree
Hide file tree
Showing 17 changed files with 759 additions and 86 deletions.
6 changes: 6 additions & 0 deletions x-pack/plugins/security_solution/common/utility_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export const stringEnum = <T>(enumObj: T, enumName = 'enum') =>
*
* Optionally you can avoid the use of this by using early returns and TypeScript will clear your type checking without complaints
* but there are situations and times where this function might still be needed.
*
* If you see an error, DO NOT cast "as never" such as:
* assertUnreachable(x as never) // BUG IN YOUR CODE NOW AND IT WILL THROW DURING RUNTIME
* If you see code like that remove it, as that deactivates the intent of this utility.
* If you need to do that, then you should remove assertUnreachable from your code and
* use a default at the end of the switch instead.
* @param x Unreachable field
* @param message Message of error thrown
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* 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 { ROLES } from '../../../common/test';
import { DETECTIONS_RULE_MANAGEMENT_URL, DETECTIONS_URL } from '../../urls/navigation';
import { newRule } from '../../objects/rule';
import { PAGE_TITLE } from '../../screens/common/page';

import {
login,
loginAndWaitForPageWithoutDateRange,
waitForPageWithoutDateRange,
} from '../../tasks/login';
import { waitForAlertsIndexToBeCreated } from '../../tasks/alerts';
import { goToRuleDetails } from '../../tasks/alerts_detection_rules';
import { createCustomRule, deleteCustomRule } from '../../tasks/api_calls/rules';
import { getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts';
import { cleanKibana } from '../../tasks/common';

const loadPageAsPlatformEngineerUser = (url: string) => {
waitForPageWithoutDateRange(url, ROLES.soc_manager);
waitForPageTitleToBeShown();
};

const waitForPageTitleToBeShown = () => {
cy.get(PAGE_TITLE).should('be.visible');
};

describe('Detections > Need Admin Callouts indicating an admin is needed to migrate the alert data set', () => {
const NEED_ADMIN_FOR_UPDATE_CALLOUT = 'need-admin-for-update-rules';

before(() => {
// First, we have to open the app on behalf of a privileged user in order to initialize it.
// Otherwise the app will be disabled and show a "welcome"-like page.
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL, ROLES.platform_engineer);
waitForAlertsIndexToBeCreated();

// After that we can login as a soc manager.
login(ROLES.soc_manager);
});

context(
'The users index_mapping_outdated is "true" and their admin callouts should show up',
() => {
beforeEach(() => {
// Index mapping outdated is forced to return true as being outdated so that we get the
// need admin callouts being shown.
cy.intercept('GET', '/api/detection_engine/index', {
index_mapping_outdated: true,
name: '.siem-signals-default',
});
});
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_URL);
});

it('We show the need admin primary callout', () => {
waitForCallOutToBeShown(NEED_ADMIN_FOR_UPDATE_CALLOUT, 'primary');
});
});

context('On Rules Management page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL);
});

it('We show 1 primary callout of need admin', () => {
waitForCallOutToBeShown(NEED_ADMIN_FOR_UPDATE_CALLOUT, 'primary');
});
});

context('On Rule Details page', () => {
beforeEach(() => {
createCustomRule(newRule);
loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL);
waitForPageTitleToBeShown();
goToRuleDetails();
});

afterEach(() => {
deleteCustomRule();
});

it('We show 1 primary callout', () => {
waitForCallOutToBeShown(NEED_ADMIN_FOR_UPDATE_CALLOUT, 'primary');
});
});
}
);

context(
'The users index_mapping_outdated is "false" and their admin callouts should not show up ',
() => {
beforeEach(() => {
// Index mapping outdated is forced to return true as being outdated so that we get the
// need admin callouts being shown.
cy.intercept('GET', '/api/detection_engine/index', {
index_mapping_outdated: false,
name: '.siem-signals-default',
});
});
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_URL);
});

it('We show the need admin primary callout', () => {
getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist');
});
});

context('On Rules Management page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL);
});

it('We show 1 primary callout of need admin', () => {
getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist');
});
});

context('On Rule Details page', () => {
beforeEach(() => {
createCustomRule(newRule);
loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL);
waitForPageTitleToBeShown();
goToRuleDetails();
});

afterEach(() => {
deleteCustomRule();
});

it('We show 1 primary callout', () => {
getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist');
});
});
}
);

context(
'The users index_mapping_outdated is "null" and their admin callouts should not show up ',
() => {
beforeEach(() => {
// Index mapping outdated is forced to return true as being outdated so that we get the
// need admin callouts being shown.
cy.intercept('GET', '/api/detection_engine/index', {
index_mapping_outdated: null,
name: '.siem-signals-default',
});
});
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_URL);
});

it('We show the need admin primary callout', () => {
getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist');
});
});

context('On Rules Management page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL);
});

it('We show 1 primary callout of need admin', () => {
getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist');
});
});

context('On Rule Details page', () => {
beforeEach(() => {
createCustomRule(newRule);
loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL);
waitForPageTitleToBeShown();
goToRuleDetails();
});

afterEach(() => {
deleteCustomRule();
});

it('We show 1 primary callout', () => {
getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist');
});
});
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const loadPageAsReadOnlyUser = (url: string) => {
waitForPageTitleToBeShown();
};

const loadPageAsPlatformEngineer = (url: string) => {
waitForPageWithoutDateRange(url, ROLES.platform_engineer);
waitForPageTitleToBeShown();
};

const reloadPage = () => {
cy.reload();
waitForPageTitleToBeShown();
Expand All @@ -35,7 +40,7 @@ const waitForPageTitleToBeShown = () => {
cy.get(PAGE_TITLE).should('be.visible');
};

describe('Detections > Callouts indicating read-only access to resources', () => {
describe('Detections > Callouts', () => {
const ALERTS_CALLOUT = 'read-only-access-to-alerts';
const RULES_CALLOUT = 'read-only-access-to-rules';

Expand All @@ -50,75 +55,119 @@ describe('Detections > Callouts indicating read-only access to resources', () =>
login(ROLES.reader);
});

context('On Detections home page', () => {
beforeEach(() => {
loadPageAsReadOnlyUser(DETECTIONS_URL);
});

it('We show one primary callout', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
});
context('indicating read-only access to resources', () => {
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsReadOnlyUser(DETECTIONS_URL);
});

context('When a user clicks Dismiss on the callout', () => {
it('We hide it and persist the dismissal', () => {
it('We show one primary callout', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
dismissCallOut(ALERTS_CALLOUT);
reloadPage();
getCallOut(ALERTS_CALLOUT).should('not.exist');
});
});
});

context('On Rules Management page', () => {
beforeEach(() => {
loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL);
context('When a user clicks Dismiss on the callout', () => {
it('We hide it and persist the dismissal', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
dismissCallOut(ALERTS_CALLOUT);
reloadPage();
getCallOut(ALERTS_CALLOUT).should('not.exist');
});
});
});

it('We show one primary callout', () => {
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
});
context('On Rules Management page', () => {
beforeEach(() => {
loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL);
});

context('When a user clicks Dismiss on the callout', () => {
it('We hide it and persist the dismissal', () => {
it('We show one primary callout', () => {
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
dismissCallOut(RULES_CALLOUT);
reloadPage();
getCallOut(RULES_CALLOUT).should('not.exist');
});
});
});

context('On Rule Details page', () => {
beforeEach(() => {
createCustomRule(newRule);
loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL);
waitForPageTitleToBeShown();
goToRuleDetails();
context('When a user clicks Dismiss on the callout', () => {
it('We hide it and persist the dismissal', () => {
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
dismissCallOut(RULES_CALLOUT);
reloadPage();
getCallOut(RULES_CALLOUT).should('not.exist');
});
});
});

afterEach(() => {
deleteCustomRule();
});
context('On Rule Details page', () => {
beforeEach(() => {
createCustomRule(newRule);
loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL);
waitForPageTitleToBeShown();
goToRuleDetails();
});

it('We show two primary callouts', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
});
afterEach(() => {
deleteCustomRule();
});

context('When a user clicks Dismiss on the callouts', () => {
it('We hide them and persist the dismissal', () => {
it('We show two primary callouts', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
});

dismissCallOut(ALERTS_CALLOUT);
reloadPage();
context('When a user clicks Dismiss on the callouts', () => {
it('We hide them and persist the dismissal', () => {
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');

dismissCallOut(ALERTS_CALLOUT);
reloadPage();

getCallOut(ALERTS_CALLOUT).should('not.exist');
getCallOut(RULES_CALLOUT).should('be.visible');

dismissCallOut(RULES_CALLOUT);
reloadPage();

getCallOut(ALERTS_CALLOUT).should('not.exist');
getCallOut(RULES_CALLOUT).should('not.exist');
});
});
});
});

context('indicating read-write access to resources', () => {
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsPlatformEngineer(DETECTIONS_URL);
});

it('We show no callout', () => {
getCallOut(ALERTS_CALLOUT).should('not.exist');
getCallOut(RULES_CALLOUT).should('not.exist');
});
});

context('On Rules Management page', () => {
beforeEach(() => {
loadPageAsPlatformEngineer(DETECTIONS_RULE_MANAGEMENT_URL);
});

it('We show no callout', () => {
getCallOut(ALERTS_CALLOUT).should('not.exist');
getCallOut(RULES_CALLOUT).should('be.visible');
getCallOut(RULES_CALLOUT).should('not.exist');
});
});

dismissCallOut(RULES_CALLOUT);
reloadPage();
context('On Rule Details page', () => {
beforeEach(() => {
createCustomRule(newRule);
loadPageAsPlatformEngineer(DETECTIONS_RULE_MANAGEMENT_URL);
waitForPageTitleToBeShown();
goToRuleDetails();
});

afterEach(() => {
deleteCustomRule();
});

it('We show no callouts', () => {
getCallOut(ALERTS_CALLOUT).should('not.exist');
getCallOut(RULES_CALLOUT).should('not.exist');
});
Expand Down
Loading

0 comments on commit 1e66526

Please sign in to comment.