Skip to content

Commit

Permalink
[Cloud Security] add GCP support for agentless (elastic#177965)
Browse files Browse the repository at this point in the history
## Summary

Part of:
- elastic/security-team#8040

Adding support for GCP for Agentless. Specifics:
- only JSON blob credentials type is supported
- in contrast to Agent-based, in "GCP organisation" option there is no
need to provide to `Project ID` field as it's not required for Agentless

## Screencast

[screencast-github.com-2024.03.07-10_25_43.webm](https://github.com/elastic/kibana/assets/478762/cae1483c-20de-48f5-9814-b6510c1482da)

## how to test
The simplest way is to deploy the Kibana image built for this PR to dev
MKI env, following this documentation
https://docs.elastic.dev/kibana-dev-docs/serverless/custom-kibana-image-on-serverless

I tested both Org and Single Account set up with real credentials of
Cloud Security Google Cloud account, got findings in the dev MKI
environments

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
maxcold authored Mar 11, 2024
1 parent 03ce083 commit 9500c3b
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,24 @@ 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_old';

import { GcpCredentialsType } from '../../../../common/types_old';
import {
CLOUDBEAT_GCP,
SETUP_ACCESS_CLOUD_SHELL,
SETUP_ACCESS_MANUAL,
} from '../../../common/constants';
import { CspRadioOption, RadioGroup } from './csp_boxed_radio_group';
} from '../../../../common/constants';
import { CspRadioOption, RadioGroup } from '../csp_boxed_radio_group';
import {
getCspmCloudShellDefaultValue,
getPosturePolicy,
NewPackagePolicyPostureInput,
} from './utils';
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';
} from '../utils';
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';
import { GCP_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ } from '../../test_subjects';

export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = {
GOOGLE_CLOUD_SHELL_SETUP: 'google_cloud_shell_setup_test_id',
Expand All @@ -51,7 +53,7 @@ export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = {
CREDENTIALS_JSON: 'credentials_json_test_id',
};
type SetupFormatGCP = 'google_cloud_shell' | 'manual';
const GCPSetupInfoContent = () => (
export const GCPSetupInfoContent = () => (
<>
<EuiHorizontalRule margin="xl" />
<EuiTitle size="xs">
Expand Down Expand Up @@ -238,19 +240,19 @@ const getSetupFormatOptions = (): CspRadioOption[] => [
defaultMessage: 'Google Cloud Shell',
}),
disabled: false,
testId: 'gcpGoogleCloudShellOptionTestId',
testId: GCP_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ.CLOUD_SHELL,
},
{
id: SETUP_ACCESS_MANUAL,
label: i18n.translate('xpack.csp.gcpIntegration.setupFormatOptions.manual', {
defaultMessage: 'Manual',
}),
disabled: false,
testId: 'gcpManualOptionTestId',
testId: GCP_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ.MANUAL,
},
];

