[SIEM][Detection Engine] Adds critical missing status route to prepackaged rules (#55080)

## Summary

* Fixes a critical bug where the missing status for the REST route was missing
* Fixes a bug with the 400 not being used for the missing index in some cases
* Changes create and update to NO LONGER allow immutable to be passed
* Fixes a bug with the add prepackaged schema to where you could use `immutable: false`. Now it is required to be missing or set to `immutable: true` within it. 
* Cleans up unit tests

To use the critical bug missing status for the REST route:

```ts
GET /api/detection_engine/rules/prepackaged/_status 
```

And you will get back:

```ts
{
  "rules_installed": 252,
  "rules_not_installed": 87,
  "rules_not_updated": 0
}
```

See the script:

```ts
get_prepackaged_rules_status.sh
```

for more details

### Checklist

Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR.

~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~

~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~

~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~

- [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios

~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~

### For maintainers

~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~

- [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
Frank Hassanabad 2020-01-16 11:52:48 -07:00 committed by GitHub
parent 40ec30a367
commit 6559224c51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 788 additions and 312 deletions

View file

@ -28,6 +28,7 @@ import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete
import { importRulesRoute } from './lib/detection_engine/routes/rules/import_rules_route';
import { exportRulesRoute } from './lib/detection_engine/routes/rules/export_rules_route';
import { findRulesStatusesRoute } from './lib/detection_engine/routes/rules/find_rules_status_route';
import { getPrepackagedRulesStatusRoute } from './lib/detection_engine/routes/rules/get_prepackaged_rules_status_route';
const APP_ID = 'siem';
@ -49,12 +50,16 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy
updateRulesRoute(__legacy);
deleteRulesRoute(__legacy);
findRulesRoute(__legacy);
addPrepackedRulesRoute(__legacy);
getPrepackagedRulesStatusRoute(__legacy);
createRulesBulkRoute(__legacy);
updateRulesBulkRoute(__legacy);
deleteRulesBulkRoute(__legacy);
importRulesRoute(__legacy);
exportRulesRoute(__legacy);
findRulesStatusesRoute(__legacy);
// Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals

View file

@ -10,9 +10,12 @@ import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch';
import { savedObjectsClientMock } from '../../../../../../../../../src/core/server/mocks';
import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock';
import { actionsClientMock } from '../../../../../../actions/server/actions_client.mock';
import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../../common/constants';
import { ServerFacade } from '../../../../types';
const defaultConfig = {
'kibana.index': '.kibana',
[`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`]: '.siem-signals',
};
const isKibanaConfig = (config: unknown): config is KibanaConfig =>
@ -58,10 +61,10 @@ export const createMockServer = (config: Record<string, string> = defaultConfig)
server.decorate('request', 'getBasePath', () => '/s/default');
server.decorate('request', 'getActionsClient', () => actionsClient);
server.plugins.elasticsearch = (elasticsearch as unknown) as ElasticsearchPlugin;
server.plugins.spaces = { getSpaceId: () => 'default' };
server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient);
return {
server,
server: server as ServerFacade & Hapi.Server,
alertsClient,
actionsClient,
elasticsearch,
@ -82,7 +85,10 @@ export const createMockServerWithoutAlertClientDecoration = (
serverWithoutAlertClient.decorate('request', 'getBasePath', () => '/s/default');
serverWithoutAlertClient.decorate('request', 'getActionsClient', () => actionsClient);
return { serverWithoutAlertClient, actionsClient };
return {
serverWithoutAlertClient: serverWithoutAlertClient as ServerFacade & Hapi.Server,
actionsClient,
};
};
export const createMockServerWithoutActionClientDecoration = (
@ -98,7 +104,10 @@ export const createMockServerWithoutActionClientDecoration = (
serverWithoutActionClient.decorate('request', 'getBasePath', () => '/s/default');
serverWithoutActionClient.decorate('request', 'getAlertsClient', () => alertsClient);
return { serverWithoutActionClient, alertsClient };
return {
serverWithoutActionClient: serverWithoutActionClient as ServerFacade & Hapi.Server,
alertsClient,
};
};
export const createMockServerWithoutActionOrAlertClientDecoration = (
@ -111,6 +120,22 @@ export const createMockServerWithoutActionOrAlertClientDecoration = (
serverWithoutActionOrAlertClient.config = () => createMockKibanaConfig(config);
return {
serverWithoutActionOrAlertClient,
serverWithoutActionOrAlertClient: serverWithoutActionOrAlertClient as ServerFacade &
Hapi.Server,
};
};
export const getMockIndexName = () =>
jest.fn().mockImplementation(() => ({
callWithRequest: jest.fn().mockImplementationOnce(() => 'index-name'),
}));
export const getMockEmptyIndex = () =>
jest.fn().mockImplementation(() => ({
callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 0 } })),
}));
export const getMockNonEmptyIndex = () =>
jest.fn().mockImplementation(() => ({
callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 1 } })),
}));

View file

