[Cloud Security] [CIS GCP] Google Cloud Shell Onboarding steps (#163030)

Added Google Cloud Shell onboarding option
- User now are able to choose Google Cloud Shell Option
- Upon Saving integration with Google Cloud Shell Setup access option,
Google Cloud Shell deployment post installation modal will pop up.
Clicking on the Launch Cloud Shell button will redirect user to Google
Cloud Shell page
- User could also click on the Launch Cloud Shell button from Agent
installation flyout modal and get redirected to Google Cloud Shell page

NOTE:
- The cloud shell Url are not fully functioning right now as we don't
have 8.10 branch yet, as such when we want to test this for now, user
could just change the cloudshell_git_branch value on the url to main
from 8.10 manually

<img width="840" alt="Screenshot 2023-08-05 at 10 48 02 AM"
src="bf7d5acc-1006-4807-b7a9-1ebb3e2aa847">
<img width="914" alt="Screenshot 2023-08-05 at 10 46 45 AM"
src="41a5a2e2-1e32-471d-a150-bbd319eee592">
<img width="1679" alt="Screenshot 2023-08-05 at 10 44 28 AM"
src="2e04a05a-c0bd-4f6a-ab49-a6efe3600c66">



fc0f5825-882b-4091-8a62-2917d108abb6


e8ea0ca8-997e-452d-8280-5180db34aaa9
This commit is contained in:
Rickyanto Ang 2023-08-10 10:37:26 -07:00 committed by GitHub
parent e5a591c369
commit a8f3a5ac8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1009 additions and 129 deletions

View file

@ -123,3 +123,7 @@ export const VULNERABILITIES_SEVERITY: Record<VulnSeverity, VulnSeverity> = {
CRITICAL: 'CRITICAL',
UNKNOWN: 'UNKNOWN',
};
export const VULNERABILITIES_ENUMERATION = 'CVE';
export const SETUP_ACCESS_CLOUD_SHELL = 'google_cloud_shell';
export const SETUP_ACCESS_MANUAL = 'manual';

View file

@ -75,7 +75,7 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = {
{
type: CLOUDBEAT_AWS,
name: i18n.translate('xpack.csp.cspmIntegration.awsOption.nameTitle', {
defaultMessage: 'Amazon Web Services',
defaultMessage: 'AWS',
}),
benchmark: i18n.translate('xpack.csp.cspmIntegration.awsOption.benchmarkTitle', {
defaultMessage: 'CIS AWS',

View file

@ -10,6 +10,7 @@ import { CIS_AWS, CIS_GCP } from '../../common/constants';
import { Cluster } from '../../common/types';
import { CISBenchmarkIcon } from './cis_benchmark_icon';
import { CompactFormattedNumber } from './compact_formatted_number';
import { useNavigateFindings } from '../common/hooks/use_navigate_findings';
export const AccountsEvaluatedWidget = ({
clusters,
@ -23,6 +24,12 @@ export const AccountsEvaluatedWidget = ({
return clusters?.filter((obj) => obj?.meta.benchmark.id === benchmarkId) || [];
};
const navToFindings = useNavigateFindings();
const navToFindingsByCloudProvider = (provider: string) => {
navToFindings({ 'cloud.provider': provider });
};
const cisAwsClusterAmount = filterClustersById(CIS_AWS).length;
const cisGcpClusterAmount = filterClustersById(CIS_GCP).length;
@ -32,32 +39,46 @@ export const AccountsEvaluatedWidget = ({
return (
<>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<CISBenchmarkIcon type={CIS_AWS} name={cisAwsBenchmarkName} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CompactFormattedNumber
number={cisAwsClusterAmount}
abbreviateAbove={benchmarkAbbreviateAbove}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<CISBenchmarkIcon type={CIS_GCP} name={cisGcpBenchmarkName} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CompactFormattedNumber
number={cisGcpClusterAmount}
abbreviateAbove={benchmarkAbbreviateAbove}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{cisAwsClusterAmount > 0 && (
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<CISBenchmarkIcon type={CIS_AWS} name={cisAwsBenchmarkName} />
</EuiFlexItem>
<EuiFlexItem
grow={false}
onClick={() => {
navToFindingsByCloudProvider('aws');
}}
>
<CompactFormattedNumber
number={cisAwsClusterAmount}
abbreviateAbove={benchmarkAbbreviateAbove}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
{cisGcpClusterAmount > 0 && (
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<CISBenchmarkIcon type={CIS_GCP} name={cisGcpBenchmarkName} />
</EuiFlexItem>
<EuiFlexItem
grow={false}
onClick={() => {
navToFindingsByCloudProvider('gcp');
}}
>
<CompactFormattedNumber
number={cisGcpClusterAmount}
abbreviateAbove={benchmarkAbbreviateAbove}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
</>
);

View file

@ -167,7 +167,7 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) =>
</EuiLink>
);
const ReadDocumentation = ({ url }: { url: string }) => {
export const ReadDocumentation = ({ url }: { url: string }) => {
return (
<EuiText color="subdued" size="s">
<FormattedMessage

View file

@ -4,14 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import semverLt from 'semver/functions/lt';
import semverCoerce from 'semver/functions/coerce';
import semverValid from 'semver/functions/valid';
import { css } from '@emotion/react';
import {
EuiFieldText,
EuiFormRow,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
@ -19,16 +19,29 @@ import {
EuiForm,
EuiCallOut,
EuiTextArea,
EuiHorizontalRule,
} from '@elastic/eui';
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 {
CLOUDBEAT_GCP,
SETUP_ACCESS_CLOUD_SHELL,
SETUP_ACCESS_MANUAL,
} from '../../../common/constants';
import { RadioGroup } from './csp_boxed_radio_group';
import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils';
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';
export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = {
GOOGLE_CLOUD_SHELL_SETUP: 'google_cloud_shell_setup_test_id',
PROJECT_ID: 'project_id_test_id',
CREDENTIALS_TYPE: 'credentials_type_test_id',
CREDENTIALS_FILE: 'credentials_file_test_id',
@ -37,7 +50,8 @@ export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = {
type SetupFormatGCP = 'google_cloud_shell' | 'manual';
const GCPSetupInfoContent = () => (
<>
<EuiSpacer size="l" />
<EuiHorizontalRule margin="xxl" />
<EuiSpacer size="s" />
<EuiTitle size="s">
<h2>
<FormattedMessage
@ -58,30 +72,50 @@ const GCPSetupInfoContent = () => (
</>
);
/* NEED TO FIND THE REAL URL HERE LATER */
const DocsLink = (
<EuiText color={'subdued'} size="s">
<FormattedMessage
id="xpack.csp.gcpIntegration.docsLink"
defaultMessage="Read the {docs} for more details"
values={{
docs: (
<EuiLink href="https://cloud.google.com/docs/authentication" external>
documentation
</EuiLink>
),
}}
/>
</EuiText>
);
const GoogleCloudShellSetup = () => {
return (
<>
<EuiText
color="subdued"
size="s"
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.GOOGLE_CLOUD_SHELL_SETUP}
>
<ol
css={css`
list-style: auto;
`}
>
<li>
<FormattedMessage
id="xpack.csp.gcpIntegration.cloudShellSetupStep.login"
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>
<li>
<FormattedMessage
id="xpack.csp.gcpIntegration.cloudShellSetupStep.launch"
defaultMessage='Click "Save and Continue" at the bottom right of the page. Then, on the pop-up modal, click "Launch Google Cloud Shell"'
/>
</li>
</ol>
</EuiText>
<EuiSpacer size="l" />
</>
);
};
type GcpCredentialsType = 'credentials_file' | 'credentials_json';
type GcpFields = Record<string, { label: string; type?: 'password' | 'text' }>;
interface GcpInputFields {
fields: GcpFields;
}
const gcpField: GcpInputFields = {
export const gcpField: GcpInputFields = {
fields: {
project_id: {
label: i18n.translate('xpack.csp.gcpIntegration.projectidFieldLabel', {
@ -132,14 +166,14 @@ const getSetupFormatOptions = (): Array<{
disabled: boolean;
}> => [
{
id: 'google_cloud_shell',
id: SETUP_ACCESS_CLOUD_SHELL,
label: i18n.translate('xpack.csp.gcpIntegration.setupFormatOptions.googleCloudShell', {
defaultMessage: 'Google Cloud Shell',
}),
disabled: true,
disabled: false,
},
{
id: 'manual',
id: SETUP_ACCESS_MANUAL,
label: i18n.translate('xpack.csp.gcpIntegration.setupFormatOptions.manual', {
defaultMessage: 'Manual',
}),
@ -175,6 +209,83 @@ const getInputVarsFields = (
} as const;
});
const getSetupFormatFromInput = (
input: Extract<
NewPackagePolicyPostureInput,
{ type: 'cloudbeat/cis_aws' | 'cloudbeat/cis_eks' | 'cloudbeat/cis_gcp' }
>
): SetupFormatGCP => {
const credentialsType = input.streams[0].vars?.setup_access?.value;
// Google Cloud shell is the default value
if (!credentialsType) {
return SETUP_ACCESS_CLOUD_SHELL;
}
if (credentialsType !== SETUP_ACCESS_CLOUD_SHELL) {
return SETUP_ACCESS_MANUAL;
}
return SETUP_ACCESS_CLOUD_SHELL;
};
const getGoogleCloudShellUrl = (newPolicy: NewPackagePolicy) => {
const template: string | undefined = newPolicy?.inputs?.find((i) => i.type === CLOUDBEAT_GCP)
?.config?.cloud_shell_url?.value;
return template || undefined;
};
const updateCloudShellUrl = (
newPolicy: NewPackagePolicy,
updatePolicy: (policy: NewPackagePolicy) => void,
templateUrl: string | undefined
) => {
updatePolicy?.({
...newPolicy,
inputs: newPolicy.inputs.map((input) => {
if (input.type === CLOUDBEAT_GCP) {
return {
...input,
config: { cloud_shell_url: { value: templateUrl } },
};
}
return input;
}),
});
};
const useCloudShellUrl = ({
packageInfo,
newPolicy,
updatePolicy,
setupFormat,
}: {
packageInfo: PackageInfo;
newPolicy: NewPackagePolicy;
updatePolicy: (policy: NewPackagePolicy) => void;
setupFormat: SetupFormatGCP;
}) => {
useEffect(() => {
const policyInputCloudShellUrl = getGoogleCloudShellUrl(newPolicy);
if (setupFormat === SETUP_ACCESS_MANUAL) {
if (!!policyInputCloudShellUrl) {
updateCloudShellUrl(newPolicy, updatePolicy, undefined);
}
return;
}
const templateUrl = getCspmCloudShellDefaultValue(packageInfo);
// If the template is not available, do not update the policy
if (templateUrl === '') return;
// If the template is already set, do not update the policy
if (policyInputCloudShellUrl === templateUrl) return;
updateCloudShellUrl(newPolicy, updatePolicy, templateUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newPolicy?.vars?.cloud_shell_url, newPolicy, packageInfo, setupFormat]);
};
export const GcpCredentialsForm = ({
input,
newPolicy,
@ -187,15 +298,67 @@ export const GcpCredentialsForm = ({
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);
};
useCloudShellUrl({
packageInfo,
newPolicy,
updatePolicy,
setupFormat,
});
const onSetupFormatChange = (newSetupFormat: SetupFormatGCP) => {
if (newSetupFormat === SETUP_ACCESS_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 }])
);
// We need to store the last manual credentials type to restore it later
lastSetupAccessType.current = input.streams[0].vars?.setup_access?.value;
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
setup_access: {
value: SETUP_ACCESS_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 }])),
})
);
} else {
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
setup_access: {
// Restoring last manual credentials type or defaulting to the first option
value: lastSetupAccessType.current || SETUP_ACCESS_MANUAL,
type: 'text',
},
// Restoring fields from manual setup format if any
...fieldsSnapshot.current,
})
);
}
};
// Integration is Invalid IF Version is not at least 1.5.0 OR Setup Access is manual but Project ID is empty
useEffect(() => {
setIsValid(!isInvalid);
const isProjectIdEmpty =
setupFormat === SETUP_ACCESS_MANUAL && !getFieldById('project_id')?.value;
const isInvalidPolicy = isInvalid || isProjectIdEmpty;
setIsValid(!isInvalidPolicy);
onChange({
isValid: !isInvalid,
isValid: !isInvalidPolicy,
updatedPolicy: newPolicy,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input, packageInfo]);
}, [input, packageInfo, setupFormat]);
if (isInvalid) {
return (
@ -214,32 +377,30 @@ export const GcpCredentialsForm = ({
<>
<GCPSetupInfoContent />
<EuiSpacer size="l" />
<GcpSetupAccessSelector
onChange={(optionId) => updatePolicy(getPosturePolicy(newPolicy, input.type))}
<RadioGroup
size="s"
options={getSetupFormatOptions()}
idSelected={setupFormat}
onChange={onSetupFormatChange}
/>
<EuiSpacer size="l" />
<GcpInputVarFields
fields={fields}
onChange={(key, value) =>
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }))
}
/>
{setupFormat === SETUP_ACCESS_MANUAL ? (
<GcpInputVarFields
fields={fields}
onChange={(key, value) =>
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }))
}
/>
) : (
<GoogleCloudShellSetup />
)}
<EuiSpacer size="s" />
{DocsLink}
<ReadDocumentation url={cspIntegrationDocsNavigation.cspm.getStartedPath} />
<EuiSpacer />
</>
);
};
const GcpSetupAccessSelector = ({ onChange }: { onChange(type: GcpCredentialsType): void }) => (
<RadioGroup
size="s"
options={getSetupFormatOptions()}
idSelected={'manual'}
onChange={(id: GcpCredentialsType) => onChange(id)}
/>
);
const GcpInputVarFields = ({
fields,
onChange,
@ -278,7 +439,7 @@ const GcpInputVarFields = ({
data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_TYPE}
fullWidth
options={credentialOptionsList}
value={credentialsTypeFields?.value}
value={credentialsTypeFields?.value || credentialOptionsList[0].value}
onChange={(optionElem) => {
onChange('credentials_type', optionElem.target.value);
}}

View file

@ -196,7 +196,7 @@ describe('<CspPolicyTemplateForm />', () => {
it('renders CSPM input selector', () => {
const { getByLabelText } = render(<WrappedComponent newPolicy={getMockPolicyAWS()} />);
const option1 = getByLabelText('Amazon Web Services');
const option1 = getByLabelText('AWS');
const option2 = getByLabelText('GCP');
const option3 = getByLabelText('Azure');
@ -229,7 +229,7 @@ describe('<CspPolicyTemplateForm />', () => {
<WrappedComponent newPolicy={getMockPolicyAWS()} edit={true} />
);
const option1 = getByLabelText('Amazon Web Services');
const option1 = getByLabelText('AWS');
const option2 = getByLabelText('GCP');
const option3 = getByLabelText('Azure');
@ -983,12 +983,12 @@ describe('<CspPolicyTemplateForm />', () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_type: { value: 'credentials-file' },
setup_access: { value: 'manual' },
});
const { getByText } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP('1.3.1')} />
);
expect(onChange).toHaveBeenCalledWith({
isValid: false,
updatedPolicy: policy,
@ -1001,10 +1001,56 @@ describe('<CspPolicyTemplateForm />', () => {
).toBeInTheDocument();
});
it(`renders Google Cloud Shell forms when Setup Access is set to Google Cloud Shell`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_type: { value: 'credentials-file' },
setup_access: { value: 'google_cloud_shell' },
});
const { getByTestId } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />
);
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
expect(
getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.GOOGLE_CLOUD_SHELL_SETUP)
).toBeInTheDocument();
});
it(`project ID is required for Manual users`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
project_id: { value: undefined },
setup_access: { value: 'manual' },
});
const { rerender } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />
);
expect(onChange).toHaveBeenCalledWith({
isValid: false,
updatedPolicy: policy,
});
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
project_id: { value: '' },
setup_access: { value: 'manual' },
});
rerender(<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />);
expect(onChange).toHaveBeenCalledWith({
isValid: false,
updatedPolicy: policy,
});
});
it(`renders ${CLOUDBEAT_GCP} Credentials File fields`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
credentials_type: { value: 'credentials-file' },
setup_access: { value: 'manual' },
});
const { getByLabelText, getByRole } = render(
@ -1021,33 +1067,22 @@ describe('<CspPolicyTemplateForm />', () => {
it(`updates ${CLOUDBEAT_GCP} Credentials File fields`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
project_id: { value: 'a' },
credentials_type: { value: 'credentials-file' },
setup_access: { value: 'manual' },
});
const { rerender, getByTestId } = render(
const { 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, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
@ -1056,10 +1091,11 @@ describe('<CspPolicyTemplateForm />', () => {
it(`renders ${CLOUDBEAT_GCP} Credentials JSON fields`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
setup_access: { value: 'manual' },
credentials_type: { value: 'credentials-json' },
});
const { getByLabelText, getByRole } = render(
const { getByRole, getByLabelText } = render(
<WrappedComponent newPolicy={policy} packageInfo={getMockPackageInfoCspmGCP()} />
);
@ -1073,33 +1109,22 @@ describe('<CspPolicyTemplateForm />', () => {
it(`updates ${CLOUDBEAT_GCP} Credentials JSON fields`, () => {
let policy = getMockPolicyGCP();
policy = getPosturePolicy(policy, CLOUDBEAT_GCP, {
project_id: { value: 'a' },
credentials_type: { value: 'credentials-json' },
setup_access: { value: 'manual' },
});
const { rerender, getByTestId } = render(
const { 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, {
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});

View file

@ -81,6 +81,8 @@ interface IntegrationInfoFieldsProps {
export const AWS_SINGLE_ACCOUNT = 'single-account';
export const AWS_ORGANIZATION_ACCOUNT = 'organization-account';
export const GCP_SINGLE_ACCOUNT = 'single-account-gcp';
export const GCP_ORGANIZATION_ACCOUNT = 'organization-account-gcp';
type AwsAccountType = typeof AWS_SINGLE_ACCOUNT | typeof AWS_ORGANIZATION_ACCOUNT;
const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps['options'] => [
@ -104,6 +106,28 @@ const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps
},
];
const getGcpAccountTypeOptions = (): CspRadioGroupProps['options'] => [
{
id: GCP_ORGANIZATION_ACCOUNT,
label: i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationLabel', {
defaultMessage: 'GCP Organization',
}),
disabled: true,
tooltip: i18n.translate(
'xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationDisabledTooltip',
{
defaultMessage: 'Coming Soon',
}
),
},
{
id: GCP_SINGLE_ACCOUNT,
label: i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpSingleAccountLabel', {
defaultMessage: 'Single Account',
}),
},
];
const getAwsAccountType = (
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_aws' }>
): AwsAccountType | undefined => input.streams[0].vars?.['aws.account_type']?.value;
@ -208,6 +232,53 @@ const AwsAccountTypeSelect = ({
);
};
const GcpAccountTypeSelect = ({
input,
newPolicy,
updatePolicy,
packageInfo,
}: {
input: Extract<NewPackagePolicyPostureInput, { type: 'cloudbeat/cis_gcp' }>;
newPolicy: NewPackagePolicy;
updatePolicy: (updatedPolicy: NewPackagePolicy) => void;
packageInfo: PackageInfo;
}) => {
return (
<>
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.fleetIntegration.gcpAccountTypeDescriptionLabel"
defaultMessage="Select between single account or organization, and then fill in the name and description to help identify this integration."
/>
</EuiText>
<EuiSpacer size="l" />
<RadioGroup
idSelected={GCP_SINGLE_ACCOUNT}
options={getGcpAccountTypeOptions()}
onChange={(accountType) => {
updatePolicy(
getPosturePolicy(newPolicy, input.type, {
gcp_account_type: {
value: accountType,
type: 'text',
},
})
);
}}
size="m"
/>
<EuiSpacer size="l" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.csp.fleetIntegration.gcpAccountType.singleAccountDescription"
defaultMessage="Deploying to a single account is suitable for an initial POC. To ensure complete coverage, it is strongly recommended to deploy CSPM at the organization-level, which automatically connects all accounts (both current and future)."
/>
</EuiText>
<EuiSpacer size="l" />
</>
);
};
const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) => (
<div>
{fields.map(({ value, id, label, error }) => (
@ -375,6 +446,15 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
/>
)}
{input.type === 'cloudbeat/cis_gcp' && (
<GcpAccountTypeSelect
input={input}
newPolicy={newPolicy}
updatePolicy={updatePolicy}
packageInfo={packageInfo}
/>
)}
{/* Defines the name/description */}
<IntegrationSettings
fields={integrationFields}

View file

@ -225,3 +225,22 @@ export const getMaxPackageName = (
return `${packageName}-${maxPkgPolicyName + 1}`;
};
export const getCspmCloudShellDefaultValue = (packageInfo: PackageInfo): string => {
if (!packageInfo.policy_templates) return '';
const policyTemplate = packageInfo.policy_templates.find((p) => p.name === CSPM_POLICY_TEMPLATE);
if (!policyTemplate) return '';
const policyTemplateInputs = hasPolicyTemplateInputs(policyTemplate) && policyTemplate.inputs;
if (!policyTemplateInputs) return '';
const cloudShellUrl = policyTemplateInputs.reduce((acc, input): string => {
if (!input.vars) return acc;
const template = input.vars.find((v) => v.name === 'cloud_shell_url')?.default;
return template ? String(template) : acc;
}, '');
return cloudShellUrl;
};

View file

@ -52,6 +52,9 @@ function getArtifact(platform: PLATFORM_TYPE, kibanaVersion: string) {
kubernetes: {
downloadCommand: '',
},
googleCloudShell: {
downloadCommand: '',
},
};
return artifactMap[platform];
@ -116,6 +119,7 @@ export function getInstallCommandForPlatform(
rpm: `${artifact.downloadCommand}\nsudo elastic-agent enroll ${commandArgumentsStr}\nsudo systemctl enable elastic-agent\nsudo systemctl start elastic-agent`,
kubernetes: '',
cloudFormation: '',
googleCloudShell: '',
};
return commands[platform];

View file

@ -0,0 +1,111 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useQuery } from '@tanstack/react-query';
import type { AgentPolicy, PackagePolicy } from '../../../../../types';
import {
sendGetEnrollmentAPIKeys,
useCreateCloudShellUrl,
useFleetServerHostsForPolicy,
useKibanaVersion,
} from '../../../../../hooks';
import { GoogleCloudShellGuide } from '../../../../../components';
import { ManualInstructions } from '../../../../../../../components/enrollment_instructions';
export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{
onConfirm: () => void;
onCancel: () => void;
agentPolicy: AgentPolicy;
packagePolicy: PackagePolicy;
}> = ({ onConfirm, onCancel, agentPolicy, packagePolicy }) => {
const { data: apyKeysData } = useQuery(['googleCloudShellApiKeys'], () =>
sendGetEnrollmentAPIKeys({
page: 1,
perPage: 1,
kuery: `policy_id:${agentPolicy.id}`,
})
);
const { fleetServerHosts, fleetProxy } = useFleetServerHostsForPolicy(agentPolicy);
const kibanaVersion = useKibanaVersion();
const installManagedCommands = ManualInstructions({
apiKey: apyKeysData?.data?.items[0]?.api_key || 'no_key',
fleetServerHosts,
fleetProxy,
kibanaVersion,
});
const { cloudShellUrl, error, isError, isLoading } = useCreateCloudShellUrl({
enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key,
packagePolicy,
});
return (
<EuiModal data-test-subj="postInstallGoogleCloudShellModal" onClose={onCancel}>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="confirmGoogleCloudShellTitleText">
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallGoogleCloudShellModalTitle"
defaultMessage="Google Cloud Shell deployment"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<GoogleCloudShellGuide commandText={installManagedCommands.googleCloudShell} />
{error && isError && (
<>
<EuiSpacer size="m" />
<EuiCallOut title={error} color="danger" iconType="error" />
</>
)}
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty
data-test-subj="confirmGoogleCloudShellModalCancelButton"
onClick={onCancel}
>
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallGoogleCloudShellModal.cancelButton"
defaultMessage="Launch Google Cloud Shell later"
/>
</EuiButtonEmpty>
<EuiButton
data-test-subj="confirmGoogleCloudShellModalConfirmButton"
onClick={() => {
window.open(cloudShellUrl);
onConfirm();
}}
fill
color="primary"
isLoading={isLoading}
isDisabled={isError}
>
<FormattedMessage
id="xpack.fleet.agentPolicy.postInstallGoogleCloudShellModalConfirmButtonLabel"
defaultMessage="Launch Google Cloud Shell"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -24,7 +24,11 @@ import {
sendBulkInstallPackages,
sendGetPackagePolicies,
} from '../../../../../hooks';
import { isVerificationError, packageToPackagePolicy } from '../../../../../services';
import {
getCloudShellUrlFromPackagePolicy,
isVerificationError,
packageToPackagePolicy,
} from '../../../../../services';
import {
FLEET_ELASTIC_AGENT_PACKAGE,
FLEET_SYSTEM_PACKAGE,
@ -304,11 +308,18 @@ export function useOnSubmit({
? getCloudFormationPropsFromPackagePolicy(data.item).templateUrl
: false;
const hasGoogleCloudShell = data?.item ? getCloudShellUrlFromPackagePolicy(data.item) : false;
if (hasCloudFormation) {
setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_CLOUD_FORMATION');
} else {
setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS');
}
if (hasGoogleCloudShell) {
setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_GOOGLE_CLOUD_SHELL');
} else {
setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS');
}
if (!error) {
setSavedPackagePolicy(data!.item);
@ -317,6 +328,10 @@ export function useOnSubmit({
setFormState('SUBMITTED_CLOUD_FORMATION');
return;
}
if (!hasAgentsAssigned && hasGoogleCloudShell) {
setFormState('SUBMITTED_GOOGLE_CLOUD_SHELL');
return;
}
if (!hasAgentsAssigned) {
setFormState('SUBMITTED_NO_AGENTS');
return;

View file

@ -60,6 +60,7 @@ import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/
import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from './components';
import { useDevToolsRequest, useOnSubmit } from './hooks';
import { PostInstallCloudFormationModal } from './components/post_install_cloud_formation_modal';
import { PostInstallGoogleCloudShellModal } from './components/post_install_google_cloud_shell_modal';
const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
@ -422,6 +423,14 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
/>
)}
{formState === 'SUBMITTED_GOOGLE_CLOUD_SHELL' && agentPolicy && savedPackagePolicy && (
<PostInstallGoogleCloudShellModal
agentPolicy={agentPolicy}
packagePolicy={savedPackagePolicy}
onConfirm={() => navigateAddAgent(savedPackagePolicy)}
onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
/>
)}
{packageInfo && (
<IntegrationBreadcrumb
pkgTitle={integrationInfo?.title || packageInfo.title}

View file

@ -22,7 +22,8 @@ export type PackagePolicyFormState =
| 'LOADING'
| 'SUBMITTED'
| 'SUBMITTED_NO_AGENTS'
| 'SUBMITTED_CLOUD_FORMATION';
| 'SUBMITTED_CLOUD_FORMATION'
| 'SUBMITTED_GOOGLE_CLOUD_SHELL';
export interface AddToPolicyParams {
pkgkey: string;

View file

@ -0,0 +1,43 @@
/*
* 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 { EuiButton, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { GoogleCloudShellGuide } from '../google_cloud_shell_guide';
interface Props {
cloudShellUrl: string;
cloudShellCommand: string;
}
export const GoogleCloudShellInstructions: React.FunctionComponent<Props> = ({
cloudShellUrl,
cloudShellCommand,
}) => {
return (
<>
<GoogleCloudShellGuide commandText={cloudShellCommand} />
<EuiSpacer size="m" />
<EuiButton
color="primary"
fill
target="_blank"
iconSide="left"
iconType="launch"
fullWidth
href={cloudShellUrl}
>
<FormattedMessage
id="xpack.fleet.agentEnrollment.googleCloudShell.launchButton"
defaultMessage="Launch Google Cloud Shell"
/>
</EuiButton>
</>
);
};

View file

@ -14,6 +14,7 @@ import {
FLEET_CLOUD_SECURITY_POSTURE_PACKAGE,
FLEET_CLOUD_DEFEND_PACKAGE,
} from '../../../common';
import { getCloudShellUrlFromAgentPolicy } from '../../services';
import {
getCloudFormationTemplateUrlFromPackageInfo,
@ -127,6 +128,7 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) {
AWS_ACCOUNT_TYPE
]?.value;
const cloudShellUrl = getCloudShellUrlFromAgentPolicy(agentPolicy);
return {
isLoading,
integrationType,
@ -135,6 +137,7 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) {
awsAccountType: cloudFormationAwsAccountType,
templateUrl: cloudFormationTemplateUrl,
},
cloudShellUrl,
};
}, [agentPolicy, packageInfoData?.item, isLoading, cloudSecurityPackagePolicy]);

View file

@ -81,7 +81,10 @@ export const Instructions = (props: InstructionProps) => {
useEffect(() => {
// If we detect a CloudFormation integration, we want to hide the selection type
if (props.cloudSecurityIntegration?.isCloudFormation) {
if (
props.cloudSecurityIntegration?.isCloudFormation ||
props.cloudSecurityIntegration?.cloudShellUrl
) {
setSelectionType(undefined);
} else if (!isIntegrationFlow && showAgentEnrollment) {
setSelectionType('radio');
@ -103,7 +106,7 @@ export const Instructions = (props: InstructionProps) => {
} else if (showAgentEnrollment) {
return (
<>
{selectionType === 'tabs' && (
{selectionType === 'tabs' && !props.cloudSecurityIntegration?.cloudShellUrl && (
<>
<EuiText>
<FormattedMessage

View file

@ -31,6 +31,7 @@ export interface CloudSecurityIntegration {
isLoading: boolean;
isCloudFormation: boolean;
cloudFormationProps?: CloudFormationProps;
cloudShellUrl: string | undefined;
}
export interface BaseProps {

View file

@ -44,6 +44,7 @@ export const InstallSection: React.FunctionComponent<Props> = ({
windowsCommand={installCommand.windows}
linuxDebCommand={installCommand.deb}
linuxRpmCommand={installCommand.rpm}
googleCloudShellCommand={installCommand.googleCloudShell}
k8sCommand={installCommand.kubernetes}
hasK8sIntegration={isK8s === 'IS_KUBERNETES' || isK8s === 'IS_KUBERNETES_MULTIPAGE'}
cloudSecurityIntegration={cloudSecurityIntegration}

View file

@ -35,6 +35,8 @@ export const ManualInstructions = ({
kibanaVersion: string;
}) => {
const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts, fleetProxy);
const fleetServerUrl = enrollArgs?.split('--url=')?.pop()?.split('--enrollment')[0];
const enrollmentToken = enrollArgs?.split('--enrollment-token=')[1];
const k8sCommand = 'kubectl apply -f elastic-agent-managed-kubernetes.yml';
@ -62,6 +64,8 @@ sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \n
sudo rpm -vi elastic-agent-${kibanaVersion}-x86_64.rpm
sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \nsudo systemctl start elastic-agent`;
const googleCloudShellCommand = `FLEET_URL=${fleetServerUrl} ENROLLMENT_TOKEN=${enrollmentToken} STACK_VERSION=${kibanaVersion} ./deploy.sh`;
return {
linux: linuxCommand,
mac: macCommand,
@ -70,5 +74,6 @@ sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \n
rpm: linuxRpmCommand,
kubernetes: k8sCommand,
cloudFormation: '',
googleCloudShell: googleCloudShellCommand,
};
};

View file

@ -38,5 +38,6 @@ cd elastic-agent-${kibanaVersion}-windows-x86_64
deb: linuxDebCommand,
rpm: linuxRpmCommand,
kubernetes: k8sCommand,
googleCloudShell: '',
};
};

View file

@ -0,0 +1,78 @@
/*
* 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 { EuiCodeBlock, EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
/* Need to change to the real URL */
const GOOGLE_CLOUD_SHELL_EXTERNAL_DOC_URL = 'https://cloud.google.com/shell/docs';
const Link = ({ children, url }: { children: React.ReactNode; url: string }) => (
<EuiLink
href={url}
target="_blank"
rel="noopener nofollow noreferrer"
data-test-subj="externalLink"
>
{children}
</EuiLink>
);
export const GoogleCloudShellGuide = (props: { commandText: string }) => {
return (
<>
<EuiSpacer size="m" />
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.fleet.googleCloudShell.guide.description"
defaultMessage="The Google Cloud Shell Command below will create all the necessary resources to evaluate the security posture of your GCP projects. Learn more about {learnMore}."
values={{
learnMore: (
<Link url={GOOGLE_CLOUD_SHELL_EXTERNAL_DOC_URL}>
<FormattedMessage
id="xpack.fleet.googleCloudShell.guide.learnMoreLinkText"
defaultMessage="Google Cloud Shell"
/>
</Link>
),
}}
/>
</p>
<EuiText size="s" color="subdued">
<ol>
<li>
<FormattedMessage
id="xpack.fleet.googleCloudShell.guide.steps.login"
defaultMessage="Log into your Google Cloud Console"
/>
</li>
<li>
<>
<FormattedMessage
id="xpack.fleet.googleCloudShell.guide.steps.copy"
defaultMessage="Copy the command below"
/>
<EuiSpacer size="m" />
<EuiCodeBlock language="bash" isCopyable>
{props.commandText}
</EuiCodeBlock>
</>
</li>
<li>
<FormattedMessage
id="xpack.fleet.googleCloudShell.guide.steps.launch"
defaultMessage="Click the Launch Google Cloud Shell button below and then execute the command you copied earlier in google cloud shell."
/>
</li>
</ol>
</EuiText>
</EuiText>
</>
);
};

View file

@ -30,3 +30,4 @@ export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge';
export { WithGuidedOnboardingTour } from './with_guided_onboarding_tour';
export { UninstallCommandFlyout } from './uninstall_command_flyout';
export { CloudFormationGuide } from './cloud_formation_guide';
export { GoogleCloudShellGuide } from './google_cloud_shell_guide';

View file

@ -24,9 +24,15 @@ import {
FLEET_CLOUD_SECURITY_POSTURE_CSPM_POLICY_TEMPLATE,
} from '../../common/constants/epm';
import { type PLATFORM_TYPE } from '../hooks';
import { REDUCED_PLATFORM_OPTIONS, PLATFORM_OPTIONS, usePlatform } from '../hooks';
import {
REDUCED_PLATFORM_OPTIONS,
PLATFORM_OPTIONS,
PLATFORM_OPTIONS_CLOUD_SHELL,
usePlatform,
} from '../hooks';
import { KubernetesInstructions } from './agent_enrollment_flyout/kubernetes_instructions';
import { GoogleCloudShellInstructions } from './agent_enrollment_flyout/google_cloud_shell_instructions';
import type { CloudSecurityIntegration } from './agent_enrollment_flyout/types';
interface Props {
@ -36,6 +42,7 @@ interface Props {
linuxDebCommand: string;
linuxRpmCommand: string;
k8sCommand: string;
googleCloudShellCommand?: string | undefined;
hasK8sIntegration: boolean;
cloudSecurityIntegration?: CloudSecurityIntegration | undefined;
hasK8sIntegrationMultiPage: boolean;
@ -58,6 +65,7 @@ export const PlatformSelector: React.FunctionComponent<Props> = ({
linuxDebCommand,
linuxRpmCommand,
k8sCommand,
googleCloudShellCommand,
hasK8sIntegration,
cloudSecurityIntegration,
hasK8sIntegrationMultiPage,
@ -68,6 +76,9 @@ export const PlatformSelector: React.FunctionComponent<Props> = ({
onCopy,
}) => {
const getInitialPlatform = useCallback(() => {
if (cloudSecurityIntegration?.cloudShellUrl) {
return 'googleCloudShell';
}
if (
hasK8sIntegration ||
(cloudSecurityIntegration?.integrationType ===
@ -77,19 +88,28 @@ export const PlatformSelector: React.FunctionComponent<Props> = ({
return 'kubernetes';
return 'linux';
}, [hasK8sIntegration, cloudSecurityIntegration?.integrationType, isManaged]);
}, [
hasK8sIntegration,
cloudSecurityIntegration?.integrationType,
isManaged,
cloudSecurityIntegration?.cloudShellUrl,
]);
const { platform, setPlatform } = usePlatform(getInitialPlatform());
// In case of fleet server installation or standalone agent without
// Kubernetes integration in the policy use reduced platform options
// If it has Cloud Shell URL, then it should show platform options with Cloudshell in it
const isReduced = hasFleetServer || (!isManaged && !hasK8sIntegration);
const getPlatformOptions = useCallback(() => {
const platformOptions = isReduced ? REDUCED_PLATFORM_OPTIONS : PLATFORM_OPTIONS;
const platformOptionsWithCloudShell = cloudSecurityIntegration?.cloudShellUrl
? PLATFORM_OPTIONS_CLOUD_SHELL
: platformOptions;
return platformOptions;
}, [isReduced]);
return platformOptionsWithCloudShell;
}, [isReduced, cloudSecurityIntegration?.cloudShellUrl]);
const [copyButtonClicked, setCopyButtonClicked] = useState(false);
@ -144,6 +164,7 @@ export const PlatformSelector: React.FunctionComponent<Props> = ({
deb: linuxDebCommand,
rpm: linuxRpmCommand,
kubernetes: k8sCommand,
googleCloudShell: k8sCommand,
};
const onTextAreaClick = () => {
if (onCopy) onCopy();
@ -208,6 +229,15 @@ export const PlatformSelector: React.FunctionComponent<Props> = ({
<EuiSpacer size="s" />
</>
)}
{platform === 'googleCloudShell' && isManaged && (
<>
<GoogleCloudShellInstructions
cloudShellUrl={cloudSecurityIntegration?.cloudShellUrl || ''}
cloudShellCommand={googleCloudShellCommand || ''}
/>
<EuiSpacer size="s" />
</>
)}
{!hasK8sIntegrationMultiPage && (
<>
{platform === 'kubernetes' && (
@ -220,17 +250,20 @@ export const PlatformSelector: React.FunctionComponent<Props> = ({
<EuiSpacer size="m" />
</EuiText>
)}
<EuiCodeBlock
onClick={onTextAreaClick}
fontSize="m"
isCopyable={!fullCopyButton}
paddingSize="m"
css={`
max-width: 1100px;
`}
>
<CommandCode>{commandsByPlatform[platform]}</CommandCode>
</EuiCodeBlock>
{platform !== 'googleCloudShell' && (
<EuiCodeBlock
onClick={onTextAreaClick}
fontSize="m"
isCopyable={!fullCopyButton}
paddingSize="m"
css={`
max-width: 1100px;
`}
>
<CommandCode>{commandsByPlatform[platform]}</CommandCode>
</EuiCodeBlock>
)}
<EuiSpacer size="s" />
{fullCopyButton && (
<EuiCopy textToCopy={commandsByPlatform[platform]}>

View file

@ -33,3 +33,4 @@ export * from './use_fleet_server_hosts_for_policy';
export * from './use_fleet_server_standalone';
export * from './use_locator';
export * from './use_create_cloud_formation_url';
export * from './use_create_cloud_shell_url';

View file

@ -0,0 +1,52 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { PackagePolicy } from '../../common';
import { getCloudShellUrlFromPackagePolicy } from '../services';
import { useGetSettings } from './use_request';
export const useCreateCloudShellUrl = ({
enrollmentAPIKey,
packagePolicy,
}: {
enrollmentAPIKey: string | undefined;
packagePolicy?: PackagePolicy;
}) => {
const { data, isLoading } = useGetSettings();
let isError = false;
let error: string | undefined;
// Default fleet server host
const fleetServerHost = data?.item.fleet_server_hosts?.[0];
if (!fleetServerHost && !isLoading) {
isError = true;
error = i18n.translate('xpack.fleet.agentEnrollment.cloudShell.noFleetServerHost', {
defaultMessage: 'No Fleet Server host found',
});
}
if (!enrollmentAPIKey && !isLoading) {
isError = true;
error = i18n.translate('xpack.fleet.agentEnrollment.cloudShell.noApiKey', {
defaultMessage: 'No enrollment token found',
});
}
const cloudShellUrl = getCloudShellUrlFromPackagePolicy(packagePolicy) || '';
return {
isLoading,
cloudShellUrl,
isError,
error,
};
};

View file

@ -8,7 +8,14 @@
import { useState } from 'react';
import { i18n } from '@kbn/i18n';
export type PLATFORM_TYPE = 'linux' | 'mac' | 'windows' | 'rpm' | 'deb' | 'kubernetes';
export type PLATFORM_TYPE =
| 'linux'
| 'mac'
| 'windows'
| 'rpm'
| 'deb'
| 'kubernetes'
| 'googleCloudShell';
export const REDUCED_PLATFORM_OPTIONS: Array<{
label: string;
@ -63,6 +70,17 @@ export const PLATFORM_OPTIONS = [
},
];
export const PLATFORM_OPTIONS_CLOUD_SHELL = [
...PLATFORM_OPTIONS,
{
id: 'googleCloudShell',
label: i18n.translate('xpack.fleet.enrollmentInstructions.platformButtons.googleCloudShell', {
defaultMessage: 'Google Cloud Shell Script',
}),
'data-test-subj': 'platformTypeGoogleCloudShellScript',
},
];
export function usePlatform(initialPlatform: PLATFORM_TYPE = 'linux') {
const [platform, setPlatform] = useState<PLATFORM_TYPE>(initialPlatform);

View file

@ -0,0 +1,68 @@
/*
* 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 { getCloudShellUrlFromAgentPolicy } from './get_cloud_shell_url_from_agent_policy';
describe('getCloudShellUrlFromAgentPolicy', () => {
it('should return undefined when selectedPolicy is undefined', () => {
const result = getCloudShellUrlFromAgentPolicy();
expect(result).toBeUndefined();
});
it('should return undefined when selectedPolicy has no package_policies', () => {
const selectedPolicy = {};
// @ts-expect-error
const result = getCloudShellUrlFromAgentPolicy(selectedPolicy);
expect(result).toBeUndefined();
});
it('should return undefined when no input has enabled and config.cloud_shell_url', () => {
const selectedPolicy = {
package_policies: [
{
inputs: [
{ enabled: false, config: {} },
{ enabled: true, config: {} },
{ enabled: true, config: { other_property: 'value' } },
],
},
{
inputs: [
{ enabled: false, config: {} },
{ enabled: false, config: {} },
],
},
],
};
// @ts-expect-error
const result = getCloudShellUrlFromAgentPolicy(selectedPolicy);
expect(result).toBeUndefined();
});
it('should return the first config.cloud_shell_url when available', () => {
const selectedPolicy = {
package_policies: [
{
inputs: [
{ enabled: false, config: { cloud_shell_url: { value: 'url1' } } },
{ enabled: false, config: { cloud_shell_url: { value: 'url2' } } },
{ enabled: false, config: { other_property: 'value' } },
],
},
{
inputs: [
{ enabled: false, config: {} },
{ enabled: true, config: { cloud_shell_url: { value: 'url3' } } },
{ enabled: true, config: { cloud_shell_url: { value: 'url4' } } },
],
},
],
};
// @ts-expect-error
const result = getCloudShellUrlFromAgentPolicy(selectedPolicy);
expect(result).toBe('url3');
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { AgentPolicy } from '../types';
/**
* Get the cloud shell url from a agent policy
* It looks for a config with a cloud_shell_url object present in
* the enabled package_policies inputs of the agent policy
*/
export const getCloudShellUrlFromAgentPolicy = (selectedPolicy?: AgentPolicy) => {
const cloudShellUrl = selectedPolicy?.package_policies?.reduce((acc, packagePolicy) => {
const findCloudShellUrlConfig = packagePolicy.inputs?.reduce((accInput, input) => {
if (accInput !== '') {
return accInput;
}
if (input?.enabled && input?.config?.cloud_shell_url) {
return input.config.cloud_shell_url.value;
}
return accInput;
}, '');
if (findCloudShellUrlConfig) {
return findCloudShellUrlConfig;
}
return acc;
}, '');
return cloudShellUrl !== '' ? cloudShellUrl : undefined;
};

View file

@ -0,0 +1,61 @@
/*
* 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 { getCloudShellUrlFromPackagePolicy } from './get_cloud_shell_url_from_package_policy';
describe('getCloudShellUrlFromPackagePolicyy', () => {
test('returns undefined when packagePolicy is undefined', () => {
const result = getCloudShellUrlFromPackagePolicy(undefined);
expect(result).toBeUndefined();
});
test('returns undefined when packagePolicy is defined but inputs are empty', () => {
const packagePolicy = { inputs: [] };
// @ts-expect-error
const result = getCloudShellUrlFromPackagePolicy(packagePolicy);
expect(result).toBeUndefined();
});
test('returns undefined when no enabled input has a CloudShellUrl', () => {
const packagePolicy = {
inputs: [
{ enabled: false, config: { cloud_shell_url: { value: 'url1' } } },
{ enabled: false, config: { cloud_shell_url: { value: 'url2' } } },
],
};
// @ts-expect-error
const result = getCloudShellUrlFromPackagePolicy(packagePolicy);
expect(result).toBeUndefined();
});
test('returns the CloudShellUrl of the first enabled input', () => {
const packagePolicy = {
inputs: [
{ enabled: false, config: { cloud_shell_url: { value: 'url1' } } },
{ enabled: true, config: { cloud_shell_url: { value: 'url2' } } },
{ enabled: true, config: { cloud_shell_url: { value: 'url3' } } },
],
};
// @ts-expect-error
const result = getCloudShellUrlFromPackagePolicy(packagePolicy);
expect(result).toBe('url2');
});
test('returns the CloudShellUrl of the first enabled input and ignores subsequent inputs', () => {
const packagePolicy = {
inputs: [
{ enabled: true, config: { cloud_shell_url: { value: 'url1' } } },
{ enabled: true, config: { cloud_shell_url: { value: 'url2' } } },
{ enabled: true, config: { cloud_shell_url: { value: 'url3' } } },
],
};
// @ts-expect-error
const result = getCloudShellUrlFromPackagePolicy(packagePolicy);
expect(result).toBe('url1');
});
// Add more test cases as needed
});

View file

@ -0,0 +1,27 @@
/*
* 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 { PackagePolicy } from '../types';
/**
* Get the cloud shell url from a package policy
* It looks for a config with a cloud_shell_url object present in
* the enabled inputs of the package policy
*/
export const getCloudShellUrlFromPackagePolicy = (packagePolicy?: PackagePolicy) => {
const cloudShellUrl = packagePolicy?.inputs?.reduce((accInput, input) => {
if (accInput !== '') {
return accInput;
}
if (input?.enabled && input?.config?.cloud_shell_url) {
return input.config.cloud_shell_url.value;
}
return accInput;
}, '');
return cloudShellUrl !== '' ? cloudShellUrl : undefined;
};

View file

@ -51,3 +51,5 @@ export { incrementPolicyName } from './increment_policy_name';
export { getCloudFormationPropsFromPackagePolicy } from './get_cloud_formation_props_from_package_policy';
export { getCloudFormationTemplateUrlFromAgentPolicy } from './get_cloud_formation_template_url_from_agent_policy';
export { getCloudFormationTemplateUrlFromPackageInfo } from './get_cloud_formation_template_url_from_package_info';
export { getCloudShellUrlFromPackagePolicy } from './get_cloud_shell_url_from_package_policy';
export { getCloudShellUrlFromAgentPolicy } from './get_cloud_shell_url_from_agent_policy';