Skip to content

Commit

Permalink
[SECURITY SOLUTIONS] Bugs overview page + investigate eql in timeline (
Browse files Browse the repository at this point in the history
…#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>
  • Loading branch information
XavierM and kibanamachine committed Oct 27, 2020
1 parent 0592938 commit b304051
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand All @@ -40,15 +49,19 @@ 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,
selectedPatterns: [...ConfigIndexPatterns, signalIndexName],
})
);
}
}, [ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]);
}, [activeTimeline, ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]);

// Related to the detection page
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,29 @@ 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 ?? ''];
}
return defaultIndexPatterns;
}
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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({
<EventCounts
filters={filters}
from={from}
indexNames={[]}
indexNames={selectedPatterns}
indexPattern={indexPattern}
query={query}
setQuery={setQuery}
Expand Down
Loading

0 comments on commit b304051

Please sign in to comment.