@ -15,6 +15,7 @@ import {
DETECTION_ENGINE_QUERY_SIGNALS_URL,
INTERNAL_RULE_ID_KEY,
INTERNAL_IMMUTABLE_KEY,
DETECTION_ENGINE_PREPACKAGED_URL,
} from '../../../../../common/constants';
import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
import { RuleAlertParamsRest } from '../../types';
@ -157,7 +158,17 @@ export const getDeleteAsPostBulkRequest = (): ServerInjectOptions => ({
export const getPrivilegeRequest = (): ServerInjectOptions => ({
method: 'GET',
url: `${DETECTION_ENGINE_PRIVILEGES_URL}`,
url: DETECTION_ENGINE_PRIVILEGES_URL,
});
export const addPrepackagedRulesRequest = (): ServerInjectOptions => ({
method: 'PUT',
url: DETECTION_ENGINE_PREPACKAGED_URL,
});
export const getPrepackagedRulesStatusRequest = (): ServerInjectOptions => ({
method: 'GET',
url: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`,
});
export interface FindHit {

View file

@ -7,7 +7,6 @@
import { createMockServer } from '../__mocks__/_mock_server';
import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses';
import { readPrivilegesRoute } from './read_privileges_route';
import { ServerFacade } from '../../../../types';
import * as myUtils from '../utils';
describe('read_privileges', () => {
@ -19,7 +18,7 @@ describe('read_privileges', () => {
elasticsearch.getCluster = jest.fn(() => ({
callWithRequest: jest.fn(() => getMockPrivileges()),
}));
readPrivilegesRoute((server as unknown) as ServerFacade);
readPrivilegesRoute(server);
});
afterEach(() => {

View file

@ -0,0 +1,138 @@
/*
* 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 {
createMockServer,
createMockServerWithoutActionClientDecoration,
createMockServerWithoutAlertClientDecoration,
createMockServerWithoutActionOrAlertClientDecoration,
getMockEmptyIndex,
getMockNonEmptyIndex,
} from '../__mocks__/_mock_server';
import { createRulesRoute } from './create_rules_route';
import {
getFindResult,
getResult,
createActionResult,
addPrepackagedRulesRequest,
getFindResultWithSingleHit,
} from '../__mocks__/request_responses';
jest.mock('../../rules/get_prepackaged_rules', () => {
return {
getPrepackagedRules: () => {
return [
{
rule_id: 'rule-1',
output_index: '.siem-signals',
risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
index: ['index-1'],
name: 'some-name',
severity: 'severity',
interval: '5m',
type: 'query',
version: 2, // set one higher than the mocks which is set to 1 to trigger updates
},
];
},
};
});
import { addPrepackedRulesRoute } from './add_prepackaged_rules_route';
describe('add_prepackaged_rules_route', () => {
let { server, alertsClient, actionsClient, elasticsearch } = createMockServer();
beforeEach(() => {
jest.resetAllMocks();
({ server, alertsClient, actionsClient, elasticsearch } = createMockServer());
elasticsearch.getCluster = getMockNonEmptyIndex();
addPrepackedRulesRoute(server);
});
describe('status codes with actionClient and alertClient', () => {
test('returns 200 when creating a with a valid actionClient and alertClient', async () => {
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { statusCode } = await server.inject(addPrepackagedRulesRequest());
expect(statusCode).toBe(200);
});
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
createRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(addPrepackagedRulesRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
createRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(addPrepackagedRulesRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient and actionClient are both not available on the route', async () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
createRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(
addPrepackagedRulesRequest()
);
expect(statusCode).toBe(404);
});
});
describe('validation', () => {
test('it returns a 400 if the index does not exist', async () => {
elasticsearch.getCluster = getMockEmptyIndex();
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(addPrepackagedRulesRequest());
expect(JSON.parse(payload)).toEqual({
error: 'Bad Request',
message:
'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default',
statusCode: 400,
});
});
});
describe('payload', () => {
test('1 rule is installed and 0 are updated when find results are empty', async () => {
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(addPrepackagedRulesRequest());
expect(JSON.parse(payload)).toEqual({
rules_installed: 1,
rules_updated: 0,
});
});
test('1 rule is updated and 0 are installed when we return a single find and the versions are different', async () => {
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(addPrepackagedRulesRequest());
expect(JSON.parse(payload)).toEqual({
rules_installed: 0,
rules_updated: 1,
});
});
});
});

View file

@ -45,15 +45,15 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR
const callWithRequest = callWithRequestFactory(request, server);
const rulesFromFileSystem = getPrepackagedRules();
const prepackedRules = await getExistingPrepackagedRules({ alertsClient });
const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackedRules);
const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackedRules);
const prepackagedRules = await getExistingPrepackagedRules({ alertsClient });
const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules);
const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules);
const spaceIndex = getIndex(request, server);
if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) {
const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex);
if (!spaceIndexExists) {
throw new Boom(
return Boom.badRequest(
`Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}`
);
}

View file

@ -9,10 +9,10 @@ import {
createMockServerWithoutActionClientDecoration,
createMockServerWithoutAlertClientDecoration,
createMockServerWithoutActionOrAlertClientDecoration,
getMockEmptyIndex,
} from '../__mocks__/_mock_server';
import { createRulesRoute } from './create_rules_route';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import {
getFindResult,
getResult,
@ -29,11 +29,7 @@ describe('create_rules_bulk', () => {
beforeEach(() => {
jest.resetAllMocks();
({ server, alertsClient, actionsClient, elasticsearch } = createMockServer());
elasticsearch.getCluster = jest.fn().mockImplementation(() => ({
callWithRequest: jest.fn().mockImplementation(() => true),
}));
createRulesBulkRoute((server as unknown) as ServerFacade);
createRulesBulkRoute(server);
});
describe('status codes with actionClient and alertClient', () => {
@ -48,14 +44,14 @@ describe('create_rules_bulk', () => {
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
createRulesRoute((serverWithoutActionClient as unknown) as ServerFacade);
createRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(getReadBulkRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
createRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade);
createRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(getReadBulkRequest());
expect(statusCode).toBe(404);
});
@ -64,13 +60,32 @@ describe('create_rules_bulk', () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
createRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade);
createRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadBulkRequest());
expect(statusCode).toBe(404);
});
});
describe('validation', () => {
test('it gets a 409 if the index does not exist', async () => {
elasticsearch.getCluster = getMockEmptyIndex();
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(getReadBulkRequest());
expect(JSON.parse(payload)).toEqual([
{
error: {
message:
'To create a rule, the index must exist first. Index .siem-signals does not exist',
status_code: 400,
},
rule_id: 'rule-1',
},
]);
});
test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => {
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());

View file

@ -87,7 +87,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
if (!indexExists) {
return createBulkErrorObject({
ruleId: ruleIdOrUuid,
statusCode: 409,
statusCode: 400,
message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`,
});
}

View file

