[Security Solution][API testing] Move and restructures Basic detection engine tests (#171531)

## Summary

Following the initial work in this
https://github.com/elastic/kibana/pull/166755
- Addresses part of https://github.com/elastic/kibana/issues/151902 for
basic detection engine-related tests
- Introduced a new license folder to hold the `Basic` Ess tests and the
`Essentials` Serverless tests, is called `basic_essentials_license`
- Added new base configurations files for
`serverless/config.base.essentials` and `ess/config.base.basic`
- Moved the utility files associated with Basic tests to the new
directory `security_solution_api_integration`. Files not actively used
in the previous folder were moved, while duplicate files remained in
their original positions.
- Updated the CodeOwner file for the newly moved tests
- Old/new group details, decisions, and execution time are mentioned in
this
[document](https://docs.google.com/document/d/1CRFfDWMzw3ob03euWIvT4-IoiLXjoiPWI8mTBqP4Zks/edit)
- The **Privileges** Tests are skipped in Serverless now until the FTR
Roles [PR](https://github.com/elastic/kibana/pull/170131) gets merged

| Action | File | New Path  |
|--------|------|----------|
| Moved|basic/create_rules|
basic_essentials_license/detection_engine/rules/create_rules|
| Moved|basic/create_rules|
basic_essentials_license/detection_engine/rules/create_ml_rules_privileges|
| Moved|basic/create_rules|
basic_essentials_license/detection_engine/alerts/open_close_alerts|
| Moved|basic/create_rules|
basic_essentials_license/detection_engine/alerts/query_alerts_backword_compatibility|
| Moved|basic/create_rules|
basic_essentials_license/detection_engine/alerts/query_alerts|
This commit is contained in:
Wafaa Nasr 2023-11-23 09:03:50 +01:00 committed by GitHub
parent 11acc025b5
commit 5fa20cc3a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 9616 additions and 173 deletions

View file

@ -14,7 +14,9 @@ disabled:
- x-pack/test/api_integration/config.ts
- x-pack/test/fleet_api_integration/config.base.ts
- x-pack/test/security_solution_api_integration/config/ess/config.base.ts
- x-pack/test/security_solution_api_integration/config/ess/config.base.basic.ts
- x-pack/test/security_solution_api_integration/config/serverless/config.base.ts
- x-pack/test/security_solution_api_integration/config/serverless/config.base.essentials.ts
- x-pack/test/security_solution_endpoint/config.base.ts
- x-pack/test/security_solution_endpoint_api_int/config.base.ts
@ -486,3 +488,6 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/user_roles/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/telemetry/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/telemetry/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/basic_essentials_license/detection_engine/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/basic_essentials_license/detection_engine/configs/ess.config.ts

1
.github/CODEOWNERS vendored
View file

@ -1395,6 +1395,7 @@ x-pack/test/security_solution_api_integration/test_suites/detections_response/de
x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions @elastic/security-detection-engine
x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts @elastic/security-detection-engine
x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/user_roles @elastic/security-detection-engine
x-pack/test/security_solution_api_integration/test_suites/detections_response/basic_essentials_license/detection_engine @elastic/security-detection-engine
/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users @elastic/security-detection-engine
## Security Threat Intelligence - Under Security Platform

View file

@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default ({ loadTestFile }: FtrProviderContext): void => {
describe('detection engine api basic license', function () {
loadTestFile(require.resolve('./create_rules'));
loadTestFile(require.resolve('./create_rules_bulk'));
loadTestFile(require.resolve('./delete_rules'));
loadTestFile(require.resolve('./delete_rules_bulk'));
@ -22,8 +21,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./update_rules_bulk'));
loadTestFile(require.resolve('./patch_rules_bulk'));
loadTestFile(require.resolve('./patch_rules'));
loadTestFile(require.resolve('./query_signals'));
loadTestFile(require.resolve('./open_close_signals'));
loadTestFile(require.resolve('./import_timelines'));
loadTestFile(require.resolve('./coverage_overview'));
});

View file

@ -44,7 +44,6 @@ export * from './get_rule_for_signal_testing_with_timestamp_override';
export * from './get_rule_with_web_hook_action';
export * from './get_rule_with_legacy_investigation_fields';
export * from './get_saved_query_rule_for_signal_testing';
export * from './get_signal_status';
export * from './get_signals_by_id';
export * from './get_signals_by_ids';
export * from './get_signals_by_rule_ids';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createTestConfig } from './config.base';
export default createTestConfig({
license: 'basic',
ssl: true,
});

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrConfigProviderContext } from '@kbn/test';
export interface CreateTestConfigOptions {
testFiles: string[];
junit: { reportName: string };
kbnTestServerArgs?: string[];
kbnTestServerEnv?: Record<string, string>;
}
import { services } from '../../../../test_serverless/api_integration/services';
export function createTestConfig(options: CreateTestConfigOptions) {
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const svlSharedConfig = await readConfigFile(
require.resolve('../../../../test_serverless/shared/config.base.ts')
);
return {
...svlSharedConfig.getAll(),
services: {
...services,
},
kbnTestServer: {
...svlSharedConfig.get('kbnTestServer'),
serverArgs: [
...svlSharedConfig.get('kbnTestServer.serverArgs'),
'--serverless=security',
`--xpack.securitySolutionServerless.productTypes=${JSON.stringify([
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
])}`,
...(options.kbnTestServerArgs || []),
],
env: {
...svlSharedConfig.get('kbnTestServer.env'),
...options.kbnTestServerEnv,
},
},
testFiles: options.testFiles,
junit: options.junit,
mochaOpts: {
...svlSharedConfig.get('mochaOpts'),
grep: '/^(?!.*@brokenInServerless).*@serverless.*/',
},
};
};
}

View file

@ -7,6 +7,8 @@
"scripts": {
"initialize-server:dr:default": "node ./scripts/index.js server detections_response default_license",
"run-tests:dr:default": "node ./scripts/index.js runner detections_response default_license",
"initialize-server:dr:basicEssentials": "node ./scripts/index.js server detections_response basic_essentials_license",
"run-tests:dr:basicEssentials": "node ./scripts/index.js runner detections_response basic_essentials_license",
"initialize-server:ea:default": "node ./scripts/index.js server entity_analytics default_license",
"run-tests:ea:default": "node ./scripts/index.js runner entity_analytics default_license",
"exception_workflows:server:serverless": "npm run initialize-server:dr:default exceptions/workflows serverless",
@ -98,6 +100,11 @@
"telemetry:runner:serverless": "npm run run-tests:dr:default telemetry serverless serverlessEnv",
"telemetry:qa:serverless": "npm run run-tests:dr:default telemetry serverless qaEnv",
"telemetry:server:ess": "npm run initialize-server:dr:default telemetry ess",
"telemetry:runner:ess": "npm run run-tests:dr:default telemetry ess essEnv"
"telemetry:runner:ess": "npm run run-tests:dr:default telemetry ess essEnv",
"detection_engine_basicessentionals:server:serverless": "npm run initialize-server:dr:basicEssentials detection_engine serverless",
"detection_engine_basicessentionals:runner:serverless": "npm run run-tests:dr:basicEssentials detection_engine serverless serverlessEnv",
"detection_engine_basicessentionals:qa:serverless": "npm run run-tests:dr:basicEssentials detection_engine serverless qaEnv",
"detection_engine_basicessentionals:server:ess": "npm run initialize-server:dr:basicEssentials detection_engine ess",
"detection_engine_basicessentionals:runner:ess": "npm run run-tests:dr:basicEssentials detection_engine ess essEnv"
}
}

