[Security Solution] Bugfix for inspect index pattern not aligned with data view index pattern (#125007) (#125174)

(cherry picked from commit bad98b6892)

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
Kibana Machine 2022-02-09 20:42:29 -05:00 committed by GitHub
parent dc18a00b69
commit 231230be6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 291 additions and 161 deletions

View file

@ -21,6 +21,10 @@ import { UpdateQueryParams, upsertQuery } from '../../store/inputs/helpers';
import { InspectButton } from '.';
import { cloneDeep } from 'lodash/fp';
jest.mock('./modal', () => ({
ModalInspectQuery: jest.fn(() => <div data-test-subj="mocker-modal" />),
}));
describe('Inspect Button', () => {
const refetch = jest.fn();
const state: State = mockGlobalState;
@ -103,6 +107,54 @@ describe('Inspect Button', () => {
);
expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true);
});
test('Button disabled when inspect == null', () => {
const myState = cloneDeep(state);
const myQuery = cloneDeep(newQuery);
myQuery.inspect = null;
myState.inputs = upsertQuery(myQuery);
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true);
});
test('Button disabled when inspect.dsl.length == 0', () => {
const myState = cloneDeep(state);
const myQuery = cloneDeep(newQuery);
myQuery.inspect = {
dsl: [],
response: ['my response'],
};
myState.inputs = upsertQuery(myQuery);
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true);
});
test('Button disabled when inspect.response.length == 0', () => {
const myState = cloneDeep(state);
const myQuery = cloneDeep(newQuery);
myQuery.inspect = {
dsl: ['my dsl'],
response: [],
};
myState.inputs = upsertQuery(myQuery);
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true);
});
});
describe('Modal Inspect - happy path', () => {
@ -127,29 +179,7 @@ describe('Inspect Button', () => {
wrapper.update();
expect(store.getState().inputs.global.queries[0].isInspected).toBe(true);
expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe(
true
);
});
test('Close Inspect Modal', () => {
const wrapper = mount(
<TestProviders store={store}>
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="modal-inspect-close"]').first().simulate('click');
wrapper.update();
expect(store.getState().inputs.global.queries[0].isInspected).toBe(false);
expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe(
false
);
expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toBe(true);
});
test('Do not Open Inspect Modal if it is loading', () => {
@ -158,6 +188,7 @@ describe('Inspect Button', () => {
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
expect(store.getState().inputs.global.queries[0].isInspected).toBe(false);
store.getState().inputs.global.queries[0].loading = true;
wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click');
@ -169,4 +200,82 @@ describe('Inspect Button', () => {
);
});
});
describe('Modal Inspect - show or hide', () => {
test('shows when request/response are complete and isInspected=true', () => {
const myState = cloneDeep(state);
const myQuery = cloneDeep(newQuery);
myQuery.inspect = {
dsl: ['a length'],
response: ['my response'],
};
myState.inputs = upsertQuery(myQuery);
myState.inputs.global.queries[0].isInspected = true;
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(true);
});
test('hides when request/response are complete and isInspected=false', () => {
const myState = cloneDeep(state);
const myQuery = cloneDeep(newQuery);
myQuery.inspect = {
dsl: ['a length'],
response: ['my response'],
};
myState.inputs = upsertQuery(myQuery);
myState.inputs.global.queries[0].isInspected = false;
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false);
});
test('hides when request is empty and isInspected=true', () => {
const myState = cloneDeep(state);
const myQuery = cloneDeep(newQuery);
myQuery.inspect = {
dsl: [],
response: ['my response'],
};
myState.inputs = upsertQuery(myQuery);
myState.inputs.global.queries[0].isInspected = true;
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false);
});
test('hides when response is empty and isInspected=true', () => {
const myState = cloneDeep(state);
const myQuery = cloneDeep(newQuery);
myQuery.inspect = {
dsl: ['my dsl'],
response: [],
};
myState.inputs = upsertQuery(myQuery);
myState.inputs.global.queries[0].isInspected = true;
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false);
});
});
});

View file

