[Security Solution] Adds tests for coverage overview page (#168058)

**Resolves: https://github.com/elastic/kibana/issues/162250**

## Summary

Adds remaining unit, api integration, and e2e cypress tests for the
coverage overview page in accordance to the [existing test
plan](https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/rule_management/coverage_overview_dashboard.md)

- [Flaky test runner
build](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4756)

### Checklist

Delete any items that are not applicable to this PR.

- [x] 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/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios



### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Davis Plumlee 2024-01-09 13:09:40 -06:00 committed by GitHub
parent 60c3ff05a6
commit 315cf9d399
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1624 additions and 608 deletions

View file

@ -496,6 +496,8 @@ enabled:
- 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
- x-pack/test/security_solution_api_integration/test_suites/detections_response/basic_essentials_license/rule_management/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/basic_essentials_license/rule_management/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/default_license/exception_lists_items/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/default_license/exception_lists_items/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/default_license/lists_items/configs/serverless.config.ts

1
.github/CODEOWNERS vendored
View file

@ -1378,6 +1378,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/rule_management @elastic/security-detection-rule-management
/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/prebuilt_rules @elastic/security-detection-rule-management
/x-pack/test/security_solution_api_integration/test_suites/detections_response/basic_essentials_license/rule_management @elastic/security-detection-rule-management
/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_delete @elastic/security-detection-rule-management
x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_update @elastic/security-detection-rule-management
/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_patch @elastic/security-detection-rule-management

View file

@ -0,0 +1,121 @@
/*
* 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 { left } from 'fp-ts/lib/Either';
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import {
CoverageOverviewRequestBody,
CoverageOverviewRuleActivity,
CoverageOverviewRuleSource,
} from './coverage_overview_route';
describe('Coverage overview request schema', () => {
test('empty object validates', () => {
const payload: CoverageOverviewRequestBody = {};
const decoded = CoverageOverviewRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('validates with all fields populated', () => {
const payload: CoverageOverviewRequestBody = {
filter: {
activity: [CoverageOverviewRuleActivity.Enabled, CoverageOverviewRuleActivity.Disabled],
source: [CoverageOverviewRuleSource.Custom, CoverageOverviewRuleSource.Prebuilt],
search_term: 'search term',
},
};
const decoded = CoverageOverviewRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('does NOT validate with extra fields', () => {
const payload: CoverageOverviewRequestBody & { invalid_field: string } = {
filter: {
activity: [CoverageOverviewRuleActivity.Enabled, CoverageOverviewRuleActivity.Disabled],
source: [CoverageOverviewRuleSource.Custom, CoverageOverviewRuleSource.Prebuilt],
search_term: 'search term',
},
invalid_field: 'invalid field',
};
const decoded = CoverageOverviewRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_field"']);
expect(message.schema).toEqual({});
});
test('does NOT validate with invalid filter values', () => {
const payload: CoverageOverviewRequestBody = {
filter: {
// @ts-expect-error
activity: ['Wrong activity field'],
// @ts-expect-error
source: ['Wrong source field'],
search_term: 'search term',
},
};
const decoded = CoverageOverviewRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "Wrong activity field" supplied to "filter,activity"',
'Invalid value "Wrong source field" supplied to "filter,source"',
]);
expect(message.schema).toEqual({});
});
test('does NOT validate with empty filter arrays', () => {
const payload: CoverageOverviewRequestBody = {
filter: {
activity: [],
source: [],
search_term: 'search term',
},
};
const decoded = CoverageOverviewRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[]" supplied to "filter,activity"',
'Invalid value "[]" supplied to "filter,source"',
]);
expect(message.schema).toEqual({});
});
test('does NOT validate with empty search_term', () => {
const payload: CoverageOverviewRequestBody = {
filter: {
search_term: '',
},
};
const decoded = CoverageOverviewRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const message = foldLeftRight(checked);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "" supplied to "filter,search_term"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -24,9 +24,9 @@ Status: `in progress`. The current test plan matches `Milestone 1 - MVP` of the
- **Rule Source**: The filter type defining rule type, current options are `prebuilt`(from elastic prebuilt rules package) and `custom`(created by user)
-**Initial filter state**: The filters present on initial page load. Rule activity will be set to `enabled`, rule source will be set to `prebuilt` and `custom` simultaneously.
- **Initial filter state**: The filters present on initial page load. Rule activity will be set to `enabled`, rule source will be set to `prebuilt` and `custom` simultaneously.
-**Dashboard containing the rule data**: The normal render of the coverage overview dashboard. Any returned rule data mapped correctly to the tile layout of all the MITRE data in a colored grid
- **Dashboard containing the rule data**: The normal render of the coverage overview dashboard. Any returned rule data mapped correctly to the tile layout of all the MITRE data in a colored grid
### Assumptions

View file

@ -8,12 +8,14 @@
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import * as i18n from '../../../rule_management_ui/pages/coverage_overview/translations';
import type { CoverageOverviewFilter } from '../../../../../common/api/detection_engine';
import { RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL } from '../../../../../common/api/detection_engine';
import { fetchCoverageOverview } from '../api';
import { buildCoverageOverviewDashboardModel } from '../../logic/coverage_overview/build_coverage_overview_dashboard_model';
import type { CoverageOverviewDashboard } from '../../model/coverage_overview/dashboard';
import { DEFAULT_QUERY_OPTIONS } from './constants';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
const COVERAGE_OVERVIEW_QUERY_KEY = ['POST', RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL];
@ -29,6 +31,8 @@ export const useFetchCoverageOverviewQuery = (
filter: CoverageOverviewFilter = {},
options?: UseQueryOptions<CoverageOverviewDashboard>
) => {
const { addError } = useAppToasts();
return useQuery<CoverageOverviewDashboard>(
[...COVERAGE_OVERVIEW_QUERY_KEY, filter],
async ({ signal }) => {
@ -39,6 +43,11 @@ export const useFetchCoverageOverviewQuery = (
{
...DEFAULT_QUERY_OPTIONS,
...options,
onError: (error) => {
addError(error, {
title: i18n.COVERAGE_OVERVIEW_FETCH_ERROR_TITLE,
});
},
}
);
};

View file

@ -40,7 +40,12 @@ const CoverageOverviewDashboardComponent = () => {
<EuiSpacer />
<EuiFlexGroup gutterSize="m" className="eui-xScroll">
{data?.mitreTactics.map((tactic) => (
<EuiFlexGroup direction="column" key={tactic.id} gutterSize="s">
<EuiFlexGroup
data-test-subj={`coverageOverviewTacticGroup-${tactic.id}`}
direction="column"
key={tactic.id}
gutterSize="s"
>
<EuiFlexItem grow={false}>
<CoverageOverviewTacticPanel tactic={tactic} />
</EuiFlexItem>

View file

@ -87,7 +87,7 @@ const RuleSourceFilterComponent = ({
`}
>
<EuiPopover
id="ruleActivityFilterPopover"
id="ruleSourceFilterPopover"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}

View file

@ -79,7 +79,7 @@ const CoverageOverviewMitreTechniquePanelComponent = ({
>
<EuiFlexGroup css={{ height: '100%' }} direction="column" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiText size="xs">
<EuiText data-test-subj={`coverageOverviewTechniqueTitle-${technique.id}`} size="xs">
<h4>{technique.name}</h4>
</EuiText>
{SubtechniqueInfo}

View file

@ -177,3 +177,10 @@ export const CoverageOverviewDashboardInformation = i18n.translate(
"Your current coverage of MITRE ATT&CK\u00AE tactics and techniques, based on installed rules. Click a cell to view and enable a technique's rules. Rules must be mapped to the MITRE ATT&CK\u00AE framework to be displayed.",
}
);
export const COVERAGE_OVERVIEW_FETCH_ERROR_TITLE = i18n.translate(
'xpack.securitySolution.coverageOverviewDashboard.fetchErrorTitle',
{
defaultMessage: 'Failed to fetch coverage overview data',
}
);

View file

@ -7251,27 +7251,123 @@ export const subtechniques: MitreSubTechnique[] = [
];
/**
* A full object of Mitre Attack Threat data that is taken directly from the `mitre_tactics_techniques.ts` file
* An array of full Mitre Attack Threat objects that are taken directly from the `mitre_tactics_techniques.ts` file
*
* Is built alongside and sampled from the data in the file so to always be valid with the most up to date MITRE ATT&CK data
*/
export const getMockThreatData = () => ({
tactic: {
name: 'Credential Access',
id: 'TA0006',
reference: 'https://attack.mitre.org/tactics/TA0006',
export const getMockThreatData = () => [
{
tactic: {
name: 'Credential Access',
id: 'TA0006',
reference: 'https://attack.mitre.org/tactics/TA0006',
},
technique: {
name: 'OS Credential Dumping',
id: 'T1003',
reference: 'https://attack.mitre.org/techniques/T1003',
tactics: ['credential-access'],
},
subtechnique: {
name: '/etc/passwd and /etc/shadow',
id: 'T1003.008',
reference: 'https://attack.mitre.org/techniques/T1003/008',
tactics: ['credential-access'],
techniqueId: 'T1003',
},
},
technique: {
name: 'OS Credential Dumping',
id: 'T1003',
reference: 'https://attack.mitre.org/techniques/T1003',
tactics: ['credential-access'],
{
tactic: {
name: 'Credential Access',
id: 'TA0006',
reference: 'https://attack.mitre.org/tactics/TA0006',
},
technique: {
name: 'Steal or Forge Kerberos Tickets',
id: 'T1558',
reference: 'https://attack.mitre.org/techniques/T1558',
tactics: ['credential-access'],
},
subtechnique: {
name: 'AS-REP Roasting',
id: 'T1558.004',
reference: 'https://attack.mitre.org/techniques/T1558/004',
tactics: ['credential-access'],
techniqueId: 'T1558',
},
},
subtechnique: {
name: '/etc/passwd and /etc/shadow',
id: 'T1003.008',
reference: 'https://attack.mitre.org/techniques/T1003/008',
tactics: ['credential-access'],
techniqueId: 'T1003',
{
tactic: {
name: 'Persistence',
id: 'TA0003',
reference: 'https://attack.mitre.org/tactics/TA0003',
},
technique: {
name: 'Boot or Logon Autostart Execution',
id: 'T1547',
reference: 'https://attack.mitre.org/techniques/T1547',
tactics: ['persistence', 'privilege-escalation'],
},
subtechnique: {
name: 'Active Setup',
id: 'T1547.014',
reference: 'https://attack.mitre.org/techniques/T1547/014',
tactics: ['persistence', 'privilege-escalation'],
techniqueId: 'T1547',
},
},
});
{
tactic: {
name: 'Persistence',
id: 'TA0003',
reference: 'https://attack.mitre.org/tactics/TA0003',
},
technique: {
name: 'Account Manipulation',
id: 'T1098',
reference: 'https://attack.mitre.org/techniques/T1098',
tactics: ['persistence'],
},
subtechnique: {
name: 'Additional Cloud Credentials',
id: 'T1098.001',
reference: 'https://attack.mitre.org/techniques/T1098/001',
tactics: ['persistence'],
techniqueId: 'T1098',
},
},
];
/**
* An array of specifically chosen Mitre Attack Threat objects that is taken directly from the `mitre_tactics_techniques.ts` file
*
* These objects have identical technique fields but are assigned to different tactics
*/
export const getDuplicateTechniqueThreatData = () => [
{
tactic: {
name: 'Privilege Escalation',
id: 'TA0004',
reference: 'https://attack.mitre.org/tactics/TA0004',
},
technique: {
name: 'Event Triggered Execution',
id: 'T1546',
reference: 'https://attack.mitre.org/techniques/T1546',
tactics: ['privilege-escalation', 'persistence'],
},
},
{
tactic: {
name: 'Persistence',
id: 'TA0003',
reference: 'https://attack.mitre.org/tactics/TA0003',
},
technique: {
name: 'Event Triggered Execution',
id: 'T1546',
reference: 'https://attack.mitre.org/techniques/T1546',
tactics: ['privilege-escalation', 'persistence'],
},
},
];

View file

@ -8,7 +8,7 @@
import type { Threats } from '@kbn/securitysolution-io-ts-alerting-types';
import { getMockThreatData } from './mitre_tactics_techniques';
const { tactic, technique, subtechnique } = getMockThreatData();
const { tactic, technique, subtechnique } = getMockThreatData()[0];
const { tactics, ...mockTechnique } = technique;
const { tactics: subtechniqueTactics, ...mockSubtechnique } = subtechnique;

View file

@ -22,6 +22,15 @@ const OUTPUT_DIRECTORY = resolve('public', 'detections', 'mitre');
const MITRE_CONTENT_VERSION = 'ATT&CK-v13.1'; // last updated when preparing for 8.10.3 release
const MITRE_CONTENT_URL = `https://raw.githubusercontent.com/mitre/cti/${MITRE_CONTENT_VERSION}/enterprise-attack/enterprise-attack.json`;
/**
* An ID for a technique that exists in multiple tactics. This may change in further updates and on MITRE
* version upgrade, this ID should be double-checked to make sure it still represents these parameters.
*
* We have this in order to cover edge cases with our mock data that can't be achieved by simply generating
* data from the MITRE api.
*/
const MOCK_DUPLICATE_TECHNIQUE_ID = 'T1546';
const getTacticsOptions = (tactics) =>
tactics.map((t) =>
`{
@ -172,15 +181,37 @@ const extractSubtechniques = (mitreData) => {
};
const buildMockThreatData = (tacticsData, techniques, subtechniques) => {
const subtechnique = subtechniques[0];
const technique = techniques.find((technique) => technique.id === subtechnique.techniqueId);
const tactic = tacticsData.find((tactic) => tactic.shortName === technique.tactics[0]);
const numberOfThreatsToGenerate = 4;
const mockThreatData = [];
for (let i = 0; i < numberOfThreatsToGenerate; i++) {
const subtechnique = subtechniques[i * 2]; // Double our interval to broaden the subtechnique types we're pulling data from a bit
const technique = techniques.find((technique) => technique.id === subtechnique.techniqueId);
const tactic = tacticsData.find((tactic) => tactic.shortName === technique.tactics[0]);
return {
tactic: normalizeTacticsData([tactic])[0],
technique,
subtechnique,
};
mockThreatData.push({
tactic: normalizeTacticsData([tactic])[0],
technique,
subtechnique,
});
}
return mockThreatData;
};
const buildDuplicateTechniqueMockThreatData = (tacticsData, techniques) => {
const technique = techniques.find((technique) => technique.id === MOCK_DUPLICATE_TECHNIQUE_ID);
const tacticOne = tacticsData.find((tactic) => tactic.shortName === technique.tactics[0]);
const tacticTwo = tacticsData.find((tactic) => tactic.shortName === technique.tactics[1]);
return [
{
tactic: normalizeTacticsData([tacticOne])[0],
technique,
},
{
tactic: normalizeTacticsData([tacticTwo])[0],
technique,
},
];
};
async function main() {
@ -224,7 +255,7 @@ async function main() {
.replace(/"{/g, '{')};
/**
* A full object of Mitre Attack Threat data that is taken directly from the \`mitre_tactics_techniques.ts\` file
* An array of full Mitre Attack Threat objects that are taken directly from the \`mitre_tactics_techniques.ts\` file
*
* Is built alongside and sampled from the data in the file so to always be valid with the most up to date MITRE ATT&CK data
*/
@ -235,6 +266,19 @@ async function main() {
)
.replace(/}"/g, '}')
.replace(/"{/g, '{')});
/**
* An array of specifically chosen Mitre Attack Threat objects that is taken directly from the \`mitre_tactics_techniques.ts\` file
*
* These objects have identical technique fields but are assigned to different tactics
*/
export const getDuplicateTechniqueThreatData = () => (${JSON.stringify(
buildDuplicateTechniqueMockThreatData(tacticsData, techniques),
null,
2
)
.replace(/}"/g, '}')
.replace(/"{/g, '{')});
`;
fs.writeFileSync(`${OUTPUT_DIRECTORY}/mitre_tactics_techniques.ts`, body, 'utf-8');

View file

@ -1,510 +0,0 @@
/*
* 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 {
RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL,
ThreatArray,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createPrebuiltRuleAssetSavedObjects,
createRuleAssetSavedObject,
createRule,
deleteAllRules,
getSimpleRule,
installPrebuiltRulesAndTimelines,
createNonSecurityRule,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const log = getService('log');
const es = getService('es');
describe('coverage_overview', () => {
beforeEach(async () => {
await deleteAllRules(supertest, log);
});
it('does NOT error when there are no security rules', async () => {
await createNonSecurityRule(supertest);
const rule1 = await createRule(supertest, log, {
...getSimpleRule(),
threat: generateThreatArray(1),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({})
.expect(200);
expect(body).to.eql({
coverage: {
T001: [rule1.id],
TA001: [rule1.id],
'T001.001': [rule1.id],
},
unmapped_rule_ids: [],
rules_data: {
[rule1.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
describe('without filters', () => {
it('returns an empty response if there are no rules', async () => {
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({})
.expect(200);
expect(body).to.eql({
coverage: {},
unmapped_rule_ids: [],
rules_data: {},
});
});
it('returns response with a single rule mapped to MITRE categories', async () => {
const rule1 = await createRule(supertest, log, {
...getSimpleRule(),
threat: generateThreatArray(1),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({})
.expect(200);
expect(body).to.eql({
coverage: {
T001: [rule1.id],
TA001: [rule1.id],
'T001.001': [rule1.id],
},
unmapped_rule_ids: [],
rules_data: {
[rule1.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
it('returns response with an unmapped rule', async () => {
const rule1 = await createRule(supertest, log, { ...getSimpleRule(), threat: undefined });
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({})
.expect(200);
expect(body).to.eql({
coverage: {},
unmapped_rule_ids: [rule1.id],
rules_data: {
[rule1.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
});
describe('with filters', () => {
describe('search_term', () => {
it('returns response filtered by tactic', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
threat: generateThreatArray(1),
});
const expectedRule = await createRule(supertest, log, {
...getSimpleRule('rule-2'),
threat: generateThreatArray(2),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
search_term: 'TA002',
},
})
.expect(200);
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
it('returns response filtered by technique', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
threat: generateThreatArray(1),
});
const expectedRule = await createRule(supertest, log, {
...getSimpleRule('rule-2'),
threat: generateThreatArray(2),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
search_term: 'T002',
},
})
.expect(200);
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
it('returns response filtered by subtechnique', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
threat: generateThreatArray(1),
});
const expectedRule = await createRule(supertest, log, {
...getSimpleRule('rule-2'),
threat: generateThreatArray(2),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
search_term: 'T002.002',
},
})
.expect(200);
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
it('returns response filtered by rule name', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const expectedRule = await createRule(supertest, log, {
...getSimpleRule('rule-2'),
name: 'rule-2',
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
search_term: 'rule-2',
},
})
.expect(200);
expect(body).to.eql({
coverage: {},
unmapped_rule_ids: [expectedRule.id],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'rule-2',
},
},
});
});
it('returns response filtered by index pattern', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
index: ['index-pattern-1'],
});
const expectedRule = await createRule(supertest, log, {
...getSimpleRule('rule-2'),
index: ['index-pattern-2'],
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
search_term: 'index-pattern-2',
},
})
.expect(200);
expect(body).to.eql({
coverage: {},
unmapped_rule_ids: [expectedRule.id],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
});
describe('activity', () => {
it('returns response filtered by disabled rules', async () => {
const expectedRule = await createRule(supertest, log, {
...getSimpleRule('rule-1'),
threat: generateThreatArray(1),
});
await createRule(supertest, log, {
...getSimpleRule('rule-2', true),
threat: generateThreatArray(2),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
activity: ['disabled'],
},
})
.expect(200);
expect(body).to.eql({
coverage: {
T001: [expectedRule.id],
TA001: [expectedRule.id],
'T001.001': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
it('returns response filtered by enabled rules', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
threat: generateThreatArray(1),
});
const expectedRule = await createRule(supertest, log, {
...getSimpleRule('rule-2', true),
threat: generateThreatArray(2),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
activity: ['enabled'],
},
})
.expect(200);
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'enabled',
name: 'Simple Rule Query',
},
},
});
});
it('returns all rules if both enabled and disabled filters are specified in the request', async () => {
const expectedRule1 = await createRule(supertest, log, {
...getSimpleRule('rule-1', false),
name: 'Disabled rule',
threat: generateThreatArray(1),
});
const expectedRule2 = await createRule(supertest, log, {
...getSimpleRule('rule-2', true),
name: 'Enabled rule',
threat: generateThreatArray(2),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
activity: ['enabled', 'disabled'],
},
})
.expect(200);
expect(body).to.eql({
coverage: {
T001: [expectedRule1.id],
TA001: [expectedRule1.id],
'T001.001': [expectedRule1.id],
T002: [expectedRule2.id],
TA002: [expectedRule2.id],
'T002.002': [expectedRule2.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule1.id]: {
activity: 'disabled',
name: 'Disabled rule',
},
[expectedRule2.id]: {
activity: 'enabled',
name: 'Enabled rule',
},
},
});
});
});
describe('source', () => {
it('returns response filtered by custom rules', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({
rule_id: 'prebuilt-rule-1',
threat: generateThreatArray(1),
}),
]);
await installPrebuiltRulesAndTimelines(es, supertest);
const expectedRule = await createRule(supertest, log, {
...getSimpleRule('rule-1'),
threat: generateThreatArray(2),
});
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.send({
filter: {
source: ['custom'],
},
})
.expect(200);
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Simple Rule Query',
},
},
});
});
});
});
});
};
function generateThreatArray(startIndex: number, count = 1): ThreatArray {
const result: ThreatArray = [];
for (let i = 0; i < count; ++i) {
const indexName = (i + startIndex).toString().padStart(3, '0');
result.push({
framework: 'MITRE ATT&CK',
tactic: {
id: `TA${indexName}`,
name: `Tactic ${indexName}`,
reference: `http://some-link-${indexName}`,
},
technique: [
{
id: `T${indexName}`,
name: `Technique ${indexName}`,
reference: `http://some-technique-link-${indexName}`,
subtechnique: [
{
id: `T${indexName}.${indexName}`,
name: `Subtechnique ${indexName}`,
reference: `http://some-sub-technique-link-${indexName}`,
},
],
},
],
});
}
return result;
}

View file

@ -22,6 +22,5 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./patch_rules_bulk'));
loadTestFile(require.resolve('./patch_rules'));
loadTestFile(require.resolve('./import_timelines'));
loadTestFile(require.resolve('./coverage_overview'));
});
};

View file

@ -1,42 +0,0 @@
/*
* 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 SuperTest from 'supertest';
const SIMPLE_APM_RULE_DATA = {
name: 'Test rule',
rule_type_id: 'apm.anomaly',
enabled: false,
consumer: 'alerts',
tags: [],
actions: [],
params: {
windowSize: 30,
windowUnit: 'm',
anomalySeverityType: 'critical',
anomalyDetectorTypes: ['txLatency'],
environment: 'ENVIRONMENT_ALL',
},
schedule: {
interval: '10m',
},
};
/**
* Created a non security rule. Helpful in tests to verify functionality works with presence of non security rules.
* @param supertest The supertest deps
*/
export async function createNonSecurityRule(
supertest: SuperTest.SuperTest<SuperTest.Test>
): Promise<void> {
await supertest
.post('/api/alerting/rule')
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(SIMPLE_APM_RULE_DATA)
.expect(200);
}

View file

@ -31,7 +31,6 @@ export * from './prebuilt_rules/create_prebuilt_rule_saved_objects';
export * from './prebuilt_rules/install_prebuilt_rules_and_timelines';
export * from './get_simple_rule_update';
export * from './get_simple_ml_rule_update';
export * from './create_non_security_rule';
export * from './get_simple_rule_as_ndjson';
export * from './rule_to_ndjson';
export * from './delete_rule';

View file

@ -156,6 +156,12 @@
"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",
"rule_management_basicessentionals:server:serverless": "npm run initialize-server:dr:basicEssentials rule_management serverless",
"rule_management_basicessentionals:runner:serverless": "npm run run-tests:dr:basicEssentials rule_management serverless serverlessEnv",
"rule_management_basicessentionals:qa:serverless": "npm run run-tests:dr:basicEssentials rule_management serverless qaEnv",
"rule_management_basicessentionals:server:ess": "npm run initialize-server:dr:basicEssentials rule_management ess",
"rule_management_basicessentionals:runner:ess": "npm run run-tests:dr:basicEssentials rule_management ess essEnv",
"exception_lists_items:server:serverless": "npm run initialize-server:lists:default exception_lists_items serverless",
"exception_lists_items:runner:serverless": "npm run run-tests:lists:default exception_lists_items serverless serverlessEnv",

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/ Rule management API 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/ Rule management API Integration Tests',
},
});

View file

@ -0,0 +1,670 @@
/*
* 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 {
CoverageOverviewRuleActivity,
CoverageOverviewRuleSource,
RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL,
ThreatArray,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import { FtrProviderContext } from '../../../../ftr_provider_context';
import {
createPrebuiltRuleAssetSavedObjects,
createRuleAssetSavedObject,
createRule,
deleteAllRules,
installPrebuiltRulesAndTimelines,
installPrebuiltRules,
getCustomQueryRuleParams,
createNonSecurityRule,
} from '../../utils';
import { getCoverageOverview } from '../../utils/rules/get_coverage_overview';
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const log = getService('log');
const es = getService('es');
describe('coverage_overview', () => {
beforeEach(async () => {
await deleteAllRules(supertest, log);
});
// ESS only
describe('@ess specific tests', () => {
it('does NOT error when there exist some stack rules in addition to security detection rules', async () => {
await createNonSecurityRule(supertest);
const rule1 = await createRule(
supertest,
log,
getCustomQueryRuleParams({ threat: generateThreatArray(1) })
);
const body = await getCoverageOverview(supertest);
expect(body).to.eql({
coverage: {
T001: [rule1.id],
TA001: [rule1.id],
'T001.001': [rule1.id],
},
unmapped_rule_ids: [],
rules_data: {
[rule1.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
});
// Both serverless and ESS
describe('@serverless @ess tests', () => {
describe('base cases', () => {
it('returns an empty response if there are no rules', async () => {
const body = await getCoverageOverview(supertest);
expect(body).to.eql({
coverage: {},
unmapped_rule_ids: [],
rules_data: {},
});
});
it('returns response with a single rule mapped to MITRE categories', async () => {
const rule1 = await createRule(
supertest,
log,
getCustomQueryRuleParams({ threat: generateThreatArray(1) })
);
const body = await getCoverageOverview(supertest);
expect(body).to.eql({
coverage: {
T001: [rule1.id],
TA001: [rule1.id],
'T001.001': [rule1.id],
},
unmapped_rule_ids: [],
rules_data: {
[rule1.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
it('returns response with an unmapped rule', async () => {
const rule1 = await createRule(
supertest,
log,
getCustomQueryRuleParams({ threat: undefined })
);
const body = await getCoverageOverview(supertest);
expect(body).to.eql({
coverage: {},
unmapped_rule_ids: [rule1.id],
rules_data: {
[rule1.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
});
describe('with filters', () => {
describe('search_term', () => {
it('returns response filtered by tactic', async () => {
await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(1) })
);
const expectedRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-2', threat: generateThreatArray(2) })
);
const body = await getCoverageOverview(supertest, {
search_term: 'TA002',
});
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
it('returns response filtered by technique', async () => {
await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(1) })
);
const expectedRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-2', threat: generateThreatArray(2) })
);
const body = await getCoverageOverview(supertest, {
search_term: 'T002',
});
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
it('returns response filtered by subtechnique', async () => {
await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(1) })
);
const expectedRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-2', threat: generateThreatArray(2) })
);
const body = await getCoverageOverview(supertest, {
search_term: 'T002.002',
});
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
it('returns response filtered by rule name', async () => {
await createRule(supertest, log, getCustomQueryRuleParams({ rule_id: 'rule-1' }));
const expectedRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-2', name: 'rule-2' })
);
const body = await getCoverageOverview(supertest, {
search_term: 'rule-2',
});
expect(body).to.eql({
coverage: {},
unmapped_rule_ids: [expectedRule.id],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'rule-2',
},
},
});
});
it('returns response filtered by index pattern', async () => {
await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', index: ['index-pattern-1'] })
);
const expectedRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-2', index: ['index-pattern-2'] })
);
const body = await getCoverageOverview(supertest, {
search_term: 'index-pattern-2',
});
expect(body).to.eql({
coverage: {},
unmapped_rule_ids: [expectedRule.id],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
});
describe('activity', () => {
it('returns response filtered by disabled rules', async () => {
const expectedRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(1) })
);
await createRule(
supertest,
log,
getCustomQueryRuleParams({
rule_id: 'rule-2',
enabled: true,
threat: generateThreatArray(2),
})
);
const body = await getCoverageOverview(supertest, {
activity: [CoverageOverviewRuleActivity.Disabled],
});
expect(body).to.eql({
coverage: {
T001: [expectedRule.id],
TA001: [expectedRule.id],
'T001.001': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
it('returns response filtered by enabled rules', async () => {
await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(1) })
);
const expectedRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({
rule_id: 'rule-2',
enabled: true,
threat: generateThreatArray(2),
})
);
const body = await getCoverageOverview(supertest, {
activity: [CoverageOverviewRuleActivity.Enabled],
});
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'enabled',
name: 'Custom query rule',
},
},
});
});
it('returns all rules if both enabled and disabled filters are specified in the request', async () => {
const expectedRule1 = await createRule(
supertest,
log,
getCustomQueryRuleParams({
rule_id: 'rule-1',
enabled: false,
name: 'Disabled rule',
threat: generateThreatArray(1),
})
);
const expectedRule2 = await createRule(
supertest,
log,
getCustomQueryRuleParams({
rule_id: 'rule-2',
enabled: true,
name: 'Enabled rule',
threat: generateThreatArray(2),
})
);
const body = await getCoverageOverview(supertest, {
activity: [
CoverageOverviewRuleActivity.Enabled,
CoverageOverviewRuleActivity.Disabled,
],
});
expect(body).to.eql({
coverage: {
T001: [expectedRule1.id],
TA001: [expectedRule1.id],
'T001.001': [expectedRule1.id],
T002: [expectedRule2.id],
TA002: [expectedRule2.id],
'T002.002': [expectedRule2.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule1.id]: {
activity: 'disabled',
name: 'Disabled rule',
},
[expectedRule2.id]: {
activity: 'enabled',
name: 'Enabled rule',
},
},
});
});
it('returns all rules if neither enabled and disabled filters are specified in the request', async () => {
const expectedRule1 = await createRule(
supertest,
log,
getCustomQueryRuleParams({
rule_id: 'rule-1',
enabled: false,
name: 'Disabled rule',
threat: generateThreatArray(1),
})
);
const expectedRule2 = await createRule(
supertest,
log,
getCustomQueryRuleParams({
rule_id: 'rule-2',
enabled: true,
name: 'Enabled rule',
threat: generateThreatArray(2),
})
);
const body = await getCoverageOverview(supertest);
expect(body).to.eql({
coverage: {
T001: [expectedRule1.id],
TA001: [expectedRule1.id],
'T001.001': [expectedRule1.id],
T002: [expectedRule2.id],
TA002: [expectedRule2.id],
'T002.002': [expectedRule2.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule1.id]: {
activity: 'disabled',
name: 'Disabled rule',
},
[expectedRule2.id]: {
activity: 'enabled',
name: 'Enabled rule',
},
},
});
});
});
describe('source', () => {
it('returns response filtered by custom rules', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({
rule_id: 'prebuilt-rule-1',
threat: generateThreatArray(1),
}),
]);
await installPrebuiltRulesAndTimelines(es, supertest);
const expectedRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(2) })
);
const body = await getCoverageOverview(supertest, {
source: [CoverageOverviewRuleSource.Custom],
});
expect(body).to.eql({
coverage: {
T002: [expectedRule.id],
TA002: [expectedRule.id],
'T002.002': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
it('returns response filtered by prebuilt rules', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({
rule_id: 'prebuilt-rule-1',
threat: generateThreatArray(1),
}),
]);
const {
results: { created },
} = await installPrebuiltRules(es, supertest);
const expectedRule = created[0];
await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(2) })
);
const body = await getCoverageOverview(supertest, {
source: [CoverageOverviewRuleSource.Prebuilt],
});
expect(body).to.eql({
coverage: {
T001: [expectedRule.id],
TA001: [expectedRule.id],
'T001.001': [expectedRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedRule.id]: {
activity: 'disabled',
name: 'Query with a rule id',
},
},
});
});
it('returns all rules if both custom and prebuilt filters are specified in the request', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({
rule_id: 'prebuilt-rule-1',
threat: generateThreatArray(1),
}),
]);
const {
results: { created },
} = await installPrebuiltRules(es, supertest);
const expectedPrebuiltRule = created[0];
const expectedCustomRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(2) })
);
const body = await getCoverageOverview(supertest, {
source: [CoverageOverviewRuleSource.Prebuilt, CoverageOverviewRuleSource.Custom],
});
expect(body).to.eql({
coverage: {
T001: [expectedPrebuiltRule.id],
TA001: [expectedPrebuiltRule.id],
'T001.001': [expectedPrebuiltRule.id],
T002: [expectedCustomRule.id],
TA002: [expectedCustomRule.id],
'T002.002': [expectedCustomRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedPrebuiltRule.id]: {
activity: 'disabled',
name: 'Query with a rule id',
},
[expectedCustomRule.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
it('returns all rules if neither custom and prebuilt filters are specified in the request', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({
rule_id: 'prebuilt-rule-1',
threat: generateThreatArray(1),
}),
]);
const {
results: { created },
} = await installPrebuiltRules(es, supertest);
const expectedPrebuiltRule = created[0];
const expectedCustomRule = await createRule(
supertest,
log,
getCustomQueryRuleParams({ rule_id: 'rule-1', threat: generateThreatArray(2) })
);
const body = await getCoverageOverview(supertest);
expect(body).to.eql({
coverage: {
T001: [expectedPrebuiltRule.id],
TA001: [expectedPrebuiltRule.id],
'T001.001': [expectedPrebuiltRule.id],
T002: [expectedCustomRule.id],
TA002: [expectedCustomRule.id],
'T002.002': [expectedCustomRule.id],
},
unmapped_rule_ids: [],
rules_data: {
[expectedPrebuiltRule.id]: {
activity: 'disabled',
name: 'Query with a rule id',
},
[expectedCustomRule.id]: {
activity: 'disabled',
name: 'Custom query rule',
},
},
});
});
});
});
describe('error cases', async () => {
it('throws error when request body is not valid', async () => {
const { body } = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.set('x-elastic-internal-origin', 'foo')
.send({ filter: { source: ['give me all the rules'] } })
.expect(400);
expect(body).to.eql({
statusCode: 400,
error: 'Bad Request',
message:
'[request body]: Invalid value "give me all the rules" supplied to "filter,source"',
});
});
});
});
});
};
function generateThreatArray(startIndex: number, count = 1): ThreatArray {
const result: ThreatArray = [];
for (let i = 0; i < count; ++i) {
const indexName = (i + startIndex).toString().padStart(3, '0');
result.push({
framework: 'MITRE ATT&CK',
tactic: {
id: `TA${indexName}`,
name: `Tactic ${indexName}`,
reference: `http://some-link-${indexName}`,
},
technique: [
{
id: `T${indexName}`,
name: `Technique ${indexName}`,
reference: `http://some-technique-link-${indexName}`,
subtechnique: [
{
id: `T${indexName}.${indexName}`,
name: `Subtechnique ${indexName}`,
reference: `http://some-sub-technique-link-${indexName}`,
},
],
},
],
});
}
return result;
}

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 { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Rule management API', function () {
loadTestFile(require.resolve('./coverage_overview'));
});
}

View file

@ -18,6 +18,7 @@ const SIMPLE_APM_RULE_DATA = {
windowSize: 30,
windowUnit: 'm',
anomalySeverityType: 'critical',
anomalyDetectorTypes: ['txLatency'],
environment: 'ENVIRONMENT_ALL',
},
schedule: {

View file

@ -0,0 +1,28 @@
/*
* 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 SuperTest from 'supertest';
import {
CoverageOverviewFilter,
CoverageOverviewResponse,
RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
export const getCoverageOverview = async (
supertest: SuperTest.SuperTest<SuperTest.Test>,
filter?: CoverageOverviewFilter
): Promise<CoverageOverviewResponse> => {
const response = await supertest
.post(RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.set('x-elastic-internal-origin', 'foo')
.send({ filter })
.expect(200);
return response.body;
};

View file

@ -0,0 +1,439 @@
/*
* 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 {
getDuplicateTechniqueThreatData,
getMockThreatData,
} from '@kbn/security-solution-plugin/public/detections/mitre/mitre_tactics_techniques';
import { Threat } from '@kbn/securitysolution-io-ts-alerting-types';
import {
COVERAGE_OVERVIEW_ENABLE_ALL_DISABLED_BUTTON,
COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES,
COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES,
COVERAGE_OVERVIEW_TACTIC_DISABLED_STATS,
COVERAGE_OVERVIEW_TACTIC_ENABLED_STATS,
COVERAGE_OVERVIEW_TACTIC_PANEL,
} from '../../../../screens/rules_coverage_overview';
import { createRule } from '../../../../tasks/api_calls/rules';
import { visit } from '../../../../tasks/navigation';
import { RULES_COVERAGE_OVERVIEW_URL } from '../../../../urls/rules_management';
import { createRuleAssetSavedObject } from '../../../../helpers/rules';
import { getNewRule } from '../../../../objects/rule';
import {
createAndInstallMockedPrebuiltRules,
preventPrebuiltRulesPackageInstallation,
} from '../../../../tasks/api_calls/prebuilt_rules';
import {
deleteAlertsAndRules,
deletePrebuiltRulesAssets,
} from '../../../../tasks/api_calls/common';
import { login } from '../../../../tasks/login';
import {
enableAllDisabledRules,
filterCoverageOverviewBySearchBar,
openTechniquePanelByName,
openTechniquePanelByNameAndTacticId,
selectCoverageOverviewActivityFilterOption,
selectCoverageOverviewSourceFilterOption,
} from '../../../../tasks/rules_coverage_overview';
// Mitre data used in base case tests
const EnabledPrebuiltRuleMitreData = getMockThreatData()[0];
const DisabledPrebuiltRuleMitreData = getMockThreatData()[1];
const EnabledCustomRuleMitreData = getMockThreatData()[2];
const DisabledCustomRuleMitreData = getMockThreatData()[3];
// Mitre data used for duplicate technique tests
const DuplicateTechniqueMitreData1 = getDuplicateTechniqueThreatData()[1];
const DuplicateTechniqueMitreData2 = getDuplicateTechniqueThreatData()[0];
const MockEnabledPrebuiltRuleThreat: Threat = {
framework: 'MITRE ATT&CK',
tactic: {
name: EnabledPrebuiltRuleMitreData.tactic.name,
id: EnabledPrebuiltRuleMitreData.tactic.id,
reference: EnabledPrebuiltRuleMitreData.tactic.reference,
},
technique: [
{
id: EnabledPrebuiltRuleMitreData.technique.id,
reference: EnabledPrebuiltRuleMitreData.technique.reference,
name: EnabledPrebuiltRuleMitreData.technique.name,
subtechnique: [
{
id: EnabledPrebuiltRuleMitreData.subtechnique.id,
name: EnabledPrebuiltRuleMitreData.subtechnique.name,
reference: EnabledPrebuiltRuleMitreData.subtechnique.reference,
},
],
},
{
name: EnabledPrebuiltRuleMitreData.technique.name,
id: EnabledPrebuiltRuleMitreData.technique.id,
reference: EnabledPrebuiltRuleMitreData.technique.reference,
subtechnique: [],
},
],
};
const MockDisabledPrebuiltRuleThreat: Threat = {
framework: 'MITRE ATT&CK',
tactic: {
name: DisabledPrebuiltRuleMitreData.tactic.name,
id: DisabledPrebuiltRuleMitreData.tactic.id,
reference: DisabledPrebuiltRuleMitreData.tactic.reference,
},
technique: [
{
id: DisabledPrebuiltRuleMitreData.technique.id,
reference: DisabledPrebuiltRuleMitreData.technique.reference,
name: DisabledPrebuiltRuleMitreData.technique.name,
subtechnique: [
{
id: DisabledPrebuiltRuleMitreData.subtechnique.id,
name: DisabledPrebuiltRuleMitreData.subtechnique.name,
reference: DisabledPrebuiltRuleMitreData.subtechnique.reference,
},
],
},
],
};
const MockEnabledCustomRuleThreat: Threat = {
framework: 'MITRE ATT&CK',
tactic: {
name: EnabledCustomRuleMitreData.tactic.name,
id: EnabledCustomRuleMitreData.tactic.id,
reference: EnabledCustomRuleMitreData.tactic.reference,
},
technique: [
{
id: EnabledCustomRuleMitreData.technique.id,
reference: EnabledCustomRuleMitreData.technique.reference,
name: EnabledCustomRuleMitreData.technique.name,
subtechnique: [
{
id: EnabledCustomRuleMitreData.subtechnique.id,
name: EnabledCustomRuleMitreData.subtechnique.name,
reference: EnabledCustomRuleMitreData.subtechnique.reference,
},
],
},
],
};
const MockDisabledCustomRuleThreat: Threat = {
framework: 'MITRE ATT&CK',
tactic: {
name: DisabledCustomRuleMitreData.tactic.name,
id: DisabledCustomRuleMitreData.tactic.id,
reference: DisabledCustomRuleMitreData.tactic.reference,
},
technique: [
{
id: DisabledCustomRuleMitreData.technique.id,
reference: DisabledCustomRuleMitreData.technique.reference,
name: DisabledCustomRuleMitreData.technique.name,
},
],
};
const MockCustomRuleDuplicateTechniqueThreat1: Threat = {
framework: 'MITRE ATT&CK',
tactic: {
name: DuplicateTechniqueMitreData1.tactic.name,
id: DuplicateTechniqueMitreData1.tactic.id,
reference: DuplicateTechniqueMitreData1.tactic.reference,
},
technique: [
{
id: DuplicateTechniqueMitreData1.technique.id,
reference: DuplicateTechniqueMitreData1.technique.reference,
name: DuplicateTechniqueMitreData1.technique.name,
},
],
};
const MockCustomRuleDuplicateTechniqueThreat2: Threat = {
framework: 'MITRE ATT&CK',
tactic: {
name: DuplicateTechniqueMitreData2.tactic.name,
id: DuplicateTechniqueMitreData2.tactic.id,
reference: DuplicateTechniqueMitreData2.tactic.reference,
},
technique: [
{
id: DuplicateTechniqueMitreData2.technique.id,
reference: DuplicateTechniqueMitreData2.technique.reference,
name: DuplicateTechniqueMitreData2.technique.name,
},
],
};
const prebuiltRules = [
createRuleAssetSavedObject({
name: `Enabled prebuilt rule`,
rule_id: `enabled_prebuilt_rule`,
enabled: true,
threat: [MockEnabledPrebuiltRuleThreat],
}),
createRuleAssetSavedObject({
name: `Disabled prebuilt rule`,
rule_id: `disabled_prebuilt_rule`,
enabled: false,
threat: [MockDisabledPrebuiltRuleThreat],
}),
];
describe('Coverage overview', { tags: ['@ess', '@serverless'] }, () => {
describe('base cases', () => {
beforeEach(() => {
login();
deleteAlertsAndRules();
deletePrebuiltRulesAssets();
preventPrebuiltRulesPackageInstallation();
createAndInstallMockedPrebuiltRules(prebuiltRules);
createRule(
getNewRule({
rule_id: 'enabled_custom_rule',
enabled: true,
name: 'Enabled custom rule',
threat: [MockEnabledCustomRuleThreat],
})
);
createRule(
getNewRule({
rule_id: 'disabled_custom_rule',
name: 'Disabled custom rule',
enabled: false,
threat: [MockDisabledCustomRuleThreat],
})
);
visit(RULES_COVERAGE_OVERVIEW_URL);
});
it('technique panel renders custom and prebuilt rule data on page load', () => {
openTechniquePanelByName(EnabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES).contains('Enabled prebuilt rule');
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES)
.contains('Enabled custom rule')
.should('not.exist');
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Disabled prebuilt rule')
.should('not.exist');
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Disabled custom rule')
.should('not.exist');
cy.get(COVERAGE_OVERVIEW_ENABLE_ALL_DISABLED_BUTTON).should('be.disabled');
});
describe('filtering tests', () => {
it('filters for all data', () => {
selectCoverageOverviewActivityFilterOption('Disabled rules'); // Activates disabled rules filter as it's off by default on page load
openTechniquePanelByName(EnabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES).contains('Enabled prebuilt rule');
openTechniquePanelByName(DisabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES).contains('Disabled prebuilt rule');
openTechniquePanelByName(EnabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES).contains('Enabled custom rule');
openTechniquePanelByName(DisabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES).contains('Disabled custom rule');
});
it('filters for disabled and prebuilt rules', () => {
selectCoverageOverviewActivityFilterOption('Enabled rules'); // Disables default filter
selectCoverageOverviewActivityFilterOption('Disabled rules'); // Activates disabled rules filter as it's off by default on page load
selectCoverageOverviewSourceFilterOption('Custom rules'); // Disables default filter
openTechniquePanelByName(EnabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES)
.contains('Enabled prebuilt rule')
.should('not.exist');
openTechniquePanelByName(DisabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES).contains('Disabled prebuilt rule');
openTechniquePanelByName(EnabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES)
.contains('Enabled custom rule')
.should('not.exist');
openTechniquePanelByName(DisabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Disabled custom rule')
.should('not.exist');
});
it('filters for only prebuilt rules', () => {
selectCoverageOverviewActivityFilterOption('Disabled rules'); // Activates disabled rules filter as it's off by default on page load
selectCoverageOverviewSourceFilterOption('Custom rules'); // Disables default filter
openTechniquePanelByName(EnabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES).contains('Enabled prebuilt rule');
openTechniquePanelByName(DisabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES).contains('Disabled prebuilt rule');
openTechniquePanelByName(EnabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES)
.contains('Enabled custom rule')
.should('not.exist');
openTechniquePanelByName(DisabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Disabled custom rule')
.should('not.exist');
});
it('filters for only custom rules', () => {
selectCoverageOverviewActivityFilterOption('Disabled rules'); // Activates disabled rules filter as it's off by default on page load
selectCoverageOverviewSourceFilterOption('Elastic rules'); // Disables default filter
openTechniquePanelByName(EnabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES)
.contains('Enabled prebuilt rule')
.should('not.exist');
openTechniquePanelByName(DisabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Disabled prebuilt rule')
.should('not.exist');
openTechniquePanelByName(EnabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES).contains('Enabled custom rule');
openTechniquePanelByName(DisabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES).contains('Disabled custom rule');
});
it('filters for search term', () => {
filterCoverageOverviewBySearchBar('Enabled custom rule'); // Disables default filter
openTechniquePanelByName(EnabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES)
.contains('Enabled prebuilt rule')
.should('not.exist');
openTechniquePanelByName(DisabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Disabled prebuilt rule')
.should('not.exist');
openTechniquePanelByName(EnabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES).contains('Enabled custom rule');
openTechniquePanelByName(DisabledCustomRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Disabled custom rule')
.should('not.exist');
});
});
it('enables all disabled rules', () => {
selectCoverageOverviewActivityFilterOption('Disabled rules'); // Activates disabled rules filter as it's off by default on page load
openTechniquePanelByName(DisabledPrebuiltRuleMitreData.technique.name);
enableAllDisabledRules();
// Should now render all rules in "enabled" section
openTechniquePanelByName(DisabledPrebuiltRuleMitreData.technique.name);
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES).contains('Disabled prebuilt rule');
// Shouldn't render the rules in "disabled" section
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Disabled prebuilt rule')
.should('not.exist');
cy.get(COVERAGE_OVERVIEW_ENABLE_ALL_DISABLED_BUTTON).should('be.disabled');
});
});
describe('with rules that have identical mitre techniques that belong to multiple tactics', () => {
const SharedTechniqueName = DuplicateTechniqueMitreData1.technique.name;
const TacticOfRule1 = DuplicateTechniqueMitreData1.tactic;
const TacticOfRule2 = DuplicateTechniqueMitreData2.tactic;
beforeEach(() => {
login();
deleteAlertsAndRules();
deletePrebuiltRulesAssets();
createRule(
getNewRule({
rule_id: 'duplicate_technique_rule_1',
enabled: true,
name: 'Rule under Persistence tactic',
threat: [MockCustomRuleDuplicateTechniqueThreat1],
})
);
createRule(
getNewRule({
rule_id: 'duplicate_technique_rule_2',
name: 'Rule under Privilege Escalation tactic',
enabled: false,
threat: [MockCustomRuleDuplicateTechniqueThreat2],
})
);
visit(RULES_COVERAGE_OVERVIEW_URL);
});
it('technique panels render unique rule data', () => {
// Tests to make sure each rule only exists in the specific technique and tactic that's assigned to it
selectCoverageOverviewActivityFilterOption('Disabled rules'); // Activates disabled rules filter as it's off by default on page load
// Open duplicated technique panel under Persistence tactic
openTechniquePanelByNameAndTacticId(SharedTechniqueName, TacticOfRule1.id);
// Only rule 1 data is present
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES).contains('Rule under Persistence tactic');
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES)
.contains('Rule under Privilege Escalation tactic')
.should('not.exist');
// Open duplicated technique panel under Privilege Escalation tactic
openTechniquePanelByNameAndTacticId(SharedTechniqueName, TacticOfRule2.id);
// Only rule 2 data is present
cy.get(COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES)
.contains('Rule under Persistence tactic')
.should('not.exist');
cy.get(COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES).contains(
'Rule under Privilege Escalation tactic'
);
});
it('tactic panels render correct rule stats', () => {
selectCoverageOverviewActivityFilterOption('Disabled rules'); // Activates disabled rules filter as it's off by default on page load
// Validate rule count stats for the Persistence tactic only show stats based on its own technique
// Enabled rule count
cy.get(COVERAGE_OVERVIEW_TACTIC_PANEL)
.contains(TacticOfRule1.name)
.get(COVERAGE_OVERVIEW_TACTIC_ENABLED_STATS)
.contains('0');
// Disabled rule count
cy.get(COVERAGE_OVERVIEW_TACTIC_PANEL)
.contains(TacticOfRule1.name)
.get(COVERAGE_OVERVIEW_TACTIC_DISABLED_STATS)
.contains('1');
// Validate rule count stats for the Privilege Escalation tactic only show stats based on its own technique
// Enabled rule count
cy.get(COVERAGE_OVERVIEW_TACTIC_PANEL)
.contains(TacticOfRule2.name)
.get(COVERAGE_OVERVIEW_TACTIC_ENABLED_STATS)
.contains('1');
// Disabled rule count
cy.get(COVERAGE_OVERVIEW_TACTIC_PANEL)
.contains(TacticOfRule2.name)
.get(COVERAGE_OVERVIEW_TACTIC_DISABLED_STATS)
.contains('0');
});
});
});

View file

@ -39,27 +39,27 @@ export const getThreatIndexPatterns = (): string[] => ['logs-ti_*'];
const getMitre1 = (): Threat => ({
framework: 'MITRE ATT&CK',
tactic: {
name: getMockThreatData().tactic.name,
id: getMockThreatData().tactic.id,
reference: getMockThreatData().tactic.reference,
name: getMockThreatData()[0].tactic.name,
id: getMockThreatData()[0].tactic.id,
reference: getMockThreatData()[0].tactic.reference,
},
technique: [
{
id: getMockThreatData().technique.id,
reference: getMockThreatData().technique.reference,
name: getMockThreatData().technique.name,
id: getMockThreatData()[0].technique.id,
reference: getMockThreatData()[0].technique.reference,
name: getMockThreatData()[0].technique.name,
subtechnique: [
{
id: getMockThreatData().subtechnique.id,
name: getMockThreatData().subtechnique.name,
reference: getMockThreatData().subtechnique.reference,
id: getMockThreatData()[0].subtechnique.id,
name: getMockThreatData()[0].subtechnique.name,
reference: getMockThreatData()[0].subtechnique.reference,
},
],
},
{
name: getMockThreatData().technique.name,
id: getMockThreatData().technique.id,
reference: getMockThreatData().technique.reference,
name: getMockThreatData()[0].technique.name,
id: getMockThreatData()[0].technique.id,
reference: getMockThreatData()[0].technique.reference,
subtechnique: [],
},
],
@ -68,20 +68,20 @@ const getMitre1 = (): Threat => ({
const getMitre2 = (): Threat => ({
framework: 'MITRE ATT&CK',
tactic: {
name: getMockThreatData().tactic.name,
id: getMockThreatData().tactic.id,
reference: getMockThreatData().tactic.reference,
name: getMockThreatData()[1].tactic.name,
id: getMockThreatData()[1].tactic.id,
reference: getMockThreatData()[1].tactic.reference,
},
technique: [
{
id: getMockThreatData().technique.id,
reference: getMockThreatData().technique.reference,
name: getMockThreatData().technique.name,
id: getMockThreatData()[1].technique.id,
reference: getMockThreatData()[1].technique.reference,
name: getMockThreatData()[1].technique.name,
subtechnique: [
{
id: getMockThreatData().subtechnique.id,
name: getMockThreatData().subtechnique.name,
reference: getMockThreatData().subtechnique.reference,
id: getMockThreatData()[1].subtechnique.id,
name: getMockThreatData()[1].subtechnique.name,
reference: getMockThreatData()[1].subtechnique.reference,
},
],
},

View file

@ -0,0 +1,39 @@
/*
* 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.
*/
export const COVERAGE_OVERVIEW_TECHNIQUE_PANEL =
'[data-test-subj="coverageOverviewTechniquePanel"]';
export const COVERAGE_OVERVIEW_TECHNIQUE_PANEL_IN_TACTIC_GROUP = (id: string) =>
`[data-test-subj="coverageOverviewTacticGroup-${id}"] [data-test-subj="coverageOverviewTechniquePanel"]`;
export const COVERAGE_OVERVIEW_POPOVER_ENABLED_RULES =
'[data-test-subj="coverageOverviewEnabledRulesList"]';
export const COVERAGE_OVERVIEW_POPOVER_DISABLED_RULES =
'[data-test-subj="coverageOverviewDisabledRulesList"]';
export const COVERAGE_OVERVIEW_ENABLE_ALL_DISABLED_BUTTON =
'[data-test-subj="enableAllDisabledButton"]';
export const COVERAGE_OVERVIEW_ACTIVITY_FILTER_BUTTON =
'[data-test-subj="coverageOverviewRuleActivityFilterButton"]';
export const COVERAGE_OVERVIEW_SOURCE_FILTER_BUTTON =
'[data-test-subj="coverageOverviewRuleSourceFilterButton"]';
export const COVERAGE_OVERVIEW_FILTER_LIST = '[data-test-subj="coverageOverviewFilterList"]';
export const COVERAGE_OVERVIEW_SEARCH_BAR = '[data-test-subj="coverageOverviewFilterSearchBar"]';
export const COVERAGE_OVERVIEW_TACTIC_PANEL = '[data-test-subj="coverageOverviewTacticPanel"]';
export const COVERAGE_OVERVIEW_TACTIC_ENABLED_STATS =
'[data-test-subj="ruleStatsEnabledRulesCount"]';
export const COVERAGE_OVERVIEW_TACTIC_DISABLED_STATS =
'[data-test-subj="ruleStatsDisabledRulesCount"]';

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 {
COVERAGE_OVERVIEW_ACTIVITY_FILTER_BUTTON,
COVERAGE_OVERVIEW_ENABLE_ALL_DISABLED_BUTTON,
COVERAGE_OVERVIEW_FILTER_LIST,
COVERAGE_OVERVIEW_SEARCH_BAR,
COVERAGE_OVERVIEW_SOURCE_FILTER_BUTTON,
COVERAGE_OVERVIEW_TECHNIQUE_PANEL_IN_TACTIC_GROUP,
COVERAGE_OVERVIEW_TECHNIQUE_PANEL,
} from '../screens/rules_coverage_overview';
import { LOADING_INDICATOR } from '../screens/security_header';
export const openTechniquePanelByName = (label: string) => {
cy.get(COVERAGE_OVERVIEW_TECHNIQUE_PANEL).contains(label).click();
};
export const openTechniquePanelByNameAndTacticId = (label: string, tacticId: string) => {
cy.get(COVERAGE_OVERVIEW_TECHNIQUE_PANEL_IN_TACTIC_GROUP(tacticId)).contains(label).click();
};
export const selectCoverageOverviewActivityFilterOption = (option: string) => {
cy.get(COVERAGE_OVERVIEW_ACTIVITY_FILTER_BUTTON).click(); // open filter popover
cy.get(COVERAGE_OVERVIEW_FILTER_LIST).contains(option).click();
cy.get(LOADING_INDICATOR).should('not.exist');
cy.get(COVERAGE_OVERVIEW_ACTIVITY_FILTER_BUTTON).click(); // close filter popover
};
export const selectCoverageOverviewSourceFilterOption = (option: string) => {
cy.get(COVERAGE_OVERVIEW_SOURCE_FILTER_BUTTON).click(); // open filter popover
cy.get(COVERAGE_OVERVIEW_FILTER_LIST).contains(option).click();
cy.get(LOADING_INDICATOR).should('not.exist');
cy.get(COVERAGE_OVERVIEW_SOURCE_FILTER_BUTTON).click(); // close filter popover
};
export const filterCoverageOverviewBySearchBar = (searchTerm: string) => {
cy.get(COVERAGE_OVERVIEW_SEARCH_BAR).type(`${searchTerm}`);
cy.get(COVERAGE_OVERVIEW_SEARCH_BAR).focus();
cy.get(COVERAGE_OVERVIEW_SEARCH_BAR).realType('{enter}');
};
export const enableAllDisabledRules = () => {
cy.get(COVERAGE_OVERVIEW_ENABLE_ALL_DISABLED_BUTTON).click();
cy.get(COVERAGE_OVERVIEW_ENABLE_ALL_DISABLED_BUTTON).should('not.exist');
cy.get(LOADING_INDICATOR).should('not.exist');
};

View file

@ -7,3 +7,4 @@
export const RULES_MANAGEMENT_URL = '/app/security/rules/management';
export const RULES_MONITORING_URL = '/app/security/rules/monitoring';
export const RULES_COVERAGE_OVERVIEW_URL = '/app/security/rules_coverage_overview';