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
…elastic#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 <[email protected]>
  • Loading branch information
XavierM and kibanamachine committed Oct 27, 2020
1 parent 2a882c0 commit 815ce3d
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 38 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 @@ -12,7 +12,7 @@ import { Provider } from 'react-redux';

import { useInitSourcerer } from '.';
import { mockPatterns, mockSource } from './mocks';
// import { SourcererScopeName } from '../../store/sourcerer/model';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { RouteSpyState } from '../../utils/route/types';
import { SecurityPageName } from '../../../../common/constants';
import { createStore, State } from '../../store';
Expand Down Expand Up @@ -85,7 +85,29 @@ describe('Sourcerer Hooks', () => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
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 @@ -15,6 +15,7 @@ import {
setSelectedIndexPatterns,
setSignalIndexName,
setSource,
initTimelineIndexPatterns,
} from './actions';
import { initialSourcererState, SourcererModel, SourcererScopeName } from './model';

Expand Down Expand Up @@ -76,6 +77,32 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState)
},
};
})
.case(initTimelineIndexPatterns, (state, { id, selectedPatterns, eventType }) => {
let defaultIndexPatterns = state.configIndexPatterns;
if (isEmpty(selectedPatterns)) {
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 {
...state,
sourcererScopes: {
...state.sourcererScopes,
[id]: {
...state.sourcererScopes[id],
selectedPatterns: isEmpty(selectedPatterns) ? defaultIndexPatterns : 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 @@ -45,7 +45,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'],
};

Expand Down Expand Up @@ -397,6 +399,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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 815ce3d

Please sign in to comment.