diff --git a/x-pack/plugins/osquery/common/constants.ts b/x-pack/plugins/osquery/common/constants.ts index be8c3fc4b0cadd..1c0498dc4217a5 100644 --- a/x-pack/plugins/osquery/common/constants.ts +++ b/x-pack/plugins/osquery/common/constants.ts @@ -9,7 +9,12 @@ export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; export const DEFAULT_DARK_MODE = 'theme:darkMode'; export const OSQUERY_INTEGRATION_NAME = 'osquery_manager'; export const BASE_PATH = '/app/osquery'; -export const ACTIONS_INDEX = `.logs-${OSQUERY_INTEGRATION_NAME}.actions`; + +export const OSQUERY_LOGS_BASE = `.logs-${OSQUERY_INTEGRATION_NAME}`; +export const ACTIONS_INDEX = `${OSQUERY_LOGS_BASE}.actions`; +export const RESULTS_INDEX = `${OSQUERY_LOGS_BASE}.results`; +export const OSQUERY_ACTIONS_INDEX = `${ACTIONS_INDEX}-*`; + export const ACTION_RESPONSES_INDEX = `.logs-${OSQUERY_INTEGRATION_NAME}.action.responses`; export const DEFAULT_PLATFORM = 'linux,windows,darwin'; diff --git a/x-pack/plugins/osquery/common/types/osquery_action.ts b/x-pack/plugins/osquery/common/types/osquery_action.ts new file mode 100644 index 00000000000000..62680947f27157 --- /dev/null +++ b/x-pack/plugins/osquery/common/types/osquery_action.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +export interface LogsOsqueryAction { + '@timestamp': string; + action_id: string; + alert_ids: string[]; + expiration: string; + input_type: 'osquery'; + queries: Array<{ + action_id: string; + id: string; + query: string; + agents: string[]; + ecs_mapping?: unknown; + version?: string; + platform?: string; + saved_query_id?: string; + expiration?: string; + }>; + type: 'INPUT_ACTION'; +} diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index fa36d5adabfb6c..303b6aab4f7f44 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -17,6 +17,7 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; import { useAllLiveQueries } from './actions/use_all_live_queries'; import { getLazyOsqueryResponseActionTypeForm } from './shared_components/lazy_osquery_action_params_form'; import { useFetchStatus } from './fleet_integration/use_fetch_status'; +import { getLazyOsqueryResult } from './shared_components/lazy_osquery_result'; import { getLazyOsqueryResults } from './shared_components/lazy_osquery_results'; import type { OsqueryPluginSetup, @@ -122,6 +123,12 @@ export class OsqueryPlugin implements Plugin ( + {PERMISSION_DENIED}} + titleSize="xs" + body={ + osquery, + }} + /> + } + /> +); + +export const EmptyPrompt = React.memo(EmptyPromptComponent); diff --git a/x-pack/plugins/osquery/public/shared_components/attachments/lazy_external_reference_content.tsx b/x-pack/plugins/osquery/public/shared_components/attachments/lazy_external_reference_content.tsx index b7ae3b5ac8a607..3bd2d4ef5d94ec 100644 --- a/x-pack/plugins/osquery/public/shared_components/attachments/lazy_external_reference_content.tsx +++ b/x-pack/plugins/osquery/public/shared_components/attachments/lazy_external_reference_content.tsx @@ -6,13 +6,10 @@ */ import React, { lazy, Suspense } from 'react'; -import { EuiCode, EuiEmptyPrompt } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { OsqueryIcon } from '../../components/osquery_icon'; import { useKibana } from '../../common/lib/kibana'; import type { ServicesWrapperProps } from '../services_wrapper'; import ServicesWrapper from '../services_wrapper'; -import { PERMISSION_DENIED } from '../osquery_action/translations'; +import { EmptyPrompt } from '../../routes/components/empty_prompt'; export interface IExternalReferenceMetaDataProps { externalReferenceMetadata: { @@ -35,24 +32,7 @@ export const getLazyExternalContent = } = useKibana(); if (!osquery.read) { - return ( - } - title={

{PERMISSION_DENIED}

} - titleSize="xs" - body={ - osquery, - }} - /> - } - /> - ); + return ; } return ( diff --git a/x-pack/plugins/osquery/public/shared_components/index.tsx b/x-pack/plugins/osquery/public/shared_components/index.tsx index 2398ddf9538d14..92de73af1ed889 100644 --- a/x-pack/plugins/osquery/public/shared_components/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/index.tsx @@ -5,6 +5,7 @@ * 2.0. */ +export { getLazyOsqueryResult } from './lazy_osquery_result'; export { getLazyOsqueryResults } from './lazy_osquery_results'; export { getLazyOsqueryAction } from './lazy_osquery_action'; export { getLazyLiveQueryField } from './lazy_live_query_field'; diff --git a/x-pack/plugins/osquery/public/shared_components/lazy_osquery_result.tsx b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_result.tsx new file mode 100644 index 00000000000000..205b05c4fc9253 --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_result.tsx @@ -0,0 +1,26 @@ +/* + * 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, { lazy, Suspense } from 'react'; +import type { OsqueryActionResultProps } from './osquery_results/types'; +import type { StartServices } from '../types'; + +interface BigServices extends StartServices { + kibanaVersion: string; + storage: unknown; +} + +const OsqueryResult = lazy(() => import('./osquery_results/osquery_result_wrapper')); + +export const getLazyOsqueryResult = + // eslint-disable-next-line react/display-name + (services: BigServices) => (props: OsqueryActionResultProps) => + ( + + + + ); diff --git a/x-pack/plugins/osquery/public/shared_components/lazy_osquery_results.tsx b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_results.tsx index 22b6af098a468c..e488284a2096b3 100644 --- a/x-pack/plugins/osquery/public/shared_components/lazy_osquery_results.tsx +++ b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_results.tsx @@ -6,15 +6,15 @@ */ import React, { lazy, Suspense } from 'react'; -import type { OsqueryActionResultsProps } from './osquery_results/types'; import type { StartServices } from '../types'; +import type { OsqueryActionResultsProps } from './osquery_results/types'; interface BigServices extends StartServices { kibanaVersion: string; storage: unknown; } -const OsqueryResults = lazy(() => import('./osquery_results')); +const OsqueryResults = lazy(() => import('./osquery_results/osquery_results')); export const getLazyOsqueryResults = // eslint-disable-next-line react/display-name diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.test.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result_wrapper.test.tsx similarity index 94% rename from x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.test.tsx rename to x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result_wrapper.test.tsx index d4573eb291d89c..7b30df09da79ba 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.test.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result_wrapper.test.tsx @@ -14,7 +14,7 @@ import { queryClient } from '../../query_client'; import { useKibana } from '../../common/lib/kibana'; import * as useLiveQueryDetails from '../../actions/use_live_query_details'; import { PERMISSION_DENIED } from '../osquery_action/translations'; -import { OsqueryResult } from './osquery_result'; +import { OsqueryActionResult } from './osquery_result_wrapper'; import { defaultLiveQueryDetails, DETAILS_ID, @@ -29,6 +29,7 @@ const useKibanaMock = useKibana as jest.MockedFunction; const defaultPermissions = { osquery: { + read: true, runSavedQueries: true, readSavedQueries: true, }, @@ -67,7 +68,7 @@ describe('Osquery Results', () => { it('return results table', async () => { const { getByText, queryByText, getByTestId } = renderWithContext( - + ); expect(queryByText(PERMISSION_DENIED)).not.toBeInTheDocument(); expect(getByTestId('osquery-results-comment')); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result_wrapper.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result_wrapper.tsx new file mode 100644 index 00000000000000..a5c7f7daedca2b --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result_wrapper.tsx @@ -0,0 +1,90 @@ +/* + * 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 { EuiComment, EuiErrorBoundary, EuiSpacer } from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; +import { FormattedRelative } from '@kbn/i18n-react'; + +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { EmptyPrompt } from '../../routes/components/empty_prompt'; +import { useKibana } from '../../common/lib/kibana'; +import type { StartPlugins } from '../../types'; +import { queryClient } from '../../query_client'; +import { AlertAttachmentContext } from '../../common/contexts'; +import { PackQueriesStatusTable } from '../../live_queries/form/pack_queries_status_table'; +import { ATTACHED_QUERY } from '../../agents/translations'; +import { useLiveQueryDetails } from '../../actions/use_live_query_details'; +import type { OsqueryActionResultProps } from './types'; + +const OsqueryResultComponent = React.memo( + ({ actionId, ruleName, startDate, ecsData }) => { + const { read } = useKibana().services.application.capabilities.osquery; + + const [isLive, setIsLive] = useState(false); + const { data } = useLiveQueryDetails({ + actionId, + isLive, + skip: !read, + }); + + useEffect(() => { + setIsLive(() => !(data?.status === 'completed')); + }, [data?.status]); + + return ( + + + } + event={ATTACHED_QUERY} + data-test-subj={'osquery-results-comment'} + > + {!read ? ( + + ) : ( + + )} + + + + ); + } +); + +export const OsqueryActionResult = React.memo(OsqueryResultComponent); +type OsqueryActionResultsWrapperProps = { + services: CoreStart & StartPlugins; +} & OsqueryActionResultProps; + +const OsqueryActionResultWrapperComponent: React.FC = ({ + services, + ...restProps +}) => ( + + + + + + + + + +); + +const OsqueryActionResultWrapper = React.memo(OsqueryActionResultWrapperComponent); + +// eslint-disable-next-line import/no-default-export +export { OsqueryActionResultWrapper as default }; diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.test.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.test.tsx index 3eb34cac08e8d5..f5a46ef4bb6da7 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.test.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.test.tsx @@ -10,7 +10,7 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { QueryClientProvider } from '@tanstack/react-query'; -import { OsqueryActionResults } from '.'; +import { OsqueryActionResults } from './osquery_results'; import { queryClient } from '../../query_client'; import { useKibana } from '../../common/lib/kibana'; import * as useLiveQueryDetails from '../../actions/use_live_query_details'; @@ -52,6 +52,7 @@ const defaultProps: OsqueryActionResultsProps = { const defaultPermissions = { osquery: { + read: true, runSavedQueries: false, readSavedQueries: false, }, diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.tsx similarity index 66% rename from x-pack/plugins/osquery/public/shared_components/osquery_results/index.tsx rename to x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.tsx index ec55c977a2e625..92f01cc76a076c 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.tsx @@ -10,7 +10,8 @@ import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import type { CoreStart } from '@kbn/core/public'; -import { KibanaContextProvider } from '../../common/lib/kibana'; +import { EmptyPrompt } from '../../routes/components/empty_prompt'; +import { KibanaContextProvider, useKibana } from '../../common/lib/kibana'; import { queryClient } from '../../query_client'; import { KibanaThemeProvider } from '../../shared_imports'; @@ -22,27 +23,33 @@ const OsqueryActionResultsComponent: React.FC = ({ ruleName, actionItems, ecsData, -}) => ( -
- {actionItems?.map((item) => { - const actionId = item.fields?.action_id?.[0]; - const queryId = item.fields?.['queries.action_id']?.[0]; - const startDate = item.fields?.['@timestamp'][0]; +}) => { + const { read } = useKibana().services.application.capabilities.osquery; - return ( - - ); - })} - -
-); + return !read ? ( + + ) : ( +
+ {actionItems?.map((item) => { + const actionId = item.fields?.action_id?.[0]; + const queryId = item.fields?.['queries.action_id']?.[0]; + const startDate = item.fields?.['@timestamp'][0]; + + return ( + + ); + })} + +
+ ); +}; export const OsqueryActionResults = React.memo(OsqueryActionResultsComponent); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/types.ts b/x-pack/plugins/osquery/public/shared_components/osquery_results/types.ts index b1c613fe032f90..9501fbac3a2156 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/types.ts +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/types.ts @@ -13,3 +13,17 @@ export interface OsqueryActionResultsProps { ecsData: EcsSecurityExtension; actionItems?: ActionEdges; } +/* + * 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. + */ + +export interface OsqueryActionResultProps { + ruleName?: string[]; + ecsData: EcsSecurityExtension; + actionId: string; + queryId: string; + startDate: string; +} diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts index 6f26c56fcf91be..8e6ea4354d2838 100644 --- a/x-pack/plugins/osquery/public/types.ts +++ b/x-pack/plugins/osquery/public/types.ts @@ -19,18 +19,20 @@ import type { import type { CasesUiStart, CasesUiSetup } from '@kbn/cases-plugin/public'; import type { TimelinesUIStart } from '@kbn/timelines-plugin/public'; import type { - getLazyOsqueryResults, getLazyLiveQueryField, getLazyOsqueryAction, getLazyOsqueryResponseActionTypeForm, + getLazyOsqueryResult, } from './shared_components'; import type { useAllLiveQueries, UseAllLiveQueriesConfig } from './actions/use_all_live_queries'; +import type { getLazyOsqueryResults } from './shared_components/lazy_osquery_results'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface OsqueryPluginSetup {} export interface OsqueryPluginStart { OsqueryAction?: ReturnType; + OsqueryResult: ReturnType; OsqueryResults: ReturnType; LiveQueryField?: ReturnType; isOsqueryAvailable: (props: { agentId: string }) => boolean; diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 534a424a02d068..d595a23a7b21c8 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -71,6 +71,7 @@ "@kbn/shared-ux-router", "@kbn/securitysolution-ecs", "@kbn/licensing-plugin", + "@kbn/core-lifecycle-browser", "@kbn/core-saved-objects-server" ] } diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 542c4fa2487906..32e04329830930 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -73,7 +73,6 @@ export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`; /** Base Actions route. Used to get a list of all actions and is root to other action related routes */ export const BASE_ENDPOINT_ACTION_ROUTE = `${BASE_ENDPOINT_ROUTE}/action`; -export const BASE_ENDPOINT_ACTION_ALERTS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/alerts`; export const ISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/isolate`; export const UNISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/unisolate`; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/automated_actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/automated_actions.ts new file mode 100644 index 00000000000000..6a466be9ae549d --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/automated_actions.ts @@ -0,0 +1,40 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; + +const AutomatedActionListRequestSchema = { + query: schema.object({ + alertIds: schema.arrayOf(schema.string({ minLength: 1 }), { + minSize: 1, + validate: (alertIds) => { + if (alertIds.map((v) => v.trim()).some((v) => !v.length)) { + return 'alertIds cannot contain empty strings'; + } + }, + }), + }), +}; + +export type EndpointAutomatedActionListRequestQuery = TypeOf< + typeof AutomatedActionListRequestSchema.query +>; + +const AutomatedActionResponseRequestSchema = { + query: schema.object({ + expiration: schema.string(), + actionId: schema.string(), + agent: schema.object({ + id: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + }), + }), +}; + +export type EndpointAutomatedActionResponseRequestQuery = TypeOf< + typeof AutomatedActionResponseRequestSchema.query +>; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index c06e8903057939..fe1b22644d8e11 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -133,6 +133,15 @@ export interface LogsEndpointAction { }; } +export interface LogsEndpointActionWithHosts extends LogsEndpointAction { + EndpointActions: EndpointActionFields & + ActionRequestFields & { + data: EndpointActionData & { + hosts: Record; + }; + }; +} + /** * An Action response written by the endpoint to the Endpoint `.logs-endpoint.action.responses` datastream * @since v7.16 @@ -189,6 +198,7 @@ export interface EndpointActionData< parameters?: TParameters; output?: ActionResponseOutput; alert_id?: string[]; + hosts?: Record; } export interface FleetActionResponseData { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 399f0a6db10076..a283ea4cbade9d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { IEsSearchRequest } from '@kbn/data-plugin/common'; +import type { IEsSearchRequest, IEsSearchResponse } from '@kbn/data-plugin/common'; +import type { ActionResponsesRequestStrategyParseResponse } from './response_actions/response'; import type { ESQuery } from '../../typed_json'; import type { HostDetailsStrategyResponse, @@ -99,6 +100,13 @@ import type { FirstLastSeenRequestOptions, FirstLastSeenStrategyResponse, } from './first_last_seen'; +import type { + ActionRequestOptions, + ActionRequestStrategyResponse, + ActionResponsesRequestOptions, + ActionResponsesRequestStrategyResponse, + ResponseActionsQueries, +} from './response_actions'; import type { ManagedUserDetailsRequestOptions, ManagedUserDetailsStrategyResponse, @@ -121,7 +129,8 @@ export type FactoryQueryTypes = | RiskQueries | CtiQueries | typeof MatrixHistogramQuery - | typeof FirstLastSeenQuery; + | typeof FirstLastSeenQuery + | ResponseActionsQueries; export interface RequestBasicOptions extends IEsSearchRequest { timerange: TimerangeInput; @@ -137,6 +146,11 @@ export interface RequestOptionsPaginated extends RequestBasicOpt sort: SortField; } +export type StrategyParseResponseType = + T extends ResponseActionsQueries.results + ? ActionResponsesRequestStrategyParseResponse + : IEsSearchResponse; + export type StrategyResponseType = T extends HostsQueries.hosts ? HostsStrategyResponse : T extends HostsQueries.details @@ -201,6 +215,10 @@ export type StrategyResponseType = T extends HostsQ ? UsersRiskScoreStrategyResponse : T extends RiskQueries.kpiRiskScore ? KpiRiskScoreStrategyResponse + : T extends ResponseActionsQueries.actions + ? ActionRequestStrategyResponse + : T extends ResponseActionsQueries.results + ? ActionResponsesRequestStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -267,6 +285,10 @@ export type StrategyRequestType = T extends HostsQu ? RiskScoreRequestOptions : T extends RiskQueries.kpiRiskScore ? KpiRiskScoreRequestOptions + : T extends ResponseActionsQueries.actions + ? ActionRequestOptions + : T extends ResponseActionsQueries.results + ? ActionResponsesRequestOptions : never; export interface CommonFields { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/action.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/action.ts new file mode 100644 index 00000000000000..79a7cfb9a57c2c --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/action.ts @@ -0,0 +1,29 @@ +/* + * 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 { IEsSearchResponse } from '@kbn/data-plugin/common'; +import type { + SortOrder, + Inspect, + Maybe, + RequestBasicOptions, + ResponseActionsSearchHit, +} from './types'; + +export interface ActionRequestOptions extends RequestBasicOptions { + alertIds: string[]; + agentId?: string; + sort: { + order: SortOrder; + field: string; + }; +} + +export interface ActionRequestStrategyResponse extends IEsSearchResponse { + edges: ResponseActionsSearchHit[]; + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/index.ts new file mode 100644 index 00000000000000..f7170115a47e50 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export enum ResponseActionsQueries { + actions = 'responseActions', + results = 'responseActionsResults', +} + +export type { ActionRequestOptions, ActionRequestStrategyResponse } from './action'; +export type { + ActionResponsesRequestOptions, + ActionResponsesRequestStrategyResponse, +} from './response'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/response.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/response.ts new file mode 100644 index 00000000000000..e2e562e2fede90 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/response.ts @@ -0,0 +1,54 @@ +/* + * 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 { IKibanaSearchResponse } from '@kbn/data-plugin/common'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { LogsEndpointActionResponse } from '../../../endpoint/types'; +import type { SortOrder, Inspect, Maybe, RequestBasicOptions } from './types'; + +export type ResultEdges = estypes.SearchResponse['hits']['hits']; + +export interface ActionResponsesRequestOptions extends RequestBasicOptions { + expiration: string; + actionId: string; + sort: { + order: SortOrder; + field: string; + }; + agents: number; +} + +export interface ActionResponsesRequestStrategyResponse + extends ActionResponsesRequestStrategyParseResponse { + isCompleted: boolean; + wasSuccessful: boolean; + isExpired: boolean; + status: 'successful' | 'failed' | 'pending'; + edges: ResultEdges; +} + +export interface ActionResponsesRequestStrategyParseResponse + extends IKibanaSearchResponse< + estypes.SearchResponse< + LogsEndpointActionResponse, + { + aggs: { + responses_by_action_id: estypes.AggregationsSingleBucketAggregateBase & { + rows_count: estypes.AggregationsSumAggregate; + responses: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }; + }; + } + > + > { + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/types.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/types.ts new file mode 100644 index 00000000000000..b7b5ca63a0b757 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/response_actions/types.ts @@ -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 { IEsSearchRequest } from '@kbn/data-plugin/common'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { LogsOsqueryAction } from '@kbn/osquery-plugin/common/types/osquery_action'; +import type { LogsEndpointActionWithHosts } from '../../../endpoint/types'; +import type { ResponseActionsQueries } from '.'; + +export enum SortOrder { + asc = 'asc', + desc = 'desc', +} + +export interface RequestBasicOptions extends IEsSearchRequest { + factoryQueryType?: ResponseActionsQueries; + aggregations?: Record; +} + +export type ResponseActionsSearchHit = estypes.SearchHit< + LogsOsqueryAction | LogsEndpointActionWithHosts +>; + +export interface Inspect { + dsl: string[]; +} + +export type Maybe = T | null; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/endpoint_response_actions_tab.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/endpoint_response_actions_tab.tsx deleted file mode 100644 index cec9955aa94da1..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/endpoint_response_actions_tab.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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, { useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { EuiNotificationBadge, EuiSpacer } from '@elastic/eui'; -import { some } from 'lodash'; -import { ActionsLogTable } from '../../../management/components/endpoint_response_actions_list/components/actions_log_table'; -import { useGetEndpointActionList } from '../../../management/hooks'; -import type { ExpandedEventFieldsObject, RawEventData } from './types'; -import { EventsViewType } from './event_details'; -import * as i18n from './translations'; - -import { expandDottedObject } from '../../../../common/utils/expand_dotted'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; -import { RESPONSE_ACTION_TYPES } from '../../../../common/detection_engine/rule_response_actions/schemas/response_actions'; - -const TabContentWrapper = styled.div` - height: 100%; - position: relative; -`; - -export const useEndpointResponseActionsTab = ({ - rawEventData, -}: { - rawEventData?: RawEventData; -}) => { - const responseActionsEnabled = useIsExperimentalFeatureEnabled('endpointResponseActionsEnabled'); - const [isLive, setIsLive] = useState(false); - - const { - data: actionList, - isFetching, - isFetched, - } = useGetEndpointActionList( - { - alertId: [rawEventData?._id ?? ''], - withAutomatedActions: true, - }, - { - retry: false, - enabled: rawEventData && responseActionsEnabled, - refetchInterval: isLive ? 5000 : false, - } - ); - - useEffect( - () => - setIsLive(() => - some(actionList?.data, (action) => !action.errors?.length && action.status === 'pending') - ), - [actionList?.data] - ); - - const totalItemCount = useMemo(() => actionList?.total ?? 0, [actionList]); - - const expandedEventFieldsObject = rawEventData - ? (expandDottedObject(rawEventData.fields) as ExpandedEventFieldsObject) - : undefined; - - const responseActions = useMemo( - () => expandedEventFieldsObject?.kibana?.alert?.rule?.parameters?.[0].response_actions, - [expandedEventFieldsObject] - ); - - const endpointResponseActions = useMemo( - () => - responseActions?.filter( - (responseAction) => responseAction.action_type_id === RESPONSE_ACTION_TYPES.ENDPOINT - ), - [responseActions] - ); - - if (!endpointResponseActions?.length || !rawEventData || !responseActionsEnabled) { - return; - } - - return { - id: EventsViewType.endpointView, - 'data-test-subj': 'endpointViewTab', - name: i18n.ENDPOINT_VIEW, - append: ( - - {totalItemCount} - - ), - content: ( - <> - - - {isFetched && totalItemCount ? ( - null} - items={actionList?.data || []} - loading={isFetching} - onChange={() => null} - totalItemCount={totalItemCount} - queryParams={{ withAutomatedActions: true }} - showHostNames - /> - ) : null} - - - ), - }; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index c1f9b522aad545..b51c5e0e3699e7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -21,8 +21,9 @@ import styled from 'styled-components'; import { isEmpty } from 'lodash'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { useResponseActionsView } from './response_actions_view'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import type { RawEventData } from './types'; -import { useEndpointResponseActionsTab } from './endpoint_response_actions_tab'; import type { SearchHit } from '../../../../common/search_strategy'; import { getMitreComponentParts } from '../../../detections/mitre/get_mitre_threat_component'; import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step'; @@ -32,7 +33,6 @@ import { getTourAnchor, SecurityStepId, } from '../guided_onboarding_tour/tour_config'; -import { useOsqueryTab } from './osquery_tab'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import { ThreatSummaryView } from './cti_details/threat_summary_view'; @@ -57,6 +57,7 @@ import { useRiskScoreData } from './use_risk_score_data'; import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer'; import { DETAILS_CLASS_NAME } from '../../../timelines/components/timeline/body/renderers/helpers'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { useOsqueryTab } from './osquery_tab'; export const EVENT_DETAILS_CONTEXT_ID = 'event-details'; @@ -67,7 +68,9 @@ export type EventViewId = | EventsViewType.jsonView | EventsViewType.summaryView | EventsViewType.threatIntelView - | EventsViewType.osqueryView; + // Depending on endpointResponseActionsEnabled flag whether to render Osquery Tab or the commonTab (osquery + endpoint results) + | EventsViewType.osqueryView + | EventsViewType.responseActionsView; export enum EventsViewType { tableView = 'table-view', @@ -75,7 +78,7 @@ export enum EventsViewType { summaryView = 'summary-view', threatIntelView = 'threat-intel-view', osqueryView = 'osquery-results-view', - endpointView = 'endpoint-results-view', + responseActionsView = 'response-actions-results-view', } interface Props { @@ -214,7 +217,9 @@ const EventDetailsComponent: React.FC = ({ const hasRiskInfoWithLicense = isLicenseValid && (hostRisk || userRisk); return hasEnrichments || hasRiskInfoWithLicense; }, [enrichmentCount, hostRisk, isLicenseValid, userRisk]); - + const endpointResponseActionsEnabled = useIsExperimentalFeatureEnabled( + 'endpointResponseActionsEnabled' + ); const summaryTab: EventViewTab | undefined = useMemo( () => isAlert @@ -427,26 +432,24 @@ const EventDetailsComponent: React.FC = ({ }), [rawEventData] ); - - const osqueryTab = useOsqueryTab({ + const responseActionsTab = useResponseActionsView({ rawEventData: rawEventData as RawEventData, ...(detailsEcsData !== null ? { ecsData: detailsEcsData } : {}), }); - - const endpointResponseActionsTab = useEndpointResponseActionsTab({ + const osqueryTab = useOsqueryTab({ rawEventData: rawEventData as RawEventData, + ...(detailsEcsData !== null ? { ecsData: detailsEcsData } : {}), }); + const responseActionsTabs = useMemo(() => { + return endpointResponseActionsEnabled ? [responseActionsTab] : [osqueryTab]; + }, [endpointResponseActionsEnabled, osqueryTab, responseActionsTab]); + const tabs = useMemo(() => { - return [ - summaryTab, - threatIntelTab, - tableTab, - jsonTab, - osqueryTab, - endpointResponseActionsTab, - ].filter((tab: EventViewTab | undefined): tab is EventViewTab => !!tab); - }, [summaryTab, threatIntelTab, tableTab, jsonTab, osqueryTab, endpointResponseActionsTab]); + return [summaryTab, threatIntelTab, tableTab, jsonTab, ...responseActionsTabs].filter( + (tab: EventViewTab | undefined): tab is EventViewTab => !!tab + ); + }, [summaryTab, threatIntelTab, tableTab, jsonTab, responseActionsTabs]); const selectedTab = useMemo( () => tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0], diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx index e82e42f78e2f62..e3ef5233216e0f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx @@ -5,13 +5,11 @@ * 2.0. */ -import { EuiCode, EuiEmptyPrompt, EuiNotificationBadge, EuiSpacer } from '@elastic/eui'; +import { EuiNotificationBadge, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n-react'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import type { RawEventData } from './types'; -import { PERMISSION_DENIED } from '../../../detection_engine/rule_response_actions/osquery/translations'; import { expandDottedObject } from '../../../../common/utils/expand_dotted'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { useKibana } from '../../lib/kibana'; @@ -52,33 +50,15 @@ export const useOsqueryTab = ({ ecsData?: Ecs; }) => { const { - services: { osquery, application }, + services: { osquery }, } = useKibana(); const responseActionsEnabled = useIsExperimentalFeatureEnabled('responseActionsEnabled'); - - const emptyPrompt = useMemo( - () => ( - {PERMISSION_DENIED}} - titleSize="xs" - body={ - osquery, - }} - /> - } - /> - ), - [] + const endpointResponseActionsEnabled = useIsExperimentalFeatureEnabled( + 'endpointResponseActionsEnabled' ); - const shouldEarlyReturn = !rawEventData || !responseActionsEnabled || !ecsData; + const shouldEarlyReturn = + !rawEventData || !responseActionsEnabled || !ecsData || endpointResponseActionsEnabled; const alertId = rawEventData?._id ?? ''; const { OsqueryResults, fetchAllLiveQueries } = osquery; @@ -124,14 +104,8 @@ export const useOsqueryTab = ({ content: ( <> - {!application?.capabilities?.osquery?.read ? ( - emptyPrompt - ) : ( - <> - - - - )} + + ), diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx new file mode 100644 index 00000000000000..15867e10064c73 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx @@ -0,0 +1,84 @@ +/* + * 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, { useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiNotificationBadge, EuiSpacer } from '@elastic/eui'; +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { ResponseActionsResults } from '../response_actions/response_actions_results'; +import { expandDottedObject } from '../../../../common/utils/expand_dotted'; +import { useGetAutomatedActionList } from '../../../management/hooks/response_actions/use_get_automated_action_list'; +import { EventsViewType } from './event_details'; +import * as i18n from './translations'; + +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import type { ExpandedEventFieldsObject, RawEventData } from './types'; + +const TabContentWrapper = styled.div` + height: 100%; + position: relative; +`; + +export const useResponseActionsView = ({ + rawEventData, + ecsData, +}: { + ecsData?: Ecs; + rawEventData: RawEventData; +}) => { + const responseActionsEnabled = useIsExperimentalFeatureEnabled('endpointResponseActionsEnabled'); + + const expandedEventFieldsObject = rawEventData + ? (expandDottedObject(rawEventData.fields) as ExpandedEventFieldsObject) + : undefined; + + const responseActions = + expandedEventFieldsObject?.kibana?.alert?.rule?.parameters?.[0].response_actions; + + const shouldEarlyReturn = !ecsData || !responseActionsEnabled || !responseActions; + const alertId = rawEventData?._id ?? ''; + + const { data: automatedList, isFetched } = useGetAutomatedActionList( + { + alertIds: [alertId], + }, + { enabled: !shouldEarlyReturn } + ); + + const ruleName = expandedEventFieldsObject?.kibana?.alert?.rule?.name; + + const totalItemCount = useMemo(() => automatedList?.items?.length ?? 0, [automatedList]); + + if (shouldEarlyReturn || !responseActions?.length) { + return; + } + + return { + id: EventsViewType.responseActionsView, + 'data-test-subj': 'responseActionsViewTab', + name: i18n.RESPONSE_ACTIONS_VIEW, + append: ( + + {totalItemCount} + + ), + content: ( + <> + + + {isFetched && totalItemCount && automatedList?.items.length ? ( + + ) : null} + + + ), + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index a0fb58551deb51..7de58571da846a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -70,9 +70,12 @@ export const OSQUERY_VIEW = i18n.translate('xpack.securitySolution.eventDetails. defaultMessage: 'Osquery Results', }); -export const ENDPOINT_VIEW = i18n.translate('xpack.securitySolution.eventDetails.endpointView', { - defaultMessage: 'Endpoint Results', -}); +export const RESPONSE_ACTIONS_VIEW = i18n.translate( + 'xpack.securitySolution.eventDetails.responseActionsView', + { + defaultMessage: 'Response Results', + } +); export const FIELD = i18n.translate('xpack.securitySolution.eventDetails.field', { defaultMessage: 'Field', @@ -146,3 +149,17 @@ export const ALERT_REASON = i18n.translate('xpack.securitySolution.eventDetails. export const VIEW_ALL_FIELDS = i18n.translate('xpack.securitySolution.eventDetails.viewAllFields', { defaultMessage: 'View all fields in table', }); + +export const ENDPOINT_COMMANDS = Object.freeze({ + isolated: i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.isolate', { + defaultMessage: 'isolated the host', + }), + released: i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.release', { + defaultMessage: 'released the host', + }), + generic: (command: string) => + i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.generic', { + values: { command }, + defaultMessage: 'executed command {command}', + }), +}); diff --git a/x-pack/plugins/security_solution/public/common/components/response_actions/endpoint_action_results.tsx b/x-pack/plugins/security_solution/public/common/components/response_actions/endpoint_action_results.tsx new file mode 100644 index 00000000000000..5e76488ddd1beb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/response_actions/endpoint_action_results.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiComment, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import { FormattedRelative } from '@kbn/i18n-react'; +import React, { useEffect, useState, useMemo } from 'react'; +import type { LogsEndpointActionWithHosts } from '../../../../common/endpoint/types/actions'; +import { useUserPrivileges } from '../user_privileges'; +import { useGetAutomatedActionResponseList } from '../../../management/hooks/response_actions/use_get_automated_action_list'; +import { ActionsLogExpandedTray } from '../../../management/components/endpoint_response_actions_list/components/action_log_expanded_tray'; +import type { ResponseActionsApiCommandNames } from '../../../../common/endpoint/service/response_actions/constants'; +import { ENDPOINT_COMMANDS } from '../event_details/translations'; +import { ResponseActionsEmptyPrompt } from './response_actions_empty_prompt'; + +interface EndpointResponseActionResultsProps { + action: LogsEndpointActionWithHosts; +} + +export const EndpointResponseActionResults = ({ action }: EndpointResponseActionResultsProps) => { + const { rule, agent } = action; + const { action_id: actionId, expiration } = action.EndpointActions; + const { + endpointPrivileges: { canReadActionsLogManagement }, + } = useUserPrivileges(); + + const [isLive, setIsLive] = useState(true); + const { data: expandedAction } = useGetAutomatedActionResponseList( + { actionId, expiration, agent }, + { enabled: canReadActionsLogManagement, action, isLive } + ); + + useEffect(() => { + setIsLive(() => { + if (!expandedAction) { + return true; + } + return !expandedAction.errors?.length && expandedAction.status === 'pending'; + }); + }, [expandedAction]); + + const eventText = getCommentText(action.EndpointActions.data.command); + + const hostName = useMemo( + () => expandedAction?.hosts[expandedAction?.agents[0]].name, + [expandedAction?.agents, expandedAction?.hosts] + ); + + return ( + <> + + } + event={eventText} + data-test-subj={'endpoint-results-comment'} + > + {canReadActionsLogManagement ? ( + expandedAction ? ( + + ) : ( + + ) + ) : ( + + )} + + + + ); +}; + +const getCommentText = (command: ResponseActionsApiCommandNames): string => { + if (command === 'isolate') { + return ENDPOINT_COMMANDS.isolated; + } + if (command === 'unisolate') { + return ENDPOINT_COMMANDS.released; + } + return ENDPOINT_COMMANDS.generic(command); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/response_actions/response_actions_empty_prompt.tsx b/x-pack/plugins/security_solution/public/common/components/response_actions/response_actions_empty_prompt.tsx new file mode 100644 index 00000000000000..cb55c4c441f427 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/response_actions/response_actions_empty_prompt.tsx @@ -0,0 +1,45 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiCode, EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { PERMISSION_DENIED } from '../../../detection_engine/rule_response_actions/osquery/translations'; + +interface ResponseActionsEmptyPromptProps { + type: 'endpoint'; +} + +export const ResponseActionsEmptyPrompt = ({ type }: ResponseActionsEmptyPromptProps) => { + const integration = useMemo(() => { + switch (type) { + case 'endpoint': + return { + icon: 'logoSecurity', + name: 'Elastic Defend', + }; + } + }, [type]); + + return ( + {PERMISSION_DENIED}} + titleSize="xs" + body={ + {integration.name}, + }} + /> + } + /> + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/response_actions/response_actions_results.tsx b/x-pack/plugins/security_solution/public/common/components/response_actions/response_actions_results.tsx new file mode 100644 index 00000000000000..550e3bd40b9a83 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/response_actions/response_actions_results.tsx @@ -0,0 +1,72 @@ +/* + * 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 type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import type { LogsOsqueryAction } from '@kbn/osquery-plugin/common/types/osquery_action'; +import { EndpointResponseActionResults } from './endpoint_action_results'; +import type { + LogsEndpointAction, + LogsEndpointActionWithHosts, +} from '../../../../common/endpoint/types'; +import { useKibana } from '../../lib/kibana'; + +interface ResponseActionsResultsProps { + actions: Array; + ruleName?: string[]; + ecsData: Ecs; +} + +export const ResponseActionsResults = React.memo( + ({ actions, ruleName, ecsData }: ResponseActionsResultsProps) => { + const { + services: { osquery }, + } = useKibana(); + const { OsqueryResult } = osquery; + + return ( + <> + {actions.map((action) => { + if (isOsquery(action)) { + const actionId = action.action_id; + const queryId = action.queries[0].id; + const startDate = action['@timestamp']; + + return ( + + ); + } + if (isEndpoint(action)) { + return ( + + ); + } + return null; + })} + + ); + } +); + +ResponseActionsResults.displayName = 'ResponseActionsResults'; + +const isOsquery = (item: LogsEndpointAction | LogsOsqueryAction): item is LogsOsqueryAction => { + return item && 'input_type' in item && item?.input_type === 'osquery'; +}; +const isEndpoint = (item: LogsEndpointAction | LogsOsqueryAction): item is LogsEndpointAction => { + return item && 'EndpointActions' in item && item?.EndpointActions.input_type === 'endpoint'; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx index 3ee32999aeec63..849e9e484b0083 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx @@ -8,6 +8,7 @@ import React, { memo, useMemo } from 'react'; import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiDescriptionList } from '@elastic/eui'; import { css, euiStyled } from '@kbn/kibana-react-plugin/common'; +import { map } from 'lodash'; import { EndpointUploadActionResult } from '../../endpoint_upload_action_result'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { OUTPUT_MESSAGES } from '../translations'; @@ -98,7 +99,7 @@ const OutputContent = memo<{ action: MaybeImmutable; 'data-test-s const { command, isCompleted, isExpired, wasSuccessful, errors } = action; - if (errors) { + if (errors?.length) { return ( // TODO: temporary solution, waiting for UI <> @@ -182,7 +183,7 @@ export const ActionsLogExpandedTray = memo<{ }>(({ action, 'data-test-subj': dataTestSubj }) => { const getTestId = useTestIdGenerator(dataTestSubj); - const { startedAt, completedAt, command: _command, comment, parameters } = action; + const { hosts, startedAt, completedAt, command: _command, comment, parameters } = action; const parametersList = useMemo( () => @@ -223,13 +224,17 @@ export const ActionsLogExpandedTray = memo<{ title: OUTPUT_MESSAGES.expandSection.comment, description: comment ? comment : emptyValue, }, + { + title: OUTPUT_MESSAGES.expandSection.hostname, + description: map(hosts, (host) => host.name).join(', ') || emptyValue, + }, ].map(({ title, description }) => { return { title: {title}, description: {description}, }; }), - [command, comment, completedAt, parametersList, startedAt] + [command, comment, completedAt, hosts, parametersList, startedAt] ); const outputList = useMemo( diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_with_rule_toggle.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_automated_actions_filter.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_with_rule_toggle.tsx rename to x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_automated_actions_filter.tsx index 84384024b380b2..442ae9bb0b93f0 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_with_rule_toggle.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_automated_actions_filter.tsx @@ -6,30 +6,41 @@ */ import { EuiFilterButton } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { useActionHistoryUrlParams } from './use_action_history_url_params'; import { FILTER_NAMES } from '../translations'; interface ActionsLogWithRuleToggleProps { - isFlyout: boolean; dataTestSubj?: string; + onChangeWithAutomatedActionsFilter: () => void; + isFlyout: boolean; } - -export const ActionsLogWithRuleToggle = React.memo( - ({ isFlyout, dataTestSubj }: ActionsLogWithRuleToggleProps) => { +export const AutomatedActionsFilter = React.memo( + ({ + dataTestSubj, + onChangeWithAutomatedActionsFilter, + isFlyout, + }: ActionsLogWithRuleToggleProps) => { const { withAutomatedActions: withAutomatedActionsUrlParam, setUrlWithAutomatedActions } = useActionHistoryUrlParams(); + const [value, setValue] = useState(withAutomatedActionsUrlParam); const onClick = useCallback(() => { if (!isFlyout) { - // set and show `withAutomatedActions` URL param on history page setUrlWithAutomatedActions(!withAutomatedActionsUrlParam); } - }, [isFlyout, setUrlWithAutomatedActions, withAutomatedActionsUrlParam]); + setValue((prevState) => !prevState); + onChangeWithAutomatedActionsFilter(); + }, [ + isFlyout, + onChangeWithAutomatedActionsFilter, + setUrlWithAutomatedActions, + withAutomatedActionsUrlParam, + ]); return ( @@ -39,4 +50,4 @@ export const ActionsLogWithRuleToggle = React.memo( } ); -ActionsLogWithRuleToggle.displayName = 'ActionsLogWithRuleToggle'; +AutomatedActionsFilter.displayName = 'AutomatedActionsFilter'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx index e1f5ee2fb93839..2c47fc0d717a59 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx @@ -11,7 +11,7 @@ import type { OnRefreshChangeProps, } from '@elastic/eui/src/components/date_picker/types'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { ActionsLogWithRuleToggle } from './actions_log_with_rule_toggle'; +import { AutomatedActionsFilter } from './actions_log_automated_actions_filter'; import type { useGetEndpointActionList } from '../../../hooks'; import { type DateRangePickerValues, @@ -31,6 +31,7 @@ export const ActionsLogFilters = memo( onChangeCommandsFilter, onChangeStatusesFilter, onChangeUsersFilter, + onChangeWithAutomatedActionsFilter, onRefresh, onRefreshChange, onTimeChange, @@ -44,6 +45,7 @@ export const ActionsLogFilters = memo( onChangeCommandsFilter: (selectedCommands: string[]) => void; onChangeStatusesFilter: (selectedStatuses: string[]) => void; onChangeUsersFilter: (selectedUsers: string[]) => void; + onChangeWithAutomatedActionsFilter: () => void; onRefresh: () => void; onRefreshChange: (evt: OnRefreshChangeProps) => void; onTimeChange: ({ start, end }: DurationRange) => void; @@ -79,7 +81,11 @@ export const ActionsLogFilters = memo( data-test-subj={dataTestSubj} /> {responseActionsEnabled && ( - + )} ); @@ -88,6 +94,7 @@ export const ActionsLogFilters = memo( isFlyout, onChangeCommandsFilter, onChangeHostsFilter, + onChangeWithAutomatedActionsFilter, onChangeStatusesFilter, responseActionsEnabled, showHostsFilter, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx index f7af034b2a9efb..5e2fb4bb4969fe 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx @@ -294,7 +294,7 @@ describe('Response actions history', () => { statuses: [], userIds: [], withOutputs: [], - withAutomatedActions: true, + withAutomatedActions: false, }, expect.anything() ); @@ -540,6 +540,7 @@ describe('Response actions history', () => { 'Input', 'Parameters', 'Comment', + 'Hostname', 'Output:', ] ); @@ -1141,7 +1142,7 @@ describe('Response actions history', () => { statuses: ['failed', 'pending'], userIds: [], withOutputs: [], - withAutomatedActions: true, + withAutomatedActions: false, }, expect.anything() ); @@ -1343,7 +1344,7 @@ describe('Response actions history', () => { statuses: [], userIds: [], withOutputs: [], - withAutomatedActions: true, + withAutomatedActions: false, }, expect.anything() ); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx index f3a19317f794cd..86fd2ea11f54af 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx @@ -71,7 +71,7 @@ export const ResponseActionsLog = memo< statuses: [], userIds: [], withOutputs: [], - withAutomatedActions: true, + withAutomatedActions: false, }); // update query state from URL params @@ -175,6 +175,13 @@ export const ResponseActionsLog = memo< [setQueryParams] ); + const onToggleAutomatedActionsFilter = useCallback(() => { + setQueryParams((prevState) => ({ + ...prevState, + withAutomatedActions: !prevState.withAutomatedActions, + })); + }, [setQueryParams]); + // handle on change hosts filter const onChangeHostsFilter = useCallback( (selectedAgentIds: string[]) => { @@ -245,6 +252,7 @@ export const ResponseActionsLog = memo< onChangeCommandsFilter={onChangeCommandsFilter} onChangeStatusesFilter={onChangeStatusesFilter} onChangeUsersFilter={onChangeUsersFilter} + onChangeWithAutomatedActionsFilter={onToggleAutomatedActionsFilter} onRefresh={onRefresh} onRefreshChange={onRefreshChange} onTimeChange={onTimeChange} diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx index 901d0c68b90405..8a93193a42a97e 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx @@ -71,6 +71,12 @@ export const OUTPUT_MESSAGES = Object.freeze({ defaultMessage: 'Comment', } ), + hostname: i18n.translate( + 'xpack.securitySolution.responseActionsList.list.item.expandSection.hostname', + { + defaultMessage: 'Hostname', + } + ), }, }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/automated_response_actions.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/automated_response_actions.cy.ts index 3b7c59a55a0348..a509af049fbf12 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/automated_response_actions.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/automated_response_actions.cy.ts @@ -23,7 +23,9 @@ import { changeAlertsFilter } from '../../tasks/alerts'; describe('Automated Response Actions', () => { const endpointHostname = Cypress.env(ENDPOINT_VM_NAME); - const unenrolledHost = 'Host unenrolled'; + const hostname = Cypress.env('hostname'); + const fleetHostname = `dev-fleet-server.${hostname}`; + beforeEach(() => { login(); }); @@ -75,37 +77,16 @@ describe('Automated Response Actions', () => { changeAlertsFilter('event.category: "file"'); cy.getByTestSubj('expand-event').first().click(); - cy.getByTestSubj('endpointViewTab').click(); - cy.getByTestSubj('endpoint-actions-notification').should('not.have.text', '0'); - cy.getByTestSubj('endpoint-actions-results-table').within(() => { - cy.get('tbody tr').each(($tr, index) => { - cy.wrap($tr).within(() => { - cy.contains('Triggered by rule'); - if (index === 0) { - cy.getByTestSubj('endpoint-actions-results-table-column-hostname').contains( - endpointHostname - ); - cy.getByTestSubj('endpoint-actions-results-table-column-status', { - timeout: 60000, - }).should(($status) => { - expect($status).to.contain('Successful'); - }); - cy.getByTestSubj('endpoint-actions-results-table-expand-button').click(); - cy.wrap($tr).siblings().contains('isolate completed successfully'); - } - if (index === 1) { - cy.getByTestSubj('endpoint-actions-results-table-column-hostname').contains( - unenrolledHost - ); - cy.getByTestSubj('endpoint-actions-results-table-column-status').contains('Failed'); - cy.getByTestSubj('endpoint-actions-results-table-expand-button').click(); - cy.wrap($tr) - .siblings() - .contains('The host does not have Elastic Defend integration installed'); - } - }); - }); - }); + cy.getByTestSubj('responseActionsViewTab').click(); + cy.getByTestSubj('response-actions-notification').should('not.have.text', '0'); + + cy.getByTestSubj(`response-results-${endpointHostname}-details-tray`) + .should('contain', 'isolate completed successfully') + .and('contain', endpointHostname); + + cy.getByTestSubj(`response-results-${fleetHostname}-details-tray`) + .should('contain', 'The host does not have Elastic Defend integration installed') + .and('contain', 'dev-fleet-server'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_automated_action_list.ts b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_automated_action_list.ts new file mode 100644 index 00000000000000..9b1cf1b5a79b59 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_automated_action_list.ts @@ -0,0 +1,158 @@ +/* + * 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 { UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { lastValueFrom } from 'rxjs'; + +import { compact, map } from 'lodash'; +import type { ActionDetails, LogsEndpointActionWithHosts } from '../../../../common/endpoint/types'; +import { SortOrder } from '../../../../common/search_strategy/security_solution/response_actions/types'; +import type { + ActionResponsesRequestOptions, + ActionRequestOptions, + ActionRequestStrategyResponse, + ActionResponsesRequestStrategyResponse, +} from '../../../../common/search_strategy/security_solution/response_actions'; +import { ResponseActionsQueries } from '../../../../common/search_strategy/security_solution/response_actions'; +import { useKibana } from '../../../common/lib/kibana'; +import type { + EndpointAutomatedActionListRequestQuery, + EndpointAutomatedActionResponseRequestQuery, +} from '../../../../common/endpoint/schema/automated_actions'; + +interface GetAutomatedActionsListOptions { + enabled: boolean; +} + +export const useGetAutomatedActionList = ( + query: EndpointAutomatedActionListRequestQuery, + { enabled }: GetAutomatedActionsListOptions +): UseQueryResult => { + const { data } = useKibana().services; + + const { alertIds } = query; + return useQuery({ + queryKey: ['get-automated-action-list', { alertIds }], + queryFn: async () => { + const responseData = await lastValueFrom( + data.search.search( + { + alertIds, + sort: { + order: SortOrder.desc, + field: '@timestamp', + }, + factoryQueryType: ResponseActionsQueries.actions, + }, + { + strategy: 'securitySolutionSearchStrategy', + } + ) + ); + + return { + ...responseData, + items: compact(map(responseData.edges, '_source')), + }; + }, + enabled, + keepPreviousData: true, + }); +}; + +interface GetAutomatedActionResponseListOptions { + enabled: boolean; + action: LogsEndpointActionWithHosts; + isLive?: boolean; +} + +type GetAutomatedActionResponseListResponse = Pick< + ActionDetails, + 'completedAt' | 'isExpired' | 'wasSuccessful' | 'isCompleted' | 'status' +> & { + action_id: string; +}; + +export const useGetAutomatedActionResponseList = ( + query: EndpointAutomatedActionResponseRequestQuery, + { enabled, action: requestAction, isLive = false }: GetAutomatedActionResponseListOptions +): UseQueryResult => { + const { data } = useKibana().services; + const { expiration, actionId, agent } = query; + + return useQuery({ + queryKey: ['allResponsesResults', { actionId }], + queryFn: async (): Promise => { + const responseData = await lastValueFrom( + data.search.search( + { + actionId, + expiration, + sort: { + order: SortOrder.desc, + field: '@timestamp', + }, + agents: (Array.isArray(agent.id) ? agent.id : [agent.id]).length, + factoryQueryType: ResponseActionsQueries.results, + }, + { + strategy: 'securitySolutionSearchStrategy', + } + ) + ); + + const action = responseData.edges[0]?._source; + + return { + action_id: actionId, + completedAt: action?.EndpointActions.completed_at, + isExpired: responseData.isExpired, + wasSuccessful: responseData.wasSuccessful, + isCompleted: responseData.isCompleted, + status: responseData.status, + }; + }, + select: (response) => combineResponse(requestAction, response), + keepPreviousData: true, + enabled, + refetchInterval: isLive ? 5000 : false, + }); +}; + +const combineResponse = ( + action: LogsEndpointActionWithHosts, + responseData: GetAutomatedActionResponseListResponse +): ActionDetails => { + const { rule } = action; + const { parameters, alert_id: alertId, comment, command, hosts } = action.EndpointActions.data; + + return { + id: action.EndpointActions.action_id, + agents: action.agent.id as string[], + parameters, + ...(alertId?.length ? { alertIds: alertId } : {}), + ...(rule + ? { + ruleId: rule.id, + ruleName: rule.name, + } + : {}), + createdBy: action.rule?.name || 'unknown', + comment, + command, + hosts, + startedAt: action['@timestamp'], + completedAt: responseData?.completedAt, + isCompleted: !!responseData?.isCompleted, + isExpired: !!responseData?.isExpired, + wasSuccessful: !!responseData?.isCompleted, + status: responseData.status, + agentState: {}, + errors: action.error ? [action.error.message as string] : undefined, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.test.ts b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.test.ts index 0296d83469ccf8..932a25deeac43c 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.test.ts @@ -51,7 +51,7 @@ describe('useGetEndpointActionList hook', () => { pageSize: 20, startDate: 'now-5d', endDate: 'now', - withAutomatedActions: true, + withAutomatedActions: false, }) ); @@ -66,7 +66,7 @@ describe('useGetEndpointActionList hook', () => { pageSize: 20, startDate: 'now-5d', userIds: ['*elastic*', '*citsale*'], - withAutomatedActions: true, + withAutomatedActions: false, }, }); }); diff --git a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.ts b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.ts index a7c1f563887db6..5b9c23760aaf97 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.ts @@ -10,10 +10,7 @@ import type { IHttpFetchError } from '@kbn/core-http-browser'; import { useQuery } from '@tanstack/react-query'; import type { EndpointActionListRequestQuery } from '../../../../common/endpoint/schema/actions'; import { useHttp } from '../../../common/lib/kibana'; -import { - BASE_ENDPOINT_ACTION_ALERTS_ROUTE, - BASE_ENDPOINT_ACTION_ROUTE, -} from '../../../../common/endpoint/constants'; +import { BASE_ENDPOINT_ACTION_ROUTE } from '../../../../common/endpoint/constants'; import type { ActionListApiResponse } from '../../../../common/endpoint/types'; interface ErrorType { @@ -35,14 +32,12 @@ export const useGetEndpointActionList = ( userIds = query.userIds.map((userId) => `*${userId}*`); } - // TODO: Temporary solution until we decide on how RBAC should look like for Actions in Alerts - const path = query.alertId ? BASE_ENDPOINT_ACTION_ALERTS_ROUTE : BASE_ENDPOINT_ACTION_ROUTE; return useQuery>({ queryKey: ['get-action-list', query], ...options, keepPreviousData: true, queryFn: async () => { - return http.get(path, { + return http.get(BASE_ENDPOINT_ACTION_ROUTE, { query: { agentIds: query.agentIds, commands: query.commands, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx index 7c70328c8e5a97..ac425b18bbb7ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx @@ -152,7 +152,7 @@ describe('event details panel component', () => { }), }, osquery: { - OsqueryResults: jest.fn().mockReturnValue(null), + OsqueryResult: jest.fn().mockReturnValue(null), fetchAllLiveQueries: jest.fn().mockReturnValue({ data: { data: { items: [] } } }), }, }, diff --git a/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/action_responder.ts b/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/action_responder.ts index 8686698dfdfc6f..e8aa408b66e317 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/action_responder.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/action_responder.ts @@ -42,7 +42,7 @@ export class ActionResponderService extends BaseRunningService { const { data: actions } = await fetchEndpointActionList(kbnClient, { page: nextPage++, pageSize: 100, - withAutomatedActions: true, + withAutomatedActions: false, }); if (actions.length === 0) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts index 82ff69e4d4bda3..5ce5cf1b522763 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts @@ -11,10 +11,7 @@ * 2.0. */ -import { - BASE_ENDPOINT_ACTION_ALERTS_ROUTE, - BASE_ENDPOINT_ACTION_ROUTE, -} from '../../../../common/endpoint/constants'; +import { BASE_ENDPOINT_ACTION_ROUTE } from '../../../../common/endpoint/constants'; import { EndpointActionListRequestSchema } from '../../../../common/endpoint/schema/actions'; import { actionListHandler } from './list_handler'; @@ -41,20 +38,4 @@ export function registerActionListRoutes( actionListHandler(endpointContext) ) ); - - // TODO: This route is a temporary solution until we decide on how RBAC should look like for Actions in Alerts - if (endpointContext.experimentalFeatures.endpointResponseActionsEnabled) { - router.get( - { - path: BASE_ENDPOINT_ACTION_ALERTS_ROUTE, - validate: EndpointActionListRequestSchema, - options: { authRequired: true, tags: ['access:securitySolution'] }, - }, - withEndpointAuthz( - {}, - endpointContext.logFactory.get('endpointActionList'), - actionListHandler(endpointContext) - ) - ); - } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts index 560e059952fe2e..d1a84878f7468d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts @@ -117,7 +117,7 @@ describe('Action List Handler', () => { commands: 'running-processes', statuses: 'failed', userIds: 'userX', - withAutomatedActions: true, + withAutomatedActions: false, }); expect(mockGetActionListByStatus).toBeCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts index 90945f49a8edf8..12f41c821f1b41 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/create/index.ts @@ -61,6 +61,7 @@ type CreateActionPayload = TypeOf & { rule_id?: string; rule_name?: string; error?: string; + hosts?: Record; }; interface CreateActionMetadata { @@ -152,6 +153,7 @@ export const actionCreateService = ( command: payload.command, comment: payload.comment ?? undefined, ...(payload.alert_ids ? { alert_id: payload.alert_ids } : {}), + ...(payload.hosts ? { hosts: payload.hosts } : {}), parameters: getActionParameters() ?? undefined, }, } as Omit, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts index b7e54c2d95a17b..e665a3cbf99a14 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts @@ -13,11 +13,12 @@ import type { AlertsWithAgentType } from './types'; export const endpointResponseAction = ( responseAction: RuleResponseEndpointAction, endpointAppContextService: EndpointAppContextService, - { alertIds, agentIds, ruleId, ruleName }: AlertsWithAgentType + { alertIds, agentIds, ruleId, ruleName, hosts }: AlertsWithAgentType ) => Promise.all( each(agentIds, async (agent) => endpointAppContextService.getActionCreateService().createActionFromAlert({ + hosts, endpoint_ids: [agent], alert_ids: alertIds, comment: responseAction.params.comment, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/osquery_response_action.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/osquery_response_action.ts index d9ef5ed1574c07..f2bfae7a0b197b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/osquery_response_action.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/osquery_response_action.ts @@ -14,7 +14,7 @@ import type { AlertsWithAgentType } from './types'; export const osqueryResponseAction = ( responseAction: RuleResponseOsqueryAction, osqueryCreateActionService: SetupPlugins['osquery']['createActionService'], - { alerts, alertIds, agentIds }: AlertsWithAgentType + { alerts, alertIds, agentIds }: Pick ) => { const temporaryQueries = responseAction.params.queries?.length ? responseAction.params.queries diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts index da1bdf7022c378..bd88e7e5cd1bd6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts @@ -16,7 +16,7 @@ import { endpointResponseAction } from './endpoint_response_action'; import type { AlertsWithAgentType } from './types'; import type { ScheduleNotificationActions } from '../rule_types/types'; -type Alerts = Array; +type Alerts = Array; interface ScheduleNotificationResponseActionsService { endpointAppContextService: EndpointAppContextService; @@ -31,7 +31,7 @@ export const getScheduleNotificationResponseActionsService = ({ signals, responseActions }: ScheduleNotificationActions) => { const filteredAlerts = (signals as Alerts).filter((alert) => alert.agent?.id); - const { alerts, agentIds, alertIds }: AlertsWithAgentType = reduce( + const { alerts, agentIds, alertIds, hosts }: AlertsWithAgentType = reduce( filteredAlerts, (acc, alert) => { const agentId = alert.agent?.id; @@ -40,11 +40,17 @@ export const getScheduleNotificationResponseActionsService = alerts: [...acc.alerts, alert], agentIds: uniq([...acc.agentIds, agentId]), alertIds: [...acc.alertIds, (alert as unknown as { _id: string })._id], + hosts: { + ...acc.hosts, + [agentId]: { + name: alert.agent?.name || '', + }, + }, }; } return acc; }, - { alerts: [], agentIds: [], alertIds: [] } as AlertsWithAgentType + { alerts: [], agentIds: [], alertIds: [], hosts: {} } as AlertsWithAgentType ); each(responseActions, (responseAction) => { @@ -63,6 +69,7 @@ export const getScheduleNotificationResponseActionsService = alerts, alertIds, agentIds, + hosts, ruleId: alerts[0][ALERT_RULE_UUID], ruleName: alerts[0][ALERT_RULE_NAME], }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/types.ts index d63d3905828274..fcb18adf850c3d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/types.ts @@ -17,4 +17,5 @@ export interface AlertsWithAgentType { alertIds: string[]; ruleId?: string; ruleName?: string; + hosts: Record; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts index 33e57ff62216b0..3ef80e3fa909c6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts @@ -15,6 +15,7 @@ import { ctiFactoryTypes } from './cti'; import { riskScoreFactory } from './risk_score'; import { usersFactory } from './users'; import { firstLastSeenFactory } from './last_first_seen'; +import { responseActionsFactory } from './response_actions'; export const securitySolutionFactory: Record< FactoryQueryTypes, @@ -27,4 +28,5 @@ export const securitySolutionFactory: Record< ...ctiFactoryTypes, ...riskScoreFactory, ...firstLastSeenFactory, + ...responseActionsFactory, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/actions/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/actions/index.ts new file mode 100644 index 00000000000000..66790b731ea932 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/actions/index.ts @@ -0,0 +1,37 @@ +/* + * 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 { IEsSearchResponse } from '@kbn/data-plugin/common'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +import { buildResponseActionsQuery } from './query.all_actions.dsl'; +import type { SecuritySolutionFactory } from '../../types'; +import type { + ActionRequestOptions, + ActionRequestStrategyResponse, + ResponseActionsQueries, +} from '../../../../../../common/search_strategy/security_solution/response_actions'; + +export const allActions: SecuritySolutionFactory = { + buildDsl: (options: ActionRequestOptions) => { + return buildResponseActionsQuery(options); + }, + parse: async ( + options: ActionRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildResponseActionsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/actions/query.all_actions.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/actions/query.all_actions.dsl.ts new file mode 100644 index 00000000000000..40f422fcd25b03 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/actions/query.all_actions.dsl.ts @@ -0,0 +1,47 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { ISearchRequestParams } from '@kbn/data-plugin/common'; +import { OSQUERY_ACTIONS_INDEX } from '@kbn/osquery-plugin/common/constants'; +import type { ActionRequestOptions } from '../../../../../../common/search_strategy/security_solution/response_actions'; +import { ENDPOINT_ACTIONS_INDEX } from '../../../../../../common/endpoint/constants'; + +export const buildResponseActionsQuery = ({ + alertIds, + sort, +}: ActionRequestOptions): ISearchRequestParams => { + const dslQuery = { + allow_no_indices: true, + index: [ENDPOINT_ACTIONS_INDEX, OSQUERY_ACTIONS_INDEX], + ignore_unavailable: true, + body: { + query: { + bool: { + minimum_should_match: 2, + should: [ + { term: { type: 'INPUT_ACTION' } }, + { terms: { alert_ids: alertIds } }, + { + terms: { 'data.alert_id': alertIds }, + }, + ] as estypes.QueryDslQueryContainer[], + }, + }, + sort: [ + { + [sort.field]: { + order: sort.order, + }, + }, + ], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/index.ts new file mode 100644 index 00000000000000..a9eb802b62441c --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { FactoryQueryTypes } from '../../../../../common/search_strategy'; +import { ResponseActionsQueries } from '../../../../../common/search_strategy/security_solution/response_actions'; +import type { SecuritySolutionFactory } from '../types'; +import { allActions } from './actions'; +import { actionResults } from './results'; + +export const responseActionsFactory: Record< + ResponseActionsQueries, + SecuritySolutionFactory +> = { + [ResponseActionsQueries.actions]: allActions, + [ResponseActionsQueries.results]: actionResults, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/results/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/results/index.ts new file mode 100644 index 00000000000000..cc3d0fdda46254 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/results/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import type { + ActionResponsesRequestOptions, + ActionResponsesRequestStrategyResponse, + ResponseActionsQueries, +} from '../../../../../../common/search_strategy/security_solution/response_actions'; + +import { buildActionResultsQuery } from './query.action_results.dsl'; +import type { SecuritySolutionFactory } from '../../types'; + +export const actionResults: SecuritySolutionFactory = { + buildDsl: (options: ActionResponsesRequestOptions) => { + return buildActionResultsQuery(options); + }, + parse: async (options, response): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildActionResultsQuery(options))], + }; + + const responded = + response.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count ?? 0; + + // We should get just one agent id, but just in case we get more than one, we'll use the length + const agentsWithPendingActions = options.agents - responded; + const isExpired = !options.expiration ? true : new Date(options.expiration) < new Date(); + const isCompleted = isExpired || agentsWithPendingActions <= 0; + + const aggsBuckets = + response.rawResponse?.aggregations?.aggs.responses_by_action_id?.responses.buckets; + const successful = aggsBuckets?.find((bucket) => bucket.key === 'success')?.doc_count ?? 0; + + const wasSuccessful = responded === successful; + + // TODO use getActionsStatus() - this requires a refactor of the function to accept isExpired + const status = isExpired + ? 'failed' + : isCompleted + ? wasSuccessful + ? 'successful' + : 'failed' + : 'pending'; + + return { + ...response, + edges: response.rawResponse.hits.hits, + isCompleted, + wasSuccessful: responded === successful, + isExpired, + inspect, + status, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/results/query.action_results.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/results/query.action_results.dsl.ts new file mode 100644 index 00000000000000..80758a18513b1a --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/response_actions/results/query.action_results.dsl.ts @@ -0,0 +1,66 @@ +/* + * 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 { ISearchRequestParams } from '@kbn/data-plugin/common'; +import type { ActionResponsesRequestOptions } from '../../../../../../common/search_strategy/security_solution/response_actions'; +import { ENDPOINT_ACTION_RESPONSES_INDEX } from '../../../../../../common/endpoint/constants'; + +export const buildActionResultsQuery = ({ + actionId, + sort, +}: ActionResponsesRequestOptions): ISearchRequestParams => { + const dslQuery = { + allow_no_indices: true, + index: [ENDPOINT_ACTION_RESPONSES_INDEX], + body: { + size: 1, + query: { + term: { action_id: actionId }, + }, + aggs: { + aggs: { + global: {}, + aggs: { + responses_by_action_id: { + filter: { + bool: { + must: [ + { + match: { + action_id: actionId, + }, + }, + ], + }, + }, + aggs: { + responses: { + terms: { + script: { + lang: 'painless', + source: + "if (doc.containsKey('error.code') && doc['error.code'].size()==0) { return 'success' } else { return 'error' }", + } as const, + }, + }, + }, + }, + }, + }, + }, + sort: [ + { + [sort.field]: { + order: sort.order, + }, + }, + ], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts index f222e2130ee260..4864bde288546f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts @@ -10,12 +10,13 @@ import type { KibanaRequest, SavedObjectsClientContract, } from '@kbn/core/server'; -import type { IEsSearchResponse, ISearchRequestParams } from '@kbn/data-plugin/common'; +import type { ISearchRequestParams } from '@kbn/data-plugin/common'; import type { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import type { FactoryQueryTypes, StrategyRequestType, StrategyResponseType, + StrategyParseResponseType, } from '../../../../common/search_strategy/security_solution'; import type { EndpointAppContext } from '../../../endpoint/types'; @@ -23,7 +24,7 @@ export interface SecuritySolutionFactory { buildDsl: (options: StrategyRequestType) => ISearchRequestParams; parse: ( options: StrategyRequestType, - response: IEsSearchResponse, + response: StrategyParseResponseType, deps?: { esClient: IScopedClusterClient; savedObjectsClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts index 1acb6687b8ace6..f513d07d4c047f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -15,6 +15,7 @@ import type { FactoryQueryTypes, StrategyResponseType, StrategyRequestType, + StrategyParseResponseType, } from '../../../common/search_strategy/security_solution'; import { securitySolutionFactory } from './factory'; import type { SecuritySolutionFactory } from './factory/types'; @@ -37,7 +38,9 @@ export const securitySolutionSearchStrategyProvider = string, ruleDataClient?: IRuleDataClient | null ): ISearchStrategy, StrategyResponseType> => { - const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + const es = data.search.getSearchStrategy( + ENHANCED_ES_SEARCH_STRATEGY + ) as unknown as ISearchStrategy, StrategyParseResponseType>; return { search: (request, options, deps) => { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 569854a2019bd5..1768271cd6fbf7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27217,7 +27217,6 @@ "xpack.osquery.agentPolicy.confirmModalCalloutTitle": "Cette action va mettre à jour {agentCount, plural, one {# agent} many {# agents} other {# agents}}", "xpack.osquery.agents.mulitpleSelectedAgentsText": "{numAgents} agents sélectionnés.", "xpack.osquery.agents.oneSelectedAgentText": "{numAgents} agent sélectionné.", - "xpack.osquery.cases.permissionDenied": " Pour accéder à ces résultats, demandez à votre administrateur vos privilèges Kibana pour {osquery}.", "xpack.osquery.configUploader.unsupportedFileTypeText": "Le type de fichier {fileType} n'est pas pris en charge, veuillez charger le fichier de configuration {supportedFileTypes}", "xpack.osquery.createScheduledQuery.agentPolicyAgentsCountText": "{count, plural, one {# agent a été enregistré} many {# agents ont été enregistrés} other {# agents ont été enregistrés}}", "xpack.osquery.editPack.pageTitle": "Modifier {queryName}", @@ -29432,7 +29431,6 @@ "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "Exportation réussie de {totalTimelines, plural, =0 {toutes les chronologies} =1 {{totalTimelines} chronologie} one {{totalTimelines} chronologies} many {{totalTimelines} chronologies} other {{totalTimelines} chronologies}}", "xpack.securitySolution.open.timeline.successfullyExportedTimelineTemplatesTitle": "Exportation réussie de {totalTimelineTemplates, plural, =0 {toutes les chronologies} =1 {{totalTimelineTemplates} modèle de chronologie} one {{totalTimelineTemplates} modèles de chronologies} many {{totalTimelineTemplates} modèles de chronologies} other {{totalTimelineTemplates} modèles de chronologies}}", "xpack.securitySolution.osquery.action.missingPrivileges": "Pour accéder à cette page, demandez à votre administrateur vos privilèges Kibana pour {osquery}.", - "xpack.securitySolution.osquery.results.missingPrivileges": "Pour accéder à ces résultats, demandez à votre administrateur vos privilèges Kibana pour {osquery}.", "xpack.securitySolution.overview.ctiDashboardSubtitle": "Affichage : {totalCount} {totalCount, plural, one {indicateur} many {indicateurs} other {indicateurs}}", "xpack.securitySolution.overview.overviewHost.hostsSubtitle": "Affichage : {formattedHostEventsCount} {hostEventsCount, plural, one {événement} many {événements} other {événements}}", "xpack.securitySolution.overview.overviewNetwork.networkSubtitle": "Affichage : {formattedNetworkEventsCount} {networkEventsCount, plural, one {événement} many {événements} other {événements}}", @@ -29454,6 +29452,7 @@ "xpack.securitySolution.responder.header.lastSeen": "Vu pour la dernière fois le {date}", "xpack.securitySolution.responder.hostOffline.callout.body": "L'hôte {name} est hors connexion, donc ses réponses peuvent avoir du retard. Les commandes en attente seront exécutées quand l'hôte se reconnectera.", "xpack.securitySolution.responseActionFileDownloadLink.passcodeInfo": "(Code secret du fichier ZIP : {passcode}).", + "xpack.securitySolution.responseActions.results.missingPrivileges": "Pour accéder à ces résultats, demandez à votre administrateur vos privilèges Kibana pour {integration}.", "xpack.securitySolution.responseActionsList.flyout.title": "Historique des actions de réponse : {hostname}", "xpack.securitySolution.responseActionsList.investigationGuideSuggestion": "Il y a {queriesLength, plural, one {une recherche} many {des recherches} other {des recherches}} dans le guide d'investigation. {queriesLength, plural, one {L'ajouter en tant qu'action de réponse} many {Les ajouter en tant qu'actions de réponse} other {Les ajouter en tant qu'actions de réponse}} ?", "xpack.securitySolution.responseActionsList.list.filter.emptyMessage": "Aucun {filterName} disponible", @@ -32204,7 +32203,6 @@ "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "Enrichi avec la Threat Intelligence", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "Cette valeur de champ possède des informations supplémentaires disponibles provenant de sources de Threat Intelligence.", "xpack.securitySolution.eventDetails.description": "Description", - "xpack.securitySolution.eventDetails.endpointView": "Résultats du point de terminaison", "xpack.securitySolution.eventDetails.field": "Champ", "xpack.securitySolution.eventDetails.filter.placeholder": "Filtre par Champ, Valeur ou Description...", "xpack.securitySolution.eventDetails.jsonView": "JSON", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 60d990d4271a66..f2cba718cfc37b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27199,7 +27199,6 @@ "xpack.osquery.agentPolicy.confirmModalCalloutTitle": "{agentCount, plural, other {#個のエージェント}}が更新されます", "xpack.osquery.agents.mulitpleSelectedAgentsText": "{numAgents}個のエージェントが選択されました。", "xpack.osquery.agents.oneSelectedAgentText": "{numAgents}個のエージェントが選択されました。", - "xpack.osquery.cases.permissionDenied": " これらの結果にアクセスするには、{osquery} Kibana権限について管理者に確認してください。", "xpack.osquery.configUploader.unsupportedFileTypeText": "ファイルタイプ{fileType}はサポートされていません。{supportedFileTypes}構成ファイルをアップロードしてください", "xpack.osquery.createScheduledQuery.agentPolicyAgentsCountText": "{count, plural, other {#個のエージェント}}が登録されました", "xpack.osquery.editPack.pageTitle": "{queryName}の編集", @@ -29413,7 +29412,7 @@ "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} =1 {{totalTimelines}個のタイムライン} other {{totalTimelines}個のタイムライン}}が正常にエクスポートされました", "xpack.securitySolution.open.timeline.successfullyExportedTimelineTemplatesTitle": "{totalTimelineTemplates, plural, =0 {すべてのタイムライン} =1 {{totalTimelineTemplates}個のタイムラインテンプレート} other {{totalTimelineTemplates}個のタイムラインテンプレート}}が正常にエクスポートされました", "xpack.securitySolution.osquery.action.missingPrivileges": "このページにアクセスするには、{osquery} Kibana権限について管理者に確認してください。", - "xpack.securitySolution.osquery.results.missingPrivileges": "これらの結果にアクセスするには、{osquery} Kibana権限について管理者に確認してください。", + "xpack.securitySolution.responseActions.results.missingPrivileges": "これらの結果にアクセスするには、{integration} Kibana権限について管理者に確認してください。", "xpack.securitySolution.overview.ctiDashboardSubtitle": "{totalCount}個の{totalCount, plural, other {インジケーター}}を表示中", "xpack.securitySolution.overview.overviewHost.hostsSubtitle": "{formattedHostEventsCount}個の{hostEventsCount, plural, other {イベント}}を表示中", "xpack.securitySolution.overview.overviewNetwork.networkSubtitle": "{formattedNetworkEventsCount}個の{networkEventsCount, plural, other {イベント}}を表示中", @@ -32185,7 +32184,6 @@ "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "Threat Intelligenceで拡張", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "このフィールド値には脅威インテリジェンスソースの別の情報があります。", "xpack.securitySolution.eventDetails.description": "説明", - "xpack.securitySolution.eventDetails.endpointView": "エンドポイント結果", "xpack.securitySolution.eventDetails.field": "フィールド", "xpack.securitySolution.eventDetails.filter.placeholder": "フィールド、値、または説明でフィルター...", "xpack.securitySolution.eventDetails.jsonView": "JSON", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a69af8bea61f7e..cb804bf53c7295 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27197,7 +27197,6 @@ "xpack.osquery.agentPolicy.confirmModalCalloutTitle": "此操作将更新 {agentCount, plural, other {# 个代理}}", "xpack.osquery.agents.mulitpleSelectedAgentsText": "已选择 {numAgents} 个代理。", "xpack.osquery.agents.oneSelectedAgentText": "已选择 {numAgents} 个代理。", - "xpack.osquery.cases.permissionDenied": " 要访问这些结果,请联系管理员获取 {osquery} Kibana 权限。", "xpack.osquery.configUploader.unsupportedFileTypeText": "文件类型 {fileType} 不受支持,请上传 {supportedFileTypes} 配置文件", "xpack.osquery.createScheduledQuery.agentPolicyAgentsCountText": "{count, plural, other {# 个代理}}已注册", "xpack.osquery.editPack.pageTitle": "编辑 {queryName}", @@ -29409,7 +29408,7 @@ "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "已成功导出{totalTimelines, plural, =0 {所有时间线} =1 {{totalTimelines} 个时间线} other {{totalTimelines} 个时间线}}", "xpack.securitySolution.open.timeline.successfullyExportedTimelineTemplatesTitle": "已成功导出{totalTimelineTemplates, plural, =0 {所有时间线} =1 {{totalTimelineTemplates} 个时间线模板} other {{totalTimelineTemplates} 个时间线模板}}", "xpack.securitySolution.osquery.action.missingPrivileges": "要访问此页面,请联系管理员获取 {osquery} Kibana 权限。", - "xpack.securitySolution.osquery.results.missingPrivileges": "要访问这些结果,请联系管理员获取 {osquery} Kibana 权限。", + "xpack.securitySolution.responseActions.results.missingPrivileges": "要访问这些结果,请联系管理员获取 {integration} Kibana 权限。", "xpack.securitySolution.overview.ctiDashboardSubtitle": "正在显示:{totalCount} 个{totalCount, plural, other {指标}}", "xpack.securitySolution.overview.overviewHost.hostsSubtitle": "正在显示:{formattedHostEventsCount} 个{hostEventsCount, plural, other {事件}}", "xpack.securitySolution.overview.overviewNetwork.networkSubtitle": "正在显示:{formattedNetworkEventsCount} 个{networkEventsCount, plural, other {事件}}", @@ -32181,7 +32180,6 @@ "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "已使用威胁情报扩充", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "此字段值具有威胁情报源提供的其他信息。", "xpack.securitySolution.eventDetails.description": "描述", - "xpack.securitySolution.eventDetails.endpointView": "终端结果", "xpack.securitySolution.eventDetails.field": "字段", "xpack.securitySolution.eventDetails.filter.placeholder": "按字段、值或描述筛选......", "xpack.securitySolution.eventDetails.jsonView": "JSON",