mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] Replaced sourcerer API with the dataView plugin matching indices capabilities (#139671)
Replaces the sourcerer api with the new DataView capability `matchedIndices` (introduced by the changes [here](https://github.com/elastic/kibana/pull/139067)) ref: https://github.com/elastic/kibana/issues/142904 Co-authored-by: Devin Hurley <devin.hurley@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e38350f7f9
commit
09c9b480c3
22 changed files with 206 additions and 907 deletions
|
@ -24,7 +24,7 @@ import {
|
|||
checkAutoRefreshIsDisabled,
|
||||
checkAutoRefreshIsEnabled,
|
||||
} from '../../tasks/alerts_detection_rules';
|
||||
import { login, visit } from '../../tasks/login';
|
||||
import { login, visit, visitWithoutDateRange } from '../../tasks/login';
|
||||
|
||||
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
|
||||
import { createCustomRule } from '../../tasks/api_calls/rules';
|
||||
|
@ -43,36 +43,36 @@ describe('Alerts detection rules table auto-refresh', () => {
|
|||
});
|
||||
|
||||
it('Auto refreshes rules', () => {
|
||||
visit(DETECTIONS_RULE_MANAGEMENT_URL);
|
||||
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
|
||||
|
||||
mockGlobalClock();
|
||||
waitForRulesTableToBeLoaded();
|
||||
|
||||
// ensure rules have rendered. As there is no user interaction in this test,
|
||||
// rules were not rendered before test completes
|
||||
cy.get(RULE_CHECKBOX).should('have.length', 6);
|
||||
|
||||
// mock 1 minute passing to make sure refresh is conducted
|
||||
// // mock 1 minute passing to make sure refresh is conducted
|
||||
mockGlobalClock();
|
||||
checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'be.visible');
|
||||
|
||||
cy.contains(REFRESH_RULES_STATUS, 'Updated now');
|
||||
});
|
||||
|
||||
it('should prevent table from rules refetch if any rule selected', () => {
|
||||
visit(DETECTIONS_RULE_MANAGEMENT_URL);
|
||||
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
|
||||
|
||||
mockGlobalClock();
|
||||
waitForRulesTableToBeLoaded();
|
||||
|
||||
selectNumberOfRules(1);
|
||||
|
||||
// mock 1 minute passing to make sure refresh is not conducted
|
||||
mockGlobalClock();
|
||||
checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'not.exist');
|
||||
|
||||
// ensure rule is still selected
|
||||
cy.get(RULE_CHECKBOX).first().should('be.checked');
|
||||
|
||||
cy.contains(REFRESH_RULES_STATUS, 'Updated 1 minute ago');
|
||||
cy.get(REFRESH_RULES_STATUS).should('have.not.text', 'Updated now');
|
||||
});
|
||||
|
||||
it('should disable auto refresh when any rule selected and enable it after rules unselected', () => {
|
||||
|
|
|
@ -32,16 +32,7 @@ jest.mock('react-router-dom', () => {
|
|||
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
|
||||
});
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: { theme: { theme$: {} }, http: { basePath: { prepend: jest.fn((href) => href) } } },
|
||||
}),
|
||||
useUiSetting$: jest.fn().mockReturnValue([]),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
jest.mock('../../../common/containers/source', () => ({
|
||||
useFetchIndex: () => [false, { indicesExist: true, indexPatterns: mockIndexPattern }],
|
||||
|
|
|
@ -9,12 +9,13 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
|
|||
import type { EuiComboBoxOptionOption, EuiSuperSelectOption } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { getSourcererDataView } from '../../containers/sourcerer/api';
|
||||
import { getScopePatternListSelection } from '../../store/sourcerer/helpers';
|
||||
import { sourcererActions, sourcererModel } from '../../store/sourcerer';
|
||||
import { getDataViewSelectOptions, getPatternListWithoutSignals } from './helpers';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { getSourcererDataView } from '../../containers/sourcerer/get_sourcerer_data_view';
|
||||
|
||||
interface UsePickIndexPatternsProps {
|
||||
dataViewId: string | null;
|
||||
|
@ -61,6 +62,9 @@ export const usePickIndexPatterns = ({
|
|||
signalIndexName,
|
||||
}: UsePickIndexPatternsProps): UsePickIndexPatterns => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
data: { dataViews },
|
||||
} = useKibana().services;
|
||||
const isHookAlive = useRef(true);
|
||||
const [loadingIndexPatterns, setLoadingIndexPatterns] = useState(false);
|
||||
const alertsOptions = useMemo(
|
||||
|
@ -191,15 +195,12 @@ export const usePickIndexPatterns = ({
|
|||
try {
|
||||
setLoadingIndexPatterns(true);
|
||||
setSelectedOptions([]);
|
||||
// TODO We will need to figure out how to pass an abortController, but as right now this hook is
|
||||
// constantly getting destroy and re-init
|
||||
const pickedDataViewData = await getSourcererDataView(newSelectedDataViewId);
|
||||
const dataView = await getSourcererDataView(newSelectedDataViewId, dataViews);
|
||||
|
||||
if (isHookAlive.current) {
|
||||
dispatch(sourcererActions.setDataView(pickedDataViewData));
|
||||
dispatch(sourcererActions.setDataView(dataView));
|
||||
setSelectedOptions(
|
||||
isOnlyDetectionAlerts
|
||||
? alertsOptions
|
||||
: patternListToOptions(pickedDataViewData.patternList)
|
||||
isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(dataView.patternList)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -216,6 +217,7 @@ export const usePickIndexPatterns = ({
|
|||
getDefaultSelectedOptionsByDataView,
|
||||
isOnlyDetectionAlerts,
|
||||
kibanaDataViews,
|
||||
dataViews,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { useAppToasts } from '../../hooks/use_app_toasts';
|
|||
import { sourcererActions } from '../../store/sourcerer';
|
||||
import * as i18n from './translations';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { getSourcererDataView } from '../sourcerer/api';
|
||||
import { getSourcererDataView } from '../sourcerer/get_sourcerer_data_view';
|
||||
import { useTrackHttpRequest } from '../../lib/apm/use_track_http_request';
|
||||
import { APP_UI_ID } from '../../../../common/constants';
|
||||
|
||||
|
@ -121,11 +121,8 @@ export const useDataView = (): {
|
|||
const { endTracking } = startTracking({ name: `${APP_UI_ID} indexFieldsSearch` });
|
||||
|
||||
if (needToBeInit) {
|
||||
const dataViewToUpdate = await getSourcererDataView(
|
||||
dataViewId,
|
||||
abortCtrl.current[dataViewId].signal
|
||||
);
|
||||
dispatch(sourcererActions.setDataView(dataViewToUpdate));
|
||||
const dataView = await getSourcererDataView(dataViewId, data.dataViews);
|
||||
dispatch(sourcererActions.setDataView(dataView));
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
|
@ -208,7 +205,7 @@ export const useDataView = (): {
|
|||
}
|
||||
return asyncSearch();
|
||||
},
|
||||
[addError, addWarning, data.search, dispatch, setLoading, startTracking]
|
||||
[addError, addWarning, data.search, dispatch, setLoading, startTracking, data.dataViews]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaServices } from '../../lib/kibana';
|
||||
import { SOURCERER_API_URL } from '../../../../common/constants';
|
||||
import type { KibanaDataView } from '../../store/sourcerer/model';
|
||||
|
||||
export interface GetSourcererDataView {
|
||||
signal: AbortSignal;
|
||||
body: {
|
||||
patternList: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SecurityDataView {
|
||||
defaultDataView: KibanaDataView;
|
||||
kibanaDataViews: KibanaDataView[];
|
||||
}
|
||||
|
||||
export const postSourcererDataView = async ({
|
||||
body,
|
||||
signal,
|
||||
}: GetSourcererDataView): Promise<SecurityDataView> =>
|
||||
KibanaServices.get().http.fetch(SOURCERER_API_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
});
|
||||
|
||||
export const getSourcererDataView = async (
|
||||
dataViewId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<KibanaDataView> => {
|
||||
return KibanaServices.get().http.fetch<KibanaDataView>(SOURCERER_API_URL, {
|
||||
method: 'GET',
|
||||
query: { dataViewId },
|
||||
asSystemRequest: true,
|
||||
signal,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
DataViewListItem,
|
||||
DataViewsContract,
|
||||
DataView as DataViewType,
|
||||
} from '@kbn/data-views-plugin/common';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { ensurePatternFormat } from '../../../../common/utils/sourcerer';
|
||||
import type { KibanaDataView } from '../../store/sourcerer/model';
|
||||
import { DEFAULT_TIME_FIELD } from '../../../../common/constants';
|
||||
import { getSourcererDataView } from './get_sourcerer_data_view';
|
||||
|
||||
export interface GetSourcererDataView {
|
||||
signal?: AbortSignal;
|
||||
body: {
|
||||
patternList: string[];
|
||||
};
|
||||
dataViewService: DataViewsContract;
|
||||
dataViewId: string | null;
|
||||
}
|
||||
|
||||
export interface SecurityDataView {
|
||||
defaultDataView: KibanaDataView;
|
||||
kibanaDataViews: KibanaDataView[];
|
||||
}
|
||||
|
||||
export const createSourcererDataView = async ({
|
||||
body,
|
||||
dataViewService,
|
||||
dataViewId,
|
||||
}: GetSourcererDataView): Promise<SecurityDataView | undefined> => {
|
||||
if (dataViewId === null) {
|
||||
return;
|
||||
}
|
||||
let allDataViews: DataViewListItem[] = await dataViewService.getIdsWithTitle();
|
||||
const siemDataViewExist = allDataViews.find((dv) => dv.id === dataViewId);
|
||||
|
||||
const { patternList } = body;
|
||||
const patternListAsTitle = ensurePatternFormat(patternList).join();
|
||||
let siemDataView: DataViewType;
|
||||
if (siemDataViewExist === undefined) {
|
||||
try {
|
||||
siemDataView = await dataViewService.createAndSave(
|
||||
{
|
||||
allowNoIndex: true,
|
||||
id: dataViewId,
|
||||
title: patternListAsTitle,
|
||||
timeFieldName: DEFAULT_TIME_FIELD,
|
||||
},
|
||||
// Override property - if a data view exists with the security solution pattern
|
||||
// delete it and replace it with our data view
|
||||
true
|
||||
);
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
if (err.name === 'DuplicateDataViewError' || error.statusCode === 409) {
|
||||
siemDataView = await dataViewService.get(dataViewId);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const siemDataViewTitle = siemDataViewExist
|
||||
? ensurePatternFormat(siemDataViewExist.title.split(',')).join()
|
||||
: '';
|
||||
siemDataView = await dataViewService.get(dataViewId);
|
||||
|
||||
if (patternListAsTitle !== siemDataViewTitle) {
|
||||
siemDataView.title = patternListAsTitle;
|
||||
await dataViewService.updateSavedObject(siemDataView);
|
||||
}
|
||||
}
|
||||
|
||||
if (allDataViews.some((dv) => dv.id === dataViewId)) {
|
||||
allDataViews = allDataViews.map((v) =>
|
||||
v.id === dataViewId ? { ...v, title: patternListAsTitle } : v
|
||||
);
|
||||
} else if (siemDataView !== null) {
|
||||
allDataViews.push({ id: siemDataView.id ?? dataViewId, title: siemDataView?.title });
|
||||
}
|
||||
|
||||
const siemSourcererDataView = await getSourcererDataView(dataViewId, dataViewService);
|
||||
|
||||
return {
|
||||
defaultDataView: siemSourcererDataView,
|
||||
kibanaDataViews: allDataViews.map((dv) =>
|
||||
dv.id === dataViewId
|
||||
? siemSourcererDataView
|
||||
: {
|
||||
id: dv.id,
|
||||
patternList: dv.title.split(','),
|
||||
title: dv.title,
|
||||
}
|
||||
),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
import { ensurePatternFormat } from '../../../../common/utils/sourcerer';
|
||||
|
||||
export const getSourcererDataView = async (
|
||||
dataViewId: string,
|
||||
dataViewsService: DataViewsContract
|
||||
) => {
|
||||
const dataViewData = await dataViewsService.get(dataViewId);
|
||||
const defaultPatternsList = ensurePatternFormat(dataViewData.getIndexPattern().split(','));
|
||||
const patternList = defaultPatternsList.reduce((res: string[], pattern) => {
|
||||
if (dataViewData.matchedIndices.find((q) => q.includes(pattern.replaceAll('*', '')))) {
|
||||
res.push(pattern);
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
id: dataViewData.id ?? '',
|
||||
title: dataViewData.getIndexPattern(),
|
||||
patternList,
|
||||
};
|
||||
};
|
|
@ -33,10 +33,10 @@ import {
|
|||
} from '../../mock';
|
||||
import type { SelectedDataView } from '../../store/sourcerer/model';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { postSourcererDataView } from './api';
|
||||
import * as source from '../source/use_data_view';
|
||||
import { sourcererActions } from '../../store/sourcerer';
|
||||
import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string';
|
||||
import { createSourcererDataView } from './create_sourcerer_data_view';
|
||||
|
||||
const mockRouteSpy: RouteSpyState = {
|
||||
pageName: SecurityPageName.overview,
|
||||
|
@ -49,7 +49,7 @@ const mockDispatch = jest.fn();
|
|||
const mockUseUserInfo = useUserInfo as jest.Mock;
|
||||
jest.mock('../../lib/apm/use_track_http_request');
|
||||
jest.mock('../../../detections/components/user_info');
|
||||
jest.mock('./api');
|
||||
jest.mock('./create_sourcerer_data_view');
|
||||
jest.mock('../../utils/global_query_string');
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
@ -141,7 +141,7 @@ describe('Sourcerer Hooks', () => {
|
|||
defaultDataView: mockSourcererState.defaultDataView,
|
||||
kibanaDataViews: [mockSourcererState.defaultDataView],
|
||||
};
|
||||
(postSourcererDataView as jest.Mock).mockResolvedValue(mockNewDataViews);
|
||||
(createSourcererDataView as jest.Mock).mockResolvedValue(mockNewDataViews);
|
||||
|
||||
store = createStore(
|
||||
{
|
||||
|
|
|
@ -31,17 +31,21 @@ import { TimelineId } from '../../../../common/types';
|
|||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { checkIfIndicesExist, getScopePatternListSelection } from '../../store/sourcerer/helpers';
|
||||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
import { postSourcererDataView } from './api';
|
||||
import { createSourcererDataView } from './create_sourcerer_data_view';
|
||||
import { useDataView } from '../source/use_data_view';
|
||||
import { useFetchIndex } from '../source';
|
||||
import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string';
|
||||
import { URL_PARAM_KEY } from '../../hooks/use_url_state';
|
||||
import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
|
||||
export const useInitSourcerer = (
|
||||
scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
data: { dataViews },
|
||||
} = useKibana().services;
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
const initialTimelineSourcerer = useRef(true);
|
||||
const initialDetectionSourcerer = useRef(true);
|
||||
|
@ -229,6 +233,7 @@ export const useInitSourcerer = (
|
|||
signalIndexName,
|
||||
signalIndexNameSourcerer,
|
||||
]);
|
||||
const { dataViewId } = useSourcererDataView(scopeId);
|
||||
|
||||
const updateSourcererDataView = useCallback(
|
||||
(newSignalsIndex: string) => {
|
||||
|
@ -236,18 +241,21 @@ export const useInitSourcerer = (
|
|||
abortCtrl.current = new AbortController();
|
||||
|
||||
dispatch(sourcererActions.setSourcererScopeLoading({ loading: true }));
|
||||
|
||||
try {
|
||||
const response = await postSourcererDataView({
|
||||
const response = await createSourcererDataView({
|
||||
body: { patternList: newPatternList },
|
||||
signal: abortCtrl.current.signal,
|
||||
dataViewService: dataViews,
|
||||
dataViewId,
|
||||
});
|
||||
|
||||
if (response.defaultDataView.patternList.includes(newSignalsIndex)) {
|
||||
if (response?.defaultDataView.patternList.includes(newSignalsIndex)) {
|
||||
// first time signals is defined and validated in the sourcerer
|
||||
// redo indexFieldsSearch
|
||||
indexFieldsSearch({ dataViewId: response.defaultDataView.id });
|
||||
dispatch(sourcererActions.setSourcererDataViews(response));
|
||||
}
|
||||
dispatch(sourcererActions.setSourcererDataViews(response));
|
||||
dispatch(sourcererActions.setSourcererScopeLoading({ loading: false }));
|
||||
} catch (err) {
|
||||
addError(err, {
|
||||
|
@ -267,7 +275,7 @@ export const useInitSourcerer = (
|
|||
asyncSearch([...defaultDataView.title.split(','), newSignalsIndex]);
|
||||
}
|
||||
},
|
||||
[defaultDataView.title, dispatch, indexFieldsSearch, addError]
|
||||
[defaultDataView.title, dispatch, dataViews, dataViewId, indexFieldsSearch, addError]
|
||||
);
|
||||
|
||||
const onSignalIndexUpdated = useCallback(() => {
|
||||
|
|
|
@ -12,10 +12,11 @@ import { sourcererSelectors } from '../../store';
|
|||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { useSourcererDataView } from '.';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { postSourcererDataView } from './api';
|
||||
import { sourcererActions } from '../../store/sourcerer';
|
||||
import { useDataView } from '../source/use_data_view';
|
||||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { createSourcererDataView } from './create_sourcerer_data_view';
|
||||
import { sourcererActions } from '../../store/sourcerer';
|
||||
|
||||
export const useSignalHelpers = (): {
|
||||
/* when defined, signal index has been initiated but does not exist */
|
||||
|
@ -23,11 +24,14 @@ export const useSignalHelpers = (): {
|
|||
/* when false, signal index has been initiated */
|
||||
signalIndexNeedsInit: boolean;
|
||||
} => {
|
||||
const { indicesExist } = useSourcererDataView(SourcererScopeName.detections);
|
||||
const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.detections);
|
||||
const { indexFieldsSearch } = useDataView();
|
||||
const dispatch = useDispatch();
|
||||
const { addError } = useAppToasts();
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
const {
|
||||
data: { dataViews },
|
||||
} = useKibana().services;
|
||||
|
||||
const getDefaultDataViewSelector = useMemo(
|
||||
() => sourcererSelectors.defaultDataViewSelector(),
|
||||
|
@ -53,19 +57,21 @@ export const useSignalHelpers = (): {
|
|||
const asyncSearch = async () => {
|
||||
abortCtrl.current = new AbortController();
|
||||
try {
|
||||
const response = await postSourcererDataView({
|
||||
const sourcererDataView = await createSourcererDataView({
|
||||
body: { patternList: defaultDataView.title.split(',') },
|
||||
signal: abortCtrl.current.signal,
|
||||
dataViewId,
|
||||
dataViewService: dataViews,
|
||||
});
|
||||
|
||||
if (
|
||||
signalIndexNameSourcerer !== null &&
|
||||
response.defaultDataView.patternList.includes(signalIndexNameSourcerer)
|
||||
sourcererDataView?.defaultDataView.patternList.includes(signalIndexNameSourcerer)
|
||||
) {
|
||||
// first time signals is defined and validated in the sourcerer
|
||||
// redo indexFieldsSearch
|
||||
indexFieldsSearch({ dataViewId: response.defaultDataView.id });
|
||||
dispatch(sourcererActions.setSourcererDataViews(response));
|
||||
indexFieldsSearch({ dataViewId: sourcererDataView.defaultDataView.id });
|
||||
dispatch(sourcererActions.setSourcererDataViews(sourcererDataView));
|
||||
}
|
||||
} catch (err) {
|
||||
addError(err, {
|
||||
|
@ -83,7 +89,15 @@ export const useSignalHelpers = (): {
|
|||
abortCtrl.current.abort();
|
||||
asyncSearch();
|
||||
}
|
||||
}, [addError, defaultDataView.title, dispatch, indexFieldsSearch, signalIndexNameSourcerer]);
|
||||
}, [
|
||||
addError,
|
||||
dataViewId,
|
||||
dataViews,
|
||||
defaultDataView.title,
|
||||
dispatch,
|
||||
indexFieldsSearch,
|
||||
signalIndexNameSourcerer,
|
||||
]);
|
||||
|
||||
return {
|
||||
...(shouldWePollForIndex ? { pollForSignalIndex } : {}),
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import actionCreatorFactory from 'typescript-fsa';
|
||||
|
||||
import type { SelectedDataView, SourcererDataView, SourcererScopeName } from './model';
|
||||
import type { SecurityDataView } from '../../containers/sourcerer/api';
|
||||
import type { SecurityDataView } from '../../containers/sourcerer/create_sourcerer_data_view';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer');
|
||||
|
||||
|
|
|
@ -25,10 +25,10 @@ import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
|||
import type { CoreStart } from '@kbn/core/public';
|
||||
import reduceReducers from 'reduce-reducers';
|
||||
import {
|
||||
DEFAULT_DATA_VIEW_ID,
|
||||
DEFAULT_INDEX_KEY,
|
||||
DETECTION_ENGINE_INDEX_URL,
|
||||
SERVER_APP_ID,
|
||||
SOURCERER_API_URL,
|
||||
} from '../../../common/constants';
|
||||
import { telemetryMiddleware } from '../lib/telemetry';
|
||||
import { appSelectors } from './app';
|
||||
|
@ -45,8 +45,8 @@ import { dataTableSelectors } from './data_table';
|
|||
import type { KibanaDataView, SourcererModel } from './sourcerer/model';
|
||||
import { initDataView } from './sourcerer/model';
|
||||
import type { AppObservableLibs, StartedSubPlugins, StartPlugins } from '../../types';
|
||||
import type { SecurityDataView } from '../containers/sourcerer/api';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import { createSourcererDataView } from '../containers/sourcerer/create_sourcerer_data_view';
|
||||
|
||||
type ComposeType = typeof compose;
|
||||
declare global {
|
||||
|
@ -79,12 +79,17 @@ export const createStoreFactory = async (
|
|||
let kibanaDataViews: SourcererModel['kibanaDataViews'];
|
||||
try {
|
||||
// check for/generate default Security Solution Kibana data view
|
||||
const sourcererDataViews: SecurityDataView = await coreStart.http.fetch(SOURCERER_API_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
const sourcererDataViews = await createSourcererDataView({
|
||||
body: {
|
||||
patternList: [...configPatternList, ...(signal.name != null ? [signal.name] : [])],
|
||||
}),
|
||||
},
|
||||
dataViewService: startPlugins.data.dataViews,
|
||||
dataViewId: `${DEFAULT_DATA_VIEW_ID}-${(await startPlugins.spaces?.getActiveSpace())?.id}`,
|
||||
});
|
||||
|
||||
if (sourcererDataViews === undefined) {
|
||||
throw new Error('');
|
||||
}
|
||||
defaultDataView = { ...initDataView, ...sourcererDataViews.defaultDataView };
|
||||
kibanaDataViews = sourcererDataViews.kibanaDataViews.map((dataView: KibanaDataView) => ({
|
||||
...initDataView,
|
||||
|
|
|
@ -58,6 +58,7 @@ import { getLazyEndpointGenericErrorsListExtension } from './management/pages/po
|
|||
import type { ExperimentalFeatures } from '../common/experimental_features';
|
||||
import { parseExperimentalConfigValue } from '../common/experimental_features';
|
||||
import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension';
|
||||
|
||||
import type { SecurityAppStore } from './common/store/types';
|
||||
|
||||
export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
|
||||
|
|
|
@ -23,7 +23,6 @@ import { useTimelineEvents } from '../../../containers';
|
|||
import { useTimelineEventsDetails } from '../../../containers/details';
|
||||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
|
||||
import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context';
|
||||
|
||||
jest.mock('../../../containers', () => ({
|
||||
useTimelineEvents: jest.fn(),
|
||||
|
@ -47,52 +46,7 @@ const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock;
|
|||
jest.mock('use-resize-observer/polyfilled');
|
||||
mockUseResizeObserver.mockImplementation(() => ({}));
|
||||
|
||||
const useAddToTimeline = () => ({
|
||||
beginDrag: jest.fn(),
|
||||
cancelDrag: jest.fn(),
|
||||
dragToLocation: jest.fn(),
|
||||
endDrag: jest.fn(),
|
||||
hasDraggableLock: jest.fn(),
|
||||
startDragToTimeline: jest.fn(),
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
theme: {
|
||||
theme$: {},
|
||||
},
|
||||
application: {
|
||||
navigateToApp: jest.fn(),
|
||||
getUrlForApp: jest.fn(),
|
||||
},
|
||||
cases: {
|
||||
ui: {
|
||||
getCasesContext: () => mockCasesContext,
|
||||
},
|
||||
},
|
||||
docLinks: { links: { query: { eql: 'url-eql_doc' } } },
|
||||
uiSettings: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
savedObjects: {
|
||||
client: {},
|
||||
},
|
||||
timelines: {
|
||||
getLastUpdated: jest.fn(),
|
||||
getUseAddToTimeline: () => useAddToTimeline,
|
||||
},
|
||||
triggersActionsUi: {
|
||||
getFieldBrowser: jest.fn(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
useGetUserSavedObjectPermissions: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('Timeline', () => {
|
||||
let props = {} as EqlTabContentComponentProps;
|
||||
|
|
|
@ -26,7 +26,6 @@ import { useTimelineEventsDetails } from '../../../containers/details';
|
|||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
|
||||
import { Direction } from '../../../../../common/search_strategy';
|
||||
import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context';
|
||||
import * as helpers from '../../../../common/lib/kuery';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
|
@ -54,52 +53,7 @@ const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock;
|
|||
jest.mock('use-resize-observer/polyfilled');
|
||||
mockUseResizeObserver.mockImplementation(() => ({}));
|
||||
|
||||
const useAddToTimeline = () => ({
|
||||
beginDrag: jest.fn(),
|
||||
cancelDrag: jest.fn(),
|
||||
dragToLocation: jest.fn(),
|
||||
endDrag: jest.fn(),
|
||||
hasDraggableLock: jest.fn(),
|
||||
startDragToTimeline: jest.fn(),
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
theme: {
|
||||
theme$: {},
|
||||
},
|
||||
application: {
|
||||
navigateToApp: jest.fn(),
|
||||
getUrlForApp: jest.fn(),
|
||||
},
|
||||
cases: {
|
||||
ui: {
|
||||
getCasesContext: () => mockCasesContext,
|
||||
},
|
||||
},
|
||||
uiSettings: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
savedObjects: {
|
||||
client: {},
|
||||
},
|
||||
triggersActionsUi: {
|
||||
getFieldBrowser: jest.fn(),
|
||||
},
|
||||
timelines: {
|
||||
getLastUpdated: jest.fn(),
|
||||
getLoadingPanel: jest.fn(),
|
||||
getUseAddToTimeline: () => useAddToTimeline,
|
||||
},
|
||||
},
|
||||
}),
|
||||
useGetUserSavedObjectPermissions: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('Timeline', () => {
|
||||
let props = {} as QueryTabContentComponentProps;
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
# Sourcerer API
|
||||
|
||||
### Model reference
|
||||
|
||||
```typescript
|
||||
interface KibanaDataView {
|
||||
/** Uniquely identifies a Kibana Data View */
|
||||
id: string;
|
||||
/** list of active patterns that return data */
|
||||
patternList: string[];
|
||||
/**
|
||||
* title of Kibana Data View
|
||||
* title also serves as "all pattern list", including inactive
|
||||
* comma separated string
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
```
|
||||
|
||||
### API usage
|
||||
|
||||
The sourcerer API has one route with 2 methods
|
||||
|
||||
1. POST - `createSourcererDataViewRoute`
|
||||
1. REQUEST:
|
||||
```typescript
|
||||
POST /internal/security_solution/sourcerer
|
||||
{
|
||||
patternList: [...configPatternList, ...(signal.name != null ? [signal.name] : [])]
|
||||
}
|
||||
```
|
||||
2. RESPONSE:
|
||||
```typescript
|
||||
{
|
||||
/** default security-solution data view */
|
||||
defaultDataView: KibanaDataView;
|
||||
|
||||
/** all Kibana data views, including default security-solution */
|
||||
kibanaDataViews: KibanaDataView[];
|
||||
}
|
||||
```
|
||||
3. This route is called from `security_solution/public/plugin.tsx` on app load. It passes an argument of `patternList` which is an array of the config index patterns defined in Stack Management > Advanced Settings > Security Solution > Elasticsearch indices along with the default signal index
|
||||
4. `dataViewService.getIdsWithTitle` is called to get all existing data views ids and titles
|
||||
5. Next `dataViewService.get` method is called to attempt to retrieve the default security data view by id (`siemClient.getSourcererDataViewId()`). If the data view id does not exist, it uses `dataViewService.createAndSave` to create the default security data view.
|
||||
6. `patternListAsTitle` (a string of the patternList passed) is compared to the current `siemDataViewTitle`. If they do not match, we use `dataViewService.updateSavedObject` to update the data view title. This may happen when a pattern is added or removed from the Stack Management > Advanced Settings > Security Solution > Elasticsearch indices.
|
||||
7. Next we call `buildSourcererDataView` for the default data view only. This takes the `dataView.title` and finds which patterns on the list returns data. Valid patterns are returned in an array called `patternList`. The non-default data views will have an empty array for patternList, and we will call this function if/when the data view is selected to save time.
|
||||
8. At the end we return a body of
|
||||
```
|
||||
{
|
||||
/** default security-solution data view */
|
||||
defaultDataView: KibanaDataView;
|
||||
|
||||
/** all Kibana data views, including default security-solution */
|
||||
kibanaDataViews: KibanaDataView[];
|
||||
}
|
||||
```
|
||||
9. The other place this POST is called is when the default signal index does not yet exist. In the front-end there is a method called `pollForSignalIndex` that is defined when the signal index has been initiated but does not have data. It is called whenever the detection sourcerer or timeline sourcerer mounts, or whenever the search bar is refreshed. If the signal index is defined, `pollForSignalIndex` ceases to exist and is not called.
|
||||
10. One more place we call the POST method is when the signal index first has data, we send a POST in a method called `onSignalIndexUpdated` to include the newly created index in the data view
|
||||
2. GET - `getSourcererDataViewRoute`
|
||||
1. REQUEST:
|
||||
```typescript
|
||||
GET /internal/security_solution/sourcerer?dataViewId=security-solution-default
|
||||
```
|
||||
2. RESPONSE:
|
||||
```typescript
|
||||
KibanaDataView
|
||||
```
|
||||
3. When the user changes the data view from the default in the UI, we call the GET method to find which index patterns in the `dataView.title` are valid, returning a valid `patternList`.
|
||||
4. We return a body of a single `KibanaDataView`
|
||||
|
||||
### Helpers
|
||||
To build the valid pattern list, we call `findExistingIndices` which takes the pattern list as an argument, and returns a boolean array of which patterns are valid. To check if indices exist, we use the field caps API for each pattern checking for the field `_id`. This will return a list of valid indices. If the array is empty, no indices exist for the pattern. For example:
|
||||
```typescript
|
||||
// Given
|
||||
findExistingIndices(['auditbeat-*', 'fakebeat-*', 'packetbeat-*'])
|
||||
// Returns
|
||||
[true, false, true]
|
||||
```
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { findExistingIndices } from './helpers';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
|
||||
const fieldCaps = jest
|
||||
.fn()
|
||||
.mockImplementation(() => new Promise((resolve) => resolve([true, true])));
|
||||
const esClient = {
|
||||
fieldCaps,
|
||||
};
|
||||
|
||||
describe('sourcerer helpers', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('findExistingIndices calls with regular indices', async () => {
|
||||
await findExistingIndices(['a', 'b'], esClient as unknown as ElasticsearchClient);
|
||||
expect(esClient.fieldCaps.mock.calls[0][0].index).toEqual('a');
|
||||
expect(esClient.fieldCaps.mock.calls[1][0].index).toEqual('b');
|
||||
});
|
||||
it('findExistingIndices calls with regular indices in place of exclude indices', async () => {
|
||||
await findExistingIndices(['a', '-b'], esClient as unknown as ElasticsearchClient);
|
||||
expect(esClient.fieldCaps.mock.calls[0][0].index).toEqual('a');
|
||||
expect(esClient.fieldCaps.mock.calls[1][0].index).toEqual('b');
|
||||
});
|
||||
it('findExistingIndices removes leading / trailing whitespace, and dashes from exclude patterns', async () => {
|
||||
await findExistingIndices(
|
||||
[
|
||||
' include-with-leading-and-trailing-whitespace ',
|
||||
' -exclude-with-leading-and-trailing-whitespace ',
|
||||
],
|
||||
esClient as unknown as ElasticsearchClient
|
||||
);
|
||||
expect(esClient.fieldCaps.mock.calls[0][0].index).toEqual(
|
||||
'include-with-leading-and-trailing-whitespace'
|
||||
);
|
||||
expect(esClient.fieldCaps.mock.calls[1][0].index).toEqual(
|
||||
'exclude-with-leading-and-trailing-whitespace'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
|
||||
export const findExistingIndices = async (
|
||||
indices: string[],
|
||||
esClient: ElasticsearchClient
|
||||
): Promise<boolean[]> =>
|
||||
Promise.all(
|
||||
indices
|
||||
.map(async (index) => {
|
||||
const indexToQuery = index.trim().startsWith('-')
|
||||
? index.trim().substring(1)
|
||||
: index.trim();
|
||||
const searchResponse = await esClient.fieldCaps({
|
||||
index: indexToQuery,
|
||||
fields: '_id',
|
||||
ignore_unavailable: true,
|
||||
allow_no_indices: false,
|
||||
});
|
||||
return searchResponse.indices.length > 0;
|
||||
})
|
||||
.map((p) => p.catch((e) => false))
|
||||
);
|
|
@ -1,330 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createSourcererDataViewRoute } from '.';
|
||||
import {
|
||||
requestMock,
|
||||
serverMock,
|
||||
requestContextMock,
|
||||
} from '../../detection_engine/routes/__mocks__';
|
||||
|
||||
import { SOURCERER_API_URL } from '../../../../common/constants';
|
||||
import type { StartServicesAccessor } from '@kbn/core/server';
|
||||
import type { StartPlugins } from '../../../plugin';
|
||||
|
||||
jest.mock('./helpers', () => {
|
||||
const original = jest.requireActual('./helpers');
|
||||
|
||||
return {
|
||||
...original,
|
||||
findExistingIndices: () => new Promise((resolve) => resolve([true, true])),
|
||||
};
|
||||
});
|
||||
const mockPattern = {
|
||||
id: 'security-solution',
|
||||
fields: [
|
||||
{ name: '@timestamp', searchable: true, type: 'date', aggregatable: true },
|
||||
{ name: '@version', searchable: true, type: 'string', aggregatable: true },
|
||||
{ name: 'agent.ephemeral_id', searchable: true, type: 'string', aggregatable: true },
|
||||
{ name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true },
|
||||
{ name: 'agent.id', searchable: true, type: 'string', aggregatable: true },
|
||||
],
|
||||
title:
|
||||
'apm-*-transaction*,traces-apm*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*,ml_host_risk_score_*,.siem-signals-default',
|
||||
};
|
||||
const mockPatternList = [
|
||||
'apm-*-transaction*',
|
||||
'traces-apm*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'winlogbeat-*',
|
||||
'ml_host_risk_score_*',
|
||||
'.siem-signals-default',
|
||||
];
|
||||
const mockDataViews = [
|
||||
{
|
||||
id: 'metrics-*',
|
||||
title: 'metrics-*',
|
||||
},
|
||||
{
|
||||
id: 'logs-*',
|
||||
title: 'logs-*',
|
||||
},
|
||||
mockPattern,
|
||||
];
|
||||
const getStartServices = jest.fn().mockReturnValue([
|
||||
null,
|
||||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
dataViewsServiceFactory: () => ({
|
||||
getIdsWithTitle: () => new Promise((rs) => rs(mockDataViews)),
|
||||
get: () => new Promise((rs) => rs(mockPattern)),
|
||||
createAndSave: () => new Promise((rs) => rs(mockPattern)),
|
||||
updateSavedObject: () => new Promise((rs) => rs(mockPattern)),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown) as StartServicesAccessor<StartPlugins>;
|
||||
|
||||
const getStartServicesNotSiem = jest.fn().mockReturnValue([
|
||||
null,
|
||||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
dataViewsServiceFactory: () => ({
|
||||
getIdsWithTitle: () =>
|
||||
new Promise((rs) => rs(mockDataViews.filter((v) => v.id !== mockPattern.id))),
|
||||
get: (id: string) =>
|
||||
new Promise((rs) =>
|
||||
id === mockPattern.id
|
||||
? rs(null)
|
||||
: rs({
|
||||
id: 'dataview-lambda',
|
||||
title: 'fun-*,dog-*,cat-*',
|
||||
})
|
||||
),
|
||||
createAndSave: () => new Promise((rs) => rs(mockPattern)),
|
||||
updateSavedObject: () => new Promise((rs) => rs(mockPattern)),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown) as StartServicesAccessor<StartPlugins>;
|
||||
|
||||
const mockDataViewsTransformed = {
|
||||
defaultDataView: {
|
||||
id: 'security-solution',
|
||||
patternList: ['apm-*-transaction*', 'traces-apm*'],
|
||||
title:
|
||||
'apm-*-transaction*,traces-apm*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*,ml_host_risk_score_*,.siem-signals-default',
|
||||
},
|
||||
kibanaDataViews: [
|
||||
{
|
||||
id: 'metrics-*',
|
||||
title: 'metrics-*',
|
||||
},
|
||||
{
|
||||
id: 'logs-*',
|
||||
title: 'logs-*',
|
||||
},
|
||||
{
|
||||
id: 'security-solution',
|
||||
patternList: ['apm-*-transaction*', 'traces-apm*'],
|
||||
title:
|
||||
'apm-*-transaction*,traces-apm*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*,ml_host_risk_score_*,.siem-signals-default',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('sourcerer route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { context } = requestContextMock.createTools();
|
||||
|
||||
describe('post', () => {
|
||||
const getSourcererRequest = (patternList: string[]) =>
|
||||
requestMock.create({
|
||||
method: 'post',
|
||||
path: SOURCERER_API_URL,
|
||||
body: { patternList },
|
||||
});
|
||||
|
||||
describe('functional tests', () => {
|
||||
beforeEach(() => {
|
||||
server = serverMock.create();
|
||||
({ context } = requestContextMock.createTools());
|
||||
});
|
||||
test('returns sourcerer formatted Data Views when SIEM Data View does NOT exist', async () => {
|
||||
createSourcererDataViewRoute(server.router, getStartServicesNotSiem);
|
||||
const response = await server.inject(
|
||||
getSourcererRequest(mockPatternList),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockDataViewsTransformed);
|
||||
});
|
||||
|
||||
test('returns sourcerer formatted Data Views when SIEM Data View does NOT exist but has been created in the mean time', async () => {
|
||||
const getMock = jest.fn();
|
||||
getMock.mockResolvedValueOnce(mockPattern);
|
||||
const getStartServicesSpecial = jest.fn().mockResolvedValue([
|
||||
null,
|
||||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
dataViewsServiceFactory: () => ({
|
||||
getIdsWithTitle: () =>
|
||||
new Promise((rs) => rs(mockDataViews.filter((v) => v.id !== mockPattern.id))),
|
||||
get: getMock,
|
||||
createAndSave: jest.fn().mockRejectedValue({ statusCode: 409 }),
|
||||
updateSavedObject: () => new Promise((rs, rj) => rj(new Error('error'))),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown) as StartServicesAccessor<StartPlugins>;
|
||||
createSourcererDataViewRoute(server.router, getStartServicesSpecial);
|
||||
const response = await server.inject(
|
||||
getSourcererRequest(mockPatternList),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockDataViewsTransformed);
|
||||
});
|
||||
|
||||
test('passes sorted title on create and save', async () => {
|
||||
const getMock = jest.fn();
|
||||
getMock.mockResolvedValueOnce(null);
|
||||
getMock.mockResolvedValueOnce(mockPattern);
|
||||
const mockCreateAndSave = jest.fn();
|
||||
const getStartServicesSpecial = jest.fn().mockResolvedValue([
|
||||
null,
|
||||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
dataViewsServiceFactory: () => ({
|
||||
getIdsWithTitle: () =>
|
||||
new Promise((rs) => rs(mockDataViews.filter((v) => v.id !== mockPattern.id))),
|
||||
get: getMock,
|
||||
createAndSave: mockCreateAndSave.mockImplementation(
|
||||
() => new Promise((rs) => rs(mockPattern))
|
||||
),
|
||||
updateSavedObject: () => new Promise((rs, rj) => rj(new Error('error'))),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown) as StartServicesAccessor<StartPlugins>;
|
||||
createSourcererDataViewRoute(server.router, getStartServicesSpecial);
|
||||
await server.inject(
|
||||
getSourcererRequest(['-elastic-logs-*', ...mockPatternList]),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(mockCreateAndSave.mock.calls[0][0].title).toEqual(
|
||||
'.siem-signals-default,apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,ml_host_risk_score_*,packetbeat-*,traces-apm*,winlogbeat-*,-elastic-logs-*'
|
||||
);
|
||||
});
|
||||
|
||||
test('passes override=true on create and save', async () => {
|
||||
const getMock = jest.fn();
|
||||
getMock.mockResolvedValueOnce(null);
|
||||
getMock.mockResolvedValueOnce(mockPattern);
|
||||
const mockCreateAndSave = jest.fn();
|
||||
const getStartServicesSpecial = jest.fn().mockResolvedValue([
|
||||
null,
|
||||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
dataViewsServiceFactory: () => ({
|
||||
getIdsWithTitle: () =>
|
||||
new Promise((rs) => rs(mockDataViews.filter((v) => v.id !== mockPattern.id))),
|
||||
get: getMock,
|
||||
createAndSave: mockCreateAndSave.mockImplementation(
|
||||
() => new Promise((rs) => rs(mockPattern))
|
||||
),
|
||||
updateSavedObject: () => new Promise((rs, rj) => rj(new Error('error'))),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown) as StartServicesAccessor<StartPlugins>;
|
||||
createSourcererDataViewRoute(server.router, getStartServicesSpecial);
|
||||
await server.inject(
|
||||
getSourcererRequest(mockPatternList),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(mockCreateAndSave).toHaveBeenCalled();
|
||||
expect(mockCreateAndSave.mock.calls[0][1]).toEqual(true);
|
||||
});
|
||||
|
||||
test('passes sorted title on updateSavedObject', async () => {
|
||||
const getMock = jest.fn();
|
||||
getMock.mockResolvedValueOnce(null);
|
||||
getMock.mockResolvedValueOnce(mockPattern);
|
||||
const mockCreateAndSave = jest.fn();
|
||||
const mockUpdateSavedObject = jest.fn();
|
||||
const getStartServicesSpecial = jest.fn().mockResolvedValue([
|
||||
null,
|
||||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
dataViewsServiceFactory: () => ({
|
||||
getIdsWithTitle: () =>
|
||||
new Promise((rs) =>
|
||||
rs([{ id: 'security-solution', title: 'winlogbeat-*,-elastic-logs-*' }])
|
||||
),
|
||||
get: jest.fn().mockResolvedValue({
|
||||
id: 'security-solution',
|
||||
title: 'winlogbeat-*,-elastic-logs-*',
|
||||
}),
|
||||
createAndSave: mockCreateAndSave.mockImplementation(
|
||||
() => new Promise((rs) => rs(mockPattern))
|
||||
),
|
||||
updateSavedObject: mockUpdateSavedObject.mockImplementation(
|
||||
() => new Promise((rs) => rs(mockPattern))
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown) as StartServicesAccessor<StartPlugins>;
|
||||
createSourcererDataViewRoute(server.router, getStartServicesSpecial);
|
||||
await server.inject(
|
||||
getSourcererRequest(['-elastic-logs-*', ...mockPatternList]),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(mockUpdateSavedObject).toHaveBeenCalledWith({
|
||||
id: 'security-solution',
|
||||
title:
|
||||
'.siem-signals-default,apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,ml_host_risk_score_*,packetbeat-*,traces-apm*,winlogbeat-*,-elastic-logs-*',
|
||||
});
|
||||
});
|
||||
|
||||
test('returns sourcerer formatted Data Views when SIEM Data View exists', async () => {
|
||||
createSourcererDataViewRoute(server.router, getStartServices);
|
||||
const response = await server.inject(
|
||||
getSourcererRequest(mockPatternList),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockDataViewsTransformed);
|
||||
});
|
||||
|
||||
test('returns sourcerer formatted Data Views when SIEM Data View exists and patternList input is changed', async () => {
|
||||
createSourcererDataViewRoute(server.router, getStartServices);
|
||||
mockPatternList.shift();
|
||||
const response = await server.inject(
|
||||
getSourcererRequest(mockPatternList),
|
||||
requestContextMock.convertContext(context)
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
defaultDataView: {
|
||||
id: 'security-solution',
|
||||
patternList: ['.siem-signals-default', 'auditbeat-*'],
|
||||
title:
|
||||
'.siem-signals-default,auditbeat-*,endgame-*,filebeat-*,logs-*,ml_host_risk_score_*,packetbeat-*,traces-apm*,winlogbeat-*',
|
||||
},
|
||||
kibanaDataViews: [
|
||||
mockDataViewsTransformed.kibanaDataViews[0],
|
||||
mockDataViewsTransformed.kibanaDataViews[1],
|
||||
{
|
||||
id: 'security-solution',
|
||||
patternList: ['.siem-signals-default', 'auditbeat-*'],
|
||||
title:
|
||||
'.siem-signals-default,auditbeat-*,endgame-*,filebeat-*,logs-*,ml_host_risk_score_*,packetbeat-*,traces-apm*,winlogbeat-*',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,210 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { ElasticsearchClient, StartServicesAccessor } from '@kbn/core/server';
|
||||
|
||||
import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
import { DEFAULT_TIME_FIELD, SOURCERER_API_URL } from '../../../../common/constants';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
|
||||
import type { StartPlugins } from '../../../plugin';
|
||||
import { buildSiemResponse } from '../../detection_engine/routes/utils';
|
||||
import { findExistingIndices } from './helpers';
|
||||
import { sourcererDataViewSchema, sourcererSchema } from './schema';
|
||||
import { ensurePatternFormat } from '../../../../common/utils/sourcerer';
|
||||
|
||||
export const createSourcererDataViewRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
getStartServices: StartServicesAccessor<StartPlugins>
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: SOURCERER_API_URL,
|
||||
validate: {
|
||||
body: buildRouteValidation(sourcererSchema),
|
||||
},
|
||||
options: {
|
||||
authRequired: true,
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const coreContext = await context.core;
|
||||
const siemClient = (await context.securitySolution)?.getAppClient();
|
||||
const dataViewId = siemClient.getSourcererDataViewId();
|
||||
|
||||
try {
|
||||
const [
|
||||
,
|
||||
{
|
||||
data: { indexPatterns },
|
||||
},
|
||||
] = await getStartServices();
|
||||
|
||||
const dataViewService = await indexPatterns.dataViewsServiceFactory(
|
||||
coreContext.savedObjects.client,
|
||||
coreContext.elasticsearch.client.asCurrentUser,
|
||||
request,
|
||||
true
|
||||
);
|
||||
|
||||
let allDataViews: DataViewListItem[] = await dataViewService.getIdsWithTitle();
|
||||
let siemDataView: DataView | DataViewListItem | null =
|
||||
allDataViews.find((dv) => dv.id === dataViewId) ?? null;
|
||||
|
||||
const { patternList } = request.body;
|
||||
const patternListAsTitle = ensurePatternFormat(patternList).join();
|
||||
const siemDataViewTitle = siemDataView
|
||||
? ensurePatternFormat(siemDataView.title.split(',')).join()
|
||||
: '';
|
||||
|
||||
if (siemDataView == null) {
|
||||
try {
|
||||
siemDataView = await dataViewService.createAndSave(
|
||||
{
|
||||
allowNoIndex: true,
|
||||
id: dataViewId,
|
||||
title: patternListAsTitle,
|
||||
timeFieldName: DEFAULT_TIME_FIELD,
|
||||
},
|
||||
// Override property - if a data view exists with the security solution pattern
|
||||
// delete it and replace it with our data view
|
||||
true
|
||||
);
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
if (err.name === 'DuplicateDataViewError' || error.statusCode === 409) {
|
||||
siemDataView = await dataViewService.get(dataViewId);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (patternListAsTitle !== siemDataViewTitle) {
|
||||
siemDataView = await dataViewService.get(dataViewId);
|
||||
siemDataView.title = patternListAsTitle;
|
||||
await dataViewService.updateSavedObject(siemDataView);
|
||||
}
|
||||
|
||||
if (allDataViews.some((dv) => dv.id === dataViewId)) {
|
||||
allDataViews = allDataViews.map((v) =>
|
||||
v.id === dataViewId ? { ...v, title: patternListAsTitle } : v
|
||||
);
|
||||
} else {
|
||||
allDataViews.push({ ...siemDataView, id: siemDataView.id ?? dataViewId });
|
||||
}
|
||||
|
||||
const defaultDataView = await buildSourcererDataView(
|
||||
siemDataView,
|
||||
coreContext.elasticsearch.client.asCurrentUser
|
||||
);
|
||||
return response.ok({
|
||||
body: {
|
||||
defaultDataView,
|
||||
kibanaDataViews: allDataViews.map((dv) =>
|
||||
dv.id === dataViewId ? defaultDataView : dv
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body:
|
||||
error.statusCode === 403
|
||||
? 'Users with write permissions need to access the Elastic Security app to initialize the app source data.'
|
||||
: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getSourcererDataViewRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
getStartServices: StartServicesAccessor<StartPlugins>
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: SOURCERER_API_URL,
|
||||
validate: {
|
||||
query: buildRouteValidation(sourcererDataViewSchema),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const coreContext = await context.core;
|
||||
const { dataViewId } = request.query;
|
||||
try {
|
||||
const [
|
||||
,
|
||||
{
|
||||
data: { indexPatterns },
|
||||
},
|
||||
] = await getStartServices();
|
||||
|
||||
const dataViewService = await indexPatterns.dataViewsServiceFactory(
|
||||
coreContext.savedObjects.client,
|
||||
coreContext.elasticsearch.client.asCurrentUser,
|
||||
request,
|
||||
true
|
||||
);
|
||||
const allDataViews: DataViewListItem[] = await dataViewService.getIdsWithTitle();
|
||||
const siemDataView: DataViewListItem | null =
|
||||
allDataViews.find((dv) => dv.id === dataViewId) ?? null;
|
||||
const kibanaDataView = siemDataView
|
||||
? await buildSourcererDataView(
|
||||
siemDataView,
|
||||
coreContext.elasticsearch.client.asCurrentUser
|
||||
)
|
||||
: {};
|
||||
|
||||
return response.ok({
|
||||
body: kibanaDataView,
|
||||
});
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body:
|
||||
error.statusCode === 403
|
||||
? 'Users with write permissions need to access the Elastic Security app to initialize the app source data.'
|
||||
: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
interface KibanaDataView {
|
||||
/** Uniquely identifies a Kibana Data View */
|
||||
id: string;
|
||||
/** list of active patterns that return data */
|
||||
patternList: string[];
|
||||
/**
|
||||
* title of Kibana Data View
|
||||
* title also serves as "all pattern list", including inactive
|
||||
* comma separated string
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
const buildSourcererDataView = async (
|
||||
dataView: DataView | DataViewListItem,
|
||||
clientAsCurrentUser: ElasticsearchClient
|
||||
): Promise<KibanaDataView> => {
|
||||
const patternList = dataView.title.split(',');
|
||||
const activePatternBools: boolean[] = await findExistingIndices(patternList, clientAsCurrentUser);
|
||||
const activePatternLists: string[] = patternList.filter(
|
||||
(pattern, j, self) => self.indexOf(pattern) === j && activePatternBools[j]
|
||||
);
|
||||
return { id: dataView.id ?? '', title: dataView.title, patternList: activePatternLists };
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const sourcererSchema = t.type({
|
||||
patternList: t.array(t.string),
|
||||
});
|
||||
|
||||
export const sourcererDataViewSchema = t.type({
|
||||
dataViewId: t.string,
|
||||
});
|
|
@ -55,7 +55,6 @@ import type {
|
|||
CreateRuleOptions,
|
||||
CreateSecurityRuleTypeWrapperProps,
|
||||
} from '../lib/detection_engine/rule_types/types';
|
||||
import { createSourcererDataViewRoute, getSourcererDataViewRoute } from '../lib/sourcerer/routes';
|
||||
import type { ITelemetryReceiver } from '../lib/telemetry/receiver';
|
||||
import { telemetryDetectionRulesPreviewRoute } from '../lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route';
|
||||
import { readAlertsIndexExistsRoute } from '../lib/detection_engine/routes/index/read_alerts_index_exists_route';
|
||||
|
@ -148,10 +147,6 @@ export const initRoutes = (
|
|||
// Privileges API to get the generic user privileges
|
||||
readPrivilegesRoute(router, hasEncryptionKey);
|
||||
|
||||
// Sourcerer API to generate default pattern
|
||||
createSourcererDataViewRoute(router, getStartServices);
|
||||
getSourcererDataViewRoute(router, getStartServices);
|
||||
|
||||
// risky score module
|
||||
createEsIndexRoute(router, logger);
|
||||
deleteEsIndicesRoute(router);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue