Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export interface FiltersProps {
}

export const Filters = ({ onFiltersChange }: FiltersProps) => {
const { dataView, dataViewIsLoading, dataViewIsRefetching } = useDataViewContext();
const { dataView, dataViewIsLoading } = useDataViewContext();
const spaceId = useSpaceId();

const dataViewSpec = useMemo(
Expand All @@ -72,7 +72,7 @@ export const Filters = ({ onFiltersChange }: FiltersProps) => {
return null;
}

if (dataViewIsLoading || dataViewIsRefetching) {
if (dataViewIsLoading) {
return (
<EuiFlexItem grow={true}>
<FilterGroupLoading />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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 from 'react';
import { EuiProgress, EuiFlexGroup, EuiLoadingChart } from '@elastic/eui';
import { Chart, Settings, Axis, BarSeries, Position, ScaleType } from '@elastic/charts';
import { useElasticChartsTheme } from '@kbn/charts-theme';
import { i18n } from '@kbn/i18n';
import type { AggregationResult } from '../hooks/use_fetch_chart_data';

const chartTitle = i18n.translate(
'xpack.securitySolution.assetInventory.topAssetsBarChart.chartTitle',
{
defaultMessage: 'Top 10 Asset Types',
}
);

const yAxisTitle = i18n.translate(
'xpack.securitySolution.assetInventory.topAssetsBarChart.yAxisTitle',
{
defaultMessage: 'Count of Assets',
}
);

const chartStyles = { height: '260px' };

export interface TopAssetsBarChartProps {
isLoading: boolean;
isFetching: boolean;
entities: AggregationResult[];
}

export const TopAssetsBarChart = ({ isLoading, isFetching, entities }: TopAssetsBarChartProps) => {
const baseTheme = useElasticChartsTheme();
return (
<div css={chartStyles}>
<EuiProgress size="xs" color="accent" style={{ opacity: isFetching ? 1 : 0 }} />
{isLoading ? (
<EuiFlexGroup
justifyContent="center"
alignItems="center"
css={{ height: '100%', width: '100%' }}
>
<EuiLoadingChart size="xl" />
</EuiFlexGroup>
) : (
<Chart title={chartTitle}>
<Settings baseTheme={baseTheme} showLegend={true} animateData={true} />
<Axis
id="X-axis"
position={Position.Bottom}
gridLine={{
visible: false,
}}
/>
<Axis
id="Y-axis"
position={Position.Left}
title={yAxisTitle}
maximumFractionDigits={0}
showOverlappingTicks={false}
gridLine={{
visible: false,
}}
/>
<BarSeries
id="grouped-categories"
xScaleType={ScaleType.Ordinal}
yScaleType={ScaleType.Linear}
xAccessor="category"
yAccessors={['count']}
yNice={true}
splitSeriesAccessors={['source']}
stackAccessors={['category']}
minBarHeight={1}
data={entities}
/>
</Chart>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 { RuntimePrimitiveTypes } from '@kbn/data-views-plugin/common';

export const getRuntimeMappingsFromSort = (fields: string[], sort: string[][]) => {
return sort
.filter(([field]) => fields.includes(field))
.reduce((acc, [field]) => {
const type: RuntimePrimitiveTypes = 'keyword';

return {
...acc,
[field]: {
type,
},
};
}, {});
};

export const getMultiFieldsSort = (sort: string[][]) => {
return sort.map(([id, direction]) => {
return {
...getSortField({ field: id, direction }),
};
});
};

/**
* By default, ES will sort keyword fields in case-sensitive format, the
* following fields are required to have a case-insensitive sorting.
*/
const fieldsRequiredSortingByPainlessScript = ['entity.name']; // TODO TBD

/**
* Generates Painless sorting if the given field is matched or returns default sorting
* This painless script will sort the field in case-insensitive manner
*/
const getSortField = ({ field, direction }: { field: string; direction: string }) => {
if (fieldsRequiredSortingByPainlessScript.includes(field)) {
return {
_script: {
type: 'string',
order: direction,
script: {
source: `doc["${field}"].value.toLowerCase()`,
lang: 'painless',
},
},
};
}
return { [field]: direction };
};
Original file line number Diff line number Diff line change
@@ -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 { useQuery } from '@tanstack/react-query';
import { lastValueFrom } from 'rxjs';
import { i18n } from '@kbn/i18n';
import type * as estypes from '@elastic/elasticsearch/lib/api/types';
import { showErrorToast } from '@kbn/cloud-security-posture';
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types';
import type { FindingsBaseEsQuery } from '@kbn/cloud-security-posture';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Todo: I think we should rename FindingsBaseEsQuery to not be related to Findings, i.e something like BaseEsQuery (in a separate PR)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(can be done in this PR as well if its just a simple rename)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@opauloh @JordanSh I raised a separate PR, even if minor, since it touches a bunch of unrelated files. Still in draft since it depends on this PR:

import { useKibana } from '../../common/lib/kibana';
import { ASSET_INVENTORY_INDEX_PATTERN } from '../constants';
import { getMultiFieldsSort } from './fetch_utils';

interface UseTopAssetsOptions extends FindingsBaseEsQuery {
sort: string[][];
enabled: boolean;
}

const getTopAssetsQuery = ({ query, sort }: UseTopAssetsOptions) => ({
size: 0,
index: ASSET_INVENTORY_INDEX_PATTERN,
aggs: {
entityCategory: {
terms: {
field: 'entity.category',
order: {
entityId: 'desc',
},
size: 10,
},
aggs: {
entityType: {
terms: {
field: 'entity.type',
order: {
entityId: 'desc',
},
size: 10,
},
aggs: {
entityId: {
value_count: {
field: 'entity.id',
},
},
},
},
entityId: {
value_count: {
field: 'entity.id',
},
},
},
},
},
query: {
...query,
bool: {
...query?.bool,
filter: [...(query?.bool?.filter ?? [])],
should: [...(query?.bool?.should ?? [])],
must: [...(query?.bool?.must ?? [])],
must_not: [...(query?.bool?.must_not ?? [])],
},
},
sort: getMultiFieldsSort(sort),
ignore_unavailable: true,
});

export interface AggregationResult {
category: string;
source: string;
count: number;
}

interface TypeBucket {
key: string;
doc_count: number;
entityId: {
value: number;
};
}

interface CategoryBucket {
key: string;
doc_count: number;
entityId: {
value: number;
};
entityType: {
buckets: TypeBucket[];
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
};
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
}

interface AssetAggs {
entityCategory: {
buckets: CategoryBucket[];
};
}

const tooltipOtherLabel = i18n.translate(
'xpack.securitySolution.assetInventory.chart.tooltip.otherLabel',
{
defaultMessage: 'Other',
}
);

// Example output:
//
// [
// { category: 'cloud-compute', source: 'gcp-compute', count: 500, },
// { category: 'cloud-compute', source: 'aws-security', count: 300, },
// { category: 'cloud-storage', source: 'gcp-compute', count: 221, },
// { category: 'cloud-storage', source: 'aws-security', count: 117, },
// ];
function transformAggregation(agg: AssetAggs) {
const result: AggregationResult[] = [];

for (const categoryBucket of agg.entityCategory.buckets) {
const typeBucket = categoryBucket.entityType;

for (const sourceBucket of typeBucket.buckets) {
result.push({
category: categoryBucket.key,
source: sourceBucket.key,
count: sourceBucket.doc_count,
});
}

if (typeBucket.sum_other_doc_count > 0) {
result.push({
category: categoryBucket.key,
source: `${categoryBucket.key} - ${tooltipOtherLabel}`,
count: typeBucket.sum_other_doc_count,
});
}
}

return result;
}

type TopAssetsRequest = IKibanaSearchRequest<estypes.SearchRequest>;
type TopAssetsResponse = IKibanaSearchResponse<
estypes.SearchResponse<AggregationResult, AssetAggs>
>;

export function useFetchChartData(options: UseTopAssetsOptions) {
const {
data,
notifications: { toasts },
} = useKibana().services;
return useQuery(
['asset_inventory_top_assets_chart', { params: options }],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Consider extracting the React Query key into a constant to improve maintainability and avoid potential duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll tackle this and other minor changes in a refactor PR. But I totally agree with you 💯

async () => {
const {
rawResponse: { aggregations },
} = await lastValueFrom(
data.search.search<TopAssetsRequest, TopAssetsResponse>({
params: getTopAssetsQuery(options) as TopAssetsRequest['params'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we actually need type casting here? would be nice if we could add a return type to getTopAssetsQuery

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it's needed. Found the same type casting in several files:

  • packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts
  • packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_preview.ts
  • packages/kbn-cloud-security-posture/public/src/hooks/use_vulnerabilities_findings.ts
  • packages/kbn-cloud-security-posture/public/src/hooks/use_vulnerabilities_preview.ts
  • plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx

})
);

if (!aggregations) {
throw new Error('expected aggregations to be defined');
}

return transformAggregation(aggregations);
},
{
enabled: options.enabled,
keepPreviousData: true,
onError: (err: Error) => showErrorToast(toasts, err),
}
);
}
Loading