mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cloud Security] Host Name Misconfiguration Datagrid & Refactor CSP Plugin PHASE 2 (#192535)
In an attempt to make Reviewing easier and more accurate, the implementation of Misconfiguration Data grid on Host.name flyout in Alerts Page will be split into 2 Phases Phase 1: Move Functions, Utils or Helpers, Hooks, constants to Package Phase 2: Implementing the feature This is **Phase 2** of the process <img width="1712" alt="Screenshot 2024-09-11 at 2 16 20 PM" src="https://github.com/user-attachments/assets/29ab56db-8561-486c-ae8d-c254b932cea4"> How to test: Pre req: In order to test this, you need to generate some fake alerts. This [repo](https://github.com/elastic/security-documents-generator) will help you do that 1. Generate Some Alerts 2. Use the Reindex API to get some Findings data in (change the host.name field to match the host.name from alerts generated if you want to test Findings table in the left panel flyout) 3. Turn on Risky Entity Score if you want to test if both Risk Contribution and Insights tabs shows up, follow this [guide](https://www.elastic.co/guide/en/security/current/turn-on-risk-engine.html) to turn on Risk Entity Score
This commit is contained in:
parent
b5abc4ec7e
commit
28becfdce9
14 changed files with 636 additions and 134 deletions
|
@ -17,7 +17,7 @@ export type {
|
|||
BaseCspSetupStatus,
|
||||
CspSetupStatus,
|
||||
} from './types/status';
|
||||
export type { CspFinding } from './types/findings';
|
||||
export type { CspFinding, CspFindingResult } from './types/findings';
|
||||
export type { BenchmarksCisId } from './types/benchmark';
|
||||
export * from './constants';
|
||||
export {
|
||||
|
|
|
@ -41,7 +41,7 @@ interface CspFindingCloud {
|
|||
region?: string;
|
||||
}
|
||||
|
||||
interface CspFindingResult {
|
||||
export interface CspFindingResult {
|
||||
evaluation: 'passed' | 'failed';
|
||||
expected?: Record<string, unknown>;
|
||||
evidence: Record<string, unknown>;
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { CspFinding } 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,
|
||||
LatestFindingsRequest,
|
||||
LatestFindingsResponse,
|
||||
UseMisconfigurationOptions,
|
||||
} from '../../type';
|
||||
|
||||
import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api';
|
||||
import {
|
||||
buildMisconfigurationsFindingsQuery,
|
||||
getMisconfigurationAggregationCount,
|
||||
} from '../utils/hooks_utils';
|
||||
|
||||
export const useMisconfigurationFindings = (options: UseMisconfigurationOptions) => {
|
||||
const {
|
||||
data,
|
||||
notifications: { toasts },
|
||||
} = useKibana<CoreStart & CspClientPluginStartDeps>().services;
|
||||
const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi();
|
||||
|
||||
return useQuery(
|
||||
['csp_misconfiguration_findings', { params: options }, rulesStates],
|
||||
async () => {
|
||||
const {
|
||||
rawResponse: { hits, 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),
|
||||
rows: hits.hits.map((finding) => ({
|
||||
result: finding._source?.result,
|
||||
rule: finding?._source?.rule,
|
||||
resource: finding?._source?.resource,
|
||||
})) as Array<Pick<CspFinding, 'result' | 'rule' | 'resource'>>,
|
||||
};
|
||||
},
|
||||
{
|
||||
enabled: options.enabled && !!rulesStates,
|
||||
keepPreviousData: true,
|
||||
onError: (err: Error) => showErrorToast(toasts, err),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -6,118 +6,22 @@
|
|||
*/
|
||||
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 type {
|
||||
CspClientPluginStartDeps,
|
||||
LatestFindingsRequest,
|
||||
LatestFindingsResponse,
|
||||
UseMisconfigurationOptions,
|
||||
} from '../../type';
|
||||
import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api';
|
||||
import {
|
||||
buildMisconfigurationsFindingsQuery,
|
||||
getMisconfigurationAggregationCount,
|
||||
} from '../utils/hooks_utils';
|
||||
|
||||
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) => {
|
||||
export const useMisconfigurationPreview = (options: UseMisconfigurationOptions) => {
|
||||
const {
|
||||
data,
|
||||
notifications: { toasts },
|
||||
|
@ -134,10 +38,10 @@ export const useMisconfigurationPreview = (options: UseMisconfigurationPreviewOp
|
|||
params: buildMisconfigurationsFindingsQuery(options, rulesStates!),
|
||||
})
|
||||
);
|
||||
if (!aggregations) throw new Error('expected aggregations to be defined');
|
||||
|
||||
if (!aggregations && !options.ignore_unavailable)
|
||||
throw new Error('expected aggregations to be defined');
|
||||
return {
|
||||
count: getMisconfigurationAggregationCount(aggregations.count.buckets),
|
||||
count: getMisconfigurationAggregationCount(aggregations?.count?.buckets),
|
||||
};
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
CDR_MISCONFIGURATIONS_INDEX_PATTERN,
|
||||
LATEST_FINDINGS_RETENTION_POLICY,
|
||||
} 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 type { UseMisconfigurationOptions } from '../../type';
|
||||
|
||||
const MISCONFIGURATIONS_SOURCE_FIELDS = ['result.*', 'rule.*', 'resource.*'];
|
||||
interface AggregationBucket {
|
||||
doc_count?: number;
|
||||
}
|
||||
|
||||
type AggregationBuckets = Record<string, AggregationBucket>;
|
||||
|
||||
const RESULT_EVALUATION = {
|
||||
PASSED: 'passed',
|
||||
FAILED: 'failed',
|
||||
UNKNOWN: 'unknown',
|
||||
};
|
||||
|
||||
export const getFindingsCountAggQueryMisconfiguration = () => ({
|
||||
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>
|
||||
) => {
|
||||
const defaultBuckets: AggregationBuckets = {
|
||||
[RESULT_EVALUATION.PASSED]: { doc_count: 0 },
|
||||
[RESULT_EVALUATION.FAILED]: { doc_count: 0 },
|
||||
[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;
|
||||
},
|
||||
{
|
||||
[RESULT_EVALUATION.PASSED]: 0,
|
||||
[RESULT_EVALUATION.FAILED]: 0,
|
||||
[RESULT_EVALUATION.UNKNOWN]: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const buildMisconfigurationsFindingsQuery = (
|
||||
{ query }: UseMisconfigurationOptions,
|
||||
rulesStates: CspBenchmarkRulesStates,
|
||||
isPreview = false
|
||||
) => {
|
||||
const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates);
|
||||
|
||||
return {
|
||||
index: CDR_MISCONFIGURATIONS_INDEX_PATTERN,
|
||||
size: isPreview ? 0 : 500,
|
||||
aggs: getFindingsCountAggQueryMisconfiguration(),
|
||||
ignore_unavailable: true,
|
||||
query: buildMisconfigurationsFindingsQueryWithFilters(query, mutedRulesFilterQuery),
|
||||
_source: MISCONFIGURATIONS_SOURCE_FIELDS,
|
||||
};
|
||||
};
|
||||
|
||||
const buildMisconfigurationsFindingsQueryWithFilters = (
|
||||
query: UseMisconfigurationOptions['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],
|
||||
},
|
||||
};
|
||||
};
|
|
@ -22,6 +22,9 @@ import type { FleetStart } from '@kbn/fleet-plugin/public';
|
|||
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
import { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { CspFinding } from '@kbn/cloud-security-posture-common';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types';
|
||||
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
export interface FindingsBaseEsQuery {
|
||||
|
@ -51,3 +54,27 @@ export interface CspClientPluginStartDeps {
|
|||
// optional
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
||||
export interface MisconfigurationBaseEsQuery {
|
||||
query?: {
|
||||
bool: {
|
||||
filter: estypes.QueryDslQueryContainer[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseMisconfigurationOptions extends MisconfigurationBaseEsQuery {
|
||||
sort: string[][];
|
||||
enabled: boolean;
|
||||
pageSize: number;
|
||||
ignore_unavailable?: boolean;
|
||||
}
|
||||
|
||||
export type LatestFindingsRequest = IKibanaSearchRequest<estypes.SearchRequest>;
|
||||
export type LatestFindingsResponse = IKibanaSearchResponse<
|
||||
estypes.SearchResponse<CspFinding, FindingsAggs>
|
||||
>;
|
||||
|
||||
export interface FindingsAggs {
|
||||
count: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import { EuiButtonGroup, EuiSpacer } from '@elastic/eui';
|
||||
import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/button/button_group/button_group';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useExpandableFlyoutState } from '@kbn/expandable-flyout';
|
||||
import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table';
|
||||
|
||||
enum InsightsTabCspTab {
|
||||
MISCONFIGURATION = 'misconfigurationTabId',
|
||||
}
|
||||
|
||||
const insightsButtons: EuiButtonGroupOptionProps[] = [
|
||||
{
|
||||
id: InsightsTabCspTab.MISCONFIGURATION,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.misconfigurationButtonLabel"
|
||||
defaultMessage="Misconfiguration"
|
||||
/>
|
||||
),
|
||||
'data-test-subj': 'misconfigurationTabDataTestId',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Insights view displayed in the document details expandable flyout left section
|
||||
*/
|
||||
export const InsightsTabCsp = memo(
|
||||
({ name, fieldName }: { name: string; fieldName: 'host.name' | 'user.name' }) => {
|
||||
const panels = useExpandableFlyoutState();
|
||||
const activeInsightsId = panels.left?.path?.subTab ?? 'misconfigurationTabId';
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
legend={i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.optionsButtonGroups',
|
||||
{
|
||||
defaultMessage: 'Insights options',
|
||||
}
|
||||
)}
|
||||
options={insightsButtons}
|
||||
idSelected={activeInsightsId}
|
||||
onChange={() => {}}
|
||||
buttonSize="compressed"
|
||||
isFullWidth
|
||||
data-test-subj={'insightButtonGroupsTestId'}
|
||||
/>
|
||||
<EuiSpacer size="xl" />
|
||||
<MisconfigurationFindingsDetailsTable fieldName={fieldName} queryName={name} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InsightsTabCsp.displayName = 'InsightsTab';
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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 React, { memo, useState } from 'react';
|
||||
import type { Criteria, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiIcon, EuiPanel, EuiLink, EuiText, EuiBasicTable } from '@elastic/eui';
|
||||
import { useMisconfigurationFindings } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_findings';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CspFinding, CspFindingResult } from '@kbn/cloud-security-posture-common';
|
||||
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
|
||||
import { useNavigateFindings } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings';
|
||||
import type { CspBenchmarkRuleMetadata } from '@kbn/cloud-security-posture-common/schema/rules/latest';
|
||||
import { CspEvaluationBadge } from '@kbn/cloud-security-posture';
|
||||
|
||||
type MisconfigurationFindingDetailFields = Pick<CspFinding, 'result' | 'rule' | 'resource'>;
|
||||
|
||||
const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => {
|
||||
if (passedFindingsStats === 0 && failedFindingsStats === 0) return [];
|
||||
return [
|
||||
{
|
||||
key: i18n.translate(
|
||||
'xpack.securitySolution.flyout.right.insights.misconfigurations.passedFindingsText',
|
||||
{
|
||||
defaultMessage: 'Passed findings',
|
||||
}
|
||||
),
|
||||
count: passedFindingsStats,
|
||||
color: euiThemeVars.euiColorSuccess,
|
||||
},
|
||||
{
|
||||
key: i18n.translate(
|
||||
'xpack.securitySolution.flyout.right.insights.misconfigurations.failedFindingsText',
|
||||
{
|
||||
defaultMessage: 'Failed findings',
|
||||
}
|
||||
),
|
||||
count: failedFindingsStats,
|
||||
color: euiThemeVars.euiColorVis9,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Insights view displayed in the document details expandable flyout left section
|
||||
*/
|
||||
export const MisconfigurationFindingsDetailsTable = memo(
|
||||
({ fieldName, queryName }: { fieldName: 'host.name' | 'user.name'; queryName: string }) => {
|
||||
const { data } = useMisconfigurationFindings({
|
||||
query: buildEntityFlyoutPreviewQuery(fieldName, queryName),
|
||||
sort: [],
|
||||
enabled: true,
|
||||
pageSize: 1,
|
||||
});
|
||||
|
||||
const passedFindings = data?.count.passed || 0;
|
||||
const failedFindings = data?.count.failed || 0;
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const findingsPagination = (findings: MisconfigurationFindingDetailFields[]) => {
|
||||
let pageOfItems;
|
||||
|
||||
if (!pageIndex && !pageSize) {
|
||||
pageOfItems = findings;
|
||||
} else {
|
||||
const startIndex = pageIndex * pageSize;
|
||||
pageOfItems = findings?.slice(
|
||||
startIndex,
|
||||
Math.min(startIndex + pageSize, findings?.length)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
pageOfItems,
|
||||
totalItemCount: findings?.length,
|
||||
};
|
||||
};
|
||||
|
||||
const { pageOfItems, totalItemCount } = findingsPagination(data?.rows || []);
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
};
|
||||
|
||||
const onTableChange = ({ page }: Criteria<MisconfigurationFindingDetailFields>) => {
|
||||
if (page) {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
}
|
||||
};
|
||||
|
||||
const navToFindings = useNavigateFindings();
|
||||
|
||||
const navToFindingsByHostName = (hostName: string) => {
|
||||
navToFindings({ 'host.name': hostName }, ['rule.name']);
|
||||
};
|
||||
|
||||
const navToFindingsByRuleAndResourceId = (ruleId: string, resourceId: string) => {
|
||||
navToFindings({ 'rule.id': ruleId, 'resource.id': resourceId });
|
||||
};
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<MisconfigurationFindingDetailFields>> = [
|
||||
{
|
||||
field: 'rule',
|
||||
name: '',
|
||||
width: '5%',
|
||||
render: (rule: CspBenchmarkRuleMetadata, finding: MisconfigurationFindingDetailFields) => (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
navToFindingsByRuleAndResourceId(rule?.id, finding?.resource?.id);
|
||||
}}
|
||||
>
|
||||
<EuiIcon type={'popout'} />
|
||||
</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'result',
|
||||
render: (result: CspFindingResult) => <CspEvaluationBadge type={result?.evaluation} />,
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.misconfigurations.table.resultColumnName',
|
||||
{
|
||||
defaultMessage: 'Result',
|
||||
}
|
||||
),
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
field: 'rule',
|
||||
render: (rule: CspBenchmarkRuleMetadata) => <EuiText size="s">{rule?.name}</EuiText>,
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.misconfigurations.table.ruleColumnName',
|
||||
{
|
||||
defaultMessage: 'Rule',
|
||||
}
|
||||
),
|
||||
width: '90%',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
navToFindingsByHostName(queryName);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.misconfigurations.tableTitle',
|
||||
{
|
||||
defaultMessage: 'Misconfigurations',
|
||||
}
|
||||
)}
|
||||
<EuiIcon type={'popout'} />
|
||||
</EuiLink>
|
||||
<EuiSpacer size="xl" />
|
||||
<DistributionBar stats={getFindingsStats(passedFindings, failedFindings)} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiBasicTable
|
||||
items={pageOfItems || []}
|
||||
rowHeader="result"
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
onChange={onTableChange}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
MisconfigurationFindingsDetailsTable.displayName = 'MisconfigurationFindingsDetailsTable';
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import type { EuiThemeComputed } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui';
|
||||
|
@ -16,6 +16,16 @@ import { euiThemeVars } from '@kbn/ui-theme';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpandablePanel } from '@kbn/security-solution-common';
|
||||
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left';
|
||||
import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
|
||||
import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine';
|
||||
import { buildHostNamesFilter } from '../../../../common/search_strategy';
|
||||
|
||||
const FIRST_RECORD_PAGINATION = {
|
||||
cursorStart: 0,
|
||||
querySize: 1,
|
||||
};
|
||||
|
||||
const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => {
|
||||
if (passedFindingsStats === 0 && failedFindingsStats === 0) return [];
|
||||
|
@ -75,10 +85,14 @@ const MisconfigurationPreviewScore = ({
|
|||
passedFindings,
|
||||
failedFindings,
|
||||
euiTheme,
|
||||
numberOfPassedFindings,
|
||||
numberOfFailedFindings,
|
||||
}: {
|
||||
passedFindings: number;
|
||||
failedFindings: number;
|
||||
euiTheme: EuiThemeComputed<{}>;
|
||||
numberOfPassedFindings?: number;
|
||||
numberOfFailedFindings?: number;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
|
@ -119,9 +133,52 @@ export const MisconfigurationsPreview = ({ hostName }: { hostName: string }) =>
|
|||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
|
||||
const hostNameFilterQuery = useMemo(
|
||||
() => (hostName ? buildHostNamesFilter([hostName]) : undefined),
|
||||
[hostName]
|
||||
);
|
||||
|
||||
const riskScoreState = useRiskScore({
|
||||
riskEntity: RiskScoreEntity.host,
|
||||
filterQuery: hostNameFilterQuery,
|
||||
onlyLatest: false,
|
||||
pagination: FIRST_RECORD_PAGINATION,
|
||||
});
|
||||
const { data: hostRisk } = riskScoreState;
|
||||
const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined;
|
||||
const isRiskScoreExist = !!hostRiskData?.host.risk;
|
||||
const { openLeftPanel } = useExpandableFlyoutApi();
|
||||
const isPreviewMode = false;
|
||||
const goToEntityInsightTab = useCallback(() => {
|
||||
openLeftPanel({
|
||||
id: HostDetailsPanelKey,
|
||||
params: {
|
||||
name: hostName,
|
||||
isRiskScoreExist,
|
||||
hasMisconfigurationFindings,
|
||||
path: { tab: 'csp_insights' },
|
||||
},
|
||||
});
|
||||
}, [hasMisconfigurationFindings, hostName, isRiskScoreExist, openLeftPanel]);
|
||||
const link = useMemo(
|
||||
() =>
|
||||
!isPreviewMode
|
||||
? {
|
||||
callback: goToEntityInsightTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.misconfiguration.misconfigurationTooltip"
|
||||
defaultMessage="Show all misconfiguration findings"
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[isPreviewMode, goToEntityInsightTab]
|
||||
);
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
iconType: hasMisconfigurationFindings ? 'arrowStart' : '',
|
||||
title: (
|
||||
<EuiText
|
||||
size="xs"
|
||||
|
@ -135,6 +192,7 @@ export const MisconfigurationsPreview = ({ hostName }: { hostName: string }) =>
|
|||
/>
|
||||
</EuiText>
|
||||
),
|
||||
link: hasMisconfigurationFindings ? link : undefined,
|
||||
}}
|
||||
data-test-subj={'securitySolutionFlyoutInsightsMisconfigurations'}
|
||||
>
|
||||
|
|
|
@ -11,8 +11,10 @@ import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared
|
|||
import { PREFIX } from '../../../flyout/shared/test_ids';
|
||||
import type { RiskInputsTabProps } from './tabs/risk_inputs/risk_inputs_tab';
|
||||
import { RiskInputsTab } from './tabs/risk_inputs/risk_inputs_tab';
|
||||
import { InsightsTabCsp } from '../../../cloud_security_posture/components/csp_details/insights_tab_csp';
|
||||
|
||||
export const RISK_INPUTS_TAB_TEST_ID = `${PREFIX}RiskInputsTab` as const;
|
||||
export const INSIGHTS_TAB_TEST_ID = `${PREFIX}InsightInputsTab` as const;
|
||||
|
||||
export const getRiskInputTab = ({ entityType, entityName, scopeId }: RiskInputsTabProps) => ({
|
||||
id: EntityDetailsLeftPanelTab.RISK_INPUTS,
|
||||
|
@ -25,3 +27,21 @@ export const getRiskInputTab = ({ entityType, entityName, scopeId }: RiskInputsT
|
|||
),
|
||||
content: <RiskInputsTab entityType={entityType} entityName={entityName} scopeId={scopeId} />,
|
||||
});
|
||||
|
||||
export const getInsightsInputTab = ({
|
||||
name,
|
||||
fieldName,
|
||||
}: {
|
||||
name: string;
|
||||
fieldName: 'host.name' | 'user.name';
|
||||
}) => ({
|
||||
id: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
|
||||
'data-test-subj': INSIGHTS_TAB_TEST_ID,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.entityDetails.insightsDetails.insights.tabLabel"
|
||||
defaultMessage="Insights"
|
||||
/>
|
||||
),
|
||||
content: <InsightsTabCsp name={name} fieldName={fieldName} />,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RISK_INPUTS_TAB_TEST_ID } from '../../../entity_analytics/components/entity_details_flyout';
|
||||
import {
|
||||
RISK_INPUTS_TAB_TEST_ID,
|
||||
INSIGHTS_TAB_TEST_ID,
|
||||
} from '../../../entity_analytics/components/entity_details_flyout';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { HostDetailsPanel } from '.';
|
||||
|
@ -59,4 +62,34 @@ describe('HostDetailsPanel', () => {
|
|||
);
|
||||
expect(queryByTestId(RISK_INPUTS_TAB_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("doesn't render insights panel when there no misconfiguration findings", () => {
|
||||
const { queryByTestId } = render(
|
||||
<HostDetailsPanel
|
||||
name="elastic"
|
||||
isRiskScoreExist={false}
|
||||
scopeId={'scopeId'}
|
||||
hasMisconfigurationFindings={false}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(queryByTestId(INSIGHTS_TAB_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render insights panel when there are misconfiguration findings', () => {
|
||||
const { queryByTestId } = render(
|
||||
<HostDetailsPanel
|
||||
name="elastic"
|
||||
isRiskScoreExist={false}
|
||||
scopeId={'scopeId'}
|
||||
hasMisconfigurationFindings={true}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(queryByTestId(INSIGHTS_TAB_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import { getRiskInputTab } from '../../../entity_analytics/components/entity_details_flyout';
|
||||
import {
|
||||
getRiskInputTab,
|
||||
getInsightsInputTab,
|
||||
} from '../../../entity_analytics/components/entity_details_flyout';
|
||||
import { LeftPanelContent } from '../shared/components/left_panel/left_panel_content';
|
||||
import {
|
||||
EntityDetailsLeftPanelTab,
|
||||
|
@ -19,6 +22,10 @@ export interface HostDetailsPanelProps extends Record<string, unknown> {
|
|||
isRiskScoreExist: boolean;
|
||||
name: string;
|
||||
scopeId: string;
|
||||
hasMisconfigurationFindings?: boolean;
|
||||
path?: {
|
||||
tab?: EntityDetailsLeftPanelTab;
|
||||
};
|
||||
}
|
||||
export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps {
|
||||
key: 'host_details';
|
||||
|
@ -26,18 +33,31 @@ export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps {
|
|||
}
|
||||
export const HostDetailsPanelKey: HostDetailsExpandableFlyoutProps['key'] = 'host_details';
|
||||
|
||||
export const HostDetailsPanel = ({ name, isRiskScoreExist, scopeId }: HostDetailsPanelProps) => {
|
||||
// Temporary implementation while Host details left panel don't have Asset tabs
|
||||
const [tabs, selectedTabId, setSelectedTabId] = useMemo(() => {
|
||||
export const HostDetailsPanel = ({
|
||||
name,
|
||||
isRiskScoreExist,
|
||||
scopeId,
|
||||
path,
|
||||
hasMisconfigurationFindings,
|
||||
}: HostDetailsPanelProps) => {
|
||||
const [selectedTabId, setSelectedTabId] = useState(
|
||||
path?.tab === EntityDetailsLeftPanelTab.CSP_INSIGHTS
|
||||
? EntityDetailsLeftPanelTab.CSP_INSIGHTS
|
||||
: EntityDetailsLeftPanelTab.RISK_INPUTS
|
||||
);
|
||||
|
||||
const [tabs] = useMemo(() => {
|
||||
const isRiskScoreTabAvailable = isRiskScoreExist && name;
|
||||
return [
|
||||
isRiskScoreTabAvailable
|
||||
? [getRiskInputTab({ entityName: name, entityType: RiskScoreEntity.host, scopeId })]
|
||||
: [],
|
||||
EntityDetailsLeftPanelTab.RISK_INPUTS,
|
||||
() => {},
|
||||
];
|
||||
}, [name, isRiskScoreExist, scopeId]);
|
||||
const riskScoreTab = isRiskScoreTabAvailable
|
||||
? [getRiskInputTab({ entityName: name, entityType: RiskScoreEntity.host, scopeId })]
|
||||
: [];
|
||||
|
||||
// Determine if the Insights tab should be included
|
||||
const insightsTab = hasMisconfigurationFindings
|
||||
? [getInsightsInputTab({ name, fieldName: 'host.name' })]
|
||||
: [];
|
||||
return [[...riskScoreTab, ...insightsTab], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}];
|
||||
}, [isRiskScoreExist, name, scopeId, hasMisconfigurationFindings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -10,6 +10,8 @@ import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
|||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
|
||||
import { FlyoutLoading, FlyoutNavigation } from '@kbn/security-solution-common';
|
||||
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
|
||||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id';
|
||||
import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab';
|
||||
import type { Refetch } from '../../../common/types';
|
||||
|
@ -28,7 +30,7 @@ import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anom
|
|||
import type { ObservedEntityData } from '../shared/components/observed_entity/types';
|
||||
import { useObservedHost } from './hooks/use_observed_host';
|
||||
import { HostDetailsPanelKey } from '../host_details_left';
|
||||
import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
|
||||
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
|
||||
import { HostPreviewPanelFooter } from '../host_preview/footer';
|
||||
|
||||
export interface HostPanelProps extends Record<string, unknown> {
|
||||
|
@ -92,6 +94,19 @@ export const HostPanel = ({
|
|||
{ onSuccess: refetchRiskScore }
|
||||
);
|
||||
|
||||
const { data } = useMisconfigurationPreview({
|
||||
query: buildEntityFlyoutPreviewQuery('host.name', hostName),
|
||||
sort: [],
|
||||
enabled: true,
|
||||
pageSize: 1,
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
|
||||
const passedFindings = data?.count.passed || 0;
|
||||
const failedFindings = data?.count.failed || 0;
|
||||
|
||||
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
|
||||
|
||||
useQueryInspector({
|
||||
deleteQuery,
|
||||
inspect: inspectRiskScore,
|
||||
|
@ -114,13 +129,23 @@ export const HostPanel = ({
|
|||
scopeId,
|
||||
isRiskScoreExist,
|
||||
path: tab ? { tab } : undefined,
|
||||
hasMisconfigurationFindings,
|
||||
},
|
||||
});
|
||||
},
|
||||
[telemetry, openLeftPanel, hostName, isRiskScoreExist, scopeId]
|
||||
[telemetry, openLeftPanel, hostName, scopeId, isRiskScoreExist, hasMisconfigurationFindings]
|
||||
);
|
||||
|
||||
const openDefaultPanel = useCallback(
|
||||
() =>
|
||||
openTabPanel(
|
||||
isRiskScoreExist
|
||||
? EntityDetailsLeftPanelTab.RISK_INPUTS
|
||||
: EntityDetailsLeftPanelTab.CSP_INSIGHTS
|
||||
),
|
||||
[isRiskScoreExist, openTabPanel]
|
||||
);
|
||||
|
||||
const openDefaultPanel = useCallback(() => openTabPanel(), [openTabPanel]);
|
||||
const observedHost = useObservedHost(hostName, scopeId);
|
||||
|
||||
if (observedHost.isLoading) {
|
||||
|
@ -147,7 +172,9 @@ export const HostPanel = ({
|
|||
return (
|
||||
<>
|
||||
<FlyoutNavigation
|
||||
flyoutIsExpandable={!isPreviewMode && isRiskScoreExist}
|
||||
flyoutIsExpandable={
|
||||
!isPreviewMode && (isRiskScoreExist || hasMisconfigurationFindings)
|
||||
}
|
||||
expandDetails={openDefaultPanel}
|
||||
/>
|
||||
<HostPanelHeader hostName={hostName} observedHost={observedHostWithAnomalies} />
|
||||
|
|
|
@ -22,6 +22,7 @@ export enum EntityDetailsLeftPanelTab {
|
|||
RISK_INPUTS = 'risk_inputs',
|
||||
OKTA = 'okta_document',
|
||||
ENTRA = 'entra_document',
|
||||
CSP_INSIGHTS = 'csp_insights',
|
||||
}
|
||||
|
||||
export interface PanelHeaderProps {
|
||||
|
@ -65,9 +66,7 @@ export const LeftPanelHeader: VFC<PanelHeaderProps> = memo(
|
|||
border-block-end: none !important;
|
||||
`}
|
||||
>
|
||||
<EuiTabs size="l" expand>
|
||||
{renderTabs}
|
||||
</EuiTabs>
|
||||
<EuiTabs size="l">{renderTabs}</EuiTabs>
|
||||
</FlyoutHeader>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue