[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:
Marshall Main 2020-10-15 02:11:42 -04:00 committed by GitHub
parent 725550f58f
commit 0c7ca14630
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 128 additions and 43 deletions

View file

@ -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);
});
});

View file

@ -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,
};
};

View file

@ -980,6 +980,7 @@ export const mockStatusAlertQuery: object = {
export const mockSignalIndex: AlertsIndex = {
name: 'mock-signal-index',
template_outdated: false,
};
export const mockUserPrivilege: Privilege = {

View file

@ -44,6 +44,7 @@ export interface UpdateAlertStatusProps {
export interface AlertsIndex {
name: string;
template_outdated: boolean;
}
export interface Privilege {

View file

@ -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,
});
});
});

View file

@ -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 });

View file

@ -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;
};

View file

@ -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({

View file

@ -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,