From 079e929a1cfaac103dea4e301c9b697e561ffd17 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 28 Oct 2024 15:00:00 +0100 Subject: [PATCH 01/10] [Fleet] Fix agents count in agent list table and add tooltip with correct info (#197834) Fixes https://github.com/elastic/kibana/issues/195441 ## Summary Selection agent count on agent list table gets incorrect when there are multiple hosted agents, especially if they are on inactive state. In fact to calculate the selected number of agents we were getting hosted agents, but without taking into account the filtering applied on the page, i.e. we were always getting all the hosted agent (inactive too). This caused the final calculation to be off. In this PR I'm fixing [the query](https://github.com/elastic/kibana/pull/197834/files#diff-9707a4b93a96749876e4cf173a0b39cd5a620e311e2652c5ed4b8670ca7e6becR309-R320) used to get those agents to take in account the filters and I'm also adding a small tooltip that breaks up the number of agents (selected, total, hosted) ### Testing - Make sure to have many agents, hosted and not in different states (inactive, unenrolled) - To make a hosted agent inactive follow the steps explained [here](https://github.com/elastic/kibana/issues/195441) - Verify that the selection numbers are correct: select agents on all pages and hover on the new tooltip shown besides the "selected agents". This number should match the number shown on the actions dropdown ### Screenshots Screenshot 2024-10-25 at 17 00 44 Screenshot 2024-10-25 at 17 00 59 https://github.com/user-attachments/assets/c153c491-29a1-481c-a3e3-25bab6412963 ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine --- .../components/agents_selection_status.tsx | 50 ++++++++++++++++--- .../components/bulk_actions.tsx | 2 +- .../components/table_header.tsx | 3 ++ .../hooks/use_fetch_agents_data.tsx | 21 +++++--- .../sections/agents/agent_list_page/index.tsx | 1 + 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agents_selection_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agents_selection_status.tsx index 682aaa91af6b6..618a7a6b8e112 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agents_selection_status.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agents_selection_status.tsx @@ -7,7 +7,7 @@ import React from 'react'; import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonEmpty } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonEmpty, EuiIconTip } from '@elastic/eui'; import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; import { SO_SEARCH_LIMIT } from '../../../../constants'; @@ -33,6 +33,7 @@ const Button = styled(EuiButtonEmpty)` export const AgentsSelectionStatus: React.FunctionComponent<{ totalAgents: number; + totalManagedAgents: number; selectableAgents: number; managedAgentsOnCurrentPage: number; selectionMode: SelectionMode; @@ -41,6 +42,7 @@ export const AgentsSelectionStatus: React.FunctionComponent<{ setSelectedAgents: (agents: Agent[]) => void; }> = ({ totalAgents, + totalManagedAgents, selectableAgents, managedAgentsOnCurrentPage, selectionMode, @@ -71,11 +73,28 @@ export const AgentsSelectionStatus: React.FunctionComponent<{ }} /> ) : ( - + <> + {' '} + + } + /> + )} @@ -96,7 +115,24 @@ export const AgentsSelectionStatus: React.FunctionComponent<{ selectionMode, count: selectedAgents.length, }} - /> + />{' '} + {selectionMode === 'query' && ( + + } + /> + )} {showSelectEverything ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index e0235fab01446..c5fd1c2caec81 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -77,7 +77,7 @@ export const AgentBulkActions: React.FunctionComponent = ({ const [isRequestDiagnosticsModalOpen, setIsRequestDiagnosticsModalOpen] = useState(false); - // update the query removing the "managed" agents + // update the query removing the "managed" agents in any state (unenrolled, offline, etc) const selectionQuery = useMemo(() => { if (totalManagedAgentIds.length) { const excludedKuery = `${AGENTS_PREFIX}.agent.id : (${totalManagedAgentIds diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx index bcac45801be05..48757ecebdd80 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx @@ -21,6 +21,7 @@ export const AgentTableHeader: React.FunctionComponent<{ agentStatus?: { [k in SimplifiedAgentStatus]: number }; totalAgents: number; selectableAgents: number; + totalManagedAgents: number; managedAgentsOnCurrentPage: number; selectionMode: SelectionMode; setSelectionMode: (mode: SelectionMode) => void; @@ -31,6 +32,7 @@ export const AgentTableHeader: React.FunctionComponent<{ }> = ({ agentStatus, totalAgents, + totalManagedAgents, selectableAgents, managedAgentsOnCurrentPage, selectionMode, @@ -47,6 +49,7 @@ export const AgentTableHeader: React.FunctionComponent<{ `policy_id:"${policy.id}"`) - .join(' or '); + // Find all the agents that have managed policies + // to the correct ids we need to build the kuery applying the same filters as the global ones + const managedPoliciesKuery = getKuery({ + search, + selectedAgentPolicies: managedAgentPolicies.map((policy) => policy.id), + selectedTags, + selectedStatus, + }); const response = await sendGetAgents({ - kuery: `NOT (status:unenrolled) and ${policiesKuery}`, + kuery: `${managedPoliciesKuery}`, perPage: SO_SEARCH_LIMIT, - showInactive: true, + showInactive, }); if (response.error) { throw new Error(response.error.message); @@ -350,7 +354,6 @@ export function useFetchAgentsData() { fetchDataAsync(); }, [ - fullAgentPolicyFecher, pagination.currentPage, pagination.pageSize, kuery, @@ -359,8 +362,12 @@ export function useFetchAgentsData() { showInactive, showUpgradeable, displayAgentMetrics, + fullAgentPolicyFecher, allTags, latestAgentActionErrors, + search, + selectedTags, + selectedStatus, notifications.toasts, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 51f3fe68a9d95..a4171a8d5197a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -434,6 +434,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { {/* Agent total, bulk actions and status bar */} Date: Mon, 28 Oct 2024 15:09:47 +0100 Subject: [PATCH 02/10] [Cloud Security] exclude unknown findings from compliance score calculation (#197829) ## Summary Findings from 3rd party date can have `result.evaluation: unknown`. This leads to incorrect posture/compliance score in our flows. This PR removes these findings from the score calculation and graphical representation. properly introducing `unknown` in the compliance score UX flows will be solved separately - fixes https://github.com/elastic/security-team/issues/10913 ### Screenshots Screenshot 2024-10-25 at 14 19 03 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../src/constants/component_constants.ts | 1 + .../components/compliance_score_bar.test.tsx | 49 +++++++++ .../components/compliance_score_bar.tsx | 18 +++- .../public/components/test_subjects.ts | 4 + .../latest_findings_group_renderer.test.tsx | 101 ++++++++++++++++++ .../latest_findings_group_renderer.tsx | 8 +- .../misconfiguration_preview.tsx | 11 +- 7 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.test.tsx create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.test.tsx diff --git a/x-pack/packages/kbn-cloud-security-posture/public/src/constants/component_constants.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/constants/component_constants.ts index 04a47f0fc12a1..d4d436e981cc4 100644 --- a/x-pack/packages/kbn-cloud-security-posture/public/src/constants/component_constants.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/constants/component_constants.ts @@ -9,4 +9,5 @@ import { euiThemeVars } from '@kbn/ui-theme'; export const statusColors = { passed: euiThemeVars.euiColorSuccess, failed: euiThemeVars.euiColorVis9, + unknown: euiThemeVars.euiColorLightShade, }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.test.tsx new file mode 100644 index 0000000000000..166fb1184e0b9 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.test.tsx @@ -0,0 +1,49 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { ComplianceScoreBar } from './compliance_score_bar'; +import { + COMPLIANCE_SCORE_BAR_UNKNOWN, + COMPLIANCE_SCORE_BAR_PASSED, + COMPLIANCE_SCORE_BAR_FAILED, +} from './test_subjects'; + +describe('', () => { + it('should display 0% compliance score with status unknown when both passed and failed are 0', () => { + render(); + expect(screen.getByText('0%')).toBeInTheDocument(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_UNKNOWN)).not.toBeNull(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_FAILED)).toBeNull(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_PASSED)).toBeNull(); + }); + + it('should display 100% compliance score when passed is greater than 0 and failed is 0', () => { + render(); + expect(screen.getByText('100%')).toBeInTheDocument(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_PASSED)).not.toBeNull(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_FAILED)).toBeNull(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_UNKNOWN)).toBeNull(); + }); + + it('should display 0% compliance score when passed is 0 and failed is greater than 0', () => { + render(); + expect(screen.getByText('0%')).toBeInTheDocument(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_FAILED)).not.toBeNull(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_PASSED)).toBeNull(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_UNKNOWN)).toBeNull(); + }); + + it('should display 50% compliance score when passed is equal to failed', () => { + render(); + expect(screen.getByText('50%')).toBeInTheDocument(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_FAILED)).not.toBeNull(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_PASSED)).not.toBeNull(); + expect(screen.queryByTestId(COMPLIANCE_SCORE_BAR_UNKNOWN)).toBeNull(); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx index d4acbc97ab10c..3829542829909 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx @@ -11,7 +11,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { statusColors } from '@kbn/cloud-security-posture'; import { calculatePostureScore } from '../../common/utils/helpers'; -import { CSP_FINDINGS_COMPLIANCE_SCORE } from './test_subjects'; +import { + CSP_FINDINGS_COMPLIANCE_SCORE, + COMPLIANCE_SCORE_BAR_UNKNOWN, + COMPLIANCE_SCORE_BAR_FAILED, + COMPLIANCE_SCORE_BAR_PASSED, +} from './test_subjects'; /** * This component will take 100% of the width set by the parent @@ -59,12 +64,22 @@ export const ComplianceScoreBar = ({ gap: 1px; `} > + {!totalPassed && !totalFailed && ( + + )} {!!totalPassed && ( )} {!!totalFailed && ( @@ -73,6 +88,7 @@ export const ComplianceScoreBar = ({ flex: ${totalFailed}; background: ${statusColors.failed}; `} + data-test-subj={COMPLIANCE_SCORE_BAR_FAILED} /> )} diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts index d29971d3352e3..b609950720ecd 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts @@ -92,3 +92,7 @@ export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = { }; export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_posture_page_subscription_not_allowed'; + +export const COMPLIANCE_SCORE_BAR_UNKNOWN = 'complianceScoreBarUnknown'; +export const COMPLIANCE_SCORE_BAR_FAILED = 'complianceScoreBarFailed'; +export const COMPLIANCE_SCORE_BAR_PASSED = 'complianceScoreBarPassed'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.test.tsx new file mode 100644 index 0000000000000..60aa64aa88141 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 { render } from '@testing-library/react'; +import { useEuiTheme } from '@elastic/eui'; +import { ComplianceBarComponent } from './latest_findings_group_renderer'; +import { RawBucket } from '@kbn/grouping/src'; +import { FindingsGroupingAggregation } from './use_grouped_findings'; +import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; + +jest.mock('@elastic/eui', () => { + const actual = jest.requireActual('@elastic/eui'); + return { + ...actual, + useEuiTheme: jest.fn(), + }; +}); + +jest.mock('../../../components/compliance_score_bar', () => ({ + ComplianceScoreBar: jest.fn(() => null), +})); + +jest.mock('../../../components/cloud_security_grouping'); + +describe('', () => { + beforeEach(() => { + (useEuiTheme as jest.Mock).mockReturnValue({ euiTheme: { size: { s: 's' } } }); + (ComplianceScoreBar as jest.Mock).mockClear(); + }); + + it('renders ComplianceScoreBar with correct totalFailed and totalPassed, when total = failed+passed', () => { + const bucket = { + doc_count: 10, + failedFindings: { + doc_count: 4, + }, + passedFindings: { + doc_count: 6, + }, + } as RawBucket; + + render(); + + expect(ComplianceScoreBar).toHaveBeenCalledWith( + expect.objectContaining({ + totalFailed: 4, + totalPassed: 6, + }), + {} + ); + }); + + it('renders ComplianceScoreBar with correct totalFailed and totalPassed, when there are unknown findings', () => { + const bucket = { + doc_count: 10, + failedFindings: { + doc_count: 3, + }, + passedFindings: { + doc_count: 6, + }, + } as RawBucket; + + render(); + + expect(ComplianceScoreBar).toHaveBeenCalledWith( + expect.objectContaining({ + totalFailed: 3, + totalPassed: 6, + }), + {} + ); + }); + + it('renders ComplianceScoreBar with correct totalFailed and totalPassed, when there are no findings', () => { + const bucket = { + doc_count: 10, + failedFindings: { + doc_count: 0, + }, + passedFindings: { + doc_count: 0, + }, + } as RawBucket; + + render(); + + expect(ComplianceScoreBar).toHaveBeenCalledWith( + expect.objectContaining({ + totalFailed: 0, + totalPassed: 0, + }), + {} + ); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index b4ad5d15ec8e9..b41c5e4996db1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -198,11 +198,15 @@ const FindingsCountComponent = ({ bucket }: { bucket: RawBucket }) => { +export const ComplianceBarComponent = ({ + bucket, +}: { + bucket: RawBucket; +}) => { const { euiTheme } = useEuiTheme(); const totalFailed = bucket.failedFindings?.doc_count || 0; - const totalPassed = bucket.doc_count - totalFailed; + const totalPassed = bucket.passedFindings?.doc_count || 0; return ( ; - numberOfPassedFindings?: number; - numberOfFailedFindings?: number; }) => { return ( From a1684580bc3d6a54dc7e4375384ebaee1410b186 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 28 Oct 2024 15:12:23 +0100 Subject: [PATCH 03/10] [Authz] OAS Descriptions for Route Authz (#197001) Closes https://github.com/elastic/kibana/issues/191714 ## Summary Update process router to generate authz descriptions based on the new Route Security objects. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../src/extract_authz_description.test.ts | 81 +++++++++++++++++++ .../src/extract_authz_description.ts | 60 ++++++++++++++ .../src/process_router.test.ts | 35 +++++++- .../src/process_router.ts | 13 ++- .../src/process_versioned_router.test.ts | 21 +++++ .../src/process_versioned_router.ts | 9 +++ .../kbn-router-to-openapispec/src/type.ts | 3 + .../kbn-router-to-openapispec/tsconfig.json | 2 +- 8 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 packages/kbn-router-to-openapispec/src/extract_authz_description.test.ts create mode 100644 packages/kbn-router-to-openapispec/src/extract_authz_description.ts diff --git a/packages/kbn-router-to-openapispec/src/extract_authz_description.test.ts b/packages/kbn-router-to-openapispec/src/extract_authz_description.test.ts new file mode 100644 index 0000000000000..8da2324e68f02 --- /dev/null +++ b/packages/kbn-router-to-openapispec/src/extract_authz_description.test.ts @@ -0,0 +1,81 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { schema } from '@kbn/config-schema'; +import { extractAuthzDescription } from './extract_authz_description'; +import { InternalRouterRoute } from './type'; +import { RouteSecurity } from '@kbn/core-http-server'; + +describe('extractAuthzDescription', () => { + it('should return empty if route does not require privileges', () => { + const route: InternalRouterRoute = { + path: '/foo', + options: { access: 'internal' }, + handler: jest.fn(), + validationSchemas: { request: { body: schema.object({}) } }, + method: 'get', + isVersioned: false, + }; + const description = extractAuthzDescription(route.security); + expect(description).toBe(''); + }); + + it('should return route authz description for simple privileges', () => { + const routeSecurity: RouteSecurity = { + authz: { + requiredPrivileges: ['manage_spaces'], + }, + }; + const description = extractAuthzDescription(routeSecurity); + expect(description).toBe('[Authz] Route required privileges: ALL of [manage_spaces].'); + }); + + it('should return route authz description for privilege groups', () => { + { + const routeSecurity: RouteSecurity = { + authz: { + requiredPrivileges: [{ allRequired: ['console'] }], + }, + }; + const description = extractAuthzDescription(routeSecurity); + expect(description).toBe('[Authz] Route required privileges: ALL of [console].'); + } + { + const routeSecurity: RouteSecurity = { + authz: { + requiredPrivileges: [ + { + anyRequired: ['manage_spaces', 'taskmanager'], + }, + ], + }, + }; + const description = extractAuthzDescription(routeSecurity); + expect(description).toBe( + '[Authz] Route required privileges: ANY of [manage_spaces OR taskmanager].' + ); + } + { + const routeSecurity: RouteSecurity = { + authz: { + requiredPrivileges: [ + { + allRequired: ['console', 'filesManagement'], + anyRequired: ['manage_spaces', 'taskmanager'], + }, + ], + }, + }; + const description = extractAuthzDescription(routeSecurity); + expect(description).toBe( + '[Authz] Route required privileges: ALL of [console, filesManagement] AND ANY of [manage_spaces OR taskmanager].' + ); + } + }); +}); diff --git a/packages/kbn-router-to-openapispec/src/extract_authz_description.ts b/packages/kbn-router-to-openapispec/src/extract_authz_description.ts new file mode 100644 index 0000000000000..4cd6875913780 --- /dev/null +++ b/packages/kbn-router-to-openapispec/src/extract_authz_description.ts @@ -0,0 +1,60 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { AuthzEnabled, AuthzDisabled, InternalRouteSecurity } from '@kbn/core-http-server'; + +interface PrivilegeGroupValue { + allRequired: string[]; + anyRequired: string[]; +} + +export const extractAuthzDescription = (routeSecurity: InternalRouteSecurity | undefined) => { + if (!routeSecurity) { + return ''; + } + if (!('authz' in routeSecurity) || (routeSecurity.authz as AuthzDisabled).enabled === false) { + return ''; + } + + const privileges = (routeSecurity.authz as AuthzEnabled).requiredPrivileges; + + const groupedPrivileges = privileges.reduce( + (groups, privilege) => { + if (typeof privilege === 'string') { + groups.allRequired.push(privilege); + + return groups; + } + groups.allRequired.push(...(privilege.allRequired ?? [])); + groups.anyRequired.push(...(privilege.anyRequired ?? [])); + + return groups; + }, + { + anyRequired: [], + allRequired: [], + } + ); + + const getPrivilegesDescription = (allRequired: string[], anyRequired: string[]) => { + const allDescription = allRequired.length ? `ALL of [${allRequired.join(', ')}]` : ''; + const anyDescription = anyRequired.length ? `ANY of [${anyRequired.join(' OR ')}]` : ''; + + return `${allDescription}${allDescription && anyDescription ? ' AND ' : ''}${anyDescription}`; + }; + + const getDescriptionForRoute = () => { + const allRequired = [...groupedPrivileges.allRequired]; + const anyRequired = [...groupedPrivileges.anyRequired]; + + return `Route required privileges: ${getPrivilegesDescription(allRequired, anyRequired)}.`; + }; + + return `[Authz] ${getDescriptionForRoute()}`; +}; diff --git a/packages/kbn-router-to-openapispec/src/process_router.test.ts b/packages/kbn-router-to-openapispec/src/process_router.test.ts index 22e03efdf08fc..96a10b25d648a 100644 --- a/packages/kbn-router-to-openapispec/src/process_router.test.ts +++ b/packages/kbn-router-to-openapispec/src/process_router.test.ts @@ -11,7 +11,8 @@ import { schema } from '@kbn/config-schema'; import { Router } from '@kbn/core-http-router-server-internal'; import { OasConverter } from './oas_converter'; import { createOperationIdCounter } from './operation_id_counter'; -import { extractResponses, processRouter, type InternalRouterRoute } from './process_router'; +import { extractResponses, processRouter } from './process_router'; +import { type InternalRouterRoute } from './type'; describe('extractResponses', () => { let oasConverter: OasConverter; @@ -102,6 +103,24 @@ describe('processRouter', () => { handler: jest.fn(), validationSchemas: { request: { body: schema.object({}) } }, }, + { + path: '/qux', + method: 'post', + options: {}, + handler: jest.fn(), + validationSchemas: { request: { body: schema.object({}) } }, + security: { + authz: { + requiredPrivileges: [ + 'manage_spaces', + { + allRequired: ['taskmanager'], + anyRequired: ['console'], + }, + ], + }, + }, + }, ], } as unknown as Router; @@ -110,11 +129,23 @@ describe('processRouter', () => { version: '2023-10-31', }); - expect(Object.keys(result1.paths!)).toHaveLength(3); + expect(Object.keys(result1.paths!)).toHaveLength(4); const result2 = processRouter(testRouter, new OasConverter(), createOperationIdCounter(), { version: '2024-10-31', }); expect(Object.keys(result2.paths!)).toHaveLength(0); }); + + it('updates description with privileges required', () => { + const result = processRouter(testRouter, new OasConverter(), createOperationIdCounter(), { + version: '2023-10-31', + }); + + expect(result.paths['/qux']?.post).toBeDefined(); + + expect(result.paths['/qux']?.post?.description).toEqual( + '[Authz] Route required privileges: ALL of [manage_spaces, taskmanager] AND ANY of [console].' + ); + }); }); diff --git a/packages/kbn-router-to-openapispec/src/process_router.ts b/packages/kbn-router-to-openapispec/src/process_router.ts index f0d37fd208b7b..cb55af3735b34 100644 --- a/packages/kbn-router-to-openapispec/src/process_router.ts +++ b/packages/kbn-router-to-openapispec/src/process_router.ts @@ -27,7 +27,8 @@ import { } from './util'; import type { OperationIdCounter } from './operation_id_counter'; import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas'; -import type { CustomOperationObject } from './type'; +import type { CustomOperationObject, InternalRouterRoute } from './type'; +import { extractAuthzDescription } from './extract_authz_description'; export const processRouter = ( appRouter: Router, @@ -63,10 +64,19 @@ export const processRouter = ( parameters.push(...pathObjects, ...queryObjects); } + let description = ''; + if (route.security) { + const authzDescription = extractAuthzDescription(route.security); + + description = `${route.options.description ?? ''}${authzDescription ?? ''}`; + } + const hasDeprecations = !!route.options.deprecated; + const operation: CustomOperationObject = { summary: route.options.summary ?? '', tags: route.options.tags ? extractTags(route.options.tags) : [], + ...(description ? { description } : {}), ...(route.options.description ? { description: route.options.description } : {}), ...(hasDeprecations ? { deprecated: true } : {}), ...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}), @@ -99,7 +109,6 @@ export const processRouter = ( return { paths }; }; -export type InternalRouterRoute = ReturnType[0]; export const extractResponses = (route: InternalRouterRoute, converter: OasConverter) => { const responses: OpenAPIV3.ResponsesObject = {}; if (!route.validationSchemas) return responses; diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts index f9f4f4898c1d0..3738c207f1f78 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts @@ -144,6 +144,22 @@ describe('processVersionedRouter', () => { 'application/test+json; Elastic-Api-Version=2023-10-31', ]); }); + + it('correctly updates the authz description for routes that require privileges', () => { + const results = processVersionedRouter( + { getRoutes: () => [createTestRoute()] } as unknown as CoreVersionedRouter, + new OasConverter(), + createOperationIdCounter(), + {} + ); + expect(results.paths['/foo']).toBeDefined(); + + expect(results.paths['/foo']!.get).toBeDefined(); + + expect(results.paths['/foo']!.get!.description).toBe( + '[Authz] Route required privileges: ALL of [manage_spaces].' + ); + }); }); const createTestRoute: () => VersionedRouterRoute = () => ({ @@ -155,6 +171,11 @@ const createTestRoute: () => VersionedRouterRoute = () => ({ deprecated: true, discontinued: 'discontinued versioned router', options: { body: { access: ['application/test+json'] } as any }, + security: { + authz: { + requiredPrivileges: ['manage_spaces'], + }, + }, }, handlers: [ { diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts index 7eee0d20c11d2..5dad5677c94ac 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts @@ -14,6 +14,7 @@ import { } from '@kbn/core-http-router-server-internal'; import type { RouteMethod, VersionedRouterRoute } from '@kbn/core-http-server'; import type { OpenAPIV3 } from 'openapi-types'; +import { extractAuthzDescription } from './extract_authz_description'; import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas'; import type { OasConverter } from './oas_converter'; import { isReferenceObject } from './oas_converter/common'; @@ -90,6 +91,13 @@ export const processVersionedRouter = ( ]; } + let description = ''; + if (route.options.security) { + const authzDescription = extractAuthzDescription(route.options.security); + + description = `${route.options.description ?? ''}${authzDescription ?? ''}`; + } + const hasBody = Boolean(extractValidationSchemaFromVersionedHandler(handler)?.request?.body); const contentType = extractContentType(route.options.options?.body); const hasVersionFilter = Boolean(filters?.version); @@ -98,6 +106,7 @@ export const processVersionedRouter = ( const operation: OpenAPIV3.OperationObject = { summary: route.options.summary ?? '', tags: route.options.options?.tags ? extractTags(route.options.options.tags) : [], + ...(description ? { description } : {}), ...(route.options.description ? { description: route.options.description } : {}), ...(hasDeprecations ? { deprecated: true } : {}), ...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}), diff --git a/packages/kbn-router-to-openapispec/src/type.ts b/packages/kbn-router-to-openapispec/src/type.ts index 5c5f992a0de0f..f57e4d00ad7db 100644 --- a/packages/kbn-router-to-openapispec/src/type.ts +++ b/packages/kbn-router-to-openapispec/src/type.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { Router } from '@kbn/core-http-router-server-internal'; import type { OpenAPIV3 } from '../openapi-types'; export type { OpenAPIV3 } from '../openapi-types'; export interface KnownParameters { @@ -39,3 +40,5 @@ export type CustomOperationObject = OpenAPIV3.OperationObject<{ // Custom OpenAPI from ES API spec based on @availability 'x-state'?: 'Technical Preview' | 'Beta'; }>; + +export type InternalRouterRoute = ReturnType[0]; diff --git a/packages/kbn-router-to-openapispec/tsconfig.json b/packages/kbn-router-to-openapispec/tsconfig.json index d82ca0bf48910..3536a90a8256f 100644 --- a/packages/kbn-router-to-openapispec/tsconfig.json +++ b/packages/kbn-router-to-openapispec/tsconfig.json @@ -17,6 +17,6 @@ "@kbn/core-http-router-server-internal", "@kbn/core-http-server", "@kbn/config-schema", - "@kbn/zod" + "@kbn/zod", ] } From e6bb35ac3d2376b616df4e687517d8ae650dca50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Mon, 28 Oct 2024 10:21:25 -0400 Subject: [PATCH 04/10] Add tags to connector run failures indicating if it's user or framework error (#197818) Resolves https://github.com/elastic/kibana/issues/197315 In this PR, I'm adding the following tags to the connector failure logs so it makes it easier to filter for systematic errors. - `connector-run-failed` for logs specific to connector run failures - `user-error` for errors caused by the user - `framework-error` for systematic errors ## To verify You can either use the jest test to observe the returned flags or set your logging to JSON and make connectors fail. kibana.yml to set logging to JSON ``` logging: appenders: json-layout: type: console layout: type: json root: appenders: [json-layout] ``` --- .../server/lib/task_runner_factory.test.ts | 15 ++++++-- .../actions/server/lib/task_runner_factory.ts | 36 ++++++++++++++----- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 6c4cdd31ccf6c..9733b56638d77 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -885,7 +885,8 @@ describe('Task Runner Factory', () => { expect(err).toBeDefined(); expect(isRetryableError(err)).toEqual(false); expect(taskRunnerFactoryInitializerParams.logger.error as jest.Mock).toHaveBeenCalledWith( - `Action '2' failed: Error message` + `Action '2' failed: Error message`, + { tags: ['connector-run-failed', 'framework-error'] } ); expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK); }); @@ -934,7 +935,8 @@ describe('Task Runner Factory', () => { expect(err).toBeDefined(); expect(taskRunnerFactoryInitializerParams.logger.error as jest.Mock).toHaveBeenCalledWith( - `Action '2' failed: Error message: Service message` + `Action '2' failed: Error message: Service message`, + { tags: ['connector-run-failed', 'framework-error'] } ); }); @@ -1033,7 +1035,8 @@ describe('Task Runner Factory', () => { } expect(err).toBeDefined(); expect(taskRunnerFactoryInitializerParams.logger.error as jest.Mock).toHaveBeenCalledWith( - `Action '2' failed: Fail` + `Action '2' failed: Fail`, + { tags: ['connector-run-failed', 'framework-error'] } ); expect(thrownError).toEqual(err); expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK); @@ -1140,10 +1143,16 @@ describe('Task Runner Factory', () => { try { await taskRunner.run(); + throw new Error('Should have thrown'); } catch (e) { expect(mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(1); expect(getErrorSource(e)).toBe(TaskErrorSource.FRAMEWORK); expect(e).toEqual(error); + + expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith( + `Failed to load action task params ${mockedTaskInstance.params.actionTaskParamsId}: test`, + { tags: ['connector-run-failed', 'framework-error'] } + ); } }); }); diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index d6b418c481ea5..d067ddaaae7ad 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -115,7 +115,8 @@ export class TaskRunnerFactory { } = await getActionTaskParams( actionTaskExecutorParams, encryptedSavedObjectsClient, - spaceIdToNamespace + spaceIdToNamespace, + logger ); const { spaceId } = actionTaskExecutorParams; @@ -139,12 +140,18 @@ export class TaskRunnerFactory { ...getSource(references, source), }); } catch (e) { - logger.error(`Action '${actionId}' failed: ${e.message}`); + const errorSource = + e instanceof ActionTypeDisabledError + ? TaskErrorSource.USER + : getErrorSource(e) || TaskErrorSource.FRAMEWORK; + logger.error(`Action '${actionId}' failed: ${e.message}`, { + tags: ['connector-run-failed', `${errorSource}-error`], + }); if (e instanceof ActionTypeDisabledError) { // We'll stop re-trying due to action being forbidden - throwUnrecoverableError(createTaskRunError(e, TaskErrorSource.USER)); + throwUnrecoverableError(createTaskRunError(e, errorSource)); } - throw createTaskRunError(e, getErrorSource(e) || TaskErrorSource.FRAMEWORK); + throw createTaskRunError(e, errorSource); } inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_EXECUTIONS); @@ -155,7 +162,9 @@ export class TaskRunnerFactory { if (executorResult.serviceMessage) { message = `${message}: ${executorResult.serviceMessage}`; } - logger.error(`Action '${actionId}' failed: ${message}`); + logger.error(`Action '${actionId}' failed: ${message}`, { + tags: ['connector-run-failed', `${executorResult.errorSource}-error`], + }); // Task manager error handler only kicks in when an error thrown (at this time) // So what we have to do is throw when the return status is `error`. @@ -175,7 +184,8 @@ export class TaskRunnerFactory { } = await getActionTaskParams( actionTaskExecutorParams, encryptedSavedObjectsClient, - spaceIdToNamespace + spaceIdToNamespace, + logger ); const request = getFakeRequest(apiKey); @@ -239,7 +249,8 @@ function getFakeRequest(apiKey?: string) { async function getActionTaskParams( executorParams: ActionTaskExecutorParams, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, - spaceIdToNamespace: SpaceIdToNamespaceFunction + spaceIdToNamespace: SpaceIdToNamespaceFunction, + logger: Logger ): Promise { const { spaceId } = executorParams; const namespace = spaceIdToNamespace(spaceId); @@ -268,10 +279,17 @@ async function getActionTaskParams( }, }; } catch (e) { + const errorSource = SavedObjectsErrorHelpers.isNotFoundError(e) + ? TaskErrorSource.USER + : TaskErrorSource.FRAMEWORK; + logger.error( + `Failed to load action task params ${executorParams.actionTaskParamsId}: ${e.message}`, + { tags: ['connector-run-failed', `${errorSource}-error`] } + ); if (SavedObjectsErrorHelpers.isNotFoundError(e)) { - throw createRetryableError(createTaskRunError(e, TaskErrorSource.USER), true); + throw createRetryableError(createTaskRunError(e, errorSource), true); } - throw createRetryableError(createTaskRunError(e, TaskErrorSource.FRAMEWORK), true); + throw createRetryableError(createTaskRunError(e, errorSource), true); } } else { return { attributes: executorParams.taskParams, references: executorParams.references ?? [] }; From 962f73130b96df919473de79b6a9b0067652e607 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 28 Oct 2024 15:44:29 +0100 Subject: [PATCH 05/10] [EDR Workflows] Fix Cypress tests failing on Alerts step (#197384) --- .../all/alerts_automated_action_results.cy.ts | 3 +- .../cypress/e2e/all/alerts_cases.cy.ts | 6 +- .../cypress/e2e/all/alerts_linked_apps.cy.ts | 4 +- .../e2e/all/alerts_multiple_agents.cy.ts | 3 +- .../cypress/e2e/all/ecs_mappings.cy.ts | 3 +- .../osquery/cypress/tasks/api_fixtures.ts | 2 +- .../osquery/cypress/tasks/live_query.ts | 3 +- .../automated_response_actions.cy.ts | 91 +++++++++---------- .../management/cypress/tasks/api_fixtures.ts | 2 +- .../public/management/cypress/tsconfig.json | 1 + 10 files changed, 53 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts index 1bc058b188fcc..4c7c9663b2d40 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts @@ -11,8 +11,7 @@ import { checkActionItemsInResults, loadRuleAlerts } from '../../tasks/live_quer const UUID_REGEX = '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}'; -// FLAKY: https://github.com/elastic/kibana/issues/169727 -describe.skip('Alert Flyout Automated Action Results', () => { +describe('Alert Flyout Automated Action Results', () => { let ruleId: string; before(() => { diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts index 2dbd905b4df93..1817a81e46821 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts @@ -49,8 +49,7 @@ describe.skip('Alert Event Details - Cases', { tags: ['@ess', '@serverless'] }, cleanupRule(ruleId); }); - // FLAKY: https://github.com/elastic/kibana/issues/197151 - describe.skip('Case creation', () => { + describe('Case creation', () => { let caseId: string; before(() => { @@ -86,8 +85,7 @@ describe.skip('Alert Event Details - Cases', { tags: ['@ess', '@serverless'] }, }); }); - // FLAKY: https://github.com/elastic/kibana/issues/176783 - describe.skip('Case', () => { + describe('Case', () => { let caseId: string; beforeEach(() => { diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts index f7585d32a2bba..2b04a99bd4f9c 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts @@ -18,9 +18,7 @@ import { import { closeModalIfVisible, closeToastIfVisible } from '../../tasks/integrations'; import { RESULTS_TABLE, RESULTS_TABLE_BUTTON } from '../../screens/live_query'; -// Failing: See https://github.com/elastic/kibana/issues/181889 -// Failing: See https://github.com/elastic/kibana/issues/181889 -describe.skip( +describe( 'Alert Event Details', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'], diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_multiple_agents.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_multiple_agents.cy.ts index 4f6d30dd71431..95f0d947b8e84 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_multiple_agents.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_multiple_agents.cy.ts @@ -15,8 +15,7 @@ import { } from '../../tasks/live_query'; import { OSQUERY_FLYOUT_BODY_EDITOR } from '../../screens/live_query'; -// FLAKY: https://github.com/elastic/kibana/issues/170157 -describe.skip( +describe( 'Alert Event Details - dynamic params', { tags: ['@ess', '@serverless'], diff --git a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts index 89881c47083fd..5330b7869e6f4 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts @@ -18,8 +18,7 @@ import { typeInOsqueryFieldInput, } from '../../tasks/live_query'; -// Failing: See https://github.com/elastic/kibana/issues/192128 -describe.skip('EcsMapping', { tags: ['@ess', '@serverless'] }, () => { +describe('EcsMapping', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { initializeDataViews(); }); diff --git a/x-pack/plugins/osquery/cypress/tasks/api_fixtures.ts b/x-pack/plugins/osquery/cypress/tasks/api_fixtures.ts index 2b0db52f45699..4aa2879883b38 100644 --- a/x-pack/plugins/osquery/cypress/tasks/api_fixtures.ts +++ b/x-pack/plugins/osquery/cypress/tasks/api_fixtures.ts @@ -231,7 +231,7 @@ export const loadRule = (includeResponseActions = false) => { tags: [], license: '', interval: '1m', - from: 'now-120s', + from: 'now-360s', to: 'now', meta: { from: '1m', kibana_siem_app_url: 'http://localhost:5620/app/security' }, actions: [], diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index c8ef188010130..910427272c5ff 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -58,7 +58,7 @@ export const verifyQueryTimeout = (timeout: string) => { // sometimes the results get stuck in the tests, this is a workaround export const checkResults = () => { - cy.getBySel('osqueryResultsTable').then(($table) => { + cy.getBySel('osqueryResultsTable', { timeout: 120000 }).then(($table) => { if ($table.find('div .euiDataGridRow').length > 0) { cy.getBySel('dataGridRowCell', { timeout: 120000 }).should('have.lengthOf.above', 0); } else { @@ -158,6 +158,7 @@ export const checkActionItemsInResults = ({ cases: boolean; timeline: boolean; }) => { + checkResults(); cy.contains('View in Discover').should(discover ? 'exist' : 'not.exist'); cy.contains('View in Lens').should(lens ? 'exist' : 'not.exist'); cy.contains('Add to Case').should(cases ? 'exist' : 'not.exist'); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts index 887fa47d03918..86e07e65e83ae 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { waitForAlertsToPopulate } from '@kbn/test-suites-xpack/security_solution_cypress/cypress/tasks/create_new_rule'; +import { login } from '../../tasks/login'; +import { waitForEndpointListPageToBeLoaded } from '../../tasks/response_console'; import type { PolicyData } from '../../../../../common/endpoint/types'; -import { APP_ENDPOINTS_PATH } from '../../../../../common/constants'; import { closeAllToasts } from '../../tasks/toasts'; import { toggleRuleOffAndOn, visitRuleAlerts } from '../../tasks/isolate'; import { cleanupRule, loadRule } from '../../tasks/api_fixtures'; -import { login } from '../../tasks/login'; -import { loadPage } from '../../tasks/common'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet'; import { changeAlertsFilter } from '../../tasks/alerts'; @@ -38,21 +38,33 @@ describe( let indexedPolicy: IndexedFleetEndpointPolicyResponse; let policy: PolicyData; let createdHost: CreateAndEnrollEndpointHostResponse; + let ruleId: string; + let ruleName: string; + beforeEach(() => { + login(); + }); before(() => { - getEndpointIntegrationVersion().then((version) => - createAgentPolicyTask(version, 'automated_response_actions').then((data) => { - indexedPolicy = data; - policy = indexedPolicy.integrationPolicies[0]; - - return enableAllPolicyProtections(policy.id).then(() => { - // Create and enroll a new Endpoint host - return createEndpointHost(policy.policy_ids[0]).then((host) => { - createdHost = host as CreateAndEnrollEndpointHostResponse; + getEndpointIntegrationVersion() + .then((version) => + createAgentPolicyTask(version, 'automated_response_actions').then((data) => { + indexedPolicy = data; + policy = indexedPolicy.integrationPolicies[0]; + + return enableAllPolicyProtections(policy.id).then(() => { + // Create and enroll a new Endpoint host + return createEndpointHost(policy.policy_ids[0]).then((host) => { + createdHost = host as CreateAndEnrollEndpointHostResponse; + }); }); + }) + ) + .then(() => { + loadRule().then((data) => { + ruleId = data.id; + ruleName = data.name; }); - }) - ); + }); }); after(() => { @@ -67,48 +79,29 @@ describe( if (createdHost) { deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] }); } - }); - beforeEach(() => { - login(); + if (ruleId) { + cleanupRule(ruleId); + } }); - // FLAKY: https://github.com/elastic/kibana/issues/169828 - describe.skip('From alerts', () => { - let ruleId: string; - let ruleName: string; - - before(() => { - loadRule().then((data) => { - ruleId = data.id; - ruleName = data.name; - }); - }); - - after(() => { - if (ruleId) { - cleanupRule(ruleId); - } - }); - - it('should have generated endpoint and rule', () => { - loadPage(APP_ENDPOINTS_PATH); - cy.contains(createdHost.hostname).should('exist'); + it('should have been called against a created host', () => { + waitForEndpointListPageToBeLoaded(createdHost.hostname); + toggleRuleOffAndOn(ruleName); - toggleRuleOffAndOn(ruleName); + visitRuleAlerts(ruleName); + closeAllToasts(); - visitRuleAlerts(ruleName); - closeAllToasts(); + changeAlertsFilter(`process.name: "agentbeat" and agent.id: "${createdHost.agentId}"`); + waitForAlertsToPopulate(); - changeAlertsFilter(`process.name: "agentbeat" and agent.id: "${createdHost.agentId}"`); - cy.getByTestSubj('expand-event').first().click(); - cy.getByTestSubj('securitySolutionFlyoutNavigationExpandDetailButton').click(); - cy.getByTestSubj('securitySolutionFlyoutResponseTab').click(); + cy.getByTestSubj('expand-event').first().click(); + cy.getByTestSubj('securitySolutionFlyoutNavigationExpandDetailButton').click(); + cy.getByTestSubj('securitySolutionFlyoutResponseTab').click(); - cy.contains(/isolate is pending|isolate completed successfully/g); - cy.contains(/kill-process is pending|kill-process completed successfully/g); - cy.contains('The action was called with a non-existing event field name: entity_id'); - }); + cy.contains(/isolate is pending|isolate completed successfully/g); + cy.contains(/kill-process is pending|kill-process completed successfully/g); + cy.contains('The action was called with a non-existing event field name: entity_id'); }); } ); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts index 1bd0e5652b442..e6e44e7e5517a 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts @@ -55,7 +55,7 @@ export const loadRule = (body = {}, includeResponseActions = true) => tags: [], license: '', interval: '1m', - from: 'now-120s', + from: 'now-360s', to: 'now', meta: { from: '1m', kibana_siem_app_url: 'http://localhost:5620/app/security' }, actions: [], diff --git a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json index 5d124d1035259..c983368164906 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json @@ -34,5 +34,6 @@ "@kbn/security-solution-serverless", "@kbn/dev-utils", "@kbn/spaces-plugin", + "@kbn/test-suites-xpack/security_solution_cypress/cypress", ] } From ffc88107f0f136481662f1fe3d1a19e23319eecd Mon Sep 17 00:00:00 2001 From: Jared Burgett <147995946+jaredburgettelastic@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:59:01 -0500 Subject: [PATCH 06/10] Delete data when clearing security entity store (#197938) ## Summary Fixed a bug where the "Clear all entities" button in the security entity store didn't delete data due to a missing query parameter. --- .../public/entity_analytics/api/entity_store.ts | 3 ++- .../components/entity_store/hooks/use_entity_store.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts index 34789402c89a5..54f5415d24a35 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts @@ -43,9 +43,10 @@ export const useEntityStoreRoutes = () => { }); }; - const deleteEntityEngine = async (entityType: EntityType) => { + const deleteEntityEngine = async (entityType: EntityType, deleteData: boolean) => { return http.fetch(`/api/entity_store/engines/${entityType}`, { method: 'DELETE', + query: { data: deleteData }, version: API_VERSIONS.public.v1, }); }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts index 29e9e6c5098c4..0ac684555fd0d 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts @@ -110,7 +110,7 @@ export const useDeleteEntityEngineMutation = (options?: UseMutationOptions<{}>) const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery(); const { deleteEntityEngine } = useEntityStoreRoutes(); return useMutation( - () => Promise.all([deleteEntityEngine('user'), deleteEntityEngine('host')]), + () => Promise.all([deleteEntityEngine('user', true), deleteEntityEngine('host', true)]), { ...options, mutationKey: DELETE_ENTITY_ENGINE_STATUS_KEY, From 7b211bdb55a5c4ea6a41c2eb0bbbef682803e1be Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Mon, 28 Oct 2024 16:00:12 +0100 Subject: [PATCH 07/10] FTR SAML Auth - Adjust stateful internal request header (#197994) ## Summary This PR adds the `x-elastic-internal-origin` header to the stateful internal request headers used by FTR. This fixes an issue that we're seeing when running deployment agnostic tests against ESS on 9.0.0-SNAPSHOT. --- .../services/saml_auth/default_request_headers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/default_request_headers.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/default_request_headers.ts index e81f3383c9e93..b07ac58c56854 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/default_request_headers.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/default_request_headers.ts @@ -14,6 +14,7 @@ export const COMMON_REQUEST_HEADERS = { // possible change in 9.0 to match serverless const STATEFUL_INTERNAL_REQUEST_HEADERS = { ...COMMON_REQUEST_HEADERS, + 'x-elastic-internal-origin': 'kibana', }; const SERVERLESS_INTERNAL_REQUEST_HEADERS = { From 7ae6f481b6657fe47bb4e6332745b35cf207e7a6 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 28 Oct 2024 15:01:35 +0000 Subject: [PATCH 08/10] [SKIP ON MKI] reporting datastream (#197958) ## Summary See details: https://github.com/elastic/kibana/issues/197955 --- .../api_integration/test_suites/common/reporting/datastream.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/test_serverless/api_integration/test_suites/common/reporting/datastream.ts b/x-pack/test_serverless/api_integration/test_suites/common/reporting/datastream.ts index 671b42f5a02a3..f2b78426ec644 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/reporting/datastream.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/reporting/datastream.ts @@ -33,6 +33,9 @@ export default function ({ getService }: FtrProviderContext) { }; describe('Data Stream', function () { + // see details: https://github.com/elastic/kibana/issues/197955 + this.tags(['failsOnMKI']); + const generatedReports = new Set(); before(async () => { roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); From 6b91b154874801e9599f46f7932d051571e1ad81 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 28 Oct 2024 15:02:00 +0000 Subject: [PATCH 09/10] [SKIP ON MKI] discover reporting (#197959) # Summary See details: https://github.com/elastic/kibana/issues/197957 --- .../test_suites/common/discover/x_pack/reporting.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts index 76c95ebbd890e..c944865d06327 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts @@ -58,7 +58,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return res; }; - describe('Discover CSV Export', () => { + describe('Discover CSV Export', function () { + // see details: https://github.com/elastic/kibana/issues/197957 + this.tags(['failsOnMKI']); describe('Check Available', () => { before(async () => { await PageObjects.svlCommonPage.loginAsAdmin(); From 0220874130fd9bb900d2d262d6e415f5a9e1bd7c Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 28 Oct 2024 17:07:49 +0200 Subject: [PATCH 10/10] fix: [Stateful:Connectors:New connector page]Configuration form missing instructions and field names from announcement (#197963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: #197586 ## Description Forms, requiring user input, should have clear instructions on how to fill them. Specific fields can have their own help (guidance) text on how to fill them with examples. All fields which are present in the form can be programmatically determined, especially for the users using assistive technology to understand what fields are present, what input is expected. ## What was changed: 1. `aria-label` values ​​are explicitly set for `ConnectorConfigurationField` child components. I suspect that due to the dynamic nature of this component, the standard mechanism does not work properly. ## Screen: image --- .../configuration/connector_configuration_field.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx index a43163caa6091..08b91ffc1842c 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx @@ -52,7 +52,7 @@ export const ConfigInputField: React.FC = ({ isLoading, validateAndSetConfigValue, }) => { - const { isValid, required, placeholder, value } = configEntry; + const { isValid, required, placeholder, value, label } = configEntry; const [innerValue, setInnerValue] = useState(value); return ( = ({ validateAndSetConfigValue(event.target.value); }} placeholder={placeholder} + aria-label={label} /> ); }; @@ -74,7 +75,7 @@ export const ConfigInputTextArea: React.FC = ({ configEntry, validateAndSetConfigValue, }) => { - const { isValid, required, placeholder, value } = configEntry; + const { isValid, required, placeholder, value, label } = configEntry; const [innerValue, setInnerValue] = useState(value); return ( = ({ validateAndSetConfigValue(event.target.value); }} placeholder={placeholder} + aria-label={label} /> ); }; @@ -129,7 +131,7 @@ export const ConfigInputPassword: React.FC = ({ configEntry, validateAndSetConfigValue, }) => { - const { required, value } = configEntry; + const { required, value, label } = configEntry; const [innerValue, setInnerValue] = useState(value); return ( = ({ setInnerValue(event.target.value); validateAndSetConfigValue(event.target.value); }} + aria-label={label} /> ); }; @@ -170,6 +173,7 @@ export const ConnectorConfigurationField: React.FC { validateAndSetConfigValue(event.target.value); }} + aria-label={label} /> ) : ( { validateAndSetConfigValue(id); }} + aria-label={label} /> ); @@ -227,6 +232,7 @@ export const ConnectorConfigurationField: React.FC { validateAndSetConfigValue(event.target.checked); }} + aria-label={label} /> {!hasPlatinumLicense && (