[Security Solution] Timeline sourcerer bug fix (#118619) (#118710)

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
Kibana Machine 2021-11-16 12:03:49 -05:00 committed by GitHub
parent 7fbf857e92
commit 40c8c063e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 80 additions and 210 deletions

View file

@ -643,7 +643,7 @@ describe('timeline sourcerer', () => {
.simulate('click');
});
it('renders "alerts only" checkbox', () => {
it('renders "alerts only" checkbox, unchecked', () => {
wrapper
.find(
`[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-alert-only-checkbox"]`
@ -653,6 +653,9 @@ describe('timeline sourcerer', () => {
expect(wrapper.find(`[data-test-subj="sourcerer-alert-only-checkbox"]`).first().text()).toEqual(
'Show only detection alerts'
);
expect(
wrapper.find(`[data-test-subj="sourcerer-alert-only-checkbox"] input`).first().prop('checked')
).toEqual(false);
});
it('data view selector is enabled', () => {
@ -691,6 +694,35 @@ describe('timeline sourcerer', () => {
it('render save button', () => {
expect(wrapper.find(`[data-test-subj="sourcerer-save"]`).exists()).toBeTruthy();
});
it('Checks box when only alerts index is selected in timeline', () => {
const state2 = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
loading: false,
selectedDataViewId: id,
selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`],
},
},
},
};
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
wrapper = mount(
<TestProviders store={store}>
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
</TestProviders>
);
wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click');
expect(
wrapper.find(`[data-test-subj="sourcerer-alert-only-checkbox"] input`).first().prop('checked')
).toEqual(true);
});
});
describe('Sourcerer integration tests', () => {

View file

@ -48,11 +48,6 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
const isDetectionsSourcerer = scopeId === SourcererScopeName.detections;
const isTimelineSourcerer = scopeId === SourcererScopeName.timeline;
const [isOnlyDetectionAlertsChecked, setIsOnlyDetectionAlertsChecked] = useState(false);
const isOnlyDetectionAlerts: boolean =
isDetectionsSourcerer || (isTimelineSourcerer && isOnlyDetectionAlertsChecked);
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
const {
defaultDataView,
@ -61,6 +56,13 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
sourcererScope: { selectedDataViewId, selectedPatterns, loading },
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId));
const [isOnlyDetectionAlertsChecked, setIsOnlyDetectionAlertsChecked] = useState(
isTimelineSourcerer && selectedPatterns.join() === signalIndexName
);
const isOnlyDetectionAlerts: boolean =
isDetectionsSourcerer || (isTimelineSourcerer && isOnlyDetectionAlertsChecked);
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
const [dataViewId, setDataViewId] = useState<string>(selectedDataViewId ?? defaultDataView.id);
@ -244,7 +246,7 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
fullWidth
onChange={onChangeSuper}
options={dataViewSelectOptions}
placeholder={i18n.PICK_INDEX_PATTERNS}
placeholder={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}
valueOfSelected={dataViewId}
/>
</StyledFormRow>

View file

@ -7,11 +7,7 @@
import { mockGlobalState } from '../../mock';
import { SourcererScopeName } from './model';
import {
defaultDataViewByEventType,
getScopePatternListSelection,
validateSelectedPatterns,
} from './helpers';
import { getScopePatternListSelection, validateSelectedPatterns } from './helpers';
const signalIndexName = mockGlobalState.sourcerer.signalIndexName;
@ -23,10 +19,6 @@ const dataView = {
const patternListNoSignals = mockGlobalState.sourcerer.defaultDataView.patternList
.filter((p) => p !== signalIndexName)
.sort();
const patternListSignals = [
signalIndexName,
...mockGlobalState.sourcerer.defaultDataView.patternList.filter((p) => p !== signalIndexName),
].sort();
describe('sourcerer store helpers', () => {
describe('getScopePatternListSelection', () => {
@ -130,6 +122,29 @@ describe('sourcerer store helpers', () => {
},
});
});
it('sets to alerts in timeline even when does not yet exist', () => {
const dataViewNoSignals = {
...mockGlobalState.sourcerer.defaultDataView,
patternList: patternListNoSignals,
};
const stateNoSignals = {
...mockGlobalState.sourcerer,
defaultDataView: dataViewNoSignals,
kibanaDataViews: [dataViewNoSignals],
};
const result = validateSelectedPatterns(stateNoSignals, {
...payload,
id: SourcererScopeName.timeline,
selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`],
});
expect(result).toEqual({
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: dataView.id,
selectedPatterns: [signalIndexName],
},
});
});
describe('handles missing dataViewId, 7.16 -> 8.0', () => {
it('selectedPatterns.length > 0 & all selectedPatterns exist in defaultDataView, set dataViewId to defaultDataView.id', () => {
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
@ -175,73 +190,4 @@ describe('sourcerer store helpers', () => {
});
});
});
describe('defaultDataViewByEventType', () => {
it('defaults with no eventType', () => {
const result = defaultDataViewByEventType({ state: mockGlobalState.sourcerer });
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListSignals,
});
});
it('defaults with eventType: all', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'all',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListSignals,
});
});
it('defaults with eventType: raw', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'raw',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListNoSignals,
});
});
it('defaults with eventType: alert', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'alert',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: [signalIndexName],
});
});
it('defaults with eventType: signal', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'signal',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: [signalIndexName],
});
});
it('defaults with eventType: custom', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'custom',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListSignals,
});
});
it('defaults with eventType: eql', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'eql',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListSignals,
});
});
});
});

