mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cloud Security] Filter muted findings
This commit is contained in:
parent
a3f3386081
commit
b043cf0ff1
7 changed files with 303 additions and 44 deletions
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
|
||||
import { CspBenchmarkRulesStates } from '../types/latest';
|
||||
|
||||
export const buildMutedRulesFilter = (
|
||||
rulesStates: CspBenchmarkRulesStates
|
||||
): QueryDslQueryContainer[] => {
|
||||
const mutedRules = Object.fromEntries(
|
||||
Object.entries(rulesStates).filter(([key, value]) => value.muted === true)
|
||||
);
|
||||
|
||||
const mutedRulesFilterQuery = Object.keys(mutedRules).map((key) => {
|
||||
const rule = mutedRules[key];
|
||||
return {
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'rule.benchmark.id': rule.benchmark_id } },
|
||||
{ term: { 'rule.benchmark.version': rule.benchmark_version } },
|
||||
{ term: { 'rule.benchmark.rule_number': rule.rule_number } },
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mutedRulesFilterQuery;
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { CspBenchmarkRulesStates } from '../../../../common/types/latest';
|
||||
import {
|
||||
CSP_GET_BENCHMARK_RULES_STATE_API_CURRENT_VERSION,
|
||||
CSP_GET_BENCHMARK_RULES_STATE_ROUTE_PATH,
|
||||
} from '../../../../common/constants';
|
||||
import { useKibana } from '../../../common/hooks/use_kibana';
|
||||
|
||||
const getRuleStatesKey = 'get_rules_state_key';
|
||||
|
||||
export const useGetCspBenchmarkRulesStatesApi = () => {
|
||||
const { http } = useKibana().services;
|
||||
return useQuery<CspBenchmarkRulesStates, unknown, CspBenchmarkRulesStates>(
|
||||
[getRuleStatesKey],
|
||||
() =>
|
||||
http.get<CspBenchmarkRulesStates>(CSP_GET_BENCHMARK_RULES_STATE_ROUTE_PATH, {
|
||||
version: CSP_GET_BENCHMARK_RULES_STATE_API_CURRENT_VERSION,
|
||||
})
|
||||
);
|
||||
};
|
|
@ -22,6 +22,9 @@ import {
|
|||
} from '../../../../common/constants';
|
||||
import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants';
|
||||
import { showErrorToast } from '../../../common/utils/show_error_toast';
|
||||
import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api';
|
||||
import { CspBenchmarkRulesStates } from '../../../../common/types/latest';
|
||||
import { buildMutedRulesFilter } from '../../../../common/utils/rules_states';
|
||||
|
||||
interface UseFindingsOptions extends FindingsBaseEsQuery {
|
||||
sort: string[][];
|
||||
|
@ -42,31 +45,40 @@ interface FindingsAggs {
|
|||
count: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
|
||||
}
|
||||
|
||||
export const getFindingsQuery = ({ query, sort }: UseFindingsOptions, pageParam: any) => ({
|
||||
index: CSP_LATEST_FINDINGS_DATA_VIEW,
|
||||
sort: getMultiFieldsSort(sort),
|
||||
size: MAX_FINDINGS_TO_LOAD,
|
||||
aggs: getFindingsCountAggQuery(),
|
||||
ignore_unavailable: false,
|
||||
query: {
|
||||
...query,
|
||||
bool: {
|
||||
...query?.bool,
|
||||
filter: [
|
||||
...(query?.bool?.filter ?? []),
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`,
|
||||
lte: 'now',
|
||||
export const getFindingsQuery = (
|
||||
{ query, sort }: UseFindingsOptions,
|
||||
rulesStates: CspBenchmarkRulesStates,
|
||||
pageParam: any
|
||||
) => {
|
||||
const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates);
|
||||
|
||||
return {
|
||||
index: CSP_LATEST_FINDINGS_DATA_VIEW,
|
||||
sort: getMultiFieldsSort(sort),
|
||||
size: MAX_FINDINGS_TO_LOAD,
|
||||
aggs: getFindingsCountAggQuery(),
|
||||
ignore_unavailable: false,
|
||||
query: {
|
||||
...query,
|
||||
bool: {
|
||||
...query?.bool,
|
||||
filter: [
|
||||
...(query?.bool?.filter ?? []),
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`,
|
||||
lte: 'now',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
must_not: mutedRulesFilterQuery,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(pageParam ? { search_after: pageParam } : {}),
|
||||
});
|
||||
...(pageParam ? { search_after: pageParam } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const getMultiFieldsSort = (sort: string[][]) => {
|
||||
return sort.map(([id, direction]) => {
|
||||
|
@ -111,6 +123,8 @@ export const useLatestFindings = (options: UseFindingsOptions) => {
|
|||
data,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi();
|
||||
|
||||
return useInfiniteQuery(
|
||||
['csp_findings', { params: options }],
|
||||
async ({ pageParam }) => {
|
||||
|
@ -118,7 +132,7 @@ export const useLatestFindings = (options: UseFindingsOptions) => {
|
|||
rawResponse: { hits, aggregations },
|
||||
} = await lastValueFrom(
|
||||
data.search.search<LatestFindingsRequest, LatestFindingsResponse>({
|
||||
params: getFindingsQuery(options, pageParam),
|
||||
params: getFindingsQuery(options, rulesStates!, pageParam), // ruleStates always exists since it under the `enabled` dependency.
|
||||
})
|
||||
);
|
||||
if (!aggregations) throw new Error('expected aggregations to be an defined');
|
||||
|
@ -132,7 +146,7 @@ export const useLatestFindings = (options: UseFindingsOptions) => {
|
|||
};
|
||||
},
|
||||
{
|
||||
enabled: options.enabled,
|
||||
enabled: options.enabled && !!rulesStates,
|
||||
keepPreviousData: true,
|
||||
onError: (err: Error) => showErrorToast(toasts, err),
|
||||
getNextPageParam: (lastPage) => {
|
||||
|
|
|
@ -31,6 +31,8 @@ import {
|
|||
} from './constants';
|
||||
import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping';
|
||||
import { getFilters } from '../utils/get_filters';
|
||||
import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api';
|
||||
import { buildMutedRulesFilter } from '../../../../common/utils/rules_states';
|
||||
|
||||
const getTermAggregation = (key: keyof FindingsGroupingAggregation, field: string) => ({
|
||||
[key]: {
|
||||
|
@ -154,6 +156,9 @@ export const useLatestFindingsGrouping = ({
|
|||
groupStatsRenderer,
|
||||
});
|
||||
|
||||
const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi();
|
||||
const mutedRulesFilterQuery = rulesStates ? buildMutedRulesFilter(rulesStates) : [];
|
||||
|
||||
const groupingQuery = getGroupingQuery({
|
||||
additionalFilters: query ? [query] : [],
|
||||
groupByField: selectedGroup,
|
||||
|
@ -184,8 +189,16 @@ export const useLatestFindingsGrouping = ({
|
|||
],
|
||||
});
|
||||
|
||||
const filteredGroupingQuery = {
|
||||
...groupingQuery,
|
||||
query: {
|
||||
...groupingQuery.query,
|
||||
bool: { ...groupingQuery.query.bool, must_not: mutedRulesFilterQuery },
|
||||
},
|
||||
};
|
||||
|
||||
const { data, isFetching } = useGroupedFindings({
|
||||
query: groupingQuery,
|
||||
query: filteredGroupingQuery,
|
||||
enabled: !isNoneSelected,
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
INTERNAL_CSP_SETTINGS_SAVED_OBJECT_ID,
|
||||
INTERNAL_CSP_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
} from '../../../../common/constants';
|
||||
import { buildMutedRulesFilter } from '../../../../common/utils/rules_states';
|
||||
|
||||
export const createCspSettingObject = async (soClient: SavedObjectsClientContract) => {
|
||||
return soClient.create<CspSettings>(
|
||||
|
@ -52,22 +53,6 @@ export const getMutedRulesFilterQuery = async (
|
|||
encryptedSoClient: ISavedObjectsRepository | SavedObjectsClientContract
|
||||
): Promise<QueryDslQueryContainer[]> => {
|
||||
const rulesStates = await getCspBenchmarkRulesStatesHandler(encryptedSoClient);
|
||||
const mutedRules = Object.fromEntries(
|
||||
Object.entries(rulesStates).filter(([key, value]) => value.muted === true)
|
||||
);
|
||||
|
||||
const mutedRulesFilterQuery = Object.keys(mutedRules).map((key) => {
|
||||
const rule = mutedRules[key];
|
||||
return {
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'rule.benchmark.id': rule.benchmark_id } },
|
||||
{ term: { 'rule.benchmark.version': rule.benchmark_version } },
|
||||
{ term: { 'rule.benchmark.rule_number': rule.rule_number } },
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates);
|
||||
return mutedRulesFilterQuery;
|
||||
};
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import Chance from 'chance';
|
||||
import { CspBenchmarkRule } from '@kbn/cloud-security-posture-plugin/common/types/latest';
|
||||
import { CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE } from '@kbn/cloud-security-posture-plugin/common/constants';
|
||||
import {
|
||||
ELASTIC_HTTP_VERSION_HEADER,
|
||||
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
|
||||
} from '@kbn/core-http-common';
|
||||
import type { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@ -15,6 +21,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const filterBar = getService('filterBar');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const pageObjects = getPageObjects(['common', 'findings', 'header']);
|
||||
const chance = new Chance();
|
||||
const timeFiveHoursAgo = (Date.now() - 18000000).toString();
|
||||
|
@ -95,13 +103,29 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const ruleName1 = data[0].rule.name;
|
||||
const ruleName2 = data[1].rule.name;
|
||||
|
||||
const getCspBenchmarkRules = async (benchmarkId: string): Promise<CspBenchmarkRule[]> => {
|
||||
const cspBenchmarkRules = await kibanaServer.savedObjects.find<CspBenchmarkRule>({
|
||||
type: CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
const requestedBenchmarkRules = cspBenchmarkRules.saved_objects.filter(
|
||||
(cspBenchmarkRule) => cspBenchmarkRule.attributes.metadata.benchmark.id === benchmarkId
|
||||
);
|
||||
expect(requestedBenchmarkRules.length).greaterThan(0);
|
||||
|
||||
return requestedBenchmarkRules.map((item) => item.attributes);
|
||||
};
|
||||
|
||||
describe('Findings Page - DataTable', function () {
|
||||
this.tags(['cloud_security_posture_findings']);
|
||||
let findings: typeof pageObjects.findings;
|
||||
let latestFindingsTable: typeof findings.latestFindingsTable;
|
||||
let distributionBar: typeof findings.distributionBar;
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
await kibanaServer.savedObjects.clean({
|
||||
types: ['cloud-security-posture-settings'],
|
||||
});
|
||||
|
||||
findings = pageObjects.findings;
|
||||
latestFindingsTable = findings.latestFindingsTable;
|
||||
distributionBar = findings.distributionBar;
|
||||
|
@ -121,7 +145,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
pageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
afterEach(async () => {
|
||||
await findings.index.remove();
|
||||
});
|
||||
|
||||
|
@ -298,5 +322,74 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await testSubjects.missingOrFail(CSP_FIELDS_SELECTOR_MODAL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Findings Page - support muting rules', () => {
|
||||
it(`verify only enabled rules appears`, async () => {
|
||||
const passedFindings = data.filter(({ result }) => result.evaluation === 'passed');
|
||||
const passedFindingsCount = passedFindings.length;
|
||||
|
||||
const rule = (await getCspBenchmarkRules('cis_k8s'))[0];
|
||||
const modifiedFinding = {
|
||||
...passedFindings[0],
|
||||
rule: {
|
||||
name: 'Upper case rule name1',
|
||||
id: rule.metadata.id,
|
||||
section: 'Upper case section1',
|
||||
benchmark: {
|
||||
id: rule.metadata.benchmark.id,
|
||||
posture_type: rule.metadata.benchmark.posture_type,
|
||||
name: rule.metadata.benchmark.name,
|
||||
version: rule.metadata.benchmark.version,
|
||||
rule_number: rule.metadata.benchmark.rule_number,
|
||||
},
|
||||
type: 'process',
|
||||
},
|
||||
};
|
||||
|
||||
await findings.index.add([modifiedFinding]);
|
||||
|
||||
await findings.navigateToLatestFindingsPage();
|
||||
await retry.waitFor(
|
||||
'Findings table to be loaded',
|
||||
async () => (await latestFindingsTable.getRowsCount()) === data.length + 1
|
||||
);
|
||||
pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await distributionBar.filterBy('passed');
|
||||
|
||||
expect(await latestFindingsTable.getFindingsCount('passed')).to.eql(
|
||||
passedFindingsCount + 1
|
||||
);
|
||||
|
||||
await supertest
|
||||
.post(`/internal/cloud_security_posture/rules/_bulk_action`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
action: 'mute',
|
||||
rules: [
|
||||
{
|
||||
benchmark_id: modifiedFinding.rule.benchmark.id,
|
||||
benchmark_version: modifiedFinding.rule.benchmark.version,
|
||||
rule_number: modifiedFinding.rule.benchmark.rule_number || '',
|
||||
rule_id: modifiedFinding.rule.id,
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await findings.navigateToLatestFindingsPage();
|
||||
await retry.waitFor(
|
||||
'Findings table to be loaded',
|
||||
async () => (await latestFindingsTable.getRowsCount()) === data.length
|
||||
);
|
||||
pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await distributionBar.filterBy('passed');
|
||||
|
||||
expect(await latestFindingsTable.getFindingsCount('passed')).to.eql(passedFindingsCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,12 +8,20 @@
|
|||
import expect from '@kbn/expect';
|
||||
import Chance from 'chance';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { CspBenchmarkRule } from '@kbn/cloud-security-posture-plugin/common/types/latest';
|
||||
import { CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE } from '@kbn/cloud-security-posture-plugin/common/constants';
|
||||
import {
|
||||
ELASTIC_HTTP_VERSION_HEADER,
|
||||
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
|
||||
} from '@kbn/core-http-common';
|
||||
import type { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||
const queryBar = getService('queryBar');
|
||||
const filterBar = getService('filterBar');
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const pageObjects = getPageObjects(['common', 'findings', 'header']);
|
||||
const chance = new Chance();
|
||||
|
||||
|
@ -116,12 +124,27 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
|
||||
const ruleName1 = data[0].rule.name;
|
||||
|
||||
const getCspBenchmarkRules = async (benchmarkId: string): Promise<CspBenchmarkRule[]> => {
|
||||
const cspBenchmarkRules = await kibanaServer.savedObjects.find<CspBenchmarkRule>({
|
||||
type: CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
const requestedBenchmarkRules = cspBenchmarkRules.saved_objects.filter(
|
||||
(cspBenchmarkRule) => cspBenchmarkRule.attributes.metadata.benchmark.id === benchmarkId
|
||||
);
|
||||
expect(requestedBenchmarkRules.length).greaterThan(0);
|
||||
|
||||
return requestedBenchmarkRules.map((item) => item.attributes);
|
||||
};
|
||||
|
||||
describe('Findings Page - Grouping', function () {
|
||||
this.tags(['cloud_security_posture_findings_grouping']);
|
||||
let findings: typeof pageObjects.findings;
|
||||
// let groupSelector: ReturnType<typeof findings.groupSelector>;
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.clean({
|
||||
types: ['cloud-security-posture-settings'],
|
||||
});
|
||||
findings = pageObjects.findings;
|
||||
|
||||
// Before we start any test we must wait for cloud_security_posture plugin to complete its initialization
|
||||
|
@ -434,5 +457,78 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
});
|
||||
describe('Default Grouping - support muting rules', async () => {
|
||||
it('groups findings by resource after muting rule', async () => {
|
||||
const findingsCount = data.length;
|
||||
const resourceGroupCount = Array.from(new Set(data.map((obj) => obj.resource.name))).length;
|
||||
|
||||
const finding = data[0];
|
||||
const rule = (await getCspBenchmarkRules('cis_k8s'))[0];
|
||||
const modifiedFinding = {
|
||||
...finding,
|
||||
resource: {
|
||||
...finding.resource,
|
||||
name: 'foo',
|
||||
},
|
||||
rule: {
|
||||
name: 'Upper case rule name1',
|
||||
id: rule.metadata.id,
|
||||
section: 'Upper case section1',
|
||||
benchmark: {
|
||||
id: rule.metadata.benchmark.id,
|
||||
posture_type: rule.metadata.benchmark.posture_type,
|
||||
name: rule.metadata.benchmark.name,
|
||||
version: rule.metadata.benchmark.version,
|
||||
rule_number: rule.metadata.benchmark.rule_number,
|
||||
},
|
||||
type: 'process',
|
||||
},
|
||||
};
|
||||
|
||||
await findings.index.add([modifiedFinding]);
|
||||
|
||||
await findings.navigateToLatestFindingsPage();
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
const groupSelector = await findings.groupSelector();
|
||||
await groupSelector.openDropDown();
|
||||
await groupSelector.setValue('Resource');
|
||||
|
||||
const grouping = await findings.findingsGrouping();
|
||||
|
||||
const groupCount = await grouping.getGroupCount();
|
||||
expect(groupCount).to.be(`${resourceGroupCount + 1} groups`);
|
||||
|
||||
const unitCount = await grouping.getUnitCount();
|
||||
expect(unitCount).to.be(`${findingsCount + 1} findings`);
|
||||
|
||||
await supertest
|
||||
.post(`/internal/cloud_security_posture/rules/_bulk_action`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
action: 'mute',
|
||||
rules: [
|
||||
{
|
||||
benchmark_id: modifiedFinding.rule.benchmark.id,
|
||||
benchmark_version: modifiedFinding.rule.benchmark.version,
|
||||
rule_number: modifiedFinding.rule.benchmark.rule_number || '',
|
||||
rule_id: modifiedFinding.rule.id,
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await findings.navigateToLatestFindingsPage();
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
const groupCountAfterMute = await grouping.getGroupCount();
|
||||
expect(groupCountAfterMute).to.be(`${resourceGroupCount} groups`);
|
||||
|
||||
const unitCountAfterMute = await grouping.getUnitCount();
|
||||
expect(unitCountAfterMute).to.be(`${findingsCount} findings`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue