[Security Solution] Use sourcerer selected indices in resolver (#90727)

* Use sourcer indices

* Add indices to panel requests

* Use a separate indices selector for resolver events

* Use valid timeline id in tests

* Update TimelineId type usage, make selector test clearer

* Update tests to use TimelineId type
This commit is contained in:
Kevin Qualters 2021-02-10 11:15:21 -05:00 committed by GitHub
parent 874960e1c5
commit f563a82903
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 172 additions and 45 deletions

View file

@ -280,6 +280,7 @@ export enum TimelineId {
active = 'timeline-1',
casePage = 'timeline-case',
test = 'test', // Reserved for testing purposes
test2 = 'test2',
}
export const TimelineIdLiteralRt = runtimeTypes.union([

View file

@ -119,7 +119,7 @@ describe('EventsViewer', () => {
let testProps = {
defaultModel: eventsDefaultModel,
end: to,
id: 'test-stateful-events-viewer',
id: TimelineId.test,
start: from,
scopeId: SourcererScopeName.timeline,
};
@ -155,7 +155,7 @@ describe('EventsViewer', () => {
indexName: 'auditbeat-7.10.1-2020.12.18-000001',
},
tabType: 'query',
timelineId: 'test-stateful-events-viewer',
timelineId: TimelineId.test,
},
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
});
@ -199,17 +199,22 @@ describe('EventsViewer', () => {
defaultHeaders.forEach((header) => {
test(`it renders the ${header.id} default EventsViewer column header`, () => {
testProps = {
...testProps,
// Update with a new id, to force columns back to default.
id: TimelineId.test2,
};
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
defaultHeaders.forEach((h) =>
defaultHeaders.forEach((h) => {
expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe(
true
)
);
);
});
});
});
});

View file

@ -117,7 +117,7 @@ interface Props {
filters: Filter[];
headerFilterGroup?: React.ReactNode;
height?: number;
id: string;
id: TimelineId;
indexNames: string[];
indexPattern: IIndexPattern;
isLive: boolean;

View file

@ -16,6 +16,7 @@ import { useMountAppended } from '../../utils/use_mount_appended';
import { mockEventViewerResponse } from './mock';
import { StatefulEventsViewer } from '.';
import { eventsDefaultModel } from './default_model';
import { TimelineId } from '../../../../common/types/timeline';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useTimelineEvents } from '../../../timelines/containers';
@ -36,7 +37,7 @@ const testProps = {
defaultModel: eventsDefaultModel,
end: to,
indexNames: [],
id: 'test-stateful-events-viewer',
id: TimelineId.test,
scopeId: SourcererScopeName.default,
start: from,
};

View file

@ -12,6 +12,7 @@ import styled from 'styled-components';
import { inputsModel, inputsSelectors, State } from '../../store';
import { inputsActions } from '../../store/actions';
import { TimelineId } from '../../../../common/types/timeline';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
import { Filter } from '../../../../../../../src/plugins/data/public';
@ -34,7 +35,7 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
export interface OwnProps {
defaultModel: SubsetTimelineModel;
end: string;
id: string;
id: TimelineId;
scopeId: SourcererScopeName;
start: string;
headerFilterGroup?: React.ReactNode;

View file

@ -19,6 +19,7 @@ const initialState: DataState = {
data: null,
},
resolverComponentInstanceID: undefined,
indices: [],
};
/* eslint-disable complexity */
export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState, action) => {
@ -35,6 +36,7 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
},
resolverComponentInstanceID: action.payload.resolverComponentInstanceID,
locationSearch: action.payload.locationSearch,
indices: action.payload.indices,
};
const panelViewAndParameters = selectors.panelViewAndParameters(nextState);
return {

View file

@ -664,4 +664,85 @@ describe('data state', () => {
`);
});
});
describe('when the resolver tree response is complete, still use non-default indices', () => {
beforeEach(() => {
const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({
originID: 'a',
firstChildID: 'b',
secondChildID: 'c',
});
const { schema, dataSource } = endpointSourceSchema();
actions = [
{
type: 'serverReturnedResolverData',
payload: {
result: resolverTree,
dataSource,
schema,
parameters: {
databaseDocumentID: '',
indices: ['someNonDefaultIndex'],
filters: {},
},
},
},
];
});
it('should have an empty array for tree parameter indices, and a non empty array for event indices', () => {
const treeParameterIndices = selectors.treeParameterIndices(state());
expect(treeParameterIndices.length).toBe(0);
const eventIndices = selectors.eventIndices(state());
expect(eventIndices.length).toBe(1);
});
});
describe('when the resolver tree response is pending use the same indices the user is currently looking at data from', () => {
beforeEach(() => {
const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({
originID: 'a',
firstChildID: 'b',
secondChildID: 'c',
});
const { schema, dataSource } = endpointSourceSchema();
actions = [
{
type: 'serverReturnedResolverData',
payload: {
result: resolverTree,
dataSource,
schema,
parameters: {
databaseDocumentID: '',
indices: ['defaultIndex'],
filters: {},
},
},
},
{
type: 'appReceivedNewExternalProperties',
payload: {
databaseDocumentID: '',
resolverComponentInstanceID: '',
locationSearch: '',
indices: ['someNonDefaultIndex', 'someOtherIndex'],
shouldUpdate: false,
filters: {},
},
},
{
type: 'appRequestedResolverData',
payload: {
databaseDocumentID: '',
indices: ['someNonDefaultIndex', 'someOtherIndex'],
filters: {},
},
},
];
});
it('should have an empty array for tree parameter indices, and the same set of indices as the last tree response', () => {
const treeParameterIndices = selectors.treeParameterIndices(state());
expect(treeParameterIndices.length).toBe(0);
const eventIndices = selectors.eventIndices(state());
expect(eventIndices.length).toBe(1);
});
});
});

View file

@ -63,6 +63,13 @@ export function resolverComponentInstanceID(state: DataState): string {
return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : '';
}
/**
* The indices resolver should use, passed in as external props.
*/
const currentIndices = (state: DataState): string[] => {
return state.indices;
};
/**
* The last NewResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that
* we're currently interested in.
@ -71,6 +78,12 @@ const resolverTreeResponse = (state: DataState): NewResolverTree | undefined =>
return state.tree?.lastResponse?.successful ? state.tree?.lastResponse.result : undefined;
};
const lastResponseIndices = (state: DataState): string[] | undefined => {
return state.tree?.lastResponse?.successful
? state.tree?.lastResponse?.parameters?.indices
: undefined;
};
/**
* If we received a NewResolverTree, return the schema associated with that tree, otherwise return undefined.
* As of writing, this is only used for the info popover in the graph_controls panel
@ -336,10 +349,22 @@ export const timeRangeFilters = createSelector(
/**
* The indices to use for the requests with the backend.
*/
export const treeParamterIndices = createSelector(treeParametersToFetch, (parameters) => {
export const treeParameterIndices = createSelector(treeParametersToFetch, (parameters) => {
return parameters?.indices ?? [];
});
/**
* Panel requests should not use indices derived from the tree parameter selector, as this is only defined briefly while the resolver_tree_fetcher middleware is running.
* Instead, panel requests should use the indices used by the last good request, falling back to the indices passed as external props.
*/
export const eventIndices = createSelector(
lastResponseIndices,
currentIndices,
function eventIndices(lastIndices, current): string[] {
return lastIndices ?? current ?? [];
}
);
export const layout: (state: DataState) => IsometricTaxiLayout = createSelector(
tree,
originID,

View file

@ -32,7 +32,7 @@ export function CurrentRelatedEventFetcher(
const state = api.getState();
const newParams = selectors.panelViewAndParameters(state);
const indices = selectors.treeParameterIndices(state);
const indices = selectors.eventIndices(state);
const oldParams = last;
last = newParams;

View file

@ -38,7 +38,7 @@ export function NodeDataFetcher(
* This gets the visible nodes that we haven't already requested or received data for
*/
const newIDsToRequest: Set<string> = selectors.newIDsToRequest(state)(Number.POSITIVE_INFINITY);
const indices = selectors.treeParameterIndices(state);
const indices = selectors.eventIndices(state);
if (newIDsToRequest.size <= 0) {
return;

View file

@ -27,7 +27,7 @@ export function RelatedEventsFetcher(
const newParams = selectors.panelViewAndParameters(state);
const isLoadingMoreEvents = selectors.isLoadingMoreNodeEventsInCategory(state);
const indices = selectors.treeParameterIndices(state);
const indices = selectors.eventIndices(state);
const oldParams = last;
const timeRangeFilters = selectors.timeRangeFilters(state);

View file

@ -80,9 +80,14 @@ export const treeRequestParametersToAbort = composeSelectors(
*/
export const treeParameterIndices = composeSelectors(
dataStateSelector,
dataSelectors.treeParamterIndices
dataSelectors.treeParameterIndices
);
/**
* An array of indices to use for resolver panel requests.
*/
export const eventIndices = composeSelectors(dataStateSelector, dataSelectors.eventIndices);
export const resolverComponentInstanceID = composeSelectors(
dataStateSelector,
dataSelectors.resolverComponentInstanceID

View file

@ -376,6 +376,8 @@ export interface DataState {
*/
readonly resolverComponentInstanceID?: string;
readonly indices: string[];
/**
* The `search` part of the URL.
*/

View file

@ -18,6 +18,7 @@ import {
kibanaObservable,
createSecuritySolutionStorageMock,
} from '../../../common/mock';
import { TimelineId } from '../../../../common/types/timeline';
import { createStore, State } from '../../../common/store';
import * as timelineActions from '../../store/timeline/actions';
@ -43,7 +44,7 @@ describe('Flyout', () => {
const { storage } = createSecuritySolutionStorageMock();
const props = {
onAppLeave: jest.fn(),
timelineId: 'test',
timelineId: TimelineId.test,
};
beforeEach(() => {

View file

@ -26,7 +26,7 @@ const Visible = styled.div<{ show?: boolean }>`
Visible.displayName = 'Visible';
interface OwnProps {
timelineId: string;
timelineId: TimelineId;
onAppLeave: (handler: AppLeaveHandler) => void;
}

View file

@ -9,13 +9,14 @@ import { shallow } from 'enzyme';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { TimelineId } from '../../../../../common/types/timeline';
import { Pane } from '.';
describe('Pane', () => {
test('renders correctly against snapshot', () => {
const EmptyComponent = shallow(
<TestProviders>
<Pane timelineId={'test'} />
<Pane timelineId={TimelineId.test} />
</TestProviders>
);
expect(EmptyComponent.find('Pane')).toMatchSnapshot();

View file

@ -11,12 +11,13 @@ import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { StatefulTimeline } from '../../timeline';
import { TimelineId } from '../../../../../common/types/timeline';
import * as i18n from './translations';
import { timelineActions } from '../../../store/timeline';
import { focusActiveTimelineButton } from '../../timeline/helpers';
interface FlyoutPaneComponentProps {
timelineId: string;
timelineId: TimelineId;
}
const EuiFlyoutContainer = styled.div`

View file

@ -15,7 +15,6 @@ import {
} from '../../../common/containers/use_full_screen';
import { mockTimelineModel, TestProviders } from '../../../common/mock';
import { TimelineId } from '../../../../common/types/timeline';
import { GraphOverlay } from '.';
jest.mock('../../../common/hooks/use_selector', () => ({
@ -28,6 +27,10 @@ jest.mock('../../../common/containers/use_full_screen', () => ({
useTimelineFullScreen: jest.fn(),
}));
jest.mock('../../../resolver/view/use_resolver_query_params_cleaner');
jest.mock('../../../resolver/view/use_state_syncing_actions');
jest.mock('../../../resolver/view/use_sync_selected_node');
describe('GraphOverlay', () => {
beforeEach(() => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
@ -42,12 +45,11 @@ describe('GraphOverlay', () => {
describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => {
const isEventViewer = true;
const timelineId = 'used-as-an-events-viewer';
test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => {
const wrapper = mount(
<TestProviders>
<GraphOverlay timelineId={timelineId} isEventViewer={isEventViewer} />
<GraphOverlay timelineId={TimelineId.test} isEventViewer={isEventViewer} />
</TestProviders>
);
@ -69,7 +71,7 @@ describe('GraphOverlay', () => {
const wrapper = mount(
<TestProviders>
<GraphOverlay timelineId={timelineId} isEventViewer={isEventViewer} />
<GraphOverlay timelineId={TimelineId.test} isEventViewer={isEventViewer} />
</TestProviders>
);

View file

@ -20,7 +20,7 @@ import styled from 'styled-components';
import { FULL_SCREEN } from '../timeline/body/column_headers/translations';
import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations';
import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
import {
useGlobalFullScreen,
useTimelineFullScreen,
@ -30,6 +30,8 @@ import { TimelineId } from '../../../../common/types/timeline';
import { timelineSelectors } from '../../store/timeline';
import { timelineDefaults } from '../../store/timeline/defaults';
import { isFullScreen } from '../timeline/body/column_headers';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions';
import { Resolver } from '../../../resolver/view';
import {
@ -38,8 +40,6 @@ import {
endSelector,
} from '../../../common/components/super_date_picker/selectors';
import * as i18n from './translations';
import { useUiSetting$ } from '../../../common/lib/kibana';
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
const OverlayContainer = styled.div`
${({ $restrictWidth }: { $restrictWidth: boolean }) =>
@ -61,14 +61,14 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)`
interface OwnProps {
isEventViewer: boolean;
timelineId: string;
timelineId: TimelineId;
}
interface NavigationProps {
fullScreen: boolean;
globalFullScreen: boolean;
onCloseOverlay: () => void;
timelineId: string;
timelineId: TimelineId;
timelineFullScreen: boolean;
toggleFullScreen: () => void;
}
@ -169,16 +169,14 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ isEventViewer, timelineId }
globalFullScreen,
]);
const { signalIndexName } = useSignalIndex();
const [siemDefaultIndices] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const indices: string[] | null = useMemo(() => {
if (signalIndexName === null) {
return null;
} else {
return [...siemDefaultIndices, signalIndexName];
}
}, [signalIndexName, siemDefaultIndices]);
let sourcereScope = SourcererScopeName.default;
if ([TimelineId.detectionsRulesDetailsPage, TimelineId.detectionsPage].includes(timelineId)) {
sourcereScope = SourcererScopeName.detections;
} else if (timelineId === TimelineId.active) {
sourcereScope = SourcererScopeName.timeline;
}
const { selectedPatterns } = useSourcererScope(sourcereScope);
return (
<OverlayContainer
data-test-subj="overlayContainer"
@ -198,11 +196,11 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ isEventViewer, timelineId }
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="none" />
{graphEventId !== undefined && indices !== null ? (
{graphEventId !== undefined ? (
<StyledResolver
databaseDocumentID={graphEventId}
resolverComponentInstanceID={timelineId}
indices={indices}
indices={selectedPatterns}
shouldUpdate={shouldUpdate}
filters={{ from, to }}
/>

View file

@ -9,10 +9,11 @@ import React, { useMemo } from 'react';
import { timelineSelectors } from '../../../store/timeline';
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
import { TimelineId } from '../../../../../common/types/timeline';
import { GraphOverlay } from '../../graph_overlay';
interface GraphTabContentProps {
timelineId: string;
timelineId: TimelineId;
}
const GraphTabContentComponent: React.FC<GraphTabContentProps> = ({ timelineId }) => {

View file

@ -12,7 +12,7 @@ import useResizeObserver from 'use-resize-observer/polyfilled';
import { DragDropContextWrapper } from '../../../common/components/drag_and_drop/drag_drop_context_wrapper';
import '../../../common/mock/match_media';
import { mockBrowserFields, mockDocValueFields } from '../../../common/containers/source/mock';
import { TimelineId } from '../../../../common/types/timeline';
import { mockIndexNames, mockIndexPattern, TestProviders } from '../../../common/mock';
import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index';
@ -55,7 +55,7 @@ jest.mock('../../../common/containers/sourcerer', () => {
});
describe('StatefulTimeline', () => {
const props: StatefulTimelineOwnProps = {
timelineId: 'timeline-test',
timelineId: TimelineId.test,
};
beforeEach(() => {
@ -91,7 +91,7 @@ describe('StatefulTimeline', () => {
);
expect(
wrapper
.find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`)
.find(`[data-timeline-id="test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`)
.first()
.exists()
).toEqual(true);

View file

@ -18,7 +18,7 @@ import { isTab } from '../../../common/components/accessibility/helpers';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header';
import { TimelineType, TimelineTabs } from '../../../../common/types/timeline';
import { TimelineType, TimelineTabs, TimelineId } from '../../../../common/types/timeline';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { activeTimeline } from '../../containers/active_timeline_context';
import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers';
@ -35,7 +35,7 @@ const TimelineTemplateBadge = styled.div`
`;
export interface Props {
timelineId: string;
timelineId: TimelineId;
}
const TimelineSavingProgressComponent: React.FC<Props> = ({ timelineId }) => {

View file

@ -9,7 +9,7 @@ import { EuiBadge, EuiLoadingContent, EuiTabs, EuiTab } from '@elastic/eui';
import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { TimelineTabs } from '../../../../../common/types/timeline';
import { TimelineTabs, TimelineId } from '../../../../../common/types/timeline';
import {
useShallowEqualSelector,
@ -42,7 +42,7 @@ const NotesTabContent = lazy(() => import('../notes_tab_content'));
const PinnedTabContent = lazy(() => import('../pinned_tab_content'));
interface BasicTimelineTab {
timelineId: string;
timelineId: TimelineId;
graphEventId?: string;
}