View file

@ -7,16 +7,8 @@
import { isEmpty } from 'lodash';
import { SourcererDataView, SourcererModel, SourcererScopeById, SourcererScopeName } from './model';
import { TimelineEventsType } from '../../../../common';
import { SelectedDataViewPayload } from './actions';
export interface Args {
eventType?: TimelineEventsType;
id: SourcererScopeName;
selectedPatterns: string[];
state: SourcererModel;
}
export const getScopePatternListSelection = (
theDataView: SourcererDataView | undefined,
sourcererScope: SourcererScopeName,
@ -45,7 +37,7 @@ export const validateSelectedPatterns = (
state: SourcererModel,
payload: SelectedDataViewPayload
): Partial<SourcererScopeById> => {
const { id, eventType, ...rest } = payload;
const { id, ...rest } = payload;
let dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId);
// dedupe because these could come from a silly url or pre 8.0 timeline
const dedupePatterns = [...new Set(rest.selectedPatterns)];
@ -57,10 +49,12 @@ export const validateSelectedPatterns = (
// so redoing the dataView != null check
(dataView != null && dataView.patternList.includes(pattern)) ||
// this is a hack, but sometimes signal index is deleted and is getting regenerated. it gets set before it is put in the dataView
state.signalIndexName == null
state.signalIndexName == null ||
state.signalIndexName === pattern
)
: // 7.16 -> 8.0 this will get hit because dataView == null
dedupePatterns;
if (selectedPatterns.length > 0 && dataView == null) {
// we have index patterns, but not a data view id
// find out if we have these index patterns in the defaultDataView
@ -74,7 +68,6 @@ export const validateSelectedPatterns = (
);
}
}
// TO DO: Steph/sourcerer If dataView is still undefined here, create temporary dataView
// and prompt user to go create this dataView
// currently UI will take the undefined dataView and default to defaultDataView anyways
@ -101,36 +94,3 @@ export const validateSelectedPatterns = (
},
};
};
// TODO: Steph/sourcerer eventType will be alerts only, when ui updates delete raw
export const defaultDataViewByEventType = ({
state,
eventType,
}: {
state: SourcererModel;
eventType?: TimelineEventsType;
}) => {
const {
signalIndexName,
defaultDataView: { id, patternList },
} = state;
if (signalIndexName != null && (eventType === 'signal' || eventType === 'alert')) {
return {
selectedPatterns: [signalIndexName],
selectedDataViewId: id,
};
} else if (eventType === 'raw') {
return {
selectedPatterns: patternList.filter((index) => index !== signalIndexName).sort(),
selectedDataViewId: id,
};
}
return {
selectedPatterns: [
// remove signalIndexName in case its already in there and add it whether or not it exists yet in the patternList
...patternList.filter((index) => index !== signalIndexName),
signalIndexName,
].sort(),
selectedDataViewId: id,
};
};

View file

