[SECURITY SOLUTIONS] Bugs overview page + investigate eql in timeline (#81550)

* fix overview query to be connected to sourcerer

* investigate eql in timeline

* keep timeline indices

* trusting what is coming from timeline saved object for index pattern at initialization

* fix type + initialize old timeline to sourcerer

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Xavier Mouligneau 2020-10-26 20:46:32 -04:00 committed by GitHub
parent 0592938a97
commit b304051d67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 194 additions and 51 deletions

View file

@ -194,15 +194,14 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
const { data, notifications } = useKibana().services;
const abortCtrl = useRef(new AbortController());
const dispatch = useDispatch();
const previousIndexesName = useRef<string[]>([]);
const indexNamesSelectedSelector = useMemo(
() => sourcererSelectors.getIndexNamesSelectedSelector(),
[]
);
const indexNames = useShallowEqualSelector<string[]>((state) =>
indexNamesSelectedSelector(state, sourcererScopeName)
);
const { indexNames, previousIndexNames } = useShallowEqualSelector<{
indexNames: string[];
previousIndexNames: string;
}>((state) => indexNamesSelectedSelector(state, sourcererScopeName));
const setLoading = useCallback(
(loading: boolean) => {
@ -230,7 +229,6 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
if (!response.isPartial && !response.isRunning) {
if (!didCancel) {
const stringifyIndices = response.indicesExist.sort().join();
previousIndexesName.current = response.indicesExist;
dispatch(
sourcererActions.setSource({
id: sourcererScopeName,
@ -279,8 +277,8 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
);
useEffect(() => {
if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) {
if (!isEmpty(indexNames) && previousIndexNames !== indexNames.sort().join()) {
indexFieldsSearch(indexNames);
}
}, [indexNames, indexFieldsSearch, previousIndexesName]);
}, [indexNames, indexFieldsSearch, previousIndexNames]);
};

View file

@ -86,7 +86,29 @@ jest.mock('../../utils/apollo_context', () => ({
}));
describe('Sourcerer Hooks', () => {
const state: State = mockGlobalState;
const state: State = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
indexPattern: {
fields: [],
title: '',
},
},
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
indexPattern: {
fields: [],
title: '',
},
},
},
},
};
const { storage } = createSecuritySolutionStorageMock();
let store = createStore(
state,

View file

@ -16,6 +16,9 @@ import { ManageScope, SourcererScopeName } from '../../store/sourcerer/model';
import { useIndexFields } from '../source';
import { State } from '../../store';
import { useUserInfo } from '../../../detections/components/user_info';
import { timelineSelectors } from '../../../timelines/store/timeline';
import { TimelineId } from '../../../../common/types/timeline';
import { TimelineModel } from '../../../timelines/store/timeline/model';
export const useInitSourcerer = (
scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default
@ -29,6 +32,12 @@ export const useInitSourcerer = (
);
const ConfigIndexPatterns = useSelector(getConfigIndexPatternsSelector, isEqual);
const getTimelineSelector = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const activeTimeline = useSelector<State, TimelineModel>(
(state) => getTimelineSelector(state, TimelineId.active),
isEqual
);
useIndexFields(scopeId);
useIndexFields(SourcererScopeName.timeline);
@ -40,7 +49,11 @@ export const useInitSourcerer = (
// Related to timeline
useEffect(() => {
if (!loadingSignalIndex && signalIndexName != null) {
if (
!loadingSignalIndex &&
signalIndexName != null &&
(activeTimeline == null || (activeTimeline != null && activeTimeline.savedObjectId == null))
) {
dispatch(
sourcererActions.setSelectedIndexPatterns({
id: SourcererScopeName.timeline,
@ -48,7 +61,7 @@ export const useInitSourcerer = (
})
);
}
}, [ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]);
}, [activeTimeline, ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]);
// Related to the detection page
useEffect(() => {

View file

@ -34,3 +34,9 @@ export const setSelectedIndexPatterns = actionCreator<{
selectedPatterns: string[];
eventType?: TimelineEventsType;
}>('SET_SELECTED_INDEX_PATTERNS');
export const initTimelineIndexPatterns = actionCreator<{
id: SourcererScopeName;
selectedPatterns: string[];
eventType?: TimelineEventsType;
}>('INIT_TIMELINE_INDEX_PATTERNS');

View file

@ -25,16 +25,7 @@ export const createDefaultIndexPatterns = ({ eventType, id, selectedPatterns, st
if (isEmpty(newSelectedPatterns)) {
let defaultIndexPatterns = state.configIndexPatterns;
if (id === SourcererScopeName.timeline && isEmpty(newSelectedPatterns)) {
if (eventType === 'all' && !isEmpty(state.signalIndexName)) {
defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? ''];
} else if (eventType === 'raw') {
defaultIndexPatterns = state.configIndexPatterns;
} else if (
!isEmpty(state.signalIndexName) &&
(eventType === 'signal' || eventType === 'alert')
) {
defaultIndexPatterns = [state.signalIndexName ?? ''];
}
defaultIndexPatterns = defaultIndexPatternByEventType({ state, eventType });
} else if (id === SourcererScopeName.detections && isEmpty(newSelectedPatterns)) {
defaultIndexPatterns = [state.signalIndexName ?? ''];
}
@ -42,3 +33,21 @@ export const createDefaultIndexPatterns = ({ eventType, id, selectedPatterns, st
}
return newSelectedPatterns;
};
export const defaultIndexPatternByEventType = ({
state,
eventType,
}: {
state: SourcererModel;
eventType?: TimelineEventsType;
}) => {
let defaultIndexPatterns = state.configIndexPatterns;
if (eventType === 'all' && !isEmpty(state.signalIndexName)) {
defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? ''];
} else if (eventType === 'raw') {
defaultIndexPatterns = state.configIndexPatterns;
} else if (!isEmpty(state.signalIndexName) && (eventType === 'signal' || eventType === 'alert')) {
defaultIndexPatterns = [state.signalIndexName ?? ''];
}
return defaultIndexPatterns;
};

View file

@ -4,8 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
// Prefer importing entire lodash library, e.g. import { get } from "lodash"
import { isEmpty } from 'lodash/fp';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import {
@ -14,9 +13,10 @@ import {
setSelectedIndexPatterns,
setSignalIndexName,
setSource,
initTimelineIndexPatterns,
} from './actions';
import { initialSourcererState, SourcererModel } from './model';
import { createDefaultIndexPatterns } from './helpers';
import { createDefaultIndexPatterns, defaultIndexPatternByEventType } from './helpers';
export type SourcererState = SourcererModel;
@ -52,6 +52,21 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState)
},
};
})
.case(initTimelineIndexPatterns, (state, { id, selectedPatterns, eventType }) => {
return {
...state,
sourcererScopes: {
...state.sourcererScopes,
[id]: {
...state.sourcererScopes[id],
selectedPatterns: isEmpty(selectedPatterns)
? defaultIndexPatternByEventType({ state, eventType })
: selectedPatterns,
},
},
};
})
.case(setSource, (state, { id, payload }) => {
const { ...sourcererScopes } = payload;
return {

View file

@ -41,13 +41,18 @@ export const getIndexNamesSelectedSelector = () => {
const getScopesSelector = scopesSelector();
const getConfigIndexPatternsSelector = configIndexPatternsSelector();
const mapStateToProps = (state: State, scopeId: SourcererScopeName): string[] => {
const mapStateToProps = (
state: State,
scopeId: SourcererScopeName
): { indexNames: string[]; previousIndexNames: string } => {
const scope = getScopesSelector(state)[scopeId];
const configIndexPatterns = getConfigIndexPatternsSelector(state);
return scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns;
return {
indexNames:
scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns,
previousIndexNames: scope.indexPattern.title,
};
};
return mapStateToProps;
};

View file

@ -47,7 +47,9 @@ describe('alert actions', () => {
searchStrategyClient = {
aggs: {} as ISearchStart['aggs'],
showError: jest.fn(),
search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }),
search: jest
.fn()
.mockImplementation(() => ({ toPromise: () => ({ data: mockTimelineDetails }) })),
searchSource: {} as ISearchStart['searchSource'],
session: dataPluginMock.createStartContract().search.session,
};
@ -400,6 +402,78 @@ describe('alert actions', () => {
expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps);
});
});
describe('Eql', () => {
test(' with signal.group.id', async () => {
const ecsDataMock: Ecs = {
...mockEcsDataWithAlert,
signal: {
rule: {
...mockEcsDataWithAlert.signal?.rule!,
type: ['eql'],
timeline_id: [''],
},
group: {
id: ['my-group-id'],
},
},
};
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMock,
nonEcsData: [],
updateTimelineIsLoading,
searchStrategyClient,
});
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith({
...defaultTimelineProps,
timeline: {
...defaultTimelineProps.timeline,
dataProviders: [
{
and: [],
enabled: true,
excluded: false,
id:
'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-my-group-id',
kqlQuery: '',
name: '1',
queryMatch: { field: 'signal.group.id', operator: ':', value: 'my-group-id' },
},
],
},
});
});
test(' with NO signal.group.id', async () => {
const ecsDataMock: Ecs = {
...mockEcsDataWithAlert,
signal: {
rule: {
...mockEcsDataWithAlert.signal?.rule!,
type: ['eql'],
timeline_id: [''],
},
},
};
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMock,
nonEcsData: [],
updateTimelineIsLoading,
searchStrategyClient,
});
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps);
});
});
});
describe('determineToAndFrom', () => {

View file

@ -150,8 +150,10 @@ export const getThresholdAggregationDataProvider = (
];
};
export const isEqlRule = (ecsData: Ecs) =>
ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'eql';
export const isEqlRuleWithGroupId = (ecsData: Ecs) =>
ecsData.signal?.rule?.type?.length &&
ecsData.signal?.rule?.type[0] === 'eql' &&
ecsData.signal?.group?.id?.length;
export const isThresholdRule = (ecsData: Ecs) =>
ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'threshold';
@ -181,24 +183,23 @@ export const sendAlertToTimelineAction = async ({
timelineType: TimelineType.template,
},
}),
searchStrategyClient.search<
TimelineEventsDetailsRequestOptions,
TimelineEventsDetailsStrategyResponse
>(
{
defaultIndex: [],
docValueFields: [],
indexName: ecsData._index ?? '',
eventId: ecsData._id,
factoryQueryType: TimelineEventsQueries.details,
},
{
strategy: 'securitySolutionTimelineSearchStrategy',
}
),
searchStrategyClient
.search<TimelineEventsDetailsRequestOptions, TimelineEventsDetailsStrategyResponse>(
{
defaultIndex: [],
docValueFields: [],
indexName: ecsData._index ?? '',
eventId: ecsData._id,
factoryQueryType: TimelineEventsQueries.details,
},
{
strategy: 'securitySolutionTimelineSearchStrategy',
}
)
.toPromise(),
]);
const resultingTimeline: TimelineResult = getOr({}, 'data.getOneTimeline', responseTimeline);
const eventData: TimelineEventsDetailsItem[] = getOr([], 'data', eventDataResp);
const eventData: TimelineEventsDetailsItem[] = eventDataResp.data ?? [];
if (!isEmpty(resultingTimeline)) {
const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline);
const { timeline, notes } = formatTimelineResultToModel(
@ -327,7 +328,7 @@ export const sendAlertToTimelineAction = async ({
},
},
];
if (isEqlRule(ecsData)) {
if (isEqlRuleWithGroupId(ecsData)) {
const signalGroupId = ecsData.signal?.group?.id?.length
? ecsData.signal?.group?.id[0]
: 'unknown-signal-group-id';

View file

@ -131,7 +131,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({
<EventCounts
filters={filters}
from={from}
indexNames={[]}
indexNames={selectedPatterns}
indexPattern={indexPattern}
query={query}
setQuery={setQuery}

View file

@ -414,7 +414,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
this.subPlugins(),
startPlugins.data.search
.search<IndexFieldsStrategyRequest, IndexFieldsStrategyResponse>(
{ indices: defaultIndicesName, onlyCheckIfIndicesExist: false },
{ indices: defaultIndicesName, onlyCheckIfIndicesExist: true },
{
strategy: 'securitySolutionIndexFields',
}

View file

@ -378,7 +378,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli
ruleNote,
}: UpdateTimeline): (() => void) => () => {
dispatch(
sourcererActions.setSelectedIndexPatterns({
sourcererActions.initTimelineIndexPatterns({
id: SourcererScopeName.timeline,
selectedPatterns: timeline.indexNames,
eventType: timeline.eventType,