Skip to content
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

Integrate Asset Inventory with backend #208417

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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 @@ -4,27 +4,20 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo, useCallback } from 'react';
import React, { useMemo } from 'react';
import { EuiSpacer, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FilterGroup } from '@kbn/alerts-ui-shared/src/alert_filter_controls/filter_group';
import type { FilterControlConfig } from '@kbn/alerts-ui-shared';
import type { Filter } from '@kbn/es-query';
import { createKbnUrlStateStorage, Storage } from '@kbn/kibana-utils-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
import { useHistory } from 'react-router-dom';
import { useDataViewContext } from '../../hooks/data_view_context';
import { useSpaceId } from '../../../common/hooks/use_space_id';
import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state';
import { useGlobalTime } from '../../../common/containers/use_global_time';
import { ASSET_INVENTORY_INDEX_PATTERN } from '../../constants';
import { FilterGroupLoading } from './filters_loading';
import { ASSET_INVENTORY_RULE_TYPE_IDS } from './rule_type_ids';

const SECURITY_ASSET_INVENTORY_DATA_VIEW = {
id: 'asset-inventory-logs-default',
name: 'asset-inventory-logs',
};

const DEFAULT_ASSET_INVENTORY_FILTERS: FilterControlConfig[] = [
{
title: i18n.translate('xpack.securitySolution.assetInventory.filters.type', {
Expand Down Expand Up @@ -57,64 +50,22 @@ export interface FiltersProps {
}

export const Filters = ({ onFiltersChange }: FiltersProps) => {
const { dataView: indexPattern, dataViewIsLoading, dataViewIsRefetching } = useDataViewContext();
const { from, to } = useGlobalTime();
const { dataView, dataViewIsLoading, dataViewIsRefetching } = useDataViewContext();
const spaceId = useSpaceId();
const history = useHistory();
const urlStorage = useMemo(
() =>
createKbnUrlStateStorage({
history,
useHash: false,
useHashQuery: false,
}),
[history]
);
const filterControlsUrlState = useMemo(
() =>
urlStorage.get<FilterControlConfig[] | undefined>(URL_PARAM_KEY.assetInventory) ?? undefined,
[urlStorage]
);

const setFilterControlsUrlState = useCallback(
(newFilterControls: FilterControlConfig[]) => {
urlStorage.set(URL_PARAM_KEY.assetInventory, newFilterControls);
},
[urlStorage]
);
Comment on lines -64 to -84
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Previously I propagated showFilterBar={true} onto the <SearchBar> component. Using these props here broke the display value of the selected filter tags, rendering " - " instead.

Then, I switched showFilterBar={true} in <SearchBar> and this was not needed, so I removed it. Please let me know if this might cause an issue


const dataViewSpec = useMemo(
() =>
indexPattern
dataView
? {
id: SECURITY_ASSET_INVENTORY_DATA_VIEW.id,
name: SECURITY_ASSET_INVENTORY_DATA_VIEW.name,
// TODO We need this hard-coded id because `ASSET_INVENTORY_INDEX_PATTERN` does not populate the filter dropdowns
id: 'cloud_asset_inventory-2773feaf-50bb-43f8-9fa9-8f9a5f85e566',
name: ASSET_INVENTORY_INDEX_PATTERN,
Comment on lines +60 to +62
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: I needed to hard-code this value to populate the filters. However, in the future it should be ASSET_INVENTORY_INDEX_PATTERN, whose value is "logs-cloud_asset_inventory.asset_inventory-*"

allowNoIndex: true,
title: indexPattern.title,
title: dataView.getIndexPattern(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This tiny change happens because .title is a deprecated field

timeFieldName: '@timestamp',
}
: null,
[indexPattern]
);

const handleFilterChanges = useCallback(
(newFilters: Filter[]) => {
if (!onFiltersChange) {
return;
}
const updatedFilters = newFilters.map((filter) => {
return {
...filter,
meta: {
...filter.meta,
disabled: false,
},
};
});

onFiltersChange(updatedFilters);
},
[onFiltersChange]
[dataView]
);

if (!spaceId || !dataViewSpec) {
Expand All @@ -134,21 +85,14 @@ export const Filters = ({ onFiltersChange }: FiltersProps) => {
<EuiSpacer size="l" />
<FilterGroup
dataViewId={dataViewSpec?.id || null}
onFiltersChange={handleFilterChanges}
onFiltersChange={onFiltersChange}
ruleTypeIds={ASSET_INVENTORY_RULE_TYPE_IDS}
Storage={Storage}
defaultControls={DEFAULT_ASSET_INVENTORY_FILTERS}
chainingSystem="HIERARCHICAL"
spaceId={spaceId}
controlsUrlState={filterControlsUrlState}
setControlsUrlState={setFilterControlsUrlState}
ControlGroupRenderer={ControlGroupRenderer}
maxControls={4}
timeRange={{
from,
to,
mode: 'absolute',
}}
Comment on lines -147 to -151
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 It seems the best way to solve the time range issue is simply not passing in any timeRange value. The filter dropdowns get populated correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great, thanks for letting me know!

/>
<EuiSpacer size="l" />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const AssetInventorySearchBar = ({
<div css={getContainerStyle(euiTheme)}>
<SearchBar
appName="Asset Inventory"
showFilterBar={true}
showFilterBar={false}
showQueryInput={true}
showDatePicker={false}
isLoading={loading}
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,202 @@
/*
* 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 type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
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[] = ['entity.id', 'entity.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 buildMutedRulesFilter = (rulesStates: CspBenchmarkRulesStates): QueryDslQueryContainer[] => {
const mutedRules = Object.fromEntries(
Object.entries(rulesStates).filter(([_key, value]) => value.muted === true)
);

const mutedRulesFilterQuery = Object.keys(mutedRules).map((key) => {
// const rule = mutedRules[key];
return {
bool: {
must: [
// TODO Determine which rules are mutable
// { term: { 'rule.benchmark.id': rule.benchmark_id } },
// { term: { 'rule.benchmark.version': rule.benchmark_version } },
// { term: { 'rule.benchmark.rule_number': rule.rule_number } },
],
},
};
});

return mutedRulesFilterQuery;
};
Comment on lines +47 to +67
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This whole file was duplicated from x-pack/solutions/security/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts.

We might not need to mute any filter query but I left it for now.


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 };
};

const getAssetsQuery = (
{ query, sort }: UseAssetsOptions,
rulesStates: CspBenchmarkRulesStates,
pageParam: unknown
) => {
const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates);

return {
index: ASSET_INVENTORY_INDEX_PATTERN,
sort: getMultiFieldsSort(sort),
runtime_mappings: getRuntimeMappingsFromSort(sort),
size: MAX_ASSETS_TO_LOAD,
aggs: {
count: {
terms: {
field: 'entity.id',
},
},
},
ignore_unavailable: true,
query: {
...query,
bool: {
...query?.bool,
filter: [...(query?.bool?.filter ?? [])],
must_not: [...(query?.bool?.must_not ?? []), ...mutedRulesFilterQuery],
},
},
...(pageParam ? { from: pageParam } : {}),
};
};

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;
},
}
);
}
Loading