mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
9900c0875c
commit
f0050dbc70
3 changed files with 242 additions and 63 deletions
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue