[Cloud Posture] fix deployment type selection (#137313) (#138379)

(cherry picked from commit ce090a508a)

Co-authored-by: Or Ouziel <or.ouziel@elastic.co>
This commit is contained in:
Kibana Machine 2022-08-09 08:29:26 -04:00 committed by GitHub
parent 7a26538af2
commit cf43c0ac11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 647 additions and 0 deletions

View file

@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiComboBox,
EuiToolTip,
EuiFormRow,
EuiIcon,
type EuiComboBoxOptionOption,
EuiDescribedFormGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { CIS_INTEGRATION_INPUTS_MAP } from '../../../common/constants';
export type InputType = keyof typeof CIS_INTEGRATION_INPUTS_MAP;
interface Props {
type: InputType;
onChange?: (type: InputType) => void;
isDisabled?: boolean;
}
const kubeDeployOptions: Array<EuiComboBoxOptionOption<InputType>> = [
{
value: 'cloudbeat/vanilla',
label: i18n.translate(
'xpack.csp.createPackagePolicy.stepConfigure.integrationSettingsSection.vanillaKubernetesDeploymentOption',
{ defaultMessage: 'Unmanaged Kubernetes' }
),
},
{
value: 'cloudbeat/eks',
label: i18n.translate(
'xpack.csp.createPackagePolicy.stepConfigure.integrationSettingsSection.eksKubernetesDeploymentOption',
{ defaultMessage: 'EKS (Elastic Kubernetes Service)' }
),
},
];
const KubernetesDeploymentFieldLabel = () => (
<EuiToolTip
content={
<FormattedMessage
id="xpack.csp.createPackagePolicy.stepConfigure.integrationSettingsSection.kubernetesDeploymentLabelTooltip"
defaultMessage="Select your Kubernetes deployment type"
/>
}
>
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
<EuiFlexItem grow style={{ flexDirection: 'row' }}>
<FormattedMessage
id="xpack.csp.createPackagePolicy.stepConfigure.integrationSettingsSection.kubernetesDeploymentLabel"
defaultMessage="Kubernetes Deployment"
/>
&nbsp;
<EuiIcon size="m" color="subdued" type="questionInCircle" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
);
export const DeploymentTypeSelect = ({ type, isDisabled, onChange }: Props) => (
<EuiDescribedFormGroup title={<div />}>
<EuiFormRow label={<KubernetesDeploymentFieldLabel />}>
<EuiComboBox
singleSelection={{ asPlainText: true }}
options={kubeDeployOptions}
selectedOptions={kubeDeployOptions.filter((o) => o.value === type)}
isDisabled={isDisabled}
onChange={(options) => !isDisabled && onChange?.(options[0].value!)}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);

View file

@ -0,0 +1,107 @@
/*
* 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 { EuiFormRow, EuiFieldText, EuiDescribedFormGroup, EuiText, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import { i18n } from '@kbn/i18n';
import { isEksInput } from './utils';
export const eksVars = [
{
id: 'access_key_id',
label: i18n.translate(
'xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.accessKeyIdFieldLabel',
{ defaultMessage: 'Access key ID' }
),
},
{
id: 'secret_access_key',
label: i18n.translate(
'xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.secretAccessKeyFieldLabel',
{ defaultMessage: 'Secret access key' }
),
},
{
id: 'session_token',
label: i18n.translate(
'xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.sessionTokenFieldLabel',
{ defaultMessage: 'Session token' }
),
},
] as const;
type EksVars = typeof eksVars;
type EksVarId = EksVars[number]['id'];
type EksFormVars = { [K in EksVarId]: string };
interface Props {
onChange(key: EksVarId, value: string): void;
inputs: NewPackagePolicyInput[];
}
const getEksVars = (input?: NewPackagePolicyInput): EksFormVars => {
const vars = input?.streams?.[0]?.vars;
return {
access_key_id: vars?.access_key_id.value || '',
secret_access_key: vars?.secret_access_key.value || '',
session_token: vars?.session_token.value || '',
};
};
export const EksFormWrapper = ({ onChange, inputs }: Props) => (
<>
<EuiSpacer size="m" />
<EksForm inputs={inputs} onChange={onChange} />
</>
);
const EksForm = ({ onChange, inputs }: Props) => {
const values = getEksVars(inputs.find(isEksInput));
const eksFormTitle = (
<h4>
<FormattedMessage
id="xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.awsCredentialsTitle"
defaultMessage="AWS Credentials"
/>
</h4>
);
const eksFormDescription = (
<>
<FormattedMessage
id="xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.awsCredentialsNote"
defaultMessage="If you choose not to provide credentials, only a subset of the benchmark rules will be evaluated against your cluster(s)."
/>
</>
);
return (
<EuiDescribedFormGroup title={eksFormTitle} description={eksFormDescription}>
{eksVars.map((field) => (
<EuiFormRow
key={field.id}
label={field.label}
labelAppend={
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.csp.createPackagePolicy.eksIntegrationSettingsSection.optionalField"
defaultMessage="Optional"
/>
</EuiText>
}
>
<EuiFieldText
value={values[field.id]}
onChange={(event) => onChange(field.id, event.target.value)}
/>
</EuiFormRow>
))}
</EuiDescribedFormGroup>
);
};

View file

@ -0,0 +1,139 @@
/*
* 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 { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
import { BenchmarkId } from '../../../common/types';
export const getCspNewPolicyMock = (type: BenchmarkId = 'cis_k8s'): NewPackagePolicy => ({
name: 'some-cloud_security_posture-policy',
description: '',
namespace: 'default',
policy_id: '',
enabled: true,
output_id: '',
inputs: [
{
type: 'cloudbeat/vanilla',
policy_template: 'kspm',
enabled: type === 'cis_k8s',
streams: [
{
enabled: true,
data_stream: {
type: 'logs',
dataset: 'cloud_security_posture.findings',
},
},
],
},
{
type: 'cloudbeat/eks',
policy_template: 'kspm',
enabled: type === 'cis_eks',
streams: [
{
enabled: false,
data_stream: {
type: 'logs',
dataset: 'cloud_security_posture.findings',
},
vars: {
access_key_id: {
type: 'text',
},
secret_access_key: {
type: 'text',
},
session_token: {
type: 'text',
},
},
},
],
},
],
package: {
name: 'cloud_security_posture',
title: 'Kubernetes Security Posture Management',
version: '0.0.21',
},
vars: {
dataYaml: {
type: 'yaml',
},
},
});
export const getCspPolicyMock = (type: BenchmarkId = 'cis_k8s'): PackagePolicy => ({
...getCspNewPolicyMock(type),
id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1',
version: 'abcd',
revision: 1,
updated_at: '2020-06-25T16:03:38.159292',
updated_by: 'kibana',
created_at: '2020-06-25T16:03:38.159292',
created_by: 'kibana',
inputs: [
{
policy_template: 'kspm',
streams: [
{
compiled_stream: {
data_yaml: {
activated_rules: {
cis_k8s: [],
cis_eks: ['cis_3_1_4'],
},
},
name: 'Findings',
processors: [{ add_cluster_id: null }],
},
data_stream: {
type: 'logs',
dataset: 'cloud_security_posture.findings',
},
id: 'cloudbeat/vanilla-cloud_security_posture.findings-de97ed6f-5024-46af-a4f9-9acd7bd012d8',
enabled: true,
},
],
type: 'cloudbeat/vanilla',
enabled: type === 'cis_k8s',
},
{
policy_template: 'kspm',
streams: [
{
data_stream: {
type: 'logs',
dataset: 'cloud_security_posture.findings',
},
vars: {
access_key_id: {
type: 'text',
},
session_token: {
type: 'text',
},
secret_access_key: {
type: 'text',
},
},
id: 'cloudbeat/eks-cloud_security_posture.findings-de97ed6f-5024-46af-a4f9-9acd7bd012d8',
enabled: false,
},
],
type: 'cloudbeat/eks',
enabled: type === 'cis_eks',
},
],
vars: {
dataYaml: {
type: 'yaml',
value: 'data_yaml:\n activated_rules:\n cis_k8s: []\n cis_eks:\n - cis_3_1_4\n ',
},
},
});

View file

@ -0,0 +1,93 @@
/*
* 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 { fireEvent, render } from '@testing-library/react';
import CspCreatePolicyExtension from './policy_extension_create';
import { eksVars } from './eks_form';
import Chance from 'chance';
import { TestProvider } from '../../test/test_provider';
import userEvent from '@testing-library/user-event';
import { getCspNewPolicyMock } from './mocks';
// ensures that fields appropriately match to their label
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
// ensures that fields appropriately match to their label
jest.mock('@elastic/eui/lib/services/accessibility', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility'),
useGeneratedHtmlId: () => `id-${Math.random()}`,
}));
const chance = new Chance();
describe('<CspCreatePolicyExtension />', () => {
const onChange = jest.fn();
const WrappedComponent = ({ newPolicy = getCspNewPolicyMock() }) => (
<TestProvider>
<CspCreatePolicyExtension newPolicy={newPolicy} onChange={onChange} />
</TestProvider>
);
beforeEach(() => {
onChange.mockClear();
});
it('renders non-disabled <DeploymentTypeSelect/>', () => {
const { getByLabelText } = render(<WrappedComponent />);
const input = getByLabelText('Kubernetes Deployment') as HTMLInputElement;
expect(input).toBeInTheDocument();
expect(input).not.toBeDisabled();
});
it('renders non-disabled <EksForm/>', () => {
const { getByLabelText } = render(
<WrappedComponent newPolicy={getCspNewPolicyMock('cis_eks')} />
);
eksVars.forEach((eksVar) => {
expect(getByLabelText(eksVar.label)).toBeInTheDocument();
expect(getByLabelText(eksVar.label)).not.toBeDisabled();
});
});
it('handles updating deployment type', () => {
const { getByLabelText } = render(<WrappedComponent />);
const input = getByLabelText('Kubernetes Deployment') as HTMLInputElement;
userEvent.type(input, 'EKS (Elastic Kubernetes Service){enter}');
expect(onChange).toBeCalledWith({
isValid: true,
updatedPolicy: getCspNewPolicyMock('cis_eks'),
});
});
it('handles updating EKS vars', () => {
const { getByLabelText } = render(
<WrappedComponent newPolicy={getCspNewPolicyMock('cis_eks')} />
);
const randomValues = chance.unique(chance.string, eksVars.length);
eksVars.forEach((eksVar, i) => {
const eksVarInput = getByLabelText(eksVar.label) as HTMLInputElement;
fireEvent.change(eksVarInput, { target: { value: randomValues[i] } });
const policy = getCspNewPolicyMock('cis_eks');
policy.inputs[1].streams[0].vars![eksVar.id].value = randomValues[i];
expect(onChange).toBeCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
});
});

View file

@ -0,0 +1,39 @@
/*
* 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, { memo } from 'react';
import { EuiForm } from '@elastic/eui';
import type { PackagePolicyCreateExtensionComponentProps } from '@kbn/fleet-plugin/public';
import { CLOUDBEAT_EKS } from '../../../common/constants';
import { DeploymentTypeSelect, InputType } from './deployment_type_select';
import { EksFormWrapper } from './eks_form';
import { getEnabledInputType, getUpdatedDeploymentType, getUpdatedEksVar } from './utils';
export const CspCreatePolicyExtension = memo<PackagePolicyCreateExtensionComponentProps>(
({ newPolicy, onChange }) => {
const selectedDeploymentType = getEnabledInputType(newPolicy.inputs);
const updateDeploymentType = (inputType: InputType) =>
onChange(getUpdatedDeploymentType(newPolicy, inputType));
const updateEksVar = (key: string, value: string) =>
onChange(getUpdatedEksVar(newPolicy, key, value));
return (
<EuiForm>
<DeploymentTypeSelect type={selectedDeploymentType} onChange={updateDeploymentType} />
{selectedDeploymentType === CLOUDBEAT_EKS && (
<EksFormWrapper inputs={newPolicy.inputs} onChange={updateEksVar} />
)}
</EuiForm>
);
}
);
CspCreatePolicyExtension.displayName = 'CspCreatePolicyExtension';
// eslint-disable-next-line import/no-default-export
export { CspCreatePolicyExtension as default };

View file

@ -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 React from 'react';
import { fireEvent, render } from '@testing-library/react';
import CspEditPolicyExtension from './policy_extension_edit';
import { TestProvider } from '../../test/test_provider';
import { getCspNewPolicyMock, getCspPolicyMock } from './mocks';
import Chance from 'chance';
import { eksVars } from './eks_form';
const chance = new Chance();
// ensures that fields appropriately match to their label
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
// ensures that fields appropriately match to their label
jest.mock('@elastic/eui/lib/services/accessibility', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility'),
useGeneratedHtmlId: () => `id-${Math.random()}`,
}));
describe('<CspEditPolicyExtension />', () => {
const onChange = jest.fn();
const WrappedComponent = ({ policy = getCspPolicyMock(), newPolicy = getCspNewPolicyMock() }) => (
<TestProvider>
<CspEditPolicyExtension policy={policy} newPolicy={newPolicy} onChange={onChange} />
</TestProvider>
);
beforeEach(() => {
onChange.mockClear();
});
it('renders disabled <DeploymentTypeSelect/>', () => {
const { getByLabelText } = render(<WrappedComponent />);
const input = getByLabelText('Kubernetes Deployment') as HTMLInputElement;
expect(input).toBeInTheDocument();
expect(input).toBeDisabled();
});
it('renders non-disabled <EksForm/>', () => {
const { getByLabelText } = render(
<WrappedComponent newPolicy={getCspNewPolicyMock('cis_eks')} />
);
eksVars.forEach((eksVar) => {
expect(getByLabelText(eksVar.label)).toBeInTheDocument();
expect(getByLabelText(eksVar.label)).not.toBeDisabled();
});
});
it('handles updating EKS vars', () => {
const { getByLabelText } = render(
<WrappedComponent newPolicy={getCspNewPolicyMock('cis_eks')} />
);
const randomValues = chance.unique(chance.string, eksVars.length);
eksVars.forEach((eksVar, i) => {
const eksVarInput = getByLabelText(eksVar.label) as HTMLInputElement;
fireEvent.change(eksVarInput, { target: { value: randomValues[i] } });
const policy = getCspNewPolicyMock('cis_eks');
policy.inputs[1].streams[0].vars![eksVar.id].value = randomValues[i];
expect(onChange).toBeCalledWith({
isValid: true,
updatedPolicy: policy,
});
});
});
});

View file

@ -0,0 +1,36 @@
/*
* 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, { memo } from 'react';
import { EuiForm } from '@elastic/eui';
import type { PackagePolicyEditExtensionComponentProps } from '@kbn/fleet-plugin/public';
import { CLOUDBEAT_EKS } from '../../../common/constants';
import { DeploymentTypeSelect } from './deployment_type_select';
import { EksFormWrapper } from './eks_form';
import { getEnabledInputType, getUpdatedEksVar } from './utils';
export const CspEditPolicyExtension = memo<PackagePolicyEditExtensionComponentProps>(
({ newPolicy, onChange }) => {
const selectedDeploymentType = getEnabledInputType(newPolicy.inputs);
const updateEksVar = (key: string, value: string) =>
onChange(getUpdatedEksVar(newPolicy, key, value));
return (
<EuiForm>
<DeploymentTypeSelect type={selectedDeploymentType} isDisabled />
{selectedDeploymentType === CLOUDBEAT_EKS && (
<EksFormWrapper inputs={newPolicy.inputs} onChange={updateEksVar} />
)}
</EuiForm>
);
}
);
CspEditPolicyExtension.displayName = 'CspEditPolicyExtension';
// eslint-disable-next-line import/no-default-export
export { CspEditPolicyExtension as default };

View file

@ -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 type { NewPackagePolicy, NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import { CLOUDBEAT_EKS, CLOUDBEAT_VANILLA } from '../../../common/constants';
import type { InputType } from './deployment_type_select';
export const isEksInput = (input: NewPackagePolicyInput) => input.type === CLOUDBEAT_EKS;
export const getEnabledInputType = (inputs: NewPackagePolicy['inputs']): InputType =>
(inputs.find((input) => input.enabled)?.type as InputType) || CLOUDBEAT_VANILLA;
export const getUpdatedDeploymentType = (newPolicy: NewPackagePolicy, inputType: InputType) => ({
isValid: true, // TODO: add validations
updatedPolicy: {
...newPolicy,
inputs: newPolicy.inputs.map((item) => ({
...item,
enabled: item.type === inputType,
})),
},
});
export const getUpdatedEksVar = (newPolicy: NewPackagePolicy, key: string, value: string) => ({
isValid: true, // TODO: add validations
updatedPolicy: {
...newPolicy,
inputs: newPolicy.inputs.map((item) =>
isEksInput(item) ? getUpdatedStreamVars(item, key, value) : item
),
},
});
// TODO: remove access to first stream
const getUpdatedStreamVars = (item: NewPackagePolicyInput, key: string, value: string) => {
if (!item.streams[0]) return item;
return {
...item,
streams: [
{
...item.streams[0],
vars: {
...item.streams[0]?.vars,
[key]: {
...item.streams[0]?.vars?.[key],
value,
},
},
},
],
};
};

View file

@ -18,6 +18,10 @@ import type {
} from './types';
import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants';
const LazyCspEditPolicy = lazy(() => import('./components/fleet_extensions/policy_extension_edit'));
const LazyCspCreatePolicy = lazy(
() => import('./components/fleet_extensions/policy_extension_create')
);
const LazyCspCustomAssets = lazy(
() => import('./components/fleet_extensions/custom_assets_extension')
);
@ -47,6 +51,18 @@ export class CspPlugin
}
public start(core: CoreStart, plugins: CspClientPluginStartDeps): CspClientPluginStart {
plugins.fleet.registerExtension({
package: CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
view: 'package-policy-create',
Component: LazyCspCreatePolicy,
});
plugins.fleet.registerExtension({
package: CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
view: 'package-policy-edit',
Component: LazyCspEditPolicy,
});
plugins.fleet.registerExtension({
package: CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
view: 'package-detail-assets',