Skip to content

Commit

Permalink
[SIEM] Toggle Column / Code Coverage and Cypress (elastic#42766)
Browse files Browse the repository at this point in the history
## Features

-  A `Toggle column` checkbox for adding / removing fields in the timeline offers
an alternative to drag and drop in an expanded event:

![toggle-field](https://user-images.githubusercontent.com/4459398/62572146-4d94c280-b850-11e9-8ff9-5feaca3f305d.gif)

The behavior of the `Toggle field` checkbox in the expanded event is the same
as the checkboxes that appear in the timeline _Fields Browser_.

## Fixes

- Pinned timeline events now use a "filled" styling when pinned instead of rotation, and unpinned events have a higher contrast in dark mode:

Before:

![unpinned-before](https://user-images.githubusercontent.com/4459398/62572175-5b4a4800-b850-11e9-9d4f-317d5abad002.png)

After (unpinned):
![after-unpinned](https://user-images.githubusercontent.com/4459398/62908225-28aebc80-bd34-11e9-9237-8a98c88a6da1.png)

After (pinned):
![after-pinned](https://user-images.githubusercontent.com/4459398/62908230-306e6100-bd34-11e9-9830-45d725bac5b1.png)

- Fixed an issue where the `Copy to Clipboard` icon rendered incorrectly (too
small, and not the right color):

Before:

![copy-to-clipboard-before](https://user-images.githubusercontent.com/4459398/62572224-7452f900-b850-11e9-962c-eef0b96202ff.png)

After:

![copy-to-clipboard-after](https://user-images.githubusercontent.com/4459398/62572240-7f0d8e00-b850-11e9-8b89-7c14bf92e705.png)

- Fixed an issue where a previously invisible control became visible and
occupied additonal row space in the timeline:

Before:

![row-height-before](https://user-images.githubusercontent.com/4459398/62572265-8e8cd700-b850-11e9-9c36-1d74ba0ba6f5.png)

After:

![row-height-after](https://user-images.githubusercontent.com/4459398/62572285-977da880-b850-11e9-8448-7085bbac8da4.png)

- Fixed an issue where the timeline Fields Browser Overlaps Column Headers

Before:

![field-browser-position-before](https://user-images.githubusercontent.com/4459398/62572309-a5332e00-b850-11e9-9b6f-38700d2f48a0.png)

After:

![field-browser-position-after](https://user-images.githubusercontent.com/4459398/62572335-b845fe00-b850-11e9-84f8-4b273f7ef163.png)

## Code Coverage and Cypress Tests

- This PR increases the `jest` unit test coverage for the _Fields Browser_, and
introduces `Cypress` tests for it
- Added new `Cypress` test helpers, configuration, and test refactorings to
support running smoke tests against remote Kibana instances, including
[Elastic Cloud](https://www.elastic.co/cloud)
- Refactored some `Cypress` tests to speed them up and reduce flakyness

elastic/siem-team#380
elastic/siem-team#431
elastic/siem-team#432
elastic/siem-team#433
elastic/siem-team#434
elastic/siem-team#435
  • Loading branch information
andrew-goldstein committed Aug 13, 2019
1 parent 8a24422 commit 3c6fbf1
Show file tree
Hide file tree
Showing 77 changed files with 3,939 additions and 287 deletions.
4 changes: 3 additions & 1 deletion x-pack/legacy/plugins/siem/cypress.json
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{}
{
"baseUrl": "http://localhost:5601"
}
9 changes: 9 additions & 0 deletions x-pack/legacy/plugins/siem/cypress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ The `username` and `password` from `config/kibana.dev.yml` will be read by the `

See the `Running Tests Interactively` section for details.

### Content Security Policy (CSP) Settings

Your local or cloud Kibana server must have the `csp.strict: false` setting
configured in `kibana.dev.yml`, or `kibana.yml`, as shown in the example below:

```yaml
csp.strict: false
```

## Running Tests Interactively

To run tests in interactively via the Cypress test runner:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { FIELDS_BROWSER_CONTAINER, FIELDS_BROWSER_FILTER_INPUT, FIELDS_BUTTON } from './selectors';
import {
assertAtLeastOneEventMatchesSearch,
executeKQL,
hostExistsQuery,
toggleTimelineVisibility,
} from '../timeline/helpers';
import { TIMELINE_DATA_PROVIDERS } from '../timeline/selectors';

/** Opens the timeline's Field Browser */
export const openFieldsBrowser = () => {
cy.get(FIELDS_BUTTON).click();

cy.get(FIELDS_BROWSER_CONTAINER).should('exist');
};

/** Populates the timeline with a host from the hosts page */
export const populateTimeline = () => {
toggleTimelineVisibility();

executeKQL(hostExistsQuery);

assertAtLeastOneEventMatchesSearch();
};

/** Clicks an arbitrary UI element that's not part of the fields browser (to dismiss it) */
export const clickOutsideFieldsBrowser = () => {
cy.get(TIMELINE_DATA_PROVIDERS).click();
};

/** Filters the Field Browser by typing `fieldName` in the input */
export const filterFieldsBrowser = (fieldName: string) => {
cy.get(FIELDS_BROWSER_FILTER_INPUT).type(fieldName);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

/** Clicking this button in the timeline opens the Fields browser */
export const FIELDS_BUTTON = '[data-test-subj="show-field-browser"]';

/** The title displayed in the fields browser (i.e. Customize Columns) */
export const FIELDS_BROWSER_TITLE = '[data-test-subj="field-browser-title"]';

/** Contains the body of the fields browser */
export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]';

/** The title of the selected category in the right-hand side of the fields browser */
export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = '[data-test-subj="selected-category-title"]';

/** A count of the fields in the selected category in the right-hand side of the fields browser */
export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT =
'[data-test-subj="selected-category-count-badge"]';

/** Typing in this input filters the Field Browser */
export const FIELDS_BROWSER_FILTER_INPUT = '[data-test-subj="field-search"]';

/**
* This label displays a count of the categories containing (one or more)
* fields that match the filter criteria
*/
export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]';

/**
* This label displays a count of the fields that match the filter criteria
*/
export const FIELDS_BROWSER_FIELDS_COUNT = '[data-test-subj="fields-count"]';
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ALL_HOSTS_WIDGET } from './selectors';
import { DEFAULT_TIMEOUT } from '../util/helpers';

/** Wait this long for the for the `All Hosts` widget on the `Hosts` page to load */
const ALL_HOSTS_TIMEOUT = 10 * 1000;
import { ALL_HOSTS_WIDGET } from './selectors';

/** Wait for the for the `All Hosts` widget on the `Hosts` page to load */
export const waitForAllHostsWidget = () => cy.get(ALL_HOSTS_WIDGET, { timeout: ALL_HOSTS_TIMEOUT });
export const waitForAllHostsWidget = () => cy.get(ALL_HOSTS_WIDGET, { timeout: DEFAULT_TIMEOUT });
125 changes: 82 additions & 43 deletions x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,73 +6,112 @@

import * as yaml from 'js-yaml';

import { LOGIN_PAGE } from '../urls';
/**
* Credentials in the `kibana.dev.yml` config file will be used to authenticate
* with Kibana when credentials are not provided via environment variables
*/
const KIBANA_DEV_YML_PATH = '../../../../config/kibana.dev.yml';

import { DEFAULT_SPACE_BUTTON, PASSWORD, USERNAME } from './selectors';
/**
* The configuration path in `kibana.dev.yml` to the username to be used when
* authenticating with Kibana.
*/
const ELASTICSEARCH_USERNAME_CONFIG_PATH = 'config.elasticsearch.username';

/**
* The `username` and `password` values in the `elasticsearch` section of this
* file will be used to login to Kibana
* The configuration path in `kibana.dev.yml` to the password to be used when
* authenticating with Kibana.
*/
const KIBANA_DEV_YML_PATH = '../../../../config/kibana.dev.yml';
const ELASTICSEARCH_PASSWORD_CONFIG_PATH = 'config.elasticsearch.password';

/** Wait this long for the login page wait (ms) */
const USERNAME_TIMEOUT = 10 * 1000;
/**
* The `CYPRESS_ELASTICSEARCH_USERNAME` environment variable specifies the
* username to be used when authenticating with Kibana
*/
const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME';

/**
* Authenticates with Kibana by POSTing the username and password directly to
* Kibana's `security/v1/login` endpoint, bypassing the login page (for speed).
* The `CYPRESS_ELASTICSEARCH_PASSWORD` environment variable specifies the
* username to be used when authenticating with Kibana
*/
const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD';

/**
* The Kibana server endpoint used for authentication
*/
const LOGIN_API_ENDPOINT = '/api/security/v1/login';

/**
* Authenticates with Kibana using, if specified, credentials specified by
* environment variables. The credentials in `kibana.dev.yml` will be used
* for authentication when the environment variables are unset.
*
* To speed the execution of tests, prefer this function over authenticating
* via an interactive login.
* To speed the execution of tests, prefer this non-interactive authentication,
* which is faster than authentication via Kibana's interactive login page.
*/
export const login = () => {
if (credentialsProvidedByEnvironment()) {
loginViaEnvironmentCredentials();
} else {
loginViaConfig();
}
};

/**
* Returns `true` if the credentials used to login to Kibana are provided
* via environment variables
*/
const credentialsProvidedByEnvironment = (): boolean =>
Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null;

/**
* Authenticates with Kibana by reading credentials from the
* `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD`
* environment variables, and POSTing the username and password directly to
* Kibana's `security/v1/login` endpoint, bypassing the login page (for speed).
*/
const loginViaEnvironmentCredentials = () => {
cy.log(
`NON-interactively logging into Kibana with the \`username\` and \`password\` from the \`elasticsearch\` section of \`${KIBANA_DEV_YML_PATH}\``
`Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables`
);

// read the login details
cy.readFile(KIBANA_DEV_YML_PATH).then(kibanaDevYml => {
const config = yaml.safeLoad(kibanaDevYml);

// programmatically log us in without needing the UI
cy.request({
body: {
username: config.elasticsearch.username,
password: config.elasticsearch.password,
},
followRedirect: false,
headers: { 'kbn-xsrf': 'cypress' },
method: 'POST',
url: 'http://localhost:5601/api/security/v1/login',
});
// programmatically authenticate without interacting with the Kibana login page
cy.request({
body: {
username: Cypress.env(ELASTICSEARCH_USERNAME),
password: Cypress.env(ELASTICSEARCH_PASSWORD),
},
followRedirect: false,
headers: { 'kbn-xsrf': 'cypress-creds-via-env' },
method: 'POST',
url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
});
};

/**
* This (slower) login function authenticates with Kibana via the login page.
* To speed the execution of tests, is generally preferable to use the
* NON-interactive `login` function (in this file) instead, because in
* addition to having to wait for the interactive Kibana login page to load,
* this function also waits for the "spaces" page to load after
* authenticating so it can click on the default space.
* Authenticates with Kibana by reading credentials from the
* `kibana.dev.yml` file and POSTing the username and password directly to
* Kibana's `security/v1/login` endpoint, bypassing the login page (for speed).
*/
export const interactiveLogin = () => {
const loginViaConfig = () => {
cy.log(
`Interactively logging into Kibana with the \`username\` and \`password\` from the \`elasticsearch\` section of \`${KIBANA_DEV_YML_PATH}\``
`Authenticating via config credentials \`${ELASTICSEARCH_USERNAME_CONFIG_PATH}\` and \`${ELASTICSEARCH_PASSWORD_CONFIG_PATH}\` from \`${KIBANA_DEV_YML_PATH}\``
);

// read the login details
// read the login details from `kibana.dev.yaml`
cy.readFile(KIBANA_DEV_YML_PATH).then(kibanaDevYml => {
const config = yaml.safeLoad(kibanaDevYml);

cy.visit(LOGIN_PAGE);

cy.get(USERNAME, { timeout: USERNAME_TIMEOUT }).type(config.elasticsearch.username);
cy.get(PASSWORD).type(`${config.elasticsearch.password}{enter}`, {
log: false,
// programmatically authenticate without interacting with the Kibana login page
cy.request({
body: {
username: config.elasticsearch.username,
password: config.elasticsearch.password,
},
followRedirect: false,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'POST',
url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
});

cy.get(DEFAULT_SPACE_BUTTON).click(); // click the `Default` space in the `Select Your Space` page
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { LOGOUT_LINK, USER_MENU } from './selectors';
import { LOGOUT } from '../urls';

export const logout = () => {
cy.get(USER_MENU).click();

cy.get(LOGOUT_LINK).click();
cy.visit(`${Cypress.config().baseUrl}${LOGOUT}`);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
*/

/** Top-level (global) navigation link to the `Hosts` page */
export const NAVIGATION_HOSTS = '[data-test-subj="navigation-hosts"]';
export const NAVIGATION_HOSTS = '[data-test-subj="navigation-link-hosts"]';

/** Top-level (global) navigation link to the `Network` page */
export const NAVIGATION_NETWORK = '[data-test-subj="navigation-network"]';
export const NAVIGATION_NETWORK = '[data-test-subj="navigation-link-network"]';

/** Top-level (global) navigation link to the `Overview` page */
export const NAVIGATION_OVERVIEW = '[data-test-subj="navigation-overview"]';
export const NAVIGATION_OVERVIEW = '[data-test-subj="navigation-link-overview"]';

/** Top-level (global) navigation link to the `Timelines` page */
export const NAVIGATION_TIMELINES = '[data-test-subj="navigation-timelines"]';
export const NAVIGATION_TIMELINES = '[data-test-subj="navigation-link-timelines"]';
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,46 @@

import { drag, drop } from '../drag_n_drop/helpers';
import { ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS } from '../hosts/selectors';

import { TIMELINE_DATA_PROVIDERS, TIMELINE_TOGGLE_BUTTON } from './selectors';

/** Wait up to this many ms for the timeline to render data providers that were dropped in the timeline */
export const TIMELINE_RENDER_DATA_PROVIDERS_TIMEOUT = 10 * 1000;
import {
SEARCH_OR_FILTER_CONTAINER,
SERVER_SIDE_EVENT_COUNT,
TIMELINE_DATA_PROVIDERS,
TIMELINE_TOGGLE_BUTTON,
TOGGLE_EXPAND_EVENT,
} from './selectors';
import { DEFAULT_TIMEOUT } from '../util/helpers';

/** Toggles the timeline's open / closed state by clicking the `T I M E L I N E` button */
export const toggleTimelineVisibility = () =>
cy
.get(TIMELINE_TOGGLE_BUTTON)
.first()
.click();
cy.get(TIMELINE_TOGGLE_BUTTON, { timeout: DEFAULT_TIMEOUT }).click();

/** Drags and drops a host from the `All Hosts` widget on the `Hosts` page to the timeline */
export const dragFromAllHostsToTimeline = () => {
cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS)
.first()
.then(host => drag(host));

cy.get(TIMELINE_DATA_PROVIDERS)
cy.get(TIMELINE_DATA_PROVIDERS).then(dataProvidersDropArea => drop(dataProvidersDropArea));
};

/** Executes the specified KQL query in the timeline */
export const executeKQL = (query: string) => {
cy.get(`${SEARCH_OR_FILTER_CONTAINER} input`).type(`${query} {enter}`);
};

/** A sample KQL query that finds any documents where the `host.name` field exists */
export const hostExistsQuery = 'host.name: *';

/** Asserts that at least one event matches the timeline's search criteria */
export const assertAtLeastOneEventMatchesSearch = () =>
cy
.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT })
.invoke('text')
.should('be.above', 0);

/** Toggles open or closed the first event in the timeline */
export const toggleFirstEventDetails = () => {
cy.get(TOGGLE_EXPAND_EVENT, { timeout: DEFAULT_TIMEOUT })
.first()
.then(dataProvidersDropArea => drop(dataProvidersDropArea));
.click();
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ export const TIMELINE_DROPPED_DATA_PROVIDERS = `${TIMELINE_DATA_PROVIDERS} ${DAT

/** The `T I M E L I N E` button that toggles visibility of the Timeline */
export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]';

/** Contains the KQL bar for searching or filtering in the timeline */
export const SEARCH_OR_FILTER_CONTAINER =
'[data-test-subj="timeline-search-or-filter-search-container"]';

/** The total server-side count of the events matching the timeline's search criteria */
export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]';

/** Expands or collapses an event in the timeline */
export const TOGGLE_EXPAND_EVENT = '[data-test-subj="expand-event"]';
13 changes: 8 additions & 5 deletions x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
*/

/** The SIEM app's Hosts page */
export const HOSTS_PAGE = 'http://localhost:5601/app/siem#/hosts';
export const HOSTS_PAGE = '/app/siem#/hosts';

/** Kibana's login page */
export const LOGIN_PAGE = 'http://localhost:5601/login';
export const LOGIN_PAGE = '/login';

/** The SIEM app's Network page */
export const NETWORK_PAGE = 'http://localhost:5601/app/siem#/network';
export const NETWORK_PAGE = '/app/siem#/network';

/** The SIEM app's Overview page */
export const OVERVIEW_PAGE = 'http://localhost:5601/app/siem#/overview';
export const OVERVIEW_PAGE = '/app/siem#/overview';

/** The SIEM app's Timelines page */
export const TIMELINES_PAGE = 'http://localhost:5601/app/siem#/timelines';
export const TIMELINES_PAGE = '/app/siem#/timelines';

/** Visit this URL to logout of Kibana */
export const LOGOUT = '/logout';
Loading

0 comments on commit 3c6fbf1

Please sign in to comment.