[Cloud Security] Filter muted findings

This commit is contained in:
Ido Cohen 2024-01-11 15:57:43 +02:00 committed by GitHub
parent a3f3386081
commit b043cf0ff1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 303 additions and 44 deletions

View file

@ -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;
};

View file

@ -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,
})
);
};

View file

@ -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) => {

View file

@ -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,
});

View file

@ -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;
};

View file

@ -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);
});
});
});
}

View file

@ -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`);
});
});
});
}