[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:
parent
0592938a97
commit
b304051d67
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -131,7 +131,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({
|
|||
<EventCounts
|
||||
filters={filters}
|
||||
from={from}
|
||||
indexNames={[]}
|
||||
indexNames={selectedPatterns}
|
||||
indexPattern={indexPattern}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue