From 6804232663b330843966210511a35cf0396a7e3a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 28 Sep 2023 14:26:09 -0400 Subject: [PATCH 01/10] skip failing test suite (#167552) --- x-pack/test/functional/apps/lens/group6/annotations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/group6/annotations.ts b/x-pack/test/functional/apps/lens/group6/annotations.ts index aa3b409d21fb0..cac637ca64ff6 100644 --- a/x-pack/test/functional/apps/lens/group6/annotations.ts +++ b/x-pack/test/functional/apps/lens/group6/annotations.ts @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const from = 'Sep 19, 2015 @ 06:31:44.000'; const to = 'Sep 23, 2015 @ 18:31:44.000'; - describe('lens annotations tests', () => { + // Failing: See https://github.com/elastic/kibana/issues/167552 + describe.skip('lens annotations tests', () => { before(async () => { await PageObjects.common.setTime({ from, to }); }); From 5a785e8a4197e415f7c680e8623882be30c7d3b8 Mon Sep 17 00:00:00 2001 From: Alex Szabo Date: Thu, 28 Sep 2023 20:35:32 +0200 Subject: [PATCH 02/10] [Ops] ES Serverless image verification fixes (#167223) ## Summary ### Error 1: Strict mode throws an error on non-filled variables. I forgot to add this when creating the script ``` .buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh: line 40: UPLOAD_MANIFEST: unbound variable ``` ### Error 2: Uploading multi-arch images https://elastic.slack.com/archives/C5UDAFZQU/p1695725623585409 We've noticed that the downloaded `latest-verified` images are slower to start up than normal. After inspecting the manifests, it seems we were getting `linux/amd64` on our arm devices as well. The solution is to grab and upload both platform variants. (using this blog: https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/) --- .../promote_es_serverless_image.sh | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/.buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh b/.buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh index c6bf1738fe144..c0a5a0d4e8407 100755 --- a/.buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh +++ b/.buildkite/scripts/steps/es_serverless/promote_es_serverless_image.sh @@ -24,11 +24,37 @@ echo "--- Promoting ${SOURCE_IMAGE_OR_TAG} to ':latest-verified'" echo "Re-tagging $SOURCE_IMAGE -> $TARGET_IMAGE" echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co -docker pull "$SOURCE_IMAGE" -docker tag "$SOURCE_IMAGE" "$TARGET_IMAGE" -docker push "$TARGET_IMAGE" -ORIG_IMG_DATA=$(docker inspect "$SOURCE_IMAGE") +docker manifest inspect "$SOURCE_IMAGE" | tee manifests.json + +ARM_64_DIGEST=$(jq -r '.manifests[] | select(.platform.architecture == "arm64") | .digest' manifests.json) +AMD_64_DIGEST=$(jq -r '.manifests[] | select(.platform.architecture == "amd64") | .digest' manifests.json) + +echo docker pull --platform linux/arm64 "$SOURCE_IMAGE@$ARM_64_DIGEST" +docker pull --platform linux/arm64 "$SOURCE_IMAGE@$ARM_64_DIGEST" +echo linux/arm64 image pulled, with digest: $ARM_64_DIGEST + +echo docker pull --platform linux/amd64 "$SOURCE_IMAGE@$AMD_64_DIGEST" +docker pull --platform linux/amd64 "$SOURCE_IMAGE@$AMD_64_DIGEST" +echo linux/amd64 image pulled, with digest: $AMD_64_DIGEST + +docker tag "$SOURCE_IMAGE@$ARM_64_DIGEST" "$TARGET_IMAGE-arm64" +docker tag "$SOURCE_IMAGE@$AMD_64_DIGEST" "$TARGET_IMAGE-amd64" + +docker push "$TARGET_IMAGE-arm64" +docker push "$TARGET_IMAGE-amd64" + +docker manifest rm "$TARGET_IMAGE" || echo "Nothing to delete" + +docker manifest create "$TARGET_IMAGE" \ +--amend "$TARGET_IMAGE-arm64" \ +--amend "$TARGET_IMAGE-amd64" + +docker manifest push "$TARGET_IMAGE" + +docker manifest inspect "$TARGET_IMAGE" + +ORIG_IMG_DATA=$(docker inspect "$SOURCE_IMAGE@$ARM_64_DIGEST") ELASTIC_COMMIT_HASH=$(echo $ORIG_IMG_DATA | jq -r '.[].Config.Labels["org.opencontainers.image.revision"]') docker logout docker.elastic.co @@ -37,7 +63,7 @@ echo "Image push to $TARGET_IMAGE successful." echo "Promotion successful! Henceforth, thou shall be named Sir $TARGET_IMAGE" MANIFEST_UPLOAD_PATH="Skipped" -if [[ "$UPLOAD_MANIFEST" =~ ^(1|true)$ && "$SOURCE_IMAGE_OR_TAG" =~ ^git-[0-9a-fA-F]{12}$ ]]; then +if [[ "${UPLOAD_MANIFEST:-}" =~ ^(1|true)$ && "$SOURCE_IMAGE_OR_TAG" =~ ^git-[0-9a-fA-F]{12}$ ]]; then echo "--- Uploading latest-verified manifest to GCS" cat << EOT >> $MANIFEST_FILE_NAME { @@ -58,10 +84,12 @@ EOT gsutil acl ch -u AllUsers:R "gs://$ES_SERVERLESS_BUCKET/$MANIFEST_FILE_NAME" MANIFEST_UPLOAD_PATH="$MANIFEST_FILE_NAME" -elif [[ "$UPLOAD_MANIFEST" =~ ^(1|true)$ ]]; then +elif [[ "${UPLOAD_MANIFEST:-}" =~ ^(1|true)$ ]]; then echo "--- Skipping upload of latest-verified manifest to GCS, ES Serverless build tag is not pointing to a hash" elif [[ "$SOURCE_IMAGE_OR_TAG" =~ ^git-[0-9a-fA-F]{12}$ ]]; then echo "--- Skipping upload of latest-verified manifest to GCS, flag was not provided" +else + echo "--- Skipping upload of latest-verified manifest to GCS, no flag and hash provided" fi echo "--- Annotating build with info" From 4c3fe718210d5fac8b87faf4872a991dc44a8439 Mon Sep 17 00:00:00 2001 From: Panagiota Mitsopoulou Date: Thu, 28 Sep 2023 20:39:37 +0200 Subject: [PATCH 03/10] [SLO] create SLO embeddable widget (#165949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves https://github.com/elastic/kibana/issues/165947 Resolves https://github.com/elastic/actionable-observability/issues/124 ### Summary This PR adds an Embeddable SLO Overview Widget to the Dashboard app. It uses a [Metric chart](https://elastic.github.io/elastic-charts/?path=/story/metric-alpha--basic) component and displays an overview of the SLO health: - name - current sli value - target - status (background color) ### ✔️ Acceptance criteria - The SLO widget should display the basic information listed above - The SLO widget should be clickable and lead to the slo detail page - The user should be able to select the SLO and filter to instanceId - The tag "url.domain:mail.co" is the partition field and instanceId value Screenshot 2023-09-21 at 21 07 23 For more information regarding the key concepts and the usage of an embeddable you can have a look at the Embeddable plugin [README](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/observability/kibana.jsonc | 3 +- .../slo/overview/handle_explicit_input.tsx | 55 ++++++ .../public/embeddable/slo/overview/index.ts | 8 + .../slo/overview/slo_configuration.tsx | 85 ++++++++++ .../slo/overview/slo_embeddable.tsx | 96 +++++++++++ .../slo/overview/slo_embeddable_factory.ts | 73 ++++++++ .../embeddable/slo/overview/slo_overview.tsx | 157 ++++++++++++++++++ .../embeddable/slo/overview/slo_selector.tsx | 101 +++++++++++ .../public/embeddable/slo/overview/types.ts | 15 ++ x-pack/plugins/observability/public/plugin.ts | 10 ++ x-pack/plugins/observability/tsconfig.json | 2 + 11 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/observability/public/embeddable/slo/overview/handle_explicit_input.tsx create mode 100644 x-pack/plugins/observability/public/embeddable/slo/overview/index.ts create mode 100644 x-pack/plugins/observability/public/embeddable/slo/overview/slo_configuration.tsx create mode 100644 x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable.tsx create mode 100644 x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable_factory.ts create mode 100644 x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx create mode 100644 x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx create mode 100644 x-pack/plugins/observability/public/embeddable/slo/overview/types.ts diff --git a/x-pack/plugins/observability/kibana.jsonc b/x-pack/plugins/observability/kibana.jsonc index 5064e06b156e0..5410d58ae7c92 100644 --- a/x-pack/plugins/observability/kibana.jsonc +++ b/x-pack/plugins/observability/kibana.jsonc @@ -30,7 +30,8 @@ "security", "share", "unifiedSearch", - "visualizations" + "visualizations", + "dashboard", ], "optionalPlugins": ["discover", "home", "licensing", "usageCollection", "cloud", "spaces"], "requiredBundles": ["data", "kibanaReact", "kibanaUtils", "unifiedSearch", "cloudChat", "stackAlerts", "spaces"], diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/handle_explicit_input.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/handle_explicit_input.tsx new file mode 100644 index 0000000000000..0c36b4e915c6c --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/handle_explicit_input.tsx @@ -0,0 +1,55 @@ +/* + * 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 { toMountPoint } from '@kbn/react-kibana-mount'; + +import type { CoreStart } from '@kbn/core/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { EmbeddableSloProps, SloEmbeddableInput } from './types'; + +import { ObservabilityPublicPluginsStart } from '../../..'; +import { SloConfiguration } from './slo_configuration'; +export async function resolveEmbeddableSloUserInput( + coreStart: CoreStart, + pluginStart: ObservabilityPublicPluginsStart, + input?: SloEmbeddableInput +): Promise { + const { overlays } = coreStart; + const queryClient = new QueryClient(); + return new Promise(async (resolve, reject) => { + try { + const modalSession = overlays.openModal( + toMountPoint( + + + { + modalSession.close(); + resolve(update); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + /> + + , + { i18n: coreStart.i18n, theme: coreStart.theme } + ) + ); + } catch (error) { + reject(error); + } + }); +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/index.ts b/x-pack/plugins/observability/public/embeddable/slo/overview/index.ts new file mode 100644 index 0000000000000..9cc48e8c635f2 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/index.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 { SloOverviewEmbeddableFactoryDefinition } from './slo_embeddable_factory'; diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_configuration.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_configuration.tsx new file mode 100644 index 0000000000000..cf83690800318 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_configuration.tsx @@ -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, { useState } from 'react'; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { SloSelector } from './slo_selector'; + +import type { EmbeddableSloProps } from './types'; + +interface SloConfigurationProps { + onCreate: (props: EmbeddableSloProps) => void; + onCancel: () => void; +} + +export function SloConfiguration({ onCreate, onCancel }: SloConfigurationProps) { + const [selectedSlo, setSelectedSlo] = useState(); + const onConfirmClick = () => + onCreate({ sloId: selectedSlo?.sloId, sloInstanceId: selectedSlo?.sloInstanceId }); + const [hasError, setHasError] = useState(false); + + return ( + + + + {i18n.translate('xpack.observability.sloEmbeddable.config.sloSelector.headerTitle', { + defaultMessage: 'SLO configuration', + })} + + + + + + { + if (slo === undefined) { + setHasError(true); + } else { + setHasError(false); + } + setSelectedSlo({ sloId: slo?.id, sloInstanceId: slo?.instanceId }); + }} + /> + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable.tsx new file mode 100644 index 0000000000000..faadbcb637646 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable.tsx @@ -0,0 +1,96 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { Subscription } from 'rxjs'; +import { i18n } from '@kbn/i18n'; + +import { + Embeddable as AbstractEmbeddable, + EmbeddableOutput, + IContainer, +} from '@kbn/embeddable-plugin/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { type CoreStart, IUiSettingsClient, ApplicationStart } from '@kbn/core/public'; +import { SloOverview } from './slo_overview'; +import type { SloEmbeddableInput } from './types'; + +export const SLO_EMBEDDABLE = 'SLO_EMBEDDABLE'; + +interface SloEmbeddableDeps { + uiSettings: IUiSettingsClient; + http: CoreStart['http']; + i18n: CoreStart['i18n']; + application: ApplicationStart; +} + +export class SLOEmbeddable extends AbstractEmbeddable { + public readonly type = SLO_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + + constructor( + private readonly deps: SloEmbeddableDeps, + initialInput: SloEmbeddableInput, + parent?: IContainer + ) { + super(initialInput, {}, parent); + + this.subscription = new Subscription(); + this.subscription.add(this.getInput$().subscribe(() => this.reload())); + } + + setTitle(title: string) { + this.updateInput({ title }); + } + + public render(node: HTMLElement) { + this.node = node; + this.setTitle( + this.input.title || + i18n.translate('xpack.observability.sloEmbeddable.displayTitle', { + defaultMessage: 'SLO Overview', + }) + ); + this.input.lastReloadRequestTime = Date.now(); + + const { sloId, sloInstanceId } = this.getInput(); + const queryClient = new QueryClient(); + + const I18nContext = this.deps.i18n.Context; + ReactDOM.render( + + + + + + + , + node + ); + } + + public reload() { + if (this.node) { + this.render(this.node); + } + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable_factory.ts b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable_factory.ts new file mode 100644 index 0000000000000..7adb76eb9acfe --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_embeddable_factory.ts @@ -0,0 +1,73 @@ +/* + * 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 type { CoreSetup } from '@kbn/core/public'; +import { + IContainer, + EmbeddableFactoryDefinition, + EmbeddableFactory, + ErrorEmbeddable, +} from '@kbn/embeddable-plugin/public'; +import { SLOEmbeddable, SLO_EMBEDDABLE } from './slo_embeddable'; +import { ObservabilityPublicPluginsStart, ObservabilityPublicStart } from '../../..'; +import type { SloEmbeddableInput } from './types'; + +export type SloOverviewEmbeddableFactory = EmbeddableFactory; +export class SloOverviewEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition { + public readonly type = SLO_EMBEDDABLE; + + constructor( + private getStartServices: CoreSetup< + ObservabilityPublicPluginsStart, + ObservabilityPublicStart + >['getStartServices'] + ) {} + + public async isEditable() { + return true; + } + + public async getExplicitInput(): Promise> { + const [coreStart, pluginStart] = await this.getStartServices(); + try { + const { resolveEmbeddableSloUserInput } = await import('./handle_explicit_input'); + return await resolveEmbeddableSloUserInput(coreStart, pluginStart); + } catch (e) { + return Promise.reject(); + } + } + + public async create(initialInput: SloEmbeddableInput, parent?: IContainer) { + try { + const [{ uiSettings, application, http, i18n: i18nService }] = await this.getStartServices(); + return new SLOEmbeddable( + { uiSettings, application, http, i18n: i18nService }, + initialInput, + parent + ); + } catch (e) { + return new ErrorEmbeddable(e, initialInput, parent); + } + } + + public getDescription() { + return i18n.translate('xpack.observability.sloEmbeddable.description', { + defaultMessage: 'Get an overview of your SLO health', + }); + } + + public getDisplayName() { + return i18n.translate('xpack.observability.sloEmbeddable.displayName', { + defaultMessage: 'SLO Overview', + }); + } + + public getIconType() { + return 'visGauge'; + } +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx new file mode 100644 index 0000000000000..5e8947a6c5ba9 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx @@ -0,0 +1,157 @@ +/* + * 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, { useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, useEuiBackgroundColor } from '@elastic/eui'; +import { Chart, Metric, MetricTrendShape, Settings } from '@elastic/charts'; +import numeral from '@elastic/numeral'; +import { ALL_VALUE } from '@kbn/slo-schema'; +import { EuiLoadingChart } from '@elastic/eui'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { useKibana } from '../../../utils/kibana_react'; +import { useFetchSloDetails } from '../../../hooks/slo/use_fetch_slo_details'; +import { paths } from '../../../../common/locators/paths'; + +import { EmbeddableSloProps } from './types'; + +export function SloOverview({ sloId, sloInstanceId, lastReloadRequestTime }: EmbeddableSloProps) { + const { + uiSettings, + application: { navigateToUrl }, + http: { basePath }, + } = useKibana().services; + const { isLoading, slo, refetch, isRefetching } = useFetchSloDetails({ + sloId, + instanceId: sloInstanceId, + }); + + useEffect(() => { + refetch(); + }, [lastReloadRequestTime, refetch]); + + const percentFormat = uiSettings.get('format:percent:defaultPattern'); + const isSloNotFound = !isLoading && slo === undefined; + + const getIcon = useCallback( + (type: string) => + ({ width = 20, height = 20, color }: { width: number; height: number; color: string }) => { + return ; + }, + [] + ); + + const sloSummary = slo?.summary; + const sloStatus = sloSummary?.status; + const healthyColor = useEuiBackgroundColor('success'); + const noDataColor = useEuiBackgroundColor('subdued'); + const degradingColor = useEuiBackgroundColor('warning'); + const violatedColor = useEuiBackgroundColor('danger'); + let color; + switch (sloStatus) { + case 'HEALTHY': + color = healthyColor; + break; + case 'NO_DATA': + color = noDataColor; + break; + case 'DEGRADING': + color = degradingColor; + break; + case 'VIOLATED': + color = violatedColor; + break; + default: + color = noDataColor; + } + + if (isRefetching || isLoading) { + return ( + + + + + + ); + } + + if (isSloNotFound) { + return ( + + + {i18n.translate('xpack.observability.sloEmbeddable.overview.sloNotFoundText', { + defaultMessage: + 'The SLO has been deleted. You can safely delete the widget from the dashboard.', + })} + + + ); + } + const TargetCopy = i18n.translate('xpack.observability.sloEmbeddable.overview.sloTargetLabel', { + defaultMessage: 'Target', + }); + const extraContent = `${TargetCopy} ${numeral(slo?.objective.target).format( + percentFormat + )}`; + // eslint-disable-next-line react/no-danger + const extra = ; + const metricData = + slo !== undefined + ? [ + { + color, + title: slo.name, + subtitle: slo.groupBy !== ALL_VALUE ? `${slo.groupBy}:${slo.instanceId}` : '', + icon: getIcon('visGauge'), + value: + sloStatus === 'NO_DATA' + ? NOT_AVAILABLE_LABEL + : numeral(slo.summary.sliValue).format(percentFormat), + valueFormatter: (value: number) => `${value}%`, + extra, + trend: [], + trendShape: MetricTrendShape.Area, + }, + ] + : []; + return ( + <> + + { + navigateToUrl( + basePath.prepend( + paths.observability.sloDetails( + slo!.id, + slo?.groupBy !== ALL_VALUE && slo?.instanceId ? slo.instanceId : undefined + ) + ) + ); + }} + /> + + + + ); +} + +export const LoadingContainer = euiStyled.div` + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; +`; + +export const LoadingContent = euiStyled.div` + flex: 0 0 auto; + align-self: center; + text-align: center; +`; diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx new file mode 100644 index 0000000000000..468358127bd18 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx @@ -0,0 +1,101 @@ +/* + * 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, useMemo, useState } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; +import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list'; + +interface Props { + initialSlo?: SLOWithSummaryResponse; + onSelected: (slo: SLOWithSummaryResponse | undefined) => void; + hasError?: boolean; +} + +const SLO_REQUIRED = i18n.translate('xpack.observability.sloEmbeddable.config.errors.sloRequired', { + defaultMessage: 'SLO is required.', +}); + +export function SloSelector({ initialSlo, onSelected, hasError }: Props) { + const [options, setOptions] = useState>>([]); + const [selectedOptions, setSelectedOptions] = useState>>(); + const [searchValue, setSearchValue] = useState(''); + const { isInitialLoading, isLoading, sloList } = useFetchSloList({ + kqlQuery: `slo.name: ${searchValue.replaceAll(' ', '*')}*`, + }); + + useEffect(() => { + const isLoadedWithData = !isLoading && sloList!.results !== undefined; + const opts: Array> = isLoadedWithData + ? sloList!.results!.map((slo) => { + const label = + slo.instanceId !== ALL_VALUE + ? `${slo.name} (${slo.groupBy}: ${slo.instanceId})` + : slo.name; + return { + value: `${slo.id}-${slo.instanceId}`, + label, + instanceId: slo.instanceId, + }; + }) + : []; + setOptions(opts); + }, [isLoading, sloList]); + + const onChange = (opts: Array>) => { + setSelectedOptions(opts); + const selectedSlo = + opts.length === 1 + ? sloList!.results?.find((slo) => opts[0].value === `${slo.id}-${slo.instanceId}`) + : undefined; + + onSelected(selectedSlo); + }; + + const onSearchChange = useMemo( + () => + debounce((value: string) => { + setSearchValue(value); + }, 300), + [] + ); + + if (isInitialLoading) { + return null; + } + + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/types.ts b/x-pack/plugins/observability/public/embeddable/slo/overview/types.ts new file mode 100644 index 0000000000000..ea125ffa8a9d5 --- /dev/null +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/types.ts @@ -0,0 +1,15 @@ +/* + * 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 { EmbeddableInput } from '@kbn/embeddable-plugin/public'; + +export interface EmbeddableSloProps { + sloId: string | undefined; + sloInstanceId: string | undefined; + lastReloadRequestTime?: number | undefined; +} + +export type SloEmbeddableInput = EmbeddableInput & EmbeddableSloProps; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index c40662219ddb7..d864a09fe6fdc 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -56,6 +56,7 @@ import { ObservabilityAIAssistantPluginSetup, ObservabilityAIAssistantPluginStart, } from '@kbn/observability-ai-assistant-plugin/public'; +import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; import { AiopsPluginStart } from '@kbn/aiops-plugin/public/types'; import { RulesLocatorDefinition } from './locators/rules'; import { RuleDetailsLocatorDefinition } from './locators/rule_details'; @@ -111,6 +112,7 @@ export interface ObservabilityPublicPluginsSetup { triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; home?: HomePublicPluginSetup; usageCollection: UsageCollectionSetup; + embeddable: EmbeddableSetup; } export interface ObservabilityPublicPluginsStart { @@ -286,6 +288,14 @@ export class Plugin coreSetup.application.register(app); registerObservabilityRuleTypes(config, this.observabilityRuleTypeRegistry); + const registerSloEmbeddableFactory = async () => { + const { SloOverviewEmbeddableFactoryDefinition } = await import( + './embeddable/slo/overview/slo_embeddable_factory' + ); + const factory = new SloOverviewEmbeddableFactoryDefinition(coreSetup.getStartServices); + pluginsSetup.embeddable.registerEmbeddableFactory(factory.type, factory); + }; + registerSloEmbeddableFactory(); if (pluginsSetup.home) { pluginsSetup.home.featureCatalogue.registerSolution({ diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 2b7a9e0749650..cfb16fea9a839 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -84,6 +84,8 @@ "@kbn/core-capabilities-common", "@kbn/observability-ai-assistant-plugin", "@kbn/osquery-plugin", + "@kbn/content-management-plugin", + "@kbn/embeddable-plugin", "@kbn/aiops-plugin", "@kbn/content-management-plugin", "@kbn/deeplinks-observability", From 460a84e4f9a2aba70921e8a07a3df8b276445f14 Mon Sep 17 00:00:00 2001 From: Gabriel Landau <42078554+gabriellandau@users.noreply.github.com> Date: Thu, 28 Sep 2023 14:53:44 -0400 Subject: [PATCH 04/10] [8.11.0] Promote Defend API events to Production (#167549) ## Summary This PR supercedes https://github.com/elastic/kibana/pull/167107 Elastic Defend for Windows now collects ETW Threat Intelligence (ETW-TI) events. Defend calls these API events. API events currently include the existing Credential Access and ETW-TI. We will add more events under the API umbrella in the future. The Windows Events Policy `Credential Access` category has been renamed to `API` in the UI and documentation - but it remains as `credential_access` in the yaml for backwards compatibility. This new category definition is a superset of the previous category. Two new advanced options are added - * `windows.advanced.events.api_disabled` - comma separated list * `windows.advanced.events.api_verbose` - boolean ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../policy/models/advanced_policy_schema.ts | 23 ++++++++++++++++++- .../windows_event_collection_card.test.tsx | 4 ++-- .../cards/windows_event_collection_card.tsx | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index f9818447c2e0d..056577f7944ef 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -1271,7 +1271,28 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.events.api', { defaultMessage: - 'Controls whether API events are enabled. Set to false to disable API event collection. Default: true', + 'Controls whether ETW API events are enabled. Set to false to disable ETW event collection. Default: true', + } + ), + }, + { + key: 'windows.advanced.events.api_disabled', + first_supported_version: '8.11', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.events.api_disabled', + { + defaultMessage: 'A comma separated list of API names to selectively disable.', + } + ), + }, + { + key: 'windows.advanced.events.api_verbose', + first_supported_version: '8.11', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.events.api_verbose', + { + defaultMessage: + 'Controls whether high volume API events are forwarded. Event filtering is recommended if enabled. Default: false', } ), }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx index 16c84436684f1..64c0473a55393 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx @@ -70,7 +70,7 @@ describe('Policy Windows Event Collection Card', () => { 'Operating system' + 'Windows 8 / 8 event collections enabled' + 'Events' + - 'Credential Access' + + 'API' + 'DLL and Driver Load' + 'DNS' + 'File' + @@ -98,7 +98,7 @@ describe('Policy Windows Event Collection Card', () => { 'Windows ' + '6 / 8 event collections enabled' + 'Events' + - 'Credential Access' + + 'API' + 'DLL and Driver Load' + 'Network' + 'Process' + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx index 3fadf3665d9fd..fd20184113468 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx @@ -17,7 +17,7 @@ const OPTIONS: ReadonlyArray> = [ name: i18n.translate( 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.credentialAccess', { - defaultMessage: 'Credential Access', + defaultMessage: 'API', } ), protectionField: 'credential_access', From 05a7d30072c0f66df1d00b56efbcd96659b1065d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:40:51 -0400 Subject: [PATCH 05/10] skip failing test suite (#167561) --- x-pack/test/functional/apps/lens/group6/lens_tagging.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/group6/lens_tagging.ts b/x-pack/test/functional/apps/lens/group6/lens_tagging.ts index c3b279f591cdb..b8d9c332f64f5 100644 --- a/x-pack/test/functional/apps/lens/group6/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/group6/lens_tagging.ts @@ -29,7 +29,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const lensTag = 'extreme-lens-tag'; const lensTitle = 'lens tag test'; - describe('lens tagging', () => { + // Failing: See https://github.com/elastic/kibana/issues/167561 + describe.skip('lens tagging', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); From 424dec613f72fe52ccee300d34e6dc1db54af873 Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Thu, 28 Sep 2023 13:54:56 -0600 Subject: [PATCH 06/10] [Accessibility] Loading indicator aria labels (#166391) ## Summary Closes https://github.com/elastic/kibana/issues/153597 In full screen, the loading indicator fails a11y tests. By removing the aria-label and aria-hidden attributes, this allows the validation to pass. https://github.com/elastic/kibana/issues/153597#issuecomment-1482805134 This PR also introduces optional props for max and value props on the `EuiProgress` component to provide consumers of the component greater ability to define how the component is being used. [Information](https://eui.elastic.co/#/display/progress#progress-with-values) on the max and value props can be found in the [EUI docs](https://eui.elastic.co/#/display/progress#progress-with-values). --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/ui/loading_indicator.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/loading_indicator.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/loading_indicator.tsx index 8e3cae33ceb84..22e9b5dd9276d 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/loading_indicator.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/loading_indicator.tsx @@ -19,6 +19,8 @@ export interface LoadingIndicatorProps { loadingCount$: ReturnType; showAsBar?: boolean; customLogo?: string; + maxAmount?: number; + valueAmount?: string | number; } export class LoadingIndicator extends React.Component { @@ -62,8 +64,6 @@ export class LoadingIndicator extends React.Component Date: Thu, 28 Sep 2023 16:26:24 -0400 Subject: [PATCH 07/10] [Asset Manager] Creates baseline public asset client for use in public plugins (#167191) Closes #167075 ## Summary Adds a public asset client available in the `setup` lifecycle hook for plugins that depend on this one. `getHosts` is the only method available on this client for now. TODO, before merge: - [x] Add docs for the server client - [x] Add docs for the public client - [x] Remove REST docs from plugin docs, not needed - [x] Add unit tests for public client ### Testing this PR One way of testing this new client is to apply the attached test-assets.patch file locally, adjust the date range in the getHosts query that is added in the infra plugin, and then start Kibana and navigate to the infra app. You should see print out in the browser console. [test-assets.patch](https://github.com/elastic/kibana/files/12718693/test-assets.patch) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 3 +- packages/kbn-optimizer/limits.yml | 1 + .../test_suites/core_plugins/rendering.ts | 1 + x-pack/plugins/asset_manager/README.md | 36 +--- x-pack/plugins/asset_manager/common/config.ts | 56 ++++++ .../asset_manager/common/constants_routes.ts | 18 ++ .../plugins/asset_manager/common/types_api.ts | 181 +++++++++++------ .../asset_manager/common/types_client.ts | 17 ++ x-pack/plugins/asset_manager/docs/api.md | 187 ++++++++++++++++++ .../plugins/asset_manager/docs/development.md | 34 ++++ .../docs/{index.md => rest_deprecated.md} | 70 +++---- x-pack/plugins/asset_manager/kibana.jsonc | 2 +- x-pack/plugins/asset_manager/public/index.ts | 20 ++ .../public/lib/public_assets_client.test.ts | 48 +++++ .../public/lib/public_assets_client.ts | 26 +++ x-pack/plugins/asset_manager/public/plugin.ts | 51 +++++ x-pack/plugins/asset_manager/public/types.ts | 25 +++ .../plugins/asset_manager/server/constants.ts | 1 - x-pack/plugins/asset_manager/server/index.ts | 16 +- .../accessors/hosts/get_hosts_by_assets.ts | 2 +- .../accessors/hosts/get_hosts_by_signals.ts | 4 +- .../server/lib/accessors/hosts/index.ts | 9 +- .../server/lib/accessors/index.ts | 8 +- .../services/get_services_by_assets.ts | 4 +- .../services/get_services_by_signals.ts | 4 +- .../server/lib/accessors/services/index.ts | 10 +- .../{asset_accessor.ts => asset_client.ts} | 18 +- .../server/lib/get_all_related_assets.ts | 16 +- .../asset_manager/server/lib/get_assets.ts | 4 +- .../lib/get_indirectly_related_assets.ts | 4 +- .../asset_manager/server/lib/write_assets.ts | 4 +- x-pack/plugins/asset_manager/server/plugin.ts | 15 +- .../server/routes/assets/hosts.ts | 41 ++-- .../server/routes/assets/index.ts | 24 +-- .../server/routes/assets/services.ts | 16 +- .../asset_manager/server/routes/index.ts | 12 +- .../asset_manager/server/routes/ping.ts | 2 +- .../server/routes/sample_assets.ts | 14 +- .../asset_manager/server/routes/types.ts | 4 +- .../asset_manager/server/routes/utils.ts | 10 +- x-pack/plugins/asset_manager/server/types.ts | 38 +--- x-pack/plugins/asset_manager/tsconfig.json | 4 +- 42 files changed, 758 insertions(+), 302 deletions(-) create mode 100644 x-pack/plugins/asset_manager/common/config.ts create mode 100644 x-pack/plugins/asset_manager/common/constants_routes.ts create mode 100644 x-pack/plugins/asset_manager/common/types_client.ts create mode 100644 x-pack/plugins/asset_manager/docs/api.md create mode 100644 x-pack/plugins/asset_manager/docs/development.md rename x-pack/plugins/asset_manager/docs/{index.md => rest_deprecated.md} (93%) create mode 100644 x-pack/plugins/asset_manager/public/index.ts create mode 100644 x-pack/plugins/asset_manager/public/lib/public_assets_client.test.ts create mode 100644 x-pack/plugins/asset_manager/public/lib/public_assets_client.ts create mode 100644 x-pack/plugins/asset_manager/public/plugin.ts create mode 100644 x-pack/plugins/asset_manager/public/types.ts rename x-pack/plugins/asset_manager/server/lib/{asset_accessor.ts => asset_client.ts} (81%) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 71190c26c4894..ccea77d906970 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -451,8 +451,7 @@ The plugin exposes the static DefaultEditorController class to consume. |{kib-repo}blob/{branch}/x-pack/plugins/asset_manager/README.md[assetManager] -|This plugin provides access to the asset data stored in assets-* indices, primarily -for inventory and topology purposes. +|This plugin provides access to observed asset data, such as information about hosts, pods, containers, services, and more. |{kib-repo}blob/{branch}/x-pack/plugins/banners/README.md[banners] diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index ec34d257eadc2..9b12708a506fc 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -4,6 +4,7 @@ pageLoadAssetSize: aiops: 10000 alerting: 106936 apm: 64385 + assetManager: 25000 banners: 17946 bfetch: 22837 canvas: 1066647 diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 94af20ff4f86b..ca639ed3272fd 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -197,6 +197,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.apm.featureFlags.sourcemapApiAvailable (any)', 'xpack.apm.featureFlags.storageExplorerAvailable (any)', 'xpack.apm.serverless.enabled (any)', // It's a boolean (any because schema.conditional) + 'xpack.assetManager.alphaEnabled (boolean)', 'xpack.observability_onboarding.serverless.enabled (any)', // It's a boolean (any because schema.conditional) 'xpack.cases.files.allowedMimeTypes (array)', 'xpack.cases.files.maxSize (number)', diff --git a/x-pack/plugins/asset_manager/README.md b/x-pack/plugins/asset_manager/README.md index f82f174af471c..d73bfbb53b087 100644 --- a/x-pack/plugins/asset_manager/README.md +++ b/x-pack/plugins/asset_manager/README.md @@ -1,39 +1,13 @@ # Asset Manager Plugin -This plugin provides access to the asset data stored in assets-\* indices, primarily -for inventory and topology purposes. +This plugin provides access to observed asset data, such as information about hosts, pods, containers, services, and more. ## Documentation -See [docs for the provided APIs in the docs folder](./docs/index.md). +### User Docs -## Running Tests +For those interested in making use of the APIs provided by this plugin, see [our API docs](./docs/api.md). -There are integration tests for the endpoints implemented thus far as well as for -the sample data tests. There is also a small set of tests meant to ensure that the -plugin is not doing anything without the proper config value in place to enable -the plugin fully. For more on enabling the plugin, see [the docs page](./docs/index.md). +### Developer Docs -The "not enabled" tests are run by default in CI. To run them manually, do the following: - -```shell -$ node scripts/functional_tests_server --config x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts -$ node scripts/functional_test_runner --config=x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts -``` - -The "enabled" tests are NOT run by CI yet, to prevent blocking Kibana development for a -test failure in this alpha, tech preview plugin. They will be moved into the right place -to make them run for CI before the plugin is enabled by default. To run them manually: - -```shell -$ node scripts/functional_tests_server --config x-pack/test/api_integration/apis/asset_manager/config.ts -$ node scripts/functional_test_runner --config=x-pack/test/api_integration/apis/asset_manager/config.ts -``` - -## Using Sample Data - -This plugin comes with a full "working set" of sample asset documents, meant -to provide enough data in the correct schema format so that all of the API -endpoints return expected values. - -To create the sample data, follow [the instructions in the REST API docs](./docs/index.md#sample-data). +For those working on this plugin directly and developing it, please see [our development docs](./docs/development.md). diff --git a/x-pack/plugins/asset_manager/common/config.ts b/x-pack/plugins/asset_manager/common/config.ts new file mode 100644 index 0000000000000..0a57e37d497bb --- /dev/null +++ b/x-pack/plugins/asset_manager/common/config.ts @@ -0,0 +1,56 @@ +/* + * 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, TypeOf } from '@kbn/config-schema'; + +export const INDEX_DEFAULTS = { + logs: 'filebeat-*,logs-*', +}; + +export const configSchema = schema.object({ + alphaEnabled: schema.maybe(schema.boolean()), + // Designate where various types of data live. + // NOTE: this should be handled in a centralized way for observability, so + // that when a user configures these differently from the known defaults, + // that value is propagated everywhere. For now, we duplicate the value here. + sourceIndices: schema.object( + { + logs: schema.string({ defaultValue: INDEX_DEFAULTS.logs }), + }, + { defaultValue: INDEX_DEFAULTS } + ), + // Choose an explicit source for asset queries. + // NOTE: This will eventually need to be able to cleverly switch + // between these values based on the availability of data in the + // indices, and possibly for each asset kind/type value. + // For now, we set this explicitly. + lockedSource: schema.oneOf([schema.literal('assets'), schema.literal('signals')], { + defaultValue: 'signals', + }), +}); + +export type AssetManagerConfig = TypeOf; + +/** + * The following map is passed to the server plugin setup under the + * exposeToBrowser: option, and controls which of the above config + * keys are allow-listed to be available in the browser config. + * + * NOTE: anything exposed here will be visible in the UI dev tools, + * and therefore MUST NOT be anything that is sensitive information! + */ +export const exposeToBrowserConfig = { + alphaEnabled: true, +} as const; + +type ValidKeys = keyof { + [K in keyof typeof exposeToBrowserConfig as typeof exposeToBrowserConfig[K] extends true + ? K + : never]: true; +}; + +export type AssetManagerPublicConfig = Pick; diff --git a/x-pack/plugins/asset_manager/common/constants_routes.ts b/x-pack/plugins/asset_manager/common/constants_routes.ts new file mode 100644 index 0000000000000..1aef43f7383bd --- /dev/null +++ b/x-pack/plugins/asset_manager/common/constants_routes.ts @@ -0,0 +1,18 @@ +/* + * 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 ASSET_MANAGER_API_BASE = '/api/asset-manager'; + +function base(path: string) { + return `${ASSET_MANAGER_API_BASE}${path}`; +} + +export const GET_ASSETS = base('/assets'); +export const GET_RELATED_ASSETS = base('/assets/related'); +export const GET_ASSETS_DIFF = base('/assets/diff'); + +export const GET_HOSTS = base('/assets/hosts'); diff --git a/x-pack/plugins/asset_manager/common/types_api.ts b/x-pack/plugins/asset_manager/common/types_api.ts index 8e9e9181a29e4..11b5ea4bda3a4 100644 --- a/x-pack/plugins/asset_manager/common/types_api.ts +++ b/x-pack/plugins/asset_manager/common/types_api.ts @@ -6,77 +6,111 @@ */ import * as rt from 'io-ts'; - -export const assetTypeRT = rt.union([ - rt.literal('k8s.pod'), - rt.literal('k8s.cluster'), - rt.literal('k8s.node'), -]); +import { + dateRt, + inRangeFromStringRt, + datemathStringRt, + createLiteralValueFromUndefinedRT, +} from '@kbn/io-ts-utils'; + +export const assetTypeRT = rt.keyof({ + 'k8s.pod': null, + 'k8s.cluster': null, + 'k8s.node': null, +}); export type AssetType = rt.TypeOf; -export const assetKindRT = rt.union([ - rt.literal('cluster'), - rt.literal('host'), - rt.literal('pod'), - rt.literal('container'), - rt.literal('service'), - rt.literal('alert'), -]); +export const assetKindRT = rt.keyof({ + cluster: null, + host: null, + pod: null, + container: null, + service: null, + alert: null, +}); export type AssetKind = rt.TypeOf; -export type AssetStatus = - | 'CREATING' - | 'ACTIVE' - | 'DELETING' - | 'FAILED' - | 'UPDATING' - | 'PENDING' - | 'UNKNOWN'; -export type CloudProviderName = 'aws' | 'gcp' | 'azure' | 'other' | 'unknown' | 'none'; - -interface WithTimestamp { - '@timestamp': string; -} -export interface ECSDocument extends WithTimestamp { - 'kubernetes.namespace'?: string; - 'kubernetes.pod.name'?: string; - 'kubernetes.pod.uid'?: string; - 'kubernetes.pod.start_time'?: Date; - 'kubernetes.node.name'?: string; - 'kubernetes.node.start_time'?: Date; - - 'orchestrator.api_version'?: string; - 'orchestrator.namespace'?: string; - 'orchestrator.organization'?: string; - 'orchestrator.type'?: string; - 'orchestrator.cluster.id'?: string; - 'orchestrator.cluster.name'?: string; - 'orchestrator.cluster.url'?: string; - 'orchestrator.cluster.version'?: string; - - 'cloud.provider'?: CloudProviderName; - 'cloud.instance.id'?: string; - 'cloud.region'?: string; - 'cloud.service.name'?: string; - - 'service.environment'?: string; -} +export const assetStatusRT = rt.keyof({ + CREATING: null, + ACTIVE: null, + DELETING: null, + FAILED: null, + UPDATING: null, + PENDING: null, + UNKNOWN: null, +}); + +export type AssetStatus = rt.TypeOf; + +// https://github.com/gcanti/io-ts/blob/master/index.md#union-of-string-literals +export const cloudProviderNameRT = rt.keyof({ + aws: null, + gcp: null, + azure: null, + other: null, + unknown: null, + none: null, +}); + +export type CloudProviderName = rt.TypeOf; + +const withTimestampRT = rt.type({ + '@timestamp': rt.string, +}); + +export type WithTimestamp = rt.TypeOf; + +export const ECSDocumentRT = rt.intersection([ + withTimestampRT, + rt.partial({ + 'kubernetes.namespace': rt.string, + 'kubernetes.pod.name': rt.string, + 'kubernetes.pod.uid': rt.string, + 'kubernetes.pod.start_time': rt.string, + 'kubernetes.node.name': rt.string, + 'kubernetes.node.start_time': rt.string, + 'orchestrator.api_version': rt.string, + 'orchestrator.namespace': rt.string, + 'orchestrator.organization': rt.string, + 'orchestrator.type': rt.string, + 'orchestrator.cluster.id': rt.string, + 'orchestrator.cluster.name': rt.string, + 'orchestrator.cluster.url': rt.string, + 'orchestrator.cluster.version': rt.string, + 'cloud.provider': cloudProviderNameRT, + 'cloud.instance.id': rt.string, + 'cloud.region': rt.string, + 'cloud.service.name': rt.string, + 'service.environment': rt.string, + }), +]); -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.references'?: string | string[]; - 'asset.namespace'?: string; -} +export type ECSDocument = rt.TypeOf; + +export const assetRT = rt.intersection([ + ECSDocumentRT, + rt.type({ + 'asset.ean': rt.string, + 'asset.id': rt.string, + 'asset.kind': assetKindRT, + }), + // mixed required and optional require separate hashes combined via intersection + // https://github.com/gcanti/io-ts/blob/master/index.md#mixing-required-and-optional-props + rt.partial({ + 'asset.collection_version': rt.string, + 'asset.name': rt.string, + 'asset.type': assetTypeRT, + 'asset.status': assetStatusRT, + 'asset.parents': rt.union([rt.string, rt.array(rt.string)]), + 'asset.children': rt.union([rt.string, rt.array(rt.string)]), + 'asset.references': rt.union([rt.string, rt.array(rt.string)]), + 'asset.namespace': rt.string, + }), +]); + +export type Asset = rt.TypeOf; export type AssetWithoutTimestamp = Omit; @@ -156,3 +190,22 @@ export type RelationField = keyof Pick< Asset, 'asset.children' | 'asset.parents' | 'asset.references' >; + +export const sizeRT = rt.union([ + inRangeFromStringRt(1, 100), + createLiteralValueFromUndefinedRT(10), +]); +export const assetDateRT = rt.union([dateRt, datemathStringRt]); +export const getHostAssetsQueryOptionsRT = rt.exact( + rt.partial({ + from: assetDateRT, + to: assetDateRT, + size: sizeRT, + }) +); +export type GetHostAssetsQueryOptions = rt.TypeOf; + +export const getHostAssetsResponseRT = rt.type({ + hosts: rt.array(assetRT), +}); +export type GetHostAssetsResponse = rt.TypeOf; diff --git a/x-pack/plugins/asset_manager/common/types_client.ts b/x-pack/plugins/asset_manager/common/types_client.ts new file mode 100644 index 0000000000000..350a168da8965 --- /dev/null +++ b/x-pack/plugins/asset_manager/common/types_client.ts @@ -0,0 +1,17 @@ +/* + * 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 interface GetHostsOptionsPublic { + from: string; + to: string; +} + +export interface GetServicesOptionsPublic { + from: string; + to: string; + parent?: string; +} diff --git a/x-pack/plugins/asset_manager/docs/api.md b/x-pack/plugins/asset_manager/docs/api.md new file mode 100644 index 0000000000000..755abfe4be373 --- /dev/null +++ b/x-pack/plugins/asset_manager/docs/api.md @@ -0,0 +1,187 @@ +# Asset Manager API Documentation + +## Plugin configuration + +This plugin is NOT fully enabled by default, even though it's always enabled +by Kibana's definition of "enabled". However, without the following configuration, +it will bail before it sets up any routes or returns anything from its +start, setup, or stop hooks. + +To fully enable the plugin, set the following config values in your kibana.yml file: + +```yaml +xpack.assetManager: + alphaEnabled: true +``` + +## Depending on an asset client in your packages + +If you're creating a shared UI component or tool that needs to access asset data, you +can create that code in a stateless Kibana package that can itself be imported into +any Kibana plugin without any dependency restrictions. To gain access to the asset data, +this component or tool can require the appropriate asset client to be passed in. + +TODO: need to move main client types to a package so that they can be depended on by +other packages that require an injected asset client. Then we can list that package name +here and explain how to use those types in a package. + +## Client APIs + +This plugin provides asset clients for Kibana server and public usage. The differences between these +two clients are described below in their sections, while the methods for both APIs are described +in the Client Methods section. + +These clients are set up in the following way. For a given "methodA": + +``` +publicMethodA(...options: MethodAPublicOptions) + -> browser client calls corresponding REST API method with MethodAPublicOptions + -> REST API handler calls corresponding serverMethodA + -> serverMethodA requires MethodAPublicOptions & AssetClientDependencies, and it also + injects some internal dependencies from the plugin's config on your behalf +``` + +The public and server clientss are both accessible to plugin dependants, but the REST API is NOT. + +### Required dependency setup + +To use either client, you must first add "assetManager" to your `"requiredDependencies"` array +in your plugin's kibana.jsonc file. + +TECH PREVIEW NOTE: While this plugin is in "tech preview", in both the server and public clients, +the provided plugin dependencies can be undefined for this plugin if the proper configuration +has not been set (see above). For that reason, the types will force you to guard against this +undefined scenario. Once the tech preview gating is removed, this will no longer be the case. + +### Server client usage + +In your plugin's `setup` method, you can gain access to the client from the injected `plugins` map. +Make sure you import the `AssetManagerServerPluginSetup` type from the plugin's server +directory and add it to your own SetupPlugins type, as seen below. + +```ts +import { AssetManagerServerPluginSetup } from '@kbn/assetManager-plugin/server'; + +interface MyPluginSetupDeps { + assetManager: AssetManagerServerPluginSetup; +} + +class MyPlugin { + setup(core: CoreSetup, plugins: MyPluginSetupDeps) { + // assetClient is found on plugins.assetManager.assetClient + setupRoutes(router, plugins); + } +} +``` + +To use the server client in your server routes, you can use something like this: + +```ts +export function setupRoutes(router: IRouter, plugins: MyPluginDeps) { + router.get( + { + path: '/my/path', + validate: {}, + }, + async (context, req, res) => { + // handle route + // optionally, use asset client + // NOTE: see below for important info on required server client args + const hosts = await plugins.assetManager.assetClient.getHosts(); + } + ); +} +``` + +#### Required parameters for server client methods + +All methods called via the server client require some core Kibana clients to be passed in, +so that they are pulled from the request context and properly scoped. If the asset manager +plugin provided these clients internally, they would not be scoped to the user that made +the API request, so they are required arguments for every server client method. + +_Note: These required arguments are referred to as `AssetClientDependencies`, which can be +seen in the [the server types file](../server/types.ts)._ + +For example: + +```ts +router.get( + { + path: '/my/path', + validate: {}, + }, + async (context, req, res) => { + // to use server asset client, you must get the following clients + // from the request context and pass them to the client method + // alongside whatever "public" arguments that method defines + const coreContext = await context.core; + const hostsOptions: PublicGetHostsOptions = {}; // these will be different for each method + + const hosts = await plugins.assetManager.assetClient.getHosts({ + ...hostsOptions, + elasticsearchClient: coreContext.elasticsearch.client.asCurrentUser, + savedObjectsClient: coreContext.savedObjects.client, + }); + } +); +``` + +### Public client usage + +You should grab the public client in the same way as the server one, via the plugin dependencies +in your `setup` lifecycle. + +```ts +import { AssetManagerPublicPluginStart } from '@kbn/assetManager-plugin/public'; + +interface MyPluginStartDeps { + assetManager: AssetManagerPublicPluginStart; +} + +class MyPlugin { + setup(core: CoreSetup) { + core.application.register({ + id: 'my-other-plugin', + title: '', + appRoute: '/app/my-other-plugin', + mount: async (params: AppMountParameters) => { + // mount callback should not use setup dependencies, get start dependencies instead + // so the pluginStart map passed to your renderApp method will be the start deps, + // not the setup deps -- the same asset client is provided to both setup and start in public + const [coreStart, , pluginStart] = await core.getStartServices(); + // assetClient is found on pluginStart.assetManager.assetClient + return renderApp(coreStart, pluginStart, params); + }, + }); + } +} +``` + +All methods in the public client only require their public options (seen below), and don't require +the "AssetClientDependencies" that are required for the server client versions of the same methods. +This is because the public client will use the asset manager's internal REST API under the hood, where +it will be able to pull the properly-scoped client dependencies off of that request context for you. + +### Client methods + +#### getHosts + +Get a group of host assets found within a specified time range. + +| Parameter | Type | Required? | Description | +| :-------- | :-------------- | :-------- | :--------------------------------------------------------------------- | +| from | datetime string | yes | ISO date string representing the START of the time range being queried | +| to | datetime string | yes | ISO date string representing the END of the time range being queried | + +**Response** + +```json +{ + "hosts": [ + ...found host assets + ] +} +``` + +TODO: Link to a centralized asset document example that each response can reference? diff --git a/x-pack/plugins/asset_manager/docs/development.md b/x-pack/plugins/asset_manager/docs/development.md new file mode 100644 index 0000000000000..a98e8e46a8ce4 --- /dev/null +++ b/x-pack/plugins/asset_manager/docs/development.md @@ -0,0 +1,34 @@ +# Asset Manager Plugin Development + +These docs contain information you might need if you are developing this plugin in Kibana. If you are interested in the APIs this plugin exposes, please see [./api.md](our API docs) instead. + +## Running Tests + +There are integration tests for the endpoints implemented thus far as well as for +the sample data tests. There is also a small set of tests meant to ensure that the +plugin is not doing anything without the proper config value in place to enable +the plugin fully. For more on enabling the plugin, see [the docs page](./docs/index.md). + +The "not enabled" tests are run by default in CI. To run them manually, do the following: + +```shell +$ node scripts/functional_tests_server --config x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts +$ node scripts/functional_test_runner --config=x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts +``` + +The "enabled" tests are NOT run by CI yet, to prevent blocking Kibana development for a +test failure in this alpha, tech preview plugin. They will be moved into the right place +to make them run for CI before the plugin is enabled by default. To run them manually: + +```shell +$ node scripts/functional_tests_server --config x-pack/test/api_integration/apis/asset_manager/config.ts +$ node scripts/functional_test_runner --config=x-pack/test/api_integration/apis/asset_manager/config.ts +``` + +## Using Sample Data + +This plugin comes with a full "working set" of sample asset documents, meant +to provide enough data in the correct schema format so that all of the API +endpoints return expected values. + +To create the sample data, follow [the instructions in the REST API docs](./docs/index.md#sample-data). diff --git a/x-pack/plugins/asset_manager/docs/index.md b/x-pack/plugins/asset_manager/docs/rest_deprecated.md similarity index 93% rename from x-pack/plugins/asset_manager/docs/index.md rename to x-pack/plugins/asset_manager/docs/rest_deprecated.md index 790beb87b4f7e..43a5f74a0d058 100644 --- a/x-pack/plugins/asset_manager/docs/index.md +++ b/x-pack/plugins/asset_manager/docs/rest_deprecated.md @@ -1,24 +1,6 @@ -# Asset Manager Documentation +## Deprecated REST API docs -_Note:_ To read about development guidance around testing, sample data, etc., see the -[plugin's main README file](../README.md) - -## Alpha Configuration - -This plugin is NOT fully enabled by default, even though it's always enabled -by Kibana's definition of "enabled". However, without the following configuration, -it will bail before it sets up any routes or returns anything from its -start, setup, or stop hooks. - -To fully enable the plugin, set the following config value in your kibana.yml file: - -```yaml -xpack.assetManager.alphaEnabled: true -``` - -## APIs - -This plugin provides the following APIs. +These docs are not being currently maintained because they pertain to an internal REST API. Please see [our docs for our API clients](./api.md) instead. ### Shared Types @@ -58,16 +40,16 @@ Returns a list of assets present within a given time range. Can be limited by as ##### Request -| Option | Type | Required? | Default | Description | -| :------ | :------------ | :-------- | :------ | :--------------------------------------------------------------------------------- | -| from | RangeDate | No | "now-24h" | Starting point for date range to search for assets within | -| to | RangeDate | No | "now" | End point for date range to search for assets | -| type | AssetType[] | No | all | Specify one or more types to restrict the query | -| ean | AssetEan[] | No | all | Specify one or more EANs (specific assets) to restrict the query | -| size | number | No | all | Limit the amount of assets returned | - +| Option | Type | Required? | Default | Description | +| :----- | :---------- | :-------- | :-------- | :--------------------------------------------------------------- | +| from | RangeDate | No | "now-24h" | Starting point for date range to search for assets within | +| to | RangeDate | No | "now" | End point for date range to search for assets | +| type | AssetType[] | No | all | Specify one or more types to restrict the query | +| ean | AssetEan[] | No | all | Specify one or more EANs (specific assets) to restrict the query | +| size | number | No | all | Limit the amount of assets returned | _Notes:_ + - User cannot specify both type and ean at the same time. - For array types such as `type` and `ean`, user should specify the query parameter multiple times, e.g. `type=k8s.pod&type=k8s.node` @@ -410,15 +392,15 @@ GET kbn:/api/asset-manager/assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18 Returns assets found in the two time ranges, split by what occurs in only either or in both. -#### Request +#### Request -| Option | Type | Required? | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| aFrom | RangeDate | Yes | N/A | Starting point for baseline date range to search for assets within | -| aTo | RangeDate | Yes | N/A | End point for baseline date range to search for assets within | -| bFrom | RangeDate | Yes | N/A | Starting point for comparison date range | -| bTo | RangeDate | Yes | N/A | End point for comparison date range | -| type | AssetType[] | No | all | Restrict results to one or more asset.type value | +| Option | Type | Required? | Default | Description | +| :----- | :---------- | :-------- | :------ | :----------------------------------------------------------------- | +| aFrom | RangeDate | Yes | N/A | Starting point for baseline date range to search for assets within | +| aTo | RangeDate | Yes | N/A | End point for baseline date range to search for assets within | +| bFrom | RangeDate | Yes | N/A | Starting point for comparison date range | +| bTo | RangeDate | Yes | N/A | End point for comparison date range | +| type | AssetType[] | No | all | Restrict results to one or more asset.type value | #### Responses @@ -1044,14 +1026,14 @@ Returns assets related to the provided ean. The relation can be one of ancestors #### Request -| Option | Type | Required? | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| relation | string | Yes | N/A | The type of related assets we're looking for. One of (ancestors|descendants|references) | -| from | RangeDate | Yes | N/A | Starting point for date range to search for assets within | -| to | RangeDate | No | "now" | End point for date range to search for assets | -| ean | AssetEan | Yes | N/A | Single Elastic Asset Name representing the asset for which the related assets are being requested | -| type | AssetType[] | No | all | Restrict results to one or more asset.type value | -| maxDistance | number (1-5) | No | 1 | Maximum number of "hops" to search away from specified asset | +| Option | Type | Required? | Default | Description | +| :---------- | :----------- | :-------- | :------ | :------------------------------------------------------------------------------------------------ | ----------- | ----------- | +| relation | string | Yes | N/A | The type of related assets we're looking for. One of (ancestors | descendants | references) | +| from | RangeDate | Yes | N/A | Starting point for date range to search for assets within | +| to | RangeDate | No | "now" | End point for date range to search for assets | +| ean | AssetEan | Yes | N/A | Single Elastic Asset Name representing the asset for which the related assets are being requested | +| type | AssetType[] | No | all | Restrict results to one or more asset.type value | +| maxDistance | number (1-5) | No | 1 | Maximum number of "hops" to search away from specified asset | #### Responses diff --git a/x-pack/plugins/asset_manager/kibana.jsonc b/x-pack/plugins/asset_manager/kibana.jsonc index 49b1b59838d9c..b3fcd1b3a4fa1 100644 --- a/x-pack/plugins/asset_manager/kibana.jsonc +++ b/x-pack/plugins/asset_manager/kibana.jsonc @@ -15,7 +15,7 @@ "apmDataAccess", "metricsDataAccess" ], - "browser": false, + "browser": true, "server": true, "requiredBundles": [ ] diff --git a/x-pack/plugins/asset_manager/public/index.ts b/x-pack/plugins/asset_manager/public/index.ts new file mode 100644 index 0000000000000..7837c00909430 --- /dev/null +++ b/x-pack/plugins/asset_manager/public/index.ts @@ -0,0 +1,20 @@ +/* + * 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 } from './plugin'; +import { AssetManagerPublicPluginSetup, AssetManagerPublicPluginStart } from './types'; + +export const plugin: PluginInitializer< + AssetManagerPublicPluginSetup | undefined, + AssetManagerPublicPluginStart | undefined +> = (context: PluginInitializerContext) => { + return new Plugin(context); +}; + +export type { AssetManagerPublicPluginSetup, AssetManagerPublicPluginStart }; +export type AssetManagerAppId = 'assetManager'; diff --git a/x-pack/plugins/asset_manager/public/lib/public_assets_client.test.ts b/x-pack/plugins/asset_manager/public/lib/public_assets_client.test.ts new file mode 100644 index 0000000000000..93cc541a34af4 --- /dev/null +++ b/x-pack/plugins/asset_manager/public/lib/public_assets_client.test.ts @@ -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 { HttpSetupMock } from '@kbn/core-http-browser-mocks'; +import { coreMock } from '@kbn/core/public/mocks'; +import { PublicAssetsClient } from './public_assets_client'; +import * as routePaths from '../../common/constants_routes'; + +describe('Public assets client', () => { + let http: HttpSetupMock = coreMock.createSetup().http; + + beforeEach(() => { + http = coreMock.createSetup().http; + }); + + describe('class instantiation', () => { + it('should successfully instantiate', () => { + new PublicAssetsClient(http); + }); + }); + + describe('getHosts', () => { + it('should call the REST API', async () => { + const client = new PublicAssetsClient(http); + await client.getHosts({ from: 'x', to: 'y' }); + expect(http.get).toBeCalledTimes(1); + }); + + it('should include specified "from" and "to" parameters in http.get query', async () => { + const client = new PublicAssetsClient(http); + await client.getHosts({ from: 'x', to: 'y' }); + expect(http.get).toBeCalledWith(routePaths.GET_HOSTS, { + query: { from: 'x', to: 'y' }, + }); + }); + + it('should return the direct results of http.get', async () => { + const client = new PublicAssetsClient(http); + http.get.mockResolvedValueOnce('my result'); + const result = await client.getHosts({ from: 'x', to: 'y' }); + expect(result).toBe('my result'); + }); + }); +}); diff --git a/x-pack/plugins/asset_manager/public/lib/public_assets_client.ts b/x-pack/plugins/asset_manager/public/lib/public_assets_client.ts new file mode 100644 index 0000000000000..dd18386868f94 --- /dev/null +++ b/x-pack/plugins/asset_manager/public/lib/public_assets_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. + */ + +import { HttpStart } from '@kbn/core/public'; +import { GetHostsOptionsPublic } from '../../common/types_client'; +import { GetHostAssetsResponse } from '../../common/types_api'; +import { GET_HOSTS } from '../../common/constants_routes'; +import { IPublicAssetsClient } from '../types'; + +export class PublicAssetsClient implements IPublicAssetsClient { + constructor(private readonly http: HttpStart) {} + + async getHosts(options: GetHostsOptionsPublic) { + const results = await this.http.get(GET_HOSTS, { + query: { + ...options, + }, + }); + + return results; + } +} diff --git a/x-pack/plugins/asset_manager/public/plugin.ts b/x-pack/plugins/asset_manager/public/plugin.ts new file mode 100644 index 0000000000000..4b2d91f3a60f1 --- /dev/null +++ b/x-pack/plugins/asset_manager/public/plugin.ts @@ -0,0 +1,51 @@ +/* + * 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 { CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import { Logger } from '@kbn/logging'; +import { AssetManagerPluginClass } from './types'; +import { PublicAssetsClient } from './lib/public_assets_client'; +import type { AssetManagerPublicConfig } from '../common/config'; + +export class Plugin implements AssetManagerPluginClass { + public config: AssetManagerPublicConfig; + public logger: Logger; + + constructor(context: PluginInitializerContext<{}>) { + this.config = context.config.get(); + this.logger = context.logger.get(); + } + + setup(core: CoreSetup) { + // Check for config value and bail out if not "alpha-enabled" + if (!this.config.alphaEnabled) { + this.logger.debug('Public is NOT enabled'); + return; + } + + this.logger.debug('Public is enabled'); + + const publicAssetsClient = new PublicAssetsClient(core.http); + return { + publicAssetsClient, + }; + } + + start(core: CoreStart) { + // Check for config value and bail out if not "alpha-enabled" + if (!this.config.alphaEnabled) { + return; + } + + const publicAssetsClient = new PublicAssetsClient(core.http); + return { + publicAssetsClient, + }; + } + + stop() {} +} diff --git a/x-pack/plugins/asset_manager/public/types.ts b/x-pack/plugins/asset_manager/public/types.ts new file mode 100644 index 0000000000000..67f0053cfdd56 --- /dev/null +++ b/x-pack/plugins/asset_manager/public/types.ts @@ -0,0 +1,25 @@ +/* + * 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 { Plugin as PluginClass } from '@kbn/core/public'; +import { GetHostsOptionsPublic } from '../common/types_client'; +import { GetHostAssetsResponse } from '../common/types_api'; +export interface AssetManagerPublicPluginSetup { + publicAssetsClient: IPublicAssetsClient; +} + +export interface AssetManagerPublicPluginStart { + publicAssetsClient: IPublicAssetsClient; +} + +export type AssetManagerPluginClass = PluginClass< + AssetManagerPublicPluginSetup | undefined, + AssetManagerPublicPluginStart | undefined +>; + +export interface IPublicAssetsClient { + getHosts: (options: GetHostsOptionsPublic) => Promise; +} diff --git a/x-pack/plugins/asset_manager/server/constants.ts b/x-pack/plugins/asset_manager/server/constants.ts index 0aa1cb467df48..4630365e47875 100644 --- a/x-pack/plugins/asset_manager/server/constants.ts +++ b/x-pack/plugins/asset_manager/server/constants.ts @@ -6,4 +6,3 @@ */ export const ASSETS_INDEX_PREFIX = 'assets'; -export const ASSET_MANAGER_API_BASE = '/api/asset-manager'; diff --git a/x-pack/plugins/asset_manager/server/index.ts b/x-pack/plugins/asset_manager/server/index.ts index d6eafa380b857..5dbecbee5f9da 100644 --- a/x-pack/plugins/asset_manager/server/index.ts +++ b/x-pack/plugins/asset_manager/server/index.ts @@ -6,11 +6,21 @@ */ import { PluginInitializerContext } from '@kbn/core-plugins-server'; -import { AssetManagerServerPlugin, config } from './plugin'; +import { AssetManagerConfig } from '../common/config'; +import { + AssetManagerServerPlugin, + AssetManagerServerPluginSetup, + AssetManagerServerPluginStart, + config, +} from './plugin'; import type { WriteSamplesPostBody } from './routes/sample_assets'; -import { AssetManagerConfig } from './types'; -export type { AssetManagerConfig, WriteSamplesPostBody }; +export type { + AssetManagerConfig, + WriteSamplesPostBody, + AssetManagerServerPluginSetup, + AssetManagerServerPluginStart, +}; export { config }; export const plugin = (context: PluginInitializerContext) => diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_assets.ts b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_assets.ts index 13e2d00a82083..f975df1cd82f4 100644 --- a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_assets.ts @@ -13,7 +13,7 @@ export async function getHostsByAssets( options: GetHostsOptionsInjected ): Promise<{ hosts: Asset[] }> { const hosts = await getAssets({ - esClient: options.esClient, + elasticsearchClient: options.elasticsearchClient, filters: { kind: 'host', from: options.from, diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_signals.ts b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_signals.ts index 4fad9e301a89d..93e601ae00f9c 100644 --- a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_signals.ts +++ b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_signals.ts @@ -13,11 +13,11 @@ export async function getHostsBySignals( options: GetHostsOptionsInjected ): Promise<{ hosts: Asset[] }> { const metricsIndices = await options.metricsClient.getMetricIndices({ - savedObjectsClient: options.soClient, + savedObjectsClient: options.savedObjectsClient, }); const { assets } = await collectHosts({ - client: options.esClient, + client: options.elasticsearchClient, from: options.from, to: options.to, sourceIndices: { diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/index.ts b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/index.ts index 9becb15ebdc0a..1b60268d85389 100644 --- a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/index.ts +++ b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/index.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { AccessorOptions, OptionsWithInjectedValues } from '..'; +import type { AssetClientDependencies } from '../../../types'; +import type { GetHostsOptionsPublic } from '../../../../common/types_client'; +import type { OptionsWithInjectedValues } from '..'; -export interface GetHostsOptions extends AccessorOptions { - from: string; - to: string; -} +export type GetHostsOptions = GetHostsOptionsPublic & AssetClientDependencies; export type GetHostsOptionsInjected = OptionsWithInjectedValues; export interface HostIdentifier { diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/index.ts b/x-pack/plugins/asset_manager/server/lib/accessors/index.ts index f5cf4d38fadc8..6fd9254a2182e 100644 --- a/x-pack/plugins/asset_manager/server/lib/accessors/index.ts +++ b/x-pack/plugins/asset_manager/server/lib/accessors/index.ts @@ -5,11 +5,10 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { APMDataAccessConfig } from '@kbn/apm-data-access-plugin/server'; import { MetricsDataClient } from '@kbn/metrics-data-access-plugin/server'; import { SavedObjectsClientContract } from '@kbn/core/server'; -import { AssetManagerConfig } from '../../types'; +import { AssetManagerConfig } from '../../../common/config'; export interface InjectedValues { sourceIndices: AssetManagerConfig['sourceIndices']; @@ -18,8 +17,3 @@ export interface InjectedValues { } export type OptionsWithInjectedValues = T & InjectedValues; - -export interface AccessorOptions { - esClient: ElasticsearchClient; - soClient: SavedObjectsClientContract; -} diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/services/get_services_by_assets.ts b/x-pack/plugins/asset_manager/server/lib/accessors/services/get_services_by_assets.ts index 8bdd6283d6559..8e69bcbff4625 100644 --- a/x-pack/plugins/asset_manager/server/lib/accessors/services/get_services_by_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/accessors/services/get_services_by_assets.ts @@ -18,7 +18,7 @@ export async function getServicesByAssets( } const services = await getAssets({ - esClient: options.esClient, + elasticsearchClient: options.elasticsearchClient, filters: { kind: 'service', from: options.from, @@ -32,7 +32,7 @@ export async function getServicesByAssets( async function getServicesByParent( options: GetServicesOptionsInjected ): Promise<{ services: Asset[] }> { - const { descendants } = await getAllRelatedAssets(options.esClient, { + const { descendants } = await getAllRelatedAssets(options.elasticsearchClient, { from: options.from, to: options.to, maxDistance: 5, diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/services/get_services_by_signals.ts b/x-pack/plugins/asset_manager/server/lib/accessors/services/get_services_by_signals.ts index ab8de39adb301..720d6b3e30531 100644 --- a/x-pack/plugins/asset_manager/server/lib/accessors/services/get_services_by_signals.ts +++ b/x-pack/plugins/asset_manager/server/lib/accessors/services/get_services_by_signals.ts @@ -26,9 +26,9 @@ export async function getServicesBySignals( }); } - const apmIndices = await options.getApmIndices(options.soClient); + const apmIndices = await options.getApmIndices(options.savedObjectsClient); const { assets } = await collectServices({ - client: options.esClient, + client: options.elasticsearchClient, from: options.from, to: options.to, sourceIndices: { diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/services/index.ts b/x-pack/plugins/asset_manager/server/lib/accessors/services/index.ts index 3fed1047eacba..e8b52e4924c4d 100644 --- a/x-pack/plugins/asset_manager/server/lib/accessors/services/index.ts +++ b/x-pack/plugins/asset_manager/server/lib/accessors/services/index.ts @@ -5,13 +5,11 @@ * 2.0. */ -import { AccessorOptions, OptionsWithInjectedValues } from '..'; +import { AssetClientDependencies } from '../../../types'; +import { GetServicesOptionsPublic } from '../../../../common/types_client'; +import { OptionsWithInjectedValues } from '..'; -export interface GetServicesOptions extends AccessorOptions { - from: string; - to: string; - parent?: string; -} +export type GetServicesOptions = GetServicesOptionsPublic & AssetClientDependencies; export type GetServicesOptionsInjected = OptionsWithInjectedValues; export interface ServiceIdentifier { diff --git a/x-pack/plugins/asset_manager/server/lib/asset_accessor.ts b/x-pack/plugins/asset_manager/server/lib/asset_client.ts similarity index 81% rename from x-pack/plugins/asset_manager/server/lib/asset_accessor.ts rename to x-pack/plugins/asset_manager/server/lib/asset_client.ts index 73c2064e48311..8bf23313c663e 100644 --- a/x-pack/plugins/asset_manager/server/lib/asset_accessor.ts +++ b/x-pack/plugins/asset_manager/server/lib/asset_client.ts @@ -8,8 +8,8 @@ import { APMDataAccessConfig } from '@kbn/apm-data-access-plugin/server'; import { MetricsDataClient } from '@kbn/metrics-data-access-plugin/server'; import { SavedObjectsClientContract } from '@kbn/core/server'; +import { AssetManagerConfig } from '../../common/config'; import { Asset } from '../../common/types_api'; -import { AssetManagerConfig } from '../types'; import { OptionsWithInjectedValues } from './accessors'; import { GetHostsOptions } from './accessors/hosts'; import { GetServicesOptions } from './accessors/services'; @@ -18,28 +18,28 @@ import { getHostsBySignals } from './accessors/hosts/get_hosts_by_signals'; import { getServicesByAssets } from './accessors/services/get_services_by_assets'; import { getServicesBySignals } from './accessors/services/get_services_by_signals'; -interface AssetAccessorClassOptions { +interface AssetClientClassOptions { sourceIndices: AssetManagerConfig['sourceIndices']; source: AssetManagerConfig['lockedSource']; getApmIndices: (soClient: SavedObjectsClientContract) => Promise; metricsClient: MetricsDataClient; } -export class AssetAccessor { - constructor(private options: AssetAccessorClassOptions) {} +export class AssetClient { + constructor(private baseOptions: AssetClientClassOptions) {} injectOptions(options: T): OptionsWithInjectedValues { return { ...options, - sourceIndices: this.options.sourceIndices, - getApmIndices: this.options.getApmIndices, - metricsClient: this.options.metricsClient, + sourceIndices: this.baseOptions.sourceIndices, + getApmIndices: this.baseOptions.getApmIndices, + metricsClient: this.baseOptions.metricsClient, }; } async getHosts(options: GetHostsOptions): Promise<{ hosts: Asset[] }> { const withInjected = this.injectOptions(options); - if (this.options.source === 'assets') { + if (this.baseOptions.source === 'assets') { return await getHostsByAssets(withInjected); } else { return await getHostsBySignals(withInjected); @@ -48,7 +48,7 @@ export class AssetAccessor { async getServices(options: GetServicesOptions): Promise<{ services: Asset[] }> { const withInjected = this.injectOptions(options); - if (this.options.source === 'assets') { + if (this.baseOptions.source === 'assets') { return await getServicesByAssets(withInjected); } else { return await getServicesBySignals(withInjected); diff --git a/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.ts b/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.ts index ad8aff78cbb18..dddbb792b0979 100644 --- a/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.ts @@ -26,13 +26,13 @@ interface GetAllRelatedAssetsOptions { } export async function getAllRelatedAssets( - esClient: ElasticsearchClient, + elasticsearchClient: ElasticsearchClient, options: GetAllRelatedAssetsOptions ) { // How to put size into this? const { ean, from, to, relation, maxDistance, kind = [] } = options; - const primary = await findPrimary(esClient, { ean, from, to }); + const primary = await findPrimary(elasticsearchClient, { ean, from, to }); let assetsToFetch = [primary]; let currentDistance = 1; @@ -52,7 +52,7 @@ export async function getAllRelatedAssets( const results = flatten( await Promise.all( - assetsToFetch.map((asset) => findRelatedAssets(esClient, asset, queryOptions)) + assetsToFetch.map((asset) => findRelatedAssets(elasticsearchClient, asset, queryOptions)) ) ); @@ -75,11 +75,11 @@ export async function getAllRelatedAssets( } async function findPrimary( - esClient: ElasticsearchClient, + elasticsearchClient: ElasticsearchClient, { ean, from, to }: Pick ): Promise { const primaryResults = await getAssets({ - esClient, + elasticsearchClient, size: 1, filters: { ean, from, to }, }); @@ -101,7 +101,7 @@ type FindRelatedAssetsOptions = Pick< > & { visitedEans: string[] }; async function findRelatedAssets( - esClient: ElasticsearchClient, + elasticsearchClient: ElasticsearchClient, primary: Asset, { relation, from, to, kind, visitedEans }: FindRelatedAssetsOptions ): Promise { @@ -116,7 +116,7 @@ async function findRelatedAssets( const remainingEansToFind = without(directlyRelatedEans, ...visitedEans); if (remainingEansToFind.length > 0) { directlyRelatedAssets = await getAssets({ - esClient, + elasticsearchClient, filters: { ean: remainingEansToFind, from, to, kind }, }); } @@ -124,7 +124,7 @@ async function findRelatedAssets( debug('Directly related assets found:', JSON.stringify(directlyRelatedAssets)); const indirectlyRelatedAssets = await getIndirectlyRelatedAssets({ - esClient, + elasticsearchClient, ean: primary['asset.ean'], excludeEans: visitedEans.concat(directlyRelatedEans), relation, diff --git a/x-pack/plugins/asset_manager/server/lib/get_assets.ts b/x-pack/plugins/asset_manager/server/lib/get_assets.ts index 12f87e4b398fc..e3630f92f26e9 100644 --- a/x-pack/plugins/asset_manager/server/lib/get_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/get_assets.ts @@ -19,7 +19,7 @@ interface GetAssetsOptions extends ElasticsearchAccessorOptions { } export async function getAssets({ - esClient, + elasticsearchClient, size = 100, filters = {}, }: GetAssetsOptions): Promise { @@ -125,6 +125,6 @@ export async function getAssets({ debug('Performing Get Assets Query', '\n\n', JSON.stringify(dsl, null, 2)); - const response = await esClient.search(dsl); + const response = await elasticsearchClient.search(dsl); return response.hits.hits.map((hit) => hit._source).filter((asset): asset is Asset => !!asset); } diff --git a/x-pack/plugins/asset_manager/server/lib/get_indirectly_related_assets.ts b/x-pack/plugins/asset_manager/server/lib/get_indirectly_related_assets.ts index fa9f3279ec497..b91242f4aba1b 100644 --- a/x-pack/plugins/asset_manager/server/lib/get_indirectly_related_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/get_indirectly_related_assets.ts @@ -23,7 +23,7 @@ interface GetRelatedAssetsOptions extends ElasticsearchAccessorOptions { } export async function getIndirectlyRelatedAssets({ - esClient, + elasticsearchClient, size = 100, from = 'now-24h', to = 'now', @@ -91,7 +91,7 @@ export async function getIndirectlyRelatedAssets({ debug('Performing Indirectly Related Asset Query', '\n\n', JSON.stringify(dsl, null, 2)); - const response = await esClient.search(dsl); + const response = await elasticsearchClient.search(dsl); return response.hits.hits.map((hit) => hit._source).filter((asset): asset is Asset => !!asset); } diff --git a/x-pack/plugins/asset_manager/server/lib/write_assets.ts b/x-pack/plugins/asset_manager/server/lib/write_assets.ts index 55c5397645725..72b79bc366b6d 100644 --- a/x-pack/plugins/asset_manager/server/lib/write_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/write_assets.ts @@ -18,7 +18,7 @@ interface WriteAssetsOptions extends ElasticsearchAccessorOptions { } export async function writeAssets({ - esClient, + elasticsearchClient, assetDocs, namespace = 'default', refresh = false, @@ -33,5 +33,5 @@ export async function writeAssets({ debug('Performing Write Asset Query', '\n\n', JSON.stringify(dsl, null, 2)); - return await esClient.bulk<{}>(dsl); + return await elasticsearchClient.bulk<{}>(dsl); } diff --git a/x-pack/plugins/asset_manager/server/plugin.ts b/x-pack/plugins/asset_manager/server/plugin.ts index 6693e6037a836..24563b5e0fbc1 100644 --- a/x-pack/plugins/asset_manager/server/plugin.ts +++ b/x-pack/plugins/asset_manager/server/plugin.ts @@ -18,15 +18,16 @@ import { import { upsertTemplate } from './lib/manage_index_templates'; import { setupRoutes } from './routes'; import { assetsIndexTemplateConfig } from './templates/assets_template'; -import { AssetManagerConfig, configSchema } from './types'; -import { AssetAccessor } from './lib/asset_accessor'; +import { AssetClient } from './lib/asset_client'; import { AssetManagerPluginSetupDependencies, AssetManagerPluginStartDependencies } from './types'; +import { AssetManagerConfig, configSchema, exposeToBrowserConfig } from '../common/config'; export type AssetManagerServerPluginSetup = ReturnType; export type AssetManagerServerPluginStart = ReturnType; export const config: PluginConfigDescriptor = { schema: configSchema, + exposeToBrowser: exposeToBrowserConfig, }; export class AssetManagerServerPlugin @@ -49,13 +50,13 @@ export class AssetManagerServerPlugin public setup(core: CoreSetup, plugins: AssetManagerPluginSetupDependencies) { // Check for config value and bail out if not "alpha-enabled" if (!this.config.alphaEnabled) { - this.logger.info('Asset manager plugin [tech preview] is NOT enabled'); + this.logger.info('Server is NOT enabled'); return; } - this.logger.info('Asset manager plugin [tech preview] is enabled'); + this.logger.info('Server is enabled'); - const assetAccessor = new AssetAccessor({ + const assetClient = new AssetClient({ source: this.config.lockedSource, sourceIndices: this.config.sourceIndices, getApmIndices: plugins.apmDataAccess.getApmIndices, @@ -63,10 +64,10 @@ export class AssetManagerServerPlugin }); const router = core.http.createRouter(); - setupRoutes({ router, assetAccessor }); + setupRoutes({ router, assetClient }); return { - assetAccessor, + assetClient, }; } diff --git a/x-pack/plugins/asset_manager/server/routes/assets/hosts.ts b/x-pack/plugins/asset_manager/server/routes/assets/hosts.ts index e17ad95f81a24..f7780f2ef4a6c 100644 --- a/x-pack/plugins/asset_manager/server/routes/assets/hosts.ts +++ b/x-pack/plugins/asset_manager/server/routes/assets/hosts.ts @@ -5,56 +5,37 @@ * 2.0. */ -import * as rt from 'io-ts'; import datemath from '@kbn/datemath'; -import { - dateRt, - inRangeFromStringRt, - datemathStringRt, - createRouteValidationFunction, - createLiteralValueFromUndefinedRT, -} from '@kbn/io-ts-utils'; +import { createRouteValidationFunction } from '@kbn/io-ts-utils'; import { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; +import { GetHostAssetsQueryOptions, getHostAssetsQueryOptionsRT } from '../../../common/types_api'; import { debug } from '../../../common/debug_log'; import { SetupRouteOptions } from '../types'; -import { ASSET_MANAGER_API_BASE } from '../../constants'; -import { getEsClientFromContext } from '../utils'; - -const sizeRT = rt.union([inRangeFromStringRt(1, 100), createLiteralValueFromUndefinedRT(10)]); -const assetDateRT = rt.union([dateRt, datemathStringRt]); -const getHostAssetsQueryOptionsRT = rt.exact( - rt.partial({ - from: assetDateRT, - to: assetDateRT, - size: sizeRT, - }) -); - -export type GetHostAssetsQueryOptions = rt.TypeOf; +import * as routePaths from '../../../common/constants_routes'; +import { getClientsFromContext } from '../utils'; export function hostsRoutes({ router, - assetAccessor, + assetClient, }: SetupRouteOptions) { router.get( { - path: `${ASSET_MANAGER_API_BASE}/assets/hosts`, + path: routePaths.GET_HOSTS, validate: { query: createRouteValidationFunction(getHostAssetsQueryOptionsRT), }, }, async (context, req, res) => { const { from = 'now-24h', to = 'now' } = req.query || {}; - const esClient = await getEsClientFromContext(context); - const coreContext = await context.core; - const soClient = coreContext.savedObjects.client; + + const { elasticsearchClient, savedObjectsClient } = await getClientsFromContext(context); try { - const response = await assetAccessor.getHosts({ + const response = await assetClient.getHosts({ from: datemath.parse(from)!.toISOString(), to: datemath.parse(to)!.toISOString(), - esClient, - soClient, + elasticsearchClient, + savedObjectsClient, }); return res.ok({ body: response }); diff --git a/x-pack/plugins/asset_manager/server/routes/assets/index.ts b/x-pack/plugins/asset_manager/server/routes/assets/index.ts index b8b6d7ab0fa3a..8d9eaff170d30 100644 --- a/x-pack/plugins/asset_manager/server/routes/assets/index.ts +++ b/x-pack/plugins/asset_manager/server/routes/assets/index.ts @@ -17,11 +17,11 @@ import { } from '@kbn/io-ts-utils'; import { debug } from '../../../common/debug_log'; import { assetTypeRT, assetKindRT, relationRT } from '../../../common/types_api'; -import { ASSET_MANAGER_API_BASE } from '../../constants'; +import { GET_ASSETS, GET_RELATED_ASSETS, GET_ASSETS_DIFF } from '../../../common/constants_routes'; import { getAssets } from '../../lib/get_assets'; import { getAllRelatedAssets } from '../../lib/get_all_related_assets'; import { SetupRouteOptions } from '../types'; -import { getEsClientFromContext } from '../utils'; +import { getClientsFromContext } from '../utils'; import { AssetNotFoundError } from '../../lib/errors'; import { isValidRange } from '../../lib/utils'; @@ -82,7 +82,7 @@ export function assetsRoutes({ router }: SetupR // GET /assets router.get( { - path: `${ASSET_MANAGER_API_BASE}/assets`, + path: GET_ASSETS, validate: { query: createRouteValidationFunction(getAssetsQueryOptionsRT), }, @@ -102,10 +102,10 @@ export function assetsRoutes({ router }: SetupR }); } - const esClient = await getEsClientFromContext(context); + const { elasticsearchClient } = await getClientsFromContext(context); try { - const results = await getAssets({ esClient, size, filters }); + const results = await getAssets({ elasticsearchClient, size, filters }); return res.ok({ body: { results } }); } catch (error: unknown) { debug('error looking up asset records', error); @@ -120,7 +120,7 @@ export function assetsRoutes({ router }: SetupR // GET assets/related router.get( { - path: `${ASSET_MANAGER_API_BASE}/assets/related`, + path: GET_RELATED_ASSETS, validate: { query: createRouteValidationFunction(getRelatedAssetsQueryOptionsRT), }, @@ -129,7 +129,7 @@ export function assetsRoutes({ router }: SetupR // Add references into sample data and write integration tests const { from, to, ean, relation, maxDistance, size, type, kind } = req.query || {}; - const esClient = await getEsClientFromContext(context); + const { elasticsearchClient } = await getClientsFromContext(context); if (to && !isValidRange(from, to)) { return res.badRequest({ @@ -140,7 +140,7 @@ export function assetsRoutes({ router }: SetupR try { return res.ok({ body: { - results: await getAllRelatedAssets(esClient, { + results: await getAllRelatedAssets(elasticsearchClient, { ean, from, to, @@ -165,7 +165,7 @@ export function assetsRoutes({ router }: SetupR // GET /assets/diff router.get( { - path: `${ASSET_MANAGER_API_BASE}/assets/diff`, + path: GET_ASSETS_DIFF, validate: { query: createRouteValidationFunction(getAssetsDiffQueryOptionsRT), }, @@ -187,11 +187,11 @@ export function assetsRoutes({ router }: SetupR }); } - const esClient = await getEsClientFromContext(context); + const { elasticsearchClient } = await getClientsFromContext(context); try { const resultsForA = await getAssets({ - esClient, + elasticsearchClient, filters: { from: aFrom, to: aTo, @@ -201,7 +201,7 @@ export function assetsRoutes({ router }: SetupR }); const resultsForB = await getAssets({ - esClient, + elasticsearchClient, filters: { from: bFrom, to: bTo, diff --git a/x-pack/plugins/asset_manager/server/routes/assets/services.ts b/x-pack/plugins/asset_manager/server/routes/assets/services.ts index d7edf3b6f7f3c..3852a0bb60d11 100644 --- a/x-pack/plugins/asset_manager/server/routes/assets/services.ts +++ b/x-pack/plugins/asset_manager/server/routes/assets/services.ts @@ -17,8 +17,8 @@ import { import { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; import { debug } from '../../../common/debug_log'; import { SetupRouteOptions } from '../types'; -import { ASSET_MANAGER_API_BASE } from '../../constants'; -import { getEsClientFromContext } from '../utils'; +import { ASSET_MANAGER_API_BASE } from '../../../common/constants_routes'; +import { getClientsFromContext } from '../utils'; const sizeRT = rt.union([inRangeFromStringRt(1, 100), createLiteralValueFromUndefinedRT(10)]); const assetDateRT = rt.union([dateRt, datemathStringRt]); @@ -35,7 +35,7 @@ export type GetServiceAssetsQueryOptions = rt.TypeOf({ router, - assetAccessor, + assetClient, }: SetupRouteOptions) { // GET /assets/services router.get( @@ -47,16 +47,14 @@ export function servicesRoutes({ }, async (context, req, res) => { const { from = 'now-24h', to = 'now', parent } = req.query || {}; - const esClient = await getEsClientFromContext(context); - const coreContext = await context.core; - const soClient = coreContext.savedObjects.client; + const { elasticsearchClient, savedObjectsClient } = await getClientsFromContext(context); try { - const response = await assetAccessor.getServices({ + const response = await assetClient.getServices({ from: datemath.parse(from)!.toISOString(), to: datemath.parse(to)!.toISOString(), parent, - esClient, - soClient, + elasticsearchClient, + savedObjectsClient, }); return res.ok({ body: response }); diff --git a/x-pack/plugins/asset_manager/server/routes/index.ts b/x-pack/plugins/asset_manager/server/routes/index.ts index cab0b1558fa00..30064a8562b6f 100644 --- a/x-pack/plugins/asset_manager/server/routes/index.ts +++ b/x-pack/plugins/asset_manager/server/routes/index.ts @@ -15,11 +15,11 @@ import { servicesRoutes } from './assets/services'; export function setupRoutes({ router, - assetAccessor, + assetClient, }: SetupRouteOptions) { - pingRoute({ router, assetAccessor }); - assetsRoutes({ router, assetAccessor }); - sampleAssetsRoutes({ router, assetAccessor }); - hostsRoutes({ router, assetAccessor }); - servicesRoutes({ router, assetAccessor }); + pingRoute({ router, assetClient }); + assetsRoutes({ router, assetClient }); + sampleAssetsRoutes({ router, assetClient }); + hostsRoutes({ router, assetClient }); + servicesRoutes({ router, assetClient }); } diff --git a/x-pack/plugins/asset_manager/server/routes/ping.ts b/x-pack/plugins/asset_manager/server/routes/ping.ts index 3f7b1bb679b98..3d7a20b5fd476 100644 --- a/x-pack/plugins/asset_manager/server/routes/ping.ts +++ b/x-pack/plugins/asset_manager/server/routes/ping.ts @@ -6,7 +6,7 @@ */ import { RequestHandlerContextBase } from '@kbn/core-http-server'; -import { ASSET_MANAGER_API_BASE } from '../constants'; +import { ASSET_MANAGER_API_BASE } from '../../common/constants_routes'; import { SetupRouteOptions } from './types'; export function pingRoute({ router }: SetupRouteOptions) { diff --git a/x-pack/plugins/asset_manager/server/routes/sample_assets.ts b/x-pack/plugins/asset_manager/server/routes/sample_assets.ts index 98f7f32051f3f..447051bbb2730 100644 --- a/x-pack/plugins/asset_manager/server/routes/sample_assets.ts +++ b/x-pack/plugins/asset_manager/server/routes/sample_assets.ts @@ -7,11 +7,11 @@ import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from '@kbn/core/server'; -import { ASSET_MANAGER_API_BASE } from '../constants'; +import { ASSET_MANAGER_API_BASE } from '../../common/constants_routes'; import { getSampleAssetDocs, sampleAssets } from '../lib/sample_assets'; import { writeAssets } from '../lib/write_assets'; import { SetupRouteOptions } from './types'; -import { getEsClientFromContext } from './utils'; +import { getClientsFromContext } from './utils'; export type WriteSamplesPostBody = { baseDateTime?: string | number; @@ -62,12 +62,12 @@ export function sampleAssetsRoutes({ }, }); } - const esClient = await getEsClientFromContext(context); + const { elasticsearchClient } = await getClientsFromContext(context); const assetDocs = getSampleAssetDocs({ baseDateTime: parsed, excludeEans }); try { const response = await writeAssets({ - esClient, + elasticsearchClient, assetDocs, namespace: 'sample_data', refresh, @@ -101,9 +101,9 @@ export function sampleAssetsRoutes({ validate: {}, }, async (context, req, res) => { - const esClient = await getEsClientFromContext(context); + const { elasticsearchClient } = await getClientsFromContext(context); - const sampleDataStreams = await esClient.indices.getDataStream({ + const sampleDataStreams = await elasticsearchClient.indices.getDataStream({ name: 'assets-*-sample_data', expand_wildcards: 'all', }); @@ -115,7 +115,7 @@ export function sampleAssetsRoutes({ for (let i = 0; i < dataStreamsToDelete.length; i++) { const dsName = dataStreamsToDelete[i]; try { - await esClient.indices.deleteDataStream({ name: dsName }); + await elasticsearchClient.indices.deleteDataStream({ name: dsName }); deletedDataStreams.push(dsName); } catch (error: any) { errorWhileDeleting = diff --git a/x-pack/plugins/asset_manager/server/routes/types.ts b/x-pack/plugins/asset_manager/server/routes/types.ts index 2a0cf91f47df7..ae1b967a5b596 100644 --- a/x-pack/plugins/asset_manager/server/routes/types.ts +++ b/x-pack/plugins/asset_manager/server/routes/types.ts @@ -6,9 +6,9 @@ */ import { IRouter, RequestHandlerContextBase } from '@kbn/core-http-server'; -import { AssetAccessor } from '../lib/asset_accessor'; +import { AssetClient } from '../lib/asset_client'; export interface SetupRouteOptions { router: IRouter; - assetAccessor: AssetAccessor; + assetClient: AssetClient; } diff --git a/x-pack/plugins/asset_manager/server/routes/utils.ts b/x-pack/plugins/asset_manager/server/routes/utils.ts index 378ed0b48fc87..665adc0917fa0 100644 --- a/x-pack/plugins/asset_manager/server/routes/utils.ts +++ b/x-pack/plugins/asset_manager/server/routes/utils.ts @@ -7,6 +7,12 @@ import { RequestHandlerContext } from '@kbn/core/server'; -export async function getEsClientFromContext(context: T) { - return (await context.core).elasticsearch.client.asCurrentUser; +export async function getClientsFromContext(context: T) { + const coreContext = await context.core; + + return { + coreContext, + elasticsearchClient: coreContext.elasticsearch.client.asCurrentUser, + savedObjectsClient: coreContext.savedObjects.client, + }; } diff --git a/x-pack/plugins/asset_manager/server/types.ts b/x-pack/plugins/asset_manager/server/types.ts index 380d48aa0c7fe..431378c7c9a9f 100644 --- a/x-pack/plugins/asset_manager/server/types.ts +++ b/x-pack/plugins/asset_manager/server/types.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { schema, TypeOf } from '@kbn/config-schema'; -import { ElasticsearchClient } from '@kbn/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { ApmDataAccessPluginSetup, ApmDataAccessPluginStart, @@ -14,37 +13,9 @@ import { import { MetricsDataPluginSetup } from '@kbn/metrics-data-access-plugin/server'; export interface ElasticsearchAccessorOptions { - esClient: ElasticsearchClient; + elasticsearchClient: ElasticsearchClient; } -export const INDEX_DEFAULTS = { - logs: 'filebeat-*,logs-*', -}; - -export const configSchema = schema.object({ - alphaEnabled: schema.maybe(schema.boolean()), - // Designate where various types of data live. - // NOTE: this should be handled in a centralized way for observability, so - // that when a user configures these differently from the known defaults, - // that value is propagated everywhere. For now, we duplicate the value here. - sourceIndices: schema.object( - { - logs: schema.string({ defaultValue: INDEX_DEFAULTS.logs }), - }, - { defaultValue: INDEX_DEFAULTS } - ), - // Choose an explicit source for asset queries. - // NOTE: This will eventually need to be able to cleverly switch - // between these values based on the availability of data in the - // indices, and possibly for each asset kind/type value. - // For now, we set this explicitly. - lockedSource: schema.oneOf([schema.literal('assets'), schema.literal('signals')], { - defaultValue: 'signals', - }), -}); - -export type AssetManagerConfig = TypeOf; - export interface AssetManagerPluginSetupDependencies { apmDataAccess: ApmDataAccessPluginSetup; metricsDataAccess: MetricsDataPluginSetup; @@ -52,3 +23,8 @@ export interface AssetManagerPluginSetupDependencies { export interface AssetManagerPluginStartDependencies { apmDataAccess: ApmDataAccessPluginStart; } + +export interface AssetClientDependencies { + elasticsearchClient: ElasticsearchClient; + savedObjectsClient: SavedObjectsClientContract; +} diff --git a/x-pack/plugins/asset_manager/tsconfig.json b/x-pack/plugins/asset_manager/tsconfig.json index d7663856f0513..35972189e5287 100644 --- a/x-pack/plugins/asset_manager/tsconfig.json +++ b/x-pack/plugins/asset_manager/tsconfig.json @@ -7,6 +7,7 @@ "../../../typings/**/*", "common/**/*", "server/**/*", + "public/**/*", "types/**/*" ], "exclude": ["target/**/*"], @@ -17,10 +18,11 @@ "@kbn/core-http-server", "@kbn/core-elasticsearch-client-server-mocks", "@kbn/io-ts-utils", - "@kbn/core-elasticsearch-server", "@kbn/core-http-request-handler-context-server", "@kbn/datemath", "@kbn/apm-data-access-plugin", + "@kbn/core-http-browser-mocks", + "@kbn/logging", "@kbn/metrics-data-access-plugin" ] } From 8759b03474d1b18f8493f00752b417f09361a791 Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Thu, 28 Sep 2023 14:02:23 -0700 Subject: [PATCH 08/10] [Cloud Security] [CIS GCP] GCP Organization option (#166983) ## Summary This PR is for adding the GCP Organization option as well as updating the Single option to include Project ID field. Still rough Changes: - Added GCP Organization Option - Project ID field now exist on Google Cloud Shell Single option as well as Organization Option - Organization ID field added to the form when user chose account_type : GCP Organization - Project ID are now optional (previously users aren't able to save the integration without filling in the Project ID) - Removed Beta tag for CIS GCP TODO: - Make sure previous installation using previous wont break because of the new fields and requirement (migration) - More tests - Clean up --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/common/constants.ts | 1 - .../fleet_extensions/gcp_credential_form.tsx | 166 ++++++++++++++---- .../components/fleet_extensions/mocks.ts | 2 + .../policy_template_form.test.tsx | 119 ++++++++++--- .../fleet_extensions/policy_template_form.tsx | 153 ++++++++++++---- .../post_install_google_cloud_shell_modal.tsx | 11 +- .../google_cloud_shell_instructions.tsx | 4 +- .../steps/compute_steps.tsx | 14 +- ..._google_cloud_shell_managed_agent_step.tsx | 3 + .../enrollment_instructions/manual/index.tsx | 10 +- .../components/google_cloud_shell_guide.tsx | 17 +- ...egration_details_from_agent_policy.test.ts | 101 +++++++++++ ..._integration_details_from_agent_policy.tsx | 71 ++++++++ ...ration_details_from_package_policy.test.ts | 80 +++++++++ ...ntegration_details_from_package_policy.tsx | 53 ++++++ x-pack/plugins/fleet/public/services/index.ts | 2 + 16 files changed, 702 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.test.ts create mode 100644 x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.tsx create mode 100644 x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.test.ts create mode 100644 x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.tsx diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index eb6318e7c6727..bec25a70dbd1e 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -93,7 +93,6 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { defaultMessage: 'CIS GCP', }), icon: googleCloudLogo, - isBeta: true, }, // needs to be a function that disables/enabled based on integration version { diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx index 18ac1247d4b1a..411d5298580a5 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx @@ -25,6 +25,7 @@ import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { GcpCredentialsType } from '../../../common/types'; import { CLOUDBEAT_GCP, SETUP_ACCESS_CLOUD_SHELL, @@ -39,10 +40,12 @@ import { import { MIN_VERSION_GCP_CIS } from '../../common/constants'; import { cspIntegrationDocsNavigation } from '../../common/navigation/constants'; import { ReadDocumentation } from './aws_credentials_form/aws_credentials_form'; +import { GCP_ORGANIZATION_ACCOUNT } from './policy_template_form'; export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = { GOOGLE_CLOUD_SHELL_SETUP: 'google_cloud_shell_setup_test_id', PROJECT_ID: 'project_id_test_id', + ORGANIZATION_ID: 'organization_id_test_id', CREDENTIALS_TYPE: 'credentials_type_test_id', CREDENTIALS_FILE: 'credentials_file_test_id', CREDENTIALS_JSON: 'credentials_json_test_id', @@ -71,7 +74,21 @@ const GCPSetupInfoContent = () => ( ); -const GoogleCloudShellSetup = () => { +const GoogleCloudShellSetup = ({ + fields, + onChange, + input, +}: { + fields: Array; + onChange: (key: string, value: string) => void; + input: NewPackagePolicyInput; +}) => { + const accountType = input.streams?.[0]?.vars?.['gcp.account_type']?.value; + const getFieldById = (id: keyof GcpInputFields['fields']) => { + return fields.find((element) => element.id === id); + }; + const projectIdFields = getFieldById('gcp.project_id'); + const organizationIdFields = getFieldById('gcp.organization_id'); return ( <> { defaultMessage="Log into your Google Cloud Console" /> -
  • - -
  • + {accountType === GCP_ORGANIZATION_ACCOUNT ? ( +
  • + +
  • + ) : ( +
  • + +
  • + )} +
  • { + + {organizationIdFields && accountType === GCP_ORGANIZATION_ACCOUNT && ( + + onChange(organizationIdFields.id, event.target.value)} + /> + + )} + {projectIdFields && ( + + onChange(projectIdFields.id, event.target.value)} + /> + + )} + + ); }; @@ -137,6 +189,12 @@ interface GcpInputFields { export const gcpField: GcpInputFields = { fields: { + 'gcp.organization_id': { + label: i18n.translate('xpack.csp.gcpIntegration.organizationIdFieldLabel', { + defaultMessage: 'Organization ID', + }), + type: 'text', + }, 'gcp.project_id': { label: i18n.translate('xpack.csp.gcpIntegration.projectidFieldLabel', { defaultMessage: 'Project ID', @@ -190,17 +248,14 @@ const getSetupFormatOptions = (): Array<{ interface GcpFormProps { newPolicy: NewPackagePolicy; - input: Extract< - NewPackagePolicyPostureInput, - { type: 'cloudbeat/cis_aws' | 'cloudbeat/cis_eks' | 'cloudbeat/cis_gcp' } - >; + input: Extract; updatePolicy(updatedPolicy: NewPackagePolicy): void; packageInfo: PackageInfo; setIsValid: (isValid: boolean) => void; onChange: any; } -const getInputVarsFields = (input: NewPackagePolicyInput, fields: GcpFields) => +export const getInputVarsFields = (input: NewPackagePolicyInput, fields: GcpFields) => Object.entries(input.streams[0].vars || {}) .filter(([id]) => id in fields) .map(([id, inputVar]) => { @@ -290,6 +345,10 @@ const useCloudShellUrl = ({ }, [newPolicy?.vars?.cloud_shell_url, newPolicy, packageInfo, setupFormat]); }; +export const getGcpCredentialsType = ( + input: Extract +): GcpCredentialsType | undefined => input.streams[0].vars?.setup_access.value; + export const GcpCredentialsForm = ({ input, newPolicy, @@ -298,6 +357,12 @@ export const GcpCredentialsForm = ({ setIsValid, onChange, }: GcpFormProps) => { + /* Create a subset of properties from GcpField to use for hiding value of credentials json and credentials file when user switch from Manual to Cloud Shell, we wanna keep Project and Organization ID */ + const subsetOfGcpField = (({ ['gcp.credentials.file']: a, ['gcp.credentials.json']: b }) => ({ + 'gcp.credentials.file': a, + ['gcp.credentials.json']: b, + }))(gcpField.fields); + const fieldsToHide = getInputVarsFields(input, subsetOfGcpField); const fields = getInputVarsFields(input, gcpField.fields); const validSemantic = semverValid(packageInfo.version); const integrationVersionNumberOnly = semverCoerce(validSemantic) || ''; @@ -305,9 +370,20 @@ export const GcpCredentialsForm = ({ const fieldsSnapshot = useRef({}); const lastSetupAccessType = useRef(undefined); const setupFormat = getSetupFormatFromInput(input); - const getFieldById = (id: keyof GcpInputFields['fields']) => { - return fields.find((element) => element.id === id); - }; + const accountType = input.streams?.[0]?.vars?.['gcp.account_type']?.value; + const isOrganization = accountType === 'organization-account'; + // Integration is Invalid IF Version is not at least 1.5.0 OR Setup Access is manual but Project ID is empty + useEffect(() => { + const isInvalidPolicy = isInvalid; + + setIsValid(!isInvalidPolicy); + + onChange({ + isValid: !isInvalidPolicy, + updatedPolicy: newPolicy, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setupFormat, input.type]); useCloudShellUrl({ packageInfo, @@ -316,23 +392,23 @@ export const GcpCredentialsForm = ({ setupFormat, }); const onSetupFormatChange = (newSetupFormat: SetupFormatGCP) => { - if (newSetupFormat === SETUP_ACCESS_CLOUD_SHELL) { + if (newSetupFormat === 'google_cloud_shell') { // We need to store the current manual fields to restore them later fieldsSnapshot.current = Object.fromEntries( - fields.map((field) => [field.id, { value: field.value }]) + fieldsToHide.map((field) => [field.id, { value: field.value }]) ); // We need to store the last manual credentials type to restore it later - lastSetupAccessType.current = input.streams[0].vars?.setup_access?.value; + lastSetupAccessType.current = getGcpCredentialsType(input); updatePolicy( getPosturePolicy(newPolicy, input.type, { setup_access: { - value: SETUP_ACCESS_CLOUD_SHELL, + value: 'google_cloud_shell', type: 'text', }, // Clearing fields from previous setup format to prevent exposing credentials // when switching from manual to cloud formation - ...Object.fromEntries(fields.map((field) => [field.id, { value: undefined }])), + ...Object.fromEntries(fieldsToHide.map((field) => [field.id, { value: undefined }])), }) ); } else { @@ -340,7 +416,7 @@ export const GcpCredentialsForm = ({ getPosturePolicy(newPolicy, input.type, { setup_access: { // Restoring last manual credentials type - value: SETUP_ACCESS_MANUAL, + value: lastSetupAccessType.current || SETUP_ACCESS_MANUAL, type: 'text', }, // Restoring fields from manual setup format if any @@ -349,20 +425,6 @@ export const GcpCredentialsForm = ({ ); } }; - // Integration is Invalid IF Version is not at least 1.5.0 OR Setup Access is manual but Project ID is empty - useEffect(() => { - const isProjectIdEmpty = - setupFormat === SETUP_ACCESS_MANUAL && !getFieldById('gcp.project_id')?.value; - const isInvalidPolicy = isInvalid || isProjectIdEmpty; - - setIsValid(!isInvalidPolicy); - - onChange({ - isValid: !isInvalidPolicy, - updatedPolicy: newPolicy, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [input, packageInfo, setupFormat]); if (isInvalid) { return ( @@ -385,19 +447,29 @@ export const GcpCredentialsForm = ({ size="s" options={getSetupFormatOptions()} idSelected={setupFormat} - onChange={onSetupFormatChange} + onChange={(idSelected: SetupFormatGCP) => + idSelected !== setupFormat && onSetupFormatChange(idSelected) + } /> - {setupFormat === SETUP_ACCESS_MANUAL ? ( - updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) } + input={input} /> ) : ( - + + updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) + } + isOrganization={isOrganization} + /> )} + @@ -408,13 +480,18 @@ export const GcpCredentialsForm = ({ const GcpInputVarFields = ({ fields, onChange, + isOrganization, }: { fields: Array; onChange: (key: string, value: string) => void; + isOrganization: boolean; }) => { const getFieldById = (id: keyof GcpInputFields['fields']) => { return fields.find((element) => element.id === id); }; + + const organizationIdFields = getFieldById('gcp.organization_id'); + const projectIdFields = getFieldById('gcp.project_id'); const credentialsTypeFields = getFieldById('gcp.credentials.type'); const credentialFilesFields = getFieldById('gcp.credentials.file'); @@ -428,6 +505,17 @@ const GcpInputVarFields = ({ return (
    + {organizationIdFields && isOrganization && ( + + onChange(organizationIdFields.id, event.target.value)} + /> + + )} {projectIdFields && ( ', () => { it(`renders Google Cloud Shell forms when Setup Access is set to Google Cloud Shell`, () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { - credentials_type: { value: 'credentials-file' }, + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, setup_access: { value: 'google_cloud_shell' }, }); @@ -1028,31 +1030,6 @@ describe('', () => { ).toBeInTheDocument(); }); - it(`project ID is required for Manual users`, () => { - let policy = getMockPolicyGCP(); - policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { - 'gcp.project_id': { value: undefined }, - setup_access: { value: 'manual' }, - }); - - const { rerender } = render( - - ); - expect(onChange).toHaveBeenCalledWith({ - isValid: false, - updatedPolicy: policy, - }); - policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { - 'gcp.project_id': { value: '' }, - setup_access: { value: 'manual' }, - }); - rerender(); - expect(onChange).toHaveBeenCalledWith({ - isValid: false, - updatedPolicy: policy, - }); - }); - it(`renders ${CLOUDBEAT_GCP} Credentials File fields`, () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { @@ -1136,6 +1113,96 @@ describe('', () => { updatedPolicy: policy, }); }); + + it(`${CLOUDBEAT_GCP} form do not displays upgrade message for supported versions and gcp organization option is enabled`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.credentials.type': { value: 'manual' }, + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, + }); + + const { queryByText, getByLabelText } = render( + + ); + + expect( + queryByText( + 'GCP Organization not supported in current integration version. Please upgrade to the latest version to enable GCP Organizations integration.' + ) + ).not.toBeInTheDocument(); + expect(getByLabelText('GCP Organization')).toBeEnabled(); + }); + + it(`renders ${CLOUDBEAT_GCP} Organization fields when account type is Organization and Setup Access is Google Cloud Shell`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, + setup_access: { value: 'google_cloud_shell' }, + }); + + const { getByLabelText, getByTestId } = render( + + ); + + expect(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID)).toBeInTheDocument(); + + expect(getByLabelText('Organization ID')).toBeInTheDocument(); + }); + + it(`renders ${CLOUDBEAT_GCP} Organization fields when account type is Organization and Setup Access is manual`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, + setup_access: { value: 'manual' }, + }); + + const { getByLabelText, getByTestId } = render( + + ); + + expect(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID)).toBeInTheDocument(); + + expect(getByLabelText('Organization ID')).toBeInTheDocument(); + }); + + it(`Should not render ${CLOUDBEAT_GCP} Organization fields when account type is Single`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.account_type': { value: GCP_SINGLE_ACCOUNT }, + setup_access: { value: 'google_cloud_shell' }, + }); + + const { queryByLabelText, queryByTestId } = render( + + ); + + expect(queryByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID)).toBeNull(); + + expect(queryByLabelText('Organization ID')).toBeNull(); + }); + + it(`updates ${CLOUDBEAT_GCP} organization id`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, + setup_access: { value: 'manual' }, + }); + + const { getByTestId } = render( + + ); + + userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID), 'c'); + + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.organization_id': { value: 'c' }, + }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); }); describe('Azure Credentials input fields', () => { diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index 67a617decac76..306cc6da445fd 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -4,9 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import semverCompare from 'semver/functions/compare'; import semverValid from 'semver/functions/valid'; +import semverCoerce from 'semver/functions/coerce'; +import semverLt from 'semver/functions/lt'; import { EuiCallOut, EuiFieldText, @@ -54,6 +56,7 @@ import { PolicyTemplateVarsForm, } from './policy_template_selectors'; import { usePackagePolicyList } from '../../common/api/use_package_policy_list'; +import { gcpField, getInputVarsFields } from './gcp_credential_form'; const DEFAULT_INPUT_TYPE = { kspm: CLOUDBEAT_VANILLA, @@ -82,13 +85,14 @@ interface IntegrationInfoFieldsProps { export const AWS_SINGLE_ACCOUNT = 'single-account'; export const AWS_ORGANIZATION_ACCOUNT = 'organization-account'; -export const GCP_SINGLE_ACCOUNT = 'single-account-gcp'; -export const GCP_ORGANIZATION_ACCOUNT = 'organization-account-gcp'; +export const GCP_SINGLE_ACCOUNT = 'single-account'; +export const GCP_ORGANIZATION_ACCOUNT = 'organization-account'; export const AZURE_SINGLE_ACCOUNT = 'single-account-azure'; export const AZURE_ORGANIZATION_ACCOUNT = 'organization-account-azure'; type AwsAccountType = typeof AWS_SINGLE_ACCOUNT | typeof AWS_ORGANIZATION_ACCOUNT; type AzureAccountType = typeof AZURE_SINGLE_ACCOUNT | typeof AZURE_ORGANIZATION_ACCOUNT; +type GcpAccountType = typeof GCP_SINGLE_ACCOUNT | typeof GCP_ORGANIZATION_ACCOUNT; const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps['options'] => [ { @@ -111,19 +115,18 @@ const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps }, ]; -const getGcpAccountTypeOptions = (): CspRadioGroupProps['options'] => [ +const getGcpAccountTypeOptions = (isGcpOrgDisabled: boolean): CspRadioGroupProps['options'] => [ { id: GCP_ORGANIZATION_ACCOUNT, label: i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationLabel', { defaultMessage: 'GCP Organization', }), - disabled: true, - tooltip: i18n.translate( - 'xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationDisabledTooltip', - { - defaultMessage: 'Coming Soon', - } - ), + disabled: isGcpOrgDisabled, + tooltip: isGcpOrgDisabled + ? i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationDisabledTooltip', { + defaultMessage: 'Supported from integration version 1.6.0 and above', + }) + : undefined, }, { id: GCP_SINGLE_ACCOUNT, @@ -258,6 +261,12 @@ const AwsAccountTypeSelect = ({ ); }; +const getGcpAccountType = ( + input: Extract +): GcpAccountType | undefined => input.streams[0].vars?.['gcp.account_type']?.value; + +const GCP_ORG_MINIMUM_PACKAGE_VERSION = '1.6.0'; + const GcpAccountTypeSelect = ({ input, newPolicy, @@ -269,6 +278,71 @@ const GcpAccountTypeSelect = ({ updatePolicy: (updatedPolicy: NewPackagePolicy) => void; packageInfo: PackageInfo; }) => { + // This will disable the gcp org option for any version below 1.6.0 which introduced support for account_type. https://github.com/elastic/integrations/pull/6682 + const validSemantic = semverValid(packageInfo.version); + const integrationVersionNumberOnly = semverCoerce(validSemantic) || ''; + const isGcpOrgDisabled = semverLt(integrationVersionNumberOnly, GCP_ORG_MINIMUM_PACKAGE_VERSION); + + const gcpAccountTypeOptions = useMemo( + () => getGcpAccountTypeOptions(isGcpOrgDisabled), + [isGcpOrgDisabled] + ); + /* Create a subset of properties from GcpField to use for hiding value of Organization ID when switching account type from Organization to Single */ + const subsetOfGcpField = (({ ['gcp.organization_id']: a }) => ({ 'gcp.organization_id': a }))( + gcpField.fields + ); + const fieldsToHide = getInputVarsFields(input, subsetOfGcpField); + const fieldsSnapshot = useRef({}); + const lastSetupAccessType = useRef(undefined); + const onSetupFormatChange = (newSetupFormat: string) => { + if (newSetupFormat === 'single-account') { + // We need to store the current manual fields to restore them later + fieldsSnapshot.current = Object.fromEntries( + fieldsToHide.map((field) => [field.id, { value: field.value }]) + ); + // We need to store the last manual credentials type to restore it later + lastSetupAccessType.current = input.streams[0].vars?.['gcp.account_type'].value; + + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'gcp.account_type': { + value: 'single-account', + type: 'text', + }, + // Clearing fields from previous setup format to prevent exposing credentials + // when switching from manual to cloud formation + ...Object.fromEntries(fieldsToHide.map((field) => [field.id, { value: undefined }])), + }) + ); + } else { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'gcp.account_type': { + // Restoring last manual credentials type + value: lastSetupAccessType.current || 'organization-account', + type: 'text', + }, + // Restoring fields from manual setup format if any + ...fieldsSnapshot.current, + }) + ); + } + }; + + useEffect(() => { + if (!getGcpAccountType(input)) { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'gcp.account_type': { + value: isGcpOrgDisabled ? GCP_SINGLE_ACCOUNT : GCP_ORGANIZATION_ACCOUNT, + type: 'text', + }, + }) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [input]); + return ( <> @@ -278,28 +352,47 @@ const GcpAccountTypeSelect = ({ /> + {isGcpOrgDisabled && ( + <> + + + + + + )} { - updatePolicy( - getPosturePolicy(newPolicy, input.type, { - gcp_account_type: { - value: accountType, - type: 'text', - }, - }) - ); - }} + idSelected={getGcpAccountType(input) || ''} + options={gcpAccountTypeOptions} + onChange={(accountType) => + accountType !== getGcpAccountType(input) && onSetupFormatChange(accountType) + } size="m" /> - - - - + {getGcpAccountType(input) === GCP_ORGANIZATION_ACCOUNT && ( + <> + + + + + + )} + {getGcpAccountType(input) === GCP_SINGLE_ACCOUNT && ( + <> + + + + + + )} ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx index a9185d3efa743..ce43298d8a97d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx @@ -30,6 +30,7 @@ import { } from '../../../../../hooks'; import { GoogleCloudShellGuide } from '../../../../../components'; import { ManualInstructions } from '../../../../../../../components/enrollment_instructions'; +import { getGcpIntegrationDetailsFromPackagePolicy } from '../../../../../../../services'; export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ onConfirm: () => void; @@ -46,6 +47,8 @@ export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ ); const { fleetServerHosts, fleetProxy } = useFleetServerHostsForPolicy(agentPolicy); const agentVersion = useAgentVersion(); + const { gcpProjectId, gcpOrganizationId, gcpAccountType } = + getGcpIntegrationDetailsFromPackagePolicy(packagePolicy); const { cloudShellUrl, error, isError, isLoading } = useCreateCloudShellUrl({ enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key, @@ -61,6 +64,9 @@ export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ fleetServerHosts, fleetProxy, agentVersion, + gcpProjectId, + gcpOrganizationId, + gcpAccountType, }); return ( @@ -75,7 +81,10 @@ export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ - + {error && isError && ( <> diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx index 89b6f67fe0dcf..a7090370680e4 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx @@ -14,15 +14,17 @@ import { GoogleCloudShellGuide } from '../google_cloud_shell_guide'; interface Props { cloudShellUrl: string; cloudShellCommand: string; + projectId?: string; } export const GoogleCloudShellInstructions: React.FunctionComponent = ({ cloudShellUrl, cloudShellCommand, + projectId, }) => { return ( <> - + = ({ const agentVersion = useAgentVersion(); + const { gcpProjectId, gcpOrganizationId, gcpAccountType } = + getGcpIntegrationDetailsFromAgentPolicy(selectedPolicy); + const fleetServerHost = fleetServerHosts?.[0]; const installManagedCommands = ManualInstructions({ @@ -228,6 +235,9 @@ export const ManagedSteps: React.FunctionComponent = ({ fleetServerHosts, fleetProxy, agentVersion: agentVersion || '', + gcpProjectId, + gcpOrganizationId, + gcpAccountType, }); const instructionsSteps = useMemo(() => { @@ -273,6 +283,7 @@ export const ManagedSteps: React.FunctionComponent = ({ selectedApiKeyId, cloudShellUrl: cloudSecurityIntegration.cloudShellUrl, cloudShellCommand: installManagedCommands.googleCloudShell, + projectId: gcpProjectId, }) ); } else if (cloudSecurityIntegration?.isAzureArmTemplate) { @@ -343,6 +354,7 @@ export const ManagedSteps: React.FunctionComponent = ({ enrolledAgentIds, agentDataConfirmed, installedPackagePolicy, + gcpProjectId, ]); if (!agentVersion) { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_google_cloud_shell_managed_agent_step.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_google_cloud_shell_managed_agent_step.tsx index ff367be9125fd..4e5b15c626735 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_google_cloud_shell_managed_agent_step.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_google_cloud_shell_managed_agent_step.tsx @@ -21,12 +21,14 @@ export const InstallGoogleCloudShellManagedAgentStep = ({ isComplete, cloudShellUrl, cloudShellCommand, + projectId, }: { selectedApiKeyId?: string; apiKeyData?: GetOneEnrollmentAPIKeyResponse | null; isComplete?: boolean; cloudShellUrl?: string | undefined; cloudShellCommand?: string; + projectId?: string; }): EuiContainedStepProps => { const nonCompleteStatus = selectedApiKeyId ? undefined : 'disabled'; const status = isComplete ? 'complete' : nonCompleteStatus; @@ -41,6 +43,7 @@ export const InstallGoogleCloudShellManagedAgentStep = ({ ) : ( diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index b7a4fed713cad..21c8ec6172cc7 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -28,11 +28,17 @@ export const ManualInstructions = ({ fleetServerHosts, fleetProxy, agentVersion: agentVersion, + gcpProjectId = '', + gcpOrganizationId = '', + gcpAccountType, }: { apiKey: string; fleetServerHosts: string[]; fleetProxy?: FleetProxy; agentVersion: string; + gcpProjectId?: string; + gcpOrganizationId?: string; + gcpAccountType?: string; }) => { const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts, fleetProxy); const fleetServerUrl = enrollArgs?.split('--url=')?.pop()?.split('--enrollment')[0]; @@ -64,7 +70,9 @@ sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \n sudo rpm -vi elastic-agent-${agentVersion}-x86_64.rpm sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \nsudo systemctl start elastic-agent`; - const googleCloudShellCommand = `gcloud config set project && \nFLEET_URL=${fleetServerUrl} ENROLLMENT_TOKEN=${enrollmentToken} STACK_VERSION=${agentVersion} ./deploy.sh`; + const googleCloudShellCommand = `gcloud config set project ${gcpProjectId} && ${ + gcpAccountType === 'organization-account' ? `\nORG_ID=${gcpOrganizationId}` : `` + } \nFLEET_URL=${fleetServerUrl} ENROLLMENT_TOKEN=${enrollmentToken} \nSTACK_VERSION=${agentVersion} ./deploy.sh`; return { linux: linuxCommand, diff --git a/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx b/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx index d494fc1075f41..1d5c804ef157a 100644 --- a/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx +++ b/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx @@ -23,7 +23,7 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) => ); -export const GoogleCloudShellGuide = (props: { commandText: string }) => { +export const GoogleCloudShellGuide = (props: { commandText: string; hasProjectId?: boolean }) => { return ( <> @@ -48,10 +48,17 @@ export const GoogleCloudShellGuide = (props: { commandText: string }) => {
    1. <> - + {props?.hasProjectId ? ( + + ) : ( + + )} {props.commandText} diff --git a/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.test.ts b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.test.ts new file mode 100644 index 0000000000000..e53e2dc36df07 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { getGcpIntegrationDetailsFromAgentPolicy } from './get_gcp_integration_details_from_agent_policy'; + +const undefinedAllValue = { + gcpAccountType: undefined, + gcpOrganizationId: undefined, + gcpProjectId: undefined, +}; + +describe('getGcpIntegrationDetailsFromAgentPolicy', () => { + test('returns undefined when agentPolicy is undefined', () => { + const result = getGcpIntegrationDetailsFromAgentPolicy(undefined); + expect(result).toEqual(undefinedAllValue); + }); + + test('returns undefined when agentPolicy is defined but inputs are empty', () => { + const selectedPolicy = { inputs: [] }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromAgentPolicy(selectedPolicy); + expect(result).toEqual(undefinedAllValue); + }); + + it('should return undefined when no input has enabled and gcp integration details', () => { + const selectedPolicy = { + package_policies: [ + { + inputs: [ + { enabled: false, streams: [{}] }, + { enabled: true, streams: [{ vars: { other_property: 'false' } }] }, + { enabled: true, streams: [{ other_property: 'False' }] }, + ], + }, + { + inputs: [ + { enabled: false, streams: [{}] }, + { enabled: false, streams: [{}] }, + ], + }, + ], + }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromAgentPolicy(selectedPolicy); + expect(result).toEqual(undefinedAllValue); + }); + + it('should return the first gcp integration details when available', () => { + const selectedPolicy = { + package_policies: [ + { + inputs: [ + { enabled: false, streams: [{}] }, + { enabled: true, streams: [{ vars: { other_property: 'false' } }] }, + { enabled: true, streams: [{ other_property: 'False' }] }, + ], + }, + { + inputs: [ + { enabled: false, streams: [{}] }, + { + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'account_type_test_1' }, + 'gcp.project_id': { value: 'project_id_1' }, + 'gcp.organization_id': { value: 'organization_id_1' }, + }, + }, + ], + }, + { + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'account_type_test_2' }, + 'gcp.project_id': { value: 'project_id_2' }, + 'gcp.organization_id': { value: 'organization_id_2' }, + }, + }, + ], + }, + ], + }, + ], + }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromAgentPolicy(selectedPolicy); + expect(result).toEqual({ + gcpAccountType: 'account_type_test_1', + gcpOrganizationId: 'organization_id_1', + gcpProjectId: 'project_id_1', + }); + }); + // Add more test cases as needed +}); diff --git a/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.tsx b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.tsx new file mode 100644 index 0000000000000..a1112683b4f1a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.tsx @@ -0,0 +1,71 @@ +/* + * 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 { AgentPolicy } from '../types'; + +/** + * Get the project id, organization id and account type of gcp integration from an agent policy + */ +export const getGcpIntegrationDetailsFromAgentPolicy = (selectedPolicy?: AgentPolicy) => { + let gcpProjectId = selectedPolicy?.package_policies?.reduce((acc, packagePolicy) => { + const findGcpProjectId = packagePolicy.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.project_id']?.value) { + return input?.streams[0]?.vars?.['gcp.project_id']?.value; + } + return accInput; + }, ''); + if (findGcpProjectId) { + return findGcpProjectId; + } + return acc; + }, ''); + + let gcpOrganizationId = selectedPolicy?.package_policies?.reduce((acc, packagePolicy) => { + const findGcpProjectId = packagePolicy.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.organization_id']?.value) { + return input?.streams[0]?.vars?.['gcp.organization_id']?.value; + } + return accInput; + }, ''); + if (findGcpProjectId) { + return findGcpProjectId; + } + return acc; + }, ''); + + let gcpAccountType = selectedPolicy?.package_policies?.reduce((acc, packagePolicy) => { + const findGcpProjectId = packagePolicy.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.account_type']?.value) { + return input?.streams[0]?.vars?.['gcp.account_type']?.value; + } + return accInput; + }, ''); + if (findGcpProjectId) { + return findGcpProjectId; + } + return acc; + }, ''); + + gcpProjectId = gcpProjectId !== '' ? gcpProjectId : undefined; + gcpOrganizationId = gcpOrganizationId !== '' ? gcpOrganizationId : undefined; + gcpAccountType = gcpAccountType !== '' ? gcpAccountType : undefined; + + return { + gcpProjectId, + gcpOrganizationId, + gcpAccountType, + }; +}; diff --git a/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.test.ts b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.test.ts new file mode 100644 index 0000000000000..44da2fb65383a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { getGcpIntegrationDetailsFromPackagePolicy } from './get_gcp_integration_details_from_package_policy'; + +const undefinedAllValue = { + gcpAccountType: undefined, + gcpOrganizationId: undefined, + gcpProjectId: undefined, +}; + +describe('getGcpIntegrationDetailsFromPackagePolicy', () => { + test('returns undefined when packagePolicy is undefined', () => { + const result = getGcpIntegrationDetailsFromPackagePolicy(undefined); + expect(result).toEqual(undefinedAllValue); + }); + + test('returns undefined when packagePolicy is defined but inputs are empty', () => { + const packagePolicy = { inputs: [] }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromPackagePolicy(packagePolicy); + expect(result).toEqual(undefinedAllValue); + }); + + it('should return undefined when no input has enabled and gcp integration details', () => { + const packagePolicy = { + inputs: [ + { enabled: false, streams: [{}] }, + { enabled: true, streams: [{ vars: { other_property: 'false' } }] }, + { enabled: true, streams: [{ other_property: 'False' }] }, + ], + }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromPackagePolicy(packagePolicy); + expect(result).toEqual(undefinedAllValue); + }); + + it('should return the first gcp integration details when available', () => { + const packagePolicy = { + inputs: [ + { enabled: false, streams: [{}] }, + { + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'account_type_test_1' }, + 'gcp.project_id': { value: 'project_id_1' }, + 'gcp.organization_id': { value: 'organization_id_1' }, + }, + }, + ], + }, + { + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'account_type_test_2' }, + 'gcp.project_id': { value: 'project_id_2' }, + 'gcp.organization_id': { value: 'organization_id_2' }, + }, + }, + ], + }, + ], + }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromPackagePolicy(packagePolicy); + expect(result).toEqual({ + gcpAccountType: 'account_type_test_1', + gcpOrganizationId: 'organization_id_1', + gcpProjectId: 'project_id_1', + }); + }); + // Add more test cases as needed +}); diff --git a/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.tsx b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.tsx new file mode 100644 index 0000000000000..ae82352d51e0a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.tsx @@ -0,0 +1,53 @@ +/* + * 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 { PackagePolicy } from '../types'; + +/** + * Get the project id, organization id and account type of gcp integration from a package policy + */ +export const getGcpIntegrationDetailsFromPackagePolicy = (packagePolicy?: PackagePolicy) => { + let gcpProjectId = packagePolicy?.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.project_id']?.value) { + return input?.streams[0]?.vars?.['gcp.project_id']?.value; + } + return accInput; + }, ''); + + let gcpOrganizationId = packagePolicy?.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.organization_id']?.value) { + return input?.streams[0]?.vars?.['gcp.organization_id']?.value; + } + return accInput; + }, ''); + + let gcpAccountType = packagePolicy?.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.account_type']?.value) { + return input?.streams[0]?.vars?.['gcp.account_type']?.value; + } + return accInput; + }, ''); + + gcpProjectId = gcpProjectId !== '' ? gcpProjectId : undefined; + gcpOrganizationId = gcpOrganizationId !== '' ? gcpOrganizationId : undefined; + gcpAccountType = gcpAccountType !== '' ? gcpAccountType : undefined; + + return { + gcpProjectId, + gcpOrganizationId, + gcpAccountType, + }; +}; diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index 64009e4a11061..71f5fde90d93a 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -53,3 +53,5 @@ export { getTemplateUrlFromAgentPolicy } from './get_template_url_from_agent_pol export { getTemplateUrlFromPackageInfo } from './get_template_url_from_package_info'; export { getCloudShellUrlFromPackagePolicy } from './get_cloud_shell_url_from_package_policy'; export { getCloudShellUrlFromAgentPolicy } from './get_cloud_shell_url_from_agent_policy'; +export { getGcpIntegrationDetailsFromPackagePolicy } from './get_gcp_integration_details_from_package_policy'; +export { getGcpIntegrationDetailsFromAgentPolicy } from './get_gcp_integration_details_from_agent_policy'; From e84742dfc66dbb2adced21ac0cf8b5786a5be081 Mon Sep 17 00:00:00 2001 From: Alex Szabo Date: Fri, 29 Sep 2023 00:16:22 +0200 Subject: [PATCH 09/10] [Osquery][Ops] Move Osquery/cypress configurations close to cypress (#167428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The osquery-cypress package was referring to configurations from a parent folder. This caused difficulties with having to exclude/include the same files in the `tsconfig.json`s, leading to hard-to-resolve typescript issues. This PR moves the configs in, and cleans up redundancies and type errors. chore(osquery): move osquery/cypress configs to cypress folder, fix type errors --------- Co-authored-by: Patryk Kopyciński Co-authored-by: Brad White Co-authored-by: Brad White --- x-pack/plugins/osquery/{ => cypress}/cypress.config.ts | 7 +++---- .../osquery/cypress/e2e/all/add_integration.cy.ts | 1 - .../osquery/cypress/e2e/all/packs_create_edit.cy.ts | 2 -- x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts | 4 ---- .../osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts | 4 ---- .../osquery/{ => cypress}/serverless_cypress.config.ts | 3 +-- x-pack/plugins/osquery/cypress/support/e2e.ts | 10 +++++----- x-pack/plugins/osquery/cypress/tasks/navigation.ts | 6 +++++- x-pack/plugins/osquery/cypress/tsconfig.json | 7 ++++--- x-pack/plugins/osquery/package.json | 4 ++-- x-pack/plugins/osquery/tsconfig.json | 10 +++------- .../test_suites/security/cypress/tasks/login.ts | 2 +- 12 files changed, 24 insertions(+), 36 deletions(-) rename x-pack/plugins/osquery/{ => cypress}/cypress.config.ts (83%) rename x-pack/plugins/osquery/{ => cypress}/serverless_cypress.config.ts (85%) diff --git a/x-pack/plugins/osquery/cypress.config.ts b/x-pack/plugins/osquery/cypress/cypress.config.ts similarity index 83% rename from x-pack/plugins/osquery/cypress.config.ts rename to x-pack/plugins/osquery/cypress/cypress.config.ts index 4efb4ce8c5429..26b1d9b67850d 100644 --- a/x-pack/plugins/osquery/cypress.config.ts +++ b/x-pack/plugins/osquery/cypress/cypress.config.ts @@ -11,11 +11,10 @@ import path from 'path'; import { safeLoad as loadYaml } from 'js-yaml'; import { readFileSync } from 'fs'; -import type { YamlRoleDefinitions } from '../../test_serverless/shared/lib'; -// eslint-disable-next-line @kbn/imports/no_boundary_crossing -import { setupUserDataLoader } from '../../test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks'; +import type { YamlRoleDefinitions } from '../../../test_serverless/shared/lib'; +import { setupUserDataLoader } from '../../../test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks'; const ROLES_YAML_FILE_PATH = path.join( - `${__dirname}/cypress/support`, + `${__dirname}/support`, 'project_controller_osquery_roles.yml' ); const roleDefinitions = loadYaml(readFileSync(ROLES_YAML_FILE_PATH, 'utf8')) as YamlRoleDefinitions; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts index a84cdb5013047..ecad2eebf5248 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts @@ -198,7 +198,6 @@ describe('ALL - Add Integration', { tags: ['@ess', '@brokenInServerless'] }, () // test list of prebuilt queries navigateTo('/app/osquery/saved_queries'); - cy.waitForReact(); cy.react('EuiTableRow').should('have.length.above', 5); }); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts index 52d1236c1f12f..770d5afc5ec0f 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts @@ -544,8 +544,6 @@ describe('Packs - Create and Edit', () => { recurse( () => { - cy.waitForReact(); - cy.getBySel('docsLoading').should('exist'); cy.getBySel('docsLoading').should('not.exist'); diff --git a/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts b/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts index 82b02bdda7289..67f7eff7f3ed4 100644 --- a/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts @@ -47,7 +47,6 @@ describe('Reader - only READ', { tags: ['@ess'] }, () => { it('should not be able to add nor run saved queries', () => { navigateTo('/app/osquery/saved_queries'); - cy.waitForReact(1000); cy.contains(savedQueryName); cy.contains('Add saved query').should('be.disabled'); cy.react('PlayButtonComponent', { @@ -70,13 +69,11 @@ describe('Reader - only READ', { tags: ['@ess'] }, () => { it('should not be able to enter live queries with just read and no run saved queries', () => { navigateTo('/app/osquery/live_queries/new'); - cy.waitForReact(1000); cy.contains('Permission denied'); }); it('should not be able to play in live queries history', () => { navigateTo('/app/osquery/live_queries'); - cy.waitForReact(1000); cy.contains('New live query').should('be.disabled'); cy.contains(liveQueryQuery); cy.react('EuiIconPlay', { options: { timeout: 3000 } }).should('not.exist'); @@ -85,7 +82,6 @@ describe('Reader - only READ', { tags: ['@ess'] }, () => { it('should not be able to add nor edit packs', () => { navigateTo('/app/osquery/packs'); - cy.waitForReact(1000); cy.contains('Add pack').should('be.disabled'); cy.getBySel('tablePaginationPopoverButton').click(); cy.getBySel('tablePagination-50-rows').click(); diff --git a/x-pack/plugins/osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts b/x-pack/plugins/osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts index 779df77f2d382..6528c9b911932 100644 --- a/x-pack/plugins/osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts @@ -57,7 +57,6 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { it('should be able to run saved queries but not add new ones', () => { navigateTo('/app/osquery/saved_queries'); - cy.waitForReact(1000); cy.contains(savedQueryName); cy.contains('Add saved query').should('be.disabled'); cy.react('PlayButtonComponent', { @@ -79,7 +78,6 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { it('should be able to play in live queries history', () => { navigateTo('/app/osquery/live_queries'); - cy.waitForReact(1000); cy.contains('New live query').should('not.be.disabled'); cy.contains(liveQueryQuery); cy.wait(1000); @@ -91,7 +89,6 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { it('should be able to use saved query in a new query', () => { navigateTo('/app/osquery/live_queries'); - cy.waitForReact(1000); cy.contains('New live query').should('not.be.disabled').click(); selectAllAgents(); getSavedQueriesDropdown().type(`${savedQueryName}{downArrow} {enter}`); @@ -102,7 +99,6 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { it('should not be able to add nor edit packs', () => { navigateTo('/app/osquery/packs'); - cy.waitForReact(1000); cy.getBySel('tablePaginationPopoverButton').click(); cy.getBySel('tablePagination-50-rows').click(); cy.contains('Add pack').should('be.disabled'); diff --git a/x-pack/plugins/osquery/serverless_cypress.config.ts b/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts similarity index 85% rename from x-pack/plugins/osquery/serverless_cypress.config.ts rename to x-pack/plugins/osquery/cypress/serverless_cypress.config.ts index 6b60dc076631f..fff0d4431df52 100644 --- a/x-pack/plugins/osquery/serverless_cypress.config.ts +++ b/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts @@ -6,8 +6,7 @@ */ import { defineCypressConfig } from '@kbn/cypress-config'; -// eslint-disable-next-line @kbn/imports/no_boundary_crossing -import { setupUserDataLoader } from '../../test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks'; +import { setupUserDataLoader } from '../../../test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks'; // eslint-disable-next-line import/no-default-export export default defineCypressConfig({ diff --git a/x-pack/plugins/osquery/cypress/support/e2e.ts b/x-pack/plugins/osquery/cypress/support/e2e.ts index ed267eaff8ac6..760aeb80d3ee8 100644 --- a/x-pack/plugins/osquery/cypress/support/e2e.ts +++ b/x-pack/plugins/osquery/cypress/support/e2e.ts @@ -23,17 +23,17 @@ // *********************************************************** // force ESM in this module -import type { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/security-solution-plugin/scripts/run_cypress/utils'; - export {}; -import 'cypress-react-selector'; +// @ts-expect-error ts(2306) module has some interesting ways of importing, see https://github.com/cypress-io/cypress/blob/0871b03c5b21711cd23056454da8f23dcaca4950/npm/grep/README.md#support-file import registerCypressGrep from '@cypress/grep'; +registerCypressGrep(); -import { login } from '../../../../test_serverless/functional/test_suites/security/cypress/tasks/login'; +import type { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/security-solution-plugin/scripts/run_cypress/utils'; import type { ServerlessRoleName } from './roles'; -registerCypressGrep(); +import 'cypress-react-selector'; +import { login } from '../../../../test_serverless/functional/test_suites/security/cypress/tasks/login'; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts index 72fe34c7bce42..13ab8d4105b3e 100644 --- a/x-pack/plugins/osquery/cypress/tasks/navigation.ts +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts @@ -21,7 +21,11 @@ export const navigateTo = (page: string, opts?: Partial) = // There's a security warning toast that seemingly makes ui elements in the bottom right unavailable, so we close it closeToastIfVisible(); - cy.waitForReact(); + cy.waitForReact( + 10000, + Cypress.env('cypress-react-selector')?.root, + '../../../node_modules/resq/dist/index.js' + ); }; export const openNavigationFlyout = () => { diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json index cb468e0fb8893..ddd53a1ad7156 100644 --- a/x-pack/plugins/osquery/cypress/tsconfig.json +++ b/x-pack/plugins/osquery/cypress/tsconfig.json @@ -2,12 +2,13 @@ "extends": "../../../../tsconfig.base.json", "include": [ "**/*", - "../cypress.config.ts", - "../serverless_cypress.config.ts", + "./cypress.config.ts", + "./serverless_cypress.config.ts", "../../../test_serverless/shared/lib" ], "exclude": [ - "target/**/*" + "target/**/*", + "../../../test_serverless/shared/lib/security/default_http_headers.ts" ], "compilerOptions": { "outDir": "target/types", diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json index 32db6010c6573..e9ab128dd45fb 100644 --- a/x-pack/plugins/osquery/package.json +++ b/x-pack/plugins/osquery/package.json @@ -7,10 +7,10 @@ "scripts": { "cypress:burn": "yarn cypress:run --env burn=2 --headed", "cypress:changed-specs-only": "yarn cypress:run --changed-specs-only --env burn=2", - "cypress": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../osquery/cypress.config.ts --ftr-config-file ../../../x-pack/test/osquery_cypress/cli_config", + "cypress": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../osquery/cypress/cypress.config.ts --ftr-config-file ../../../x-pack/test/osquery_cypress/cli_config", "cypress:open": "yarn cypress open", "cypress:run": "yarn cypress run", - "cypress:serverless": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../osquery/serverless_cypress.config.ts --ftr-config-file ../../../x-pack/test/osquery_cypress/serverless_cli_config", + "cypress:serverless": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../osquery/cypress/serverless_cypress.config.ts --ftr-config-file ../../../x-pack/test/osquery_cypress/serverless_cli_config", "cypress:serverless:open": "yarn cypress:serverless open", "cypress:serverless:run": "yarn cypress:serverless run", "nyc": "../../../node_modules/.bin/nyc report --reporter=text-summary", diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index d2344a2581df8..6516c4241f0df 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -1,12 +1,10 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "target/types", + "outDir": "target/types" }, "exclude": [ - "cypress.config.ts", - "serverless_cypress.config.ts", - "target/**/*", + "target/**/*" ], "include": [ // add all the folders contains files to be compiled @@ -15,8 +13,6 @@ "scripts/**/*", "scripts/**/**.json", "server/**/*", - "cypress.config.ts", - "serverless_cypress.config.ts", "../../../typings/**/*", // ECS and Osquery schema files "public/common/schemas/*/**.json", @@ -77,6 +73,6 @@ "@kbn/core-saved-objects-server", "@kbn/monaco", "@kbn/io-ts-utils", - "@kbn/shared-ux-page-kibana-template", + "@kbn/shared-ux-page-kibana-template" ] } diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts index 89ca14ec0c28f..7ff366ea2cd14 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts @@ -48,7 +48,7 @@ const sendApiLoginRequest = ( }; interface CyLoginTask { - (user?: ServerlessRoleName): ReturnType; + (user?: ServerlessRoleName | 'elastic'): ReturnType; /** * Login using any username/password From 905dcfae51250c67fcbeb7938d8ddbd1115451c6 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 28 Sep 2023 18:46:45 -0400 Subject: [PATCH 10/10] skip failing test suite (#150249) --- .../functional/tests/discover_integration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_tagging/functional/tests/discover_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/discover_integration.ts index 4529bf260fef1..7a82d0aec5d34 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/discover_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/discover_integration.ts @@ -52,7 +52,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }; - describe('discover integration', () => { + // Failing: See https://github.com/elastic/kibana/issues/150249 + describe.skip('discover integration', () => { before(async () => { await kibanaServer.importExport.load( 'x-pack/test/saved_object_tagging/common/fixtures/es_archiver/discover/data.json'