diff --git a/src/dev/build/tasks/fetch_agent_versions_list.ts b/src/dev/build/tasks/fetch_agent_versions_list.ts index bc4143f5000da..560c52fad07ba 100644 --- a/src/dev/build/tasks/fetch_agent_versions_list.ts +++ b/src/dev/build/tasks/fetch_agent_versions_list.ts @@ -19,29 +19,22 @@ const getAvailableVersions = async (log: ToolingLog) => { }; // Endpoint maintained by the web-team and hosted on the elastic website // See https://github.com/elastic/website-development/issues/9331 - const url = 'https://www.elastic.co/content/product_versions'; - log.info('Fetching Elastic Agent versions list'); - const results = await fetch(url, options); - const rawBody = await results.text(); - + const url = 'https://www.elastic.co/api/product_versions'; try { - const jsonBody = JSON.parse(rawBody); + log.info('Fetching Elastic Agent versions list'); + const results = await fetch(url, options); + + const jsonBody = await results.json(); const versions: string[] = (jsonBody.length ? jsonBody[0] : []) .filter((item: any) => item?.title?.includes('Elastic Agent')) .map((item: any) => item?.version_number); - log.info(`Retrieved available Elastic Agent versions`); + log.info(`Retrieved available versions`); return versions; } catch (error) { - log.warning(`Failed to fetch Elastic Agent versions list`); - log.info(`Status: ${results.status}`); - log.info(rawBody); - if (process.env.BUILDKITE_PULL_REQUEST === 'true') { - log.warning(error); - } else { - throw new Error(error); - } + log.warning(`Failed to fetch versions list`); + log.warning(error); } return []; }; @@ -54,8 +47,8 @@ export const FetchAgentVersionsList: Task = { const versionsList = await getAvailableVersions(log); const AGENT_VERSION_BUILD_FILE = 'x-pack/plugins/fleet/target/agent_versions_list.json'; - if (versionsList.length !== 0) { - log.info(`Writing Elastic Agent versions list to ${AGENT_VERSION_BUILD_FILE}`); + if (versionsList !== []) { + log.info(`Writing versions list to ${AGENT_VERSION_BUILD_FILE}`); await write( build.resolvePath(AGENT_VERSION_BUILD_FILE), JSON.stringify(versionsList, null, ' ') diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 37453e1364f8d..fd99edd8705e7 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -63,6 +63,8 @@ export const CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE = 'csp-rule-template'; export const CLOUDBEAT_VANILLA = 'cloudbeat/cis_k8s'; export const CLOUDBEAT_EKS = 'cloudbeat/cis_eks'; +export const CLOUDBEAT_AKS = 'cloudbeat/cis_aks'; +export const CLOUDBEAT_GKE = 'cloudbeat/cis_gke'; export const CLOUDBEAT_AWS = 'cloudbeat/cis_aws'; export const CLOUDBEAT_GCP = 'cloudbeat/cis_gcp'; export const CLOUDBEAT_AZURE = 'cloudbeat/cis_azure'; diff --git a/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_aks_logo.svg b/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_aks_logo.svg new file mode 100644 index 0000000000000..f81d0ae7d0370 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_aks_logo.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_gke_logo.svg b/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_gke_logo.svg new file mode 100644 index 0000000000000..6dd28d61ebf0d --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_gke_logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 54e0b7467069a..185cd19f200cf 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -20,9 +20,13 @@ import { VULN_MGMT_POLICY_TEMPLATE, CLOUDBEAT_VULN_MGMT_GCP, CLOUDBEAT_VULN_MGMT_AZURE, + CLOUDBEAT_AKS, + CLOUDBEAT_GKE, } from '../../common/constants'; import eksLogo from '../assets/icons/cis_eks_logo.svg'; +import aksLogo from '../assets/icons/cis_aks_logo.svg'; +import gkeLogo from '../assets/icons/cis_gke_logo.svg'; export const statusColors = { passed: euiThemeVars.euiColorVis0, @@ -48,7 +52,7 @@ export interface CloudPostureIntegrationProps { name: string; shortName: string; options: Array<{ - type: PostureInput; + type: PostureInput | typeof CLOUDBEAT_AKS | typeof CLOUDBEAT_GKE; name: string; benchmark: string; disabled?: boolean; @@ -119,7 +123,7 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { { type: CLOUDBEAT_VANILLA, name: i18n.translate('xpack.csp.kspmIntegration.vanillaOption.nameTitle', { - defaultMessage: 'Self-Managed/Vanilla Kubernetes', + defaultMessage: 'Self-Managed', }), benchmark: i18n.translate('xpack.csp.kspmIntegration.vanillaOption.benchmarkTitle', { defaultMessage: 'CIS Kubernetes', @@ -129,12 +133,43 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { { type: CLOUDBEAT_EKS, name: i18n.translate('xpack.csp.kspmIntegration.eksOption.nameTitle', { - defaultMessage: 'EKS (Elastic Kubernetes Service)', + defaultMessage: 'EKS', }), benchmark: i18n.translate('xpack.csp.kspmIntegration.eksOption.benchmarkTitle', { defaultMessage: 'CIS EKS', }), icon: eksLogo, + tooltip: i18n.translate('xpack.csp.kspmIntegration.eksOption.tooltipContent', { + defaultMessage: 'Elastic Kubernetes Service', + }), + }, + { + type: CLOUDBEAT_AKS, + name: i18n.translate('xpack.csp.kspmIntegration.aksOption.nameTitle', { + defaultMessage: 'AKS', + }), + benchmark: i18n.translate('xpack.csp.kspmIntegration.aksOption.benchmarkTitle', { + defaultMessage: 'CIS AKS', + }), + disabled: true, + icon: aksLogo, + tooltip: i18n.translate('xpack.csp.kspmIntegration.aksOption.tooltipContent', { + defaultMessage: 'Azure Kubernetes Service - Coming soon', + }), + }, + { + type: CLOUDBEAT_GKE, + name: i18n.translate('xpack.csp.kspmIntegration.gkeOption.nameTitle', { + defaultMessage: 'GKE', + }), + benchmark: i18n.translate('xpack.csp.kspmIntegration.gkeOption.benchmarkTitle', { + defaultMessage: 'CIS GKE', + }), + disabled: true, + icon: gkeLogo, + tooltip: i18n.translate('xpack.csp.kspmIntegration.gkeOption.tooltipContent', { + defaultMessage: 'Google Kubernetes Engine - Coming soon', + }), }, ], }, diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx index 7f3b7dc51319e..3427c25e49c19 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx @@ -101,8 +101,8 @@ describe('', () => { it('renders KSPM input selector', () => { const { getByLabelText } = render(); - const option1 = getByLabelText('Self-Managed/Vanilla Kubernetes'); - const option2 = getByLabelText('EKS (Elastic Kubernetes Service)'); + const option1 = getByLabelText('Self-Managed'); + const option2 = getByLabelText('EKS'); expect(option1).toBeInTheDocument(); expect(option2).toBeInTheDocument(); @@ -116,7 +116,7 @@ describe('', () => { const eksPolicy = getMockPolicyEKS(); const { getByLabelText } = render(); - const option = getByLabelText('EKS (Elastic Kubernetes Service)'); + const option = getByLabelText('EKS'); userEvent.click(option); // Listen to the 2nd triggered by the test. @@ -148,8 +148,8 @@ describe('', () => { ); - const option1 = getByLabelText('Self-Managed/Vanilla Kubernetes'); - const option2 = getByLabelText('EKS (Elastic Kubernetes Service)'); + const option1 = getByLabelText('Self-Managed'); + const option2 = getByLabelText('EKS'); expect(option1).toBeInTheDocument(); expect(option2).toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts index 360becd415646..855dd3a3fe439 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts @@ -328,7 +328,7 @@ export class EndpointActionGenerator extends BaseDataGenerator { stderr_truncated: true, shell_code: 0, shell: 'bash', - cwd: '/some/path', + cwd: this.randomChoice(['/some/path', '/a-very/long/path'.repeat(30)]), output_file_id: 'some-output-file-id', output_file_stdout_truncated: this.randomChoice([true, false]), output_file_stderr_truncated: this.randomChoice([true, false]), diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.test.tsx index b0c33c5d3ba39..956118e1ac297 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.test.tsx @@ -43,17 +43,42 @@ describe('When using the `ExecuteActionHostResponse` component', () => { }; }); + const outputSuffix = 'output'; + + it('should show shell info and shell code', async () => { + render(); + const { queryByTestId } = renderResult; + expect(queryByTestId(`test-executeResponseOutput-context`)).toBeInTheDocument(); + expect(queryByTestId(`test-executeResponseOutput-shell`)).toBeInTheDocument(); + expect(queryByTestId(`test-executeResponseOutput-cwd`)).toBeInTheDocument(); + }); + + it('should show execute context accordion as `closed`', async () => { + render(); + expect(renderResult.getByTestId('test-executeResponseOutput-context').className).toEqual( + 'euiAccordion' + ); + }); + + it('should show current working directory', async () => { + render(); + const { queryByTestId } = renderResult; + expect(queryByTestId(`test-executeResponseOutput-context`)).toBeInTheDocument(); + expect(queryByTestId(`test-executeResponseOutput-cwd`)).toBeInTheDocument(); + }); + it('should show execute output and execute errors', async () => { render(); - expect(renderResult.getByTestId('test-executeResponseOutput')).toBeTruthy(); + const { queryByTestId } = renderResult; + expect(queryByTestId(`test-executeResponseOutput-${outputSuffix}`)).toBeInTheDocument(); + expect(queryByTestId(`test-executeResponseOutput-error`)).toBeInTheDocument(); }); it('should show execute output accordion as `open`', async () => { render(); - const accordionOutputButton = Array.from( - renderResult.getByTestId('test-executeResponseOutput').querySelectorAll('.euiAccordion') - )[0]; - expect(accordionOutputButton.className).toContain('isOpen'); + expect( + renderResult.getByTestId(`test-executeResponseOutput-${outputSuffix}`).className + ).toContain('isOpen'); }); it('should show `-` in output accordion when no output content', async () => { @@ -66,13 +91,11 @@ describe('When using the `ExecuteActionHostResponse` component', () => { }, }, }; + render(); - const accordionOutputButton = Array.from( - renderResult.getByTestId('test-executeResponseOutput').querySelectorAll('.euiAccordion') - )[0]; - expect(accordionOutputButton.textContent).toContain( - `Execution output (truncated)${getEmptyValue()}` - ); + expect( + renderResult.getByTestId(`test-executeResponseOutput-${outputSuffix}`).textContent + ).toContain(`Execution output (truncated)${getEmptyValue()}`); }); it('should show `-` in error accordion when no error content', async () => { @@ -85,18 +108,19 @@ describe('When using the `ExecuteActionHostResponse` component', () => { }, }, }; + render(); - const accordionErrorButton = Array.from( - renderResult.getByTestId('test-executeResponseOutput').querySelectorAll('.euiAccordion') - )[1]; - expect(accordionErrorButton.textContent).toContain( + expect(renderResult.getByTestId('test-executeResponseOutput-error').textContent).toContain( `Execution error (truncated)${getEmptyValue()}` ); }); it('should not show execute output accordions when no output in action details', () => { (renderProps.action as ActionDetails).outputs = undefined; + render(); - expect(renderResult.queryByTestId('test-executeResponseOutput')).toBeNull(); + const { queryByTestId } = renderResult; + expect(queryByTestId(`test-executeResponseOutput-context`)).not.toBeInTheDocument(); + expect(queryByTestId(`test-executeResponseOutput-${outputSuffix}`)).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx index 1db3c4cd04983..85f3444d6ef3d 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx @@ -5,16 +5,30 @@ * 2.0. */ -import React, { memo } from 'react'; -import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiText, useGeneratedHtmlId } from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import { + EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + useGeneratedHtmlId, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; import type { ResponseActionExecuteOutputContent } from '../../../../common/endpoint/types'; import { getEmptyValue } from '../../../common/components/empty_value'; const emptyValue = getEmptyValue(); const ACCORDION_BUTTON_TEXT = Object.freeze({ + context: i18n.translate( + 'xpack.securitySolution.responseActionExecuteAccordion.executionContext', + { + defaultMessage: 'Execution context', + } + ), output: { regular: i18n.translate( 'xpack.securitySolution.responseActionExecuteAccordion.outputButtonTextRegular', @@ -44,36 +58,95 @@ const ACCORDION_BUTTON_TEXT = Object.freeze({ ), }, }); + +const SHELL_INFO = Object.freeze({ + shell: i18n.translate('xpack.securitySolution.responseActionExecuteAccordion.shellInformation', { + defaultMessage: 'Shell', + }), + + returnCode: i18n.translate( + 'xpack.securitySolution.responseActionExecuteAccordion.shellReturnCode', + { + defaultMessage: 'Return code', + } + ), + currentDir: i18n.translate( + 'xpack.securitySolution.responseActionExecuteAccordion.currentWorkingDirectory', + { + defaultMessage: 'Current working directory', + } + ), +}); + +const StyledEuiText = euiStyled(EuiText)` + white-space: pre-wrap; + line-break: anywhere; +`; + +interface ShellInfoContentProps { + content: string | number; + textSize?: 's' | 'xs'; + title: string; +} +const ShellInfoContent = memo(({ content, textSize, title }) => ( + + + {title} + {': '} + + {content} + +)); + +ShellInfoContent.displayName = 'ShellInfoContent'; + interface ExecuteActionOutputProps { - content?: string; + content?: string | React.ReactNode; initialIsOpen?: boolean; isTruncated?: boolean; textSize?: 's' | 'xs'; - type: 'error' | 'output'; + type: 'error' | 'output' | 'context'; + 'data-test-subj'?: string; } const ExecutionActionOutputAccordion = memo( - ({ content = emptyValue, initialIsOpen = false, isTruncated = false, textSize, type }) => { + ({ + content = emptyValue, + initialIsOpen = false, + isTruncated = false, + textSize, + type, + 'data-test-subj': dataTestSubj, + }) => { const id = useGeneratedHtmlId({ prefix: 'executeActionOutputAccordions', suffix: type, }); + + const accordionButtonContent = useMemo( + () => ( + + {type !== 'context' + ? isTruncated + ? ACCORDION_BUTTON_TEXT[type].truncated + : ACCORDION_BUTTON_TEXT[type].regular + : ACCORDION_BUTTON_TEXT[type]} + + ), + [isTruncated, textSize, type] + ); + return ( - -

