Skip to content

Commit

Permalink
[Security Solution] Adds tests for coverage overview page (elastic#16…
Browse files Browse the repository at this point in the history
…8058)

**Resolves: elastic#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)
  • Loading branch information
dplumlee authored Jan 9, 2024
1 parent 60c3ff0 commit 315cf9d
Show file tree
Hide file tree
Showing 28 changed files with 1,626 additions and 610 deletions.
2 changes: 2 additions & 0 deletions .buildkite/ftr_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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({});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -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 }) => {
Expand All @@ -39,6 +43,11 @@ export const useFetchCoverageOverviewQuery = (
{
...DEFAULT_QUERY_OPTIONS,
...options,
onError: (error) => {
addError(error, {
title: i18n.COVERAGE_OVERVIEW_FETCH_ERROR_TITLE,
});
},
}
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const RuleSourceFilterComponent = ({
`}
>
<EuiPopover
id="ruleActivityFilterPopover"
id="ruleSourceFilterPopover"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
technique: {
name: 'OS Credential Dumping',
id: 'T1003',
reference: 'https://attack.mitre.org/techniques/T1003',
tactics: ['credential-access'],
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',
},
},
{
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',
},
},
{
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',
},
},
subtechnique: {
name: '/etc/passwd and /etc/shadow',
id: 'T1003.008',
reference: 'https://attack.mitre.org/techniques/T1003/008',
tactics: ['credential-access'],
techniqueId: 'T1003',
];

/**
* 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'],
},
},
});
];
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading

0 comments on commit 315cf9d

Please sign in to comment.