[SIEM][Detection Engine] Import/Export REST endpoints (#54332)
## Summary * Adds Import and Export REST endpoints * Fixes minor misc issues with types * Changes camel case from bulk api to become snake_case For the API and testing it is very similar to the saved objects API For import: ```ts POST /api/detection_engine/rules/_import ``` With a ndjson body of: ```ts {"created_at":"2020-01-09T01:38:00.740Z","updated_at":"2020-01-09T01:38:00.740Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"6688f367-1aa2-4895-a5a8-b3701eecf57d","immutable":false,"interval":"5m","rule_id":"query-rule-id-1","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":1,"name":"Query with a rule id Number 1","query":"user.name: root or user.name: admin","references":[],"severity":"high","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1} {"created_at":"2020-01-09T01:38:00.745Z","updated_at":"2020-01-09T01:38:00.745Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"7a912444-6cfa-4c8f-83f4-2b26fb2a2ed9","immutable":false,"interval":"5m","rule_id":"query-rule-id-2","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":2,"name":"Query with a rule id Number 2","query":"user.name: root or user.name: admin","references":[],"severity":"low","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1} {"exported_count":2,"missing_rules":[],"missing_rules_count":0} ``` If you want to overwrite existing objects you can use the overwrite query parameter like so: ```ts POST /api/detection_engine/rules/_import?overwrite=true ``` See and run the scripts of: ```ts import_rules.sh import_rules_no_overwrite.sh ``` For exporting everything: ```ts POST /api/detection_engine/rules/_export ``` For exporting just a handful of things you would send a body like so: ```ts POST /api/detection_engine/rules/_export { "objects": [ { "rule_id": "query-rule-id-1" }, { "rule_id": "query-rule-id-2" } ] } ``` To change either the filename of the file that gets downloaded or to remove the extra appended export details you can do the following: ```ts POST /api/detection_engine/rules/_export?exclude_export_details=true&file_name=my_file.ndjson" ``` See the scripts of: ```ts export_rules.sh export_rules_by_rule_id.sh export_rules_by_rule_id_to_file.sh export_rules_to_file.sh ``` ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
parent
299df2dae7
commit
7eb88c4d13
|
@ -25,6 +25,8 @@ import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_
|
|||
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 { 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';
|
||||
|
||||
const APP_ID = 'siem';
|
||||
|
||||
|
@ -50,6 +52,8 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy
|
|||
createRulesBulkRoute(__legacy);
|
||||
updateRulesBulkRoute(__legacy);
|
||||
deleteRulesBulkRoute(__legacy);
|
||||
importRulesRoute(__legacy);
|
||||
exportRulesRoute(__legacy);
|
||||
|
||||
// Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals
|
||||
// POST /api/detection_engine/signals/status
|
||||
|
|
|
@ -159,7 +159,7 @@ export const getPrivilegeRequest = (): ServerInjectOptions => ({
|
|||
url: `${DETECTION_ENGINE_PRIVILEGES_URL}`,
|
||||
});
|
||||
|
||||
interface FindHit {
|
||||
export interface FindHit {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
|
@ -176,7 +176,7 @@ export const getFindResult = (): FindHit => ({
|
|||
export const getFindResultWithSingleHit = (): FindHit => ({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
total: 1,
|
||||
data: [getResult()],
|
||||
});
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
|
||||
import { deleteRulesBulkRoute } from './delete_rules_bulk_route';
|
||||
import { BulkError } from '../utils';
|
||||
|
||||
describe('delete_rules', () => {
|
||||
let { server, alertsClient } = createMockServer();
|
||||
|
@ -83,10 +84,14 @@ describe('delete_rules', () => {
|
|||
alertsClient.get.mockResolvedValue(getResult());
|
||||
alertsClient.delete.mockResolvedValue({});
|
||||
const { payload } = await server.inject(getDeleteBulkRequest());
|
||||
const parsed = JSON.parse(payload);
|
||||
expect(parsed).toEqual([
|
||||
{ error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' },
|
||||
]);
|
||||
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 actionClient is not available on the route', async () => {
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import Hapi from 'hapi';
|
||||
import { isFunction } from 'lodash/fp';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { ExportRulesRequest } from '../../rules/types';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules';
|
||||
import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema';
|
||||
import { getExportByObjectIds } from '../../rules/get_export_by_object_ids';
|
||||
import { getExportAll } from '../../rules/get_export_all';
|
||||
|
||||
export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
return {
|
||||
method: 'POST',
|
||||
path: `${DETECTION_ENGINE_RULES_URL}/_export`,
|
||||
options: {
|
||||
tags: ['access:siem'],
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
},
|
||||
payload: exportRulesSchema,
|
||||
query: exportRulesQuerySchema,
|
||||
},
|
||||
},
|
||||
async handler(request: ExportRulesRequest, headers) {
|
||||
const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null;
|
||||
const actionsClient = isFunction(request.getActionsClient)
|
||||
? request.getActionsClient()
|
||||
: null;
|
||||
|
||||
if (!alertsClient || !actionsClient) {
|
||||
return headers.response().code(404);
|
||||
}
|
||||
|
||||
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`);
|
||||
} else {
|
||||
const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient });
|
||||
if (nonPackagedRulesCount > exportSizeLimit) {
|
||||
return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`);
|
||||
}
|
||||
}
|
||||
|
||||
const exported =
|
||||
request.payload?.objects != null
|
||||
? await getExportByObjectIds(alertsClient, request.payload.objects)
|
||||
: await getExportAll(alertsClient);
|
||||
|
||||
const response = request.query.exclude_export_details
|
||||
? headers.response(exported.rulesNdjson)
|
||||
: headers.response(`${exported.rulesNdjson}${exported.exportDetails}`);
|
||||
|
||||
return response
|
||||
.header('Content-Disposition', `attachment; filename="${request.query.file_name}"`)
|
||||
.header('Content-Type', 'application/ndjson');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const exportRulesRoute = (server: ServerFacade): void => {
|
||||
server.route(createExportRulesRoute(server));
|
||||
};
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import Hapi from 'hapi';
|
||||
import { extname } from 'path';
|
||||
import { isFunction } from 'lodash/fp';
|
||||
import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { createRules } from '../../rules/create_rules';
|
||||
import { ImportRulesRequest } from '../../rules/types';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { readRules } from '../../rules/read_rules';
|
||||
import { getIndexExists } from '../../index/get_index_exists';
|
||||
import {
|
||||
callWithRequestFactory,
|
||||
getIndex,
|
||||
createImportErrorObject,
|
||||
transformImportError,
|
||||
ImportSuccessError,
|
||||
} from '../utils';
|
||||
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
|
||||
import { ImportRuleAlertRest } from '../../types';
|
||||
import { transformOrImportError } from './utils';
|
||||
import { updateRules } from '../../rules/update_rules';
|
||||
import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema';
|
||||
|
||||
export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
return {
|
||||
method: 'POST',
|
||||
path: `${DETECTION_ENGINE_RULES_URL}/_import`,
|
||||
options: {
|
||||
tags: ['access:siem'],
|
||||
payload: {
|
||||
maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'),
|
||||
output: 'stream',
|
||||
allow: 'multipart/form-data',
|
||||
},
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
},
|
||||
query: importRulesQuerySchema,
|
||||
payload: importRulesPayloadSchema,
|
||||
},
|
||||
},
|
||||
async handler(request: ImportRulesRequest, headers) {
|
||||
const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null;
|
||||
const actionsClient = isFunction(request.getActionsClient)
|
||||
? request.getActionsClient()
|
||||
: null;
|
||||
|
||||
if (!alertsClient || !actionsClient) {
|
||||
return headers.response().code(404);
|
||||
}
|
||||
const { filename } = request.payload.file.hapi;
|
||||
const fileExtension = extname(filename).toLowerCase();
|
||||
if (fileExtension !== '.ndjson') {
|
||||
return Boom.badRequest(`Invalid file extension ${fileExtension}`);
|
||||
}
|
||||
|
||||
const objectLimit = server.config().get<number>('savedObjects.maxImportExportSize');
|
||||
const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit);
|
||||
const parsedObjects = await createPromiseFromStreams<[ImportRuleAlertRest | Error]>([
|
||||
readStream,
|
||||
]);
|
||||
|
||||
const reduced = await parsedObjects.reduce<Promise<ImportSuccessError>>(
|
||||
async (accum, parsedRule) => {
|
||||
const existingImportSuccessError = await accum;
|
||||
if (parsedRule instanceof Error) {
|
||||
// If the JSON object had a validation or parse error then we return
|
||||
// early with the error and an (unknown) for the ruleId
|
||||
return createImportErrorObject({
|
||||
ruleId: '(unknown)', // TODO: Better handling where we know which ruleId is having issues with imports
|
||||
statusCode: 400,
|
||||
message: parsedRule.message,
|
||||
existingImportSuccessError,
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
description,
|
||||
enabled,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query,
|
||||
language,
|
||||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
meta,
|
||||
filters,
|
||||
rule_id: ruleId,
|
||||
index,
|
||||
interval,
|
||||
max_signals: maxSignals,
|
||||
risk_score: riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
threats,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
version,
|
||||
} = parsedRule;
|
||||
try {
|
||||
const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server);
|
||||
const callWithRequest = callWithRequestFactory(request, server);
|
||||
const indexExists = await getIndexExists(callWithRequest, finalIndex);
|
||||
if (!indexExists) {
|
||||
return createImportErrorObject({
|
||||
ruleId,
|
||||
statusCode: 409,
|
||||
message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`,
|
||||
existingImportSuccessError,
|
||||
});
|
||||
}
|
||||
const rule = await readRules({ alertsClient, ruleId });
|
||||
if (rule == null) {
|
||||
const createdRule = await createRules({
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
createdAt: new Date().toISOString(),
|
||||
description,
|
||||
enabled,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query,
|
||||
language,
|
||||
outputIndex: finalIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
ruleId,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threats,
|
||||
updatedAt: new Date().toISOString(),
|
||||
references,
|
||||
version,
|
||||
});
|
||||
return transformOrImportError(ruleId, createdRule, existingImportSuccessError);
|
||||
} else if (rule != null && request.query.overwrite) {
|
||||
const updatedRule = await updateRules({
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
description,
|
||||
enabled,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query,
|
||||
language,
|
||||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
id: undefined,
|
||||
ruleId,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threats,
|
||||
references,
|
||||
version,
|
||||
});
|
||||
return transformOrImportError(ruleId, updatedRule, existingImportSuccessError);
|
||||
} else {
|
||||
return existingImportSuccessError;
|
||||
}
|
||||
} catch (err) {
|
||||
return transformImportError(ruleId, err, existingImportSuccessError);
|
||||
}
|
||||
},
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
})
|
||||
);
|
||||
return reduced;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const importRulesRoute = (server: ServerFacade): void => {
|
||||
server.route(createImportRulesRoute(server));
|
||||
};
|
|
@ -23,6 +23,7 @@ import {
|
|||
} from '../__mocks__/request_responses';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { updateRulesBulkRoute } from './update_rules_bulk_route';
|
||||
import { BulkError } from '../utils';
|
||||
|
||||
describe('update_rules_bulk', () => {
|
||||
let { server, alertsClient, actionsClient } = createMockServer();
|
||||
|
@ -58,10 +59,14 @@ describe('update_rules_bulk', () => {
|
|||
actionsClient.update.mockResolvedValue(updateActionResult());
|
||||
alertsClient.update.mockResolvedValue(getResult());
|
||||
const { payload } = await server.inject(getUpdateBulkRequest());
|
||||
const parsed = JSON.parse(payload);
|
||||
expect(parsed).toEqual([
|
||||
{ error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' },
|
||||
]);
|
||||
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 actionClient is not available on the route', async () => {
|
||||
|
@ -125,10 +130,14 @@ describe('update_rules_bulk', () => {
|
|||
payload: [typicalPayload()],
|
||||
};
|
||||
const { payload } = await server.inject(request);
|
||||
const parsed = JSON.parse(payload);
|
||||
expect(parsed).toEqual([
|
||||
{ error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' },
|
||||
]);
|
||||
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 () => {
|
||||
|
|
|
@ -14,11 +14,15 @@ import {
|
|||
transformTags,
|
||||
getIdBulkError,
|
||||
transformOrBulkError,
|
||||
transformRulesToNdjson,
|
||||
transformAlertsToRules,
|
||||
transformOrImportError,
|
||||
} from './utils';
|
||||
import { getResult } from '../__mocks__/request_responses';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
import { OutputRuleAlertRest } from '../../types';
|
||||
import { BulkError } from '../utils';
|
||||
import { BulkError, ImportSuccessError } from '../utils';
|
||||
import { sampleRule } from '../../signals/__mocks__/es_results';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('transformAlertToRule', () => {
|
||||
|
@ -756,8 +760,8 @@ describe('utils', () => {
|
|||
test('outputs message about id not being found if only id is defined and ruleId is undefined', () => {
|
||||
const error = getIdBulkError({ id: '123', ruleId: undefined });
|
||||
const expected: BulkError = {
|
||||
id: '123',
|
||||
error: { message: 'id: "123" not found', statusCode: 404 },
|
||||
rule_id: '123',
|
||||
error: { message: 'id: "123" not found', status_code: 404 },
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
@ -765,8 +769,8 @@ describe('utils', () => {
|
|||
test('outputs message about id not being found if only id is defined and ruleId is null', () => {
|
||||
const error = getIdBulkError({ id: '123', ruleId: null });
|
||||
const expected: BulkError = {
|
||||
id: '123',
|
||||
error: { message: 'id: "123" not found', statusCode: 404 },
|
||||
rule_id: '123',
|
||||
error: { message: 'id: "123" not found', status_code: 404 },
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
@ -774,8 +778,8 @@ describe('utils', () => {
|
|||
test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => {
|
||||
const error = getIdBulkError({ id: undefined, ruleId: 'rule-id-123' });
|
||||
const expected: BulkError = {
|
||||
id: 'rule-id-123',
|
||||
error: { message: 'rule_id: "rule-id-123" not found', statusCode: 404 },
|
||||
rule_id: 'rule-id-123',
|
||||
error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 },
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
@ -783,8 +787,8 @@ describe('utils', () => {
|
|||
test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => {
|
||||
const error = getIdBulkError({ id: null, ruleId: 'rule-id-123' });
|
||||
const expected: BulkError = {
|
||||
id: 'rule-id-123',
|
||||
error: { message: 'rule_id: "rule-id-123" not found', statusCode: 404 },
|
||||
rule_id: 'rule-id-123',
|
||||
error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 },
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
@ -792,8 +796,8 @@ describe('utils', () => {
|
|||
test('outputs message about both being not defined when both are undefined', () => {
|
||||
const error = getIdBulkError({ id: undefined, ruleId: undefined });
|
||||
const expected: BulkError = {
|
||||
id: '(unknown id)',
|
||||
error: { message: 'id or rule_id should have been defined', statusCode: 404 },
|
||||
rule_id: '(unknown id)',
|
||||
error: { message: 'id or rule_id should have been defined', status_code: 404 },
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
@ -801,8 +805,8 @@ describe('utils', () => {
|
|||
test('outputs message about both being not defined when both are null', () => {
|
||||
const error = getIdBulkError({ id: null, ruleId: null });
|
||||
const expected: BulkError = {
|
||||
id: '(unknown id)',
|
||||
error: { message: 'id or rule_id should have been defined', statusCode: 404 },
|
||||
rule_id: '(unknown id)',
|
||||
error: { message: 'id or rule_id should have been defined', status_code: 404 },
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
@ -810,8 +814,8 @@ describe('utils', () => {
|
|||
test('outputs message about both being not defined when id is null and ruleId is undefined', () => {
|
||||
const error = getIdBulkError({ id: null, ruleId: undefined });
|
||||
const expected: BulkError = {
|
||||
id: '(unknown id)',
|
||||
error: { message: 'id or rule_id should have been defined', statusCode: 404 },
|
||||
rule_id: '(unknown id)',
|
||||
error: { message: 'id or rule_id should have been defined', status_code: 404 },
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
@ -819,8 +823,8 @@ describe('utils', () => {
|
|||
test('outputs message about both being not defined when id is undefined and ruleId is null', () => {
|
||||
const error = getIdBulkError({ id: undefined, ruleId: null });
|
||||
const expected: BulkError = {
|
||||
id: '(unknown id)',
|
||||
error: { message: 'id or rule_id should have been defined', statusCode: 404 },
|
||||
rule_id: '(unknown id)',
|
||||
error: { message: 'id or rule_id should have been defined', status_code: 404 },
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
@ -893,10 +897,279 @@ describe('utils', () => {
|
|||
|
||||
test('returns 500 if the data is not of type siem alert', () => {
|
||||
const output = transformOrBulkError('rule-1', { data: [{ random: 1 }] });
|
||||
expect(output).toEqual({
|
||||
id: 'rule-1',
|
||||
error: { message: 'Internal error transforming', statusCode: 500 },
|
||||
const expected: BulkError = {
|
||||
rule_id: 'rule-1',
|
||||
error: { message: 'Internal error transforming', status_code: 500 },
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformRulesToNdjson', () => {
|
||||
test('if rules are empty it returns an empty string', () => {
|
||||
const ruleNdjson = transformRulesToNdjson([]);
|
||||
expect(ruleNdjson).toEqual('');
|
||||
});
|
||||
|
||||
test('single rule will transform with new line ending character for ndjson', () => {
|
||||
const rule = sampleRule();
|
||||
const ruleNdjson = transformRulesToNdjson([rule]);
|
||||
expect(ruleNdjson.endsWith('\n')).toBe(true);
|
||||
});
|
||||
|
||||
test('multiple rules will transform with two new line ending characters for ndjson', () => {
|
||||
const result1 = sampleRule();
|
||||
const result2 = sampleRule();
|
||||
result2.id = 'some other id';
|
||||
result2.rule_id = 'some other id';
|
||||
result2.name = 'Some other rule';
|
||||
|
||||
const ruleNdjson = transformRulesToNdjson([result1, result2]);
|
||||
// this is how we count characters in JavaScript :-)
|
||||
const count = ruleNdjson.split('\n').length - 1;
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
test('you can parse two rules back out without errors', () => {
|
||||
const result1 = sampleRule();
|
||||
const result2 = sampleRule();
|
||||
result2.id = 'some other id';
|
||||
result2.rule_id = 'some other id';
|
||||
result2.name = 'Some other rule';
|
||||
|
||||
const ruleNdjson = transformRulesToNdjson([result1, result2]);
|
||||
const ruleStrings = ruleNdjson.split('\n');
|
||||
const reParsed1 = JSON.parse(ruleStrings[0]);
|
||||
const reParsed2 = JSON.parse(ruleStrings[1]);
|
||||
expect(reParsed1).toEqual(result1);
|
||||
expect(reParsed2).toEqual(result2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformAlertsToRules', () => {
|
||||
test('given an empty array returns an empty array', () => {
|
||||
expect(transformAlertsToRules([])).toEqual([]);
|
||||
});
|
||||
|
||||
test('given single alert will return the alert transformed', () => {
|
||||
const result1 = getResult();
|
||||
const transformed = transformAlertsToRules([result1]);
|
||||
expect(transformed).toEqual([
|
||||
{
|
||||
created_at: '2019-12-13T16:40:33.400Z',
|
||||
created_by: 'elastic',
|
||||
description: 'Detecting root and admin users',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }],
|
||||
from: 'now-6m',
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
immutable: false,
|
||||
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
interval: '5m',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
meta: { someMeta: 'someField' },
|
||||
name: 'Detect Root/Admin Users',
|
||||
output_index: '.siem-signals',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
references: ['http://www.example.com', 'https://ww.example.com'],
|
||||
risk_score: 50,
|
||||
rule_id: 'rule-1',
|
||||
saved_id: 'some-id',
|
||||
severity: 'high',
|
||||
tags: [],
|
||||
threats: [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic: {
|
||||
id: 'TA0040',
|
||||
name: 'impact',
|
||||
reference: 'https://attack.mitre.org/tactics/TA0040/',
|
||||
},
|
||||
techniques: [
|
||||
{
|
||||
id: 'T1499',
|
||||
name: 'endpoint denial of service',
|
||||
reference: 'https://attack.mitre.org/techniques/T1499/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
updated_at: '2019-12-13T16:40:33.400Z',
|
||||
updated_by: 'elastic',
|
||||
version: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('given two alerts will return the two alerts transformed', () => {
|
||||
const result1 = getResult();
|
||||
const result2 = getResult();
|
||||
result2.id = 'some other id';
|
||||
result2.params.ruleId = 'some other id';
|
||||
|
||||
const transformed = transformAlertsToRules([result1, result2]);
|
||||
expect(transformed).toEqual([
|
||||
{
|
||||
created_at: '2019-12-13T16:40:33.400Z',
|
||||
created_by: 'elastic',
|
||||
description: 'Detecting root and admin users',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }],
|
||||
from: 'now-6m',
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
immutable: false,
|
||||
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
interval: '5m',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
meta: { someMeta: 'someField' },
|
||||
name: 'Detect Root/Admin Users',
|
||||
output_index: '.siem-signals',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
references: ['http://www.example.com', 'https://ww.example.com'],
|
||||
risk_score: 50,
|
||||
rule_id: 'rule-1',
|
||||
saved_id: 'some-id',
|
||||
severity: 'high',
|
||||
tags: [],
|
||||
threats: [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic: {
|
||||
id: 'TA0040',
|
||||
name: 'impact',
|
||||
reference: 'https://attack.mitre.org/tactics/TA0040/',
|
||||
},
|
||||
techniques: [
|
||||
{
|
||||
id: 'T1499',
|
||||
name: 'endpoint denial of service',
|
||||
reference: 'https://attack.mitre.org/techniques/T1499/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
updated_at: '2019-12-13T16:40:33.400Z',
|
||||
updated_by: 'elastic',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
created_at: '2019-12-13T16:40:33.400Z',
|
||||
created_by: 'elastic',
|
||||
description: 'Detecting root and admin users',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }],
|
||||
from: 'now-6m',
|
||||
id: 'some other id',
|
||||
immutable: false,
|
||||
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
interval: '5m',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
meta: { someMeta: 'someField' },
|
||||
name: 'Detect Root/Admin Users',
|
||||
output_index: '.siem-signals',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
references: ['http://www.example.com', 'https://ww.example.com'],
|
||||
risk_score: 50,
|
||||
rule_id: 'some other id',
|
||||
saved_id: 'some-id',
|
||||
severity: 'high',
|
||||
tags: [],
|
||||
threats: [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic: {
|
||||
id: 'TA0040',
|
||||
name: 'impact',
|
||||
reference: 'https://attack.mitre.org/tactics/TA0040/',
|
||||
},
|
||||
techniques: [
|
||||
{
|
||||
id: 'T1499',
|
||||
name: 'endpoint denial of service',
|
||||
reference: 'https://attack.mitre.org/techniques/T1499/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
updated_at: '2019-12-13T16:40:33.400Z',
|
||||
updated_by: 'elastic',
|
||||
version: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformOrImportError', () => {
|
||||
test('returns 1 given success if the alert is an alert type and the existing success count is 0', () => {
|
||||
const output = transformOrImportError('rule-1', getResult(), {
|
||||
success: true,
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success: true,
|
||||
errors: [],
|
||||
success_count: 1,
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns 2 given successes if the alert is an alert type and the existing success count is 1', () => {
|
||||
const output = transformOrImportError('rule-1', getResult(), {
|
||||
success: true,
|
||||
success_count: 1,
|
||||
errors: [],
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success: true,
|
||||
errors: [],
|
||||
success_count: 2,
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns 1 error and success of false if the data is not of type siem alert', () => {
|
||||
const output = transformOrImportError(
|
||||
'rule-1',
|
||||
{ data: [{ random: 1 }] },
|
||||
{
|
||||
success: true,
|
||||
success_count: 1,
|
||||
errors: [],
|
||||
}
|
||||
);
|
||||
const expected: ImportSuccessError = {
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
error: {
|
||||
message: 'Internal error transforming',
|
||||
status_code: 500,
|
||||
},
|
||||
},
|
||||
],
|
||||
success_count: 1,
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,13 @@ import { pickBy } from 'lodash/fp';
|
|||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
import { RuleAlertType, isAlertType, isAlertTypes } from '../../rules/types';
|
||||
import { OutputRuleAlertRest } from '../../types';
|
||||
import { createBulkErrorObject, BulkError } from '../utils';
|
||||
import {
|
||||
createBulkErrorObject,
|
||||
BulkError,
|
||||
createSuccessObject,
|
||||
ImportSuccessError,
|
||||
createImportErrorObject,
|
||||
} from '../utils';
|
||||
|
||||
export const getIdError = ({
|
||||
id,
|
||||
|
@ -97,6 +103,21 @@ export const transformAlertToRule = (alert: RuleAlertType): Partial<OutputRuleAl
|
|||
});
|
||||
};
|
||||
|
||||
export const transformRulesToNdjson = (rules: Array<Partial<OutputRuleAlertRest>>): string => {
|
||||
if (rules.length !== 0) {
|
||||
const rulesString = rules.map(rule => JSON.stringify(rule)).join('\n');
|
||||
return `${rulesString}\n`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const transformAlertsToRules = (
|
||||
alerts: RuleAlertType[]
|
||||
): Array<Partial<OutputRuleAlertRest>> => {
|
||||
return alerts.map(alert => transformAlertToRule(alert));
|
||||
};
|
||||
|
||||
export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => {
|
||||
if (isAlertTypes(findResults.data)) {
|
||||
findResults.data = findResults.data.map(alert => transformAlertToRule(alert));
|
||||
|
@ -128,3 +149,20 @@ export const transformOrBulkError = (
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const transformOrImportError = (
|
||||
ruleId: string,
|
||||
alert: unknown,
|
||||
existingImportSuccessError: ImportSuccessError
|
||||
): ImportSuccessError => {
|
||||
if (isAlertType(alert)) {
|
||||
return createSuccessObject(existingImportSuccessError);
|
||||
} else {
|
||||
return createImportErrorObject({
|
||||
ruleId,
|
||||
statusCode: 500,
|
||||
message: 'Internal error transforming',
|
||||
existingImportSuccessError,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { exportRulesSchema, exportRulesQuerySchema } from './export_rules_schema';
|
||||
import { ExportRulesRequest } from '../../rules/types';
|
||||
|
||||
describe('create rules schema', () => {
|
||||
describe('exportRulesSchema', () => {
|
||||
test('null value or absent values validate', () => {
|
||||
expect(exportRulesSchema.validate(null).error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('empty object does not validate', () => {
|
||||
expect(
|
||||
exportRulesSchema.validate<Partial<ExportRulesRequest['payload']>>({}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('empty object array does validate', () => {
|
||||
expect(
|
||||
exportRulesSchema.validate<Partial<ExportRulesRequest['payload']>>({ objects: [] }).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('array with rule_id validates', () => {
|
||||
expect(
|
||||
exportRulesSchema.validate<Partial<ExportRulesRequest['payload']>>({
|
||||
objects: [{ rule_id: 'test-1' }],
|
||||
}).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('array with id does not validate as we do not allow that on purpose since we export rule_id', () => {
|
||||
expect(
|
||||
exportRulesSchema.validate<Omit<ExportRulesRequest['payload'], 'objects'>>({
|
||||
objects: [{ id: 'test-1' }],
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportRulesQuerySchema', () => {
|
||||
test('default value for file_name is export.ndjson', () => {
|
||||
expect(
|
||||
exportRulesQuerySchema.validate<Partial<ExportRulesRequest['query']>>({}).value.file_name
|
||||
).toEqual('export.ndjson');
|
||||
});
|
||||
|
||||
test('default value for exclude_export_details is false', () => {
|
||||
expect(
|
||||
exportRulesQuerySchema.validate<Partial<ExportRulesRequest['query']>>({}).value
|
||||
.exclude_export_details
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
test('file_name validates', () => {
|
||||
expect(
|
||||
exportRulesQuerySchema.validate<Partial<ExportRulesRequest['query']>>({
|
||||
file_name: 'test.ndjson',
|
||||
}).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('file_name does not validate with a number', () => {
|
||||
expect(
|
||||
exportRulesQuerySchema.validate<
|
||||
Partial<Omit<ExportRulesRequest['query'], 'file_name'> & { file_name: number }>
|
||||
>({
|
||||
file_name: 5,
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('exclude_export_details validates with a boolean true', () => {
|
||||
expect(
|
||||
exportRulesQuerySchema.validate<Partial<ExportRulesRequest['query']>>({
|
||||
exclude_export_details: true,
|
||||
}).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('exclude_export_details does not validate with a weird string', () => {
|
||||
expect(
|
||||
exportRulesQuerySchema.validate<
|
||||
Partial<
|
||||
Omit<ExportRulesRequest['query'], 'exclude_export_details'> & {
|
||||
exclude_export_details: string;
|
||||
}
|
||||
>
|
||||
>({
|
||||
exclude_export_details: 'blah',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { objects, exclude_export_details, file_name } from './schemas';
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
|
||||
export const exportRulesSchema = Joi.object({
|
||||
objects,
|
||||
})
|
||||
.min(1)
|
||||
.allow(null);
|
||||
|
||||
export const exportRulesQuerySchema = Joi.object({
|
||||
file_name: file_name.default('export.ndjson'),
|
||||
exclude_export_details: exclude_export_details.default(false),
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -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 Joi from 'joi';
|
||||
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import {
|
||||
id,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by,
|
||||
enabled,
|
||||
description,
|
||||
false_positives,
|
||||
filters,
|
||||
from,
|
||||
immutable,
|
||||
index,
|
||||
rule_id,
|
||||
interval,
|
||||
query,
|
||||
language,
|
||||
output_index,
|
||||
saved_id,
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score,
|
||||
max_signals,
|
||||
name,
|
||||
severity,
|
||||
tags,
|
||||
to,
|
||||
type,
|
||||
threats,
|
||||
references,
|
||||
version,
|
||||
} from './schemas';
|
||||
/* eslint-enable @typescript-eslint/camelcase */
|
||||
|
||||
import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants';
|
||||
|
||||
/**
|
||||
* Differences from this and the createRulesSchema are
|
||||
* - rule_id is required
|
||||
* - id is optional (but ignored in the import code - rule_id is exclusively used for imports)
|
||||
* - created_at is optional (but ignored in the import code)
|
||||
* - updated_at is optional (but ignored in the import code)
|
||||
* - created_by is optional (but ignored in the import code)
|
||||
* - updated_by is optional (but ignored in the import code)
|
||||
*/
|
||||
export const importRulesSchema = Joi.object({
|
||||
id,
|
||||
description: description.required(),
|
||||
enabled: enabled.default(true),
|
||||
false_positives: false_positives.default([]),
|
||||
filters,
|
||||
from: from.required(),
|
||||
rule_id: rule_id.required(),
|
||||
immutable: immutable.default(false),
|
||||
index,
|
||||
interval: interval.default('5m'),
|
||||
query: query.allow('').default(''),
|
||||
language: language.default('kuery'),
|
||||
output_index,
|
||||
saved_id: saved_id.when('type', {
|
||||
is: 'saved_query',
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.forbidden(),
|
||||
}),
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score: risk_score.required(),
|
||||
max_signals: max_signals.default(DEFAULT_MAX_SIGNALS),
|
||||
name: name.required(),
|
||||
severity: severity.required(),
|
||||
tags: tags.default([]),
|
||||
to: to.required(),
|
||||
type: type.required(),
|
||||
threats: threats.default([]),
|
||||
references: references.default([]),
|
||||
version: version.default(1),
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by,
|
||||
});
|
||||
|
||||
export const importRulesQuerySchema = Joi.object({
|
||||
overwrite: Joi.boolean().default(false),
|
||||
});
|
||||
|
||||
export const importRulesPayloadSchema = Joi.object({
|
||||
file: Joi.object().required(),
|
||||
});
|
|
@ -9,7 +9,9 @@ import Joi from 'joi';
|
|||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
export const description = Joi.string();
|
||||
export const enabled = Joi.boolean();
|
||||
export const exclude_export_details = Joi.boolean();
|
||||
export const false_positives = Joi.array().items(Joi.string());
|
||||
export const file_name = Joi.string();
|
||||
export const filters = Joi.array();
|
||||
export const from = Joi.string();
|
||||
export const immutable = Joi.boolean();
|
||||
|
@ -21,6 +23,11 @@ export const index = Joi.array()
|
|||
export const interval = Joi.string();
|
||||
export const query = Joi.string();
|
||||
export const language = Joi.string().valid('kuery', 'lucene');
|
||||
export const objects = Joi.array().items(
|
||||
Joi.object({
|
||||
rule_id,
|
||||
}).required()
|
||||
);
|
||||
export const output_index = Joi.string();
|
||||
export const saved_id = Joi.string();
|
||||
export const timeline_id = Joi.string();
|
||||
|
@ -75,7 +82,6 @@ export const threat_technique = Joi.object({
|
|||
reference: threat_technique_reference.required(),
|
||||
});
|
||||
export const threat_techniques = Joi.array().items(threat_technique.required());
|
||||
|
||||
export const threats = Joi.array().items(
|
||||
Joi.object({
|
||||
framework: threat_framework.required(),
|
||||
|
@ -83,5 +89,12 @@ export const threats = Joi.array().items(
|
|||
techniques: threat_techniques.required(),
|
||||
})
|
||||
);
|
||||
|
||||
export const created_at = Joi.string()
|
||||
.isoDate()
|
||||
.strict();
|
||||
export const updated_at = Joi.string()
|
||||
.isoDate()
|
||||
.strict();
|
||||
export const created_by = Joi.string();
|
||||
export const updated_by = Joi.string();
|
||||
export const version = Joi.number().min(1);
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
|
||||
import Boom from 'boom';
|
||||
|
||||
import { transformError, transformBulkError, BulkError } from './utils';
|
||||
import {
|
||||
transformError,
|
||||
transformBulkError,
|
||||
BulkError,
|
||||
createSuccessObject,
|
||||
ImportSuccessError,
|
||||
createImportErrorObject,
|
||||
transformImportError,
|
||||
} from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('transformError', () => {
|
||||
|
@ -63,8 +71,8 @@ describe('utils', () => {
|
|||
const boom = new Boom('some boom message', { statusCode: 400 });
|
||||
const transformed = transformBulkError('rule-1', boom);
|
||||
const expected: BulkError = {
|
||||
id: 'rule-1',
|
||||
error: { message: 'some boom message', statusCode: 400 },
|
||||
rule_id: 'rule-1',
|
||||
error: { message: 'some boom message', status_code: 400 },
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
|
@ -77,8 +85,8 @@ describe('utils', () => {
|
|||
};
|
||||
const transformed = transformBulkError('rule-1', error);
|
||||
const expected: BulkError = {
|
||||
id: 'rule-1',
|
||||
error: { message: 'some message', statusCode: 403 },
|
||||
rule_id: 'rule-1',
|
||||
error: { message: 'some message', status_code: 403 },
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
|
@ -90,8 +98,8 @@ describe('utils', () => {
|
|||
};
|
||||
const transformed = transformBulkError('rule-1', error);
|
||||
const expected: BulkError = {
|
||||
id: 'rule-1',
|
||||
error: { message: 'some message', statusCode: 500 },
|
||||
rule_id: 'rule-1',
|
||||
error: { message: 'some message', status_code: 500 },
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
|
@ -100,8 +108,168 @@ describe('utils', () => {
|
|||
const error: TypeError = new TypeError('I have a type error');
|
||||
const transformed = transformBulkError('rule-1', error);
|
||||
const expected: BulkError = {
|
||||
id: 'rule-1',
|
||||
error: { message: 'I have a type error', statusCode: 400 },
|
||||
rule_id: 'rule-1',
|
||||
error: { message: 'I have a type error', status_code: 400 },
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSuccessObject', () => {
|
||||
test('it should increment the existing success object by 1', () => {
|
||||
const success = createSuccessObject({
|
||||
success_count: 0,
|
||||
success: true,
|
||||
errors: [],
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success_count: 1,
|
||||
success: true,
|
||||
errors: [],
|
||||
};
|
||||
expect(success).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should increment the existing success object by 1 and not touch the boolean or errors', () => {
|
||||
const success = createSuccessObject({
|
||||
success_count: 0,
|
||||
success: false,
|
||||
errors: [
|
||||
{ rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } },
|
||||
],
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [
|
||||
{ rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } },
|
||||
],
|
||||
};
|
||||
expect(success).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createImportErrorObject', () => {
|
||||
test('it creates an error message and does not increment the success count', () => {
|
||||
const error = createImportErrorObject({
|
||||
ruleId: 'some-rule-id',
|
||||
statusCode: 400,
|
||||
message: 'some-message',
|
||||
existingImportSuccessError: {
|
||||
success_count: 1,
|
||||
success: true,
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }],
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
|
||||
test('appends a second error message and does not increment the success count', () => {
|
||||
const error = createImportErrorObject({
|
||||
ruleId: 'some-rule-id',
|
||||
statusCode: 400,
|
||||
message: 'some-message',
|
||||
existingImportSuccessError: {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [
|
||||
{ rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } },
|
||||
],
|
||||
},
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [
|
||||
{ rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } },
|
||||
{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } },
|
||||
],
|
||||
};
|
||||
expect(error).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformImportError', () => {
|
||||
test('returns transformed object if it is a boom object', () => {
|
||||
const boom = new Boom('some boom message', { statusCode: 400 });
|
||||
const transformed = transformImportError('rule-1', boom, {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }],
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [
|
||||
{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } },
|
||||
{ rule_id: 'rule-1', error: { status_code: 400, message: 'some boom message' } },
|
||||
],
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns a normal error 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 = transformImportError('rule-1', error, {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }],
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [
|
||||
{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } },
|
||||
{ rule_id: 'rule-1', error: { status_code: 403, message: 'some message' } },
|
||||
],
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns a 500 if the status code is not set', () => {
|
||||
const error: Error & { statusCode?: number } = {
|
||||
name: 'some name',
|
||||
message: 'some message',
|
||||
};
|
||||
const transformed = transformImportError('rule-1', error, {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }],
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [
|
||||
{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } },
|
||||
{ rule_id: 'rule-1', error: { status_code: 500, message: 'some message' } },
|
||||
],
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it detects a TypeError and returns a Boom status of 400', () => {
|
||||
const error: TypeError = new TypeError('I have a type error');
|
||||
const transformed = transformImportError('rule-1', error, {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }],
|
||||
});
|
||||
const expected: ImportSuccessError = {
|
||||
success_count: 1,
|
||||
success: false,
|
||||
errors: [
|
||||
{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } },
|
||||
{ rule_id: 'rule-1', error: { status_code: 400, message: 'I have a type error' } },
|
||||
],
|
||||
};
|
||||
expect(transformed).toEqual(expected);
|
||||
});
|
||||
|
|
|
@ -27,12 +27,13 @@ export const transformError = (err: Error & { statusCode?: number }) => {
|
|||
};
|
||||
|
||||
export interface BulkError {
|
||||
id: string;
|
||||
rule_id: string;
|
||||
error: {
|
||||
statusCode: number;
|
||||
status_code: number;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const createBulkErrorObject = ({
|
||||
ruleId,
|
||||
statusCode,
|
||||
|
@ -43,14 +44,84 @@ export const createBulkErrorObject = ({
|
|||
message: string;
|
||||
}): BulkError => {
|
||||
return {
|
||||
id: ruleId,
|
||||
rule_id: ruleId,
|
||||
error: {
|
||||
statusCode,
|
||||
status_code: statusCode,
|
||||
message,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface ImportSuccessError {
|
||||
success: boolean;
|
||||
success_count: number;
|
||||
errors: BulkError[];
|
||||
}
|
||||
|
||||
export const createSuccessObject = (
|
||||
existingImportSuccessError: ImportSuccessError
|
||||
): ImportSuccessError => {
|
||||
return {
|
||||
success_count: existingImportSuccessError.success_count + 1,
|
||||
success: existingImportSuccessError.success,
|
||||
errors: existingImportSuccessError.errors,
|
||||
};
|
||||
};
|
||||
|
||||
export const createImportErrorObject = ({
|
||||
ruleId,
|
||||
statusCode,
|
||||
message,
|
||||
existingImportSuccessError,
|
||||
}: {
|
||||
ruleId: string;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
existingImportSuccessError: ImportSuccessError;
|
||||
}): ImportSuccessError => {
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
...existingImportSuccessError.errors,
|
||||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode,
|
||||
message,
|
||||
}),
|
||||
],
|
||||
success_count: existingImportSuccessError.success_count,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformImportError = (
|
||||
ruleId: string,
|
||||
err: Error & { statusCode?: number },
|
||||
existingImportSuccessError: ImportSuccessError
|
||||
): ImportSuccessError => {
|
||||
if (Boom.isBoom(err)) {
|
||||
return createImportErrorObject({
|
||||
ruleId,
|
||||
statusCode: err.output.statusCode,
|
||||
message: err.message,
|
||||
existingImportSuccessError,
|
||||
});
|
||||
} else if (err instanceof TypeError) {
|
||||
return createImportErrorObject({
|
||||
ruleId,
|
||||
statusCode: 400,
|
||||
message: err.message,
|
||||
existingImportSuccessError,
|
||||
});
|
||||
} else {
|
||||
return createImportErrorObject({
|
||||
ruleId,
|
||||
statusCode: err.statusCode ?? 500,
|
||||
message: err.message,
|
||||
existingImportSuccessError,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const transformBulkError = (
|
||||
ruleId: string,
|
||||
err: Error & { statusCode?: number }
|
||||
|
@ -76,13 +147,19 @@ export const transformBulkError = (
|
|||
}
|
||||
};
|
||||
|
||||
export const getIndex = (request: RequestFacade, server: ServerFacade): string => {
|
||||
export const getIndex = (
|
||||
request: RequestFacade | Omit<RequestFacade, 'query'>,
|
||||
server: ServerFacade
|
||||
): string => {
|
||||
const spaceId = server.plugins.spaces.getSpaceId(request);
|
||||
const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`);
|
||||
return `${signalsIndex}-${spaceId}`;
|
||||
};
|
||||
|
||||
export const callWithRequestFactory = (request: RequestFacade, server: ServerFacade) => {
|
||||
export const callWithRequestFactory = (
|
||||
request: RequestFacade | Omit<RequestFacade, 'query'>,
|
||||
server: ServerFacade
|
||||
) => {
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
|
||||
return <T, U>(endpoint: string, params: T, options?: U) => {
|
||||
return callWithRequest(request, endpoint, params, options);
|
||||
|
|
|
@ -0,0 +1,375 @@
|
|||
/*
|
||||
* 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 { Readable } from 'stream';
|
||||
import { createRulesStreamFromNdJson } from './create_rules_stream_from_ndjson';
|
||||
import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams';
|
||||
import { ImportRuleAlertRest } from '../types';
|
||||
|
||||
const readStreamToCompletion = (stream: Readable) => {
|
||||
return createPromiseFromStreams([stream, createConcatStream([])]);
|
||||
};
|
||||
|
||||
export const getOutputSample = (): Partial<ImportRuleAlertRest> => ({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
});
|
||||
|
||||
export const getSampleAsNdjson = (sample: Partial<ImportRuleAlertRest>): string => {
|
||||
return `${JSON.stringify(sample)}\n`;
|
||||
};
|
||||
|
||||
describe('create_rules_stream_from_ndjson', () => {
|
||||
describe('createRulesStreamFromNdJson', () => {
|
||||
test('transforms an ndjson stream into a stream of rule objects', async () => {
|
||||
const sample1 = getOutputSample();
|
||||
const sample2 = getOutputSample();
|
||||
sample2.rule_id = 'rule-2';
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(getSampleAsNdjson(sample1));
|
||||
this.push(getSampleAsNdjson(sample2));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const result = await readStreamToCompletion(rulesObjectsStream);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
rule_id: 'rule-2',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('skips empty lines', async () => {
|
||||
const sample1 = getOutputSample();
|
||||
const sample2 = getOutputSample();
|
||||
sample2.rule_id = 'rule-2';
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(getSampleAsNdjson(sample1));
|
||||
this.push('\n');
|
||||
this.push(getSampleAsNdjson(sample2));
|
||||
this.push('');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const result = await readStreamToCompletion(rulesObjectsStream);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
rule_id: 'rule-2',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('filters the export details entry from the stream', async () => {
|
||||
const sample1 = getOutputSample();
|
||||
const sample2 = getOutputSample();
|
||||
sample2.rule_id = 'rule-2';
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(getSampleAsNdjson(sample1));
|
||||
this.push(getSampleAsNdjson(sample2));
|
||||
this.push('{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const result = await readStreamToCompletion(rulesObjectsStream);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
rule_id: 'rule-2',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('handles non parsable JSON strings and inserts the error as part of the return array', async () => {
|
||||
const sample1 = getOutputSample();
|
||||
const sample2 = getOutputSample();
|
||||
sample2.rule_id = 'rule-2';
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(getSampleAsNdjson(sample1));
|
||||
this.push('{,,,,\n');
|
||||
this.push(getSampleAsNdjson(sample2));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const result = await readStreamToCompletion(rulesObjectsStream);
|
||||
const resultOrError = result as Error[];
|
||||
expect(resultOrError[0]).toEqual({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
});
|
||||
expect(resultOrError[1].message).toEqual('Unexpected token , in JSON at position 1');
|
||||
expect(resultOrError[2]).toEqual({
|
||||
rule_id: 'rule-2',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles non-validated data', async () => {
|
||||
const sample1 = getOutputSample();
|
||||
const sample2 = getOutputSample();
|
||||
sample2.rule_id = 'rule-2';
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(getSampleAsNdjson(sample1));
|
||||
this.push(`{}\n`);
|
||||
this.push(getSampleAsNdjson(sample2));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const result = await readStreamToCompletion(rulesObjectsStream);
|
||||
const resultOrError = result as TypeError[];
|
||||
expect(resultOrError[0]).toEqual({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
});
|
||||
expect(resultOrError[1].message).toEqual(
|
||||
'child "description" fails because ["description" is required]'
|
||||
);
|
||||
expect(resultOrError[2]).toEqual({
|
||||
rule_id: 'rule-2',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
immutable: false,
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
tags: [],
|
||||
threats: [],
|
||||
references: [],
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('non validated data is an instanceof TypeError', async () => {
|
||||
const sample1 = getOutputSample();
|
||||
const sample2 = getOutputSample();
|
||||
sample2.rule_id = 'rule-2';
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(getSampleAsNdjson(sample1));
|
||||
this.push(`{}\n`);
|
||||
this.push(getSampleAsNdjson(sample2));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const result = await readStreamToCompletion(rulesObjectsStream);
|
||||
const resultOrError = result as TypeError[];
|
||||
expect(resultOrError[1] instanceof TypeError).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { Readable, Transform } from 'stream';
|
||||
import { has, isString } from 'lodash/fp';
|
||||
import { ImportRuleAlertRest } from '../types';
|
||||
import {
|
||||
createSplitStream,
|
||||
createMapStream,
|
||||
createFilterStream,
|
||||
createConcatStream,
|
||||
} from '../../../../../../../../src/legacy/utils/streams';
|
||||
import { importRulesSchema } from '../routes/schemas/import_rules_schema';
|
||||
|
||||
export interface RulesObjectsExportResultDetails {
|
||||
/** number of successfully exported objects */
|
||||
exportedCount: number;
|
||||
}
|
||||
|
||||
export const parseNdjsonStrings = (): Transform => {
|
||||
return createMapStream((ndJsonStr: string) => {
|
||||
if (isString(ndJsonStr) && ndJsonStr.trim() !== '') {
|
||||
try {
|
||||
return JSON.parse(ndJsonStr);
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const filterExportedCounts = (): Transform => {
|
||||
return createFilterStream<ImportRuleAlertRest | RulesObjectsExportResultDetails>(
|
||||
obj => obj != null && !has('exported_count', obj)
|
||||
);
|
||||
};
|
||||
|
||||
export const validateRules = (): Transform => {
|
||||
return createMapStream((obj: ImportRuleAlertRest) => {
|
||||
if (!(obj instanceof Error)) {
|
||||
const validated = importRulesSchema.validate(obj);
|
||||
if (validated.error != null) {
|
||||
return new TypeError(validated.error.message);
|
||||
} else {
|
||||
return validated.value;
|
||||
}
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Adaptation from: saved_objects/import/create_limit_stream.ts
|
||||
export const createLimitStream = (limit: number): Transform => {
|
||||
let counter = 0;
|
||||
return new Transform({
|
||||
objectMode: true,
|
||||
async transform(obj, _, done) {
|
||||
if (counter >= limit) {
|
||||
return done(new Error(`Can't import more than ${limit} rules`));
|
||||
}
|
||||
counter++;
|
||||
done(undefined, obj);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: Capture both the line number and the rule_id if you have that information for the error message
|
||||
// eventually and then pass it down so we can give error messages on the line number
|
||||
|
||||
/**
|
||||
* Inspiration and the pattern of code followed is from:
|
||||
* saved_objects/lib/create_saved_objects_stream_from_ndjson.ts
|
||||
*/
|
||||
export const createRulesStreamFromNdJson = (
|
||||
ndJsonStream: Readable,
|
||||
ruleLimit: number
|
||||
): Transform => {
|
||||
return ndJsonStream
|
||||
.pipe(createSplitStream('\n'))
|
||||
.pipe(parseNdjsonStrings())
|
||||
.pipe(filterExportedCounts())
|
||||
.pipe(validateRules())
|
||||
.pipe(createLimitStream(ruleLimit))
|
||||
.pipe(createConcatStream([]));
|
||||
};
|
|
@ -4,8 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FindResult } from '../../../../../alerting/server/alerts_client';
|
||||
import { SIGNALS_ID } from '../../../../common/constants';
|
||||
import { FindRuleParams, RuleAlertType } from './types';
|
||||
import { FindRuleParams } from './types';
|
||||
|
||||
export const getFilter = (filter: string | null | undefined) => {
|
||||
if (filter == null) {
|
||||
|
@ -23,7 +24,7 @@ export const findRules = async ({
|
|||
filter,
|
||||
sortField,
|
||||
sortOrder,
|
||||
}: FindRuleParams) => {
|
||||
}: FindRuleParams): Promise<FindResult> => {
|
||||
return alertsClient.find({
|
||||
options: {
|
||||
fields,
|
||||
|
@ -33,10 +34,5 @@ export const findRules = async ({
|
|||
sortOrder,
|
||||
sortField,
|
||||
},
|
||||
}) as Promise<{
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
data: RuleAlertType[];
|
||||
}>;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -11,7 +11,13 @@ import {
|
|||
getFindResultWithSingleHit,
|
||||
getFindResultWithMultiHits,
|
||||
} from '../routes/__mocks__/request_responses';
|
||||
import { getExistingPrepackagedRules } from './get_existing_prepackaged_rules';
|
||||
import {
|
||||
getExistingPrepackagedRules,
|
||||
getNonPackagedRules,
|
||||
getRules,
|
||||
getRulesCount,
|
||||
getNonPackagedRulesCount,
|
||||
} from './get_existing_prepackaged_rules';
|
||||
|
||||
describe('get_existing_prepackaged_rules', () => {
|
||||
afterEach(() => {
|
||||
|
@ -29,6 +35,108 @@ describe('get_existing_prepackaged_rules', () => {
|
|||
expect(rules).toEqual([getResult()]);
|
||||
});
|
||||
|
||||
test('should return 2 items over two pages, one per page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
const result1 = getResult();
|
||||
result1.params.immutable = true;
|
||||
result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result2 = getResult();
|
||||
result2.params.immutable = true;
|
||||
result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 })
|
||||
);
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 })
|
||||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getExistingPrepackagedRules({
|
||||
alertsClient: unsafeCast,
|
||||
});
|
||||
expect(rules).toEqual([result1, result2]);
|
||||
});
|
||||
|
||||
test('should return 3 items with over 3 pages one per page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
const result1 = getResult();
|
||||
result1.params.immutable = true;
|
||||
result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result2 = getResult();
|
||||
result2.params.immutable = true;
|
||||
result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result3 = getResult();
|
||||
result3.params.immutable = true;
|
||||
result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a';
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 })
|
||||
);
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 })
|
||||
);
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 })
|
||||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getExistingPrepackagedRules({
|
||||
alertsClient: unsafeCast,
|
||||
});
|
||||
expect(rules).toEqual([result1, result2, result3]);
|
||||
});
|
||||
|
||||
test('should return 3 items over 1 pages with all on one page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
const result1 = getResult();
|
||||
result1.params.immutable = true;
|
||||
result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result2 = getResult();
|
||||
result2.params.immutable = true;
|
||||
result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result3 = getResult();
|
||||
result3.params.immutable = true;
|
||||
result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a';
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({
|
||||
data: [result1, result2, result3],
|
||||
perPage: 3,
|
||||
page: 1,
|
||||
total: 3,
|
||||
})
|
||||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getExistingPrepackagedRules({
|
||||
alertsClient: unsafeCast,
|
||||
});
|
||||
expect(rules).toEqual([result1, result2, result3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNonPackagedRules', () => {
|
||||
test('should return a single item in a single page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getNonPackagedRules({
|
||||
alertsClient: unsafeCast,
|
||||
});
|
||||
expect(rules).toEqual([getResult()]);
|
||||
});
|
||||
|
||||
test('should return 2 items over two pages, one per page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
|
@ -46,7 +154,7 @@ describe('get_existing_prepackaged_rules', () => {
|
|||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getExistingPrepackagedRules({
|
||||
const rules = await getNonPackagedRules({
|
||||
alertsClient: unsafeCast,
|
||||
});
|
||||
expect(rules).toEqual([result1, result2]);
|
||||
|
@ -77,7 +185,7 @@ describe('get_existing_prepackaged_rules', () => {
|
|||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getExistingPrepackagedRules({
|
||||
const rules = await getNonPackagedRules({
|
||||
alertsClient: unsafeCast,
|
||||
});
|
||||
expect(rules).toEqual([result1, result2, result3]);
|
||||
|
@ -105,10 +213,133 @@ describe('get_existing_prepackaged_rules', () => {
|
|||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getExistingPrepackagedRules({
|
||||
const rules = await getNonPackagedRules({
|
||||
alertsClient: unsafeCast,
|
||||
});
|
||||
expect(rules).toEqual([result1, result2, result3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRules', () => {
|
||||
test('should return a single item in a single page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getRules({
|
||||
alertsClient: unsafeCast,
|
||||
filter: '',
|
||||
});
|
||||
expect(rules).toEqual([getResult()]);
|
||||
});
|
||||
|
||||
test('should return 2 items over two pages, one per page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
const result1 = getResult();
|
||||
result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result2 = getResult();
|
||||
result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 })
|
||||
);
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 })
|
||||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getRules({
|
||||
alertsClient: unsafeCast,
|
||||
filter: '',
|
||||
});
|
||||
expect(rules).toEqual([result1, result2]);
|
||||
});
|
||||
|
||||
test('should return 3 items with over 3 pages one per page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
const result1 = getResult();
|
||||
result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result2 = getResult();
|
||||
result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result3 = getResult();
|
||||
result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a';
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 })
|
||||
);
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 })
|
||||
);
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 })
|
||||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getRules({
|
||||
alertsClient: unsafeCast,
|
||||
filter: '',
|
||||
});
|
||||
expect(rules).toEqual([result1, result2, result3]);
|
||||
});
|
||||
|
||||
test('should return 3 items over 1 pages with all on one page', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
const result1 = getResult();
|
||||
result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result2 = getResult();
|
||||
result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d';
|
||||
|
||||
const result3 = getResult();
|
||||
result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a';
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(
|
||||
getFindResultWithMultiHits({
|
||||
data: [result1, result2, result3],
|
||||
perPage: 3,
|
||||
page: 1,
|
||||
total: 3,
|
||||
})
|
||||
);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getRules({
|
||||
alertsClient: unsafeCast,
|
||||
filter: '',
|
||||
});
|
||||
expect(rules).toEqual([result1, result2, result3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRulesCount', () => {
|
||||
test('it returns a count', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getRulesCount({
|
||||
alertsClient: unsafeCast,
|
||||
filter: '',
|
||||
});
|
||||
expect(rules).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNonPackagedRulesCount', () => {
|
||||
test('it returns a count', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const rules = await getNonPackagedRulesCount({
|
||||
alertsClient: unsafeCast,
|
||||
});
|
||||
expect(rules).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,18 +9,46 @@ import { AlertsClient } from '../../../../../alerting';
|
|||
import { RuleAlertType, isAlertTypes } from './types';
|
||||
import { findRules } from './find_rules';
|
||||
|
||||
export const DEFAULT_PER_PAGE: number = 100;
|
||||
export const DEFAULT_PER_PAGE = 100;
|
||||
export const FILTER_NON_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`;
|
||||
export const FILTER_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`;
|
||||
|
||||
export const getExistingPrepackagedRules = async ({
|
||||
export const getNonPackagedRulesCount = async ({
|
||||
alertsClient,
|
||||
}: {
|
||||
alertsClient: AlertsClient;
|
||||
}): Promise<number> => {
|
||||
return getRulesCount({ alertsClient, filter: FILTER_NON_PREPACKED_RULES });
|
||||
};
|
||||
|
||||
export const getRulesCount = async ({
|
||||
alertsClient,
|
||||
filter,
|
||||
}: {
|
||||
alertsClient: AlertsClient;
|
||||
filter: string;
|
||||
}): Promise<number> => {
|
||||
const firstRule = await findRules({
|
||||
alertsClient,
|
||||
filter,
|
||||
perPage: 1,
|
||||
page: 1,
|
||||
});
|
||||
return firstRule.total;
|
||||
};
|
||||
|
||||
export const getRules = async ({
|
||||
alertsClient,
|
||||
perPage = DEFAULT_PER_PAGE,
|
||||
filter,
|
||||
}: {
|
||||
alertsClient: AlertsClient;
|
||||
perPage?: number;
|
||||
filter: string;
|
||||
}): Promise<RuleAlertType[]> => {
|
||||
const firstPrepackedRules = await findRules({
|
||||
alertsClient,
|
||||
filter: `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`,
|
||||
filter,
|
||||
perPage,
|
||||
page: 1,
|
||||
});
|
||||
|
@ -40,7 +68,7 @@ export const getExistingPrepackagedRules = async ({
|
|||
// page index starts at 2 as we already got the first page and we have more pages to go
|
||||
return findRules({
|
||||
alertsClient,
|
||||
filter: `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`,
|
||||
filter,
|
||||
perPage,
|
||||
page: page + 2,
|
||||
});
|
||||
|
@ -58,3 +86,31 @@ export const getExistingPrepackagedRules = async ({
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getNonPackagedRules = async ({
|
||||
alertsClient,
|
||||
perPage = DEFAULT_PER_PAGE,
|
||||
}: {
|
||||
alertsClient: AlertsClient;
|
||||
perPage?: number;
|
||||
}): Promise<RuleAlertType[]> => {
|
||||
return getRules({
|
||||
alertsClient,
|
||||
perPage,
|
||||
filter: FILTER_NON_PREPACKED_RULES,
|
||||
});
|
||||
};
|
||||
|
||||
export const getExistingPrepackagedRules = async ({
|
||||
alertsClient,
|
||||
perPage = DEFAULT_PER_PAGE,
|
||||
}: {
|
||||
alertsClient: AlertsClient;
|
||||
perPage?: number;
|
||||
}): Promise<RuleAlertType[]> => {
|
||||
return getRules({
|
||||
alertsClient,
|
||||
perPage,
|
||||
filter: FILTER_PREPACKED_RULES,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock';
|
||||
import {
|
||||
getResult,
|
||||
getFindResultWithSingleHit,
|
||||
FindHit,
|
||||
} from '../routes/__mocks__/request_responses';
|
||||
import { AlertsClient } from '../../../../../alerting';
|
||||
import { getExportAll } from './get_export_all';
|
||||
|
||||
describe('getExportAll', () => {
|
||||
test('it exports everything from the alerts client', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const exports = await getExportAll(unsafeCast);
|
||||
expect(exports).toEqual({
|
||||
rulesNdjson:
|
||||
'{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threats":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"techniques":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n',
|
||||
exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n',
|
||||
});
|
||||
});
|
||||
|
||||
test('it will export empty rules', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
const findResult: FindHit = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
|
||||
alertsClient.find.mockResolvedValue(findResult);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const exports = await getExportAll(unsafeCast);
|
||||
expect(exports).toEqual({
|
||||
rulesNdjson: '',
|
||||
exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { AlertsClient } from '../../../../../alerting';
|
||||
import { getNonPackagedRules } from './get_existing_prepackaged_rules';
|
||||
import { getExportDetailsNdjson } from './get_export_details_ndjson';
|
||||
import { transformAlertsToRules, transformRulesToNdjson } from '../routes/rules/utils';
|
||||
|
||||
export const getExportAll = async (
|
||||
alertsClient: AlertsClient
|
||||
): Promise<{
|
||||
rulesNdjson: string;
|
||||
exportDetails: string;
|
||||
}> => {
|
||||
const ruleAlertTypes = await getNonPackagedRules({ alertsClient });
|
||||
const rules = transformAlertsToRules(ruleAlertTypes);
|
||||
const rulesNdjson = transformRulesToNdjson(rules);
|
||||
const exportDetails = getExportDetailsNdjson(rules);
|
||||
return { rulesNdjson, exportDetails };
|
||||
};
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* 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 { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock';
|
||||
import { getExportByObjectIds, getRulesFromObjects, RulesErrors } from './get_export_by_object_ids';
|
||||
import {
|
||||
getResult,
|
||||
getFindResultWithSingleHit,
|
||||
FindHit,
|
||||
} from '../routes/__mocks__/request_responses';
|
||||
import { AlertsClient } from '../../../../../alerting';
|
||||
|
||||
describe('get_export_by_object_ids', () => {
|
||||
describe('getExportByObjectIds', () => {
|
||||
test('it exports object ids into an expected string with new line characters', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const objects = [{ rule_id: 'rule-1' }];
|
||||
const exports = await getExportByObjectIds(unsafeCast, objects);
|
||||
expect(exports).toEqual({
|
||||
rulesNdjson:
|
||||
'{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threats":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"techniques":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n',
|
||||
exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n',
|
||||
});
|
||||
});
|
||||
|
||||
test('it does not export immutable rules', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
const result = getResult();
|
||||
result.params.immutable = true;
|
||||
|
||||
const findResult: FindHit = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [result],
|
||||
};
|
||||
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
alertsClient.find.mockResolvedValue(findResult);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const objects = [{ rule_id: 'rule-1' }];
|
||||
const exports = await getExportByObjectIds(unsafeCast, objects);
|
||||
expect(exports).toEqual({
|
||||
rulesNdjson: '',
|
||||
exportDetails:
|
||||
'{"exported_count":0,"missing_rules":[{"rule_id":"rule-1"}],"missing_rules_count":1}\n',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRulesFromObjects', () => {
|
||||
test('it returns transformed rules from objects sent in', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const objects = [{ rule_id: 'rule-1' }];
|
||||
const exports = await getRulesFromObjects(unsafeCast, objects);
|
||||
const expected: RulesErrors = {
|
||||
missingRules: [],
|
||||
rules: [
|
||||
{
|
||||
created_at: '2019-12-13T16:40:33.400Z',
|
||||
updated_at: '2019-12-13T16:40:33.400Z',
|
||||
created_by: 'elastic',
|
||||
description: 'Detecting root and admin users',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }],
|
||||
from: 'now-6m',
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
immutable: false,
|
||||
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
interval: '5m',
|
||||
rule_id: 'rule-1',
|
||||
language: 'kuery',
|
||||
output_index: '.siem-signals',
|
||||
max_signals: 100,
|
||||
risk_score: 50,
|
||||
name: 'Detect Root/Admin Users',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
references: ['http://www.example.com', 'https://ww.example.com'],
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
meta: { someMeta: 'someField' },
|
||||
severity: 'high',
|
||||
updated_by: 'elastic',
|
||||
tags: [],
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
threats: [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic: {
|
||||
id: 'TA0040',
|
||||
name: 'impact',
|
||||
reference: 'https://attack.mitre.org/tactics/TA0040/',
|
||||
},
|
||||
techniques: [
|
||||
{
|
||||
id: 'T1499',
|
||||
name: 'endpoint denial of service',
|
||||
reference: 'https://attack.mitre.org/techniques/T1499/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(exports).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it does not transform the rule if the rule is an immutable rule and designates it as a missing rule', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
const result = getResult();
|
||||
result.params.immutable = true;
|
||||
|
||||
const findResult: FindHit = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [result],
|
||||
};
|
||||
|
||||
alertsClient.get.mockResolvedValue(result);
|
||||
alertsClient.find.mockResolvedValue(findResult);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const objects = [{ rule_id: 'rule-1' }];
|
||||
const exports = await getRulesFromObjects(unsafeCast, objects);
|
||||
const expected: RulesErrors = {
|
||||
missingRules: [{ rule_id: 'rule-1' }],
|
||||
rules: [],
|
||||
};
|
||||
expect(exports).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it exports missing rules', async () => {
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
const findResult: FindHit = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
|
||||
alertsClient.get.mockRejectedValue({ output: { statusCode: 404 } });
|
||||
alertsClient.find.mockResolvedValue(findResult);
|
||||
|
||||
const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient;
|
||||
const objects = [{ rule_id: 'rule-1' }];
|
||||
const exports = await getRulesFromObjects(unsafeCast, objects);
|
||||
const expected: RulesErrors = {
|
||||
missingRules: [{ rule_id: 'rule-1' }],
|
||||
rules: [],
|
||||
};
|
||||
expect(exports).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { AlertsClient } from '../../../../../alerting';
|
||||
import { getExportDetailsNdjson } from './get_export_details_ndjson';
|
||||
import { isAlertType } from '../rules/types';
|
||||
import { readRules } from './read_rules';
|
||||
import { transformRulesToNdjson, transformAlertToRule } from '../routes/rules/utils';
|
||||
import { OutputRuleAlertRest } from '../types';
|
||||
|
||||
export interface RulesErrors {
|
||||
missingRules: Array<{ rule_id: string }>;
|
||||
rules: Array<Partial<OutputRuleAlertRest>>;
|
||||
}
|
||||
|
||||
export const getExportByObjectIds = async (
|
||||
alertsClient: AlertsClient,
|
||||
objects: Array<{ rule_id: string }>
|
||||
): Promise<{
|
||||
rulesNdjson: string;
|
||||
exportDetails: string;
|
||||
}> => {
|
||||
const rulesAndErrors = await getRulesFromObjects(alertsClient, objects);
|
||||
const rulesNdjson = transformRulesToNdjson(rulesAndErrors.rules);
|
||||
const exportDetails = getExportDetailsNdjson(rulesAndErrors.rules, rulesAndErrors.missingRules);
|
||||
return { rulesNdjson, exportDetails };
|
||||
};
|
||||
|
||||
export const getRulesFromObjects = async (
|
||||
alertsClient: AlertsClient,
|
||||
objects: Array<{ rule_id: string }>
|
||||
): Promise<RulesErrors> => {
|
||||
const alertsAndErrors = await objects.reduce<Promise<RulesErrors>>(
|
||||
async (accumPromise, object) => {
|
||||
const accum = await accumPromise;
|
||||
const rule = await readRules({ alertsClient, ruleId: object.rule_id });
|
||||
if (rule != null && isAlertType(rule) && rule.params.immutable !== true) {
|
||||
const transformedRule = transformAlertToRule(rule);
|
||||
return {
|
||||
missingRules: accum.missingRules,
|
||||
rules: [...accum.rules, transformedRule],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
missingRules: [...accum.missingRules, { rule_id: object.rule_id }],
|
||||
rules: accum.rules,
|
||||
};
|
||||
}
|
||||
},
|
||||
Promise.resolve({
|
||||
exportedCount: 0,
|
||||
missingRules: [],
|
||||
rules: [],
|
||||
})
|
||||
);
|
||||
return alertsAndErrors;
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { sampleRule } from '../signals/__mocks__/es_results';
|
||||
import { getExportDetailsNdjson } from './get_export_details_ndjson';
|
||||
|
||||
describe('getExportDetailsNdjson', () => {
|
||||
test('it ends with a new line character', () => {
|
||||
const rule = sampleRule();
|
||||
const details = getExportDetailsNdjson([rule]);
|
||||
expect(details.endsWith('\n')).toEqual(true);
|
||||
});
|
||||
|
||||
test('it exports a correct count given a single rule and no missing rules', () => {
|
||||
const rule = sampleRule();
|
||||
const details = getExportDetailsNdjson([rule]);
|
||||
const reParsed = JSON.parse(details);
|
||||
expect(reParsed).toEqual({
|
||||
exported_count: 1,
|
||||
missing_rules: [],
|
||||
missing_rules_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('it exports a correct count given a no rules and a single missing rule', () => {
|
||||
const missingRule = { rule_id: 'rule-1' };
|
||||
const details = getExportDetailsNdjson([], [missingRule]);
|
||||
const reParsed = JSON.parse(details);
|
||||
expect(reParsed).toEqual({
|
||||
exported_count: 0,
|
||||
missing_rules: [{ rule_id: 'rule-1' }],
|
||||
missing_rules_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('it exports a correct count given multiple rules and multiple missing rules', () => {
|
||||
const rule1 = sampleRule();
|
||||
const rule2 = sampleRule();
|
||||
rule2.rule_id = 'some other id';
|
||||
rule2.id = 'some other id';
|
||||
|
||||
const missingRule1 = { rule_id: 'rule-1' };
|
||||
const missingRule2 = { rule_id: 'rule-2' };
|
||||
|
||||
const details = getExportDetailsNdjson([rule1, rule2], [missingRule1, missingRule2]);
|
||||
const reParsed = JSON.parse(details);
|
||||
expect(reParsed).toEqual({
|
||||
exported_count: 2,
|
||||
missing_rules: [missingRule1, missingRule2],
|
||||
missing_rules_count: 2,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 '../types';
|
||||
|
||||
export const getExportDetailsNdjson = (
|
||||
rules: Array<Partial<OutputRuleAlertRest>>,
|
||||
missingRules: Array<{ rule_id: string }> = []
|
||||
): string => {
|
||||
const stringified = JSON.stringify({
|
||||
exported_count: rules.length,
|
||||
missing_rules: missingRules,
|
||||
missing_rules_count: missingRules.length,
|
||||
});
|
||||
return `${stringified}\n`;
|
||||
};
|
|
@ -4,9 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Alert } from '../../../../../alerting/server/types';
|
||||
import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants';
|
||||
import { findRules } from './find_rules';
|
||||
import { RuleAlertType, ReadRuleParams, isAlertType } from './types';
|
||||
import { ReadRuleParams, isAlertType } from './types';
|
||||
|
||||
/**
|
||||
* This reads the rules through a cascade try of what is fastest to what is slowest.
|
||||
|
@ -20,7 +21,7 @@ export const readRules = async ({
|
|||
alertsClient,
|
||||
id,
|
||||
ruleId,
|
||||
}: ReadRuleParams): Promise<RuleAlertType | null> => {
|
||||
}: ReadRuleParams): Promise<Alert | null> => {
|
||||
if (id != null) {
|
||||
try {
|
||||
const rule = await alertsClient.get({ id });
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash/fp';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { SIGNALS_ID } from '../../../../common/constants';
|
||||
import { AlertsClient } from '../../../../../alerting/server/alerts_client';
|
||||
|
@ -47,6 +48,24 @@ export interface BulkRulesRequest extends RequestFacade {
|
|||
payload: RuleAlertParamsRest[];
|
||||
}
|
||||
|
||||
export interface HapiReadableStream extends Readable {
|
||||
hapi: {
|
||||
filename: string;
|
||||
};
|
||||
}
|
||||
export interface ImportRulesRequest extends Omit<RequestFacade, 'query'> {
|
||||
query: { overwrite: boolean };
|
||||
payload: { file: HapiReadableStream };
|
||||
}
|
||||
|
||||
export interface ExportRulesRequest extends Omit<RequestFacade, 'query'> {
|
||||
payload: { objects: Array<{ rule_id: string }> | null | undefined };
|
||||
query: {
|
||||
file_name: string;
|
||||
exclude_export_details: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type QueryRequest = Omit<RequestFacade, 'query'> & {
|
||||
query: { id: string | undefined; rule_id: string | undefined };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
#!/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
|
||||
|
||||
EXCLUDE_DETAILS=${1:-false}
|
||||
|
||||
# Note: This file does not use jq on purpose for testing and pipe redirections
|
||||
|
||||
# Example get all the rules except pre-packaged rules
|
||||
# ./export_rules.sh
|
||||
|
||||
# Example get the export details at the end
|
||||
# ./export_rules.sh false
|
||||
|
||||
curl -s -k \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_export?exclude_export_details=${EXCLUDE_DETAILS}
|
|
@ -0,0 +1,29 @@
|
|||
#!/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/export/ruleid_queries.json}
|
||||
EXCLUDE_DETAILS=${2:-false}
|
||||
|
||||
# Note: This file does not use jq on purpose for testing and pipe redirections
|
||||
|
||||
# Example get all the rules except pre-packaged rules
|
||||
# ./export_rules_by_rule_id.sh
|
||||
|
||||
# Example get the export details at the end
|
||||
# ./export_rules_by_rule_id.sh ./rules/export/ruleid_queries.json false
|
||||
curl -s -k \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_export?exclude_export_details=${EXCLUDE_DETAILS} \
|
||||
-d @${RULES}
|
|
@ -0,0 +1,27 @@
|
|||
#!/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 defaults if no arguments are specified
|
||||
RULES=${1:-./rules/export/ruleid_queries.json}
|
||||
FILENAME=${2:-test.ndjson}
|
||||
EXCLUDE_DETAILS=${3:-false}
|
||||
|
||||
# Example export to the file named test.ndjson
|
||||
# ./export_rules_by_rule_id_to_file.sh
|
||||
|
||||
# Example export to the file named test.ndjson with export details appended
|
||||
# ./export_rules_by_rule_id_to_file.sh ./rules/export/ruleid_queries.json test.ndjson false
|
||||
curl -s -k -OJ \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_export?exclude_export_details=${EXCLUDE_DETAILS}&file_name=${FILENAME}" \
|
||||
-d @${RULES}
|
|
@ -0,0 +1,23 @@
|
|||
#!/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
|
||||
|
||||
FILENAME=${1:-test.ndjson}
|
||||
EXCLUDE_DETAILS=${2:-false}
|
||||
|
||||
# Example export to the file named test.ndjson
|
||||
# ./export_rules_to_file.sh
|
||||
|
||||
# Example export to the file named test.ndjson with export details appended
|
||||
# ./export_rules_to_file.sh test.ndjson false
|
||||
curl -s -k -OJ \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_export?exclude_export_details=${EXCLUDE_DETAILS}&file_name=${FILENAME}"
|
|
@ -0,0 +1,26 @@
|
|||
#!/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 defaults if no argument is specified
|
||||
RULES=${1:-./rules/import/multiple_ruleid_queries.ndjson}
|
||||
OVERWRITE=${2:-true}
|
||||
|
||||
# Example to import and overwrite everything from ./rules/import/multiple_ruleid_queries.ndjson
|
||||
# ./import_rules.sh
|
||||
|
||||
# Example to not overwrite everything if it exists from ./rules/import/multiple_ruleid_queries.ndjson
|
||||
# ./import_rules.sh ./rules/import/multiple_ruleid_queries.ndjson false
|
||||
curl -s -k \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_import?overwrite=${OVERWRITE}" \
|
||||
--form file=@${RULES} \
|
||||
| jq .;
|
|
@ -0,0 +1,21 @@
|
|||
#!/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/import/multiple_ruleid_queries.ndjson}
|
||||
|
||||
# Example: ./import_rules_no_overwrite.sh
|
||||
curl -s -k \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_import \
|
||||
--form file=@${RULES} \
|
||||
| jq .;
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"objects": [
|
||||
{
|
||||
"rule_id": "query-rule-id-1"
|
||||
},
|
||||
{
|
||||
"rule_id": "query-rule-id-2"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{"created_at":"2020-01-09T01:38:00.740Z","updated_at":"2020-01-09T01:38:00.740Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"6688f367-1aa2-4895-a5a8-b3701eecf57d","immutable":false,"interval":"5m","rule_id":"query-rule-id-1","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":1,"name":"Query with a rule id Number 1","query":"user.name: root or user.name: admin","references":[],"severity":"high","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1}
|
||||
{"created_at":"2020-01-09T01:38:00.745Z","updated_at":"2020-01-09T01:38:00.745Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"7a912444-6cfa-4c8f-83f4-2b26fb2a2ed9","immutable":false,"interval":"5m","rule_id":"query-rule-id-2","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":2,"name":"Query with a rule id Number 2","query":"user.name: root or user.name: admin","references":[],"severity":"low","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1}
|
||||
{"exported_count":2,"missing_rules":[],"missing_rules_count":0}
|
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
],
|
||||
"enabled": false,
|
||||
"immutable": true,
|
||||
"immutable": false,
|
||||
"index": ["auditbeat-*", "filebeat-*"],
|
||||
"interval": "5m",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{"created_at":"2020-01-09T01:38:00.740Z","updated_at":"2020-01-09T01:38:00.740Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"6688f367-1aa2-4895-a5a8-b3701eecf57d","immutable":false,"interval":"5m","rule_id":"query-rule-id-1","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":1,"name":"Query with a rule id Number 1","query":"user.name: root or user.name: admin","references":[],"severity":"high","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1},
|
||||
{"created_at":"2020-01-09T01:38:00.745Z","updated_at":"2020-01-09T01:38:00.745Z","created_by":"elastic_kibana","enabled":true,"false_positives":[],"from":"now-6m","id":"7a912444-6cfa-4c8f-83f4-2b26fb2a2ed9","immutable":false,"interval":"5m","rule_id":"query-rule-id-2","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":2,"name":"Query with a rule id Number 2","query":"user.name: root or user.name: admin","references":[],"severity":"low","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1}
|
||||
{"created_at":"2020-01-09T01:38:00.745Z","updated_at":"2020-01-09T01:38:00.745Z","created_by":"elastic_kibana","description":"Query with a rule_id that acts like an external id","enabled":true,"false_positives":[],"from":"now-6m","id":"7a912444-6cfa-4c8f-83f4-2b26fb2a2ed9","immutable":false,"interval":"5m","rule_id":"query-rule-id-3","language":"kuery","output_index":".siem-signals-frank-hassanabad-default","max_signals":100,"risk_score":2,"name":"Query with a rule id Number 2","query":"user.name: root or user.name: admin","references":[],"severity":"low","updated_by":"elastic_kibana","tags":[],"to":"now","type":"query","threats":[],"version":1}
|
||||
{"exported_count":2,"missing_rules":[],"missing_rules_count":0}
|
|
@ -84,4 +84,9 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & {
|
|||
updated_by: string | undefined | null;
|
||||
};
|
||||
|
||||
export type ImportRuleAlertRest = Omit<OutputRuleAlertRest, 'rule_id' | 'id'> & {
|
||||
id: string | undefined | null;
|
||||
rule_id: string;
|
||||
};
|
||||
|
||||
export type CallWithRequest<T, U, V> = (endpoint: string, params: T, options?: U) => Promise<V>;
|
||||
|
|
Loading…
Reference in a new issue