@ -128,7 +128,6 @@ In other use cases the message field can be used to concatenate different values
}
end="2018-03-24T03:33:52.253Z"
eqlOptions={Object {}}
eventType="all"
expandedDetail={Object {}}
isLive={false}
itemsPerPage={5}
@ -1129,6 +1128,5 @@ In other use cases the message field can be used to concatenate different values
start="2018-03-23T18:49:23.132Z"
timelineId="test"
timerangeKind="absolute"
updateEventTypeAndIndexesName={[MockFunction]}
/>
`;

View file

@ -91,12 +91,11 @@ describe('Timeline', () => {
(useSourcererDataView as jest.Mock).mockReturnValue(mockSourcererScope);
props = {
activeTab: TimelineTabs.eql,
columns: defaultHeaders,
end: endDate,
eqlOptions: {},
expandedDetail: {},
eventType: 'all',
timelineId: TimelineId.test,
isLive: false,
itemsPerPage: 5,
itemsPerPageOptions: [5, 10, 20],
@ -105,9 +104,8 @@ describe('Timeline', () => {
rowRenderers: defaultRowRenderers,
showExpandedDetails: false,
start: startDate,
timelineId: TimelineId.test,
timerangeKind: 'absolute',
updateEventTypeAndIndexesName: jest.fn(),
activeTab: TimelineTabs.eql,
};
});

View file

@ -33,7 +33,6 @@ import { TimelineRefetch } from '../refetch_timeline';
import {
ControlColumnProps,
RowRenderer,
TimelineEventsType,
TimelineId,
TimelineTabs,
ToggleDetailPanel,
@ -43,7 +42,6 @@ import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
import { inputsModel, inputsSelectors, State } from '../../../../common/store';
import { sourcererActions } from '../../../../common/store/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
@ -159,7 +157,6 @@ export const EqlTabContentComponent: React.FC<Props> = ({
columns,
end,
eqlOptions,
eventType,
expandedDetail,
timelineId,
isLive,
@ -171,7 +168,6 @@ export const EqlTabContentComponent: React.FC<Props> = ({
showExpandedDetails,
start,
timerangeKind,
updateEventTypeAndIndexesName,
}) => {
const dispatch = useDispatch();
const { query: eqlQuery = '', ...restEqlOption } = eqlOptions;
@ -293,7 +289,9 @@ export const EqlTabContentComponent: React.FC<Props> = ({
<TimelineDatePickerLock />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Sourcerer scope={SourcererScopeName.timeline} />
{activeTab === TimelineTabs.eql && (
<Sourcerer scope={SourcererScopeName.timeline} />
)}
</EuiFlexItem>
</EuiFlexGroup>
<TimelineHeaderContainer data-test-subj="timelineHeader">
@ -374,21 +372,13 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state: State, { timelineId }: OwnProps) => {
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
const input: inputsModel.InputsRange = getInputsTimeline(state);
const {
activeTab,
columns,
eqlOptions,
eventType,
expandedDetail,
itemsPerPage,
itemsPerPageOptions,
} = timeline;
const { activeTab, columns, eqlOptions, expandedDetail, itemsPerPage, itemsPerPageOptions } =
timeline;
return {
activeTab,
columns,
eqlOptions,
eventType: eventType ?? 'raw',
end: input.timerange.to,
expandedDetail,
timelineId,
@ -404,28 +394,7 @@ const makeMapStateToProps = () => {
};
return mapStateToProps;
};
const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
updateEventTypeAndIndexesName: (
newEventType: TimelineEventsType,
newIndexNames: string[],
newDataViewId: string
) => {
dispatch(timelineActions.updateEventType({ id: timelineId, eventType: newEventType }));
dispatch(
timelineActions.updateDataView({
dataViewId: newDataViewId,
id: timelineId,
indexNames: newIndexNames,
})
);
dispatch(
sourcererActions.setSelectedDataView({
id: SourcererScopeName.timeline,
selectedDataViewId: newDataViewId,
selectedPatterns: newIndexNames,
})
);
},
const mapDispatchToProps = (dispatch: Dispatch) => ({
onEventClosed: (args: ToggleDetailPanel) => {
dispatch(timelineActions.toggleDetailPanel(args));
},
@ -442,13 +411,11 @@ const EqlTabContent = connector(
prevProps.activeTab === nextProps.activeTab &&
isTimerangeSame(prevProps, nextProps) &&
deepEqual(prevProps.eqlOptions, nextProps.eqlOptions) &&
prevProps.eventType === nextProps.eventType &&
prevProps.isLive === nextProps.isLive &&
prevProps.itemsPerPage === nextProps.itemsPerPage &&
prevProps.onEventClosed === nextProps.onEventClosed &&
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
prevProps.timelineId === nextProps.timelineId &&
prevProps.updateEventTypeAndIndexesName === nextProps.updateEventTypeAndIndexesName &&
deepEqual(prevProps.columns, nextProps.columns) &&
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions)
)

View file

@ -261,7 +261,6 @@ In other use cases the message field can be used to concatenate different values
]
}
end="2018-03-24T03:33:52.253Z"
eventType="all"
expandedDetail={Object {}}
filters={Array []}
isLive={false}
@ -1277,6 +1276,5 @@ In other use cases the message field can be used to concatenate different values
status="active"
timelineId="test"
timerangeKind="absolute"
updateEventTypeAndIndexesName={[MockFunction]}
/>
`;

