mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[SECURITY SOLUTION] Allow the application to create its own data view without using user privilege (#121109) (#121816)
* backend update to use unsecure so * wip on UI * fix UI to work with one dataview at the time * by pass capabilities in data view factory * fix sourcerer in timeline * fix types * fix unit test * fix index field to work with security data view * cypress + detection roles tests * add unit test * review I * review II * review III * clean up after talking to Larry * fix latets code * working to be green * by pass capabilities from data view API only use saved object kibana privilege * fix lint * add commnet per review Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co> Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co> Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
parent
a0aea6c112
commit
f5add805e3
27 changed files with 466 additions and 230 deletions
|
@ -20,23 +20,23 @@ import { UiSettingsServerToCommon } from './ui_settings_wrapper';
|
|||
import { IndexPatternsApiServer } from './index_patterns_api_client';
|
||||
import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper';
|
||||
|
||||
export const dataViewsServiceFactory =
|
||||
({
|
||||
logger,
|
||||
uiSettings,
|
||||
fieldFormats,
|
||||
capabilities,
|
||||
}: {
|
||||
logger: Logger;
|
||||
uiSettings: UiSettingsServiceStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
capabilities: CoreStart['capabilities'];
|
||||
}) =>
|
||||
async (
|
||||
export const dataViewsServiceFactory = ({
|
||||
logger,
|
||||
uiSettings,
|
||||
fieldFormats,
|
||||
capabilities,
|
||||
}: {
|
||||
logger: Logger;
|
||||
uiSettings: UiSettingsServiceStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
capabilities: CoreStart['capabilities'];
|
||||
}) =>
|
||||
async function (
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
elasticsearchClient: ElasticsearchClient,
|
||||
request?: KibanaRequest
|
||||
) => {
|
||||
request?: KibanaRequest,
|
||||
byPassCapabilities?: boolean
|
||||
) {
|
||||
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
|
||||
const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient);
|
||||
|
||||
|
@ -52,7 +52,9 @@ export const dataViewsServiceFactory =
|
|||
logger.warn(`${title}${text ? ` : ${text}` : ''}`);
|
||||
},
|
||||
getCanSave: async () =>
|
||||
request
|
||||
byPassCapabilities
|
||||
? true
|
||||
: request
|
||||
? (await capabilities.resolveCapabilities(request)).indexPatterns.save === true
|
||||
: false,
|
||||
});
|
||||
|
|
|
@ -20,7 +20,8 @@ import { FieldFormatsSetup, FieldFormatsStart } from '../../field_formats/server
|
|||
type ServiceFactory = (
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
elasticsearchClient: ElasticsearchClient,
|
||||
request?: KibanaRequest
|
||||
request?: KibanaRequest,
|
||||
byPassCapabilities?: boolean
|
||||
) => Promise<DataViewsService>;
|
||||
export interface DataViewsServerPluginStart {
|
||||
dataViewsServiceFactory: ServiceFactory;
|
||||
|
|
|
@ -271,7 +271,7 @@ export const TIMELINE_PREPACKAGED_URL = `${TIMELINE_URL}/_prepackaged` as const;
|
|||
|
||||
export const NOTE_URL = '/api/note' as const;
|
||||
export const PINNED_EVENT_URL = '/api/pinned_event' as const;
|
||||
export const SOURCERER_API_URL = '/api/sourcerer' as const;
|
||||
export const SOURCERER_API_URL = '/internal/security_solution/sourcerer' as const;
|
||||
|
||||
/**
|
||||
* Default signals index key for kibana.dev.yml
|
||||
|
|
|
@ -66,10 +66,6 @@ export const secAll: Role = {
|
|||
securitySolutionCases: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
// TODO: Steph/sourcerer remove once we have our internal saved object client
|
||||
// https://github.com/elastic/security-team/issues/1978
|
||||
indexPatterns: ['read'],
|
||||
savedObjectsManagement: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
|
@ -101,10 +97,6 @@ export const secReadCasesAll: Role = {
|
|||
securitySolutionCases: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
// TODO: Steph/sourcerer remove once we have our internal saved object client
|
||||
// https://github.com/elastic/security-team/issues/1978
|
||||
indexPatterns: ['read'],
|
||||
savedObjectsManagement: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
|
|
|
@ -86,9 +86,10 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
const {
|
||||
allOptions,
|
||||
dataViewSelectOptions,
|
||||
loadingIndexPatterns,
|
||||
isModified,
|
||||
handleOutsideClick,
|
||||
onChangeCombo,
|
||||
onChangeCombo: onChangeIndexPatterns,
|
||||
renderOption,
|
||||
selectedOptions,
|
||||
setIndexPatternsByDataView,
|
||||
|
@ -120,7 +121,7 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
setPopoverIsOpen((prevState) => !prevState);
|
||||
setExpandAdvancedOptions(false); // we always want setExpandAdvancedOptions collapsed by default when popover opened
|
||||
}, []);
|
||||
const onChangeDataView = useCallback(
|
||||
const dispatchChangeDataView = useCallback(
|
||||
(
|
||||
newSelectedDataView: string,
|
||||
newSelectedPatterns: string[],
|
||||
|
@ -138,7 +139,7 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
[dispatch, scopeId]
|
||||
);
|
||||
|
||||
const onChangeSuper = useCallback(
|
||||
const onChangeDataView = useCallback(
|
||||
(newSelectedOption) => {
|
||||
setDataViewId(newSelectedOption);
|
||||
setIndexPatternsByDataView(newSelectedOption);
|
||||
|
@ -156,10 +157,10 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
const handleSaveIndices = useCallback(() => {
|
||||
const patterns = selectedOptions.map((so) => so.label);
|
||||
if (dataViewId != null) {
|
||||
onChangeDataView(dataViewId, patterns);
|
||||
dispatchChangeDataView(dataViewId, patterns);
|
||||
}
|
||||
setPopoverIsOpen(false);
|
||||
}, [onChangeDataView, dataViewId, selectedOptions]);
|
||||
}, [dispatchChangeDataView, dataViewId, selectedOptions]);
|
||||
|
||||
const handleClosePopOver = useCallback(() => {
|
||||
setPopoverIsOpen(false);
|
||||
|
@ -172,9 +173,9 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
const patterns = selectedPatterns.filter((pattern) =>
|
||||
defaultDataView.patternList.includes(pattern)
|
||||
);
|
||||
onChangeDataView(defaultDataView.id, patterns);
|
||||
dispatchChangeDataView(defaultDataView.id, patterns);
|
||||
setPopoverIsOpen(false);
|
||||
}, [defaultDataView.id, defaultDataView.patternList, onChangeDataView, selectedPatterns]);
|
||||
}, [defaultDataView.id, defaultDataView.patternList, dispatchChangeDataView, selectedPatterns]);
|
||||
|
||||
const onUpdateDeprecated = useCallback(() => {
|
||||
// are all the patterns in the default?
|
||||
|
@ -200,7 +201,7 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
setPopoverIsOpen(false);
|
||||
|
||||
if (isUiSettingsSuccess) {
|
||||
onChangeDataView(
|
||||
dispatchChangeDataView(
|
||||
defaultDataView.id,
|
||||
// to be at this stage, activePatterns is defined, the ?? selectedPatterns is to make TS happy
|
||||
activePatterns ?? selectedPatterns,
|
||||
|
@ -212,7 +213,7 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
activePatterns,
|
||||
defaultDataView.id,
|
||||
missingPatterns,
|
||||
onChangeDataView,
|
||||
dispatchChangeDataView,
|
||||
selectedPatterns,
|
||||
updateDataView,
|
||||
]);
|
||||
|
@ -300,9 +301,10 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
<StyledFormRow label={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="sourcerer-select"
|
||||
isLoading={loadingIndexPatterns}
|
||||
disabled={isOnlyDetectionAlerts}
|
||||
fullWidth
|
||||
onChange={onChangeSuper}
|
||||
onChange={onChangeDataView}
|
||||
options={dataViewSelectOptions}
|
||||
placeholder={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}
|
||||
valueOfSelected={dataViewId}
|
||||
|
@ -321,6 +323,7 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
</StyledButton>
|
||||
{expandAdvancedOptions && <EuiSpacer size="m" />}
|
||||
<FormRow
|
||||
isDisabled={loadingIndexPatterns}
|
||||
$expandAdvancedOptions={expandAdvancedOptions}
|
||||
helpText={isOnlyDetectionAlerts ? undefined : i18n.INDEX_PATTERNS_DESCRIPTIONS}
|
||||
label={i18n.INDEX_PATTERNS_LABEL}
|
||||
|
@ -328,8 +331,8 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
<EuiComboBox
|
||||
data-test-subj="sourcerer-combo-box"
|
||||
fullWidth
|
||||
isDisabled={isOnlyDetectionAlerts}
|
||||
onChange={onChangeCombo}
|
||||
isDisabled={isOnlyDetectionAlerts || loadingIndexPatterns}
|
||||
onChange={onChangeIndexPatterns}
|
||||
options={allOptions}
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
renderOption={renderOption}
|
||||
|
|
|
@ -5,10 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { EuiComboBoxOptionOption, EuiSuperSelectOption } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { getSourcererDataview } from '../../containers/sourcerer/api';
|
||||
import { getScopePatternListSelection } from '../../store/sourcerer/helpers';
|
||||
import { sourcererModel } from '../../store/sourcerer';
|
||||
import { sourcererActions, sourcererModel } from '../../store/sourcerer';
|
||||
import { getDataViewSelectOptions, getPatternListWithoutSignals } from './helpers';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
|
||||
|
@ -29,6 +32,7 @@ export type ModifiedTypes = 'modified' | 'alerts' | 'deprecated' | 'missingPatte
|
|||
interface UsePickIndexPatterns {
|
||||
allOptions: Array<EuiComboBoxOptionOption<string>>;
|
||||
dataViewSelectOptions: Array<EuiSuperSelectOption<string>>;
|
||||
loadingIndexPatterns: boolean;
|
||||
handleOutsideClick: () => void;
|
||||
isModified: ModifiedTypes;
|
||||
onChangeCombo: (newSelectedDataViewId: Array<EuiComboBoxOptionOption<string>>) => void;
|
||||
|
@ -55,10 +59,19 @@ export const usePickIndexPatterns = ({
|
|||
selectedPatterns,
|
||||
signalIndexName,
|
||||
}: UsePickIndexPatternsProps): UsePickIndexPatterns => {
|
||||
const dispatch = useDispatch();
|
||||
const isHookAlive = useRef(true);
|
||||
const [loadingIndexPatterns, setLoadingIndexPatterns] = useState(false);
|
||||
const alertsOptions = useMemo(
|
||||
() => (signalIndexName ? patternListToOptions([signalIndexName]) : []),
|
||||
[signalIndexName]
|
||||
);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
|
||||
isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns)
|
||||
);
|
||||
const [isModified, setIsModified] = useState<ModifiedTypes>(
|
||||
dataViewId == null ? 'deprecated' : missingPatterns.length > 0 ? 'missingPatterns' : ''
|
||||
);
|
||||
|
||||
const { allPatterns, selectablePatterns } = useMemo<{
|
||||
allPatterns: string[];
|
||||
|
@ -99,9 +112,6 @@ export const usePickIndexPatterns = ({
|
|||
() => patternListToOptions(allPatterns, selectablePatterns),
|
||||
[allPatterns, selectablePatterns]
|
||||
);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
|
||||
isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns)
|
||||
);
|
||||
|
||||
const getDefaultSelectedOptionsByDataView = useCallback(
|
||||
(id: string, isAlerts: boolean = false): Array<EuiComboBoxOptionOption<string>> =>
|
||||
|
@ -123,9 +133,6 @@ export const usePickIndexPatterns = ({
|
|||
[dataViewId, getDefaultSelectedOptionsByDataView]
|
||||
);
|
||||
|
||||
const [isModified, setIsModified] = useState<ModifiedTypes>(
|
||||
dataViewId == null ? 'deprecated' : missingPatterns.length > 0 ? 'missingPatterns' : ''
|
||||
);
|
||||
const onSetIsModified = useCallback(
|
||||
(patterns: string[], id: string | null) => {
|
||||
if (id == null) {
|
||||
|
@ -173,9 +180,47 @@ export const usePickIndexPatterns = ({
|
|||
[]
|
||||
);
|
||||
|
||||
const setIndexPatternsByDataView = (newSelectedDataViewId: string, isAlerts?: boolean) => {
|
||||
setSelectedOptions(getDefaultSelectedOptionsByDataView(newSelectedDataViewId, isAlerts));
|
||||
};
|
||||
const setIndexPatternsByDataView = useCallback(
|
||||
async (newSelectedDataViewId: string, isAlerts?: boolean) => {
|
||||
if (
|
||||
kibanaDataViews.some(
|
||||
(kdv) => kdv.id === newSelectedDataViewId && kdv.indexFields.length === 0
|
||||
)
|
||||
) {
|
||||
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);
|
||||
if (isHookAlive.current) {
|
||||
dispatch(
|
||||
sourcererActions.updateSourcererDataViews({
|
||||
dataView: pickedDataViewData,
|
||||
})
|
||||
);
|
||||
setSelectedOptions(
|
||||
isOnlyDetectionAlerts
|
||||
? alertsOptions
|
||||
: patternListToOptions(pickedDataViewData.patternList)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
// Nothing to do
|
||||
}
|
||||
setLoadingIndexPatterns(false);
|
||||
} else {
|
||||
setSelectedOptions(getDefaultSelectedOptionsByDataView(newSelectedDataViewId, isAlerts));
|
||||
}
|
||||
},
|
||||
[
|
||||
alertsOptions,
|
||||
dispatch,
|
||||
getDefaultSelectedOptionsByDataView,
|
||||
isOnlyDetectionAlerts,
|
||||
kibanaDataViews,
|
||||
]
|
||||
);
|
||||
|
||||
const dataViewSelectOptions = useMemo(
|
||||
() =>
|
||||
|
@ -191,6 +236,13 @@ export const usePickIndexPatterns = ({
|
|||
[dataViewId, defaultDataViewId, isModified, isOnlyDetectionAlerts, kibanaDataViews]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
isHookAlive.current = true;
|
||||
return () => {
|
||||
isHookAlive.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleOutsideClick = useCallback(() => {
|
||||
setSelectedOptions(patternListToOptions(selectedPatterns));
|
||||
}, [selectedPatterns]);
|
||||
|
@ -198,6 +250,7 @@ export const usePickIndexPatterns = ({
|
|||
return {
|
||||
allOptions,
|
||||
dataViewSelectOptions,
|
||||
loadingIndexPatterns,
|
||||
handleOutsideClick,
|
||||
isModified,
|
||||
onChangeCombo,
|
||||
|
|
|
@ -26,6 +26,8 @@ import {
|
|||
} from '../../../../../../../src/plugins/data/common';
|
||||
import * as i18n from './translations';
|
||||
import { getBrowserFields, getDocValueFields } from './';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { getSourcererDataview } from '../sourcerer/api';
|
||||
|
||||
const getEsFields = memoizeOne(
|
||||
(fields: IndexField[]): FieldSpec[] =>
|
||||
|
@ -37,7 +39,13 @@ const getEsFields = memoizeOne(
|
|||
(newArgs, lastArgs) => newArgs[0].length === lastArgs[0].length
|
||||
);
|
||||
|
||||
export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string) => void } => {
|
||||
export const useDataView = (): {
|
||||
indexFieldsSearch: (
|
||||
selectedDataViewId: string,
|
||||
scopeId?: SourcererScopeName,
|
||||
needToBeInit?: boolean
|
||||
) => void;
|
||||
} => {
|
||||
const { data } = useKibana().services;
|
||||
const abortCtrl = useRef<Record<string, AbortController>>({});
|
||||
const searchSubscription$ = useRef<Record<string, Subscription>>({});
|
||||
|
@ -51,13 +59,29 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
|
|||
[dispatch]
|
||||
);
|
||||
const indexFieldsSearch = useCallback(
|
||||
(selectedDataViewId: string) => {
|
||||
(
|
||||
selectedDataViewId: string,
|
||||
scopeId: SourcererScopeName = SourcererScopeName.default,
|
||||
needToBeInit: boolean = false
|
||||
) => {
|
||||
const asyncSearch = async () => {
|
||||
abortCtrl.current = {
|
||||
...abortCtrl.current,
|
||||
[selectedDataViewId]: new AbortController(),
|
||||
};
|
||||
setLoading({ id: selectedDataViewId, loading: true });
|
||||
if (needToBeInit) {
|
||||
const dataViewToUpdate = await getSourcererDataview(
|
||||
selectedDataViewId,
|
||||
abortCtrl.current[selectedDataViewId].signal
|
||||
);
|
||||
dispatch(
|
||||
sourcererActions.updateSourcererDataViews({
|
||||
dataView: dataViewToUpdate,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const subscription = data.search
|
||||
.search<IndexFieldsStrategyRequest<'dataView'>, IndexFieldsStrategyResponse>(
|
||||
{
|
||||
|
@ -73,7 +97,15 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
|
|||
next: (response) => {
|
||||
if (isCompleteResponse(response)) {
|
||||
const patternString = response.indicesExist.sort().join();
|
||||
|
||||
if (needToBeInit && scopeId) {
|
||||
dispatch(
|
||||
sourcererActions.setSelectedDataView({
|
||||
id: scopeId,
|
||||
selectedDataViewId,
|
||||
selectedPatterns: response.indicesExist,
|
||||
})
|
||||
);
|
||||
}
|
||||
dispatch(
|
||||
sourcererActions.setDataView({
|
||||
browserFields: getBrowserFields(patternString, response.indexFields),
|
||||
|
@ -84,15 +116,11 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
|
|||
runtimeMappings: response.runtimeMappings,
|
||||
})
|
||||
);
|
||||
if (searchSubscription$.current[selectedDataViewId]) {
|
||||
searchSubscription$.current[selectedDataViewId].unsubscribe();
|
||||
}
|
||||
searchSubscription$.current[selectedDataViewId]?.unsubscribe();
|
||||
} else if (isErrorResponse(response)) {
|
||||
setLoading({ id: selectedDataViewId, loading: false });
|
||||
addWarning(i18n.ERROR_BEAT_FIELDS);
|
||||
if (searchSubscription$.current[selectedDataViewId]) {
|
||||
searchSubscription$.current[selectedDataViewId].unsubscribe();
|
||||
}
|
||||
searchSubscription$.current[selectedDataViewId]?.unsubscribe();
|
||||
}
|
||||
},
|
||||
error: (msg) => {
|
||||
|
@ -104,9 +132,7 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
|
|||
addError(msg, {
|
||||
title: i18n.FAIL_BEAT_FIELDS,
|
||||
});
|
||||
if (searchSubscription$.current[selectedDataViewId]) {
|
||||
searchSubscription$.current[selectedDataViewId].unsubscribe();
|
||||
}
|
||||
searchSubscription$.current[selectedDataViewId]?.unsubscribe();
|
||||
},
|
||||
});
|
||||
searchSubscription$.current = {
|
||||
|
@ -114,11 +140,10 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
|
|||
[selectedDataViewId]: subscription,
|
||||
};
|
||||
};
|
||||
if (searchSubscription$.current[selectedDataViewId] != null) {
|
||||
if (searchSubscription$.current[selectedDataViewId]) {
|
||||
searchSubscription$.current[selectedDataViewId].unsubscribe();
|
||||
}
|
||||
|
||||
if (abortCtrl.current[selectedDataViewId] != null) {
|
||||
if (abortCtrl.current[selectedDataViewId]) {
|
||||
abortCtrl.current[selectedDataViewId].abort();
|
||||
}
|
||||
asyncSearch();
|
||||
|
|
|
@ -30,3 +30,15 @@ export const postSourcererDataView = async ({
|
|||
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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -72,29 +72,54 @@ export const useInitSourcerer = (
|
|||
getTimelineSelector(state, TimelineId.active)
|
||||
);
|
||||
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []);
|
||||
const { selectedDataViewId: scopeDataViewId } = useDeepEqualSelector((state) =>
|
||||
scopeIdSelector(state, scopeId)
|
||||
);
|
||||
const { selectedDataViewId: timelineDataViewId } = useDeepEqualSelector((state) =>
|
||||
scopeIdSelector(state, SourcererScopeName.timeline)
|
||||
);
|
||||
const activeDataViewIds = useMemo(
|
||||
() => [...new Set([scopeDataViewId, timelineDataViewId])],
|
||||
[scopeDataViewId, timelineDataViewId]
|
||||
);
|
||||
const {
|
||||
selectedDataViewId: scopeDataViewId,
|
||||
selectedPatterns,
|
||||
missingPatterns,
|
||||
} = useDeepEqualSelector((state) => scopeIdSelector(state, scopeId));
|
||||
const {
|
||||
selectedDataViewId: timelineDataViewId,
|
||||
selectedPatterns: timelineSelectedPatterns,
|
||||
missingPatterns: timelineMissingPatterns,
|
||||
} = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline));
|
||||
const { indexFieldsSearch } = useDataView();
|
||||
|
||||
/*
|
||||
* Note for future engineer:
|
||||
* we changed the logic to not fetch all the index fields for every data view on the loading of the app
|
||||
* because user can have a lot of them and it can slow down the loading of the app
|
||||
* and maybe blow up the memory of the browser. We decided to load this data view on demand,
|
||||
* we know that will only have to load this dataview on default and timeline scope.
|
||||
* We will use two conditions to see if we need to fetch and initialize the dataview selected.
|
||||
* First, we will make sure that we did not already fetch them by using `searchedIds`
|
||||
* and then we will init them if selectedPatterns and missingPatterns are empty.
|
||||
*/
|
||||
const searchedIds = useRef<string[]>([]);
|
||||
useEffect(
|
||||
() =>
|
||||
activeDataViewIds.forEach((id) => {
|
||||
if (id != null && id.length > 0 && !searchedIds.current.includes(id)) {
|
||||
searchedIds.current = [...searchedIds.current, id];
|
||||
indexFieldsSearch(id);
|
||||
}
|
||||
}),
|
||||
[activeDataViewIds, indexFieldsSearch]
|
||||
);
|
||||
useEffect(() => {
|
||||
const activeDataViewIds = [...new Set([scopeDataViewId, timelineDataViewId])];
|
||||
activeDataViewIds.forEach((id) => {
|
||||
if (id != null && id.length > 0 && !searchedIds.current.includes(id)) {
|
||||
searchedIds.current = [...searchedIds.current, id];
|
||||
indexFieldsSearch(
|
||||
id,
|
||||
id === scopeDataViewId ? SourcererScopeName.default : SourcererScopeName.timeline,
|
||||
id === scopeDataViewId
|
||||
? selectedPatterns.length === 0 && missingPatterns.length === 0
|
||||
: timelineDataViewId === id
|
||||
? timelineMissingPatterns.length === 0 && timelineSelectedPatterns.length === 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [
|
||||
indexFieldsSearch,
|
||||
missingPatterns.length,
|
||||
scopeDataViewId,
|
||||
selectedPatterns.length,
|
||||
timelineDataViewId,
|
||||
timelineMissingPatterns.length,
|
||||
timelineSelectedPatterns.length,
|
||||
]);
|
||||
|
||||
// Related to timeline
|
||||
useEffect(() => {
|
||||
|
@ -334,12 +359,14 @@ export const useSourcererDataView = (
|
|||
|
||||
const indicesExist = useMemo(
|
||||
() =>
|
||||
checkIfIndicesExist({
|
||||
scopeId,
|
||||
signalIndexName,
|
||||
patternList: sourcererDataView.patternList,
|
||||
}),
|
||||
[scopeId, signalIndexName, sourcererDataView]
|
||||
loading || sourcererDataView.loading
|
||||
? true
|
||||
: checkIfIndicesExist({
|
||||
scopeId,
|
||||
signalIndexName,
|
||||
patternList: sourcererDataView.patternList,
|
||||
}),
|
||||
[loading, scopeId, signalIndexName, sourcererDataView.loading, sourcererDataView.patternList]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
|
||||
import { SelectedDataView, SourcererDataView, SourcererScopeName } from './model';
|
||||
import { KibanaDataView, SelectedDataView, SourcererDataView, SourcererScopeName } from './model';
|
||||
import { SecurityDataView } from '../../containers/sourcerer/api';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer');
|
||||
|
@ -43,3 +43,7 @@ export interface SelectedDataViewPayload {
|
|||
shouldValidateSelectedPatterns?: boolean;
|
||||
}
|
||||
export const setSelectedDataView = actionCreator<SelectedDataViewPayload>('SET_SELECTED_DATA_VIEW');
|
||||
|
||||
export const updateSourcererDataViews = actionCreator<{
|
||||
dataView: KibanaDataView;
|
||||
}>('UPDATE_SOURCERER_DATA_VIEWS');
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
setSignalIndexName,
|
||||
setDataView,
|
||||
setDataViewLoading,
|
||||
updateSourcererDataViews,
|
||||
} from './actions';
|
||||
import { initDataView, initialSourcererState, SourcererModel, SourcererScopeName } from './model';
|
||||
import { validateSelectedPatterns } from './helpers';
|
||||
|
@ -45,6 +46,12 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState)
|
|||
...dataView,
|
||||
})),
|
||||
}))
|
||||
.case(updateSourcererDataViews, (state, { dataView }) => ({
|
||||
...state,
|
||||
kibanaDataViews: state.kibanaDataViews.map((dv) =>
|
||||
dv.id === dataView.id ? { ...dv, ...dataView } : dv
|
||||
),
|
||||
}))
|
||||
.case(setSourcererScopeLoading, (state, { id, loading }) => ({
|
||||
...state,
|
||||
sourcererScopes: {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { KibanaFeatureConfig, SubFeatureConfig } from '../../features/common';
|
|||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
||||
import { APP_ID, CASES_FEATURE_ID, SERVER_APP_ID } from '../common/constants';
|
||||
import { savedObjectTypes } from './saved_objects';
|
||||
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../../src/plugins/data_views/common';
|
||||
|
||||
export const getCasesKibanaFeature = (): KibanaFeatureConfig => ({
|
||||
id: CASES_FEATURE_ID,
|
||||
|
@ -119,7 +120,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
|
|||
catalogue: [APP_ID],
|
||||
api: [APP_ID, 'lists-all', 'lists-read', 'rac'],
|
||||
savedObject: {
|
||||
all: ['alert', 'exception-list', 'exception-list-agnostic', ...savedObjectTypes],
|
||||
all: [
|
||||
'alert',
|
||||
'exception-list',
|
||||
'exception-list-agnostic',
|
||||
DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
...savedObjectTypes,
|
||||
],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
|
@ -138,7 +145,12 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
|
|||
api: [APP_ID, 'lists-read', 'rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['exception-list', 'exception-list-agnostic', ...savedObjectTypes],
|
||||
read: [
|
||||
'exception-list',
|
||||
'exception-list-agnostic',
|
||||
DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
...savedObjectTypes,
|
||||
],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
|
|
|
@ -35,8 +35,6 @@
|
|||
"ml": ["all"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"indexPatterns": ["all"],
|
||||
"savedObjectsManagement": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"],
|
||||
"dev_tools": ["all"]
|
||||
|
|
|
@ -39,8 +39,6 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"indexPatterns": ["read"],
|
||||
"savedObjectsManagement": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
|
|
@ -13,8 +13,3 @@ export * from './rule_author';
|
|||
export * from './soc_manager';
|
||||
export * from './t1_analyst';
|
||||
export * from './t2_analyst';
|
||||
|
||||
// TODO: Steph/sourcerer remove from detections_role.json once we have our internal saved object client
|
||||
// https://github.com/elastic/security-team/issues/1978
|
||||
// "indexPatterns": ["read"],
|
||||
// "savedObjectsManagement": ["read"],
|
||||
|
|
|
@ -34,8 +34,6 @@
|
|||
"ml": ["all"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"indexPatterns": ["all"],
|
||||
"savedObjectsManagement": ["all"],
|
||||
"actions": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
|
|
@ -28,8 +28,6 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"indexPatterns": ["read"],
|
||||
"savedObjectsManagement": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
},
|
||||
|
|
|
@ -37,8 +37,6 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"indexPatterns": ["read"],
|
||||
"savedObjectsManagement": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
|
|
@ -37,8 +37,6 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"indexPatterns": ["all"],
|
||||
"savedObjectsManagement": ["all"],
|
||||
"actions": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
|
|
@ -27,8 +27,6 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"indexPatterns": ["read"],
|
||||
"savedObjectsManagement": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
},
|
||||
|
|
|
@ -29,8 +29,6 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"indexPatterns": ["read"],
|
||||
"savedObjectsManagement": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
},
|
||||
|
|
|
@ -57,7 +57,7 @@ const getStartServices = jest.fn().mockReturnValue([
|
|||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
indexPatternsServiceFactory: () => ({
|
||||
dataViewsServiceFactory: () => ({
|
||||
getIdsWithTitle: () => new Promise((rs) => rs(mockDataViews)),
|
||||
get: () => new Promise((rs) => rs(mockPattern)),
|
||||
createAndSave: () => new Promise((rs) => rs(mockPattern)),
|
||||
|
@ -73,10 +73,18 @@ const getStartServicesNotSiem = jest.fn().mockReturnValue([
|
|||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
indexPatternsServiceFactory: () => ({
|
||||
dataViewsServiceFactory: () => ({
|
||||
getIdsWithTitle: () =>
|
||||
new Promise((rs) => rs(mockDataViews.filter((v) => v.id !== mockPattern.id))),
|
||||
get: () => new Promise((rs) => rs(mockPattern)),
|
||||
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)),
|
||||
}),
|
||||
|
@ -95,12 +103,10 @@ const mockDataViewsTransformed = {
|
|||
kibanaDataViews: [
|
||||
{
|
||||
id: 'metrics-*',
|
||||
patternList: ['metrics-*'],
|
||||
title: 'metrics-*',
|
||||
},
|
||||
{
|
||||
id: 'logs-*',
|
||||
patternList: ['logs-*'],
|
||||
title: 'logs-*',
|
||||
},
|
||||
{
|
||||
|
@ -112,58 +118,87 @@ const mockDataViewsTransformed = {
|
|||
],
|
||||
};
|
||||
|
||||
export const getSourcererRequest = (patternList: string[]) =>
|
||||
requestMock.create({
|
||||
method: 'post',
|
||||
path: SOURCERER_API_URL,
|
||||
body: { patternList },
|
||||
});
|
||||
|
||||
describe('sourcerer route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { context } = requestContextMock.createTools();
|
||||
|
||||
beforeEach(() => {
|
||||
server = serverMock.create();
|
||||
({ context } = requestContextMock.createTools());
|
||||
});
|
||||
describe('post', () => {
|
||||
const getSourcererRequest = (patternList: string[]) =>
|
||||
requestMock.create({
|
||||
method: 'post',
|
||||
path: SOURCERER_API_URL,
|
||||
body: { patternList },
|
||||
});
|
||||
|
||||
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), context);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockDataViewsTransformed);
|
||||
});
|
||||
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), context);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockDataViewsTransformed);
|
||||
});
|
||||
|
||||
test('returns sourcerer formatted Data Views when SIEM Data View exists', async () => {
|
||||
createSourcererDataViewRoute(server.router, getStartServices);
|
||||
const response = await server.inject(getSourcererRequest(mockPatternList), 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(null);
|
||||
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), 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), context);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
defaultDataView: {
|
||||
id: 'security-solution',
|
||||
patternList: ['traces-apm*', 'auditbeat-*'],
|
||||
title:
|
||||
'traces-apm*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*,ml_host_risk_score_*,.siem-signals-default',
|
||||
},
|
||||
kibanaDataViews: [
|
||||
mockDataViewsTransformed.kibanaDataViews[0],
|
||||
mockDataViewsTransformed.kibanaDataViews[1],
|
||||
{
|
||||
id: 'security-solution',
|
||||
patternList: ['traces-apm*', 'auditbeat-*'],
|
||||
title:
|
||||
'traces-apm*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*,ml_host_risk_score_*,.siem-signals-default',
|
||||
},
|
||||
],
|
||||
test('returns sourcerer formatted Data Views when SIEM Data View exists', async () => {
|
||||
createSourcererDataViewRoute(server.router, getStartServices);
|
||||
const response = await server.inject(getSourcererRequest(mockPatternList), 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), 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-*',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,19 @@
|
|||
*/
|
||||
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { StartServicesAccessor } from 'kibana/server';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
import type { ElasticsearchClient, StartServicesAccessor } from 'kibana/server';
|
||||
|
||||
import type {
|
||||
DataView,
|
||||
DataViewListItem,
|
||||
} from '../../../../../../../src/plugins/data_views/common';
|
||||
import { DEFAULT_TIME_FIELD, SOURCERER_API_URL } from '../../../../common/constants';
|
||||
import { buildSiemResponse } from '../../detection_engine/routes/utils';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../types';
|
||||
import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
|
||||
import { sourcererSchema } from './schema';
|
||||
import { StartPlugins } from '../../../plugin';
|
||||
import type { StartPlugins } from '../../../plugin';
|
||||
import { buildSiemResponse } from '../../detection_engine/routes/utils';
|
||||
import { findExistingIndices } from './helpers';
|
||||
import { sourcererDataViewSchema, sourcererSchema } from './schema';
|
||||
|
||||
export const createSourcererDataViewRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
|
@ -26,6 +31,7 @@ export const createSourcererDataViewRoute = (
|
|||
body: buildRouteValidation(sourcererSchema),
|
||||
},
|
||||
options: {
|
||||
authRequired: true,
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
},
|
||||
|
@ -33,6 +39,7 @@ export const createSourcererDataViewRoute = (
|
|||
const siemResponse = buildSiemResponse(response);
|
||||
const siemClient = context.securitySolution?.getAppClient();
|
||||
const dataViewId = siemClient.getSourcererDataViewId();
|
||||
|
||||
try {
|
||||
const [
|
||||
,
|
||||
|
@ -40,71 +47,70 @@ export const createSourcererDataViewRoute = (
|
|||
data: { indexPatterns },
|
||||
},
|
||||
] = await getStartServices();
|
||||
const dataViewService = await indexPatterns.indexPatternsServiceFactory(
|
||||
|
||||
const dataViewService = await indexPatterns.dataViewsServiceFactory(
|
||||
context.core.savedObjects.client,
|
||||
context.core.elasticsearch.client.asInternalUser,
|
||||
request
|
||||
context.core.elasticsearch.client.asCurrentUser,
|
||||
request,
|
||||
true
|
||||
);
|
||||
|
||||
let allDataViews = await dataViewService.getIdsWithTitle();
|
||||
const { patternList } = request.body;
|
||||
const siemDataView = allDataViews.find((v) => v.id === dataViewId);
|
||||
const patternListAsTitle = patternList.join();
|
||||
|
||||
if (siemDataView == null) {
|
||||
const defaultDataView = await dataViewService.createAndSave({
|
||||
allowNoIndex: true,
|
||||
id: dataViewId,
|
||||
title: patternListAsTitle,
|
||||
timeFieldName: DEFAULT_TIME_FIELD,
|
||||
});
|
||||
// ?? dataViewId -> type thing here, should never happen
|
||||
allDataViews.push({ ...defaultDataView, id: defaultDataView.id ?? dataViewId });
|
||||
} else if (patternListAsTitle !== siemDataView.title) {
|
||||
const defaultDataView = { ...siemDataView, id: siemDataView.id ?? '' };
|
||||
const wholeDataView = await dataViewService.get(defaultDataView.id);
|
||||
wholeDataView.title = patternListAsTitle;
|
||||
let didUpdate = true;
|
||||
await dataViewService.updateSavedObject(wholeDataView).catch((err) => {
|
||||
const error = transformError(err);
|
||||
if (error.statusCode === 403) {
|
||||
didUpdate = false;
|
||||
// user doesnt have permissions to update, use existing pattern
|
||||
wholeDataView.title = defaultDataView.title;
|
||||
return;
|
||||
}
|
||||
let allDataViews: DataViewListItem[] = await dataViewService.getIdsWithTitle();
|
||||
let siemDataView = null;
|
||||
try {
|
||||
siemDataView = await dataViewService.get(dataViewId);
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
// Do nothing if statusCode === 404 because we expect that the security dataview does not exist
|
||||
if (error.statusCode !== 404) {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// update the data view in allDataViews
|
||||
if (didUpdate) {
|
||||
allDataViews = allDataViews.map((v) =>
|
||||
v.id === dataViewId ? { ...v, title: patternListAsTitle } : v
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const patternLists: string[][] = allDataViews.map(({ title }) => title.split(','));
|
||||
const activePatternBools: boolean[][] = await Promise.all(
|
||||
patternLists.map((pl) =>
|
||||
findExistingIndices(pl, context.core.elasticsearch.client.asCurrentUser)
|
||||
)
|
||||
);
|
||||
const { patternList } = request.body;
|
||||
const patternListAsTitle = patternList.sort().join();
|
||||
const siemDataViewTitle = siemDataView ? siemDataView.title.split(',').sort().join() : '';
|
||||
if (siemDataView == null) {
|
||||
try {
|
||||
siemDataView = await dataViewService.createAndSave({
|
||||
allowNoIndex: true,
|
||||
id: dataViewId,
|
||||
title: patternListAsTitle,
|
||||
timeFieldName: DEFAULT_TIME_FIELD,
|
||||
});
|
||||
} 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.title = patternListAsTitle;
|
||||
await dataViewService.updateSavedObject(siemDataView);
|
||||
}
|
||||
|
||||
const activePatternLists = patternLists.map((pl, i) =>
|
||||
// also remove duplicates from active
|
||||
pl.filter((pattern, j, self) => self.indexOf(pattern) === j && activePatternBools[i][j])
|
||||
);
|
||||
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 kibanaDataViews = allDataViews.map((kip, i) => ({
|
||||
...kip,
|
||||
patternList: activePatternLists[i],
|
||||
}));
|
||||
const body = {
|
||||
defaultDataView: kibanaDataViews.find((p) => p.id === dataViewId) ?? {},
|
||||
kibanaDataViews,
|
||||
};
|
||||
return response.ok({ body });
|
||||
const defaultDataView = await buildSourcererDataView(
|
||||
siemDataView,
|
||||
context.core.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({
|
||||
|
@ -118,3 +124,72 @@ export const createSourcererDataViewRoute = (
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
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 { dataViewId } = request.query;
|
||||
try {
|
||||
const [
|
||||
,
|
||||
{
|
||||
data: { indexPatterns },
|
||||
},
|
||||
] = await getStartServices();
|
||||
|
||||
const dataViewService = await indexPatterns.dataViewsServiceFactory(
|
||||
context.core.savedObjects.client,
|
||||
context.core.elasticsearch.client.asCurrentUser,
|
||||
request,
|
||||
true
|
||||
);
|
||||
|
||||
const siemDataView = await dataViewService.get(dataViewId);
|
||||
const kibanaDataView = siemDataView
|
||||
? await buildSourcererDataView(
|
||||
siemDataView,
|
||||
context.core.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const buildSourcererDataView = async (
|
||||
dataView: DataView,
|
||||
clientAsCurrentUser: ElasticsearchClient
|
||||
) => {
|
||||
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 { ...dataView, patternList: activePatternLists };
|
||||
};
|
||||
|
|
|
@ -10,3 +10,7 @@ import * as t from 'io-ts';
|
|||
export const sourcererSchema = t.type({
|
||||
patternList: t.array(t.string),
|
||||
});
|
||||
|
||||
export const sourcererDataViewSchema = t.type({
|
||||
dataViewId: t.string,
|
||||
});
|
||||
|
|
|
@ -67,7 +67,7 @@ import {
|
|||
} from '../lib/detection_engine/rule_types/types';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyCreateLegacyNotificationRoute } from '../lib/detection_engine/routes/rules/legacy_create_legacy_notification';
|
||||
import { createSourcererDataViewRoute } from '../lib/sourcerer/routes';
|
||||
import { createSourcererDataViewRoute, getSourcererDataViewRoute } from '../lib/sourcerer/routes';
|
||||
|
||||
export const initRoutes = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
|
@ -160,4 +160,5 @@ export const initRoutes = (
|
|||
|
||||
// Sourcerer API to generate default pattern
|
||||
createSourcererDataViewRoute(router, getStartServices);
|
||||
getSourcererDataViewRoute(router, getStartServices);
|
||||
};
|
||||
|
|
|
@ -817,7 +817,7 @@ describe('Fields Provider', () => {
|
|||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
indexPatternsServiceFactory: () => ({
|
||||
dataViewsServiceFactory: () => ({
|
||||
get: jest.fn().mockReturnValue(mockPattern),
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -72,7 +72,7 @@ export const findExistingIndices = async (
|
|||
|
||||
export const requestIndexFieldSearch = async (
|
||||
request: IndexFieldsStrategyRequest<'indices' | 'dataView'>,
|
||||
{ savedObjectsClient, esClient }: SearchStrategyDependencies,
|
||||
{ savedObjectsClient, esClient, request: kRequest }: SearchStrategyDependencies,
|
||||
beatFields: BeatFields,
|
||||
getStartServices: StartServicesAccessor<StartPlugins>
|
||||
): Promise<IndexFieldsStrategyResponse> => {
|
||||
|
@ -87,9 +87,12 @@ export const requestIndexFieldSearch = async (
|
|||
data: { indexPatterns },
|
||||
},
|
||||
] = await getStartServices();
|
||||
const dataViewService = await indexPatterns.indexPatternsServiceFactory(
|
||||
|
||||
const dataViewService = await indexPatterns.dataViewsServiceFactory(
|
||||
savedObjectsClient,
|
||||
esClient.asCurrentUser
|
||||
esClient.asCurrentUser,
|
||||
kRequest,
|
||||
true
|
||||
);
|
||||
|
||||
let indicesExist: string[] = [];
|
||||
|
@ -119,6 +122,7 @@ export const requestIndexFieldSearch = async (
|
|||
(acc: string[], doesIndexExist, i) => (doesIndexExist ? [...acc, patternList[i]] : acc),
|
||||
[]
|
||||
);
|
||||
|
||||
if (!request.onlyCheckIfIndicesExist) {
|
||||
const dataViewSpec = dataView.toSpec();
|
||||
const fieldDescriptor = [Object.values(dataViewSpec.fields ?? {})];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue