diff --git a/tsconfig.base.json b/tsconfig.base.json
index 787992f6e2133..aaa195b4a304d 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1058,6 +1058,8 @@
"@kbn/alerting-plugin/*": ["x-pack/plugins/alerting/*"],
"@kbn/apm-plugin": ["x-pack/plugins/apm"],
"@kbn/apm-plugin/*": ["x-pack/plugins/apm/*"],
+ "@kbn/asset-inventory-plugin": ["x-pack/plugins/asset_inventory"],
+ "@kbn/asset-inventory-plugin/*": ["x-pack/plugins/asset_inventory/*"],
"@kbn/banners-plugin": ["x-pack/plugins/banners"],
"@kbn/banners-plugin/*": ["x-pack/plugins/banners/*"],
"@kbn/canvas-plugin": ["x-pack/plugins/canvas"],
diff --git a/x-pack/plugins/asset_inventory/common/debug_log.ts b/x-pack/plugins/asset_inventory/common/debug_log.ts
new file mode 100644
index 0000000000000..fd819c0d1ecd5
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/common/debug_log.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 function debug(...args: any[]) {
+ if (process.env.NODE_ENV === 'production') {
+ return;
+ }
+ // eslint-disable-next-line no-console
+ console.log('[DEBUG LOG]', ...args);
+}
diff --git a/x-pack/plugins/asset_inventory/common/types_api.ts b/x-pack/plugins/asset_inventory/common/types_api.ts
new file mode 100644
index 0000000000000..b86a7ce0e85d4
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/common/types_api.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 type AssetKind = 'unknown' | 'node';
+export type AssetType = 'k8s.pod' | 'k8s.cluster' | 'k8s.node';
+
+export interface ECSDocument {
+ '@timestamp': string;
+ kubernetes?: EcsKubernetesFieldset;
+ orchestrator?: EcsOrchestratorFieldset;
+ cloud?: EcsCloudFieldset;
+}
+
+export type AssetStatus = 'CREATING' | 'ACTIVE' | 'DELETING' | 'FAILED' | 'UPDATING' | 'PENDING';
+
+export interface Asset extends ECSDocument {
+ 'asset.collection_version'?: string;
+ 'asset.ean': string;
+ 'asset.id': string;
+ 'asset.kind': AssetKind;
+ 'asset.name'?: string;
+ 'asset.type': AssetType;
+ 'asset.status'?: AssetStatus;
+ 'asset.parents'?: string | string[];
+ 'asset.children'?: string | string[];
+ 'asset.namespace'?: string;
+}
+
+export interface K8sPod extends ECSDocument {
+ id: string;
+ name: string;
+ ean: string;
+ node?: string;
+}
+
+export interface K8sNode extends ECSDocument {
+ id: string;
+ name: string;
+ ean: string;
+ pods?: K8sPod[];
+ cluster?: string;
+}
+
+export interface K8sCluster extends ECSDocument {
+ name: string;
+ nodes: K8sNode[];
+ status: string;
+ version: string;
+}
+
+export interface EcsKubernetesFieldset {
+ namespace?: string;
+ pod?: {
+ name: string;
+ uid: string;
+ start_time?: Date;
+ };
+ node?: {
+ name: string;
+ start_time?: Date;
+ };
+}
+
+// See: https://www.elastic.co/guide/en/ecs/current/ecs-orchestrator.html
+export interface EcsOrchestratorFieldset {
+ api_version?: string;
+ namespace?: string;
+ organization?: string;
+ type?: string;
+ cluster?: {
+ id?: string;
+ name?: string;
+ url?: string;
+ version?: string;
+ };
+ resource?: {
+ id?: string;
+ ip?: string;
+ name?: string;
+ type?: string;
+ parent?: {
+ type?: string;
+ };
+ };
+}
+
+export type CloudProviderName = 'aws' | 'gcp' | 'azure' | 'other' | 'unknown' | 'none';
+
+export interface EcsCloudFieldset {
+ provider: CloudProviderName;
+ region?: string;
+ service?: {
+ name?: string;
+ };
+}
+
+export interface AssetFilters {
+ type?: AssetType;
+ kind?: AssetKind;
+ ean?: string;
+ id?: string;
+ typeLike?: string;
+ eanLike?: string;
+ collectionVersion?: number | 'latest' | 'all';
+}
diff --git a/x-pack/plugins/asset_inventory/kibana.json b/x-pack/plugins/asset_inventory/kibana.json
new file mode 100644
index 0000000000000..e1001df0d804d
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/kibana.json
@@ -0,0 +1,30 @@
+{
+ "id": "assetInventory",
+ "owner": {
+ "name": "TBD"
+ },
+ "version": "8.0.0",
+ "kibanaVersion": "kibana",
+ "configPath": [
+ "xpack",
+ "assetInventory"
+ ],
+ "optionalPlugins": [
+ ],
+ "requiredPlugins": [
+ "alerting",
+ "cases",
+ "data",
+ "dataViews",
+ "features",
+ "inspector",
+ "unifiedSearch",
+ "usageCollection"
+ ],
+ "ui": true,
+ "server": true,
+ "requiredBundles": [
+ "kibanaReact",
+ "kibanaUtils"
+ ]
+}
diff --git a/x-pack/plugins/asset_inventory/public/app/index.tsx b/x-pack/plugins/asset_inventory/public/app/index.tsx
new file mode 100644
index 0000000000000..3377729a8b53d
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/app/index.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Route, Router, Switch } from 'react-router-dom';
+import { Storage } from '@kbn/kibana-utils-plugin/public';
+
+import { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '@kbn/core/public';
+import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
+import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
+
+import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
+import { AssetInventoryPublicPluginsStart } from '../plugin';
+import { routes } from './routes';
+import { AssetFilterContextProvider } from '../hooks/asset_filters';
+
+function App() {
+ return (
+ <>
+
+ {routes.map((routeProps) => {
+ return ;
+ })}
+
+ >
+ );
+}
+
+export function renderApp({
+ core,
+ config,
+ plugins,
+ appMountParameters,
+ usageCollection,
+ isDev,
+}: {
+ core: CoreStart;
+ config: {};
+ plugins: AssetInventoryPublicPluginsStart;
+ appMountParameters: AppMountParameters;
+ usageCollection: UsageCollectionSetup;
+ isDev?: boolean;
+}) {
+ const { element, history, theme$ } = appMountParameters;
+ const i18nCore = core.i18n;
+ const isDarkMode = core.uiSettings.get('theme:darkMode');
+
+ core.chrome.setHelpExtension({
+ appName: i18n.translate('xpack.observability.feedbackMenu.appName', {
+ defaultMessage: 'Observability',
+ }),
+ links: [{ linkType: 'discuss', href: 'https://ela.st/observability-discuss' }],
+ });
+
+ // ensure all divs are .kbnAppWrappers
+ element.classList.add(APP_WRAPPER_CLASS);
+
+ const ApplicationUsageTrackingProvider =
+ usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
+
+ ReactDOM.render(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ element
+ );
+ return () => {
+ ReactDOM.unmountComponentAtNode(element);
+ };
+}
diff --git a/x-pack/plugins/asset_inventory/public/app/routes.ts b/x-pack/plugins/asset_inventory/public/app/routes.ts
new file mode 100644
index 0000000000000..aad421fc2e4a4
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/app/routes.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { RouteProps } from 'react-router-dom';
+import { AssetInventoryListPage } from '../pages/asset_inventory_list_page';
+import { K8sClustersListPage } from '../pages/k8s/clusters_list_page';
+import { K8sClusterPage } from '../pages/k8s/cluster_page';
+
+export const routes: RouteProps[] = [
+ {
+ path: '/',
+ exact: true,
+ component: AssetInventoryListPage,
+ },
+ {
+ path: '/k8s/clusters',
+ exact: true,
+ component: K8sClustersListPage,
+ },
+ {
+ path: '/k8s/clusters/:name',
+ exact: true,
+ component: K8sClusterPage,
+ },
+];
diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx
new file mode 100644
index 0000000000000..602e915f875d5
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
+import axios from 'axios';
+import React, { useEffect, useState } from 'react';
+import { AssetFilters } from '../../common/types_api';
+import { useAssetFilters } from '../hooks/asset_filters';
+
+interface AutocompleteOptions {
+ field: string;
+ label: string;
+ filtersKey: keyof AssetFilters;
+ allowMultipleValues?: boolean;
+ placeholder?: string;
+ fullWidth?: boolean;
+}
+
+interface FieldValueResult {
+ key: string;
+ doc_count: number;
+}
+
+export function AssetFilterAutocomplete({
+ field,
+ label,
+ filtersKey,
+ allowMultipleValues = false,
+ placeholder,
+ fullWidth = false,
+}: AutocompleteOptions) {
+ const [options, setOptions] = useState([]);
+ const [selected, setSelected] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const { filters, setFilters } = useAssetFilters();
+
+ const { collectionVersion } = filters;
+
+ useEffect(() => {
+ async function retrieve() {
+ setIsLoading(true);
+ const response = await axios.get<{ results: FieldValueResult[] }>(
+ `/local/api/asset-inventory/field-values?field=${field}&version=${
+ collectionVersion || 'all'
+ }`
+ );
+
+ if (response.data.results) {
+ const newOptions: EuiComboBoxOptionOption[] = response.data.results.map((result) => ({
+ label: `${result.key} (${result.doc_count})`,
+ value: result.key,
+ }));
+ setOptions(newOptions);
+ }
+ setIsLoading(false);
+ }
+
+ retrieve();
+ }, [field, collectionVersion]);
+
+ useEffect(() => {
+ const selectedValues = selected.map((option) => option.value);
+ const values = allowMultipleValues ? selectedValues : selectedValues[0];
+ setFilters((prevFilters) => ({ ...prevFilters, [filtersKey]: values }));
+ }, [selected, allowMultipleValues, filtersKey, setFilters]);
+
+ return (
+
+ setSelected(newOptions)}
+ placeholder={placeholder}
+ fullWidth={fullWidth}
+ />
+
+ );
+}
diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx
new file mode 100644
index 0000000000000..d02d7e07c6882
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { AssetFilterAutocomplete } from './asset_filter_autocomplete';
+
+export function AssetFilterControls() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx
new file mode 100644
index 0000000000000..12b7c25f766af
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 { EuiFormRow, EuiSelect, EuiSelectOption, useGeneratedHtmlId } from '@elastic/eui';
+import React from 'react';
+import { AssetFilters } from '../../common/types_api';
+import { useAssetFilters } from '../hooks/asset_filters';
+
+interface FilterSelectBoxOptions {
+ options: Array;
+ label?: string;
+ filterKey: keyof AssetFilters;
+ value: string | undefined;
+}
+
+export function AssetFilterSelectBox({
+ value = '',
+ options,
+ label,
+ filterKey,
+}: FilterSelectBoxOptions) {
+ const basicSelectId = useGeneratedHtmlId({ prefix: 'asset-filter-select_' });
+ const { setFilters } = useAssetFilters();
+
+ const euiSelectOptions = options.map((option) => {
+ if (typeof option === 'string') {
+ return { value: option, text: option };
+ } else {
+ return option;
+ }
+ });
+
+ return (
+
+ {
+ setFilters((filters) => ({ ...filters, [filterKey]: e.target.value }));
+ }}
+ />
+
+ );
+}
diff --git a/x-pack/plugins/asset_inventory/public/components/assets_table.tsx b/x-pack/plugins/asset_inventory/public/components/assets_table.tsx
new file mode 100644
index 0000000000000..d81c0f3cf3ee5
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/components/assets_table.tsx
@@ -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 from 'react';
+import { EuiBasicTable } from '@elastic/eui';
+import { Asset } from '../../common/types_api';
+
+interface AssetsTableProps {
+ assets: Asset[];
+}
+
+const columns = [
+ {
+ field: '@timestamp',
+ name: 'Timestamp',
+ render: (date: Asset['@timestamp']) => {
+ const d = new Date(date);
+ return d.toLocaleString();
+ },
+ width: '200px',
+ },
+ {
+ field: 'asset.ean',
+ name: 'EAN (Elastic Asset Name)',
+ sortable: true,
+ width: '300px',
+ },
+ {
+ field: 'asset.kind',
+ name: 'Asset Kind',
+ sortable: true,
+ },
+ {
+ field: 'asset.type',
+ name: 'Asset Type',
+ sortable: true,
+ },
+ {
+ field: 'asset.id',
+ name: 'Asset Original ID',
+ sortable: true,
+ },
+ {
+ field: 'asset.parents',
+ name: '# of parents',
+ render: (parents: Asset['asset.parents']) => parents?.length || 0,
+ width: '100px',
+ },
+ {
+ field: 'asset.children',
+ name: '# of children',
+ render: (children: Asset['asset.children']) => children?.length || 0,
+ width: '100px',
+ },
+];
+
+export function AssetsTable({ assets }: AssetsTableProps) {
+ return (
+ tableCaption="Asset Inventory Demo" items={assets} columns={columns} />
+ );
+}
diff --git a/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx b/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx
new file mode 100644
index 0000000000000..b55eb37af62fb
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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 { EuiDescriptionList, EuiPageTemplate } from '@elastic/eui';
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { K8sCluster } from '../../common/types_api';
+
+export function K8sClusterInfo({ cluster }: { cluster: K8sCluster }) {
+ const list = [
+ {
+ title: 'Cluster name',
+ description: cluster.name,
+ },
+ {
+ title: 'Status',
+ description: cluster.status,
+ },
+ {
+ title: 'Version',
+ description: cluster.version,
+ },
+ {
+ title: 'Nodes',
+ description: (
+
+ {cluster.nodes.map((node) => (
+ -
+ {node.name}
+
+ ))}
+
+ ),
+ },
+ ];
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx b/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx
new file mode 100644
index 0000000000000..b53dc920a2d0e
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 { EuiHealth, EuiIcon, EuiInMemoryTable } from '@elastic/eui';
+import { capitalize } from 'lodash';
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { AssetStatus, CloudProviderName, K8sCluster, K8sNode } from '../../common/types_api';
+
+const cloudIconMap: Record = {
+ gcp: 'logoGCP',
+ aws: 'logoAWS',
+ azure: 'logoAzure',
+ other: 'questionInCircle',
+ unknown: 'questionInCircle',
+ none: 'crossInACircleFilled',
+};
+
+const statusMap: Record = {
+ ACTIVE: 'success',
+ CREATING: 'subdued',
+ DELETING: 'subdued',
+ FAILED: 'danger',
+ UPDATING: 'subdued',
+ PENDING: 'warning',
+};
+
+export function K8sClustersTable({ clusters }: { clusters: K8sCluster[] }) {
+ const columns = [
+ {
+ field: 'name',
+ name: 'Cluster name',
+ sortable: true,
+ width: '400px',
+ render: (name: string) => {
+ return {name};
+ },
+ },
+ {
+ field: 'status',
+ name: 'Status',
+ render: (status: AssetStatus) => (
+ {capitalize(status)}
+ ),
+ },
+ {
+ field: 'cloud',
+ name: 'Provider',
+ render: (cloud: K8sCluster['cloud']) => (
+
+ ),
+ },
+ {
+ field: 'cloud',
+ name: 'Region',
+ render: (cloud: K8sCluster['cloud']) => cloud?.region || 'unknown',
+ },
+ {
+ field: 'version',
+ name: 'Version',
+ },
+ {
+ field: 'nodes',
+ name: 'Nodes',
+ render: (nodes: K8sNode[]) => <>{nodes.length}>,
+ },
+ ];
+
+ // @ts-ignore
+ return ;
+}
diff --git a/x-pack/plugins/asset_inventory/public/components/page_template.tsx b/x-pack/plugins/asset_inventory/public/components/page_template.tsx
new file mode 100644
index 0000000000000..c595bb84e1de0
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/components/page_template.tsx
@@ -0,0 +1,13 @@
+/*
+ * 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 { EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui';
+
+export function PageTemplate(props: EuiPageTemplateProps) {
+ return ;
+}
diff --git a/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx b/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx
new file mode 100644
index 0000000000000..ffb14368e5875
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx
@@ -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 React, { useContext, useState, useEffect } from 'react';
+import { AssetFilters } from '../../common/types_api';
+import { convertAssetFiltersToQueryString } from '../lib/convert_asset_filters_to_query_string';
+
+export type FilterSetter = React.Dispatch>;
+
+export interface AssetFilterContextValue {
+ filters: AssetFilters;
+ setFilters: FilterSetter;
+ filtersQS: string;
+}
+
+const AssetFilterContext = React.createContext({
+ filters: {},
+ setFilters: () => null,
+ filtersQS: '',
+});
+
+const AssetFilterContextConsumer = AssetFilterContext.Consumer;
+
+const AssetFilterContextProvider: React.FC<{}> = ({ children }) => {
+ const [filters, setFilters] = useState({});
+ const [filtersQS, setFiltersQS] = useState('');
+
+ useEffect(() => {
+ setFiltersQS(convertAssetFiltersToQueryString(filters));
+ }, [filters]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useAssetFilters = () => {
+ return useContext(AssetFilterContext);
+};
+
+export {
+ AssetFilterContext,
+ AssetFilterContextProvider,
+ AssetFilterContextConsumer,
+ useAssetFilters,
+};
diff --git a/x-pack/plugins/asset_inventory/public/index.ts b/x-pack/plugins/asset_inventory/public/index.ts
new file mode 100644
index 0000000000000..919c10b2b78ec
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/index.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/public';
+
+import {
+ Plugin,
+ AssetInventoryPublicPluginsStart,
+ AssetInventoryPublicPluginsSetup,
+ AssetInventoryPublicStart,
+ AssetInventoryPublicSetup,
+} from './plugin';
+
+export type {
+ AssetInventoryPublicSetup,
+ AssetInventoryPublicStart,
+ AssetInventoryPublicPluginsSetup,
+ AssetInventoryPublicPluginsStart,
+};
+
+export const plugin: PluginInitializer<
+ AssetInventoryPublicSetup,
+ AssetInventoryPublicStart,
+ AssetInventoryPublicPluginsSetup,
+ AssetInventoryPublicPluginsStart
+> = (initializerContext: PluginInitializerContext) => {
+ return new Plugin(initializerContext);
+};
diff --git a/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts b/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts
new file mode 100644
index 0000000000000..cb8089a8ad9eb
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { AssetFilters } from '../../common/types_api';
+
+export function convertAssetFiltersToQueryString(filters: AssetFilters) {
+ const keys = Object.keys(filters) as unknown as Array;
+ const definedKeys = keys.filter((k) => typeof filters[k] !== 'undefined');
+ return definedKeys.map((k) => `${k}=${filters[k]}`).join('&');
+}
diff --git a/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx
new file mode 100644
index 0000000000000..fc32bc30f59e3
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx
@@ -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 React, { useEffect, useState } from 'react';
+import { EuiButton, EuiPageTemplate, EuiSpacer } from '@elastic/eui';
+import axios from 'axios';
+import { useHistory } from 'react-router-dom';
+import { PageTemplate } from '../components/page_template';
+import { Asset } from '../../common/types_api';
+import { AssetsTable } from '../components/assets_table';
+import { AssetFilterControls } from '../components/asset_filter_controls';
+import { useAssetFilters } from '../hooks/asset_filters';
+
+export function AssetInventoryListPage() {
+ const [assets, setAssets] = useState([]);
+ const { filtersQS } = useAssetFilters();
+ const history = useHistory();
+
+ useEffect(() => {
+ // console.log('Filters changed, new qs:', filtersQS);
+
+ async function retrieve() {
+ const response = await axios.get(`/local/api/asset-inventory?${filtersQS}`);
+ if (response.data && response.data.assets) {
+ setAssets(response.data.assets);
+ }
+ }
+ retrieve();
+ }, [filtersQS]);
+
+ return (
+
+ ) => {
+ e.preventDefault();
+ history.push('/k8s/clusters');
+ }}
+ >
+ K8s Clusters
+ ,
+ ]}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx b/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx
new file mode 100644
index 0000000000000..72ac767b3fb35
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import { EuiIcon, EuiPageTemplate } from '@elastic/eui';
+import axios from 'axios';
+import { useHistory, useParams } from 'react-router-dom';
+import { K8sCluster } from '../../../common/types_api';
+import { PageTemplate } from '../../components/page_template';
+import { K8sClusterInfo } from '../../components/k8s_cluster_info';
+
+export function K8sClusterPage() {
+ const [cluster, setCluster] = useState(null);
+ const { name } = useParams<{ name: string }>();
+ const history = useHistory();
+
+ useEffect(() => {
+ async function retrieve() {
+ const response = await axios.get(
+ `/local/api/asset-inventory/k8s/clusters/${name}`
+ );
+ if (response.data && response.data?.result) {
+ setCluster(response.data.result);
+ }
+ }
+ retrieve();
+ }, [name]);
+
+ if (cluster === null) {
+ return null;
+ }
+
+ return (
+
+
+ Return to cluster list
+ >
+ ),
+ color: 'primary',
+ 'aria-current': false,
+ href: '#',
+ onClick: (e) => {
+ e.preventDefault();
+ history.push('/k8s/clusters');
+ },
+ },
+ ]}
+ />
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx b/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx
new file mode 100644
index 0000000000000..0d8d2751cf7a9
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import { EuiButton, EuiPageTemplate } from '@elastic/eui';
+import axios from 'axios';
+import { useHistory } from 'react-router-dom';
+import { K8sCluster } from '../../../common/types_api';
+import { PageTemplate } from '../../components/page_template';
+import { K8sClustersTable } from '../../components/k8s_clusters_table';
+
+export function K8sClustersListPage() {
+ const [clusters, setClusters] = useState([]);
+ const history = useHistory();
+
+ useEffect(() => {
+ async function retrieve() {
+ const response = await axios.get(
+ `/local/api/asset-inventory/k8s/clusters`
+ );
+ if (response.data && response.data?.results) {
+ setClusters(response.data.results);
+ }
+ }
+ retrieve();
+ }, []);
+
+ return (
+
+ ) => {
+ e.preventDefault();
+ history.push('/');
+ }}
+ >
+ Asset Inventory
+ ,
+ ]}
+ />
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/asset_inventory/public/plugin.ts b/x-pack/plugins/asset_inventory/public/plugin.ts
new file mode 100644
index 0000000000000..96d10585a176a
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/public/plugin.ts
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+// NOTE: Originally copied from plugins/observability/public/plugin.ts, heavily modified
+
+import { i18n } from '@kbn/i18n';
+import { BehaviorSubject } from 'rxjs';
+import {
+ AppMountParameters,
+ AppUpdater,
+ CoreSetup,
+ CoreStart,
+ DEFAULT_APP_CATEGORIES,
+ Plugin as PluginClass,
+ PluginInitializerContext,
+} from '@kbn/core/public';
+
+import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
+import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
+import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
+
+export type AssetInventoryPublicSetup = ReturnType;
+
+export interface AssetInventoryPublicPluginsSetup {
+ data: DataPublicPluginSetup;
+ usageCollection: UsageCollectionSetup;
+}
+
+export interface AssetInventoryPublicPluginsStart {
+ data: DataPublicPluginStart;
+ dataViews: DataViewsPublicPluginStart;
+}
+
+export type AssetInventoryPublicStart = ReturnType;
+
+export class Plugin
+ implements
+ PluginClass<
+ AssetInventoryPublicSetup,
+ AssetInventoryPublicStart,
+ AssetInventoryPublicPluginsSetup,
+ AssetInventoryPublicPluginsStart
+ >
+{
+ private readonly appUpdater$ = new BehaviorSubject(() => ({}));
+
+ constructor(private readonly initContext: PluginInitializerContext<{}>) {}
+
+ public setup(
+ coreSetup: CoreSetup,
+ pluginsSetup: AssetInventoryPublicPluginsSetup
+ ) {
+ const category = DEFAULT_APP_CATEGORIES.observability;
+ const euiIconType = 'logoObservability';
+ const config = this.initContext.config.get();
+
+ const mount = async (params: AppMountParameters) => {
+ // Load application bundle
+ const { renderApp } = await import('./app');
+ // Get start services
+ const [coreStart, pluginsStart] = await coreSetup.getStartServices();
+
+ return renderApp({
+ core: coreStart,
+ config,
+ plugins: pluginsStart,
+ appMountParameters: params,
+ // ObservabilityPageTemplate: navigation.PageTemplate,
+ usageCollection: pluginsSetup.usageCollection,
+ isDev: this.initContext.env.mode.dev,
+ });
+ };
+
+ const appUpdater$ = this.appUpdater$;
+ const app = {
+ appRoute: '/app/asset-inventory',
+ category,
+ euiIconType,
+ id: 'assetInventory',
+ mount,
+ order: 8000,
+ title: i18n.translate('xpack.assetInventory.overviewTitle', {
+ defaultMessage: 'Asset Inventory',
+ }),
+ updater$: appUpdater$,
+ keywords: [
+ 'observability',
+ 'monitor',
+ 'logs',
+ 'metrics',
+ 'apm',
+ 'performance',
+ 'trace',
+ 'agent',
+ 'rum',
+ 'user',
+ 'experience',
+ ],
+ };
+
+ coreSetup.application.register(app);
+
+ return {};
+ }
+
+ public start(coreStart: CoreStart, pluginsStart: AssetInventoryPublicPluginsStart) {}
+}
diff --git a/x-pack/plugins/asset_inventory/server/constants.ts b/x-pack/plugins/asset_inventory/server/constants.ts
new file mode 100644
index 0000000000000..a16b78045a8d8
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * 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 ASSETS_INDEX = 'assets*';
diff --git a/x-pack/plugins/asset_inventory/server/index.ts b/x-pack/plugins/asset_inventory/server/index.ts
new file mode 100644
index 0000000000000..d2f12f290c994
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/index.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+// TODO: https://github.com/elastic/kibana/issues/110905
+
+import { AssetInventoryServerPlugin } from './plugin';
+
+export const plugin = () => new AssetInventoryServerPlugin();
diff --git a/x-pack/plugins/asset_inventory/server/lib/es_client.ts b/x-pack/plugins/asset_inventory/server/lib/es_client.ts
new file mode 100644
index 0000000000000..54fb630d3ee51
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/lib/es_client.ts
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// TODO: Replace all of this with Kibana ES client and/or data plugin
+
+import { Client } from '@elastic/elasticsearch';
+const {
+ ELASTICSEARCH_HOSTS = 'https://localhost:9200',
+ ELASTICSEARCH_USERNAME = 'elastic',
+ ELASTICSEARCH_PASSWORD = 'changeme',
+} = process.env;
+
+export const esClient = new Client({
+ node: ELASTICSEARCH_HOSTS,
+ auth: {
+ username: ELASTICSEARCH_USERNAME,
+ password: ELASTICSEARCH_PASSWORD,
+ },
+ tls: {
+ rejectUnauthorized: false,
+ },
+});
diff --git a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts
new file mode 100644
index 0000000000000..8540865ea1afc
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts
@@ -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 { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
+import { debug } from '../../common/debug_log';
+import { Asset, AssetFilters } from '../../common/types_api';
+import { ASSETS_INDEX } from '../constants';
+import { esClient } from './es_client';
+
+interface GetAssetsOptions {
+ filters?: AssetFilters;
+}
+
+export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promise {
+ const dsl: SearchRequest = {
+ index: ASSETS_INDEX,
+ };
+
+ if (filters && Object.keys(filters).length > 0) {
+ const musts: QueryDslQueryContainer[] = [];
+
+ if (typeof filters.collectionVersion === 'number') {
+ musts.push({
+ term: {
+ ['asset.collection_version']: filters.collectionVersion,
+ },
+ });
+ }
+
+ if (filters.type) {
+ musts.push({
+ term: {
+ ['asset.type']: filters.type,
+ },
+ });
+ }
+
+ if (filters.kind) {
+ musts.push({
+ term: {
+ ['asset.kind']: filters.kind,
+ },
+ });
+ }
+
+ if (filters.ean) {
+ musts.push({
+ term: {
+ ['asset.ean']: filters.ean,
+ },
+ });
+ }
+
+ if (filters.id) {
+ musts.push({
+ term: {
+ ['asset.id']: filters.id,
+ },
+ });
+ }
+
+ if (filters.typeLike) {
+ musts.push({
+ wildcard: {
+ ['asset.type']: filters.typeLike,
+ },
+ });
+ }
+
+ if (filters.eanLike) {
+ musts.push({
+ wildcard: {
+ ['asset.ean']: filters.eanLike,
+ },
+ });
+ }
+
+ if (musts.length > 0) {
+ dsl.query = {
+ bool: {
+ must: musts,
+ },
+ };
+ }
+
+ dsl.collapse = {
+ field: 'asset.ean',
+ };
+
+ dsl.sort = {
+ '@timestamp': {
+ order: 'desc',
+ },
+ };
+ }
+
+ debug('Performing Asset Query', '\n\n', JSON.stringify(dsl, null, 2));
+
+ const response = await esClient.search<{}>(dsl);
+ return response.hits.hits.map((hit) => hit._source as Asset);
+}
diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts
new file mode 100644
index 0000000000000..179c4b3fd3e63
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
+import { debug } from '../../common/debug_log';
+import { EcsOrchestratorFieldset, K8sCluster } from '../../common/types_api';
+import { ASSETS_INDEX } from '../constants';
+import { esClient } from './es_client';
+import { getK8sNodes } from './get_k8s_nodes';
+
+export async function getK8sCluster(name: string): Promise {
+ const dsl: SearchRequest = {
+ index: ASSETS_INDEX,
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ ['asset.name']: name,
+ },
+ },
+ {
+ term: {
+ ['asset.type']: 'k8s.cluster',
+ },
+ },
+ ],
+ },
+ },
+ collapse: {
+ field: 'asset.ean',
+ },
+ sort: {
+ '@timestamp': {
+ order: 'desc',
+ },
+ },
+ };
+
+ debug('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2));
+
+ const response = await esClient.search<{
+ '@timestamp': string;
+ 'asset.name': string;
+ 'asset.ean': string;
+ 'asset.id': string;
+ orchestrator: EcsOrchestratorFieldset;
+ }>(dsl);
+
+ const { _source: cluster } = response.hits.hits[0];
+ if (!cluster) {
+ throw new Error('No cluster returned');
+ }
+ const nodes = await getK8sNodes({ clusterEan: cluster['asset.ean'] });
+
+ return {
+ '@timestamp': cluster['@timestamp'],
+ name: cluster['asset.name'],
+ nodes,
+ status: 'Healthy',
+ version: cluster.orchestrator?.cluster?.version || 'unspecified',
+ };
+}
diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts
new file mode 100644
index 0000000000000..143cff526fe98
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
+import { debug } from '../../common/debug_log';
+import { Asset, K8sCluster } from '../../common/types_api';
+import { ASSETS_INDEX } from '../constants';
+import { esClient } from './es_client';
+import { getK8sNodes } from './get_k8s_nodes';
+
+export async function getK8sClusters(): Promise {
+ const dsl: SearchRequest = {
+ index: ASSETS_INDEX,
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ ['asset.type']: 'k8s.cluster',
+ },
+ },
+ ],
+ },
+ },
+ collapse: {
+ field: 'asset.ean',
+ },
+ sort: {
+ '@timestamp': {
+ order: 'desc',
+ },
+ },
+ };
+
+ debug('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2));
+
+ const response = await esClient.search(dsl);
+
+ const results = await Promise.all(
+ response.hits.hits.map(async (hit) => {
+ if (!hit._source) {
+ throw new Error('Missing _source in cluster result');
+ }
+ const doc = hit._source;
+ return {
+ '@timestamp': doc['@timestamp'],
+ name: doc['asset.name'] || doc['asset.id'],
+ nodes: await getK8sNodes({ clusterEan: doc['asset.ean'] }),
+ status: doc['asset.status'] || 'UNKNOWN',
+ cloud: doc.cloud,
+ version: doc.orchestrator?.cluster?.version || 'Unspecified',
+ };
+ })
+ );
+
+ return results;
+}
diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts
new file mode 100644
index 0000000000000..003cd1c6b3a38
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
+import { debug } from '../../common/debug_log';
+import { K8sNode } from '../../common/types_api';
+import { ASSETS_INDEX } from '../constants';
+import { esClient } from './es_client';
+
+interface GetK8sNodesOptions {
+ clusterEan?: string;
+}
+
+export async function getK8sNodes({ clusterEan }: GetK8sNodesOptions = {}): Promise {
+ const dsl: SearchRequest = {
+ index: ASSETS_INDEX,
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ ['asset.type']: 'k8s.node',
+ },
+ },
+ {
+ term: {
+ 'asset.parents': clusterEan,
+ },
+ },
+ ],
+ },
+ },
+ collapse: {
+ field: 'asset.ean',
+ },
+ sort: {
+ '@timestamp': {
+ order: 'desc',
+ },
+ },
+ };
+
+ debug('Performing K8s Nodes Query', '\n\n', JSON.stringify(dsl, null, 2));
+
+ const response = await esClient.search<{
+ '@timestamp': string;
+ 'asset.id': string;
+ 'asset.name': string;
+ 'asset.ean': string;
+ }>(dsl);
+
+ const results = await Promise.all(
+ response.hits.hits.map(async (hit) => {
+ if (!hit._source) {
+ throw new Error('Missing _source in node result');
+ }
+ const s = hit._source;
+ return {
+ '@timestamp': s['@timestamp'],
+ id: s['asset.id'],
+ name: s['asset.name'],
+ ean: s['asset.ean'],
+ };
+ })
+ );
+
+ return results;
+}
diff --git a/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts b/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts
new file mode 100644
index 0000000000000..60a778dad78dd
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
+import { debug } from '../../common/debug_log';
+import { ASSETS_INDEX } from '../constants';
+import { esClient } from './es_client';
+
+export async function getLatestCollectionVersion() {
+ const dsl: SearchRequest = {
+ index: ASSETS_INDEX,
+ size: 0,
+ aggregations: {
+ maxVersion: {
+ max: {
+ field: 'asset.collectionVersion',
+ },
+ },
+ },
+ };
+
+ debug('Performing Latest Version Query', '\n\n', JSON.stringify(dsl, null, 2));
+
+ const response = await esClient.search(dsl);
+ return response.aggregations?.maxVersion.value || 0;
+}
diff --git a/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts b/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts
new file mode 100644
index 0000000000000..c8a487a1ee517
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
+import { debug } from '../../common/debug_log';
+import { Asset } from '../../common/types_api';
+import { ASSETS_INDEX } from '../constants';
+import { esClient } from './es_client';
+
+interface GetValuesForFieldOptions {
+ field: keyof Asset;
+ version?: number;
+ searchText?: string;
+}
+
+interface AggBucket {
+ key: string;
+ doc_count: number;
+}
+
+export async function getValuesForField({
+ field,
+ version,
+ searchText,
+}: GetValuesForFieldOptions): Promise {
+ const dsl: SearchRequest = {
+ index: ASSETS_INDEX,
+ size: 0,
+ aggs: {
+ field_values: {
+ terms: {
+ field,
+ include: searchText,
+ },
+ },
+ },
+ };
+
+ if (version || version === 0) {
+ dsl.query = {
+ bool: {
+ must: [
+ {
+ term: {
+ ['asset.collection_version']: version,
+ },
+ },
+ ],
+ },
+ };
+ }
+
+ debug(`Performing Field Value Query for ${field}`, '\n\n', JSON.stringify(dsl, null, 2));
+
+ const response = await esClient.search<{}, { field_values: { buckets: AggBucket[] } }>(dsl);
+ return response.aggregations?.field_values.buckets || [];
+}
diff --git a/x-pack/plugins/asset_inventory/server/plugin.ts b/x-pack/plugins/asset_inventory/server/plugin.ts
new file mode 100644
index 0000000000000..f393f4f99335a
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/server/plugin.ts
@@ -0,0 +1,123 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+import { Plugin, CoreSetup } from '@kbn/core/server';
+import { debug } from '../common/debug_log';
+import { AssetFilters } from '../common/types_api';
+import { getAssets } from './lib/get_assets';
+import { getK8sCluster } from './lib/get_k8s_cluster';
+import { getK8sClusters } from './lib/get_k8s_clusters';
+import { getValuesForField } from './lib/get_values_for_field';
+
+export type AssetInventoryServerPluginSetup = ReturnType;
+
+export class AssetInventoryServerPlugin implements Plugin {
+ public async setup(core: CoreSetup) {
+ const router = core.http.createRouter();
+
+ router.get(
+ {
+ path: '/api/asset-inventory/ping',
+ validate: false,
+ },
+ (context, req, res) => {
+ return res.ok({
+ body: { message: 'Asset Inventory OK' },
+ headers: { 'content-type': 'application/json' },
+ });
+ }
+ );
+
+ router.get(
+ {
+ path: '/api/asset-inventory',
+ validate: {
+ query: schema.any({}),
+ },
+ },
+ async (context, req, res) => {
+ const filters = req.query || {};
+
+ try {
+ const assets = await getAssets({ filters });
+ return res.ok({ body: { assets } });
+ } catch (error: unknown) {
+ debug('error looking up asset records', error);
+ return res.customError({ statusCode: 500 });
+ }
+ }
+ );
+
+ router.get(
+ {
+ path: '/api/asset-inventory/field-values',
+ validate: {
+ query: schema.any({}),
+ },
+ },
+ async (context, req, res) => {
+ const { field, searchText, version } = req.query;
+
+ try {
+ const results = await getValuesForField({
+ field,
+ searchText,
+ version: Number(version),
+ });
+ return res.ok({ body: { results } });
+ } catch (error: unknown) {
+ debug('error looking up field values', error);
+ return res.customError({ statusCode: 500 });
+ }
+ }
+ );
+
+ router.get(
+ {
+ path: '/api/asset-inventory/k8s/clusters',
+ validate: false,
+ },
+ async (context, req, res) => {
+ try {
+ const results = await getK8sClusters();
+ return res.ok({ body: { results } });
+ } catch (error: unknown) {
+ debug('error looking up field values', error);
+ return res.customError({ statusCode: 500 });
+ }
+ }
+ );
+
+ router.get<{ name: string }, {}, {}>(
+ {
+ path: '/api/asset-inventory/k8s/clusters/{name}',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ }),
+ },
+ },
+ async (context, req, res) => {
+ const name = req.params.name;
+ try {
+ const result = await getK8sCluster(name);
+ return res.ok({ body: { result } });
+ } catch (error: unknown) {
+ debug('error looking up field values', error);
+ return res.customError({ statusCode: 500 });
+ }
+ }
+ );
+
+ return {};
+ }
+
+ public start() {}
+
+ public stop() {}
+}
diff --git a/x-pack/plugins/asset_inventory/tsconfig.json b/x-pack/plugins/asset_inventory/tsconfig.json
new file mode 100644
index 0000000000000..4d38e0dfeaee4
--- /dev/null
+++ b/x-pack/plugins/asset_inventory/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "common/**/*",
+ "public/**/*",
+ "public/**/*.json",
+ "server/**/*",
+ "typings/**/*",
+ "../../../typings/**/*"
+ ],
+ "references": [
+ { "path": "../../../src/core/tsconfig.json" },
+ { "path": "../../../src/plugins/data/tsconfig.json" },
+ { "path": "../../../src/plugins/embeddable/tsconfig.json" },
+ { "path": "../../../src/plugins/home/tsconfig.json" },
+ { "path": "../../../src/plugins/inspector/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../actions/tsconfig.json" },
+ { "path": "../alerting/tsconfig.json" },
+ { "path": "../cloud/tsconfig.json" },
+ { "path": "../features/tsconfig.json" },
+ { "path": "../infra/tsconfig.json" },
+ { "path": "../licensing/tsconfig.json" },
+ { "path": "../maps/tsconfig.json" },
+ { "path": "../ml/tsconfig.json" },
+ { "path": "../observability/tsconfig.json" },
+ { "path": "../reporting/tsconfig.json" },
+ { "path": "../rule_registry/tsconfig.json" },
+ { "path": "../security/tsconfig.json" },
+ { "path": "../task_manager/tsconfig.json" },
+ { "path": "../triggers_actions_ui/tsconfig.json" },
+ { "path": "../fleet/tsconfig.json" }
+ ]
+}