interface GcpFormProps {
export interface GcpFormProps {
newPolicy: NewPackagePolicy;
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_gcp' }>;
updatePolicy(updatedPolicy: NewPackagePolicy): void;
Expand Down Expand Up @@ -486,7 +488,7 @@ export const GcpCredentialsForm = ({
);
};

const GcpInputVarFields = ({
export const GcpInputVarFields = ({
fields,
onChange,
isOrganization,
Expand All @@ -511,7 +513,10 @@ const GcpInputVarFields = ({
const credentialFieldValue = credentialOptionsList[0].value;
const credentialJSONValue = credentialOptionsList[1].value;

const credentialsTypeValue = credentialsTypeFields?.value || credentialOptionsList[0].value;
const credentialsTypeValue =
credentialsTypeFields?.value ||
(credentialFilesFields && credentialFieldValue) ||
(credentialJSONFields && credentialJSONValue);

return (
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { EuiSpacer } from '@elastic/eui';

import {
GcpFormProps,
GCPSetupInfoContent,
GcpInputVarFields,
gcpField,
getInputVarsFields,
} from './gcp_credential_form';
import { getPosturePolicy } from '../utils';
import { ReadDocumentation } from '../aws_credentials_form/aws_credentials_form';
import { cspIntegrationDocsNavigation } from '../../../common/navigation/constants';

export const GcpCredentialsFormAgentless = ({
input,
newPolicy,
updatePolicy,
disabled,
}: GcpFormProps) => {
const accountType = input.streams?.[0]?.vars?.['gcp.account_type']?.value;
const isOrganization = accountType === 'organization-account';
const organizationFields = ['gcp.organization_id', 'gcp.credentials.json'];
const singleAccountFields = ['gcp.project_id', 'gcp.credentials.json'];

/*
For Agentless only JSON credentials type is supported.
Also in case of organisation setup, project_id is not required in contrast to Agent-based.
*/
const fields = getInputVarsFields(input, gcpField.fields).filter((field) => {
if (isOrganization) {
return organizationFields.includes(field.id);
} else {
return singleAccountFields.includes(field.id);
}
});

return (
<>
<GCPSetupInfoContent />
<EuiSpacer size="l" />
<GcpInputVarFields
disabled={disabled}
fields={fields}
onChange={(key, value) =>
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }))
}
isOrganization={isOrganization}
/>
<EuiSpacer size="s" />
<ReadDocumentation url={cspIntegrationDocsNavigation.cspm.getStartedPath} />
<EuiSpacer />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/
import type { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import type { PackageInfo } from '@kbn/fleet-plugin/common';
import type { PackageInfo, PackagePolicyConfigRecord } from '@kbn/fleet-plugin/common';
import { createNewPackagePolicyMock, createAgentPolicyMock } from '@kbn/fleet-plugin/common/mocks';
import {
CLOUDBEAT_GCP,
Expand All @@ -17,11 +17,15 @@ import {
} from '../../../common/constants';
import type { PostureInput } from '../../../common/types_old';

export const getMockPolicyAWS = () => getPolicyMock(CLOUDBEAT_AWS, 'cspm', 'aws');
export const getMockPolicyGCP = () => getPolicyMock(CLOUDBEAT_GCP, 'cspm', 'gcp');
export const getMockPolicyAzure = () => getPolicyMock(CLOUDBEAT_AZURE, 'cspm', 'azure');
export const getMockPolicyAWS = (vars?: PackagePolicyConfigRecord) =>
getPolicyMock(CLOUDBEAT_AWS, 'cspm', 'aws', vars);
export const getMockPolicyGCP = (vars?: PackagePolicyConfigRecord) =>
getPolicyMock(CLOUDBEAT_GCP, 'cspm', 'gcp', vars);
export const getMockPolicyAzure = (vars?: PackagePolicyConfigRecord) =>
getPolicyMock(CLOUDBEAT_AZURE, 'cspm', 'azure', vars);
export const getMockPolicyK8s = () => getPolicyMock(CLOUDBEAT_VANILLA, 'kspm', 'self_managed');
export const getMockPolicyEKS = () => getPolicyMock(CLOUDBEAT_EKS, 'kspm', 'eks');
export const getMockPolicyEKS = (vars?: PackagePolicyConfigRecord) =>
getPolicyMock(CLOUDBEAT_EKS, 'kspm', 'eks', vars);
export const getMockPolicyVulnMgmtAWS = () =>
getPolicyMock(CLOUDBEAT_VULN_MGMT_AWS, 'vuln_mgmt', 'aws');
export const getMockAgentlessAgentPolicy = () => {
Expand Down Expand Up @@ -131,7 +135,8 @@ export const getMockPackageInfoCspmAzure = (packageVersion = '1.6.0') => {
const getPolicyMock = (
type: PostureInput,
posture: string,
deployment: string
deployment: string,
vars: object = {}
): NewPackagePolicy => {
const mockPackagePolicy = createNewPackagePolicyMock();

Expand Down Expand Up @@ -204,26 +209,48 @@ const getPolicyMock = (
type: CLOUDBEAT_EKS,
policy_template: 'kspm',
enabled: type === CLOUDBEAT_EKS,
streams: [{ enabled: type === CLOUDBEAT_EKS, data_stream: dataStream, vars: eksVarsMock }],
streams: [
{
enabled: type === CLOUDBEAT_EKS,
data_stream: dataStream,
vars: { ...eksVarsMock, ...vars },
},
],
},
{
type: CLOUDBEAT_AWS,
policy_template: 'cspm',
enabled: type === CLOUDBEAT_AWS,
streams: [{ enabled: type === CLOUDBEAT_AWS, data_stream: dataStream, vars: awsVarsMock }],
streams: [
{
enabled: type === CLOUDBEAT_AWS,
data_stream: dataStream,
vars: { ...awsVarsMock, ...vars },
},
],
},
{
type: CLOUDBEAT_GCP,
policy_template: 'cspm',
enabled: type === CLOUDBEAT_GCP,
streams: [{ enabled: type === CLOUDBEAT_GCP, data_stream: dataStream, vars: gcpVarsMock }],
streams: [
{
enabled: type === CLOUDBEAT_GCP,
data_stream: dataStream,
vars: { ...gcpVarsMock, ...vars },
},
],
},
{
type: CLOUDBEAT_AZURE,
policy_template: 'cspm',
enabled: false,
streams: [
{ enabled: type === CLOUDBEAT_AZURE, data_stream: dataStream, vars: azureVarsMock },
{
enabled: type === CLOUDBEAT_AZURE,
data_stream: dataStream,
vars: { ...azureVarsMock, ...vars },
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ 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';
import { CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS } from './gcp_credentials_form/gcp_credential_form';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import {
AWS_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ,
AWS_CREDENTIALS_TYPE_SELECTOR_TEST_SUBJ,
CIS_GCP_OPTION_TEST_SUBJ,
GCP_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ,
SETUP_TECHNOLOGY_SELECTOR_ACCORDION_TEST_SUBJ,
SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ,
} from '../test_subjects';
Expand Down Expand Up @@ -1489,24 +1491,107 @@ describe('<CspPolicyTemplateForm />', () => {
});
});

it('should not render setup technology selector for KSPM', () => {
it('should render setup technology selector for GCP for organisation account type', async () => {
const agentlessPolicy = getMockAgentlessAgentPolicy();
const newPackagePolicy = getMockPolicyEKS();
const newPackagePolicy = getMockPolicyGCP();

const { queryByTestId } = render(
<WrappedComponent newPolicy={newPackagePolicy} agentlessPolicy={agentlessPolicy} />
const { getByTestId, queryByTestId, getByRole } = render(
<WrappedComponent
newPolicy={newPackagePolicy}
agentlessPolicy={agentlessPolicy}
packageInfo={{ version: '1.6.0' } as PackageInfo}
/>
);

// navigate to GCP
const gcpSelectorButton = getByTestId(CIS_GCP_OPTION_TEST_SUBJ);
userEvent.click(gcpSelectorButton);

const setupTechnologySelectorAccordion = queryByTestId(
SETUP_TECHNOLOGY_SELECTOR_ACCORDION_TEST_SUBJ
);
const setupTechnologySelector = getByTestId(SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ);
const orgIdField = queryByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID);
const projectIdField = queryByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.PROJECT_ID);
const credentialsJsonField = queryByTestId(
CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_JSON
);
const credentialsTypSelector = queryByTestId(
CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_TYPE
);
const credentialsFileField = queryByTestId(
CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_FILE
);

expect(setupTechnologySelectorAccordion).not.toBeInTheDocument();
// default state for GCP with the Org selected
expect(setupTechnologySelectorAccordion).toBeInTheDocument();
expect(setupTechnologySelector).toBeInTheDocument();
expect(setupTechnologySelector).toHaveTextContent(/agentless/i);
expect(orgIdField).toBeInTheDocument();
expect(credentialsJsonField).toBeInTheDocument();
expect(projectIdField).not.toBeInTheDocument();
expect(credentialsTypSelector).not.toBeInTheDocument();
expect(credentialsFileField).not.toBeInTheDocument();

// select agent-based and check for cloudformation option
userEvent.click(setupTechnologySelector);
const agentBasedOption = getByRole('option', { name: /agent-based/i });
await waitForEuiPopoverOpen();
userEvent.click(agentBasedOption);
await waitFor(() => {
expect(getByTestId(GCP_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ.CLOUD_SHELL)).toBeInTheDocument();
expect(getByTestId(GCP_CREDENTIALS_TYPE_OPTIONS_TEST_SUBJ.MANUAL)).toBeInTheDocument();
});
});

it('should not render setup technology selector for CNVM', () => {
it('should render setup technology selector for GCP for single-account', async () => {
const agentlessPolicy = getMockAgentlessAgentPolicy();
const newPackagePolicy = getMockPolicyVulnMgmtAWS();
const newPackagePolicy = getMockPolicyGCP({
'gcp.account_type': { value: GCP_SINGLE_ACCOUNT, type: 'text' },
});

const { getByTestId, queryByTestId } = render(
<WrappedComponent
newPolicy={newPackagePolicy}
agentlessPolicy={agentlessPolicy}
packageInfo={{ version: '1.6.0' } as PackageInfo}
/>
);

// navigate to GCP
const gcpSelectorButton = getByTestId(CIS_GCP_OPTION_TEST_SUBJ);
userEvent.click(gcpSelectorButton);

const setupTechnologySelectorAccordion = queryByTestId(
SETUP_TECHNOLOGY_SELECTOR_ACCORDION_TEST_SUBJ
);
const setupTechnologySelector = queryByTestId(SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ);
const orgIdField = queryByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID);
const projectIdField = queryByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.PROJECT_ID);
const credentialsJsonField = queryByTestId(
CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_JSON
);
const credentialsTypSelector = queryByTestId(
CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_TYPE
);
const credentialsFileField = queryByTestId(
CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_FILE
);

// default state for GCP with the Org selected
expect(setupTechnologySelectorAccordion).toBeInTheDocument();
expect(setupTechnologySelector).toBeInTheDocument();
expect(setupTechnologySelector).toHaveTextContent(/agentless/i);
expect(orgIdField).not.toBeInTheDocument();
expect(credentialsJsonField).toBeInTheDocument();
expect(projectIdField).toBeInTheDocument();
expect(credentialsTypSelector).not.toBeInTheDocument();
expect(credentialsFileField).not.toBeInTheDocument();
});

it('should not render setup technology selector for KSPM', () => {
const agentlessPolicy = getMockAgentlessAgentPolicy();
const newPackagePolicy = getMockPolicyEKS();

const { queryByTestId } = render(
<WrappedComponent newPolicy={newPackagePolicy} agentlessPolicy={agentlessPolicy} />
Expand All @@ -1519,16 +1604,12 @@ describe('<CspPolicyTemplateForm />', () => {
expect(setupTechnologySelectorAccordion).not.toBeInTheDocument();
});

it('should not render setup technology selector for CSPM GCP', () => {
it('should not render setup technology selector for CNVM', () => {
const agentlessPolicy = getMockAgentlessAgentPolicy();
const newPackagePolicy = getMockPolicyGCP();
const newPackagePolicy = getMockPolicyVulnMgmtAWS();

const { queryByTestId } = render(
<WrappedComponent
newPolicy={newPackagePolicy}
packageInfo={getMockPackageInfoCspmGCP()}
agentlessPolicy={agentlessPolicy}
/>
<WrappedComponent newPolicy={newPackagePolicy} agentlessPolicy={agentlessPolicy} />
);

const setupTechnologySelectorAccordion = queryByTestId(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import {
PolicyTemplateVarsForm,
} from './policy_template_selectors';
import { usePackagePolicyList } from '../../common/api/use_package_policy_list';
import { gcpField, getInputVarsFields } from './gcp_credential_form';
import { gcpField, getInputVarsFields } from './gcp_credentials_form/gcp_credential_form';
import { SetupTechnologySelector } from './setup_technology_selector/setup_technology_selector';
import { useSetupTechnology } from './setup_technology_selector/use_setup_technology';

Expand Down
Loading

0 comments on commit 9500c3b

Please sign in to comment.