Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Oct 27, 2020
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 @@ -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' &&
Comment on lines +154 to +155
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could consolidate to the following using array item access with optional chaining if you'd like:

Suggested change
ecsData.signal?.rule?.type?.length &&
ecsData.signal?.rule?.type[0] === 'eql' &&
ecsData.signal?.rule?.type?.[0] === 'eql' &&

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've also got those rule type helpers that could be leveraged here!

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 ?? [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for cleaning up more getOr's! 🙇‍♂️

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
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Expand Down