[Security Solution] Fixes a bug with timeline sourcerer state (#143237)

This commit is contained in:
Steph Milovic 2022-10-17 15:25:55 -06:00 committed by GitHub
parent 35e8170a5a
commit 8fba39c2da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 307 additions and 56 deletions

View file

@ -195,11 +195,7 @@ export const usePickIndexPatterns = ({
// constantly getting destroy and re-init
const pickedDataViewData = await getSourcererDataView(newSelectedDataViewId);
if (isHookAlive.current) {
dispatch(
sourcererActions.updateSourcererDataViews({
dataView: pickedDataViewData,
})
);
dispatch(sourcererActions.setDataView(pickedDataViewData));
setSelectedOptions(
isOnlyDetectionAlerts
? alertsOptions

View file

@ -33,6 +33,7 @@ export type IndexFieldSearch = (param: {
scopeId?: SourcererScopeName;
needToBeInit?: boolean;
cleanCache?: boolean;
skipScopeUpdate?: boolean;
}) => Promise<void>;
type DangerCastForBrowserFieldsMutation = Record<
@ -102,6 +103,7 @@ export const useDataView = (): {
scopeId = SourcererScopeName.default,
needToBeInit = false,
cleanCache = false,
skipScopeUpdate = false,
}) => {
const unsubscribe = () => {
searchSubscription$.current[dataViewId]?.unsubscribe();
@ -123,11 +125,7 @@ export const useDataView = (): {
dataViewId,
abortCtrl.current[dataViewId].signal
);
dispatch(
sourcererActions.updateSourcererDataViews({
dataView: dataViewToUpdate,
})
);
dispatch(sourcererActions.setDataView(dataViewToUpdate));
}
return new Promise<void>((resolve) => {
@ -148,7 +146,7 @@ export const useDataView = (): {
endTracking('success');
const patternString = response.indicesExist.sort().join();
if (needToBeInit && scopeId) {
if (needToBeInit && scopeId && !skipScopeUpdate) {
dispatch(
sourcererActions.setSelectedDataView({
id: scopeId,

View file

@ -34,6 +34,7 @@ import {
import type { SelectedDataView } from '../../store/sourcerer/model';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { postSourcererDataView } from './api';
import * as source from '../source/use_data_view';
import { sourcererActions } from '../../store/sourcerer';
import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string';
@ -200,7 +201,7 @@ describe('Sourcerer Hooks', () => {
});
});
it('initilizes dataview with data from query string', async () => {
it('initializes dataview with data from query string', async () => {
const selectedPatterns = ['testPattern-*'];
const selectedDataViewId = 'security-solution-default';
(useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) =>
@ -342,6 +343,198 @@ describe('Sourcerer Hooks', () => {
});
});
});
describe('initialization settings', () => {
const mockIndexFieldsSearch = jest.fn();
beforeAll(() => {
// 👇️ not using dot-notation + the ignore clears up a ts error
// @ts-ignore
// eslint-disable-next-line dot-notation
source['useDataView'] = jest.fn(() => ({
indexFieldsSearch: mockIndexFieldsSearch,
}));
});
it('does not needToBeInit if scope is default and selectedPatterns/missingPatterns have values', async () => {
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
await waitFor(() => {
expect(mockIndexFieldsSearch).toHaveBeenCalledWith({
dataViewId: mockSourcererState.defaultDataView.id,
needToBeInit: false,
scopeId: SourcererScopeName.default,
});
});
});
});
it('does needToBeInit if scope is default and selectedPatterns/missingPatterns are empty', async () => {
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
selectedPatterns: [],
missingPatterns: [],
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
await waitFor(() => {
expect(mockIndexFieldsSearch).toHaveBeenCalledWith({
dataViewId: mockSourcererState.defaultDataView.id,
needToBeInit: true,
scopeId: SourcererScopeName.default,
});
});
});
});
it('does needToBeInit and skipScopeUpdate=false if scope is timeline and selectedPatterns/missingPatterns are empty', async () => {
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
...mockGlobalState.sourcerer.kibanaDataViews,
{ ...mockSourcererState.defaultDataView, id: 'something-weird', patternList: [] },
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: 'something-weird',
selectedPatterns: [],
missingPatterns: [],
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
await waitFor(() => {
expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, {
dataViewId: 'something-weird',
needToBeInit: true,
scopeId: SourcererScopeName.timeline,
skipScopeUpdate: false,
});
});
});
});
it('does needToBeInit and skipScopeUpdate=true if scope is timeline and selectedPatterns have value', async () => {
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
...mockGlobalState.sourcerer.kibanaDataViews,
{ ...mockSourcererState.defaultDataView, id: 'something-weird', patternList: [] },
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: 'something-weird',
selectedPatterns: ['ohboy'],
missingPatterns: [],
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
await waitFor(() => {
expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, {
dataViewId: 'something-weird',
needToBeInit: true,
scopeId: SourcererScopeName.timeline,
skipScopeUpdate: true,
});
});
});
});
it('does not needToBeInit if scope is timeline and data view has patternList', async () => {
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
...mockGlobalState.sourcerer.kibanaDataViews,
{
...mockSourcererState.defaultDataView,
id: 'something-weird',
patternList: ['ohboy'],
},
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: 'something-weird',
selectedPatterns: [],
missingPatterns: [],
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
await waitFor(() => {
expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, {
dataViewId: 'something-weird',
needToBeInit: false,
scopeId: SourcererScopeName.timeline,
});
});
});
});
});
describe('useSourcererDataView', () => {
it('Should put any excludes in the index pattern at the end of the pattern list, and sort both the includes and excludes', async () => {

View file

@ -76,17 +76,21 @@ export const useInitSourcerer = (
const activeTimeline = useDeepEqualSelector((state) =>
getTimelineSelector(state, TimelineId.active)
);
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []);
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
const {
selectedDataViewId: scopeDataViewId,
selectedPatterns,
missingPatterns,
} = useDeepEqualSelector((state) => scopeIdSelector(state, scopeId));
sourcererScope: { selectedDataViewId: scopeDataViewId, selectedPatterns, missingPatterns },
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId));
const {
selectedDataViewId: timelineDataViewId,
selectedPatterns: timelineSelectedPatterns,
missingPatterns: timelineMissingPatterns,
} = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline));
selectedDataView: timelineSelectedDataView,
sourcererScope: {
selectedDataViewId: timelineDataViewId,
selectedPatterns: timelineSelectedPatterns,
missingPatterns: timelineMissingPatterns,
},
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, SourcererScopeName.timeline));
const { indexFieldsSearch } = useDataView();
const onInitializeUrlParam = useCallback(
@ -137,19 +141,29 @@ export const useInitSourcerer = (
const searchedIds = useRef<string[]>([]);
useEffect(() => {
const activeDataViewIds = [...new Set([scopeDataViewId, timelineDataViewId])];
activeDataViewIds.forEach((id) => {
activeDataViewIds.forEach((id, i) => {
if (id != null && id.length > 0 && !searchedIds.current.includes(id)) {
searchedIds.current = [...searchedIds.current, id];
const currentScope = i === 0 ? SourcererScopeName.default : SourcererScopeName.timeline;
const needToBeInit =
id === scopeDataViewId
? selectedPatterns.length === 0 && missingPatterns.length === 0
: timelineDataViewId === id
? timelineMissingPatterns.length === 0 &&
timelineSelectedDataView?.patternList.length === 0
: false;
indexFieldsSearch({
dataViewId: id,
scopeId:
id === scopeDataViewId ? SourcererScopeName.default : SourcererScopeName.timeline,
needToBeInit:
id === scopeDataViewId
? selectedPatterns.length === 0 && missingPatterns.length === 0
: timelineDataViewId === id
? timelineMissingPatterns.length === 0 && timelineSelectedPatterns.length === 0
: false,
scopeId: currentScope,
needToBeInit,
...(needToBeInit && currentScope === SourcererScopeName.timeline
? {
skipScopeUpdate: timelineSelectedPatterns.length > 0,
}
: {}),
});
}
});
@ -160,6 +174,7 @@ export const useInitSourcerer = (
selectedPatterns.length,
timelineDataViewId,
timelineMissingPatterns.length,
timelineSelectedDataView,
timelineSelectedPatterns.length,
]);

View file

@ -7,23 +7,12 @@
import actionCreatorFactory from 'typescript-fsa';
import type {
KibanaDataView,
SelectedDataView,
SourcererDataView,
SourcererScopeName,
} from './model';
import type { SelectedDataView, SourcererDataView, SourcererScopeName } from './model';
import type { SecurityDataView } from '../../containers/sourcerer/api';
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer');
export const setDataView = actionCreator<{
browserFields: SourcererDataView['browserFields'];
id: SourcererDataView['id'];
indexFields: SourcererDataView['indexFields'];
loading: SourcererDataView['loading'];
runtimeMappings: SourcererDataView['runtimeMappings'];
}>('SET_DATA_VIEW');
export const setDataView = actionCreator<Partial<SourcererDataView>>('SET_DATA_VIEW');
export const setDataViewLoading = actionCreator<{
id: string;
@ -48,7 +37,3 @@ export interface SelectedDataViewPayload {
shouldValidateSelectedPatterns?: boolean;
}
export const setSelectedDataView = actionCreator<SelectedDataViewPayload>('SET_SELECTED_DATA_VIEW');
export const updateSourcererDataViews = actionCreator<{
dataView: KibanaDataView;
}>('UPDATE_SOURCERER_DATA_VIEWS');

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { mockGlobalState } from '../../mock';
import { mockGlobalState, mockSourcererState } from '../../mock';
import { SourcererScopeName } from './model';
import { getScopePatternListSelection, validateSelectedPatterns } from './helpers';
import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer';
@ -210,5 +210,71 @@ describe('sourcerer store helpers', () => {
});
});
});
it('does not attempt to validate when missing patterns', () => {
const state = {
...mockGlobalState.sourcerer,
defaultDataView: {
...mockSourcererState.defaultDataView,
patternList: [],
},
kibanaDataViews: [
{
...mockSourcererState.defaultDataView,
patternList: [],
},
],
};
const result = validateSelectedPatterns(
state,
{
...payload,
id: SourcererScopeName.default,
selectedPatterns: ['auditbeat-*', 'yoohoo'],
},
true
);
expect(result).toEqual({
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
missingPatterns: ['yoohoo'],
selectedPatterns: ['auditbeat-*', 'yoohoo'],
},
});
});
it('does not attempt to validate if non-default data view has not been initialized', () => {
const state = {
...mockGlobalState.sourcerer,
defaultDataView: {
...mockSourcererState.defaultDataView,
patternList: [],
},
kibanaDataViews: [
{
...mockSourcererState.defaultDataView,
id: 'wow',
patternList: [],
},
],
};
const result = validateSelectedPatterns(
state,
{
...payload,
id: SourcererScopeName.default,
selectedDataViewId: 'wow',
selectedPatterns: ['auditbeat-*', 'yoohoo'],
},
true
);
expect(result).toEqual({
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
selectedDataViewId: 'wow',
selectedPatterns: ['auditbeat-*', 'yoohoo'],
},
});
});
});
});