View file

@ -14,40 +14,45 @@ import {
DETECTION_ENGINE_QUERY_SIGNALS_URL,
} from '@kbn/security-solution-plugin/common/constants';
import { DetectionAlert } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createSignalsIndex,
setSignalStatus,
getQuerySignalIds,
createAlertsIndex,
setAlertStatus,
getQueryAlertIds,
deleteAllRules,
createRule,
waitForSignalsToBePresent,
getSignalsByIds,
waitForAlertsToBePresent,
getAlertsByIds,
waitForRuleSuccess,
getRuleForSignalTesting,
getRuleForAlertTesting,
deleteAllAlerts,
} from '../../utils';
} from '../../../utils';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { EsArchivePathBuilder } from '../../../../../es_archive_path_builder';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const log = getService('log');
const es = getService('es');
// TODO: add a new service
const config = getService('config');
const isServerless = config.get('serverless');
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const auditbeatHost = dataPathBuilder.getPath('auditbeat/hosts');
describe('open_close_signals', () => {
describe('@ess @serverless open_close_alerts', () => {
describe('tests with auditbeat data', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
await esArchiver.load(auditbeatHost);
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
await esArchiver.unload(auditbeatHost);
});
beforeEach(async () => {
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
@ -55,94 +60,94 @@ export default ({ getService }: FtrProviderContext) => {
await deleteAllRules(supertest, log);
});
it('should be able to execute and get 10 signals', async () => {
it('should be able to execute and get 10 alerts', async () => {
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
...getRuleForAlertTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
const signalsOpen = await getSignalsByIds(supertest, log, [id]);
expect(signalsOpen.hits.hits.length).equal(10);
await waitForAlertsToBePresent(supertest, log, 10, [id]);
const alertsOpen = await getAlertsByIds(supertest, log, [id]);
expect(alertsOpen.hits.hits.length).equal(10);
});
it('should be have set the signals in an open state initially', async () => {
it('should be have set the alerts in an open state initially', async () => {
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
...getRuleForAlertTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
const signalsOpen = await getSignalsByIds(supertest, log, [id]);
const everySignalOpen = signalsOpen.hits.hits.every(
await waitForAlertsToBePresent(supertest, log, 10, [id]);
const alertsOpen = await getAlertsByIds(supertest, log, [id]);
const everyAlertOpen = alertsOpen.hits.hits.every(
(hit) => hit._source?.[ALERT_WORKFLOW_STATUS] === 'open'
);
expect(everySignalOpen).to.eql(true);
expect(everyAlertOpen).to.eql(true);
});
it('should be able to get a count of 10 closed signals when closing 10', async () => {
it('should be able to get a count of 10 closed alerts when closing 10', async () => {
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
...getRuleForAlertTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
const signalsOpen = await getSignalsByIds(supertest, log, [id]);
const signalIds = signalsOpen.hits.hits.map((signal) => signal._id);
await waitForAlertsToBePresent(supertest, log, 10, [id]);
const alertsOpen = await getAlertsByIds(supertest, log, [id]);
const alertIds = alertsOpen.hits.hits.map((alert) => alert._id);
// set all of the signals to the state of closed. There is no reason to use a waitUntil here
// set all of the alerts to the state of closed. There is no reason to use a waitUntil here
// as this route intentionally has a waitFor within it and should only return when the query has
// the data.
await supertest
.post(DETECTION_ENGINE_SIGNALS_STATUS_URL)
.set('kbn-xsrf', 'true')
.send(setSignalStatus({ signalIds, status: 'closed' }))
.send(setAlertStatus({ alertIds, status: 'closed' }))
.expect(200);
const { body: signalsClosed }: { body: estypes.SearchResponse<DetectionAlert> } =
const { body: alertsClosed }: { body: estypes.SearchResponse<DetectionAlert> } =
await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQuerySignalIds(signalIds))
.send(getQueryAlertIds(alertIds))
.expect(200);
expect(signalsClosed.hits.hits.length).to.equal(10);
expect(alertsClosed.hits.hits.length).to.equal(10);
});
// Test is failing after changing refresh to false
it.skip('should be able close 10 signals immediately and they all should be closed', async () => {
it.skip('should be able close 10 alerts immediately and they all should be closed', async () => {
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
...getRuleForAlertTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
const signalsOpen = await getSignalsByIds(supertest, log, [id]);
const signalIds = signalsOpen.hits.hits.map((signal) => signal._id);
await waitForAlertsToBePresent(supertest, log, 10, [id]);
const alertsOpen = await getAlertsByIds(supertest, log, [id]);
const alertIds = alertsOpen.hits.hits.map((alert) => alert._id);
// set all of the signals to the state of closed. There is no reason to use a waitUntil here
// set all of the alerts to the state of closed. There is no reason to use a waitUntil here
// as this route intentionally has a waitFor within it and should only return when the query has
// the data.
await supertest
.post(DETECTION_ENGINE_SIGNALS_STATUS_URL)
.set('kbn-xsrf', 'true')
.send(setSignalStatus({ signalIds, status: 'closed' }))
.send(setAlertStatus({ alertIds, status: 'closed' }))
.expect(200);
const { body: signalsClosed }: { body: estypes.SearchResponse<DetectionAlert> } =
const { body: alertsClosed }: { body: estypes.SearchResponse<DetectionAlert> } =
await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQuerySignalIds(signalIds))
.send(getQueryAlertIds(alertIds))
.expect(200);
const everySignalClosed = signalsClosed.hits.hits.every(
const everyAlertClosed = alertsClosed.hits.hits.every(
(hit) => hit._source?.[ALERT_WORKFLOW_STATUS] === 'closed'
);
expect(everySignalClosed).to.eql(true);
expect(everyAlertClosed).to.eql(true);
});
});
});

View file

@ -11,25 +11,25 @@ import {
DETECTION_ENGINE_QUERY_SIGNALS_URL,
ALERTS_AS_DATA_FIND_URL,
} from '@kbn/security-solution-plugin/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { getSignalStatus, createSignalsIndex, deleteAllAlerts } from '../../utils';
import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common';
import { getAlertStatus, createAlertsIndex, deleteAllAlerts } from '../../../utils';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const log = getService('log');
const es = getService('es');
describe('query_signals_route and find_alerts_route', () => {
describe('@ess @serverless query_signals_route and find_alerts_route', () => {
describe('validation checks', () => {
// This fails and should be investigated or removed if it no longer applies
it.skip('should not give errors when querying and the signals index does exist and is empty', async () => {
await createSignalsIndex(supertest, log);
it.skip('should not give errors when querying and the alerts index does exist and is empty', async () => {
await createAlertsIndex(supertest, log);
const { body } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getSignalStatus())
.send(getAlertStatus())
.expect(200);
// remove any server generated items that are indeterministic
@ -48,56 +48,19 @@ export default ({ getService }: FtrProviderContext) => {
});
});
describe('backwards compatibility', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/endpoint/resolver/signals');
await createSignalsIndex(supertest, log);
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/endpoint/resolver/signals');
await deleteAllAlerts(supertest, log, es);
});
it('should be able to filter old signals on host.os.name.caseless using runtime field', async () => {
const query = {
query: {
bool: {
should: [{ match_phrase: { 'host.os.name.caseless': 'windows' } }],
},
},
};
const { body } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(query)
.expect(200);
expect(body.hits.total.value).to.eql(3);
});
it('should be able to filter old signals using field aliases', async () => {
const query = {
query: {
bool: {
should: [{ match_phrase: { 'kibana.alert.workflow_status': 'open' } }],
},
},
};
const { body } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(query)
.expect(200);
expect(body.hits.total.value).to.eql(3);
});
});
describe('runtime fields', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/endpoint/resolver/signals');
await createSignalsIndex(supertest, log);
await esArchiver.load(
'x-pack/test/functional/es_archives/security_solution/alerts/8.8.0_multiple_docs',
{
useCreate: true,
docsOnly: true,
}
);
await createAlertsIndex(supertest, log);
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/endpoint/resolver/signals');
// await esArchiver.unload('x-pack/test/functional/es_archives/endpoint/resolver/signals');
await deleteAllAlerts(supertest, log, es);
});
@ -129,12 +92,12 @@ export default ({ getService }: FtrProviderContext) => {
describe('find_alerts_route', () => {
describe('validation checks', () => {
// This fails and should be investigated or removed if it no longer applies
it.skip('should not give errors when querying and the signals index does exist and is empty', async () => {
await createSignalsIndex(supertest, log);
it.skip('should not give errors when querying and the alerts index does exist and is empty', async () => {
await createAlertsIndex(supertest, log);
const { body } = await supertest
.post(ALERTS_AS_DATA_FIND_URL)
.set('kbn-xsrf', 'true')
.send({ ...getSignalStatus(), index: '.siem-signals-default' })
.send({ ...getAlertStatus(), index: '.siem-signals-default' })
.expect(200);
// remove any server generated items that are indeterministic
@ -153,10 +116,11 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should not give errors when executing security solution histogram aggs', async () => {
await createSignalsIndex(supertest, log);
await createAlertsIndex(supertest, log);
await supertest
.post(ALERTS_AS_DATA_FIND_URL)
.set('kbn-xsrf', 'true')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({
index: '.siem-signals-default',
aggs: {

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '@kbn/security-solution-plugin/common/constants';
import { createAlertsIndex, deleteAllAlerts } from '../../../utils';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const log = getService('log');
const es = getService('es');
describe('@ess query_alerts_backword_compatibility', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/endpoint/resolver/signals');
await createAlertsIndex(supertest, log);
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/endpoint/resolver/signals');
await deleteAllAlerts(supertest, log, es);
});
it('should be able to filter old alerts on host.os.name.caseless using runtime field', async () => {
const query = {
query: {
bool: {
should: [{ match_phrase: { 'host.os.name.caseless': 'windows' } }],
},
},
};
const { body } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(query)
.expect(200);
expect(body.hits.total.value).to.eql(3);
});
it('should be able to filter old alerts using field aliases', async () => {
const query = {
query: {
bool: {
should: [{ match_phrase: { 'kibana.alert.workflow_status': 'open' } }],
},
},
};
const { body } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(query)
.expect(200);
expect(body.hits.total.value).to.eql(3);
});
});
};

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(
require.resolve('../../../../../config/ess/config.base.basic')
);
return {
...functionalConfig.getAll(),
testFiles: [require.resolve('..')],
junit: {
reportName: 'Detection Engine ESS - Basic Integration Tests',
},
};
}

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createTestConfig } from '../../../../../config/serverless/config.base.essentials';
export default createTestConfig({
testFiles: [require.resolve('..')],
junit: {
reportName: 'Detection Engine Serverless - Essentials Integration Tests',
},
});

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Detection Engine Basic and Essentials API', function () {
loadTestFile(require.resolve('./rules/create_rules'));
loadTestFile(require.resolve('./rules/create_ml_rules_privileges'));
loadTestFile(require.resolve('./alerts/open_close_alerts'));
loadTestFile(require.resolve('./alerts/query_alerts'));
loadTestFile(require.resolve('./alerts/query_alerts_backword_compatibility'));
});
}

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from 'expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
import {
createAlertsIndex,
deleteAllRules,
removeServerGeneratedProperties,
getSimpleMlRule,
deleteAllAlerts,
updateUsername,
} from '../../../utils';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { EsArchivePathBuilder } from '../../../../../es_archive_path_builder';
export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
const log = getService('log');
const es = getService('es');
// TODO: add a new service
const config = getService('config');
const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username');
const isServerless = config.get('serverless');
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const auditbeatPath = dataPathBuilder.getPath('auditbeat/hosts');
describe('create_ml_rules', () => {
describe('Creating Machine Learning rules', () => {
before(async () => {
await esArchiver.load(auditbeatPath);
});
after(async () => {
await esArchiver.unload(auditbeatPath);
});
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('@ess should give a 403 when trying to create a single Machine Learning rule since the license is basic', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.send(getSimpleMlRule())
.expect(403);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).toEqual({
message: 'Your license does not support machine learning. Please upgrade your license.',
status_code: 403,
});
});
it('@serverless should give a 200 when trying to create a single Machine Learning rule since the license is essentials', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.send(getSimpleMlRule())
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
const expectedRule = updateUsername(getSimpleMlRule(), ELASTICSEARCH_USERNAME);
expect(bodyToCompare).toEqual(expect.objectContaining(expectedRule));
});
});
});
};

View file

@ -6,42 +6,49 @@
*/
import expect from '@kbn/expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import { RuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createSignalsIndex,
createAlertsIndex,
deleteAllRules,
getSimpleRule,
getSimpleRuleOutput,
getSimpleRuleOutputWithoutRuleId,
getSimpleRuleWithoutRuleId,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleMlRule,
deleteAllAlerts,
} from '../../utils';
updateUsername,
} from '../../../utils';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { EsArchivePathBuilder } from '../../../../../es_archive_path_builder';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
const log = getService('log');
const es = getService('es');
// TODO: add a new service
const config = getService('config');
const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username');
const isServerless = config.get('serverless');
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const auditbeatPath = dataPathBuilder.getPath('auditbeat/hosts');
describe('create_rules', () => {
describe('@ess @serverless create_rules', () => {
describe('creating rules', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
await esArchiver.load(auditbeatPath);
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
await esArchiver.unload(auditbeatPath);
});
beforeEach(async () => {
await createSignalsIndex(supertest, log);
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
@ -53,12 +60,14 @@ export default ({ getService }: FtrProviderContext) => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.send(getSimpleRule())
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
const expectedRule = updateUsername(bodyToCompare, ELASTICSEARCH_USERNAME);
expect(bodyToCompare).to.eql(expectedRule);
});
it('should create a single rule without an input index', async () => {
@ -72,90 +81,49 @@ export default ({ getService }: FtrProviderContext) => {
type: 'query',
query: 'user.name: root or user.name: admin',
};
const expected = {
actions: [],
author: [],
created_by: 'elastic',
description: 'Simple Rule Query',
enabled: true,
false_positives: [],
from: 'now-6m',
immutable: false,
interval: '5m',
rule_id: 'rule-1',
language: 'kuery',
output_index: '',
max_signals: 100,
risk_score: 1,
risk_score_mapping: [],
name: 'Simple Rule Query',
query: 'user.name: root or user.name: admin',
references: [],
related_integrations: [],
required_fields: [],
setup: '',
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
tags: [],
to: 'now',
type: 'query',
threat: [],
exceptions_list: [],
version: 1,
revision: 0,
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.send(rule)
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(expected);
const expectedRule = updateUsername(bodyToCompare, ELASTICSEARCH_USERNAME);
expect(bodyToCompare).to.eql(expectedRule);
});
it('should create a single rule without a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.send(getSimpleRuleWithoutRuleId())
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
});
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
ELASTICSEARCH_USERNAME
);
it('should give a 403 when trying to create a single Machine Learning rule since the license is basic', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(getSimpleMlRule())
.expect(403);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql({
message: 'Your license does not support machine learning. Please upgrade your license.',
status_code: 403,
});
expect(bodyToCompare).to.eql(expectedRule);
});
it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => {
await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.send(getSimpleRule())
.expect(200);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.send(getSimpleRule())
.expect(409);

View file

@ -5,6 +5,6 @@
* 2.0.
*/
export const getSignalStatus = () => ({
export const getAlertStatus = () => ({
aggs: { statuses: { terms: { field: 'kibana.alert.workflow_status', size: 10 } } },
});

View file

@ -20,4 +20,5 @@ export * from './get_alert_status_empty_response';
export * from './get_query_alert_ids';
export * from './set_alert_tags';
export * from './get_preview_alerts';
export * from './get_alert_status';
export * from './migrations';

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getSimpleRuleOutput } from './get_simple_rule_output';
import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties';
/**
* This is the typical output of a simple rule that Kibana will output with all the defaults except
* for all the server generated properties such as created_by. Useful for testing end to end tests.
*/
export const getSimpleRuleOutputWithoutRuleId = (
ruleId = 'rule-1'
): Omit<RuleWithoutServerGeneratedProperties, 'rule_id'> => {
const rule = getSimpleRuleOutput(ruleId);
const { rule_id: rId, ...ruleWithoutRuleId } = rule;
return ruleWithoutRuleId;
};

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { getSimpleRule } from './get_simple_rule';
/**
* This is a typical simple rule for testing that is easy for most basic testing
*/
export const getSimpleRuleWithoutRuleId = (): RuleCreateProps => {
const simpleRule = getSimpleRule();
// eslint-disable-next-line @typescript-eslint/naming-convention
const { rule_id, ...ruleWithoutId } = simpleRule;
return ruleWithoutId;
};

View file

@ -39,5 +39,8 @@ export * from './generate_event';
export * from './create_legacy_rule_action';
export * from './get_simple_threat_match';
export * from './get_simple_ml_rule';
export * from './remove_server_generated_properties_including_rule_id';
export * from './get_simple_rule_output_without_rule_id';
export * from './get_simple_rule_without_rule_id';
export * from './prebuilt_rules';

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { removeServerGeneratedProperties } from './remove_server_generated_properties';
/**
* This will remove server generated properties such as date times, etc... including the rule_id
* @param rule Rule to pass in to remove typical server generated properties
*/
export const removeServerGeneratedPropertiesIncludingRuleId = (
rule: RuleResponse
): Partial<RuleResponse> => {
const ruleWithRemovedProperties = removeServerGeneratedProperties(rule);
// eslint-disable-next-line @typescript-eslint/naming-convention
const { rule_id, ...additionalRuledIdRemoved } = ruleWithRemovedProperties;
return additionalRuledIdRemoved;
};