View file

@ -108,7 +108,6 @@ describe('Timeline', () => {
columns: defaultHeaders,
dataProviders: mockDataProviders,
end: endDate,
eventType: 'all',
expandedDetail: {},
filters: [],
timelineId: TimelineId.test,
@ -126,7 +125,6 @@ describe('Timeline', () => {
start: startDate,
status: TimelineStatus.active,
timerangeKind: 'absolute',
updateEventTypeAndIndexesName: jest.fn(),
activeTab: TimelineTabs.query,
show: true,
};

View file

@ -38,7 +38,6 @@ import {
ControlColumnProps,
KueryFilterQueryKind,
RowRenderer,
TimelineEventsType,
TimelineId,
TimelineTabs,
ToggleDetailPanel,
@ -47,7 +46,6 @@ import { requiredFieldsForActions } from '../../../../detections/components/aler
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
import { inputsModel, inputsSelectors, State } from '../../../../common/store';
import { sourcererActions } from '../../../../common/store/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
@ -164,7 +162,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
columns,
dataProviders,
end,
eventType,
expandedDetail,
filters,
timelineId,
@ -183,7 +180,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
status,
sort,
timerangeKind,
updateEventTypeAndIndexesName,
}) => {
const dispatch = useDispatch();
const { portalNode: timelineEventsCountPortalNode } = useTimelineEventsCountPortal();
@ -368,7 +364,9 @@ export const QueryTabContentComponent: React.FC<Props> = ({
<TimelineDatePickerLock />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Sourcerer scope={SourcererScopeName.timeline} />
{activeTab === TimelineTabs.query && (
<Sourcerer scope={SourcererScopeName.timeline} />
)}
</EuiFlexItem>
</EuiFlexGroup>
<TimelineHeaderContainer data-test-subj="timelineHeader">
@ -461,7 +459,6 @@ const makeMapStateToProps = () => {
activeTab,
columns,
dataProviders,
eventType,
expandedDetail,
filters,
itemsPerPage,
@ -487,7 +484,6 @@ const makeMapStateToProps = () => {
activeTab,
columns,
dataProviders,
eventType: eventType ?? 'raw',
end: input.timerange.to,
expandedDetail,
filters: timelineFilter,
@ -511,27 +507,6 @@ const makeMapStateToProps = () => {
};
const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
updateEventTypeAndIndexesName: (
newEventType: TimelineEventsType,
newIndexNames: string[],
newDataViewId: string
) => {
dispatch(timelineActions.updateEventType({ id: timelineId, eventType: newEventType }));
dispatch(
timelineActions.updateDataView({
dataViewId: newDataViewId,
id: timelineId,
indexNames: newIndexNames,
})
);
dispatch(
sourcererActions.setSelectedDataView({
id: SourcererScopeName.timeline,
selectedPatterns: newIndexNames,
selectedDataViewId: newDataViewId,
})
);
},
onEventClosed: (args: ToggleDetailPanel) => {
dispatch(timelineActions.toggleDetailPanel(args));
},
@ -548,7 +523,6 @@ const QueryTabContent = connector(
compareQueryProps(prevProps, nextProps) &&
prevProps.activeTab === nextProps.activeTab &&
isTimerangeSame(prevProps, nextProps) &&
prevProps.eventType === nextProps.eventType &&
prevProps.isLive === nextProps.isLive &&
prevProps.itemsPerPage === nextProps.itemsPerPage &&
prevProps.onEventClosed === nextProps.onEventClosed &&
@ -557,7 +531,6 @@ const QueryTabContent = connector(
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
prevProps.status === nextProps.status &&
prevProps.timelineId === nextProps.timelineId &&
prevProps.updateEventTypeAndIndexesName === nextProps.updateEventTypeAndIndexesName &&
deepEqual(prevProps.columns, nextProps.columns) &&
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&

View file

@ -71,7 +71,6 @@ describe('epicLocalStorage', () => {
columns: defaultHeaders,
dataProviders: mockDataProviders,
end: endDate,
eventType: 'all',
expandedDetail: {},
filters: [],
isLive: false,
@ -89,7 +88,6 @@ describe('epicLocalStorage', () => {
sort,
timelineId: 'foo',
timerangeKind: 'absolute',
updateEventTypeAndIndexesName: jest.fn(),
activeTab: TimelineTabs.query,
show: true,
};