{content}

-
+ + {typeof content === 'string' ?

{content}

: content} +
); } @@ -87,24 +160,70 @@ export interface ExecuteActionHostResponseOutputProps { } export const ExecuteActionHostResponseOutput = memo( - ({ outputContent, 'data-test-subj': dataTestSubj, textSize = 'xs' }) => ( - - - - - - - ) + ({ outputContent, 'data-test-subj': dataTestSubj, textSize = 'xs' }) => { + const contextContent = useMemo( + () => ( + <> + + + + + + + + +
+ + +
+ + ), + [dataTestSubj, outputContent.cwd, outputContent.shell, outputContent.shell_code, textSize] + ); + return ( + <> + + + + + + + + + + + + ); + } ); ExecuteActionHostResponseOutput.displayName = 'ExecuteActionHostResponseOutput'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts index 321f04e5f4311..3750d70f6e020 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts @@ -406,9 +406,7 @@ describe('When using Actions service utilities', () => { ]) ).toEqual({ ...NOT_COMPLETED_OUTPUT, - outputs: { - '789': expect.any(Object), - }, + outputs: expect.any(Object), agentState: { '123': { completedAt: '2022-01-05T19:27:23.816Z', @@ -444,10 +442,7 @@ describe('When using Actions service utilities', () => { completedAt: COMPLETED_AT, wasSuccessful: true, errors: undefined, - outputs: { - 456: expect.any(Object), - 789: expect.any(Object), - }, + outputs: expect.any(Object), agentState: { '123': { completedAt: '2022-01-05T19:27:23.816Z', @@ -489,9 +484,7 @@ describe('When using Actions service utilities', () => { errors: ['Fleet action response error: something is no good'], isCompleted: true, wasSuccessful: false, - outputs: { - 789: expect.any(Object), - }, + outputs: expect.any(Object), agentState: { '123': { completedAt: '2022-01-05T19:27:23.816Z',