mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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-2917d108abb6e8ea0ca8
-997e-452d-8280-5180db34aaa9
This commit is contained in:
parent
e5a591c369
commit
a8f3a5ac8c
31 changed files with 1009 additions and 129 deletions
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface CloudSecurityIntegration {
|
|||
isLoading: boolean;
|
||||
isCloudFormation: boolean;
|
||||
cloudFormationProps?: CloudFormationProps;
|
||||
cloudShellUrl: string | undefined;
|
||||
}
|
||||
|
||||
export interface BaseProps {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -38,5 +38,6 @@ cd elastic-agent-${kibanaVersion}-windows-x86_64
|
|||
deb: linuxDebCommand,
|
||||
rpm: linuxRpmCommand,
|
||||
kubernetes: k8sCommand,
|
||||
googleCloudShell: '',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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]}>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue