mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
60c3ff05a6
commit
315cf9d399
28 changed files with 1624 additions and 608 deletions
|
@ -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
1
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -87,7 +87,7 @@ const RuleSourceFilterComponent = ({
|
|||
`}
|
||||
>
|
||||
<EuiPopover
|
||||
id="ruleActivityFilterPopover"
|
||||
id="ruleSourceFilterPopover"
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -18,6 +18,7 @@ const SIMPLE_APM_RULE_DATA = {
|
|||
windowSize: 30,
|
||||
windowUnit: 'm',
|
||||
anomalySeverityType: 'critical',
|
||||
anomalyDetectorTypes: ['txLatency'],
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
},
|
||||
schedule: {
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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"]';
|
|
@ -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');
|
||||
};
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue