Skip to content

Commit

Permalink
[Security Solution][Detections] Update signals template if outdated a…
Browse files Browse the repository at this point in the history
…nd rollover indices (#80019) (#80616)

* 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>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
marshallmain and kibanamachine authored Oct 15, 2020
1 parent 2ceec2b commit 00b3d87
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface State {
hasEncryptionKey: boolean | null;
loading: boolean;
signalIndexName: string | null;
signalIndexTemplateOutdated: boolean | null;
}

const initialState: State = {
Expand All @@ -31,6 +32,7 @@ const initialState: State = {
hasEncryptionKey: null,
loading: true,
signalIndexName: null,
signalIndexTemplateOutdated: null,
};

export type Action =
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -144,6 +156,7 @@ export const useUserInfo = (): State => {
hasEncryptionKey,
loading,
signalIndexName,
signalIndexTemplateOutdated,
},
dispatch,
] = useUserData();
Expand All @@ -158,6 +171,7 @@ export const useUserInfo = (): State => {
loading: indexNameLoading,
signalIndexExists: isApiSignalIndexExists,
signalIndexName: apiSignalIndexName,
signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated,
createDeSignalIndex: createSignalIndex,
} = useSignalIndex();

Expand All @@ -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]);
Expand Down Expand Up @@ -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,
Expand All @@ -239,5 +273,6 @@ export const useUserInfo = (): State => {
hasIndexManage,
hasIndexWrite,
signalIndexName,
signalIndexTemplateOutdated,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,7 @@ export const mockStatusAlertQuery: object = {

export const mockSignalIndex: AlertsIndex = {
name: 'mock-signal-index',
template_outdated: false,
};

export const mockUserPrivilege: Privilege = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface UpdateAlertStatusProps {

export interface AlertsIndex {
name: string;
template_outdated: boolean;
}

export interface Privilege {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('useSignalIndex', () => {
loading: true,
signalIndexExists: null,
signalIndexName: null,
signalIndexTemplateOutdated: null,
});
});
});
Expand All @@ -42,6 +43,7 @@ describe('useSignalIndex', () => {
loading: false,
signalIndexExists: true,
signalIndexName: 'mock-signal-index',
signalIndexTemplateOutdated: false,
});
});
});
Expand All @@ -62,6 +64,7 @@ describe('useSignalIndex', () => {
loading: false,
signalIndexExists: true,
signalIndexName: 'mock-signal-index',
signalIndexTemplateOutdated: false,
});
});
});
Expand Down Expand Up @@ -101,6 +104,7 @@ describe('useSignalIndex', () => {
loading: false,
signalIndexExists: false,
signalIndexName: null,
signalIndexTemplateOutdated: null,
});
});
});
Expand All @@ -121,6 +125,7 @@ describe('useSignalIndex', () => {
loading: false,
signalIndexExists: false,
signalIndexName: null,
signalIndexTemplateOutdated: null,
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ReturnSignalIndex {
loading: boolean;
signalIndexExists: boolean | null;
signalIndexName: string | null;
signalIndexTemplateOutdated: boolean | null;
createDeSignalIndex: Func | null;
}

Expand All @@ -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();
Expand All @@ -49,6 +49,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
setSignalIndex({
signalIndexExists: true,
signalIndexName: signal.name,
signalIndexTemplateOutdated: signal.template_outdated,
createDeSignalIndex: createIndex,
});
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 });
}
}
if (!indexExists) {
await createBootstrapIndex(callCluster, index);
return response.ok({ body: { acknowledged: true } });
}
return response.ok({ body: { acknowledged: true } });
} catch (err) {
const error = transformError(err);
return siemResponse.error({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down

0 comments on commit 00b3d87

Please sign in to comment.