Skip to content

Commit

Permalink
[Cloud Security] Misconfiguration preview & Refactor CSP Plugin to in…
Browse files Browse the repository at this point in the history
…clude new package PHASE 4 (#191677)

The previous #190105 was way too
big and made it hard to review without missing any bugs or potential
bugs, Thus we decided we are going to make series of smaller PR to make
things more manageable

We will be splitting it into 4 PR
Phase 1: Creating empty packages for csp and csp-common
Phase 2: Move Types from CSP plugin to the Package + Deleting duplicates
in the CSP plugin where possible
Phase 3: Move Functions, Utils or Helpers, Hooks to Package
Phase 4: Misconfiguration Preview feature (with Cypress test and other
required test)

<img width="681" alt="353329193-5ad22c4e-81c2-4a8b-89f7-fdbc2a686c2d"
src="https://github.com/user-attachments/assets/b369625a-efc5-4292-a690-2c5dffb5483d">


This is Phase 4 of the Process,

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
animehart and kibanamachine committed Sep 4, 2024
1 parent f6807f9 commit 8b7d965
Show file tree
Hide file tree
Showing 12 changed files with 484 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,7 @@ x-pack/plugins/osquery @elastic/security-defend-workflows
/x-pack/plugins/fleet/public/components/cloud_security_posture @elastic/fleet @elastic/kibana-cloud-security-posture
/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/cloud_security_posture @elastic/fleet @elastic/kibana-cloud-security-posture
/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.* @elastic/fleet @elastic/kibana-cloud-security-posture
/x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture

# Security Solution onboarding tour
/x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/security-threat-hunting-explore
Expand Down
6 changes: 5 additions & 1 deletion x-pack/packages/kbn-cloud-security-posture-common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ export type {
export type { CspFinding } from './types/findings';
export type { BenchmarksCisId } from './types/benchmark';
export * from './constants';
export { extractErrorMessage, buildMutedRulesFilter } from './utils/helpers';
export {
extractErrorMessage,
buildMutedRulesFilter,
buildEntityFlyoutPreviewQuery,
} from './utils/helpers';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
* 2.0.
*/

import { extractErrorMessage, defaultErrorMessage, buildMutedRulesFilter } from './helpers';
import {
extractErrorMessage,
defaultErrorMessage,
buildMutedRulesFilter,
buildEntityFlyoutPreviewQuery,
} from './helpers';

const fallbackMessage = 'thisIsAFallBackMessage';

Expand Down Expand Up @@ -138,4 +143,43 @@ describe('test helper methods', () => {
expect(buildMutedRulesFilter(rulesStates)).toEqual(expectedQuery);
});
});

describe('buildEntityFlyoutPreviewQueryTest', () => {
it('should return the correct query when given field and query', () => {
const field = 'host.name';
const query = 'exampleHost';
const expectedQuery = {
bool: {
filter: [
{
bool: {
should: [{ term: { 'host.name': { value: 'exampleHost' } } }],
minimum_should_match: 1,
},
},
],
},
};

expect(buildEntityFlyoutPreviewQuery(field, query)).toEqual(expectedQuery);
});

it('should return the correct query when given field and empty query', () => {
const field = 'host.name';
const expectedQuery = {
bool: {
filter: [
{
bool: {
should: [{ term: { 'host.name': { value: '' } } }],
minimum_should_match: 1,
},
},
],
},
};

expect(buildEntityFlyoutPreviewQuery(field)).toEqual(expectedQuery);
});
});
});
15 changes: 15 additions & 0 deletions x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,18 @@ export const buildMutedRulesFilter = (

return mutedRulesFilterQuery;
};

