mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SIEM][Detection Engine] Backend end-to-end tests
## Summary * Adds end to end integration tests * Fixes a bug with import where on imports it was forcing all rules that were being imported to be set to be "enabled: false" instead of honoring what the original export has set for its enabled. * Adds a few "to be safe" await block so that the front end does not get a race condition within the bulk deletes and other parts of the code. * Fixes `statusCode` to be `status_code` and removes most of the Hapi Boomer errors * Changes PUT to be PATCH for partial updates * Adds true updates with PUT * Put some TODO blocks around existing bugs found in the API in the e2e tests that we might have time to get to or might not. This will let others maintaining the tests know that once they fix the bug they should update the end to end test to change the behavior. Testing this: Go to the latest CI logs and look for any particular lines from the test executing such as: ```ts should set the response content types to be expected ``` Also run this manually on your machine through this command: ```ts node scripts/functional_tests --config x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts ``` Change a test manually and re-run the above command to watch something fail. Screen shot of what you should see on the CI machine when these are running: <img width="1825" alt="Screen Shot 2020-02-08 at 10 15 21 AM" src="https://user-images.githubusercontent.com/1151048/74089355-ae9a8e80-4a5d-11ea-9050-86e68d7e3bba.png"> ### Checklist Delete any items that are not applicable to this PR. ~~- [ ] 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)~~ ~~- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)~~ ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
parent
5bfbf5f530
commit
f890776e6d
98 changed files with 6683 additions and 813 deletions
|
@ -139,7 +139,7 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<R
|
|||
const response = await KibanaServices.get().http.fetch<Rule[]>(
|
||||
`${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(ids.map(id => ({ id, enabled }))),
|
||||
asResponse: true,
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<Array<Rule
|
|||
const response = await KibanaServices.get().http.fetch<Rule[]>(
|
||||
`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(ids.map(id => ({ id }))),
|
||||
asResponse: true,
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ export const throwIfNotOk = async (response?: Response): Promise<void> => {
|
|||
if (body != null && body.message) {
|
||||
if (body.statusCode != null) {
|
||||
throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.statusCode}`]);
|
||||
} else if (body.status_code != null) {
|
||||
throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.status_code}`]);
|
||||
} else {
|
||||
throw new ToasterErrors([body.message]);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export interface MessageBody {
|
|||
error?: string;
|
||||
message?: string;
|
||||
statusCode?: number;
|
||||
status_code?: number;
|
||||
}
|
||||
|
||||
export const parseJsonFromBody = async (response: Response): Promise<MessageBody | null> => {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_r
|
|||
import { readRulesRoute } from './lib/detection_engine/routes/rules/read_rules_route';
|
||||
import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_route';
|
||||
import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route';
|
||||
import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route';
|
||||
import { patchRulesRoute } from './lib/detection_engine/routes/rules/patch_rules_route';
|
||||
import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route';
|
||||
import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route';
|
||||
import { ServerFacade } from './types';
|
||||
|
@ -23,12 +23,14 @@ import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_rout
|
|||
import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route';
|
||||
import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_prepackaged_rules_route';
|
||||
import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route';
|
||||
import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route';
|
||||
import { patchRulesBulkRoute } from './lib/detection_engine/routes/rules/patch_rules_bulk_route';
|
||||
import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route';
|
||||
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';
|
||||
import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route';
|
||||
import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route';
|
||||
|
||||
const APP_ID = 'siem';
|
||||
|
||||
|
@ -50,12 +52,14 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy
|
|||
updateRulesRoute(__legacy);
|
||||
deleteRulesRoute(__legacy);
|
||||
findRulesRoute(__legacy);
|
||||
patchRulesRoute(__legacy);
|
||||
|
||||
addPrepackedRulesRoute(__legacy);
|
||||
getPrepackagedRulesStatusRoute(__legacy);
|
||||
createRulesBulkRoute(__legacy);
|
||||
updateRulesBulkRoute(__legacy);
|
||||
deleteRulesBulkRoute(__legacy);
|
||||
patchRulesBulkRoute(__legacy);
|
||||
|
||||
importRulesRoute(__legacy);
|
||||
exportRulesRoute(__legacy);
|
||||
|
|
|
@ -108,6 +108,14 @@ export const getUpdateRequest = (): ServerInjectOptions => ({
|
|||
},
|
||||
});
|
||||
|
||||
export const getPatchRequest = (): ServerInjectOptions => ({
|
||||
method: 'PATCH',
|
||||
url: DETECTION_ENGINE_RULES_URL,
|
||||
payload: {
|
||||
...typicalPayload(),
|
||||
},
|
||||
});
|
||||
|
||||
export const getReadRequest = (): ServerInjectOptions => ({
|
||||
method: 'GET',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`,
|
||||
|
@ -130,6 +138,12 @@ export const getUpdateBulkRequest = (): ServerInjectOptions => ({
|
|||
payload: [typicalPayload()],
|
||||
});
|
||||
|
||||
export const getPatchBulkRequest = (): ServerInjectOptions => ({
|
||||
method: 'PATCH',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
|
||||
payload: [typicalPayload()],
|
||||
});
|
||||
|
||||
export const getDeleteBulkRequest = (): ServerInjectOptions => ({
|
||||
method: 'DELETE',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`,
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
|
||||
import signalsPolicy from './signals_policy.json';
|
||||
|
@ -31,13 +30,18 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
},
|
||||
},
|
||||
},
|
||||
async handler(request: RequestFacade) {
|
||||
async handler(request: RequestFacade, headers) {
|
||||
try {
|
||||
const index = getIndex(request, server);
|
||||
const callWithRequest = callWithRequestFactory(request, server);
|
||||
const indexExists = await getIndexExists(callWithRequest, index);
|
||||
if (indexExists) {
|
||||
return new Boom(`index: "${index}" already exists`, { statusCode: 409 });
|
||||
return headers
|
||||
.response({
|
||||
message: `index: "${index}" already exists`,
|
||||
status_code: 409,
|
||||
})
|
||||
.code(409);
|
||||
} else {
|
||||
const policyExists = await getPolicyExists(callWithRequest, index);
|
||||
if (!policyExists) {
|
||||
|
@ -52,7 +56,13 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
return { acknowledged: true };
|
||||
}
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
|
||||
import { ServerFacade, RequestFacade } from '../../../../types';
|
||||
|
@ -39,13 +38,18 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
},
|
||||
},
|
||||
},
|
||||
async handler(request: RequestFacade) {
|
||||
async handler(request: RequestFacade, headers) {
|
||||
try {
|
||||
const index = getIndex(request, server);
|
||||
const callWithRequest = callWithRequestFactory(request, server);
|
||||
const indexExists = await getIndexExists(callWithRequest, index);
|
||||
if (!indexExists) {
|
||||
return new Boom(`index: "${index}" does not exist`, { statusCode: 404 });
|
||||
return headers
|
||||
.response({
|
||||
message: `index: "${index}" does not exist`,
|
||||
status_code: 404,
|
||||
})
|
||||
.code(404);
|
||||
} else {
|
||||
await deleteAllIndex(callWithRequest, `${index}-*`);
|
||||
const policyExists = await getPolicyExists(callWithRequest, index);
|
||||
|
@ -59,7 +63,13 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
return { acknowledged: true };
|
||||
}
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
|
||||
import { ServerFacade, RequestFacade } from '../../../../types';
|
||||
|
@ -42,11 +41,22 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute =>
|
|||
if (request.method.toLowerCase() === 'head') {
|
||||
return headers.response().code(404);
|
||||
} else {
|
||||
return new Boom('index for this space does not exist', { statusCode: 404 });
|
||||
return headers
|
||||
.response({
|
||||
message: 'index for this space does not exist',
|
||||
status_code: 404,
|
||||
})
|
||||
.code(404);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve
|
|||
},
|
||||
},
|
||||
},
|
||||
async handler(request: RulesRequest) {
|
||||
async handler(request: RulesRequest, headers) {
|
||||
try {
|
||||
const callWithRequest = callWithRequestFactory(request, server);
|
||||
const index = getIndex(request, server);
|
||||
|
@ -35,7 +35,13 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve
|
|||
has_encryption_key: !usingEphemeralEncryptionKey,
|
||||
});
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -85,10 +85,9 @@ describe('add_prepackaged_rules_route', () => {
|
|||
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,
|
||||
status_code: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import Hapi from 'hapi';
|
||||
import { isFunction } from 'lodash/fp';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants';
|
||||
import { ServerFacade, RequestFacade } from '../../../../types';
|
||||
|
@ -56,9 +55,12 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR
|
|||
if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) {
|
||||
const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex);
|
||||
if (!spaceIndexExists) {
|
||||
return Boom.badRequest(
|
||||
`Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}`
|
||||
);
|
||||
return headers
|
||||
.response({
|
||||
message: `Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}`,
|
||||
status_code: 400,
|
||||
})
|
||||
.code(400);
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
|
@ -76,7 +78,13 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR
|
|||
rules_updated: rulesToUpdate.length,
|
||||
};
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -73,9 +73,8 @@ describe('create_rules', () => {
|
|||
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,
|
||||
status_code: 400,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import Hapi from 'hapi';
|
||||
import { isFunction } from 'lodash/fp';
|
||||
import Boom from 'boom';
|
||||
import uuid from 'uuid';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { createRules } from '../../rules/create_rules';
|
||||
|
@ -15,7 +14,7 @@ import { createRulesSchema } from '../schemas/create_rules_schema';
|
|||
import { ServerFacade } from '../../../../types';
|
||||
import { readRules } from '../../rules/read_rules';
|
||||
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
|
||||
import { transformOrError } from './utils';
|
||||
import { transform } from './utils';
|
||||
import { getIndexExists } from '../../index/get_index_exists';
|
||||
import { callWithRequestFactory, getIndex, transformError } from '../utils';
|
||||
import { KibanaRequest } from '../../../../../../../../../src/core/server';
|
||||
|
@ -76,14 +75,22 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
const callWithRequest = callWithRequestFactory(request, server);
|
||||
const indexExists = await getIndexExists(callWithRequest, finalIndex);
|
||||
if (!indexExists) {
|
||||
return Boom.badRequest(
|
||||
`To create a rule, the index must exist first. Index ${finalIndex} does not exist`
|
||||
);
|
||||
return headers
|
||||
.response({
|
||||
message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`,
|
||||
status_code: 400,
|
||||
})
|
||||
.code(400);
|
||||
}
|
||||
if (ruleId != null) {
|
||||
const rule = await readRules({ alertsClient, ruleId });
|
||||
if (rule != null) {
|
||||
return Boom.conflict(`rule_id: "${ruleId}" already exists`);
|
||||
return headers
|
||||
.response({
|
||||
message: `rule_id: "${ruleId}" already exists`,
|
||||
status_code: 409,
|
||||
})
|
||||
.code(409);
|
||||
}
|
||||
}
|
||||
const createdRule = await createRules({
|
||||
|
@ -126,9 +133,25 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
search: `${createdRule.id}`,
|
||||
searchFields: ['alertId'],
|
||||
});
|
||||
return transformOrError(createdRule, ruleStatuses.saved_objects[0]);
|
||||
const transformed = transform(createdRule, ruleStatuses.saved_objects[0]);
|
||||
if (transformed == null) {
|
||||
return headers
|
||||
.response({
|
||||
message: 'Internal error transforming rules',
|
||||
status_code: 500,
|
||||
})
|
||||
.code(500);
|
||||
} else {
|
||||
return transformed;
|
||||
}
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -41,7 +41,7 @@ export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
|
|||
if (!alertsClient || !savedObjectsClient) {
|
||||
return headers.response().code(404);
|
||||
}
|
||||
const rules = Promise.all(
|
||||
const rules = await Promise.all(
|
||||
request.payload.map(async payloadRule => {
|
||||
const { id, rule_id: ruleId } = payloadRule;
|
||||
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
|
||||
|
|
|
@ -11,7 +11,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
|||
import { deleteRules } from '../../rules/delete_rules';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { queryRulesSchema } from '../schemas/query_rules_schema';
|
||||
import { getIdError, transformOrError } from './utils';
|
||||
import { getIdError, transform } from './utils';
|
||||
import { transformError } from '../utils';
|
||||
import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
|
||||
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
|
||||
|
@ -62,12 +62,34 @@ export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
ruleStatuses.saved_objects.forEach(async obj =>
|
||||
savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id)
|
||||
);
|
||||
return transformOrError(rule, ruleStatuses.saved_objects[0]);
|
||||
const transformed = transform(rule, ruleStatuses.saved_objects[0]);
|
||||
if (transformed == null) {
|
||||
return headers
|
||||
.response({
|
||||
message: 'Internal error transforming rules',
|
||||
status_code: 500,
|
||||
})
|
||||
.code(500);
|
||||
} else {
|
||||
return transformed;
|
||||
}
|
||||
} else {
|
||||
return getIdError({ id, ruleId });
|
||||
const error = getIdError({ id, ruleId });
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Hapi from 'hapi';
|
||||
import { isFunction } from 'lodash/fp';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
|
@ -14,6 +13,7 @@ import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_r
|
|||
import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema';
|
||||
import { getExportByObjectIds } from '../../rules/get_export_by_object_ids';
|
||||
import { getExportAll } from '../../rules/get_export_all';
|
||||
import { transformError } from '../utils';
|
||||
|
||||
export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
return {
|
||||
|
@ -39,11 +39,21 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
try {
|
||||
const exportSizeLimit = server.config().get<number>('savedObjects.maxImportExportSize');
|
||||
if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) {
|
||||
return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`);
|
||||
return headers
|
||||
.response({
|
||||
message: `Can't export more than ${exportSizeLimit} rules`,
|
||||
status_code: 400,
|
||||
})
|
||||
.code(400);
|
||||
} else {
|
||||
const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient });
|
||||
if (nonPackagedRulesCount > exportSizeLimit) {
|
||||
return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`);
|
||||
return headers
|
||||
.response({
|
||||
message: `Can't export more than ${exportSizeLimit} rules`,
|
||||
status_code: 400,
|
||||
})
|
||||
.code(400);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,8 +69,14 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
return response
|
||||
.header('Content-Disposition', `attachment; filename="${request.query.file_name}"`)
|
||||
.header('Content-Type', 'application/ndjson');
|
||||
} catch {
|
||||
return Boom.badRequest(`Sorry, something went wrong to export rules`);
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { findRules } from '../../rules/find_rules';
|
|||
import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
|
||||
import { findRulesSchema } from '../schemas/find_rules_schema';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { transformFindAlertsOrError } from './utils';
|
||||
import { transformFindAlerts } from './utils';
|
||||
import { transformError } from '../utils';
|
||||
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
|
||||
|
||||
|
@ -62,9 +62,25 @@ export const createFindRulesRoute = (): Hapi.ServerRoute => {
|
|||
return results;
|
||||
})
|
||||
);
|
||||
return transformFindAlertsOrError(rules, ruleStatuses);
|
||||
const transformed = transformFindAlerts(rules, ruleStatuses);
|
||||
if (transformed == null) {
|
||||
return headers
|
||||
.response({
|
||||
message: 'unknown data type, error transforming alert',
|
||||
status_code: 500,
|
||||
})
|
||||
.code(500);
|
||||
} else {
|
||||
return transformed;
|
||||
}
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -55,7 +55,13 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => {
|
|||
rules_not_updated: rulesToUpdate.length,
|
||||
};
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Hapi from 'hapi';
|
||||
import { chunk, isEmpty, isFunction } from 'lodash/fp';
|
||||
import { extname } from 'path';
|
||||
|
@ -24,18 +23,12 @@ import {
|
|||
} from '../utils';
|
||||
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
|
||||
import { ImportRuleAlertRest } from '../../types';
|
||||
import { updateRules } from '../../rules/update_rules';
|
||||
import { patchRules } from '../../rules/patch_rules';
|
||||
import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema';
|
||||
import { KibanaRequest } from '../../../../../../../../../src/core/server';
|
||||
|
||||
type PromiseFromStreams = ImportRuleAlertRest | Error;
|
||||
|
||||
/*
|
||||
* We were getting some error like that possible EventEmitter memory leak detected
|
||||
* So we decide to batch the update by 10 to avoid any complication in the node side
|
||||
* https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n
|
||||
*
|
||||
*/
|
||||
const CHUNK_PARSED_OBJECT_SIZE = 10;
|
||||
|
||||
export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
|
@ -71,13 +64,17 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
const { filename } = request.payload.file.hapi;
|
||||
const fileExtension = extname(filename).toLowerCase();
|
||||
if (fileExtension !== '.ndjson') {
|
||||
return Boom.badRequest(`Invalid file extension ${fileExtension}`);
|
||||
return headers
|
||||
.response({
|
||||
message: `Invalid file extension ${fileExtension}`,
|
||||
status_code: 400,
|
||||
})
|
||||
.code(400);
|
||||
}
|
||||
|
||||
const objectLimit = server.config().get<number>('savedObjects.maxImportExportSize');
|
||||
const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([readStream]);
|
||||
|
||||
const uniqueParsedObjects = Array.from(
|
||||
parsedObjects
|
||||
.reduce(
|
||||
|
@ -122,6 +119,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
}
|
||||
const {
|
||||
description,
|
||||
enabled,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
|
@ -166,7 +164,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
alertsClient,
|
||||
actionsClient,
|
||||
description,
|
||||
enabled: false,
|
||||
enabled,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
|
@ -194,12 +192,12 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
});
|
||||
resolve({ rule_id: ruleId, status_code: 200 });
|
||||
} else if (rule != null && request.query.overwrite) {
|
||||
await updateRules({
|
||||
await patchRules({
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
savedObjectsClient,
|
||||
description,
|
||||
enabled: false,
|
||||
enabled,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
|
@ -232,7 +230,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode: 409,
|
||||
message: `This Rule "${rule.name}" already exists`,
|
||||
message: `rule_id: "${ruleId}" already exists`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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,
|
||||
createMockServerWithoutAlertClientDecoration,
|
||||
} from '../__mocks__/_mock_server';
|
||||
|
||||
import { patchRulesRoute } from './patch_rules_route';
|
||||
import { ServerInjectOptions } from 'hapi';
|
||||
|
||||
import {
|
||||
getFindResult,
|
||||
getResult,
|
||||
updateActionResult,
|
||||
typicalPayload,
|
||||
getFindResultWithSingleHit,
|
||||
getPatchBulkRequest,
|
||||
} from '../__mocks__/request_responses';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { patchRulesBulkRoute } from './patch_rules_bulk_route';
|
||||
import { BulkError } from '../utils';
|
||||
|
||||
describe('patch_rules_bulk', () => {
|
||||
let { server, alertsClient, actionsClient } = createMockServer();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
({ server, alertsClient, actionsClient } = createMockServer());
|
||||
patchRulesBulkRoute(server);
|
||||
});
|
||||
|
||||
describe('status codes with actionClient and alertClient', () => {
|
||||
test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
const { statusCode } = await server.inject(getPatchBulkRequest());
|
||||
expect(statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('returns 200 as a response when updating a single rule that does not exist', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
const { statusCode } = await server.inject(getPatchBulkRequest());
|
||||
expect(statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('returns 404 within the payload when updating a single rule that does not exist', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
const { payload } = await server.inject(getPatchBulkRequest());
|
||||
const parsed: BulkError[] = JSON.parse(payload);
|
||||
const expected: BulkError[] = [
|
||||
{
|
||||
error: { message: 'rule_id: "rule-1" not found', status_code: 404 },
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
];
|
||||
expect(parsed).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns 404 if alertClient is not available on the route', async () => {
|
||||
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
|
||||
patchRulesRoute(serverWithoutAlertClient);
|
||||
const { statusCode } = await serverWithoutAlertClient.inject(getPatchBulkRequest());
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
test('returns 400 if id is not given in either the body or the url', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
const { rule_id, ...noId } = typicalPayload();
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
|
||||
payload: [noId],
|
||||
};
|
||||
const { statusCode } = await server.inject(request);
|
||||
expect(statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('returns errors as 200 to just indicate ok something happened', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
|
||||
payload: [typicalPayload()],
|
||||
};
|
||||
const { statusCode } = await server.inject(request);
|
||||
expect(statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
test('returns 404 in the payload if the record does not exist yet', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
|
||||
payload: [typicalPayload()],
|
||||
};
|
||||
const { payload } = await server.inject(request);
|
||||
const parsed: BulkError[] = JSON.parse(payload);
|
||||
const expected: BulkError[] = [
|
||||
{
|
||||
error: { message: 'rule_id: "rule-1" not found', status_code: 404 },
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
];
|
||||
expect(parsed).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns 200 if type is query', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
|
||||
payload: [typicalPayload()],
|
||||
};
|
||||
const { statusCode } = await server.inject(request);
|
||||
expect(statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('returns 400 if type is not filter or kql', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
const { type, ...noType } = typicalPayload();
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
|
||||
payload: [
|
||||
{
|
||||
...noType,
|
||||
type: 'something-made-up',
|
||||
},
|
||||
],
|
||||
};
|
||||
const { statusCode } = await server.inject(request);
|
||||
expect(statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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_RULES_URL } from '../../../../../common/constants';
|
||||
import {
|
||||
BulkPatchRulesRequest,
|
||||
IRuleSavedAttributesSavedObjectAttributes,
|
||||
} from '../../rules/types';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { transformOrBulkError, getIdBulkError } from './utils';
|
||||
import { transformBulkError } from '../utils';
|
||||
import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema';
|
||||
import { patchRules } from '../../rules/patch_rules';
|
||||
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
|
||||
import { KibanaRequest } from '../../../../../../../../../src/core/server';
|
||||
|
||||
export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
return {
|
||||
method: 'PATCH',
|
||||
path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
|
||||
options: {
|
||||
tags: ['access:siem'],
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
},
|
||||
payload: patchRulesBulkSchema,
|
||||
},
|
||||
},
|
||||
async handler(request: BulkPatchRulesRequest, headers) {
|
||||
const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null;
|
||||
const actionsClient = await server.plugins.actions.getActionsClientWithRequest(
|
||||
KibanaRequest.from((request as unknown) as Hapi.Request)
|
||||
);
|
||||
const savedObjectsClient = isFunction(request.getSavedObjectsClient)
|
||||
? request.getSavedObjectsClient()
|
||||
: null;
|
||||
if (!alertsClient || !savedObjectsClient) {
|
||||
return headers.response().code(404);
|
||||
}
|
||||
|
||||
const rules = await Promise.all(
|
||||
request.payload.map(async payloadRule => {
|
||||
const {
|
||||
description,
|
||||
enabled,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
query,
|
||||
language,
|
||||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule_id: ruleId,
|
||||
id,
|
||||
index,
|
||||
interval,
|
||||
max_signals: maxSignals,
|
||||
risk_score: riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
references,
|
||||
version,
|
||||
} = payloadRule;
|
||||
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
|
||||
try {
|
||||
const rule = await patchRules({
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
description,
|
||||
enabled,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
savedObjectsClient,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
id,
|
||||
ruleId,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
references,
|
||||
version,
|
||||
});
|
||||
if (rule != null) {
|
||||
const ruleStatuses = await savedObjectsClient.find<
|
||||
IRuleSavedAttributesSavedObjectAttributes
|
||||
>({
|
||||
type: ruleStatusSavedObjectType,
|
||||
perPage: 1,
|
||||
sortField: 'statusDate',
|
||||
sortOrder: 'desc',
|
||||
search: rule.id,
|
||||
searchFields: ['alertId'],
|
||||
});
|
||||
return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]);
|
||||
} else {
|
||||
return getIdBulkError({ id, ruleId });
|
||||
}
|
||||
} catch (err) {
|
||||
return transformBulkError(idOrRuleIdOrUnknown, err);
|
||||
}
|
||||
})
|
||||
);
|
||||
return rules;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const patchRulesBulkRoute = (server: ServerFacade): void => {
|
||||
server.route(createPatchRulesBulkRoute(server));
|
||||
};
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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,
|
||||
createMockServerWithoutAlertClientDecoration,
|
||||
} from '../__mocks__/_mock_server';
|
||||
|
||||
import { patchRulesRoute } from './patch_rules_route';
|
||||
import { ServerInjectOptions } from 'hapi';
|
||||
|
||||
import {
|
||||
getFindResult,
|
||||
getFindResultStatus,
|
||||
getResult,
|
||||
updateActionResult,
|
||||
getPatchRequest,
|
||||
typicalPayload,
|
||||
getFindResultWithSingleHit,
|
||||
} from '../__mocks__/request_responses';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
|
||||
describe('patch_rules', () => {
|
||||
let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer());
|
||||
patchRulesRoute(server);
|
||||
});
|
||||
|
||||
describe('status codes with actionClient and alertClient', () => {
|
||||
test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
|
||||
const { statusCode } = await server.inject(getPatchRequest());
|
||||
expect(statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('returns 404 when updating a single rule that does not exist', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
|
||||
const { statusCode } = await server.inject(getPatchRequest());
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('returns 404 if alertClient is not available on the route', async () => {
|
||||
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
|
||||
patchRulesRoute(serverWithoutAlertClient);
|
||||
const { statusCode } = await serverWithoutAlertClient.inject(getPatchRequest());
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
test('returns 400 if id is not given in either the body or the url', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
|
||||
const { rule_id, ...noId } = typicalPayload();
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: DETECTION_ENGINE_RULES_URL,
|
||||
payload: {
|
||||
payload: noId,
|
||||
},
|
||||
};
|
||||
const { statusCode } = await server.inject(request);
|
||||
expect(statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('returns 404 if the record does not exist yet', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: DETECTION_ENGINE_RULES_URL,
|
||||
payload: typicalPayload(),
|
||||
};
|
||||
const { statusCode } = await server.inject(request);
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('returns 200 if type is query', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: DETECTION_ENGINE_RULES_URL,
|
||||
payload: typicalPayload(),
|
||||
};
|
||||
const { statusCode } = await server.inject(request);
|
||||
expect(statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('returns 400 if type is not filter or kql', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
|
||||
const { type, ...noType } = typicalPayload();
|
||||
const request: ServerInjectOptions = {
|
||||
method: 'PATCH',
|
||||
url: DETECTION_ENGINE_RULES_URL,
|
||||
payload: {
|
||||
...noType,
|
||||
type: 'something-made-up',
|
||||
},
|
||||
};
|
||||
const { statusCode } = await server.inject(request);
|
||||
expect(statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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_RULES_URL } from '../../../../../common/constants';
|
||||
import { patchRules } from '../../rules/patch_rules';
|
||||
import { PatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
|
||||
import { patchRulesSchema } from '../schemas/patch_rules_schema';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { getIdError, transform } from './utils';
|
||||
import { transformError } from '../utils';
|
||||
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
|
||||
import { KibanaRequest } from '../../../../../../../../../src/core/server';
|
||||
|
||||
export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
return {
|
||||
method: 'PATCH',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
options: {
|
||||
tags: ['access:siem'],
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
},
|
||||
payload: patchRulesSchema,
|
||||
},
|
||||
},
|
||||
async handler(request: PatchRulesRequest, headers) {
|
||||
const {
|
||||
description,
|
||||
enabled,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
query,
|
||||
language,
|
||||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule_id: ruleId,
|
||||
id,
|
||||
index,
|
||||
interval,
|
||||
max_signals: maxSignals,
|
||||
risk_score: riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
references,
|
||||
version,
|
||||
} = request.payload;
|
||||
|
||||
const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null;
|
||||
const actionsClient = await server.plugins.actions.getActionsClientWithRequest(
|
||||
KibanaRequest.from((request as unknown) as Hapi.Request)
|
||||
);
|
||||
const savedObjectsClient = isFunction(request.getSavedObjectsClient)
|
||||
? request.getSavedObjectsClient()
|
||||
: null;
|
||||
if (!alertsClient || !savedObjectsClient) {
|
||||
return headers.response().code(404);
|
||||
}
|
||||
|
||||
try {
|
||||
const rule = await patchRules({
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
description,
|
||||
enabled,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
savedObjectsClient,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
id,
|
||||
ruleId,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
references,
|
||||
version,
|
||||
});
|
||||
if (rule != null) {
|
||||
const ruleStatuses = await savedObjectsClient.find<
|
||||
IRuleSavedAttributesSavedObjectAttributes
|
||||
>({
|
||||
type: ruleStatusSavedObjectType,
|
||||
perPage: 1,
|
||||
sortField: 'statusDate',
|
||||
sortOrder: 'desc',
|
||||
search: rule.id,
|
||||
searchFields: ['alertId'],
|
||||
});
|
||||
const transformed = transform(rule, ruleStatuses.saved_objects[0]);
|
||||
if (transformed == null) {
|
||||
return headers
|
||||
.response({
|
||||
message: 'Internal error transforming rules',
|
||||
status_code: 500,
|
||||
})
|
||||
.code(500);
|
||||
} else {
|
||||
return transformed;
|
||||
}
|
||||
} else {
|
||||
const error = getIdError({ id, ruleId });
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const patchRulesRoute = (server: ServerFacade) => {
|
||||
server.route(createPatchRulesRoute(server));
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
import Hapi from 'hapi';
|
||||
import { isFunction } from 'lodash/fp';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { getIdError, transformOrError } from './utils';
|
||||
import { getIdError, transform } from './utils';
|
||||
import { transformError } from '../utils';
|
||||
|
||||
import { readRules } from '../../rules/read_rules';
|
||||
|
@ -54,12 +54,34 @@ export const createReadRulesRoute: Hapi.ServerRoute = {
|
|||
search: rule.id,
|
||||
searchFields: ['alertId'],
|
||||
});
|
||||
return transformOrError(rule, ruleStatuses.saved_objects[0]);
|
||||
const transformedOrError = transform(rule, ruleStatuses.saved_objects[0]);
|
||||
if (transformedOrError == null) {
|
||||
return headers
|
||||
.response({
|
||||
message: 'Internal error transforming rules',
|
||||
status_code: 500,
|
||||
})
|
||||
.code(500);
|
||||
} else {
|
||||
return transformedOrError;
|
||||
}
|
||||
} else {
|
||||
return getIdError({ id, ruleId });
|
||||
const error = getIdError({ id, ruleId });
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -13,11 +13,11 @@ import {
|
|||
} from '../../rules/types';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { transformOrBulkError, getIdBulkError } from './utils';
|
||||
import { transformBulkError } from '../utils';
|
||||
import { transformBulkError, getIndex } from '../utils';
|
||||
import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema';
|
||||
import { updateRules } from '../../rules/update_rules';
|
||||
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
|
||||
import { KibanaRequest } from '../../../../../../../../../src/core/server';
|
||||
import { updateRules } from '../../rules/update_rules';
|
||||
|
||||
export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
return {
|
||||
|
@ -44,7 +44,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
|
|||
return headers.response().code(404);
|
||||
}
|
||||
|
||||
const rules = Promise.all(
|
||||
const rules = await Promise.all(
|
||||
request.payload.map(async payloadRule => {
|
||||
const {
|
||||
description,
|
||||
|
@ -74,6 +74,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
|
|||
references,
|
||||
version,
|
||||
} = payloadRule;
|
||||
const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server);
|
||||
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
|
||||
try {
|
||||
const rule = await updateRules({
|
||||
|
@ -81,11 +82,12 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
|
|||
actionsClient,
|
||||
description,
|
||||
enabled,
|
||||
immutable: false,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
outputIndex: finalIndex,
|
||||
savedId,
|
||||
savedObjectsClient,
|
||||
timelineId,
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
import Hapi from 'hapi';
|
||||
import { isFunction } from 'lodash/fp';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { updateRules } from '../../rules/update_rules';
|
||||
import { UpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
|
||||
import { updateRulesSchema } from '../schemas/update_rules_schema';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { getIdError, transformOrError } from './utils';
|
||||
import { transformError } from '../utils';
|
||||
import { getIdError, transform } from './utils';
|
||||
import { transformError, getIndex } from '../utils';
|
||||
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
|
||||
import { KibanaRequest } from '../../../../../../../../../src/core/server';
|
||||
import { updateRules } from '../../rules/update_rules';
|
||||
|
||||
export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
return {
|
||||
|
@ -39,8 +39,8 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
language,
|
||||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
timeline_id: timelineId = null,
|
||||
timeline_title: timelineTitle = null,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule_id: ruleId,
|
||||
|
@ -71,6 +71,7 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
}
|
||||
|
||||
try {
|
||||
const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server);
|
||||
const rule = await updateRules({
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
|
@ -78,9 +79,10 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
enabled,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable: false,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
outputIndex: finalIndex,
|
||||
savedId,
|
||||
savedObjectsClient,
|
||||
timelineId,
|
||||
|
@ -113,12 +115,34 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
search: rule.id,
|
||||
searchFields: ['alertId'],
|
||||
});
|
||||
return transformOrError(rule, ruleStatuses.saved_objects[0]);
|
||||
const transformed = transform(rule, ruleStatuses.saved_objects[0]);
|
||||
if (transformed == null) {
|
||||
return headers
|
||||
.response({
|
||||
message: 'Internal error transforming rules',
|
||||
status_code: 500,
|
||||
})
|
||||
.code(500);
|
||||
} else {
|
||||
return transformed;
|
||||
}
|
||||
} else {
|
||||
return getIdError({ id, ruleId });
|
||||
const error = getIdError({ id, ruleId });
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
|
||||
import {
|
||||
transformAlertToRule,
|
||||
getIdError,
|
||||
transformFindAlertsOrError,
|
||||
transformOrError,
|
||||
transformFindAlerts,
|
||||
transform,
|
||||
transformTags,
|
||||
getIdBulkError,
|
||||
transformOrBulkError,
|
||||
|
@ -547,55 +545,87 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
describe('getIdError', () => {
|
||||
test('it should have a status code', () => {
|
||||
const error = getIdError({ id: '123', ruleId: undefined });
|
||||
expect(error).toEqual({
|
||||
message: 'id: "123" not found',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs message about id not being found if only id is defined and ruleId is undefined', () => {
|
||||
const boom = getIdError({ id: '123', ruleId: undefined });
|
||||
expect(boom.message).toEqual('id: "123" not found');
|
||||
const error = getIdError({ id: '123', ruleId: undefined });
|
||||
expect(error).toEqual({
|
||||
message: 'id: "123" not found',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs message about id not being found if only id is defined and ruleId is null', () => {
|
||||
const boom = getIdError({ id: '123', ruleId: null });
|
||||
expect(boom.message).toEqual('id: "123" not found');
|
||||
const error = getIdError({ id: '123', ruleId: null });
|
||||
expect(error).toEqual({
|
||||
message: 'id: "123" not found',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => {
|
||||
const boom = getIdError({ id: undefined, ruleId: 'rule-id-123' });
|
||||
expect(boom.message).toEqual('rule_id: "rule-id-123" not found');
|
||||
const error = getIdError({ id: undefined, ruleId: 'rule-id-123' });
|
||||
expect(error).toEqual({
|
||||
message: 'rule_id: "rule-id-123" not found',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => {
|
||||
const boom = getIdError({ id: null, ruleId: 'rule-id-123' });
|
||||
expect(boom.message).toEqual('rule_id: "rule-id-123" not found');
|
||||
const error = getIdError({ id: null, ruleId: 'rule-id-123' });
|
||||
expect(error).toEqual({
|
||||
message: 'rule_id: "rule-id-123" not found',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs message about both being not defined when both are undefined', () => {
|
||||
const boom = getIdError({ id: undefined, ruleId: undefined });
|
||||
expect(boom.message).toEqual('id or rule_id should have been defined');
|
||||
const error = getIdError({ id: undefined, ruleId: undefined });
|
||||
expect(error).toEqual({
|
||||
message: 'id or rule_id should have been defined',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs message about both being not defined when both are null', () => {
|
||||
const boom = getIdError({ id: null, ruleId: null });
|
||||
expect(boom.message).toEqual('id or rule_id should have been defined');
|
||||
const error = getIdError({ id: null, ruleId: null });
|
||||
expect(error).toEqual({
|
||||
message: 'id or rule_id should have been defined',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs message about both being not defined when id is null and ruleId is undefined', () => {
|
||||
const boom = getIdError({ id: null, ruleId: undefined });
|
||||
expect(boom.message).toEqual('id or rule_id should have been defined');
|
||||
const error = getIdError({ id: null, ruleId: undefined });
|
||||
expect(error).toEqual({
|
||||
message: 'id or rule_id should have been defined',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
test('outputs message about both being not defined when id is undefined and ruleId is null', () => {
|
||||
const boom = getIdError({ id: undefined, ruleId: null });
|
||||
expect(boom.message).toEqual('id or rule_id should have been defined');
|
||||
const error = getIdError({ id: undefined, ruleId: null });
|
||||
expect(error).toEqual({
|
||||
message: 'id or rule_id should have been defined',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformFindAlertsOrError', () => {
|
||||
describe('transformFindAlerts', () => {
|
||||
test('outputs empty data set when data set is empty correct', () => {
|
||||
const output = transformFindAlertsOrError({ data: [] });
|
||||
const output = transformFindAlerts({ data: [] });
|
||||
expect(output).toEqual({ data: [] });
|
||||
});
|
||||
|
||||
test('outputs 200 if the data is of type siem alert', () => {
|
||||
const output = transformFindAlertsOrError({
|
||||
const output = transformFindAlerts({
|
||||
data: [getResult()],
|
||||
});
|
||||
const expected: OutputRuleAlertRest = {
|
||||
|
@ -663,14 +693,14 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
test('returns 500 if the data is not of type siem alert', () => {
|
||||
const output = transformFindAlertsOrError({ data: [{ random: 1 }] });
|
||||
expect((output as Boom).message).toEqual('Internal error transforming');
|
||||
const output = transformFindAlerts({ data: [{ random: 1 }] });
|
||||
expect(output).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformOrError', () => {
|
||||
test('outputs 200 if the data is of type siem alert', () => {
|
||||
const output = transformOrError(getResult());
|
||||
const output = transform(getResult());
|
||||
const expected: OutputRuleAlertRest = {
|
||||
created_by: 'elastic',
|
||||
created_at: '2019-12-13T16:40:33.400Z',
|
||||
|
@ -734,8 +764,8 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
test('returns 500 if the data is not of type siem alert', () => {
|
||||
const output = transformOrError({ data: [{ random: 1 }] });
|
||||
expect((output as Boom).message).toEqual('Internal error transforming');
|
||||
const output = transform({ data: [{ random: 1 }] });
|
||||
expect(output).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { pickBy } from 'lodash/fp';
|
||||
import { SavedObject } from 'kibana/server';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
|
@ -24,6 +23,7 @@ import {
|
|||
createSuccessObject,
|
||||
ImportSuccessError,
|
||||
createImportErrorObject,
|
||||
OutputError,
|
||||
} from '../utils';
|
||||
|
||||
export const getIdError = ({
|
||||
|
@ -32,13 +32,22 @@ export const getIdError = ({
|
|||
}: {
|
||||
id: string | undefined | null;
|
||||
ruleId: string | undefined | null;
|
||||
}) => {
|
||||
}): OutputError => {
|
||||
if (id != null) {
|
||||
return Boom.notFound(`id: "${id}" not found`);
|
||||
return {
|
||||
message: `id: "${id}" not found`,
|
||||
statusCode: 404,
|
||||
};
|
||||
} else if (ruleId != null) {
|
||||
return Boom.notFound(`rule_id: "${ruleId}" not found`);
|
||||
return {
|
||||
message: `rule_id: "${ruleId}" not found`,
|
||||
statusCode: 404,
|
||||
};
|
||||
} else {
|
||||
return Boom.notFound('id or rule_id should have been defined');
|
||||
return {
|
||||
message: 'id or rule_id should have been defined',
|
||||
statusCode: 404,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -136,10 +145,10 @@ export const transformAlertsToRules = (
|
|||
return alerts.map(alert => transformAlertToRule(alert));
|
||||
};
|
||||
|
||||
export const transformFindAlertsOrError = (
|
||||
export const transformFindAlerts = (
|
||||
findResults: { data: unknown[] },
|
||||
ruleStatuses?: unknown[]
|
||||
): unknown | Boom => {
|
||||
): unknown | null => {
|
||||
if (!ruleStatuses && isAlertTypes(findResults.data)) {
|
||||
findResults.data = findResults.data.map(alert => transformAlertToRule(alert));
|
||||
return findResults;
|
||||
|
@ -150,14 +159,14 @@ export const transformFindAlertsOrError = (
|
|||
);
|
||||
return findResults;
|
||||
} else {
|
||||
return new Boom('Internal error transforming', { statusCode: 500 });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const transformOrError = (
|
||||
export const transform = (
|
||||
alert: unknown,
|
||||
ruleStatus?: unknown
|
||||
): Partial<OutputRuleAlertRest> | Boom => {
|
||||
): Partial<OutputRuleAlertRest> | null => {
|
||||
if (!ruleStatus && isAlertType(alert)) {
|
||||
return transformAlertToRule(alert);
|
||||
}
|
||||
|
@ -166,7 +175,7 @@ export const transformOrError = (
|
|||
} else if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) {
|
||||
return transformAlertToRule(alert, ruleStatus);
|
||||
} else {
|
||||
return new Boom('Internal error transforming', { statusCode: 500 });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { createRulesBulkSchema } from './create_rules_bulk_schema';
|
||||
import { UpdateRuleAlertParamsRest } from '../../rules/types';
|
||||
import { PatchRuleAlertParamsRest } from '../../rules/types';
|
||||
|
||||
// only the basics of testing are here.
|
||||
// see: create_rules_schema.test.ts for the bulk of the validation tests
|
||||
|
@ -13,7 +13,7 @@ import { UpdateRuleAlertParamsRest } from '../../rules/types';
|
|||
describe('create_rules_bulk_schema', () => {
|
||||
test('can take an empty array and validate it', () => {
|
||||
expect(
|
||||
createRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([]).error
|
||||
createRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([]).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
|
@ -29,7 +29,7 @@ describe('create_rules_bulk_schema', () => {
|
|||
|
||||
test('single array of [id] does validate', () => {
|
||||
expect(
|
||||
createRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
createRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
|
@ -49,7 +49,7 @@ describe('create_rules_bulk_schema', () => {
|
|||
|
||||
test('two values of [id] does validate', () => {
|
||||
expect(
|
||||
createRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
createRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
|
@ -82,7 +82,7 @@ describe('create_rules_bulk_schema', () => {
|
|||
|
||||
test('The default for "from" will be "now-6m"', () => {
|
||||
expect(
|
||||
createRulesBulkSchema.validate<Partial<UpdateRuleAlertParamsRest>>([
|
||||
createRulesBulkSchema.validate<Partial<PatchRuleAlertParamsRest>>([
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
|
@ -102,7 +102,7 @@ describe('create_rules_bulk_schema', () => {
|
|||
|
||||
test('The default for "to" will be "now"', () => {
|
||||
expect(
|
||||
createRulesBulkSchema.validate<Partial<UpdateRuleAlertParamsRest>>([
|
||||
createRulesBulkSchema.validate<Partial<PatchRuleAlertParamsRest>>([
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
|
@ -122,7 +122,7 @@ describe('create_rules_bulk_schema', () => {
|
|||
|
||||
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
|
||||
expect(
|
||||
createRulesBulkSchema.validate<Partial<UpdateRuleAlertParamsRest>>([
|
||||
createRulesBulkSchema.validate<Partial<PatchRuleAlertParamsRest>>([
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
*/
|
||||
|
||||
import { createRulesSchema } from './create_rules_schema';
|
||||
import { UpdateRuleAlertParamsRest } from '../../rules/types';
|
||||
import { PatchRuleAlertParamsRest } from '../../rules/types';
|
||||
import { ThreatParams, RuleAlertParamsRest } from '../../types';
|
||||
|
||||
describe('create rules schema', () => {
|
||||
test('empty objects do not validate', () => {
|
||||
expect(createRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({}).error).toBeTruthy();
|
||||
expect(createRulesSchema.validate<Partial<PatchRuleAlertParamsRest>>({}).error).toBeTruthy();
|
||||
});
|
||||
|
||||
test('made up values do not validate', () => {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { patchRulesBulkSchema } from './patch_rules_bulk_schema';
|
||||
import { PatchRuleAlertParamsRest } from '../../rules/types';
|
||||
|
||||
// only the basics of testing are here.
|
||||
// see: patch_rules_schema.test.ts for the bulk of the validation tests
|
||||
// this just wraps patchRulesSchema in an array
|
||||
describe('patch_rules_bulk_schema', () => {
|
||||
test('can take an empty array and validate it', () => {
|
||||
expect(
|
||||
patchRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([]).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('made up values do not validate', () => {
|
||||
expect(
|
||||
patchRulesBulkSchema.validate<[{ madeUp: string }]>([
|
||||
{
|
||||
madeUp: 'hi',
|
||||
},
|
||||
]).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('single array of [id] does validate', () => {
|
||||
expect(
|
||||
patchRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{
|
||||
id: 'rule-1',
|
||||
},
|
||||
]).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('two values of [id] does validate', () => {
|
||||
expect(
|
||||
patchRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{
|
||||
id: 'rule-1',
|
||||
},
|
||||
{
|
||||
id: 'rule-2',
|
||||
},
|
||||
]).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 Joi from 'joi';
|
||||
|
||||
import { patchRulesSchema } from './patch_rules_schema';
|
||||
|
||||
export const patchRulesBulkSchema = Joi.array().items(patchRulesSchema);
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 Joi from 'joi';
|
||||
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import {
|
||||
enabled,
|
||||
description,
|
||||
false_positives,
|
||||
filters,
|
||||
from,
|
||||
index,
|
||||
rule_id,
|
||||
interval,
|
||||
query,
|
||||
language,
|
||||
output_index,
|
||||
saved_id,
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score,
|
||||
max_signals,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
references,
|
||||
id,
|
||||
version,
|
||||
} from './schemas';
|
||||
/* eslint-enable @typescript-eslint/camelcase */
|
||||
|
||||
export const patchRulesSchema = Joi.object({
|
||||
description,
|
||||
enabled,
|
||||
false_positives,
|
||||
filters,
|
||||
from,
|
||||
rule_id,
|
||||
id,
|
||||
index,
|
||||
interval,
|
||||
query: query.allow(''),
|
||||
language,
|
||||
output_index,
|
||||
saved_id,
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score,
|
||||
max_signals,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
references,
|
||||
version,
|
||||
}).xor('id', 'rule_id');
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { queryRulesBulkSchema } from './query_rules_bulk_schema';
|
||||
import { UpdateRuleAlertParamsRest } from '../../rules/types';
|
||||
import { PatchRuleAlertParamsRest } from '../../rules/types';
|
||||
|
||||
// only the basics of testing are here.
|
||||
// see: query_rules_bulk_schema.test.ts for the bulk of the validation tests
|
||||
|
@ -13,13 +13,13 @@ import { UpdateRuleAlertParamsRest } from '../../rules/types';
|
|||
describe('query_rules_bulk_schema', () => {
|
||||
test('can take an empty array and validate it', () => {
|
||||
expect(
|
||||
queryRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([]).error
|
||||
queryRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([]).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('both rule_id and id being supplied do not validate', () => {
|
||||
expect(
|
||||
queryRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
queryRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{
|
||||
rule_id: '1',
|
||||
id: '1',
|
||||
|
@ -32,7 +32,7 @@ describe('query_rules_bulk_schema', () => {
|
|||
|
||||
test('both rule_id and id being supplied do not validate if one array element works but the second does not', () => {
|
||||
expect(
|
||||
queryRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
queryRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{
|
||||
id: '1',
|
||||
},
|
||||
|
@ -48,13 +48,13 @@ describe('query_rules_bulk_schema', () => {
|
|||
|
||||
test('only id validates', () => {
|
||||
expect(
|
||||
queryRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([{ id: '1' }]).error
|
||||
queryRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([{ id: '1' }]).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('only id validates with two elements', () => {
|
||||
expect(
|
||||
queryRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
queryRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
]).error
|
||||
|
@ -63,14 +63,14 @@ describe('query_rules_bulk_schema', () => {
|
|||
|
||||
test('only rule_id validates', () => {
|
||||
expect(
|
||||
queryRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([{ rule_id: '1' }])
|
||||
queryRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([{ rule_id: '1' }])
|
||||
.error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('only rule_id validates with two elements', () => {
|
||||
expect(
|
||||
queryRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
queryRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{ rule_id: '1' },
|
||||
{ rule_id: '2' },
|
||||
]).error
|
||||
|
@ -79,7 +79,7 @@ describe('query_rules_bulk_schema', () => {
|
|||
|
||||
test('both id and rule_id validates with two separate elements', () => {
|
||||
expect(
|
||||
queryRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
queryRulesBulkSchema.validate<Array<Partial<PatchRuleAlertParamsRest>>>([
|
||||
{ id: '1' },
|
||||
{ rule_id: '2' },
|
||||
]).error
|
||||
|
|
|
@ -5,29 +5,29 @@
|
|||
*/
|
||||
|
||||
import { queryRulesSchema } from './query_rules_schema';
|
||||
import { UpdateRuleAlertParamsRest } from '../../rules/types';
|
||||
import { PatchRuleAlertParamsRest } from '../../rules/types';
|
||||
|
||||
describe('queryRulesSchema', () => {
|
||||
test('empty objects do not validate', () => {
|
||||
expect(queryRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({}).error).toBeTruthy();
|
||||
expect(queryRulesSchema.validate<Partial<PatchRuleAlertParamsRest>>({}).error).toBeTruthy();
|
||||
});
|
||||
|
||||
test('both rule_id and id being supplied do not validate', () => {
|
||||
expect(
|
||||
queryRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({ rule_id: '1', id: '1' }).error
|
||||
queryRulesSchema.validate<Partial<PatchRuleAlertParamsRest>>({ rule_id: '1', id: '1' }).error
|
||||
.message
|
||||
).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]');
|
||||
});
|
||||
|
||||
test('only id validates', () => {
|
||||
expect(
|
||||
queryRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({ id: '1' }).error
|
||||
queryRulesSchema.validate<Partial<PatchRuleAlertParamsRest>>({ id: '1' }).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('only rule_id validates', () => {
|
||||
expect(
|
||||
queryRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({ rule_id: '1' }).error
|
||||
queryRulesSchema.validate<Partial<PatchRuleAlertParamsRest>>({ rule_id: '1' }).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,7 +31,17 @@ describe('update_rules_bulk_schema', () => {
|
|||
expect(
|
||||
updateRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
{
|
||||
id: 'rule-1',
|
||||
id: 'id-1',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
name: 'some-name',
|
||||
severity: 'low',
|
||||
type: 'query',
|
||||
query: 'some query',
|
||||
index: ['index-1'],
|
||||
interval: '5m',
|
||||
},
|
||||
]).error
|
||||
).toBeFalsy();
|
||||
|
@ -41,10 +51,30 @@ describe('update_rules_bulk_schema', () => {
|
|||
expect(
|
||||
updateRulesBulkSchema.validate<Array<Partial<UpdateRuleAlertParamsRest>>>([
|
||||
{
|
||||
id: 'rule-1',
|
||||
id: 'id-1',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
name: 'some-name',
|
||||
severity: 'low',
|
||||
type: 'query',
|
||||
query: 'some query',
|
||||
index: ['index-1'],
|
||||
interval: '5m',
|
||||
},
|
||||
{
|
||||
id: 'rule-2',
|
||||
id: 'id-2',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
name: 'some-name',
|
||||
severity: 'low',
|
||||
type: 'query',
|
||||
query: 'some query',
|
||||
index: ['index-1'],
|
||||
interval: '5m',
|
||||
},
|
||||
]).error
|
||||
).toBeFalsy();
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -37,31 +37,44 @@ import {
|
|||
} from './schemas';
|
||||
/* eslint-enable @typescript-eslint/camelcase */
|
||||
|
||||
import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants';
|
||||
|
||||
/**
|
||||
* This almost identical to the create_rules_schema except for a few details.
|
||||
* - The version will not be defaulted to a 1. If it is not given then its default will become the previous version auto-incremented
|
||||
* This does break idempotency slightly as calls repeatedly without it will increment the number. If the version number is passed in
|
||||
* this will update the rule's version number.
|
||||
* - id is on here because you can pass in an id to update using it instead of rule_id.
|
||||
*/
|
||||
export const updateRulesSchema = Joi.object({
|
||||
description,
|
||||
enabled,
|
||||
false_positives,
|
||||
filters,
|
||||
from,
|
||||
rule_id,
|
||||
description: description.required(),
|
||||
enabled: enabled.default(true),
|
||||
id,
|
||||
false_positives: false_positives.default([]),
|
||||
filters,
|
||||
from: from.default('now-6m'),
|
||||
rule_id,
|
||||
index,
|
||||
interval,
|
||||
query: query.allow(''),
|
||||
language,
|
||||
interval: interval.default('5m'),
|
||||
query: query.allow('').default(''),
|
||||
language: language.default('kuery'),
|
||||
output_index,
|
||||
saved_id,
|
||||
saved_id: saved_id.when('type', {
|
||||
is: 'saved_query',
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.forbidden(),
|
||||
}),
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score,
|
||||
max_signals,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
references,
|
||||
risk_score: risk_score.required(),
|
||||
max_signals: max_signals.default(DEFAULT_MAX_SIGNALS),
|
||||
name: name.required(),
|
||||
severity: severity.required(),
|
||||
tags: tags.default([]),
|
||||
to: to.default('now'),
|
||||
type: type.required(),
|
||||
threat: threat.default([]),
|
||||
references: references.default([]),
|
||||
version,
|
||||
}).xor('id', 'rule_id');
|
||||
|
|
|
@ -34,7 +34,13 @@ export const createReadTagsRoute: Hapi.ServerRoute = {
|
|||
});
|
||||
return tags;
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
const error = transformError(err);
|
||||
return headers
|
||||
.response({
|
||||
message: error.message,
|
||||
status_code: error.statusCode,
|
||||
})
|
||||
.code(error.statusCode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -18,51 +18,69 @@ import {
|
|||
|
||||
describe('utils', () => {
|
||||
describe('transformError', () => {
|
||||
test('returns boom if it is a boom object', () => {
|
||||
const boom = new Boom('');
|
||||
test('returns transformed output error from boom object with a 500 and payload of internal server error', () => {
|
||||
const boom = new Boom('some boom message');
|
||||
const transformed = transformError(boom);
|
||||
expect(transformed).toBe(boom);
|
||||
expect(transformed).toEqual({
|
||||
message: 'An internal server error occurred',
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns a boom if it is some non boom object that has a statusCode', () => {
|
||||
test('returns transformed output if it is some non boom object that has a statusCode', () => {
|
||||
const error: Error & { statusCode?: number } = {
|
||||
statusCode: 403,
|
||||
name: 'some name',
|
||||
message: 'some message',
|
||||
};
|
||||
const transformed = transformError(error);
|
||||
expect(Boom.isBoom(transformed)).toBe(true);
|
||||
expect(transformed).toEqual({
|
||||
message: 'some message',
|
||||
statusCode: 403,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns a boom with the message set', () => {
|
||||
test('returns a transformed message with the message set and statusCode', () => {
|
||||
const error: Error & { statusCode?: number } = {
|
||||
statusCode: 403,
|
||||
name: 'some name',
|
||||
message: 'some message',
|
||||
};
|
||||
const transformed = transformError(error);
|
||||
expect(transformed.message).toBe('some message');
|
||||
expect(transformed).toEqual({
|
||||
message: 'some message',
|
||||
statusCode: 403,
|
||||
});
|
||||
});
|
||||
|
||||
test('does not return a boom if it is some non boom object but it does not have a status Code.', () => {
|
||||
test('transforms best it can if it is some non boom object but it does not have a status Code.', () => {
|
||||
const error: Error = {
|
||||
name: 'some name',
|
||||
message: 'some message',
|
||||
};
|
||||
const transformed = transformError(error);
|
||||
expect(Boom.isBoom(transformed)).toBe(false);
|
||||
expect(transformed).toEqual({
|
||||
message: 'some message',
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
|
||||
test('it detects a TypeError and returns a Boom', () => {
|
||||
test('it detects a TypeError and returns a status code of 400 from that particular error type', () => {
|
||||
const error: TypeError = new TypeError('I have a type error');
|
||||
const transformed = transformError(error);
|
||||
expect(Boom.isBoom(transformed)).toBe(true);
|
||||
expect(transformed).toEqual({
|
||||
message: 'I have a type error',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
test('it detects a TypeError and returns a Boom status of 400', () => {
|
||||
const error: TypeError = new TypeError('I have a type error');
|
||||
const transformed = transformError(error) as Boom;
|
||||
expect(transformed.output.statusCode).toBe(400);
|
||||
const transformed = transformError(error);
|
||||
expect(transformed).toEqual({
|
||||
message: 'I have a type error',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,20 +8,37 @@ import Boom from 'boom';
|
|||
import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants';
|
||||
import { ServerFacade, RequestFacade } from '../../../types';
|
||||
|
||||
export const transformError = (err: Error & { statusCode?: number }) => {
|
||||
export interface OutputError {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export const transformError = (err: Error & { statusCode?: number }): OutputError => {
|
||||
if (Boom.isBoom(err)) {
|
||||
return err;
|
||||
return {
|
||||
message: err.output.payload.message,
|
||||
statusCode: err.output.statusCode,
|
||||
};
|
||||
} else {
|
||||
if (err.statusCode != null) {
|
||||
return new Boom(err.message, { statusCode: err.statusCode });
|
||||
return {
|
||||
message: err.message,
|
||||
statusCode: err.statusCode,
|
||||
};
|
||||
} else if (err instanceof TypeError) {
|
||||
// allows us to throw type errors instead of booms in some conditions
|
||||
// where we don't want to mingle Boom with the rest of the code
|
||||
return new Boom(err.message, { statusCode: 400 });
|
||||
return {
|
||||
message: err.message,
|
||||
statusCode: 400,
|
||||
};
|
||||
} else {
|
||||
// natively return the err and allow the regular framework
|
||||
// to deal with the error when it is a non Boom
|
||||
return err;
|
||||
return {
|
||||
message: err.message ?? '(unknown error message)',
|
||||
statusCode: 500,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Alert } from '../../../../../alerting/common';
|
||||
import { APP_ID, SIGNALS_ID } from '../../../../common/constants';
|
||||
import { CreateRuleParams } from './types';
|
||||
import { addTags } from './add_tags';
|
||||
|
@ -37,7 +38,7 @@ export const createRules = ({
|
|||
type,
|
||||
references,
|
||||
version,
|
||||
}: CreateRuleParams) => {
|
||||
}: CreateRuleParams): Promise<Alert> => {
|
||||
return alertsClient.create({
|
||||
data: {
|
||||
name,
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 { defaults } from 'lodash/fp';
|
||||
import { PartialAlert } from '../../../../../alerting/server/types';
|
||||
import { readRules } from './read_rules';
|
||||
import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types';
|
||||
import { addTags } from './add_tags';
|
||||
import { ruleStatusSavedObjectType } from './saved_object_mappings';
|
||||
import { calculateVersion, calculateName, calculateInterval } from './utils';
|
||||
|
||||
export const patchRules = async ({
|
||||
alertsClient,
|
||||
actionsClient, // TODO: Use this whenever we add feature support for different action types
|
||||
savedObjectsClient,
|
||||
description,
|
||||
falsePositives,
|
||||
enabled,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
from,
|
||||
immutable,
|
||||
id,
|
||||
ruleId,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
threat,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
version,
|
||||
}: PatchRuleParams): Promise<PartialAlert | null> => {
|
||||
const rule = await readRules({ alertsClient, ruleId, id });
|
||||
if (rule == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, {
|
||||
description,
|
||||
falsePositives,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
from,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
threat,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
version,
|
||||
});
|
||||
|
||||
const nextParams = defaults(
|
||||
{
|
||||
...rule.params,
|
||||
},
|
||||
{
|
||||
description,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
index,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
severity,
|
||||
threat,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
version: calculatedVersion,
|
||||
}
|
||||
);
|
||||
|
||||
const update = await alertsClient.update({
|
||||
id: rule.id,
|
||||
data: {
|
||||
tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable),
|
||||
name: calculateName({ updatedName: name, originalName: rule.name }),
|
||||
schedule: {
|
||||
interval: calculateInterval(interval, rule.schedule.interval),
|
||||
},
|
||||
actions: rule.actions,
|
||||
params: nextParams,
|
||||
},
|
||||
});
|
||||
|
||||
if (rule.enabled && enabled === false) {
|
||||
await alertsClient.disable({ id: rule.id });
|
||||
} else if (!rule.enabled && enabled === true) {
|
||||
await alertsClient.enable({ id: rule.id });
|
||||
const ruleCurrentStatus = savedObjectsClient
|
||||
? await savedObjectsClient.find<IRuleSavedAttributesSavedObjectAttributes>({
|
||||
type: ruleStatusSavedObjectType,
|
||||
perPage: 1,
|
||||
sortField: 'statusDate',
|
||||
sortOrder: 'desc',
|
||||
search: rule.id,
|
||||
searchFields: ['alertId'],
|
||||
})
|
||||
: null;
|
||||
// set current status for this rule to be 'going to run'
|
||||
if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) {
|
||||
const currentStatusToDisable = ruleCurrentStatus.saved_objects[0];
|
||||
currentStatusToDisable.attributes.status = 'going to run';
|
||||
await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, {
|
||||
...currentStatusToDisable.attributes,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// enabled is null or undefined and we do not touch the rule
|
||||
}
|
||||
|
||||
if (enabled != null) {
|
||||
return { ...update, enabled };
|
||||
} else {
|
||||
return update;
|
||||
}
|
||||
};
|
|
@ -20,7 +20,12 @@ import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types';
|
|||
import { RequestFacade } from '../../../types';
|
||||
import { Alert } from '../../../../../alerting/server/types';
|
||||
|
||||
export type UpdateRuleAlertParamsRest = Partial<RuleAlertParamsRest> & {
|
||||
export type PatchRuleAlertParamsRest = Partial<RuleAlertParamsRest> & {
|
||||
id: string | undefined;
|
||||
rule_id: RuleAlertParams['ruleId'] | undefined;
|
||||
};
|
||||
|
||||
export type UpdateRuleAlertParamsRest = RuleAlertParamsRest & {
|
||||
id: string | undefined;
|
||||
rule_id: RuleAlertParams['ruleId'] | undefined;
|
||||
};
|
||||
|
@ -34,6 +39,14 @@ export interface FindParamsRest {
|
|||
filter: string;
|
||||
}
|
||||
|
||||
export interface PatchRulesRequest extends RequestFacade {
|
||||
payload: PatchRuleAlertParamsRest;
|
||||
}
|
||||
|
||||
export interface BulkPatchRulesRequest extends RequestFacade {
|
||||
payload: PatchRuleAlertParamsRest[];
|
||||
}
|
||||
|
||||
export interface UpdateRulesRequest extends RequestFacade {
|
||||
payload: UpdateRuleAlertParamsRest;
|
||||
}
|
||||
|
@ -153,7 +166,12 @@ export interface Clients {
|
|||
actionsClient: ActionsClient;
|
||||
}
|
||||
|
||||
export type UpdateRuleParams = Partial<RuleAlertParams> & {
|
||||
export type PatchRuleParams = Partial<RuleAlertParams> & {
|
||||
id: string | undefined | null;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
} & Clients;
|
||||
|
||||
export type UpdateRuleParams = RuleAlertParams & {
|
||||
id: string | undefined | null;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
} & Clients;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
import { ActionsClient } from '../../../../../../../plugins/actions/server';
|
||||
import { AlertsClient } from '../../../../../alerting';
|
||||
import { updateRules } from './update_rules';
|
||||
import { patchRules } from './patch_rules';
|
||||
import { PrepackagedRules } from '../types';
|
||||
|
||||
export const updatePrepackagedRules = async (
|
||||
|
@ -45,7 +45,7 @@ export const updatePrepackagedRules = async (
|
|||
|
||||
// Note: we do not pass down enabled as we do not want to suddenly disable
|
||||
// or enable rules on the user when they were not expecting it if a rule updates
|
||||
return updateRules({
|
||||
return patchRules({
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
description,
|
||||
|
|
|
@ -4,79 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { defaults, pickBy, isEmpty } from 'lodash/fp';
|
||||
import { PartialAlert } from '../../../../../alerting/server/types';
|
||||
import { readRules } from './read_rules';
|
||||
import { UpdateRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types';
|
||||
import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types';
|
||||
import { addTags } from './add_tags';
|
||||
import { ruleStatusSavedObjectType } from './saved_object_mappings';
|
||||
|
||||
export const calculateInterval = (
|
||||
interval: string | undefined,
|
||||
ruleInterval: string | undefined
|
||||
): string => {
|
||||
if (interval != null) {
|
||||
return interval;
|
||||
} else if (ruleInterval != null) {
|
||||
return ruleInterval;
|
||||
} else {
|
||||
return '5m';
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateVersion = (
|
||||
immutable: boolean,
|
||||
currentVersion: number,
|
||||
updateProperties: Partial<Omit<UpdateRuleParams, 'enabled' | 'ruleId'>>
|
||||
): number => {
|
||||
// early return if we are pre-packaged/immutable rule to be safe. We are never responsible
|
||||
// for changing the version number of an immutable. Immutables are only responsible for changing
|
||||
// their own version number. This would be really bad if an immutable version number is bumped by us
|
||||
// due to a bug, hence the extra check and early bail if that is detected.
|
||||
if (immutable === true) {
|
||||
if (updateProperties.version != null) {
|
||||
// we are an immutable rule but we are asking to update the version number so go ahead
|
||||
// and update it to what is asked.
|
||||
return updateProperties.version;
|
||||
} else {
|
||||
// we are immutable and not asking to update the version number so return the existing version
|
||||
return currentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// white list all properties but the enabled/disabled flag. We don't want to auto-increment
|
||||
// the version number if only the enabled/disabled flag is being set. Likewise if we get other
|
||||
// properties we are not expecting such as updatedAt we do not to cause a version number bump
|
||||
// on that either.
|
||||
const removedNullValues = pickBy<UpdateRuleParams>(
|
||||
(value: unknown) => value != null,
|
||||
updateProperties
|
||||
);
|
||||
if (isEmpty(removedNullValues)) {
|
||||
return currentVersion;
|
||||
} else {
|
||||
return currentVersion + 1;
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateName = ({
|
||||
updatedName,
|
||||
originalName,
|
||||
}: {
|
||||
updatedName: string | undefined;
|
||||
originalName: string | undefined;
|
||||
}): string => {
|
||||
if (updatedName != null) {
|
||||
return updatedName;
|
||||
} else if (originalName != null) {
|
||||
return originalName;
|
||||
} else {
|
||||
// You really should never get to this point. This is a fail safe way to send back
|
||||
// the name of "untitled" just in case a rule name became null or undefined at
|
||||
// some point since TypeScript allows it.
|
||||
return 'untitled';
|
||||
}
|
||||
};
|
||||
import { calculateVersion } from './utils';
|
||||
|
||||
export const updateRules = async ({
|
||||
alertsClient,
|
||||
|
@ -141,47 +74,40 @@ export const updateRules = async ({
|
|||
version,
|
||||
});
|
||||
|
||||
const nextParams = defaults(
|
||||
{
|
||||
...rule.params,
|
||||
},
|
||||
{
|
||||
description,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
index,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
severity,
|
||||
threat,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
version: calculatedVersion,
|
||||
}
|
||||
);
|
||||
|
||||
const update = await alertsClient.update({
|
||||
id: rule.id,
|
||||
data: {
|
||||
tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable),
|
||||
name: calculateName({ updatedName: name, originalName: rule.name }),
|
||||
schedule: {
|
||||
interval: calculateInterval(interval, rule.schedule.interval),
|
||||
},
|
||||
tags: addTags(tags, rule.params.ruleId, immutable),
|
||||
name,
|
||||
schedule: { interval },
|
||||
actions: rule.actions,
|
||||
params: nextParams,
|
||||
params: {
|
||||
description,
|
||||
ruleId: rule.params.ruleId,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
index,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
severity,
|
||||
threat,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
version: calculatedVersion,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (rule.enabled && enabled === false) {
|
||||
await alertsClient.disable({ id: rule.id });
|
||||
} else if (!rule.enabled && enabled === true) {
|
||||
|
@ -204,13 +130,7 @@ export const updateRules = async ({
|
|||
...currentStatusToDisable.attributes,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// enabled is null or undefined and we do not touch the rule
|
||||
}
|
||||
|
||||
if (enabled != null) {
|
||||
return { ...update, enabled };
|
||||
} else {
|
||||
return update;
|
||||
}
|
||||
return { ...update, enabled };
|
||||
};
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { calculateInterval, calculateName, calculateVersion } from './update_rules';
|
||||
import { UpdateRuleParams } from './types';
|
||||
import { calculateInterval, calculateVersion, calculateName } from './utils';
|
||||
import { PatchRuleParams } from './types';
|
||||
|
||||
describe('update_rules', () => {
|
||||
describe('utils', () => {
|
||||
describe('#calculateInterval', () => {
|
||||
test('given a undefined interval, it returns the ruleInterval ', () => {
|
||||
const interval = calculateInterval(undefined, '10m');
|
||||
|
@ -44,7 +44,7 @@ describe('update_rules', () => {
|
|||
|
||||
test('returning an updated version number if not given an immutable but an updated falsy value', () => {
|
||||
expect(
|
||||
calculateVersion(false, 1, ({ description: false } as unknown) as UpdateRuleParams)
|
||||
calculateVersion(false, 1, ({ description: false } as unknown) as PatchRuleParams)
|
||||
).toEqual(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { pickBy, isEmpty } from 'lodash/fp';
|
||||
import { PatchRuleParams } from './types';
|
||||
|
||||
export const calculateInterval = (
|
||||
interval: string | undefined,
|
||||
ruleInterval: string | undefined
|
||||
): string => {
|
||||
if (interval != null) {
|
||||
return interval;
|
||||
} else if (ruleInterval != null) {
|
||||
return ruleInterval;
|
||||
} else {
|
||||
return '5m';
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateVersion = (
|
||||
immutable: boolean,
|
||||
currentVersion: number,
|
||||
updateProperties: Partial<Omit<PatchRuleParams, 'enabled' | 'ruleId'>>
|
||||
): number => {
|
||||
// early return if we are pre-packaged/immutable rule to be safe. We are never responsible
|
||||
// for changing the version number of an immutable. Immutables are only responsible for changing
|
||||
// their own version number. This would be really bad if an immutable version number is bumped by us
|
||||
// due to a bug, hence the extra check and early bail if that is detected.
|
||||
if (immutable === true) {
|
||||
if (updateProperties.version != null) {
|
||||
// we are an immutable rule but we are asking to update the version number so go ahead
|
||||
// and update it to what is asked.
|
||||
return updateProperties.version;
|
||||
} else {
|
||||
// we are immutable and not asking to update the version number so return the existing version
|
||||
return currentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// white list all properties but the enabled/disabled flag. We don't want to auto-increment
|
||||
// the version number if only the enabled/disabled flag is being set. Likewise if we get other
|
||||
// properties we are not expecting such as updatedAt we do not to cause a version number bump
|
||||
// on that either.
|
||||
const removedNullValues = pickBy<PatchRuleParams>(
|
||||
(value: unknown) => value != null,
|
||||
updateProperties
|
||||
);
|
||||
if (isEmpty(removedNullValues)) {
|
||||
return currentVersion;
|
||||
} else {
|
||||
return currentVersion + 1;
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateName = ({
|
||||
updatedName,
|
||||
originalName,
|
||||
}: {
|
||||
updatedName: string | undefined;
|
||||
originalName: string | undefined;
|
||||
}): string => {
|
||||
if (updatedName != null) {
|
||||
return updatedName;
|
||||
} else if (originalName != null) {
|
||||
return originalName;
|
||||
} else {
|
||||
// You really should never get to this point. This is a fail safe way to send back
|
||||
// the name of "untitled" just in case a rule name became null or undefined at
|
||||
// some point since TypeScript allows it.
|
||||
return 'untitled';
|
||||
}
|
||||
};
|
31
x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh
Executable file
31
x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/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
|
||||
|
||||
# Uses a default if no argument is specified
|
||||
RULES=(${@:-./rules/patches/simplest_updated_name.json})
|
||||
|
||||
# Example: ./patch_rule.sh
|
||||
# Example: ./patch_rule.sh ./rules/patches/simplest_updated_name.json
|
||||
# Example glob: ./patch_rule.sh ./rules/patches/*
|
||||
for RULE in "${RULES[@]}"
|
||||
do {
|
||||
[ -e "$RULE" ] || continue
|
||||
curl -s -k \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X PATCH ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \
|
||||
-d @${RULE} \
|
||||
| jq .;
|
||||
} &
|
||||
done
|
||||
|
||||
wait
|
|
@ -0,0 +1,22 @@
|
|||
#!/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
|
||||
|
||||
# Uses a default if no argument is specified
|
||||
RULES=${1:-./rules/bulk/patch_names.json}
|
||||
|
||||
# Example: ./patch_rule_bulk.sh
|
||||
curl -s -k \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X PATCH ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_update \
|
||||
-d @${RULES} \
|
||||
| jq .;
|
|
@ -2,6 +2,7 @@
|
|||
{
|
||||
"name": "Simplest Query Number 1",
|
||||
"description": "Simplest query with the least amount of fields required",
|
||||
"rule_id": "query-rule-id-1",
|
||||
"risk_score": 1,
|
||||
"severity": "high",
|
||||
"type": "query",
|
||||
|
@ -12,6 +13,7 @@
|
|||
{
|
||||
"name": "Simplest Query Number 2",
|
||||
"description": "Simplest query with the least amount of fields required",
|
||||
"rule_id": "query-rule-id-2",
|
||||
"risk_score": 2,
|
||||
"severity": "low",
|
||||
"type": "query",
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
These are example PATCH rules to see how to patch various parts of the rules.
|
||||
You either have to use the id, or you have to use the rule_id in order to patch
|
||||
the rules. rule_id acts as an external_id where you can patch rules across different
|
||||
Kibana systems where id acts as a normal server generated id which is not normally shared
|
||||
across different Kibana systems.
|
||||
|
||||
The only thing you cannot patch is the `rule_id` or regular `id` of the system. If `rule_id`
|
||||
is incorrect then you have to delete the rule completely and re-initialize it with the
|
||||
correct `rule_id`
|
||||
|
||||
First add all the examples from queries like so:
|
||||
|
||||
```sh
|
||||
./post_rule.sh ./rules/queries/*.json
|
||||
```
|
||||
|
||||
Then to selectively patch a rule add the file of your choosing to patch:
|
||||
|
||||
```sh
|
||||
./patch_rule.sh ./rules/patches/<filename>.json
|
||||
```
|
||||
|
||||
Take note that the ones with "id" must be changed to a GUID that only you know about through
|
||||
a `./find_rules.sh`. For example to grab a GUID id off of the first found record that exists
|
||||
you can do: `./find_rules.sh | jq '.data[0].id'` and then replace the id in `patches/simplest_update_risk_score_by_id.json` with that particular id to watch it happen.
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"enabled": false
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"enabled": true
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"id": "ade31ba8-dc49-4c18-b7f4-370b35df5f57",
|
||||
"risk_score": 38
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"risk_score": 98
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"name": "Changes only the name to this new value"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"interval": "6m"
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"name": "Updates a query with all possible fields that can be updated",
|
||||
"description": "Kitchen Sink (everything) query that has all possible fields filled out.",
|
||||
"false_positives": [
|
||||
"https://www.example.com/some-article-about-a-false-positive",
|
||||
"some text string about why another condition could be a false positive"
|
||||
],
|
||||
"rule_id": "rule-id-everything",
|
||||
"filters": [
|
||||
{
|
||||
"query": {
|
||||
"match_phrase": {
|
||||
"host.name": "siem-windows"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"exists": {
|
||||
"field": "host.hostname"
|
||||
}
|
||||
}
|
||||
],
|
||||
"enabled": false,
|
||||
"index": ["auditbeat-*", "filebeat-*"],
|
||||
"interval": "5m",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
"output_index": ".siem-signals-default",
|
||||
"meta": {
|
||||
"anything_you_want_ui_related_or_otherwise": {
|
||||
"as_deep_structured_as_you_need": {
|
||||
"any_data_type": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"language": "kuery",
|
||||
"risk_score": 1,
|
||||
"max_signals": 100,
|
||||
"tags": ["tag 1", "tag 2", "any tag you want"],
|
||||
"to": "now",
|
||||
"from": "now-6m",
|
||||
"severity": "high",
|
||||
"type": "query",
|
||||
"threat": [
|
||||
{
|
||||
"framework": "MITRE ATT&CK",
|
||||
"tactic": {
|
||||
"id": "TA0040",
|
||||
"name": "impact",
|
||||
"reference": "https://attack.mitre.org/tactics/TA0040/"
|
||||
},
|
||||
"technique": [
|
||||
{
|
||||
"id": "T1499",
|
||||
"name": "endpoint denial of service",
|
||||
"reference": "https://attack.mitre.org/techniques/T1499/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"framework": "Some other Framework you want",
|
||||
"tactic": {
|
||||
"id": "some-other-id",
|
||||
"name": "Some other name",
|
||||
"reference": "https://example.com"
|
||||
},
|
||||
"technique": [
|
||||
{
|
||||
"id": "some-other-id",
|
||||
"name": "some other technique name",
|
||||
"reference": "https://example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
"http://www.example.com/some-article-about-attack",
|
||||
"Some plain text string here explaining why this is a valid thing to look out for"
|
||||
],
|
||||
"timeline_id": "other-timeline-id",
|
||||
"timeline_title": "other-timeline-title",
|
||||
"version": 42
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rule_id": "tags-query",
|
||||
"tags": ["tag_3"]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"timeline_id": "other-timeline-id",
|
||||
"timeline_title": "other-timeline-title"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"version": 500
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
These are example PUT rules to see how to update various parts of the rules.
|
||||
These are example update rules to see how to update various parts of the rules.
|
||||
You either have to use the id, or you have to use the rule_id in order to update
|
||||
the rules. rule_id acts as an external_id where you can update rules across different
|
||||
Kibana systems where id acts as a normal server generated id which is not normally shared
|
||||
|
@ -14,7 +14,7 @@ First add all the examples from queries like so:
|
|||
./post_rule.sh ./rules/queries/*.json
|
||||
```
|
||||
|
||||
Then to selectively update a rule add the file of your choosing to update:
|
||||
Then to selectively update a rule add the file of your choosing to patch:
|
||||
|
||||
```sh
|
||||
./update_rule.sh ./rules/updates/<filename>.json
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"name": "Some new name",
|
||||
"description": "Changing the name and disabling this query",
|
||||
"rule_id": "query-rule-id",
|
||||
"risk_score": 1,
|
||||
"severity": "high",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
"enabled": false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"name": "Some new name",
|
||||
"description": "Changing the name and enabling this query",
|
||||
"rule_id": "query-rule-id",
|
||||
"risk_score": 1,
|
||||
"severity": "high",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
"enabled": true
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"id": "ade31ba8-dc49-4c18-b7f4-370b35df5f57",
|
||||
"risk_score": 38
|
||||
"id": "1100ba1b-ed7e-4755-b326-1f6fa2bd6758",
|
||||
"name": "Some new name",
|
||||
"description": "Changing the name and changing the risk score",
|
||||
"risk_score": 38,
|
||||
"severity": "high",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"risk_score": 98
|
||||
"name": "Some new name",
|
||||
"description": "Changing the name and changing the risk score",
|
||||
"risk_score": 98,
|
||||
"severity": "high",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"name": "Changes only the name to this new value",
|
||||
"description": "Query with a rule_id that acts like an external id",
|
||||
"rule_id": "query-rule-id",
|
||||
"name": "Changes only the name to this new value"
|
||||
"risk_score": 1,
|
||||
"severity": "high",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"interval": "6m"
|
||||
"interval": "6m",
|
||||
"name": "Some new name",
|
||||
"description": "Changing the interval and risk score",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
"severity": "low",
|
||||
"risk_score": 0
|
||||
}
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"rule_id": "tags-query",
|
||||
"tags": ["tag_3"]
|
||||
"rule_id": "query-rule-id",
|
||||
"tags": ["tag_1", "tag_2", "tag_3"],
|
||||
"name": "Some new name",
|
||||
"description": "Adding tags and a few other updates such as name",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
"severity": "low",
|
||||
"risk_score": 10
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"timeline_id": "other-timeline-id",
|
||||
"timeline_title": "other-timeline-title"
|
||||
"timeline_title": "other-timeline-title",
|
||||
"name": "Some new name",
|
||||
"description": "Adding tags and a few other updates such as name",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
"severity": "low",
|
||||
"risk_score": 10
|
||||
}
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"version": 500
|
||||
"version": 500,
|
||||
"name": "Changes the version to arbitrary number",
|
||||
"description": "Changes the version to some arbitrary number",
|
||||
"type": "query",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
"severity": "low",
|
||||
"risk_score": 10
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ RULES=(${@:-./rules/updates/simplest_updated_name.json})
|
|||
|
||||
# Example: ./update_rule.sh
|
||||
# Example: ./update_rule.sh ./rules/updates/simplest_updated_name.json
|
||||
# Example glob: ./post_rule.sh ./rules/updates/*
|
||||
# Example glob: ./update_rule.sh ./rules/updates/*
|
||||
for RULE in "${RULES[@]}"
|
||||
do {
|
||||
[ -e "$RULE" ] || continue
|
||||
|
|
|
@ -10,7 +10,7 @@ set -e
|
|||
./check_env_variables.sh
|
||||
|
||||
# Uses a default if no argument is specified
|
||||
RULES=${1:-./rules/bulk/update_names.json}
|
||||
RULES=${1:-./rules/bulk/multiple_simplest_queries.json}
|
||||
|
||||
# Example: ./update_rule_bulk.sh
|
||||
curl -s -k \
|
||||
|
|
|
@ -15,6 +15,7 @@ const onlyNotInCoverageTests = [
|
|||
require.resolve('../test/api_integration/config.js'),
|
||||
require.resolve('../test/alerting_api_integration/spaces_only/config.ts'),
|
||||
require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'),
|
||||
require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'),
|
||||
require.resolve('../test/plugin_api_integration/config.js'),
|
||||
require.resolve('../test/plugin_functional/config.ts'),
|
||||
require.resolve('../test/kerberos_api_integration/config.ts'),
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 path from 'path';
|
||||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
|
||||
import { services } from './services';
|
||||
|
||||
interface CreateTestConfigOptions {
|
||||
license: string;
|
||||
disabledPlugins?: string[];
|
||||
ssl?: boolean;
|
||||
}
|
||||
|
||||
// test.not-enabled is specifically not enabled
|
||||
const enabledActionTypes = [
|
||||
'.email',
|
||||
'.index',
|
||||
'.pagerduty',
|
||||
'.server-log',
|
||||
'.servicenow',
|
||||
'.slack',
|
||||
'.webhook',
|
||||
'test.authorization',
|
||||
'test.failing',
|
||||
'test.index-record',
|
||||
'test.noop',
|
||||
'test.rate-limit',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export function createTestConfig(name: string, options: CreateTestConfigOptions) {
|
||||
const { license = 'trial', disabledPlugins = [], ssl = false } = options;
|
||||
|
||||
return async ({ readConfigFile }: FtrConfigProviderContext) => {
|
||||
const xPackApiIntegrationTestsConfig = await readConfigFile(
|
||||
require.resolve('../../api_integration/config.js')
|
||||
);
|
||||
const servers = {
|
||||
...xPackApiIntegrationTestsConfig.get('servers'),
|
||||
elasticsearch: {
|
||||
...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'),
|
||||
protocol: ssl ? 'https' : 'http',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve(`../${name}/tests/`)],
|
||||
servers,
|
||||
services,
|
||||
junit: {
|
||||
reportName: 'X-Pack Detection Engine API Integration Tests',
|
||||
},
|
||||
esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'),
|
||||
esTestCluster: {
|
||||
...xPackApiIntegrationTestsConfig.get('esTestCluster'),
|
||||
license,
|
||||
ssl,
|
||||
serverArgs: [
|
||||
`xpack.license.self_generated.type=${license}`,
|
||||
`xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`,
|
||||
],
|
||||
},
|
||||
kbnTestServer: {
|
||||
...xPackApiIntegrationTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
`--xpack.actions.whitelistedHosts=${JSON.stringify([
|
||||
'localhost',
|
||||
'some.non.existent.com',
|
||||
])}`,
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
|
||||
'--xpack.alerting.enabled=true',
|
||||
'--xpack.event_log.logEntries=true',
|
||||
...disabledPlugins.map(key => `--xpack.${key}.enabled=false`),
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`,
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'task_manager')}`,
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'aad')}`,
|
||||
...(ssl
|
||||
? [
|
||||
`--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
11
x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts
vendored
Normal file
11
x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr';
|
||||
|
||||
import { services } from './services';
|
||||
|
||||
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { services } from '../../api_integration/services';
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { createTestConfig } from '../common/config';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default createTestConfig('security_and_spaces', {
|
||||
disabledPlugins: [],
|
||||
license: 'trial',
|
||||
ssl: true,
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('add_prepackaged_rules', () => {
|
||||
describe('validation errors', () => {
|
||||
it('should give an error that the index must exist first if it does not exist before adding prepackaged rules', async () => {
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_PREPACKAGED_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(400);
|
||||
|
||||
expect(body).to.eql({
|
||||
message:
|
||||
'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default',
|
||||
status_code: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating prepackaged rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should contain two output keys of rules_installed and rules_updated', async () => {
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_PREPACKAGED_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(Object.keys(body)).to.eql(['rules_installed', 'rules_updated']);
|
||||
});
|
||||
|
||||
it('should create the prepackaged rules and return a count greater than zero', async () => {
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_PREPACKAGED_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(body.rules_installed).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('should create the prepackaged rules that the rules_updated is of size zero', async () => {
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_PREPACKAGED_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(body.rules_updated).to.eql(0);
|
||||
});
|
||||
|
||||
it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => {
|
||||
await supertest
|
||||
.put(DETECTION_ENGINE_PREPACKAGED_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_PREPACKAGED_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(body.rules_installed).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
getSimpleRuleWithoutRuleId,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('create_rules', () => {
|
||||
describe('validation errors', () => {
|
||||
it('should give an error that the index must exist first if it does not exist before creating a rule', async () => {
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(400);
|
||||
|
||||
expect(body).to.eql({
|
||||
message:
|
||||
'To create a rule, the index must exist first. Index .siem-signals-default does not exist',
|
||||
status_code: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should create a single rule with a rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
|
||||
});
|
||||
|
||||
it('should create a single rule without a rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRuleWithoutRuleId())
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => {
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(409);
|
||||
|
||||
expect(body).to.eql({
|
||||
message: 'rule_id: "rule-1" already exists',
|
||||
status_code: 409,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
getSimpleRuleWithoutRuleId,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('create_rules_bulk', () => {
|
||||
describe('validation errors', () => {
|
||||
it('should give a 200 even if the index does not exist as all bulks return a 200 but have an error of 409 bad request in the body', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([getSimpleRule()])
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
error: {
|
||||
message:
|
||||
'To create a rule, the index must exist first. Index .siem-signals-default does not exist',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating rules in bulk', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should create a single rule with a rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([getSimpleRule()])
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
|
||||
});
|
||||
|
||||
it('should create a single rule without a rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([getSimpleRuleWithoutRuleId()])
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
// TODO: This is a valid issue and will be fixed in an upcoming PR and then enabled once that PR is merged
|
||||
it.skip('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([getSimpleRule(), getSimpleRule()])
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
error: 'Conflict',
|
||||
message: 'rule_id: "rule-1" already exists',
|
||||
statusCode: 409,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id that already exists', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([getSimpleRule()])
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send([getSimpleRule()])
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "rule-1" already exists',
|
||||
status_code: 409,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
getSimpleRuleWithoutRuleId,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('delete_rules', () => {
|
||||
describe('deleting rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should delete a single rule with a rule_id', async () => {
|
||||
// create a rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// delete the rule by its rule_id
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
|
||||
});
|
||||
|
||||
it('should delete a single rule using an auto generated rule_id', async () => {
|
||||
// add a rule where the rule_id is auto-generated
|
||||
const { body: bodyWithCreatedRule } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRuleWithoutRuleId())
|
||||
.expect(200);
|
||||
|
||||
// delete that rule by its auto-generated rule_id
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=${bodyWithCreatedRule.rule_id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
it('should delete a single rule using an auto generated id', async () => {
|
||||
// add a rule
|
||||
const { body: bodyWithCreatedRule } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
// delete that rule by its auto-generated id
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}?id=${bodyWithCreatedRule.id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
it('should return an error if the id does not exist when trying to delete it', async () => {
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
message: 'id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if the rule_id does not exist when trying to delete it', async () => {
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=fake_id`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
message: 'rule_id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
getSimpleRuleWithoutRuleId,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('delete_rules_bulk', () => {
|
||||
describe('deleting rules bulk using DELETE', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should delete a single rule with a rule_id', async () => {
|
||||
// add a rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
// delete the rule in bulk
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ rule_id: 'rule-1' }])
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
|
||||
});
|
||||
|
||||
it('should delete a single rule using an auto generated rule_id', async () => {
|
||||
// add a rule without a rule_id
|
||||
const { body: bodyWithCreatedRule } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRuleWithoutRuleId())
|
||||
.expect(200);
|
||||
|
||||
// delete that rule by its rule_id
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ rule_id: bodyWithCreatedRule.rule_id }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
it('should delete a single rule using an auto generated id', async () => {
|
||||
// add a rule
|
||||
const { body: bodyWithCreatedRule } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
// delete that rule by its id
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ id: bodyWithCreatedRule.id }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ rule_id: 'fake_id' }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'fake_id',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an error if the id does not exist when trying to delete an id', async () => {
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ id: 'fake_id' }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
error: {
|
||||
message: 'id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'fake_id', // TODO This is a known issue where it should be id and not rule_id
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => {
|
||||
// add the rule
|
||||
const { body: bodyWithCreatedRule } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRuleWithoutRuleId())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
expect([bodyToCompare, body[1]]).to.eql([
|
||||
getSimpleRuleOutputWithoutRuleId(),
|
||||
{ rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// This is a repeat of the tests above but just using POST instead of DELETE
|
||||
describe('deleting rules bulk using POST', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should delete a single rule with a rule_id', async () => {
|
||||
// add a rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
// delete the rule in bulk
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ rule_id: 'rule-1' }])
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
|
||||
});
|
||||
|
||||
it('should delete a single rule using an auto generated rule_id', async () => {
|
||||
// add a rule without a rule_id
|
||||
const { body: bodyWithCreatedRule } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRuleWithoutRuleId())
|
||||
.expect(200);
|
||||
|
||||
// delete that rule by its rule_id
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ rule_id: bodyWithCreatedRule.rule_id }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
it('should delete a single rule using an auto generated id', async () => {
|
||||
// add a rule
|
||||
const { body: bodyWithCreatedRule } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
// delete that rule by its id
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ id: bodyWithCreatedRule.id }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ rule_id: 'fake_id' }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'fake_id',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an error if the id does not exist when trying to delete an id', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ id: 'fake_id' }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
error: {
|
||||
message: 'id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'fake_id', // TODO This is a known issue where it should be id and not rule_id
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => {
|
||||
// add the rule
|
||||
const { body: bodyWithCreatedRule } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRuleWithoutRuleId())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
|
||||
.send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
expect([bodyToCompare, body[1]]).to.eql([
|
||||
getSimpleRuleOutputWithoutRuleId(),
|
||||
{ rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
binaryToString,
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
removeServerGeneratedProperties,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('export_rules', () => {
|
||||
describe('exporting rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should set the response content types to be expected', async () => {
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.query()
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/ndjson')
|
||||
.expect('Content-Disposition', 'attachment; filename="export.ndjson"');
|
||||
});
|
||||
|
||||
it('should export a single rule with a rule_id', async () => {
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.query()
|
||||
.expect(200)
|
||||
.parse(binaryToString);
|
||||
|
||||
const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[0]);
|
||||
const bodyToTest = removeServerGeneratedProperties(bodySplitAndParsed);
|
||||
|
||||
expect(bodyToTest).to.eql(getSimpleRuleOutput());
|
||||
});
|
||||
|
||||
it('should export a exported count with a single rule_id', async () => {
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.query()
|
||||
.expect(200)
|
||||
.parse(binaryToString);
|
||||
|
||||
const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]);
|
||||
|
||||
expect(bodySplitAndParsed).to.eql({
|
||||
exported_count: 1,
|
||||
missing_rules: [],
|
||||
missing_rules_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should export exactly two rules given two rules', async () => {
|
||||
// post rule 1
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// post rule 2
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-2'))
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.query()
|
||||
.expect(200)
|
||||
.parse(binaryToString);
|
||||
|
||||
const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]);
|
||||
const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]);
|
||||
const firstRule = removeServerGeneratedProperties(firstRuleParsed);
|
||||
const secondRule = removeServerGeneratedProperties(secondRuleParsed);
|
||||
|
||||
expect([firstRule, secondRule]).to.eql([
|
||||
getSimpleRuleOutput('rule-2'),
|
||||
getSimpleRuleOutput('rule-1'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getComplexRule,
|
||||
getComplexRuleOutput,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
removeServerGeneratedProperties,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('find_rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should return an empty find body correctly if no rules are loaded', async () => {
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}/_find`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
data: [],
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a single rule when a single rule is loaded from a find with defaults added', async () => {
|
||||
// add a single rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
// query the single rule from _find
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}/_find`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send();
|
||||
|
||||
body.data = [removeServerGeneratedProperties(body.data[0])];
|
||||
expect(body).to.eql({
|
||||
data: [getSimpleRuleOutput()],
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a single rule when a single rule is loaded from a find with everything for the rule added', async () => {
|
||||
// add a single rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getComplexRule())
|
||||
.expect(200);
|
||||
|
||||
// query and expect that we get back one record in the find
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}/_find`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send();
|
||||
|
||||
body.data = [removeServerGeneratedProperties(body.data[0])];
|
||||
expect(body).to.eql({
|
||||
data: [getComplexRuleOutput()],
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import {
|
||||
DETECTION_ENGINE_PREPACKAGED_URL,
|
||||
DETECTION_ENGINE_RULES_URL,
|
||||
} from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, getSimpleRule } from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('get_prepackaged_rules_status', () => {
|
||||
describe('getting prepackaged rules status', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should return expected JSON keys of the pre-packaged rules status', async () => {
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(Object.keys(body)).to.eql([
|
||||
'rules_custom_installed',
|
||||
'rules_installed',
|
||||
'rules_not_installed',
|
||||
'rules_not_updated',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return that rules_not_installed are greater than zero', async () => {
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
expect(body.rules_not_installed).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('should return that rules_custom_installed, rules_installed, and rules_not_updated are zero', async () => {
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
expect(body.rules_custom_installed).to.eql(0);
|
||||
expect(body.rules_installed).to.eql(0);
|
||||
expect(body.rules_not_updated).to.eql(0);
|
||||
});
|
||||
|
||||
it('should show that one custom rule is installed when a custom rule is added', async () => {
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
expect(body.rules_custom_installed).to.eql(1);
|
||||
expect(body.rules_installed).to.eql(0);
|
||||
expect(body.rules_not_updated).to.eql(0);
|
||||
});
|
||||
|
||||
it('should show rules are installed when adding pre-packaged rules', async () => {
|
||||
await supertest
|
||||
.put(DETECTION_ENGINE_PREPACKAGED_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
expect(body.rules_installed).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,331 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleAsNdjson,
|
||||
getSimpleRuleOutput,
|
||||
removeServerGeneratedProperties,
|
||||
ruleToNdjson,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('import_rules', () => {
|
||||
describe('importing rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should set the response content types to be expected', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should reject with an error if the file type is not that of a ndjson', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.txt')
|
||||
.query()
|
||||
.expect(400);
|
||||
|
||||
expect(body).to.eql({
|
||||
status_code: 400,
|
||||
message: 'Invalid file extension .txt',
|
||||
});
|
||||
});
|
||||
|
||||
it('should report that it imported a simple rule successfully', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to read an imported rule back out correctly', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1'));
|
||||
});
|
||||
|
||||
it('should be able to import two rules', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should report a conflict if there is an attempt to import two rules with the same rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [], // TODO: This should have a conflict within it as an error rather than an empty array
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT report a conflict if there is an attempt to import two rules with the same rule_id and overwrite is set to true', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "rule-1" already exists',
|
||||
status_code: 409,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT report a conflict if there is an attempt to import a rule with a rule_id that already exists and overwrite is set to true', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should overwrite an existing rule if overwrite is set to true', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const simpleRule = getSimpleRule('rule-1');
|
||||
simpleRule.name = 'some other name';
|
||||
const ndjson = ruleToNdjson(simpleRule);
|
||||
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', ndjson, 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
const ruleOutput = getSimpleRuleOutput('rule-1');
|
||||
ruleOutput.name = 'some other name';
|
||||
ruleOutput.version = 2;
|
||||
expect(bodyToCompare).to.eql(ruleOutput);
|
||||
});
|
||||
|
||||
it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "rule-1" already exists',
|
||||
status_code: 409,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should report a mix of conflicts and a mix of successes', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "rule-1" already exists',
|
||||
status_code: 409,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "rule-2" already exists',
|
||||
status_code: 409,
|
||||
},
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to correctly read back a mixed import of different rules even if some cause conflicts', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson')
|
||||
.query()
|
||||
.expect(200);
|
||||
|
||||
const { body: bodyOfRule1 } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const { body: bodyOfRule2 } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-2`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const { body: bodyOfRule3 } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-3`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompareOfRule1 = removeServerGeneratedProperties(bodyOfRule1);
|
||||
const bodyToCompareOfRule2 = removeServerGeneratedProperties(bodyOfRule2);
|
||||
const bodyToCompareOfRule3 = removeServerGeneratedProperties(bodyOfRule3);
|
||||
|
||||
expect([bodyToCompareOfRule1, bodyToCompareOfRule2, bodyToCompareOfRule3]).to.eql([
|
||||
getSimpleRuleOutput('rule-1'),
|
||||
getSimpleRuleOutput('rule-2'),
|
||||
getSimpleRuleOutput('rule-3'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ loadTestFile }: FtrProviderContext): void => {
|
||||
describe('detection engine api security and spaces enabled', function() {
|
||||
this.tags('ciGroup1');
|
||||
|
||||
loadTestFile(require.resolve('./add_prepackaged_rules'));
|
||||
loadTestFile(require.resolve('./create_rules'));
|
||||
loadTestFile(require.resolve('./create_rules_bulk'));
|
||||
loadTestFile(require.resolve('./delete_rules'));
|
||||
loadTestFile(require.resolve('./delete_rules_bulk'));
|
||||
loadTestFile(require.resolve('./export_rules'));
|
||||
loadTestFile(require.resolve('./find_rules'));
|
||||
loadTestFile(require.resolve('./get_prepackaged_rules_status'));
|
||||
loadTestFile(require.resolve('./import_rules'));
|
||||
loadTestFile(require.resolve('./read_rules'));
|
||||
loadTestFile(require.resolve('./update_rules'));
|
||||
loadTestFile(require.resolve('./update_rules_bulk'));
|
||||
loadTestFile(require.resolve('./patch_rules_bulk'));
|
||||
loadTestFile(require.resolve('./patch_rules'));
|
||||
});
|
||||
};
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('patch_rules', () => {
|
||||
describe('patch rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should patch a single rule property of name using a rule_id', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: 'rule-1', name: 'some other name' })
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should patch a single rule property of name using the auto-generated rule_id', async () => {
|
||||
// create a simple rule
|
||||
const rule = getSimpleRule('rule-1');
|
||||
delete rule.rule_id;
|
||||
|
||||
const { body: createRuleBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(rule)
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: createRuleBody.rule_id, name: 'some other name' })
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutputWithoutRuleId();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should patch a single rule property of name using the auto-generated id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createdBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ id: createdBody.id, name: 'some other name' })
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should not change the version of a rule when it patches only enabled', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's enabled to false
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: 'rule-1', enabled: false })
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.enabled = false;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should change the version of a rule when it patches enabled and another property', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's enabled to false and another property
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: 'rule-1', severity: 'low', enabled: false })
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.enabled = false;
|
||||
outputRule.severity = 'low';
|
||||
outputRule.version = 2;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should not change other properties when it does patches', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's timeline_title
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' })
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: 'rule-1', name: 'some other name' })
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.timeline_title = 'some title';
|
||||
outputRule.timeline_id = 'some id';
|
||||
outputRule.version = 3;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should give a 404 if it is given a fake id', async () => {
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ id: 'fake_id', name: 'some other name' })
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
status_code: 404,
|
||||
message: 'id: "fake_id" not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should give a 404 if it is given a fake rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: 'fake_id', name: 'some other name' })
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
status_code: 404,
|
||||
message: 'rule_id: "fake_id" not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,356 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
removeServerGeneratedProperties,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('patch_rules_bulk', () => {
|
||||
describe('patch rules bulk', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should patch a single rule property of name using a rule_id', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ rule_id: 'rule-1', name: 'some other name' }])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should patch two rule properties of name using the two rules rule_id', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// create a second simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-2'))
|
||||
.expect(200);
|
||||
|
||||
// patch both rule names
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([
|
||||
{ rule_id: 'rule-1', name: 'some other name' },
|
||||
{ rule_id: 'rule-2', name: 'some other name' },
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const outputRule1 = getSimpleRuleOutput();
|
||||
outputRule1.name = 'some other name';
|
||||
outputRule1.version = 2;
|
||||
|
||||
const outputRule2 = getSimpleRuleOutput('rule-2');
|
||||
outputRule2.name = 'some other name';
|
||||
outputRule2.version = 2;
|
||||
|
||||
const bodyToCompare1 = removeServerGeneratedProperties(body[0]);
|
||||
const bodyToCompare2 = removeServerGeneratedProperties(body[1]);
|
||||
expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]);
|
||||
});
|
||||
|
||||
it('should patch a single rule property of name using an id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createRuleBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ id: createRuleBody.id, name: 'some other name' }])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should patch two rule properties of name using the two rules id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createRule1 } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// create a second simple rule
|
||||
const { body: createRule2 } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-2'))
|
||||
.expect(200);
|
||||
|
||||
// patch both rule names
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([
|
||||
{ id: createRule1.id, name: 'some other name' },
|
||||
{ id: createRule2.id, name: 'some other name' },
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1');
|
||||
outputRule1.name = 'some other name';
|
||||
outputRule1.version = 2;
|
||||
|
||||
const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2');
|
||||
outputRule2.name = 'some other name';
|
||||
outputRule2.version = 2;
|
||||
|
||||
const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]);
|
||||
expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]);
|
||||
});
|
||||
|
||||
it('should patch a single rule property of name using the auto-generated id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createdBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ id: createdBody.id, name: 'some other name' }])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should not change the version of a rule when it patches only enabled', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's enabled to false
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ rule_id: 'rule-1', enabled: false }])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.enabled = false;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should change the version of a rule when it patches enabled and another property', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's enabled to false and another property
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ rule_id: 'rule-1', severity: 'low', enabled: false }])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.enabled = false;
|
||||
outputRule.severity = 'low';
|
||||
outputRule.version = 2;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should not change other properties when it does patches', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's timeline_title
|
||||
await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }])
|
||||
.expect(200);
|
||||
|
||||
// patch a simple rule's name
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ rule_id: 'rule-1', name: 'some other name' }])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.timeline_title = 'some title';
|
||||
outputRule.timeline_id = 'some id';
|
||||
outputRule.version = 3;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should return a 200 but give a 404 in the message if it is given a fake id', async () => {
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ id: 'fake_id', name: 'some other name' }])
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{ rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([{ rule_id: 'fake_id', name: 'some other name' }])
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
rule_id: 'fake_id',
|
||||
error: { status_code: 404, message: 'rule_id: "fake_id" not found' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should patch one rule property and give an error about a second fake rule_id', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch one rule name and give a fake id for the second
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([
|
||||
{ rule_id: 'rule-1', name: 'some other name' },
|
||||
{ rule_id: 'fake_id', name: 'some other name' },
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect([bodyToCompare, body[1]]).to.eql([
|
||||
outputRule,
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'fake_id',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should patch one rule property and give an error about a second fake id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createdBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// patch one rule name and give a fake id for the second
|
||||
const { body } = await supertest
|
||||
.patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([
|
||||
{ id: createdBody.id, name: 'some other name' },
|
||||
{ id: 'fake_id', name: 'some other name' },
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect([bodyToCompare, body[1]]).to.eql([
|
||||
outputRule,
|
||||
{
|
||||
error: {
|
||||
message: 'id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'fake_id', // TODO: This should be id and not rule_id in the codebase
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
getSimpleRuleWithoutRuleId,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('read_rules', () => {
|
||||
describe('reading rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should be able to read a single rule using rule_id', async () => {
|
||||
// create a simple rule to read
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
|
||||
});
|
||||
|
||||
it('should be able to read a single rule using id', async () => {
|
||||
// create a simple rule to read
|
||||
const { body: createRuleBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
|
||||
});
|
||||
|
||||
it('should be able to read a single rule with an auto-generated rule_id', async () => {
|
||||
// create a simple rule to read
|
||||
const { body: createRuleBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRuleWithoutRuleId())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${createRuleBody.rule_id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
|
||||
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
|
||||
});
|
||||
|
||||
it('should return 404 if given a fake id', async () => {
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
status_code: 404,
|
||||
message: 'id: "fake_id" not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 if given a fake rule_id', async () => {
|
||||
const { body } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=fake_id`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule())
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
status_code: 404,
|
||||
message: 'rule_id: "fake_id" not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
removeServerGeneratedProperties,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('update_rules', () => {
|
||||
describe('update rules', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should update a single rule property of name using a rule_id', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's name
|
||||
const updatedRule = getSimpleRule('rule-1');
|
||||
updatedRule.rule_id = 'rule-1';
|
||||
updatedRule.name = 'some other name';
|
||||
delete updatedRule.id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(updatedRule)
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should update a single rule property of name using an auto-generated rule_id', async () => {
|
||||
const rule = getSimpleRule('rule-1');
|
||||
delete rule.rule_id;
|
||||
// create a simple rule
|
||||
const { body: createRuleBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(rule)
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's name
|
||||
const updatedRule = getSimpleRule('rule-1');
|
||||
updatedRule.rule_id = createRuleBody.rule_id;
|
||||
updatedRule.name = 'some other name';
|
||||
delete updatedRule.id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(updatedRule)
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutputWithoutRuleId();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should update a single rule property of name using the auto-generated id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createdBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's name
|
||||
const updatedRule = getSimpleRule('rule-1');
|
||||
updatedRule.name = 'some other name';
|
||||
updatedRule.id = createdBody.id;
|
||||
delete updatedRule.rule_id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(updatedRule)
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should change the version of a rule when it updates enabled and another property', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's enabled to false and another property
|
||||
const updatedRule = getSimpleRule('rule-1');
|
||||
updatedRule.severity = 'low';
|
||||
updatedRule.enabled = false;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(updatedRule)
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.enabled = false;
|
||||
outputRule.severity = 'low';
|
||||
outputRule.version = 2;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
const ruleUpdate = getSimpleRule('rule-1');
|
||||
ruleUpdate.timeline_title = 'some title';
|
||||
ruleUpdate.timeline_id = 'some id';
|
||||
|
||||
// update a simple rule's timeline_title
|
||||
await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(ruleUpdate)
|
||||
.expect(200);
|
||||
|
||||
const ruleUpdate2 = getSimpleRule('rule-1');
|
||||
ruleUpdate2.name = 'some other name';
|
||||
|
||||
// update a simple rule's name
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(ruleUpdate2)
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 3;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should give a 404 if it is given a fake id', async () => {
|
||||
const simpleRule = getSimpleRule();
|
||||
simpleRule.id = 'fake_id';
|
||||
delete simpleRule.rule_id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(simpleRule)
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
status_code: 404,
|
||||
message: 'id: "fake_id" not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should give a 404 if it is given a fake rule_id', async () => {
|
||||
const simpleRule = getSimpleRule();
|
||||
simpleRule.rule_id = 'fake_id';
|
||||
delete simpleRule.id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(simpleRule)
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
status_code: 404,
|
||||
message: 'rule_id: "fake_id" not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,386 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
removeServerGeneratedProperties,
|
||||
getSimpleRuleOutputWithoutRuleId,
|
||||
removeServerGeneratedPropertiesIncludingRuleId,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
describe('update_rules_bulk', () => {
|
||||
describe('update rules bulk', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(es);
|
||||
});
|
||||
|
||||
it('should update a single rule property of name using a rule_id', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
const updatedRule = getSimpleRule('rule-1');
|
||||
updatedRule.name = 'some other name';
|
||||
|
||||
// update a simple rule's name
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([updatedRule])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should update two rule properties of name using the two rules rule_id', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// create a second simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-2'))
|
||||
.expect(200);
|
||||
|
||||
const updatedRule1 = getSimpleRule('rule-1');
|
||||
updatedRule1.name = 'some other name';
|
||||
|
||||
const updatedRule2 = getSimpleRule('rule-2');
|
||||
updatedRule2.name = 'some other name';
|
||||
|
||||
// update both rule names
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([updatedRule1, updatedRule2])
|
||||
.expect(200);
|
||||
|
||||
const outputRule1 = getSimpleRuleOutput();
|
||||
outputRule1.name = 'some other name';
|
||||
outputRule1.version = 2;
|
||||
|
||||
const outputRule2 = getSimpleRuleOutput('rule-2');
|
||||
outputRule2.name = 'some other name';
|
||||
outputRule2.version = 2;
|
||||
|
||||
const bodyToCompare1 = removeServerGeneratedProperties(body[0]);
|
||||
const bodyToCompare2 = removeServerGeneratedProperties(body[1]);
|
||||
expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]);
|
||||
});
|
||||
|
||||
it('should update a single rule property of name using an id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createRuleBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's name
|
||||
const updatedRule1 = getSimpleRule('rule-1');
|
||||
updatedRule1.id = createRuleBody.id;
|
||||
updatedRule1.name = 'some other name';
|
||||
delete updatedRule1.rule_id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([updatedRule1])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should update two rule properties of name using the two rules id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createRule1 } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// create a second simple rule
|
||||
const { body: createRule2 } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-2'))
|
||||
.expect(200);
|
||||
|
||||
// update both rule names
|
||||
const updatedRule1 = getSimpleRule('rule-1');
|
||||
updatedRule1.id = createRule1.id;
|
||||
updatedRule1.name = 'some other name';
|
||||
delete updatedRule1.rule_id;
|
||||
|
||||
const updatedRule2 = getSimpleRule('rule-1');
|
||||
updatedRule2.id = createRule2.id;
|
||||
updatedRule2.name = 'some other name';
|
||||
delete updatedRule2.rule_id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([updatedRule1, updatedRule2])
|
||||
.expect(200);
|
||||
|
||||
const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1');
|
||||
outputRule1.name = 'some other name';
|
||||
outputRule1.version = 2;
|
||||
|
||||
const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2');
|
||||
outputRule2.name = 'some other name';
|
||||
outputRule2.version = 2;
|
||||
|
||||
const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
|
||||
const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]);
|
||||
expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]);
|
||||
});
|
||||
|
||||
it('should update a single rule property of name using the auto-generated id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createdBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's name
|
||||
const updatedRule1 = getSimpleRule('rule-1');
|
||||
updatedRule1.id = createdBody.id;
|
||||
updatedRule1.name = 'some other name';
|
||||
delete updatedRule1.rule_id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([updatedRule1])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should change the version of a rule when it updates enabled and another property', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's enabled to false and another property
|
||||
const updatedRule1 = getSimpleRule('rule-1');
|
||||
updatedRule1.severity = 'low';
|
||||
updatedRule1.enabled = false;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([updatedRule1])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.enabled = false;
|
||||
outputRule.severity = 'low';
|
||||
outputRule.version = 2;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's timeline_title
|
||||
const ruleUpdate = getSimpleRule('rule-1');
|
||||
ruleUpdate.timeline_title = 'some title';
|
||||
ruleUpdate.timeline_id = 'some id';
|
||||
|
||||
await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([ruleUpdate])
|
||||
.expect(200);
|
||||
|
||||
// update a simple rule's name
|
||||
const ruleUpdate2 = getSimpleRule('rule-1');
|
||||
ruleUpdate2.name = 'some other name';
|
||||
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([ruleUpdate2])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 3;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect(bodyToCompare).to.eql(outputRule);
|
||||
});
|
||||
|
||||
it('should return a 200 but give a 404 in the message if it is given a fake id', async () => {
|
||||
const ruleUpdate = getSimpleRule('rule-1');
|
||||
ruleUpdate.id = 'fake_id';
|
||||
delete ruleUpdate.rule_id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([ruleUpdate])
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{ rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => {
|
||||
const ruleUpdate = getSimpleRule('rule-1');
|
||||
ruleUpdate.rule_id = 'fake_id';
|
||||
delete ruleUpdate.id;
|
||||
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([ruleUpdate])
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql([
|
||||
{
|
||||
rule_id: 'fake_id',
|
||||
error: { status_code: 404, message: 'rule_id: "fake_id" not found' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update one rule property and give an error about a second fake rule_id', async () => {
|
||||
// create a simple rule
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
const ruleUpdate = getSimpleRule('rule-1');
|
||||
ruleUpdate.name = 'some other name';
|
||||
delete ruleUpdate.id;
|
||||
|
||||
const ruleUpdate2 = getSimpleRule('fake_id');
|
||||
ruleUpdate2.name = 'some other name';
|
||||
delete ruleUpdate.id;
|
||||
|
||||
// update one rule name and give a fake id for the second
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([ruleUpdate, ruleUpdate2])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect([bodyToCompare, body[1]]).to.eql([
|
||||
outputRule,
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'fake_id',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update one rule property and give an error about a second fake id', async () => {
|
||||
// create a simple rule
|
||||
const { body: createdBody } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimpleRule('rule-1'))
|
||||
.expect(200);
|
||||
|
||||
// update one rule name and give a fake id for the second
|
||||
const rule1 = getSimpleRule();
|
||||
delete rule1.rule_id;
|
||||
rule1.id = createdBody.id;
|
||||
rule1.name = 'some other name';
|
||||
|
||||
const rule2 = getSimpleRule();
|
||||
delete rule2.rule_id;
|
||||
rule2.id = 'fake_id';
|
||||
rule2.name = 'some other name';
|
||||
|
||||
const { body } = await supertest
|
||||
.put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send([rule1, rule2])
|
||||
.expect(200);
|
||||
|
||||
const outputRule = getSimpleRuleOutput();
|
||||
outputRule.name = 'some other name';
|
||||
outputRule.version = 2;
|
||||
|
||||
const bodyToCompare = removeServerGeneratedProperties(body[0]);
|
||||
expect([bodyToCompare, body[1]]).to.eql([
|
||||
outputRule,
|
||||
{
|
||||
error: {
|
||||
message: 'id: "fake_id" not found',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'fake_id', // TODO: This should be id and not rule_id in the codebase
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,345 @@
|
|||
/*
|
||||
* 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 { OutputRuleAlertRest } from '../../../../legacy/plugins/siem/server/lib/detection_engine/types';
|
||||
import { DETECTION_ENGINE_INDEX_URL } from '../../../../legacy/plugins/siem/common/constants';
|
||||
|
||||
/**
|
||||
* This will remove server generated properties such as date times, etc...
|
||||
* @param rule Rule to pass in to remove typical server generated properties
|
||||
*/
|
||||
export const removeServerGeneratedProperties = (
|
||||
rule: Partial<OutputRuleAlertRest>
|
||||
): Partial<OutputRuleAlertRest> => {
|
||||
const {
|
||||
created_at,
|
||||
updated_at,
|
||||
id,
|
||||
last_success_at,
|
||||
last_success_message,
|
||||
status,
|
||||
status_date,
|
||||
...removedProperties
|
||||
} = rule;
|
||||
return removedProperties;
|
||||
};
|
||||
|
||||
/**
|
||||
* This will remove server generated properties such as date times, etc... including the rule_id
|
||||
* @param rule Rule to pass in to remove typical server generated properties
|
||||
*/
|
||||
export const removeServerGeneratedPropertiesIncludingRuleId = (
|
||||
rule: Partial<OutputRuleAlertRest>
|
||||
): Partial<OutputRuleAlertRest> => {
|
||||
const ruleWithRemovedProperties = removeServerGeneratedProperties(rule);
|
||||
const { rule_id, ...additionalRuledIdRemoved } = ruleWithRemovedProperties;
|
||||
return additionalRuledIdRemoved;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a typical simple rule for testing that is easy for most basic testing
|
||||
* @param ruleId
|
||||
*/
|
||||
export const getSimpleRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({
|
||||
name: 'Simple Rule Query',
|
||||
description: 'Simple Rule Query',
|
||||
risk_score: 1,
|
||||
rule_id: ruleId,
|
||||
severity: 'high',
|
||||
type: 'query',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a typical simple rule for testing that is easy for most basic testing
|
||||
*/
|
||||
export const getSimpleRuleWithoutRuleId = (): Partial<OutputRuleAlertRest> => {
|
||||
const simpleRule = getSimpleRule();
|
||||
const { rule_id, ...ruleWithoutId } = simpleRule;
|
||||
return ruleWithoutId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Useful for export_api testing to convert from a multi-part binary back to a string
|
||||
* @param res Response
|
||||
* @param callback Callback
|
||||
*/
|
||||
export const binaryToString = (res: any, callback: any): void => {
|
||||
res.setEncoding('binary');
|
||||
res.data = '';
|
||||
res.on('data', (chunk: any) => {
|
||||
res.data += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
callback(null, Buffer.from(res.data));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the typical output of a simple rule that Kibana will output with all the defaults.
|
||||
*/
|
||||
export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({
|
||||
created_by: 'elastic',
|
||||
description: 'Simple Rule Query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
from: 'now-6m',
|
||||
immutable: false,
|
||||
interval: '5m',
|
||||
rule_id: ruleId,
|
||||
language: 'kuery',
|
||||
output_index: '.siem-signals-default',
|
||||
max_signals: 100,
|
||||
risk_score: 1,
|
||||
name: 'Simple Rule Query',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
references: [],
|
||||
severity: 'high',
|
||||
updated_by: 'elastic',
|
||||
tags: [],
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
threat: [],
|
||||
version: 1,
|
||||
});
|
||||
|
||||
/**
|
||||
* This is the typical output of a simple rule that Kibana will output with all the defaults.
|
||||
*/
|
||||
export const getSimpleRuleOutputWithoutRuleId = (
|
||||
ruleId = 'rule-1'
|
||||
): Partial<OutputRuleAlertRest> => {
|
||||
const rule = getSimpleRuleOutput(ruleId);
|
||||
const { rule_id, ...ruleWithoutRuleId } = rule;
|
||||
return ruleWithoutRuleId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove all alerts from the .kibana index
|
||||
* @param es The ElasticSearch handle
|
||||
*/
|
||||
export const deleteAllAlerts = async (es: any): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: '.kibana',
|
||||
q: 'type:alert',
|
||||
waitForCompletion: true,
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the signals index for use inside of beforeEach blocks of tests
|
||||
* @param supertest The supertest client library
|
||||
*/
|
||||
export const createSignalsIndex = async (supertest: any): Promise<void> => {
|
||||
await supertest
|
||||
.post(DETECTION_ENGINE_INDEX_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the signals index for use inside of afterEach blocks of tests
|
||||
* @param supertest The supertest client library
|
||||
*/
|
||||
export const deleteSignalsIndex = async (supertest: any): Promise<void> => {
|
||||
await supertest
|
||||
.delete(DETECTION_ENGINE_INDEX_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an array of rule_id strings this will return a ndjson buffer which is useful
|
||||
* for testing uploads.
|
||||
* @param ruleIds Array of strings of rule_ids
|
||||
*/
|
||||
export const getSimpleRuleAsNdjson = (ruleIds: string[]): Buffer => {
|
||||
const stringOfRules = ruleIds.map(ruleId => {
|
||||
const simpleRule = getSimpleRule(ruleId);
|
||||
return JSON.stringify(simpleRule);
|
||||
});
|
||||
return Buffer.from(stringOfRules.join('\n'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a rule this will convert it to an ndjson buffer which is useful for
|
||||
* testing upload features.
|
||||
* @param rule The rule to convert to ndjson
|
||||
*/
|
||||
export const ruleToNdjson = (rule: Partial<OutputRuleAlertRest>): Buffer => {
|
||||
const stringified = JSON.stringify(rule);
|
||||
return Buffer.from(`${stringified}\n`);
|
||||
};
|
||||
|
||||
/**
|
||||
* This will return a complex rule with all the outputs possible
|
||||
* @param ruleId The ruleId to set which is optional and defaults to rule-1
|
||||
*/
|
||||
export const getComplexRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({
|
||||
name: 'Complex Rule Query',
|
||||
description: 'Complex Rule Query',
|
||||
false_positives: [
|
||||
'https://www.example.com/some-article-about-a-false-positive',
|
||||
'some text string about why another condition could be a false positive',
|
||||
],
|
||||
risk_score: 1,
|
||||
rule_id: ruleId,
|
||||
filters: [
|
||||
{
|
||||
query: {
|
||||
match_phrase: {
|
||||
'host.name': 'siem-windows',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
enabled: false,
|
||||
index: ['auditbeat-*', 'filebeat-*'],
|
||||
interval: '5m',
|
||||
output_index: '.siem-signals-default',
|
||||
meta: {
|
||||
anything_you_want_ui_related_or_otherwise: {
|
||||
as_deep_structured_as_you_need: {
|
||||
any_data_type: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
max_signals: 10,
|
||||
tags: ['tag 1', 'tag 2', 'any tag you want'],
|
||||
to: 'now',
|
||||
from: 'now-6m',
|
||||
severity: 'high',
|
||||
language: 'kuery',
|
||||
type: 'query',
|
||||
threat: [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic: {
|
||||
id: 'TA0040',
|
||||
name: 'impact',
|
||||
reference: 'https://attack.mitre.org/tactics/TA0040/',
|
||||
},
|
||||
technique: [
|
||||
{
|
||||
id: 'T1499',
|
||||
name: 'endpoint denial of service',
|
||||
reference: 'https://attack.mitre.org/techniques/T1499/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
framework: 'Some other Framework you want',
|
||||
tactic: {
|
||||
id: 'some-other-id',
|
||||
name: 'Some other name',
|
||||
reference: 'https://example.com',
|
||||
},
|
||||
technique: [
|
||||
{
|
||||
id: 'some-other-id',
|
||||
name: 'some other technique name',
|
||||
reference: 'https://example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
references: [
|
||||
'http://www.example.com/some-article-about-attack',
|
||||
'Some plain text string here explaining why this is a valid thing to look out for',
|
||||
],
|
||||
timeline_id: 'timeline_id',
|
||||
timeline_title: 'timeline_title',
|
||||
version: 1,
|
||||
query: 'user.name: root or user.name: admin',
|
||||
});
|
||||
|
||||
/**
|
||||
* This will return a complex rule with all the outputs possible
|
||||
* @param ruleId The ruleId to set which is optional and defaults to rule-1
|
||||
*/
|
||||
export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({
|
||||
created_by: 'elastic',
|
||||
name: 'Complex Rule Query',
|
||||
description: 'Complex Rule Query',
|
||||
false_positives: [
|
||||
'https://www.example.com/some-article-about-a-false-positive',
|
||||
'some text string about why another condition could be a false positive',
|
||||
],
|
||||
risk_score: 1,
|
||||
rule_id: ruleId,
|
||||
filters: [
|
||||
{
|
||||
query: {
|
||||
match_phrase: {
|
||||
'host.name': 'siem-windows',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
enabled: false,
|
||||
index: ['auditbeat-*', 'filebeat-*'],
|
||||
immutable: false,
|
||||
interval: '5m',
|
||||
output_index: '.siem-signals-default',
|
||||
meta: {
|
||||
anything_you_want_ui_related_or_otherwise: {
|
||||
as_deep_structured_as_you_need: {
|
||||
any_data_type: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
max_signals: 10,
|
||||
tags: ['tag 1', 'tag 2', 'any tag you want'],
|
||||
to: 'now',
|
||||
from: 'now-6m',
|
||||
severity: 'high',
|
||||
language: 'kuery',
|
||||
type: 'query',
|
||||
threat: [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic: {
|
||||
id: 'TA0040',
|
||||
name: 'impact',
|
||||
reference: 'https://attack.mitre.org/tactics/TA0040/',
|
||||
},
|
||||
technique: [
|
||||
{
|
||||
id: 'T1499',
|
||||
name: 'endpoint denial of service',
|
||||
reference: 'https://attack.mitre.org/techniques/T1499/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
framework: 'Some other Framework you want',
|
||||
tactic: {
|
||||
id: 'some-other-id',
|
||||
name: 'Some other name',
|
||||
reference: 'https://example.com',
|
||||
},
|
||||
technique: [
|
||||
{
|
||||
id: 'some-other-id',
|
||||
name: 'some other technique name',
|
||||
reference: 'https://example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
references: [
|
||||
'http://www.example.com/some-article-about-attack',
|
||||
'Some plain text string here explaining why this is a valid thing to look out for',
|
||||
],
|
||||
timeline_id: 'timeline_id',
|
||||
timeline_title: 'timeline_title',
|
||||
updated_by: 'elastic',
|
||||
version: 1,
|
||||
query: 'user.name: root or user.name: admin',
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue