Skip to content

Commit

Permalink
Integrate AssetInventory w/ backend & render rows
Browse files Browse the repository at this point in the history
  • Loading branch information
albertoblaz committed Jan 30, 2025
1 parent cd9096c commit ad74938
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ describe('AssetInventory', () => {
cleanup();
});

it('renders unknown risk with undefined risk score', async () => {
render(<RiskBadge data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
expect(badge).toHaveTextContent('');

fireEvent.mouseOver(badge.parentElement as Node);
await waitForEuiToolTipVisible();

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(RiskSeverity.Unknown);
});
it('renders unknown risk with 0 risk score', async () => {
render(<RiskBadge risk={0} data-test-subj="badge" />);
const badge = screen.getByTestId('badge');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { RISK_SEVERITY_COLOUR } from '../../entity_analytics/common/utils';
import { getRiskLevel } from '../../../common/entity_analytics/risk_engine/risk_levels';

export interface RiskBadgeProps {
risk: number;
risk?: number;
'data-test-subj'?: string;
}

Expand All @@ -39,10 +39,12 @@ const tooltips = {
};

export const RiskBadge = ({ risk, ...props }: RiskBadgeProps) => {
const riskLevel = getRiskLevel(risk);
const riskLevel = risk ? getRiskLevel(risk) : RiskSeverity.Unknown;
const color = RISK_SEVERITY_COLOUR[riskLevel];
const tooltipContent = tooltips[riskLevel];

return (
<EuiToolTip content={tooltips[riskLevel]}>
<EuiToolTip content={tooltipContent}>
<EuiBadge {...props} color={color}>
{risk}
</EuiBadge>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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.
*/

export const MAX_ASSETS_TO_LOAD = 500; // equivalent to MAX_FINDINGS_TO_LOAD in @kbn/cloud-security-posture-common
export const DEFAULT_VISIBLE_ROWS_PER_PAGE = 25;
export const ASSET_INVENTORY_INDEX_PATTERN = 'logs-cloud_asset_inventory.asset_inventory-*';
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* 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 { useInfiniteQuery } from '@tanstack/react-query';
import { lastValueFrom } from 'rxjs';
import { number } from 'io-ts';
import type * as estypes from '@elastic/elasticsearch/lib/api/types';
import { buildDataTableRecord } from '@kbn/discover-utils';
import type { EsHitRecord } from '@kbn/discover-utils/types';
import type { RuntimePrimitiveTypes } from '@kbn/data-views-plugin/common';
import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest';
import { showErrorToast } from '@kbn/cloud-security-posture';
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types';
import { useGetCspBenchmarkRulesStatesApi } from '@kbn/cloud-security-posture/src/hooks/use_get_benchmark_rules_state_api';
import type { FindingsBaseEsQuery } from '@kbn/cloud-security-posture';
import { useKibana } from '../../common/lib/kibana';
import { MAX_ASSETS_TO_LOAD, ASSET_INVENTORY_INDEX_PATTERN } from '../constants';

interface UseAssetsOptions extends FindingsBaseEsQuery {
sort: string[][];
enabled: boolean;
pageSize: number;
}

const ASSET_INVENTORY_TABLE_RUNTIME_MAPPING_FIELDS: string[] = ['asset.risk', 'asset.name'];

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

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

const getAssetsQuery = (
{ query, sort }: UseAssetsOptions,
rulesStates: CspBenchmarkRulesStates,
pageParam: unknown
) => {
return {
index: ASSET_INVENTORY_INDEX_PATTERN,
sort: [{ '@timestamp': 'desc' }],
runtime_mappings: getRuntimeMappingsFromSort(sort),
size: MAX_ASSETS_TO_LOAD,
aggs: {
count: {
terms: {
field: 'asset.name',
},
},
},
ignore_unavailable: true,
query: {
bool: {
must: [],
filter: [
{
range: {
'@timestamp': {
gte: 'now-90d',
lte: 'now',
},
},
},
],
should: [],
must_not: [],
},
},
};
};

interface Asset {
'@timestamp': string;
name: string;
risk: number;
criticality: string;
category: string;
}

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

type LatestAssetsRequest = IKibanaSearchRequest<estypes.SearchRequest>;
type LatestAssetsResponse = IKibanaSearchResponse<estypes.SearchResponse<Asset, AssetsAggs>>;

const getAggregationCount = (
buckets: Array<estypes.AggregationsStringRareTermsBucketKeys | undefined>
) => {
const passed = buckets.find((bucket) => bucket?.key === 'passed');
const failed = buckets.find((bucket) => bucket?.key === 'failed');

return {
passed: passed?.doc_count || 0,
failed: failed?.doc_count || 0,
};
};

export function useFetchData(options: UseAssetsOptions) {
const {
data,
notifications: { toasts },
} = useKibana().services;
const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi();

return useInfiniteQuery(
['asset_inventory', { params: options }, rulesStates],
async ({ pageParam }) => {
const {
rawResponse: { hits, aggregations },
} = await lastValueFrom(
data.search.search<LatestAssetsRequest, LatestAssetsResponse>({
// ruleStates always exists since it under the `enabled` dependency.
params: getAssetsQuery(options, rulesStates!, pageParam) as LatestAssetsRequest['params'], // eslint-disable-line @typescript-eslint/no-non-null-assertion
})
);
if (!aggregations) throw new Error('expected aggregations to be an defined');
if (!Array.isArray(aggregations.count.buckets))
throw new Error('expected buckets to be an array');

return {
page: hits.hits.map((hit) => buildDataTableRecord(hit as EsHitRecord)),
total: number.is(hits.total) ? hits.total : 0,
count: getAggregationCount(aggregations.count.buckets),
};
},
{
enabled: options.enabled && !!rulesStates,
keepPreviousData: true,
onError: (err: Error) => showErrorToast(toasts, err),
getNextPageParam: (lastPage, allPages) => {
if (lastPage.page.length < options.pageSize) {
return undefined;
}
return allPages.length * options.pageSize;
},
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
type AssetsBaseURLQuery,
type URLQuery,
} from '../hooks/use_asset_inventory_data_table';
import { MAX_ASSETS_TO_LOAD } from '../constants';

const gridStyle: EuiDataGridStyle = {
border: 'horizontal',
Expand All @@ -62,8 +63,6 @@ const gridStyle: EuiDataGridStyle = {
header: 'underline',
};

const MAX_ASSETS_TO_LOAD = 500; // equivalent to MAX_FINDINGS_TO_LOAD in @kbn/cloud-security-posture-common

const title = i18n.translate('xpack.securitySolution.assetInventory.allAssets.tableRowTypeLabel', {
defaultMessage: 'assets',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { SecuritySubPluginRoutes } from '../app/types';
import { SecurityPageName } from '../app/types';
import { ASSET_INVENTORY_PATH } from '../../common/constants';
Expand All @@ -16,6 +17,8 @@ import { PluginTemplateWrapper } from '../common/components/plugin_template_wrap
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
import { DataViewContext } from './hooks/data_view_context';
import { useDataView } from './hooks/use_asset_inventory_data_table/use_data_view';
import { useFetchData } from './hooks/use_fetch_data';
import { DEFAULT_VISIBLE_ROWS_PER_PAGE, ASSET_INVENTORY_INDEX_PATTERN } from './constants';

const AllAssetsLazy = lazy(() => import('./pages/all_assets'));

Expand All @@ -30,7 +33,12 @@ const queryClient = new QueryClient({
},
});

const ASSET_INVENTORY_INDEX_PATTERN = 'logs-cloud_asset_inventory.asset_inventory-*';
const getRowsFromPages = (data: Array<{ page: DataTableRecord[] }> | undefined) =>
data
?.map(({ page }: { page: DataTableRecord[] }) => {
return page;
})
.flat() || [];

export const AssetInventoryRoutes = () => {
const dataViewQuery = useDataView(ASSET_INVENTORY_INDEX_PATTERN);
Expand All @@ -42,6 +50,18 @@ export const AssetInventoryRoutes = () => {
dataViewIsRefetching: dataViewQuery.isRefetching,
};

const {
data,
// error: fetchError,
isFetching,
fetchNextPage,
isLoading,
} = useFetchData({
sort: [['@timestamp', 'desc']],
enabled: true,
pageSize: DEFAULT_VISIBLE_ROWS_PER_PAGE,
});

return (
<QueryClientProvider client={queryClient}>
<PluginTemplateWrapper>
Expand All @@ -50,9 +70,9 @@ export const AssetInventoryRoutes = () => {
<SecuritySolutionPageWrapper noPadding>
<Suspense fallback={<EuiLoadingSpinner />}>
<AllAssetsLazy
rows={[]}
isLoading={false}
loadMore={() => {}}
rows={getRowsFromPages(data?.pages)}
isLoading={isLoading || isFetching}
loadMore={fetchNextPage}
flyoutComponent={() => <></>}
/>
</Suspense>
Expand Down

0 comments on commit ad74938

Please sign in to comment.