@ -9,10 +9,11 @@ import {
createMockServerWithoutActionClientDecoration,
createMockServerWithoutAlertClientDecoration,
createMockServerWithoutActionOrAlertClientDecoration,
getMockNonEmptyIndex,
getMockEmptyIndex,
} from '../__mocks__/_mock_server';
import { createRulesRoute } from './create_rules_route';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import {
getFindResult,
@ -42,13 +43,8 @@ describe('create_rules', () => {
elasticsearch,
savedObjectsClient,
} = createMockServer());
elasticsearch.getCluster = jest.fn().mockImplementation(() => ({
callWithRequest: jest
.fn()
.mockImplementation((endpoint, params) => ({ _shards: { total: 1 } })),
}));
createRulesRoute((server as unknown) as ServerFacade);
elasticsearch.getCluster = getMockNonEmptyIndex();
createRulesRoute(server);
});
describe('status codes with actionClient and alertClient', () => {
@ -64,14 +60,14 @@ describe('create_rules', () => {
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
createRulesRoute((serverWithoutActionClient as unknown) as ServerFacade);
createRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
createRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade);
createRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest());
expect(statusCode).toBe(404);
});
@ -80,13 +76,27 @@ describe('create_rules', () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
createRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade);
createRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(getCreateRequest());
expect(statusCode).toBe(404);
});
});
describe('validation', () => {
test('it returns a 400 if the index does not exist', async () => {
elasticsearch.getCluster = getMockEmptyIndex();
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(getCreateRequest());
expect(JSON.parse(payload)).toEqual({
error: 'Bad Request',
message: 'To create a rule, the index must exist first. Index .siem-signals does not exist',
statusCode: 400,
});
});
test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => {
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());

View file

@ -78,17 +78,14 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
const callWithRequest = callWithRequestFactory(request, server);
const indexExists = await getIndexExists(callWithRequest, finalIndex);
if (!indexExists) {
return new Boom(
`To create a rule, the index must exist first. Index ${finalIndex} does not exist`,
{
statusCode: 400,
}
return Boom.badRequest(
`To create a rule, the index must exist first. Index ${finalIndex} does not exist`
);
}
if (ruleId != null) {
const rule = await readRules({ alertsClient, ruleId });
if (rule != null) {
return new Boom(`rule_id: "${ruleId}" already exists`, { statusCode: 409 });
return Boom.conflict(`rule_id: "${ruleId}" already exists`);
}
}
const createdRule = await createRules({

View file

@ -22,7 +22,6 @@ import {
getDeleteAsPostBulkRequestById,
getFindResultStatus,
} from '../__mocks__/request_responses';
import { ServerFacade } from '../../../../types';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { deleteRulesBulkRoute } from './delete_rules_bulk_route';
@ -33,7 +32,7 @@ describe('delete_rules', () => {
beforeEach(() => {
({ server, alertsClient, savedObjectsClient } = createMockServer());
deleteRulesBulkRoute((server as unknown) as ServerFacade);
deleteRulesBulkRoute(server);
});
afterEach(() => {
@ -100,14 +99,14 @@ describe('delete_rules', () => {
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
deleteRulesBulkRoute((serverWithoutActionClient as unknown) as ServerFacade);
deleteRulesBulkRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(getDeleteBulkRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
deleteRulesBulkRoute((serverWithoutAlertClient as unknown) as ServerFacade);
deleteRulesBulkRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(getDeleteBulkRequest());
expect(statusCode).toBe(404);
});
@ -116,7 +115,7 @@ describe('delete_rules', () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
deleteRulesBulkRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade);
deleteRulesBulkRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteBulkRequest());
expect(statusCode).toBe(404);
});

View file

@ -13,7 +13,6 @@ import {
import { deleteRulesRoute } from './delete_rules_route';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import {
getFindResult,
@ -30,7 +29,7 @@ describe('delete_rules', () => {
beforeEach(() => {
({ server, alertsClient, savedObjectsClient } = createMockServer());
deleteRulesRoute((server as unknown) as ServerFacade);
deleteRulesRoute(server);
});
afterEach(() => {
@ -70,14 +69,14 @@ describe('delete_rules', () => {
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
deleteRulesRoute((serverWithoutActionClient as unknown) as ServerFacade);
deleteRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
deleteRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade);
deleteRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest());
expect(statusCode).toBe(404);
});
@ -86,7 +85,7 @@ describe('delete_rules', () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
deleteRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade);
deleteRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteRequest());
expect(statusCode).toBe(404);
});

View file

@ -13,7 +13,6 @@ import {
import { findRulesRoute } from './find_rules_route';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import { getFindResult, getResult, getFindRequest } from '../__mocks__/request_responses';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
@ -23,7 +22,7 @@ describe('find_rules', () => {
beforeEach(() => {
({ server, alertsClient, actionsClient } = createMockServer());
findRulesRoute((server as unknown) as ServerFacade);
findRulesRoute(server);
});
afterEach(() => {
@ -46,14 +45,14 @@ describe('find_rules', () => {
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
findRulesRoute((serverWithoutActionClient as unknown) as ServerFacade);
findRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(getFindRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
findRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade);
findRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest());
expect(statusCode).toBe(404);
});
@ -62,7 +61,7 @@ describe('find_rules', () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
findRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade);
findRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(getFindRequest());
expect(statusCode).toBe(404);
});

View file

@ -0,0 +1,125 @@
/*
* 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 {
createMockServer,
createMockServerWithoutActionClientDecoration,
createMockServerWithoutAlertClientDecoration,
createMockServerWithoutActionOrAlertClientDecoration,
getMockNonEmptyIndex,
} from '../__mocks__/_mock_server';
import { createRulesRoute } from './create_rules_route';
import {
getFindResult,
getResult,
createActionResult,
getFindResultWithSingleHit,
getPrepackagedRulesStatusRequest,
} from '../__mocks__/request_responses';
jest.mock('../../rules/get_prepackaged_rules', () => {
return {
getPrepackagedRules: () => {
return [
{
rule_id: 'rule-1',
output_index: '.siem-signals',
risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
index: ['index-1'],
name: 'some-name',
severity: 'severity',
interval: '5m',
type: 'query',
version: 2, // set one higher than the mocks which is set to 1 to trigger updates
},
];
},
};
});
import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route';
describe('get_prepackaged_rule_status_route', () => {
let { server, alertsClient, actionsClient, elasticsearch } = createMockServer();
beforeEach(() => {
jest.resetAllMocks();
({ server, alertsClient, actionsClient, elasticsearch } = createMockServer());
elasticsearch.getCluster = getMockNonEmptyIndex();
getPrepackagedRulesStatusRoute(server);
});
describe('status codes with actionClient and alertClient', () => {
test('returns 200 when creating a with a valid actionClient and alertClient', async () => {
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { statusCode } = await server.inject(getPrepackagedRulesStatusRequest());
expect(statusCode).toBe(200);
});
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
createRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(
getPrepackagedRulesStatusRequest()
);
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
createRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(
getPrepackagedRulesStatusRequest()
);
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient and actionClient are both not available on the route', async () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
createRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(
getPrepackagedRulesStatusRequest()
);
expect(statusCode).toBe(404);
});
});
describe('payload', () => {
test('0 rules installed, 1 rules not installed, and 1 rule not updated', async () => {
alertsClient.find.mockResolvedValue(getFindResult());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(getPrepackagedRulesStatusRequest());
expect(JSON.parse(payload)).toEqual({
rules_installed: 0,
rules_not_installed: 1,
rules_not_updated: 0,
});
});
test('1 rule installed, 0 rules not installed, and 1 rule to not updated', async () => {
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
alertsClient.get.mockResolvedValue(getResult());
actionsClient.create.mockResolvedValue(createActionResult());
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(getPrepackagedRulesStatusRequest());
expect(JSON.parse(payload)).toEqual({
rules_installed: 1,
rules_not_installed: 0,
rules_not_updated: 1,
});
});
});
});

View file

@ -0,0 +1,59 @@
/*
* 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 Hapi from 'hapi';
import { isFunction } from 'lodash/fp';
import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants';
import { ServerFacade, RequestFacade } from '../../../../types';
import { transformError } from '../utils';
import { getPrepackagedRules } from '../../rules/get_prepackaged_rules';
import { getRulesToInstall } from '../../rules/get_rules_to_install';
import { getRulesToUpdate } from '../../rules/get_rules_to_update';
import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules';
export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => {
return {
method: 'GET',
path: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`,
options: {
tags: ['access:siem'],
validate: {
options: {
abortEarly: false,
},
},
},
async handler(request: RequestFacade, headers) {
const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null;
const actionsClient = isFunction(request.getActionsClient)
? request.getActionsClient()
: null;
if (!alertsClient || !actionsClient) {
return headers.response().code(404);
}
try {
const rulesFromFileSystem = getPrepackagedRules();
const prepackagedRules = await getExistingPrepackagedRules({ alertsClient });
const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules);
const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules);
return {
rules_installed: prepackagedRules.length,
rules_not_installed: rulesToInstall.length,
rules_not_updated: rulesToUpdate.length,
};
} catch (err) {
return transformError(err);
}
},
};
};
export const getPrepackagedRulesStatusRoute = (server: ServerFacade): void => {
server.route(createGetPrepackagedRulesStatusRoute());
};

View file

@ -13,7 +13,6 @@ import {
import { readRulesRoute } from './read_rules_route';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import {
getFindResult,
@ -29,7 +28,7 @@ describe('read_signals', () => {
beforeEach(() => {
({ server, alertsClient, savedObjectsClient } = createMockServer());
readRulesRoute((server as unknown) as ServerFacade);
readRulesRoute(server);
});
afterEach(() => {
@ -47,14 +46,14 @@ describe('read_signals', () => {
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
readRulesRoute((serverWithoutActionClient as unknown) as ServerFacade);
readRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(getReadRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
readRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade);
readRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest());
expect(statusCode).toBe(404);
});
@ -63,7 +62,7 @@ describe('read_signals', () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
readRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade);
readRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadRequest());
expect(statusCode).toBe(404);
});

View file

@ -13,7 +13,6 @@ import {
import { updateRulesRoute } from './update_rules_route';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import {
getFindResult,
@ -33,7 +32,7 @@ describe('update_rules_bulk', () => {
beforeEach(() => {
jest.resetAllMocks();
({ server, alertsClient, actionsClient } = createMockServer());
updateRulesBulkRoute((server as unknown) as ServerFacade);
updateRulesBulkRoute(server);
});
describe('status codes with actionClient and alertClient', () => {
@ -73,14 +72,14 @@ describe('update_rules_bulk', () => {
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
updateRulesRoute((serverWithoutActionClient as unknown) as ServerFacade);
updateRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(getUpdateBulkRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
updateRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade);
updateRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(getUpdateBulkRequest());
expect(statusCode).toBe(404);
});
@ -89,7 +88,7 @@ describe('update_rules_bulk', () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
updateRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade);
updateRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateBulkRequest());
expect(statusCode).toBe(404);
});

View file

@ -13,7 +13,6 @@ import {
import { updateRulesRoute } from './update_rules_route';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import {
getFindResult,
@ -32,7 +31,7 @@ describe('update_rules', () => {
beforeEach(() => {
jest.resetAllMocks();
({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer());
updateRulesRoute((server as unknown) as ServerFacade);
updateRulesRoute(server);
});
describe('status codes with actionClient and alertClient', () => {
@ -58,14 +57,14 @@ describe('update_rules', () => {
test('returns 404 if actionClient is not available on the route', async () => {
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
updateRulesRoute((serverWithoutActionClient as unknown) as ServerFacade);
updateRulesRoute(serverWithoutActionClient);
const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest());
expect(statusCode).toBe(404);
});
test('returns 404 if alertClient is not available on the route', async () => {
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
updateRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade);
updateRulesRoute(serverWithoutAlertClient);
const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest());
expect(statusCode).toBe(404);
});
@ -74,7 +73,7 @@ describe('update_rules', () => {
const {
serverWithoutActionOrAlertClient,
} = createMockServerWithoutActionOrAlertClientDecoration();
updateRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade);
updateRulesRoute(serverWithoutActionOrAlertClient);
const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateRequest());
expect(statusCode).toBe(404);
});

View file

@ -34,11 +34,11 @@ export const getIdError = ({
ruleId: string | undefined | null;
}) => {
if (id != null) {
return new Boom(`id: "${id}" not found`, { statusCode: 404 });
return Boom.notFound(`id: "${id}" not found`);
} else if (ruleId != null) {
return new Boom(`rule_id: "${ruleId}" not found`, { statusCode: 404 });
return Boom.notFound(`rule_id: "${ruleId}" not found`);
} else {
return new Boom(`id or rule_id should have been defined`, { statusCode: 404 });
return Boom.notFound('id or rule_id should have been defined');
}
};

View file

@ -205,8 +205,8 @@ describe('add prepackaged rules schema', () => {
query: 'some query',
language: 'kuery',
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('"output_index" is not allowed');
});
test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version] does validate', () => {
@ -345,6 +345,48 @@ describe('add prepackaged rules schema', () => {
).toEqual(true);
});
test('immutable cannot be false', () => {
expect(
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
rule_id: 'rule-1',
risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
index: ['index-1'],
immutable: false,
name: 'some-name',
severity: 'severity',
interval: '5m',
type: 'query',
query: 'some-query',
language: 'kuery',
version: 1,
}).error.message
).toEqual('child "immutable" fails because ["immutable" must be one of [true]]');
});
test('immutable can be true', () => {
expect(
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
rule_id: 'rule-1',
risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
index: ['index-1'],
immutable: true,
name: 'some-name',
severity: 'severity',
interval: '5m',
type: 'query',
query: 'some-query',
language: 'kuery',
version: 1,
}).error
).toBeFalsy();
});
test('defaults enabled to false', () => {
expect(
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
@ -380,8 +422,8 @@ describe('add prepackaged rules schema', () => {
query: 'some-query',
language: 'kuery',
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "rule_id" fails because ["rule_id" is required]');
});
test('references cannot be numbers', () => {
@ -403,8 +445,10 @@ describe('add prepackaged rules schema', () => {
language: 'kuery',
references: [5],
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]'
);
});
test('indexes cannot be numbers', () => {
@ -425,8 +469,10 @@ describe('add prepackaged rules schema', () => {
query: 'some-query',
language: 'kuery',
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]'
);
});
test('defaults interval to 5 min', () => {
@ -478,8 +524,8 @@ describe('add prepackaged rules schema', () => {
interval: '5m',
type: 'saved_query',
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "saved_id" fails because ["saved_id" is required]');
});
test('saved_id is required when type is saved_query and validates with it', () => {
@ -539,8 +585,8 @@ describe('add prepackaged rules schema', () => {
saved_id: 'some id',
filters: 'some string',
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "filters" fails because ["filters" must be an array]');
});
test('language validates with kuery', () => {
@ -602,8 +648,8 @@ describe('add prepackaged rules schema', () => {
query: 'some query',
language: 'something-made-up',
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]');
});
test('max_signals cannot be negative', () => {
@ -624,8 +670,8 @@ describe('add prepackaged rules schema', () => {
language: 'kuery',
max_signals: -1,
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
});
test('max_signals cannot be zero', () => {
@ -646,8 +692,8 @@ describe('add prepackaged rules schema', () => {
language: 'kuery',
max_signals: 0,
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
});
test('max_signals can be 1', () => {
@ -716,8 +762,10 @@ describe('add prepackaged rules schema', () => {
max_signals: 1,
tags: [0, 1, 2],
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]'
);
});
test('You cannot send in an array of threats that are missing "framework"', () => {
@ -758,9 +806,12 @@ describe('add prepackaged rules schema', () => {
},
],
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "framework" fails because ["framework" is required]]]'
);
});
test('You cannot send in an array of threats that are missing "tactic"', () => {
expect(
addPrepackagedRulesSchema.validate<
@ -795,9 +846,12 @@ describe('add prepackaged rules schema', () => {
},
],
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]'
);
});
test('You cannot send in an array of threats that are missing "techniques"', () => {
expect(
addPrepackagedRulesSchema.validate<
@ -830,8 +884,10 @@ describe('add prepackaged rules schema', () => {
},
],
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "techniques" fails because ["techniques" is required]]]'
);
});
test('You can optionally send in an array of false positives', () => {
@ -878,8 +934,10 @@ describe('add prepackaged rules schema', () => {
language: 'kuery',
max_signals: 1,
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]'
);
});
test('You can optionally set the immutable to be true', () => {
@ -926,8 +984,8 @@ describe('add prepackaged rules schema', () => {
language: 'kuery',
max_signals: 1,
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "immutable" fails because ["immutable" must be a boolean]');
});
test('You cannot set the risk_score to 101', () => {
@ -949,8 +1007,8 @@ describe('add prepackaged rules schema', () => {
language: 'kuery',
max_signals: 1,
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]');
});
test('You cannot set the risk_score to -1', () => {
@ -972,8 +1030,8 @@ describe('add prepackaged rules schema', () => {
language: 'kuery',
max_signals: 1,
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]');
});
test('You can set the risk_score to 0', () => {
@ -1070,8 +1128,8 @@ describe('add prepackaged rules schema', () => {
max_signals: 1,
meta: 'should not work',
version: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "meta" fails because ["meta" must be an object]');
});
test('You can omit the query string when filters are present', () => {
@ -1140,8 +1198,8 @@ describe('add prepackaged rules schema', () => {
max_signals: 1,
version: 1,
timeline_id: 'timeline-id',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is required]');
});
test('You cannot have a null value for timeline_title when timeline_id is present', () => {
@ -1165,8 +1223,8 @@ describe('add prepackaged rules schema', () => {
version: 1,
timeline_id: 'timeline-id',
timeline_title: null,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]');
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
@ -1190,8 +1248,8 @@ describe('add prepackaged rules schema', () => {
version: 1,
timeline_id: 'timeline-id',
timeline_title: '',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]');
});
test('You cannot have timeline_title with an empty timeline_id', () => {
@ -1215,8 +1273,8 @@ describe('add prepackaged rules schema', () => {
version: 1,
timeline_id: '',
timeline_title: 'some-title',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]');
});
test('You cannot have timeline_title without timeline_id', () => {
@ -1239,7 +1297,7 @@ describe('add prepackaged rules schema', () => {
max_signals: 1,
version: 1,
timeline_title: 'some-title',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]');
});
});

View file

@ -42,7 +42,7 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants';
* Big differences between this schema and the createRulesSchema
* - rule_id is required here
* - output_index is not allowed (and instead the space index must be used)
* - immutable defaults to true instead of to false
* - immutable defaults to true instead of to false and if it is there can only be true
* - enabled defaults to false instead of true
* - version is a required field that must exist
*/
@ -53,7 +53,7 @@ export const addPrepackagedRulesSchema = Joi.object({
filters,
from: from.required(),
rule_id: rule_id.required(),
immutable: immutable.default(true),
immutable: immutable.default(true).valid(true),
index,
interval: interval.default('5m'),
query: query.allow('').default(''),

View file

@ -238,6 +238,7 @@ describe('create rules schema', () => {
}).error
).toBeFalsy();
});
test('You can send in an empty array to threats', () => {
expect(
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
@ -260,6 +261,7 @@ describe('create rules schema', () => {
}).error
).toBeFalsy();
});
test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => {
expect(
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
@ -355,8 +357,10 @@ describe('create rules schema', () => {
query: 'some-query',
language: 'kuery',
references: [5],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]'
);
});
test('indexes cannot be numbers', () => {
@ -377,8 +381,10 @@ describe('create rules schema', () => {
query: 'some-query',
language: 'kuery',
}
).error
).toBeTruthy();
).error.message
).toEqual(
'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]'
);
});
test('defaults interval to 5 min', () => {
@ -430,8 +436,8 @@ describe('create rules schema', () => {
severity: 'severity',
interval: '5m',
type: 'saved_query',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "saved_id" fails because ["saved_id" is required]');
});
test('saved_id is required when type is saved_query and validates with it', () => {
@ -491,8 +497,8 @@ describe('create rules schema', () => {
type: 'saved_query',
saved_id: 'some id',
filters: 'some string',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "filters" fails because ["filters" must be an array]');
});
test('language validates with kuery', () => {
@ -554,8 +560,8 @@ describe('create rules schema', () => {
references: ['index-1'],
query: 'some query',
language: 'something-made-up',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]');
});
test('max_signals cannot be negative', () => {
@ -576,8 +582,8 @@ describe('create rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: -1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
});
test('max_signals cannot be zero', () => {
@ -598,8 +604,8 @@ describe('create rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 0,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
});
test('max_signals can be 1', () => {
@ -666,8 +672,10 @@ describe('create rules schema', () => {
language: 'kuery',
max_signals: 1,
tags: [0, 1, 2],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]'
);
});
test('You cannot send in an array of threats that are missing "framework"', () => {
@ -708,9 +716,12 @@ describe('create rules schema', () => {
],
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "framework" fails because ["framework" is required]]]'
);
});
test('You cannot send in an array of threats that are missing "tactic"', () => {
expect(
createRulesSchema.validate<
@ -745,9 +756,12 @@ describe('create rules schema', () => {
],
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]'
);
});
test('You cannot send in an array of threats that are missing "techniques"', () => {
expect(
createRulesSchema.validate<
@ -780,8 +794,10 @@ describe('create rules schema', () => {
},
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "techniques" fails because ["techniques" is required]]]'
);
});
test('You can optionally send in an array of false positives', () => {
@ -828,34 +844,13 @@ describe('create rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]'
);
});
test('You can optionally set the immutable to be true', () => {
expect(
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
rule_id: 'rule-1',
output_index: '.siem-signals',
risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
immutable: true,
index: ['index-1'],
name: 'some-name',
severity: 'severity',
interval: '5m',
type: 'query',
references: ['index-1'],
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeFalsy();
});
test('You cannot set the immutable to be a number', () => {
test('You cannot set the immutable when trying to create a rule', () => {
expect(
createRulesSchema.validate<
Partial<Omit<RuleAlertParamsRest, 'immutable'>> & { immutable: number }
@ -876,8 +871,8 @@ describe('create rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('"immutable" is not allowed');
});
test('You cannot set the risk_score to 101', () => {
@ -899,8 +894,8 @@ describe('create rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]');
});
test('You cannot set the risk_score to -1', () => {
@ -922,8 +917,8 @@ describe('create rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]');
});
test('You can set the risk_score to 0', () => {
@ -935,7 +930,6 @@ describe('create rules schema', () => {
description: 'some description',
from: 'now-5m',
to: 'now',
immutable: true,
index: ['index-1'],
name: 'some-name',
severity: 'severity',
@ -958,7 +952,6 @@ describe('create rules schema', () => {
description: 'some description',
from: 'now-5m',
to: 'now',
immutable: true,
index: ['index-1'],
name: 'some-name',
severity: 'severity',
@ -981,7 +974,6 @@ describe('create rules schema', () => {
description: 'some description',
from: 'now-5m',
to: 'now',
immutable: true,
index: ['index-1'],
name: 'some-name',
severity: 'severity',
@ -1018,8 +1010,8 @@ describe('create rules schema', () => {
language: 'kuery',
max_signals: 1,
meta: 'should not work',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "meta" fails because ["meta" must be an object]');
});
test('You can omit the query string when filters are present', () => {
@ -1031,7 +1023,6 @@ describe('create rules schema', () => {
description: 'some description',
from: 'now-5m',
to: 'now',
immutable: true,
index: ['index-1'],
name: 'some-name',
severity: 'severity',
@ -1086,8 +1077,8 @@ describe('create rules schema', () => {
query: 'some query',
language: 'kuery',
timeline_id: 'some_id',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is required]');
});
test('You cannot have a null value for timeline_title when timeline_id is present', () => {
@ -1109,8 +1100,8 @@ describe('create rules schema', () => {
language: 'kuery',
timeline_id: 'some_id',
timeline_title: null,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]');
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
@ -1132,8 +1123,8 @@ describe('create rules schema', () => {
language: 'kuery',
timeline_id: 'some_id',
timeline_title: '',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]');
});
test('You cannot have timeline_title with an empty timeline_id', () => {
@ -1155,8 +1146,8 @@ describe('create rules schema', () => {
language: 'kuery',
timeline_id: '',
timeline_title: 'some-title',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]');
});
test('You cannot have timeline_title without timeline_id', () => {
@ -1177,7 +1168,7 @@ describe('create rules schema', () => {
query: 'some query',
language: 'kuery',
timeline_title: 'some-title',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]');
});
});

View file

@ -13,7 +13,6 @@ import {
false_positives,
filters,
from,
immutable,
index,
rule_id,
interval,
@ -46,7 +45,6 @@ export const createRulesSchema = Joi.object({
filters,
from: from.required(),
rule_id,
immutable: immutable.default(false),
index,
interval: interval.default('5m'),
query: query.allow('').default(''),

View file

@ -37,8 +37,10 @@ describe('create rules schema', () => {
expect(
exportRulesSchema.validate<Omit<ExportRulesRequest['payload'], 'objects'>>({
objects: [{ id: 'test-1' }],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "objects" fails because ["objects" at position 0 fails because ["id" is not allowed]]'
);
});
});
@ -70,8 +72,8 @@ describe('create rules schema', () => {
Partial<Omit<ExportRulesRequest['query'], 'file_name'> & { file_name: number }>
>({
file_name: 5,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "file_name" fails because ["file_name" must be a string]');
});
test('exclude_export_details validates with a boolean true', () => {
@ -92,8 +94,10 @@ describe('create rules schema', () => {
>
>({
exclude_export_details: 'blah',
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "exclude_export_details" fails because ["exclude_export_details" must be a boolean]'
);
});
});
});

View file

@ -69,8 +69,10 @@ describe('find rules schema', () => {
expect(
findRulesSchema.validate<Partial<Omit<FindParamsRest, 'fields'>> & { fields: number[] }>({
fields: [5],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "fields" fails because ["fields" at position 0 fails because ["0" must be a string]]'
);
});
test('per page has a default of 20', () => {
@ -93,16 +95,16 @@ describe('find rules schema', () => {
expect(
findRulesSchema.validate<Partial<Omit<FindParamsRest, 'filter'>> & { filter: number }>({
filter: 5,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "filter" fails because ["filter" must be a string]');
});
test('sort_order requires sort_field to work', () => {
expect(
findRulesSchema.validate<Partial<FindParamsRest>>({
sort_order: 'asc',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "sort_field" fails because ["sort_field" is required]');
});
test('sort_order and sort_field validate together', () => {
@ -130,7 +132,7 @@ describe('find rules schema', () => {
>({
sort_order: 'some other string',
sort_field: 'some field',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "sort_order" fails because ["sort_order" must be one of [asc, desc]]');
});
});

View file

@ -243,6 +243,7 @@ describe('import rules schema', () => {
}).error
).toBeFalsy();
});
test('You can send in an empty array to threats', () => {
expect(
importRulesSchema.validate<Partial<RuleAlertParamsRest>>({
@ -265,6 +266,7 @@ describe('import rules schema', () => {
}).error
).toBeFalsy();
});
test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => {
expect(
importRulesSchema.validate<Partial<RuleAlertParamsRest>>({
@ -360,8 +362,10 @@ describe('import rules schema', () => {
query: 'some-query',
language: 'kuery',
references: [5],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]'
);
});
test('indexes cannot be numbers', () => {
@ -382,8 +386,10 @@ describe('import rules schema', () => {
type: 'query',
query: 'some-query',
language: 'kuery',
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]'
);
});
test('defaults interval to 5 min', () => {
@ -435,8 +441,8 @@ describe('import rules schema', () => {
severity: 'severity',
interval: '5m',
type: 'saved_query',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "saved_id" fails because ["saved_id" is required]');
});
test('saved_id is required when type is saved_query and validates with it', () => {
@ -496,8 +502,8 @@ describe('import rules schema', () => {
type: 'saved_query',
saved_id: 'some id',
filters: 'some string',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "filters" fails because ["filters" must be an array]');
});
test('language validates with kuery', () => {
@ -559,8 +565,8 @@ describe('import rules schema', () => {
references: ['index-1'],
query: 'some query',
language: 'something-made-up',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]');
});
test('max_signals cannot be negative', () => {
@ -581,8 +587,8 @@ describe('import rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: -1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
});
test('max_signals cannot be zero', () => {
@ -603,8 +609,8 @@ describe('import rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 0,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
});
test('max_signals can be 1', () => {
@ -673,8 +679,10 @@ describe('import rules schema', () => {
max_signals: 1,
tags: [0, 1, 2],
}
).error
).toBeTruthy();
).error.message
).toEqual(
'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]'
);
});
test('You cannot send in an array of threats that are missing "framework"', () => {
@ -715,9 +723,12 @@ describe('import rules schema', () => {
],
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "framework" fails because ["framework" is required]]]'
);
});
test('You cannot send in an array of threats that are missing "tactic"', () => {
expect(
importRulesSchema.validate<
@ -752,9 +763,12 @@ describe('import rules schema', () => {
],
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]'
);
});
test('You cannot send in an array of threats that are missing "techniques"', () => {
expect(
importRulesSchema.validate<
@ -787,8 +801,10 @@ describe('import rules schema', () => {
},
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "techniques" fails because ["techniques" is required]]]'
);
});
test('You can optionally send in an array of false positives', () => {
@ -835,8 +851,10 @@ describe('import rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]'
);
});
test('You can optionally set the immutable to be true', () => {
@ -883,8 +901,8 @@ describe('import rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "immutable" fails because ["immutable" must be a boolean]');
});
test('You cannot set the risk_score to 101', () => {
@ -906,8 +924,8 @@ describe('import rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]');
});
test('You cannot set the risk_score to -1', () => {
@ -929,8 +947,8 @@ describe('import rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]');
});
test('You can set the risk_score to 0', () => {
@ -1025,8 +1043,8 @@ describe('import rules schema', () => {
language: 'kuery',
max_signals: 1,
meta: 'should not work',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "meta" fails because ["meta" must be an object]');
});
test('You can omit the query string when filters are present', () => {
@ -1093,8 +1111,8 @@ describe('import rules schema', () => {
query: 'some query',
language: 'kuery',
timeline_id: 'some_id',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is required]');
});
test('You cannot have a null value for timeline_title when timeline_id is present', () => {
@ -1116,8 +1134,8 @@ describe('import rules schema', () => {
language: 'kuery',
timeline_id: 'some_id',
timeline_title: null,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]');
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
@ -1139,8 +1157,10 @@ describe('import rules schema', () => {
language: 'kuery',
timeline_id: 'some_id',
timeline_title: '',
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'
);
});
test('You cannot have timeline_title with an empty timeline_id', () => {
@ -1162,8 +1182,8 @@ describe('import rules schema', () => {
language: 'kuery',
timeline_id: '',
timeline_title: 'some-title',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]');
});
test('You cannot have timeline_title without timeline_id', () => {
@ -1184,8 +1204,8 @@ describe('import rules schema', () => {
query: 'some query',
language: 'kuery',
timeline_title: 'some-title',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]');
});
test('rule_id is required and you cannot get by with just id', () => {
@ -1205,8 +1225,8 @@ describe('import rules schema', () => {
references: ['index-1'],
query: 'some query',
language: 'kuery',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "rule_id" fails because ["rule_id" is required]');
});
test('it validates with created_at, updated_at, created_by, updated_by values', () => {
@ -1255,8 +1275,8 @@ describe('import rules schema', () => {
updated_at: '2020-01-09T06:15:24.749Z',
created_by: 'Braden Hassanabad',
updated_by: 'Evan Hassanabad',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "created_at" fails because ["created_at" must be a valid ISO 8601 date]');
});
test('it does not validate with epoch strings for updated_at', () => {
@ -1280,8 +1300,8 @@ describe('import rules schema', () => {
updated_at: '1578550728650',
created_by: 'Braden Hassanabad',
updated_by: 'Evan Hassanabad',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "updated_at" fails because ["updated_at" must be a valid ISO 8601 date]');
});
});

View file

@ -24,8 +24,10 @@ describe('query_rules_bulk_schema', () => {
rule_id: '1',
id: '1',
},
]).error
).toBeTruthy();
]).error.message
).toEqual(
'"value" at position 0 fails because ["value" contains a conflict between exclusive peers [id, rule_id]]'
);
});
test('both rule_id and id being supplied do not validate if one array element works but the second does not', () => {
@ -38,8 +40,10 @@ describe('query_rules_bulk_schema', () => {
rule_id: '1',
id: '1',
},
]).error
).toBeTruthy();
]).error.message
).toEqual(
'"value" at position 1 fails because ["value" contains a conflict between exclusive peers [id, rule_id]]'
);
});
test('only id validates', () => {

View file

@ -12,10 +12,11 @@ describe('queryRulesSchema', () => {
expect(queryRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({}).error).toBeTruthy();
});
test('both rule_id and id being supplied dot not validate', () => {
test('both rule_id and id being supplied do not validate', () => {
expect(
queryRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({ rule_id: '1', id: '1' }).error
).toBeTruthy();
.message
).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]');
});
test('only id validates', () => {

View file

@ -30,24 +30,24 @@ describe('set signal status schema', () => {
expect(
setSignalsStatusSchema.validate<Partial<SignalsStatusRestParams>>({
signal_ids: ['somefakeid'],
}).error
).toBeTruthy();
}).error.message
).toEqual('child "status" fails because ["status" is required]');
});
test('query and missing status is invalid', () => {
expect(
setSignalsStatusSchema.validate<Partial<SignalsStatusRestParams>>({
query: {},
}).error
).toBeTruthy();
}).error.message
).toEqual('child "status" fails because ["status" is required]');
});
test('status is present but query or signal_ids is missing is invalid', () => {
expect(
setSignalsStatusSchema.validate<Partial<SignalsStatusRestParams>>({
status: 'closed',
}).error
).toBeTruthy();
}).error.message
).toEqual('"value" must contain at least one of [signal_ids, query]');
});
test('signal_ids is present but status has wrong value', () => {
@ -60,7 +60,7 @@ describe('set signal status schema', () => {
>
>({
status: 'fakeVal',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "status" fails because ["status" must be one of [open, closed]]');
});
});

View file

@ -442,8 +442,10 @@ describe('update rules schema', () => {
query: 'some-query',
language: 'kuery',
references: [5],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]'
);
});
test('indexes cannot be numbers', () => {
@ -462,8 +464,10 @@ describe('update rules schema', () => {
type: 'query',
query: 'some-query',
language: 'kuery',
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]'
);
});
test('saved_id is not required when type is saved_query and will validate without it', () => {
@ -570,8 +574,8 @@ describe('update rules schema', () => {
references: ['index-1'],
query: 'some query',
language: 'something-made-up',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]');
});
test('max_signals cannot be negative', () => {
@ -590,8 +594,8 @@ describe('update rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: -1,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
});
test('max_signals cannot be zero', () => {
@ -610,8 +614,8 @@ describe('update rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 0,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
});
test('max_signals can be 1', () => {
@ -643,15 +647,15 @@ describe('update rules schema', () => {
).toBeFalsy();
});
test('You update meta as a string', () => {
test('You cannot update meta as a string', () => {
expect(
updateRulesSchema.validate<
Partial<Omit<UpdateRuleAlertParamsRest, 'meta'> & { meta: string }>
>({
id: 'rule-1',
meta: 'should not work',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "meta" fails because ["meta" must be an object]');
});
test('filters cannot be a string', () => {
@ -662,8 +666,8 @@ describe('update rules schema', () => {
rule_id: 'rule-1',
type: 'query',
filters: 'some string',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "filters" fails because ["filters" must be an array]');
});
test('threats is not defaulted to empty array on update', () => {
@ -706,6 +710,7 @@ describe('update rules schema', () => {
}).value.threats
).toMatchObject([]);
});
test('threats is valid when updated with all sub-objects', () => {
const expected: ThreatParams[] = [
{
@ -759,6 +764,7 @@ describe('update rules schema', () => {
}).value.threats
).toMatchObject(expected);
});
test('threats is invalid when updated with missing property framework', () => {
expect(
updateRulesSchema.validate<
@ -795,9 +801,12 @@ describe('update rules schema', () => {
],
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "framework" fails because ["framework" is required]]]'
);
});
test('threats is invalid when updated with missing tactic sub-object', () => {
expect(
updateRulesSchema.validate<
@ -830,9 +839,12 @@ describe('update rules schema', () => {
],
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]'
);
});
test('threats is invalid when updated with missing techniques', () => {
expect(
updateRulesSchema.validate<
@ -863,8 +875,10 @@ describe('update rules schema', () => {
},
},
],
}).error
).toBeTruthy();
}).error.message
).toEqual(
'child "threats" fails because ["threats" at position 0 fails because [child "techniques" fails because ["techniques" is required]]]'
);
});
test('validates with timeline_id and timeline_title', () => {
@ -900,8 +914,8 @@ describe('update rules schema', () => {
type: 'saved_query',
saved_id: 'some id',
timeline_id: 'some-id',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is required]');
});
test('You cannot have a null value for timeline_title when timeline_id is present', () => {
@ -919,8 +933,8 @@ describe('update rules schema', () => {
saved_id: 'some id',
timeline_id: 'timeline-id',
timeline_title: null,
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]');
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
@ -938,8 +952,8 @@ describe('update rules schema', () => {
saved_id: 'some id',
timeline_id: 'some-id',
timeline_title: '',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]');
});
test('You cannot have timeline_title with an empty timeline_id', () => {
@ -957,8 +971,8 @@ describe('update rules schema', () => {
saved_id: 'some id',
timeline_id: '',
timeline_title: 'some-title',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]');
});
test('You cannot have timeline_title without timeline_id', () => {
@ -975,7 +989,7 @@ describe('update rules schema', () => {
type: 'saved_query',
saved_id: 'some id',
timeline_title: 'some-title',
}).error
).toBeTruthy();
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]');
});
});

View file

@ -13,7 +13,6 @@ import {
false_positives,
filters,
from,
immutable,
index,
rule_id,
interval,
@ -46,7 +45,6 @@ export const updateRulesSchema = Joi.object({
from,
rule_id,
id,
immutable,
index,
interval,
query: query.allow(''),

View file

@ -8,7 +8,6 @@ import { createMockServer } from '../__mocks__/_mock_server';
import { setSignalsStatusRoute } from './open_close_signals_route';
import * as myUtils from '../utils';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import {
getSetSignalStatusByIdsRequest,
@ -29,7 +28,7 @@ describe('set signal status', () => {
elasticsearch.getCluster = jest.fn(() => ({
callWithRequest: jest.fn(() => true),
}));
setSignalsStatusRoute((server as unknown) as ServerFacade);
setSignalsStatusRoute(server);
});
describe('status on signal', () => {

View file

@ -8,7 +8,6 @@ import { createMockServer } from '../__mocks__/_mock_server';
import { querySignalsRoute } from './query_signals_route';
import * as myUtils from '../utils';
import { ServerInjectOptions } from 'hapi';
import { ServerFacade } from '../../../../types';
import {
getSignalsQueryRequest,
@ -28,7 +27,7 @@ describe('query for signal', () => {
elasticsearch.getCluster = jest.fn(() => ({
callWithRequest: jest.fn(() => true),
}));
querySignalsRoute((server as unknown) as ServerFacade);
querySignalsRoute(server);
});
describe('query and agg on signals index', () => {

View file

@ -0,0 +1,17 @@
#!/bin/sh
#
# 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.
#
set -e
./check_env_variables.sh
# Example: ./get_prepackaged_rules_status.sh
curl -s -k \
-H 'Content-Type: application/json' \
-H 'kbn-xsrf: 123' \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
-X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/prepackaged/_status | jq .

View file

@ -21,7 +21,6 @@
}
],
"enabled": false,
"immutable": false,
"index": ["auditbeat-*", "filebeat-*"],
"interval": "5m",
"query": "user.name: root or user.name: admin",

View file

@ -21,7 +21,6 @@
}
],
"enabled": false,
"immutable": true,
"index": ["auditbeat-*", "filebeat-*"],
"interval": "5m",
"query": "user.name: root or user.name: admin",

View file

@ -1,4 +0,0 @@
{
"rule_id": "query-rule-id",
"immutable": true
}

View file

@ -21,7 +21,6 @@
}
],
"enabled": false,
"immutable": true,
"index": ["auditbeat-*", "filebeat-*"],
"interval": "5m",
"query": "user.name: root or user.name: admin",