mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Detections] Update signals template if outdated and rollover indices (#80019)
* Modify create_index_route to update template in place if outdated * Update frontend to always call create_index_route * Add template status to GET route * Clean up parameter type * Fix tests and types * Add test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
725550f58f
commit
0c7ca14630
9 changed files with 128 additions and 43 deletions
|
@ -4,20 +4,17 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useUserInfo } from './index';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useUserInfo, ManageUserInfo } from './index';
|
||||
|
||||
import { usePrivilegeUser } from '../../containers/detection_engine/alerts/use_privilege_user';
|
||||
import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
jest.mock('../../containers/detection_engine/alerts/use_privilege_user');
|
||||
jest.mock('../../containers/detection_engine/alerts/use_signal_index');
|
||||
import * as api from '../../containers/detection_engine/alerts/api';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../containers/detection_engine/alerts/api');
|
||||
|
||||
describe('useUserInfo', () => {
|
||||
beforeAll(() => {
|
||||
(usePrivilegeUser as jest.Mock).mockReturnValue({});
|
||||
(useSignalIndex as jest.Mock).mockReturnValue({});
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
|
@ -30,21 +27,40 @@ describe('useUserInfo', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
it('returns default state', () => {
|
||||
const { result } = renderHook(() => useUserInfo());
|
||||
it('returns default state', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useUserInfo());
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result).toEqual({
|
||||
current: {
|
||||
canUserCRUD: null,
|
||||
hasEncryptionKey: null,
|
||||
hasIndexManage: null,
|
||||
hasIndexWrite: null,
|
||||
isAuthenticated: null,
|
||||
isSignalIndexExists: null,
|
||||
loading: true,
|
||||
signalIndexName: null,
|
||||
},
|
||||
error: undefined,
|
||||
expect(result).toEqual({
|
||||
current: {
|
||||
canUserCRUD: null,
|
||||
hasEncryptionKey: null,
|
||||
hasIndexManage: null,
|
||||
hasIndexWrite: null,
|
||||
isAuthenticated: null,
|
||||
isSignalIndexExists: null,
|
||||
loading: true,
|
||||
signalIndexName: null,
|
||||
signalIndexTemplateOutdated: null,
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls createSignalIndex if signal index template is outdated', async () => {
|
||||
const spyOnCreateSignalIndex = jest.spyOn(api, 'createSignalIndex');
|
||||
const spyOnGetSignalIndex = jest.spyOn(api, 'getSignalIndex').mockResolvedValueOnce({
|
||||
name: 'mock-signal-index',
|
||||
template_outdated: true,
|
||||
});
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useUserInfo(), { wrapper: ManageUserInfo });
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
expect(spyOnGetSignalIndex).toHaveBeenCalledTimes(2);
|
||||
expect(spyOnCreateSignalIndex).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ export interface State {
|
|||
hasEncryptionKey: boolean | null;
|
||||
loading: boolean;
|
||||
signalIndexName: string | null;
|
||||
signalIndexTemplateOutdated: boolean | null;
|
||||
}
|
||||
|
||||
export const initialState: State = {
|
||||
|
@ -31,6 +32,7 @@ export const initialState: State = {
|
|||
hasEncryptionKey: null,
|
||||
loading: true,
|
||||
signalIndexName: null,
|
||||
signalIndexTemplateOutdated: null,
|
||||
};
|
||||
|
||||
export type Action =
|
||||
|
@ -62,6 +64,10 @@ export type Action =
|
|||
| {
|
||||
type: 'updateSignalIndexName';
|
||||
signalIndexName: string | null;
|
||||
}
|
||||
| {
|
||||
type: 'updateSignalIndexTemplateOutdated';
|
||||
signalIndexTemplateOutdated: boolean | null;
|
||||
};
|
||||
|
||||
export const userInfoReducer = (state: State, action: Action): State => {
|
||||
|
@ -114,6 +120,12 @@ export const userInfoReducer = (state: State, action: Action): State => {
|
|||
signalIndexName: action.signalIndexName,
|
||||
};
|
||||
}
|
||||
case 'updateSignalIndexTemplateOutdated': {
|
||||
return {
|
||||
...state,
|
||||
signalIndexTemplateOutdated: action.signalIndexTemplateOutdated,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -144,6 +156,7 @@ export const useUserInfo = (): State => {
|
|||
hasEncryptionKey,
|
||||
loading,
|
||||
signalIndexName,
|
||||
signalIndexTemplateOutdated,
|
||||
},
|
||||
dispatch,
|
||||
] = useUserData();
|
||||
|
@ -158,6 +171,7 @@ export const useUserInfo = (): State => {
|
|||
loading: indexNameLoading,
|
||||
signalIndexExists: isApiSignalIndexExists,
|
||||
signalIndexName: apiSignalIndexName,
|
||||
signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated,
|
||||
createDeSignalIndex: createSignalIndex,
|
||||
} = useSignalIndex();
|
||||
|
||||
|
@ -166,7 +180,7 @@ export const useUserInfo = (): State => {
|
|||
typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;
|
||||
|
||||
useEffect(() => {
|
||||
if (loading !== privilegeLoading || indexNameLoading) {
|
||||
if (loading !== (privilegeLoading || indexNameLoading)) {
|
||||
dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading });
|
||||
}
|
||||
}, [dispatch, loading, privilegeLoading, indexNameLoading]);
|
||||
|
@ -217,18 +231,38 @@ export const useUserInfo = (): State => {
|
|||
}
|
||||
}, [dispatch, loading, signalIndexName, apiSignalIndexName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!loading &&
|
||||
signalIndexTemplateOutdated !== apiSignalIndexTemplateOutdated &&
|
||||
apiSignalIndexTemplateOutdated != null
|
||||
) {
|
||||
dispatch({
|
||||
type: 'updateSignalIndexTemplateOutdated',
|
||||
signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated,
|
||||
});
|
||||
}
|
||||
}, [dispatch, loading, signalIndexTemplateOutdated, apiSignalIndexTemplateOutdated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isAuthenticated &&
|
||||
hasEncryptionKey &&
|
||||
hasIndexManage &&
|
||||
isSignalIndexExists != null &&
|
||||
!isSignalIndexExists &&
|
||||
((isSignalIndexExists != null && !isSignalIndexExists) ||
|
||||
(signalIndexTemplateOutdated != null && signalIndexTemplateOutdated)) &&
|
||||
createSignalIndex != null
|
||||
) {
|
||||
createSignalIndex();
|
||||
}
|
||||
}, [createSignalIndex, isAuthenticated, hasEncryptionKey, isSignalIndexExists, hasIndexManage]);
|
||||
}, [
|
||||
createSignalIndex,
|
||||
isAuthenticated,
|
||||
hasEncryptionKey,
|
||||
isSignalIndexExists,
|
||||
hasIndexManage,
|
||||
signalIndexTemplateOutdated,
|
||||
]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
|
@ -239,5 +273,6 @@ export const useUserInfo = (): State => {
|
|||
hasIndexManage,
|
||||
hasIndexWrite,
|
||||
signalIndexName,
|
||||
signalIndexTemplateOutdated,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -980,6 +980,7 @@ export const mockStatusAlertQuery: object = {
|
|||
|
||||
export const mockSignalIndex: AlertsIndex = {
|
||||
name: 'mock-signal-index',
|
||||
template_outdated: false,
|
||||
};
|
||||
|
||||
export const mockUserPrivilege: Privilege = {
|
||||
|
|
|
@ -44,6 +44,7 @@ export interface UpdateAlertStatusProps {
|
|||
|
||||
export interface AlertsIndex {
|
||||
name: string;
|
||||
template_outdated: boolean;
|
||||
}
|
||||
|
||||
export interface Privilege {
|
||||
|
|
|
@ -26,6 +26,7 @@ describe('useSignalIndex', () => {
|
|||
loading: true,
|
||||
signalIndexExists: null,
|
||||
signalIndexName: null,
|
||||
signalIndexTemplateOutdated: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -42,6 +43,7 @@ describe('useSignalIndex', () => {
|
|||
loading: false,
|
||||
signalIndexExists: true,
|
||||
signalIndexName: 'mock-signal-index',
|
||||
signalIndexTemplateOutdated: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -62,6 +64,7 @@ describe('useSignalIndex', () => {
|
|||
loading: false,
|
||||
signalIndexExists: true,
|
||||
signalIndexName: 'mock-signal-index',
|
||||
signalIndexTemplateOutdated: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -101,6 +104,7 @@ describe('useSignalIndex', () => {
|
|||
loading: false,
|
||||
signalIndexExists: false,
|
||||
signalIndexName: null,
|
||||
signalIndexTemplateOutdated: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -121,6 +125,7 @@ describe('useSignalIndex', () => {
|
|||
loading: false,
|
||||
signalIndexExists: false,
|
||||
signalIndexName: null,
|
||||
signalIndexTemplateOutdated: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface ReturnSignalIndex {
|
|||
loading: boolean;
|
||||
signalIndexExists: boolean | null;
|
||||
signalIndexName: string | null;
|
||||
signalIndexTemplateOutdated: boolean | null;
|
||||
createDeSignalIndex: Func | null;
|
||||
}
|
||||
|
||||
|
@ -27,11 +28,10 @@ export interface ReturnSignalIndex {
|
|||
*/
|
||||
export const useSignalIndex = (): ReturnSignalIndex => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [signalIndex, setSignalIndex] = useState<
|
||||
Pick<ReturnSignalIndex, 'signalIndexExists' | 'signalIndexName' | 'createDeSignalIndex'>
|
||||
>({
|
||||
const [signalIndex, setSignalIndex] = useState<Omit<ReturnSignalIndex, 'loading'>>({
|
||||
signalIndexExists: null,
|
||||
signalIndexName: null,
|
||||
signalIndexTemplateOutdated: null,
|
||||
createDeSignalIndex: null,
|
||||
});
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
@ -49,6 +49,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
|
|||
setSignalIndex({
|
||||
signalIndexExists: true,
|
||||
signalIndexName: signal.name,
|
||||
signalIndexTemplateOutdated: signal.template_outdated,
|
||||
createDeSignalIndex: createIndex,
|
||||
});
|
||||
}
|
||||
|
@ -57,6 +58,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
|
|||
setSignalIndex({
|
||||
signalIndexExists: false,
|
||||
signalIndexName: null,
|
||||
signalIndexTemplateOutdated: null,
|
||||
createDeSignalIndex: createIndex,
|
||||
});
|
||||
if (isSecurityAppError(error) && error.body.status_code !== 404) {
|
||||
|
@ -87,6 +89,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
|
|||
setSignalIndex({
|
||||
signalIndexExists: false,
|
||||
signalIndexName: null,
|
||||
signalIndexTemplateOutdated: null,
|
||||
createDeSignalIndex: createIndex,
|
||||
});
|
||||
errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster });
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { LegacyAPICaller } from '../../../../../../../../src/core/server';
|
||||
import { getSignalsTemplate } from './get_signals_template';
|
||||
import { getTemplateExists } from '../../index/get_template_exists';
|
||||
|
||||
export const templateNeedsUpdate = async (callCluster: LegacyAPICaller, index: string) => {
|
||||
const templateExists = await getTemplateExists(callCluster, index);
|
||||
let existingTemplateVersion: number | undefined;
|
||||
if (templateExists) {
|
||||
const existingTemplate: unknown = await callCluster('indices.getTemplate', {
|
||||
name: index,
|
||||
});
|
||||
existingTemplateVersion = get(existingTemplate, [index, 'version']);
|
||||
}
|
||||
const newTemplate = getSignalsTemplate(index);
|
||||
if (existingTemplateVersion === undefined || existingTemplateVersion < newTemplate.version) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -12,9 +12,9 @@ import { getPolicyExists } from '../../index/get_policy_exists';
|
|||
import { setPolicy } from '../../index/set_policy';
|
||||
import { setTemplate } from '../../index/set_template';
|
||||
import { getSignalsTemplate } from './get_signals_template';
|
||||
import { getTemplateExists } from '../../index/get_template_exists';
|
||||
import { createBootstrapIndex } from '../../index/create_bootstrap_index';
|
||||
import signalsPolicy from './signals_policy.json';
|
||||
import { templateNeedsUpdate } from './check_template_version';
|
||||
|
||||
export const createIndexRoute = (router: IRouter) => {
|
||||
router.post(
|
||||
|
@ -39,24 +39,20 @@ export const createIndexRoute = (router: IRouter) => {
|
|||
|
||||
const index = siemClient.getSignalsIndex();
|
||||
const indexExists = await getIndexExists(callCluster, index);
|
||||
if (indexExists) {
|
||||
return siemResponse.error({
|
||||
statusCode: 409,
|
||||
body: `index: "${index}" already exists`,
|
||||
});
|
||||
} else {
|
||||
if (await templateNeedsUpdate(callCluster, index)) {
|
||||
const policyExists = await getPolicyExists(callCluster, index);
|
||||
if (!policyExists) {
|
||||
await setPolicy(callCluster, index, signalsPolicy);
|
||||
}
|
||||
const templateExists = await getTemplateExists(callCluster, index);
|
||||
if (!templateExists) {
|
||||
const template = getSignalsTemplate(index);
|
||||
await setTemplate(callCluster, index, template);
|
||||
await setTemplate(callCluster, index, getSignalsTemplate(index));
|
||||
if (indexExists) {
|
||||
await callCluster('indices.rollover', { alias: index });
|
||||
}
|
||||
await createBootstrapIndex(callCluster, index);
|
||||
return response.ok({ body: { acknowledged: true } });
|
||||
}
|
||||
if (!indexExists) {
|
||||
await createBootstrapIndex(callCluster, index);
|
||||
}
|
||||
return response.ok({ body: { acknowledged: true } });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
|
|
|
@ -8,6 +8,7 @@ import { IRouter } from '../../../../../../../../src/core/server';
|
|||
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
|
||||
import { transformError, buildSiemResponse } from '../utils';
|
||||
import { getIndexExists } from '../../index/get_index_exists';
|
||||
import { templateNeedsUpdate } from './check_template_version';
|
||||
|
||||
export const readIndexRoute = (router: IRouter) => {
|
||||
router.get(
|
||||
|
@ -31,9 +32,10 @@ export const readIndexRoute = (router: IRouter) => {
|
|||
|
||||
const index = siemClient.getSignalsIndex();
|
||||
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index);
|
||||
const templateOutdated = await templateNeedsUpdate(clusterClient.callAsCurrentUser, index);
|
||||
|
||||
if (indexExists) {
|
||||
return response.ok({ body: { name: index } });
|
||||
return response.ok({ body: { name: index, template_outdated: templateOutdated } });
|
||||
} else {
|
||||
return siemResponse.error({
|
||||
statusCode: 404,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue