[Cloud Security][Onboarding]GCP Onboarding - Manual (IMPROVEMENTS) (#162434)

## Summary

Addressing PR Comments + Improvements from my previous PR
(https://github.com/elastic/kibana/pull/161913) in this PR
This commit is contained in:
Rickyanto Ang 2023-07-31 08:38:59 -07:00 committed by GitHub
parent 9900c0875c
commit f0050dbc70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 242 additions and 63 deletions

View file

@ -4,7 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import semverLt from 'semver/functions/lt';
import semverCoerce from 'semver/functions/coerce';
import semverValid from 'semver/functions/valid';
import {
EuiFieldText,
EuiFormRow,
@ -25,6 +28,12 @@ import { RadioGroup } from './csp_boxed_radio_group';
import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils';
import { MIN_VERSION_GCP_CIS } from '../../common/constants';
export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = {
PROJECT_ID: 'project_id_test_id',
CREDENTIALS_TYPE: 'credentials_type_test_id',
CREDENTIALS_FILE: 'credentials_file_test_id',
CREDENTIALS_JSON: 'credentials_json_test_id',
};
type SetupFormatGCP = 'google_cloud_shell' | 'manual';
const GCPSetupInfoContent = () => (
<>
@ -49,7 +58,7 @@ const GCPSetupInfoContent = () => (
</>
);
/* NEED TO FIND THE REAL URL HERE LATER*/
/* NEED TO FIND THE REAL URL HERE LATER */
const DocsLink = (
<EuiText color={'subdued'} size="s">
<FormattedMessage
@ -66,15 +75,6 @@ const DocsLink = (
</EuiText>
);
const CredentialFileText = i18n.translate(
'xpack.csp.findings.gcpIntegration.gcpInputText.credentialFileText',
{ defaultMessage: 'Path to JSON file containing the credentials and key used to subscribe' }
);
const CredentialJSONText = i18n.translate(
'xpack.csp.findings.gcpIntegration.gcpInputText.credentialJSONText',
{ defaultMessage: 'JSON blob containing the credentials and key used to subscribe' }
);
type GcpCredentialsType = 'credentials_file' | 'credentials_json';
type GcpFields = Record<string, { label: string; type?: 'password' | 'text' }>;
interface GcpInputFields {
@ -90,32 +90,39 @@ const gcpField: GcpInputFields = {
type: 'text',
},
credentials_file: {
label: i18n.translate('xpack.csp.gcpIntegration.credentialsFileFieldLabel', {
defaultMessage: 'Credentials File',
label: i18n.translate('xpack.csp.findings.gcpIntegration.gcpInputText.credentialFileText', {
defaultMessage: 'Path to JSON file containing the credentials and key used to subscribe',
}),
type: 'text',
},
credentials_json: {
label: i18n.translate('xpack.csp.gcpIntegration.credentialsJSONFieldLabel', {
defaultMessage: 'Credentials JSON',
label: i18n.translate('xpack.csp.findings.gcpIntegration.gcpInputText.credentialJSONText', {
defaultMessage: 'JSON blob containing the credentials and key used to subscribe',
}),
type: 'text',
},
credentials_type: {
label: i18n.translate(
'xpack.csp.findings.gcpIntegration.gcpInputText.credentialSelectBoxTitle',
{ defaultMessage: 'Credential' }
),
type: 'text',
},
},
};
const credentialOptionsList = [
{
label: i18n.translate('xpack.csp.gcpIntegration.credentialsFileOption', {
text: i18n.translate('xpack.csp.gcpIntegration.credentialsFileOption', {
defaultMessage: 'Credentials File',
}),
text: 'Credentials File',
value: 'credentials-file',
},
{
label: i18n.translate('xpack.csp.gcpIntegration.credentialsjsonOption', {
text: i18n.translate('xpack.csp.gcpIntegration.credentialsJsonOption', {
defaultMessage: 'Credentials JSON',
}),
text: 'Credentials JSON',
value: 'credentials-json',
},
];
@ -140,7 +147,7 @@ const getSetupFormatOptions = (): Array<{
},
];
interface Props {
interface GcpFormProps {
newPolicy: NewPackagePolicy;
input: Extract<
NewPackagePolicyPostureInput,
@ -175,12 +182,12 @@ export const GcpCredentialsForm = ({
packageInfo,
setIsValid,
onChange,
}: Props) => {
}: GcpFormProps) => {
const fields = getInputVarsFields(input, gcpField.fields);
const validSemantic = semverValid(packageInfo.version);
const integrationVersionNumberOnly = semverCoerce(validSemantic) || '';
const isInvalid = semverLt(integrationVersionNumberOnly, MIN_VERSION_GCP_CIS);
useEffect(() => {
const isInvalid = packageInfo.version < MIN_VERSION_GCP_CIS;
setIsValid(!isInvalid);
onChange({
@ -190,7 +197,7 @@ export const GcpCredentialsForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input, packageInfo]);
if (packageInfo.version < MIN_VERSION_GCP_CIS) {
if (isInvalid) {
return (
<>
<EuiSpacer size="l" />
@ -240,52 +247,64 @@ const GcpInputVarFields = ({
fields: Array<GcpFields[keyof GcpFields] & { value: string; id: string }>;
onChange: (key: string, value: string) => void;
}) => {
const [credentialOption, setCredentialOption] = useState('Credentials File');
const targetFieldName = (id: string) => {
const getFieldById = (id: keyof GcpInputFields['fields']) => {
return fields.find((element) => element.id === id);
};
const projectIdFields = getFieldById('project_id');
const credentialsTypeFields = getFieldById('credentials_type') || credentialOptionsList[0];
const credentialFilesFields = getFieldById('credentials_file');
const credentialJSONFields = getFieldById('credentials_json');
const credentialFieldValue = credentialOptionsList[0].value;
const credentialJSONValue = credentialOptionsList[1].value;
return (
<div>
<EuiForm component="form">
<EuiFormRow fullWidth label={gcpField.fields.project_id.label}>
<EuiFieldText
id={targetFieldName('project_id')!.id}
fullWidth
value={targetFieldName('project_id')!.value || ''}
onChange={(event) => onChange(targetFieldName('project_id')!.id, event.target.value)}
/>
</EuiFormRow>
<EuiFormRow fullWidth label={'Credentials'}>
<EuiSelect
fullWidth
options={credentialOptionsList}
value={credentialOption}
onChange={(optionElem) => {
setCredentialOption(optionElem.target.value);
}}
/>
</EuiFormRow>
{credentialOption === 'Credentials File' && (
<EuiFormRow fullWidth label={CredentialFileText}>
{projectIdFields && (
<EuiFormRow fullWidth label={gcpField.fields.project_id.label}>
<EuiFieldText
id={targetFieldName('credentials_file')!.id}
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.PROJECT_ID}
id={projectIdFields.id}
fullWidth
value={targetFieldName('credentials_file')!.value || ''}
onChange={(event) =>
onChange(targetFieldName('credentials_file')!.id, event.target.value)
}
value={projectIdFields.value || ''}
onChange={(event) => onChange(projectIdFields.id, event.target.value)}
/>
</EuiFormRow>
)}
{credentialOption === 'Credentials JSON' && (
<EuiFormRow fullWidth label={CredentialJSONText}>
<EuiTextArea
id={targetFieldName('credentials_json')!.id}
{credentialFilesFields && credentialJSONFields && (
<EuiFormRow fullWidth label={gcpField.fields.credentials_type.label}>
<EuiSelect
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_TYPE}
fullWidth
value={targetFieldName('credentials_json')!.value || ''}
onChange={(event) =>
onChange(targetFieldName('credentials_json')!.id, event.target.value)
}
options={credentialOptionsList}
value={credentialsTypeFields?.value}
onChange={(optionElem) => {
onChange('credentials_type', optionElem.target.value);
}}
/>
</EuiFormRow>
)}
{credentialsTypeFields.value === credentialFieldValue && credentialFilesFields && (
<EuiFormRow fullWidth label={gcpField.fields.credentials_file.label}>
<EuiFieldText
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_FILE}
id={credentialFilesFields.id}
fullWidth
value={credentialFilesFields.value || ''}
onChange={(event) => onChange(credentialFilesFields.id, event.target.value)}
/>
</EuiFormRow>
)}
{credentialsTypeFields?.value === credentialJSONValue && credentialJSONFields && (
<EuiFormRow fullWidth label={gcpField.fields.credentials_json.label}>
<EuiTextArea
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_JSON}
id={credentialJSONFields.id}
fullWidth
value={credentialJSONFields.value || ''}
onChange={(event) => onChange(credentialJSONFields.id, event.target.value)}
/>
</EuiFormRow>
)}

View file

@ -18,6 +18,7 @@ import {
import type { PostureInput } from '../../../common/types';
export const getMockPolicyAWS = () => getPolicyMock(CLOUDBEAT_AWS, 'cspm', 'aws');
export const getMockPolicyGCP = () => getPolicyMock(CLOUDBEAT_GCP, 'cspm', 'gcp');
export const getMockPolicyK8s = () => getPolicyMock(CLOUDBEAT_VANILLA, 'kspm', 'self_managed');
export const getMockPolicyEKS = () => getPolicyMock(CLOUDBEAT_EKS, 'kspm', 'eks');
export const getMockPolicyVulnMgmtAWS = () =>
@ -79,6 +80,28 @@ export const getMockPackageInfoCspmAWS = (packageVersion = '1.5.0') => {
} as PackageInfo;
};
export const getMockPackageInfoCspmGCP = (packageVersion = '1.5.0') => {
return {
version: packageVersion,
name: 'cspm',
policy_templates: [
{
title: '',
description: '',
name: 'cspm',
inputs: [
{
type: CLOUDBEAT_GCP,
title: 'GCP',
description: '',
vars: [{}],
},
],
},
],
} as PackageInfo;
};
const getPolicyMock = (
type: PostureInput,
posture: string,
@ -106,6 +129,12 @@ const getPolicyMock = (
'aws.credentials.type': { value: 'assume_role', type: 'text' },
};
const gcpVarsMock = {
project_id: { type: 'text' },
credentials_file: { type: 'text' },
credentials_json: { type: 'text' },
};
const dataStream = { type: 'logs', dataset: 'cloud_security_posture.findings' };
return {
@ -145,8 +174,8 @@ const getPolicyMock = (
{
type: CLOUDBEAT_GCP,
policy_template: 'cspm',
enabled: false,
streams: [{ enabled: false, data_stream: dataStream }],
enabled: type === CLOUDBEAT_GCP,
streams: [{ enabled: type === CLOUDBEAT_GCP, data_stream: dataStream, vars: gcpVarsMock }],
},
{
type: CLOUDBEAT_AZURE,

View file

@ -14,9 +14,11 @@ import {
import { TestProvider } from '../../test/test_provider';
import {
getMockPackageInfoCspmAWS,
getMockPackageInfoCspmGCP,
getMockPackageInfoVulnMgmtAWS,
getMockPolicyAWS,
getMockPolicyEKS,
getMockPolicyGCP,
getMockPolicyK8s,
getMockPolicyVulnMgmtAWS,
} from './mocks';
@ -28,11 +30,12 @@ import type {
} from '@kbn/fleet-plugin/common';
import userEvent from '@testing-library/user-event';
import { getPosturePolicy } from './utils';
import { CLOUDBEAT_AWS, CLOUDBEAT_EKS } from '../../../common/constants';
import { CLOUDBEAT_AWS, CLOUDBEAT_EKS, CLOUDBEAT_GCP } from '../../../common/constants';
import { useParams } from 'react-router-dom';
import { createReactQueryResponse } from '../../test/fixtures/react_query';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { usePackagePolicyList } from '../../common/api/use_package_policy_list';
import { CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS } from './gcp_credential_form';
// mock useParams
jest.mock('react-router-dom', () => ({
@ -974,4 +977,132 @@ describe('<CspPolicyTemplateForm />', () => {
});
});
});
describe('GCP Credentials input fields', () => {
it(`renders ${CLOUDBEAT_GCP} Not supported when version is not at least version 1.5.0`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_type: { value: 'credentials-file' },
});
const { getByText } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP('1.3.1')} />
);
expect(onChange).toHaveBeenCalledWith({
isValid: false,
updatedPolicy: policy,
});
expect(
getByText(
'CIS GCP is not supported on the current Integration version, please upgrade your integration to the latest version to use CIS GCP'
)
).toBeInTheDocument();
});
it(`renders ${CLOUDBEAT_GCP} Credentials File fields`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_type: { value: 'credentials-file' },
});
const { getByLabelText, getByRole } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />
);
expect(getByRole('option', { name: 'Credentials File', selected: true })).toBeInTheDocument();
expect(
getByLabelText('Path to JSON file containing the credentials and key used to subscribe')
).toBeInTheDocument();
});
it(`updates ${CLOUDBEAT_GCP} Credentials File fields`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_type: { value: 'credentials-file' },
});
const { rerender, getByTestId } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />
);
userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.PROJECT_ID), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
project_id: { value: 'a' },
});
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
rerender(<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />);
userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_FILE), 'b');
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_file: { value: 'b' },
});
expect(onChange).toHaveBeenNthCalledWith(5, {
isValid: true,
updatedPolicy: policy,
});
});
it(`renders ${CLOUDBEAT_GCP} Credentials JSON fields`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_type: { value: 'credentials-json' },
});
const { getByLabelText, getByRole } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />
);
expect(getByRole('option', { name: 'Credentials JSON', selected: true })).toBeInTheDocument();
expect(
getByLabelText('JSON blob containing the credentials and key used to subscribe')
).toBeInTheDocument();
});
it(`updates ${CLOUDBEAT_GCP} Credentials JSON fields`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_type: { value: 'credentials-json' },
});
const { rerender, getByTestId } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />
);
userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.PROJECT_ID), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
project_id: { value: 'a' },
});
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
rerender(<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />);
userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_JSON), 'b');
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_json: { value: 'b' },
});
expect(onChange).toHaveBeenNthCalledWith(5, {
isValid: true,
updatedPolicy: policy,
});
});
});
});