-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Cloud Security] Vulnerabilities Preview & Refactor CSP Plugin PHASE 2 #193638
Changes from 23 commits
4a2caae
2539143
e9ee30a
1b6761f
465b6a3
543cf52
656bce6
0af9662
48f0bff
611e654
b53f2e8
4c94880
06765c6
cfd6f35
b3e5cab
18c6d78
01f717e
cd0f573
f5dac9b
fa4d467
a7a20ca
8c9c384
ba589da
ccf37c1
d56fa1f
7c7aa63
cbd708e
0172edf
389592a
c9ca88c
f549e22
b117e44
71e2a5c
da540f1
5d34c0f
c1f29d4
b69cff8
6e0df80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* 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 { useKibana } from '@kbn/kibana-react-plugin/public'; | ||
import { lastValueFrom } from 'rxjs'; | ||
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types'; | ||
import { | ||
SearchRequest, | ||
SearchResponse, | ||
AggregationsMultiBucketAggregateBase, | ||
AggregationsStringRareTermsBucketKeys, | ||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; | ||
import { | ||
CDR_VULNERABILITIES_INDEX_PATTERN, | ||
LATEST_VULNERABILITIES_RETENTION_POLICY, | ||
MAX_FINDINGS_TO_LOAD, | ||
} from '@kbn/cloud-security-posture-common'; | ||
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; | ||
import type { CoreStart } from '@kbn/core/public'; | ||
import type { CspClientPluginStartDeps, UseMisconfigurationOptions } from '../../type'; | ||
import { showErrorToast } from '../..'; | ||
import { | ||
getFindingsCountAggQueryVulnerabilities, | ||
getVulnerabilitiesAggregationCount, | ||
} from '../utils/hooks_utils'; | ||
|
||
type LatestFindingsRequest = IKibanaSearchRequest<SearchRequest>; | ||
type LatestFindingsResponse = IKibanaSearchResponse< | ||
SearchResponse<CspVulnerabilityFinding, FindingsAggs> | ||
>; | ||
|
||
interface FindingsAggs { | ||
count: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>; | ||
} | ||
|
||
export const getVulnerabilitiesQuery = ( | ||
{ query }: UseMisconfigurationOptions, | ||
isPreview = false | ||
) => ({ | ||
index: CDR_VULNERABILITIES_INDEX_PATTERN, | ||
size: MAX_FINDINGS_TO_LOAD, | ||
aggs: getFindingsCountAggQueryVulnerabilities(), | ||
query: { | ||
...query, | ||
bool: { | ||
...query?.bool, | ||
filter: [ | ||
...(query?.bool?.filter ?? []), | ||
{ | ||
range: { | ||
'@timestamp': { | ||
gte: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`, | ||
lte: 'now', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}); | ||
|
||
export const useVulnerabilitiesPreview = (options: UseMisconfigurationOptions) => { | ||
const { | ||
data, | ||
notifications: { toasts }, | ||
} = useKibana<CoreStart & CspClientPluginStartDeps>().services; | ||
/** | ||
* We're using useInfiniteQuery in this case to allow the user to fetch more data (if available and up to 10k) | ||
* useInfiniteQuery differs from useQuery because it accumulates and caches a chunk of data from the previous fetches into an array | ||
* it uses the getNextPageParam to know if there are more pages to load and retrieve the position of | ||
* the last loaded record to be used as a from parameter to fetch the next chunk of data. | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this comment should be removed, as it is not true |
||
return useQuery( | ||
['csp_vulnerabilities_preview', { params: options }], | ||
async ({ pageParam }) => { | ||
const { | ||
rawResponse: { aggregations }, | ||
} = await lastValueFrom( | ||
data.search.search<LatestFindingsRequest, LatestFindingsResponse>({ | ||
params: getVulnerabilitiesQuery(options, pageParam), | ||
}) | ||
); | ||
return { | ||
count: getVulnerabilitiesAggregationCount(aggregations?.count?.buckets), | ||
}; | ||
}, | ||
{ | ||
staleTime: 5000, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can remove this |
||
keepPreviousData: true, | ||
enabled: options.enabled, | ||
onError: (err: Error) => showErrorToast(toasts, err), | ||
} | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -27,6 +27,14 @@ const RESULT_EVALUATION = { | |||||||||||||||||||||
UNKNOWN: 'unknown', | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
const VULNERABILITIES_RESULT_EVALUATION = { | ||||||||||||||||||||||
LOW: 'LOW', | ||||||||||||||||||||||
MEDIUM: 'MEDIUM', | ||||||||||||||||||||||
HIGH: 'HIGH', | ||||||||||||||||||||||
CRITICAL: 'CRITICAL', | ||||||||||||||||||||||
UNKNOWN: 'UNKNOWN', | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export const getFindingsCountAggQueryMisconfiguration = () => ({ | ||||||||||||||||||||||
count: { | ||||||||||||||||||||||
filters: { | ||||||||||||||||||||||
|
@@ -39,6 +47,8 @@ export const getFindingsCountAggQueryMisconfiguration = () => ({ | |||||||||||||||||||||
}, | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
|
||||||||||||||||||||||
// export type VulnSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | 'UNKNOWN'; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment should be removed |
||||||||||||||||||||||
|
||||||||||||||||||||||
export const getMisconfigurationAggregationCount = ( | ||||||||||||||||||||||
buckets?: estypes.AggregationsBuckets<estypes.AggregationsStringRareTermsBucketKeys> | ||||||||||||||||||||||
) => { | ||||||||||||||||||||||
|
@@ -103,3 +113,53 @@ const buildMisconfigurationsFindingsQueryWithFilters = ( | |||||||||||||||||||||
}, | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export const getVulnerabilitiesAggregationCount = ( | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's add some unit tests for the utils here |
||||||||||||||||||||||
buckets?: estypes.AggregationsBuckets<estypes.AggregationsStringRareTermsBucketKeys> | ||||||||||||||||||||||
) => { | ||||||||||||||||||||||
const defaultBuckets: AggregationBuckets = { | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.LOW]: { doc_count: 0 }, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.MEDIUM]: { doc_count: 0 }, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.HIGH]: { doc_count: 0 }, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.CRITICAL]: { doc_count: 0 }, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.UNKNOWN]: { doc_count: 0 }, | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// if buckets are undefined we will use default buckets | ||||||||||||||||||||||
const usedBuckets = buckets || defaultBuckets; | ||||||||||||||||||||||
return Object.entries(usedBuckets).reduce( | ||||||||||||||||||||||
(evaluation, [key, value]) => { | ||||||||||||||||||||||
evaluation[key] = (evaluation[key] || 0) + (value.doc_count || 0); | ||||||||||||||||||||||
return evaluation; | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.LOW]: 0, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.MEDIUM]: 0, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.HIGH]: 0, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.CRITICAL]: 0, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.UNKNOWN]: 0, | ||||||||||||||||||||||
} | ||||||||||||||||||||||
); | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export const getFindingsCountAggQueryVulnerabilities = () => ({ | ||||||||||||||||||||||
count: { | ||||||||||||||||||||||
filters: { | ||||||||||||||||||||||
other_bucket_key: VULNERABILITIES_RESULT_EVALUATION.UNKNOWN, | ||||||||||||||||||||||
filters: { | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.LOW]: { | ||||||||||||||||||||||
match: { 'vulnerability.severity': VULNERABILITIES_RESULT_EVALUATION.LOW }, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.MEDIUM]: { | ||||||||||||||||||||||
match: { 'vulnerability.severity': VULNERABILITIES_RESULT_EVALUATION.MEDIUM }, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.HIGH]: { | ||||||||||||||||||||||
match: { 'vulnerability.severity': VULNERABILITIES_RESULT_EVALUATION.HIGH }, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
[VULNERABILITIES_RESULT_EVALUATION.CRITICAL]: { | ||||||||||||||||||||||
match: { 'vulnerability.severity': VULNERABILITIES_RESULT_EVALUATION.CRITICAL }, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ 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'; | ||
import { VulnerabilitiesPreview } from './vulnerabilities/vulnerabilities_preview'; | ||
|
||
export const EntityInsight = <T,>({ | ||
name, | ||
|
@@ -25,10 +26,26 @@ export const EntityInsight = <T,>({ | |
const { euiTheme } = useEuiTheme(); | ||
const getSetupStatus = useCspSetupStatusApi(); | ||
const hasMisconfigurationFindings = getSetupStatus.data?.hasMisconfigurationsFindings; | ||
|
||
const hasVulnerabilitiesFindings = getSetupStatus.data?.hasVulnerabilitiesFindings; | ||
const insightContent: React.ReactElement[] = []; | ||
if (hasMisconfigurationFindings) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :nit can we add a space after between the |
||
insightContent.push( | ||
<> | ||
<MisconfigurationsPreview name={name} fieldName={fieldName} isPreviewMode={isPreviewMode} /> | ||
<EuiSpacer size="m" /> | ||
</> | ||
); | ||
if (hasVulnerabilitiesFindings && fieldName === 'host.name') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the condition is duplicated on line 39 and 49, I would move it to a single constant to avoid them keeping out of sync |
||
insightContent.push( | ||
<> | ||
<VulnerabilitiesPreview hostName={name} /> | ||
<EuiSpacer size="m" /> | ||
</> | ||
); | ||
return ( | ||
<> | ||
{hasMisconfigurationFindings && ( | ||
{(hasMisconfigurationFindings || | ||
(hasVulnerabilitiesFindings && fieldName === 'host.name')) && ( | ||
<> | ||
<EuiAccordion | ||
initialIsOpen={true} | ||
|
@@ -52,12 +69,7 @@ export const EntityInsight = <T,>({ | |
} | ||
> | ||
<EuiSpacer size="m" /> | ||
<MisconfigurationsPreview | ||
name={name} | ||
fieldName={fieldName} | ||
isPreviewMode={isPreviewMode} | ||
/> | ||
<EuiSpacer size="m" /> | ||
{insightContent} | ||
</EuiAccordion> | ||
<EuiHorizontalRule /> | ||
</> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { TestProviders } from '../../../common/mock'; | ||
import { render } from '@testing-library/react'; | ||
import React from 'react'; | ||
import { VulnerabilitiesPreview } from './vulnerabilities_preview'; | ||
|
||
const mockProps: { hostName: string } = { | ||
hostName: 'testContextID', | ||
}; | ||
|
||
describe('VulnerabilitiesPreview', () => { | ||
it('renders', () => { | ||
const { queryByTestId } = render(<VulnerabilitiesPreview {...mockProps} />, { | ||
wrapper: TestProviders, | ||
}); | ||
expect( | ||
queryByTestId('securitySolutionFlyoutInsightsVulnerabilitiesContent') | ||
).toBeInTheDocument(); | ||
expect(queryByTestId('noVulnerabilitiesDataTestSubj')).toBeInTheDocument(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't need any actual documents for the preview, only aggs, correct? In that case should be
size: 0
We already have similar logic in the misconfigs, let's follow the same here