@ -7,7 +7,7 @@
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
import { omit } from 'lodash/fp';
import React, { useCallback } from 'react';
import React, { useMemo, useCallback } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { inputsSelectors, State } from '../../store';
@ -52,10 +52,10 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({
compact = false,
inputId = 'global',
inspect,
inspectIndex = 0,
isDisabled,
isInspected,
loading,
inspectIndex = 0,
multiple = false, // If multiple = true we ignore the inspectIndex and pass all requests and responses to the inspect modal
onCloseInspect,
queryId = '',
@ -63,7 +63,6 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({
setIsInspected,
title = '',
}) => {
const isShowingModal = !loading && selectedInspectIndex === inspectIndex && isInspected;
const handleClick = useCallback(() => {
setIsInspected({
id: queryId,
@ -105,6 +104,16 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({
}
}
const isShowingModal = useMemo(
() => !loading && selectedInspectIndex === inspectIndex && isInspected,
[inspectIndex, isInspected, loading, selectedInspectIndex]
);
const isButtonDisabled = useMemo(
() => loading || isDisabled || request == null || response == null,
[isDisabled, loading, request, response]
);
return (
<>
{inputId === 'timeline' && !compact && (
@ -115,7 +124,7 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({
color="text"
iconSide="left"
iconType="inspect"
isDisabled={loading || isDisabled || false}
isDisabled={isButtonDisabled}
isLoading={loading}
onClick={handleClick}
>
@ -129,21 +138,23 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
isDisabled={loading || isDisabled || false}
isDisabled={isButtonDisabled}
title={i18n.INSPECT}
onClick={handleClick}
/>
)}
<ModalInspectQuery
closeModal={handleCloseModal}
isShowing={isShowingModal}
request={request}
response={response}
additionalRequests={additionalRequests}
additionalResponses={additionalResponses}
title={title}
data-test-subj="inspect-modal"
/>
{isShowingModal && request !== null && response !== null && (
<ModalInspectQuery
additionalRequests={additionalRequests}
additionalResponses={additionalResponses}
closeModal={handleCloseModal}
data-test-subj="inspect-modal"
inputId={inputId}
request={request}
response={response}
title={title}
/>
)}
</>
);
};

View file

@ -7,103 +7,50 @@
import { mount } from 'enzyme';
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { TestProviders } from '../../mock';
import { NO_ALERT_INDEX } from '../../../../common/constants';
import { ModalInspectQuery, formatIndexPatternRequested } from './modal';
import { getMockTheme } from '../../lib/kibana/kibana_react.mock';
import { InputsModelId } from '../../store/inputs/constants';
import { EXCLUDE_ELASTIC_CLOUD_INDEX } from '../../containers/sourcerer';
const mockTheme = getMockTheme({
eui: {
euiBreakpoints: {
l: '1200px',
},
},
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
return {
...original,
useLocation: jest.fn().mockReturnValue([{ pathname: '/overview' }]),
};
});
const request =
'{"index": ["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}';
const getRequest = (
indices: string[] = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']
) =>
`{"index": ${JSON.stringify(
indices
)},"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}`;
const request = getRequest();
const response =
'{"took": 880,"timed_out": false,"_shards": {"total": 26,"successful": 26,"skipped": 0,"failed": 0},"hits": {"max_score": null,"hits": []},"aggregations": {"hosts": {"value": 541},"hosts_histogram": {"buckets": [{"key_as_string": "2019 - 07 - 05T01: 00: 00.000Z", "key": 1562288400000, "doc_count": 1492321, "count": { "value": 105 }}, {"key_as_string": "2019 - 07 - 05T13: 00: 00.000Z", "key": 1562331600000, "doc_count": 2412761, "count": { "value": 453}},{"key_as_string": "2019 - 07 - 06T01: 00: 00.000Z", "key": 1562374800000, "doc_count": 111658, "count": { "value": 15}}],"interval": "12h"}},"status": 200}';
describe('Modal Inspect', () => {
const closeModal = jest.fn();
describe('rendering', () => {
test('when isShowing is positive and request and response are not null', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ModalInspectQuery
closeModal={closeModal}
isShowing={true}
request={request}
response={response}
title="My title"
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe(true);
expect(wrapper.find('.euiModalHeader__title').first().text()).toBe('Inspect My title');
});
test('when isShowing is negative and request and response are not null', () => {
const wrapper = mount(
<ModalInspectQuery
closeModal={closeModal}
isShowing={false}
request={request}
response={response}
title="My title"
/>
);
expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe(
false
);
});
test('when isShowing is positive and request is null and response is not null', () => {
const wrapper = mount(
<ModalInspectQuery
closeModal={closeModal}
isShowing={true}
request={null}
response={response}
title="My title"
/>
);
expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe(
false
);
});
test('when isShowing is positive and request is not null and response is null', () => {
const wrapper = mount(
<ModalInspectQuery
closeModal={closeModal}
isShowing={true}
request={request}
response={null}
title="My title"
/>
);
expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe(
false
);
});
});
const defaultProps = {
closeModal,
inputId: 'timeline' as InputsModelId,
request,
response,
title: 'My title',
};
describe('functionality from tab statistics/request/response', () => {
test('Click on statistic Tab', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ModalInspectQuery
closeModal={closeModal}
isShowing={true}
request={request}
response={response}
title="My title"
/>
</ThemeProvider>
<TestProviders>
<ModalInspectQuery {...defaultProps} />
</TestProviders>
);
wrapper.find('.euiTab').first().simulate('click');
@ -134,15 +81,9 @@ describe('Modal Inspect', () => {
test('Click on request Tab', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ModalInspectQuery
closeModal={closeModal}
isShowing={true}
request={request}
response={response}
title="My title"
/>
</ThemeProvider>
<TestProviders>
<ModalInspectQuery {...defaultProps} />
</TestProviders>
);
wrapper.find('.euiTab').at(2).simulate('click');
@ -201,15 +142,9 @@ describe('Modal Inspect', () => {
test('Click on response Tab', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ModalInspectQuery
closeModal={closeModal}
isShowing={true}
request={request}
response={response}
title="My title"
/>
</ThemeProvider>
<TestProviders>
<ModalInspectQuery {...defaultProps} />
</TestProviders>
);
wrapper.find('.euiTab').at(1).simulate('click');
@ -237,15 +172,9 @@ describe('Modal Inspect', () => {
describe('events', () => {
test('Make sure that toggle function has been called when you click on the close button', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<ModalInspectQuery
closeModal={closeModal}
isShowing={true}
request={request}
response={response}
title="My title"
/>
</ThemeProvider>
<TestProviders>
<ModalInspectQuery {...defaultProps} />
</TestProviders>
);
wrapper.find('button[data-test-subj="modal-inspect-close"]').simulate('click');
@ -280,4 +209,37 @@ describe('Modal Inspect', () => {
expect(expected).toEqual('Sorry about that, something went wrong.');
});
});
describe('index pattern messaging', () => {
test('no messaging when all patterns are in sourcerer selection', () => {
const wrapper = mount(
<TestProviders>
<ModalInspectQuery {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(false);
expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(false);
});
test('not-sourcerer-msg when not all patterns are in sourcerer selection', () => {
const wrapper = mount(
<TestProviders>
<ModalInspectQuery {...defaultProps} request={getRequest(['differentbeat-*'])} />
</TestProviders>
);
expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(true);
expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(false);
});
test('exclude-logs-msg when EXCLUDE_ELASTIC_CLOUD_INDEX is present in patterns', () => {
const wrapper = mount(
<TestProviders>
<ModalInspectQuery
{...defaultProps}
request={getRequest([EXCLUDE_ELASTIC_CLOUD_INDEX, 'logs-*'])}
/>
</TestProviders>
);
expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(false);
expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(true);
});
});
});

View file

@ -19,11 +19,19 @@ import {
EuiTabbedContent,
} from '@elastic/eui';
import numeral from '@elastic/numeral';
import React, { Fragment, ReactNode } from 'react';
import React, { useMemo, Fragment, ReactNode } from 'react';
import styled from 'styled-components';
import { useLocation } from 'react-router-dom';
import { NO_ALERT_INDEX } from '../../../../common/constants';
import * as i18n from './translations';
import {
EXCLUDE_ELASTIC_CLOUD_INDEX,
getScopeFromPath,
useSourcererDataView,
} from '../../containers/sourcerer';
import { InputsModelId } from '../../store/inputs/constants';
import { SourcererScopeName } from '../../store/sourcerer/model';
const DescriptionListStyled = styled(EuiDescriptionList)`
@media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.s}) {
@ -40,12 +48,12 @@ const DescriptionListStyled = styled(EuiDescriptionList)`
DescriptionListStyled.displayName = 'DescriptionListStyled';
interface ModalInspectProps {
closeModal: () => void;
isShowing: boolean;
request: string | null;
response: string | null;
additionalRequests?: string[] | null;
additionalResponses?: string[] | null;
closeModal: () => void;
inputId?: InputsModelId;
request: string;
response: string;
title: string | React.ReactElement | React.ReactNode;
}
@ -101,18 +109,18 @@ export const formatIndexPatternRequested = (indices: string[] = []) => {
};
export const ModalInspectQuery = ({
closeModal,
isShowing = false,
request,
response,
additionalRequests,
additionalResponses,
closeModal,
inputId,
request,
response,
title,
}: ModalInspectProps) => {
if (!isShowing || request == null || response == null) {
return null;
}
const { pathname } = useLocation();
const { selectedPatterns } = useSourcererDataView(
inputId === 'timeline' ? SourcererScopeName.timeline : getScopeFromPath(pathname)
);
const requests: string[] = [request, ...(additionalRequests != null ? additionalRequests : [])];
const responses: string[] = [
response,
@ -122,6 +130,16 @@ export const ModalInspectQuery = ({
const inspectRequests: Request[] = parseInspectStrings(requests);
const inspectResponses: Response[] = parseInspectStrings(responses);
const isSourcererPattern = useMemo(
() => (inspectRequests[0]?.index ?? []).every((pattern) => selectedPatterns.includes(pattern)),
[inspectRequests, selectedPatterns]
);
const isLogsExclude = useMemo(
() => (inspectRequests[0]?.index ?? []).includes(EXCLUDE_ELASTIC_CLOUD_INDEX),
[inspectRequests]
);
const statistics: Array<{
title: NonNullable<ReactNode | string>;
description: NonNullable<ReactNode | string>;
@ -135,7 +153,22 @@ export const ModalInspectQuery = ({
),
description: (
<span data-test-subj="index-pattern-description">
{formatIndexPatternRequested(inspectRequests[0]?.index ?? [])}
<p>{formatIndexPatternRequested(inspectRequests[0]?.index ?? [])}</p>
{!isSourcererPattern && (
<p>
<small>
<i data-test-subj="not-sourcerer-msg">{i18n.INSPECT_PATTERN_DIFFERENT}</i>
</small>
</p>
)}
{isLogsExclude && (
<p>
<small>
<i data-test-subj="exclude-logs-msg">{i18n.LOGS_EXCLUDE_MESSAGE}</i>
</small>
</p>
)}
</span>
),
},

View file

@ -36,6 +36,21 @@ export const INDEX_PATTERN_DESC = i18n.translate(
}
);
export const INSPECT_PATTERN_DIFFERENT = i18n.translate(
'xpack.securitySolution.inspectPatternDifferent',
{
defaultMessage: 'This element has a unique index pattern separate from the data view setting.',
}
);
export const LOGS_EXCLUDE_MESSAGE = i18n.translate(
'xpack.securitySolution.inspectPatternExcludeLogs',
{
defaultMessage:
'When the logs-* index pattern is selected, Elastic cloud logs are excluded from the search.',
}
);
export const QUERY_TIME = i18n.translate('xpack.securitySolution.inspect.modal.queryTimeLabel', {
defaultMessage: 'Query time',
});