View file

@ -58,7 +58,12 @@ export const validateSelectedPatterns = (
const selectedPatterns =
// shouldValidateSelectedPatterns is false when upgrading from
// legacy pre-8.0 timeline index patterns to data view.
shouldValidateSelectedPatterns && dataView != null && missingPatterns.length === 0
shouldValidateSelectedPatterns &&
dataView != null &&
missingPatterns.length === 0 &&
// don't validate when the data view has not been initialized (default is initialized already always)
dataView.id !== state.defaultDataView.id &&
dataView.patternList.length > 0
? dedupePatterns.filter(
(pattern) =>
(dataView != null && dataView.patternList.includes(pattern)) ||

View file

@ -14,7 +14,6 @@ import {
setSignalIndexName,
setDataView,
setDataViewLoading,
updateSourcererDataViews,
} from './actions';
import type { SourcererModel } from './model';
import { initDataView, initialSourcererState, SourcererScopeName } from './model';
@ -47,12 +46,6 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState)
...dataView,
})),
}))
.case(updateSourcererDataViews, (state, { dataView }) => ({
...state,
kibanaDataViews: state.kibanaDataViews.map((dv) =>
dv.id === dataView.id ? { ...dv, ...dataView } : dv
),
}))
.case(setSourcererScopeLoading, (state, { id, loading }) => ({
...state,
sourcererScopes: {