Skip to content

Commit

Permalink
[Cloud Security] [CIS GCP] GCP Organization option (elastic#166983)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
animehart and kibanamachine authored Sep 28, 2023
1 parent 859ae9e commit 8759b03
Show file tree
Hide file tree
Showing 16 changed files with 702 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -71,7 +74,21 @@ const GCPSetupInfoContent = () => (
</>
);

const GoogleCloudShellSetup = () => {
const GoogleCloudShellSetup = ({
fields,
onChange,
input,
}: {
fields: Array<GcpFields[keyof GcpFields] & { value: string; id: string }>;
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 (
<>
<EuiText
Expand All @@ -96,12 +113,22 @@ const GoogleCloudShellSetup = () => {
defaultMessage="Log into your Google Cloud Console"
/>
</li>
<li>
<FormattedMessage
id="xpack.csp.gcpIntegration.cloudShellSetupStep.save"
defaultMessage="Note down the GCP project ID of the project you wish to monitor"
/>
</li>
{accountType === GCP_ORGANIZATION_ACCOUNT ? (
<li>
<FormattedMessage
id="xpack.csp.gcpIntegration.organizationCloudShellSetupStep.save"
defaultMessage="Note down the GCP organization ID of the organization you wish to monitor and project ID where you want to provision resources for monitoring purposes and provide them in the input boxes below"
/>
</li>
) : (
<li>
<FormattedMessage
id="xpack.csp.gcpIntegration.cloudShellSetupStep.save"
defaultMessage="Note down the GCP project ID of the project you wish to monitor"
/>
</li>
)}

<li>
<FormattedMessage
id="xpack.csp.gcpIntegration.cloudShellSetupStep.launch"
Expand All @@ -111,6 +138,31 @@ const GoogleCloudShellSetup = () => {
</ol>
</EuiText>
<EuiSpacer size="l" />
<EuiForm component="form">
{organizationIdFields && accountType === GCP_ORGANIZATION_ACCOUNT && (
<EuiFormRow fullWidth label={gcpField.fields['gcp.organization_id'].label}>
<EuiFieldText
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID}
id={organizationIdFields.id}
fullWidth
value={organizationIdFields.value || ''}
onChange={(event) => onChange(organizationIdFields.id, event.target.value)}
/>
</EuiFormRow>
)}
{projectIdFields && (
<EuiFormRow fullWidth label={gcpField.fields['gcp.project_id'].label}>
<EuiFieldText
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.PROJECT_ID}
id={projectIdFields.id}
fullWidth
value={projectIdFields.value || ''}
onChange={(event) => onChange(projectIdFields.id, event.target.value)}
/>
</EuiFormRow>
)}
</EuiForm>
<EuiSpacer size="m" />
</>
);
};
Expand All @@ -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',
Expand Down Expand Up @@ -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<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_gcp' }>;
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]) => {
Expand Down Expand Up @@ -290,6 +345,10 @@ const useCloudShellUrl = ({
}, [newPolicy?.vars?.cloud_shell_url, newPolicy, packageInfo, setupFormat]);
};

export const getGcpCredentialsType = (
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_gcp' }>
): GcpCredentialsType | undefined => input.streams[0].vars?.setup_access.value;

export const GcpCredentialsForm = ({
input,
newPolicy,
Expand All @@ -298,16 +357,33 @@ 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) || '';
const isInvalid = semverLt(integrationVersionNumberOnly, MIN_VERSION_GCP_CIS);
const fieldsSnapshot = useRef({});
const lastSetupAccessType = useRef<string | undefined>(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,
Expand All @@ -316,31 +392,31 @@ 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 {
updatePolicy(
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
Expand All @@ -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 (
Expand All @@ -385,19 +447,29 @@ export const GcpCredentialsForm = ({
size="s"
options={getSetupFormatOptions()}
idSelected={setupFormat}
onChange={onSetupFormatChange}
onChange={(idSelected: SetupFormatGCP) =>
idSelected !== setupFormat && onSetupFormatChange(idSelected)
}
/>
<EuiSpacer size="l" />
{setupFormat === SETUP_ACCESS_MANUAL ? (
<GcpInputVarFields
{setupFormat === SETUP_ACCESS_CLOUD_SHELL ? (
<GoogleCloudShellSetup
fields={fields}
onChange={(key, value) =>
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }))
}
input={input}
/>
) : (
<GoogleCloudShellSetup />
<GcpInputVarFields
fields={fields}
onChange={(key, value) =>
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }))
}
isOrganization={isOrganization}
/>
)}

<EuiSpacer size="s" />
<ReadDocumentation url={cspIntegrationDocsNavigation.cspm.getStartedPath} />
<EuiSpacer />
Expand All @@ -408,13 +480,18 @@ export const GcpCredentialsForm = ({
const GcpInputVarFields = ({
fields,
onChange,
isOrganization,
}: {
fields: Array<GcpFields[keyof GcpFields] & { value: string; id: string }>;
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');
Expand All @@ -428,6 +505,17 @@ const GcpInputVarFields = ({
return (
<div>
<EuiForm component="form">
{organizationIdFields && isOrganization && (
<EuiFormRow fullWidth label={gcpField.fields['gcp.organization_id'].label}>
<EuiFieldText
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID}
id={organizationIdFields.id}
fullWidth
value={organizationIdFields.value || ''}
onChange={(event) => onChange(organizationIdFields.id, event.target.value)}
/>
</EuiFormRow>
)}
{projectIdFields && (
<EuiFormRow fullWidth label={gcpField.fields['gcp.project_id'].label}>
<EuiFieldText
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,11 @@ const getPolicyMock = (

const gcpVarsMock = {
'gcp.project_id': { type: 'text' },
'gcp.organization_id': { type: 'text' },
'gcp.credentials.file': { type: 'text' },
'gcp.credentials.json': { type: 'text' },
'gcp.credentials.type': { type: 'text' },
'gcp.account_type': { value: 'organization-account', type: 'text' },
};

const azureVarsMock = {
Expand Down
Loading

0 comments on commit 8759b03

Please sign in to comment.