export const buildEntityFlyoutPreviewQuery = (field: string, queryValue?: string) => {
return {
bool: {
filter: [
{
bool: {
should: [{ term: { [field]: { value: `${queryValue || ''}` } } }],
minimum_should_match: 1,
},
},
],
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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 { lastValueFrom } from 'rxjs';
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
CDR_MISCONFIGURATIONS_INDEX_PATTERN,
LATEST_FINDINGS_RETENTION_POLICY,
CspFinding,
} from '@kbn/cloud-security-posture-common';
import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest';
import { buildMutedRulesFilter } from '@kbn/cloud-security-posture-common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { showErrorToast } from '../..';
import type { CspClientPluginStartDeps } from '../../type';
import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api';

interface MisconfigurationPreviewBaseEsQuery {
query?: {
bool: {
filter: estypes.QueryDslQueryContainer[];
};
};
}

interface UseMisconfigurationPreviewOptions extends MisconfigurationPreviewBaseEsQuery {
sort: string[][];
enabled: boolean;
pageSize: number;
}

type LatestFindingsRequest = IKibanaSearchRequest<estypes.SearchRequest>;
type LatestFindingsResponse = IKibanaSearchResponse<
estypes.SearchResponse<CspFinding, FindingsAggs>
>;

interface FindingsAggs {
count: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
}

const RESULT_EVALUATION = {
PASSED: 'passed',
FAILED: 'failed',
UNKNOWN: 'unknown',
};

export const getFindingsCountAggQueryMisconfigurationPreview = () => ({
count: {
filters: {
other_bucket_key: RESULT_EVALUATION.UNKNOWN,
filters: {
[RESULT_EVALUATION.PASSED]: { match: { 'result.evaluation': RESULT_EVALUATION.PASSED } },
[RESULT_EVALUATION.FAILED]: { match: { 'result.evaluation': RESULT_EVALUATION.FAILED } },
},
},
},
});

export const getMisconfigurationAggregationCount = (
buckets: estypes.AggregationsBuckets<estypes.AggregationsStringRareTermsBucketKeys>
) => {
return Object.entries(buckets).reduce(
(evaluation, [key, value]) => {
evaluation[key] = (evaluation[key] || 0) + (value.doc_count || 0);
return evaluation;
},
{
[RESULT_EVALUATION.PASSED]: 0,
[RESULT_EVALUATION.FAILED]: 0,
[RESULT_EVALUATION.UNKNOWN]: 0,
}
);
};

export const buildMisconfigurationsFindingsQuery = (
{ query }: UseMisconfigurationPreviewOptions,
rulesStates: CspBenchmarkRulesStates
) => {
const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates);

return {
index: CDR_MISCONFIGURATIONS_INDEX_PATTERN,
size: 0,
aggs: getFindingsCountAggQueryMisconfigurationPreview(),
ignore_unavailable: false,
query: buildMisconfigurationsFindingsQueryWithFilters(query, mutedRulesFilterQuery),
};
};

const buildMisconfigurationsFindingsQueryWithFilters = (
query: UseMisconfigurationPreviewOptions['query'],
mutedRulesFilterQuery: estypes.QueryDslQueryContainer[]
) => {
return {
...query,
bool: {
...query?.bool,
filter: [
...(query?.bool?.filter ?? []),
{
range: {
'@timestamp': {
gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`,
lte: 'now',
},
},
},
],
must_not: [...mutedRulesFilterQuery],
},
};
};

export const useMisconfigurationPreview = (options: UseMisconfigurationPreviewOptions) => {
const {
data,
notifications: { toasts },
} = useKibana<CoreStart & CspClientPluginStartDeps>().services;
const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi();

return useQuery(
['csp_misconfiguration_preview', { params: options }, rulesStates],
async () => {
const {
rawResponse: { aggregations },
} = await lastValueFrom(
data.search.search<LatestFindingsRequest, LatestFindingsResponse>({
params: buildMisconfigurationsFindingsQuery(options, rulesStates!),
})
);
if (!aggregations) throw new Error('expected aggregations to be defined');

return {
count: getMisconfigurationAggregationCount(aggregations.count.buckets),
};
},
{
enabled: options.enabled && !!rulesStates,
keepPreviousData: true,
onError: (err: Error) => showErrorToast(toasts, err),
}
);
};
1 change: 1 addition & 0 deletions x-pack/packages/kbn-cloud-security-posture/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@
"@kbn/kibana-react-plugin",
"@kbn/cloud-security-posture-common",
"@kbn/i18n",
"@kbn/search-types",
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { EuiAccordion, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui';

import React from 'react';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useCspSetupStatusApi } from '@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api';
import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview';

export const EntityInsight = <T,>({ hostName }: { hostName: string }) => {
const { euiTheme } = useEuiTheme();
const getSetupStatus = useCspSetupStatusApi();
const hasMisconfigurationFindings = getSetupStatus.data?.hasMisconfigurationsFindings;

return (
<>
{hasMisconfigurationFindings && (
<EuiAccordion
initialIsOpen={true}
id="entityInsight-accordion"
data-test-subj="entityInsightTestSubj"
buttonProps={{
'data-test-subj': 'entityInsight-accordion-button',
css: css`
color: ${euiTheme.colors.primary};
`,
}}
buttonContent={
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.insightsTitle"
defaultMessage="Insights"
/>
</h3>
</EuiTitle>
}
>
<EuiSpacer size="m" />
<MisconfigurationsPreview hostName={hostName} />
<EuiSpacer size="m" />
</EuiAccordion>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -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.
*/

// Add stuff here
import { TestProviders } from '../../../common/mock';
import { render } from '@testing-library/react';
import React from 'react';
import { MisconfigurationsPreview } from './misconfiguration_preview';

const mockProps = {
hostName: 'testContextID',
};

describe('MisconfigurationsPreview', () => {
it('renders', () => {
const { queryByTestId } = render(<MisconfigurationsPreview {...mockProps} />, {
wrapper: TestProviders,
});
expect(
queryByTestId('securitySolutionFlyoutInsightsMisconfigurationsContent')
).toBeInTheDocument();
expect(queryByTestId('noFindingsDataTestSubj')).toBeInTheDocument();
});
});
Loading

0 comments on commit 8b7d965

Please sign in to comment.