From d8530ff2186da8de93e04adf0a2242a358f21330 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Thu, 22 Jan 2026 14:48:50 +0100 Subject: [PATCH 1/4] Remove @kbn/timelines-plugin dependency from osquery plugin - Create local DataProvider and AddToTimelineHandler types in osquery to avoid direct dependency on timelines plugin - Refactor AddToTimelineButton to require addToTimeline callback prop instead of using timelines service directly - Pass addToTimeline handler from security solution context (OsqueryFlyout, ResponseActionsResults) down to osquery components - Remove timelines from osquery's StartPlugins, moon.yml, and tsconfig.json - Re-enable previously broken timelines and cases cypress test --- .../cypress/e2e/all/alerts_cases.cy.ts | 4 +- .../cypress/e2e/all/alerts_linked_apps.cy.ts | 2 +- .../osquery/cypress/e2e/all/timelines.cy.ts | 3 +- .../platform/plugins/shared/osquery/moon.yml | 1 - .../public/live_queries/form/index.tsx | 6 ++ .../form/pack_queries_status_table.tsx | 15 ++- .../live_queries/form/pack_results_header.tsx | 7 +- .../osquery/public/live_queries/index.tsx | 4 + .../osquery/public/results/results_table.tsx | 21 ++-- .../public/routes/saved_queries/edit/tabs.tsx | 5 + .../osquery_action/index.tsx | 4 + .../pack_field_wrapper.tsx | 4 + .../osquery_results/osquery_result.tsx | 3 +- .../osquery_result_wrapper.tsx | 3 +- .../osquery_results/osquery_results.tsx | 2 + .../osquery_results/types.ts | 3 + .../timelines/add_to_timeline_button.tsx | 97 +++++++++++-------- .../plugins/shared/osquery/public/types.ts | 23 ++++- .../plugins/shared/osquery/tsconfig.json | 1 - .../response_actions_results.tsx | 5 +- .../common/hooks/use_add_to_timeline.ts | 52 ++++++++++ .../components/osquery/osquery_flyout.tsx | 3 + 22 files changed, 206 insertions(+), 62 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_to_timeline.ts diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts index 1b3cd0541580b..c3934c2ba0605 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts @@ -26,7 +26,7 @@ import { import { generateRandomStringName, interceptCaseId } from '../../tasks/integrations'; // Failing: See https://github.com/elastic/kibana/issues/197151 -describe.skip( +describe( 'Alert Event Details - Cases', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { @@ -107,7 +107,7 @@ describe.skip( cy.getBySel('securitySolutionFlyoutResponseButton').click(); cy.getBySel('responseActionsViewWrapper').should('exist'); cy.contains('select * from users;'); - cy.contains("SELECT * FROM os_version where name='Ubuntu';"); + cy.contains(/SELECT \* FROM os_version where name='.+'/); cy.getBySel('osquery-results-comment').each(($comment) => { cy.wrap($comment).within(() => { // On initial load result table might not render due to displayed error diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts index 9d248c8178b6b..3aad10806d1bb 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts @@ -18,7 +18,7 @@ import { import { closeModalIfVisible, closeToastIfVisible } from '../../tasks/integrations'; // FLAKY: https://github.com/elastic/kibana/issues/218206 -describe.skip( +describe( 'Alert Event Details', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'], diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts index 02e6d02d474bc..00a200d3f3262 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/timelines.cy.ts @@ -10,8 +10,7 @@ import { takeOsqueryActionWithParams } from '../../tasks/live_query'; import { ServerlessRoleName } from '../../support/roles'; import { disableNewFeaturesTours } from '../../tasks/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/229432 -describe.skip('ALL - Timelines', { tags: ['@ess'] }, () => { +describe('ALL - Timelines', { tags: ['@ess'] }, () => { before(() => { initializeDataViews(); }); diff --git a/x-pack/platform/plugins/shared/osquery/moon.yml b/x-pack/platform/plugins/shared/osquery/moon.yml index 4761def8c5e57..20dc6b872f870 100644 --- a/x-pack/platform/plugins/shared/osquery/moon.yml +++ b/x-pack/platform/plugins/shared/osquery/moon.yml @@ -36,7 +36,6 @@ dependsOn: - '@kbn/lens-plugin' - '@kbn/security-plugin' - '@kbn/triggers-actions-ui-plugin' - - '@kbn/timelines-plugin' - '@kbn/spaces-plugin' - '@kbn/i18n' - '@kbn/rison' diff --git a/x-pack/platform/plugins/shared/osquery/public/live_queries/form/index.tsx b/x-pack/platform/plugins/shared/osquery/public/live_queries/form/index.tsx index 6b99e6228be49..85aefed836918 100644 --- a/x-pack/platform/plugins/shared/osquery/public/live_queries/form/index.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/live_queries/form/index.tsx @@ -26,6 +26,7 @@ import { usePacks } from '../../packs/use_packs'; import { useCreateLiveQuery } from '../use_create_live_query_action'; import { useLiveQueryDetails } from '../../actions/use_live_query_details'; import type { AgentSelection } from '../../agents/types'; +import type { AddToTimelineHandler } from '../../types'; import LiveQueryQueryField from './live_query_query_field'; import { AgentsTableField } from './agents_table_field'; import { savedQueryDataSerializer } from '../../saved_queries/form/use_saved_query_form'; @@ -63,6 +64,7 @@ interface LiveQueryFormProps { formType?: FormType; enabled?: boolean; hideAgentsField?: boolean; + addToTimeline?: AddToTimelineHandler; } const LiveQueryFormComponent: React.FC = ({ @@ -72,6 +74,7 @@ const LiveQueryFormComponent: React.FC = ({ formType = 'steps', enabled = true, hideAgentsField = false, + addToTimeline, }) => { const alertAttachmentContext = useContext(AlertAttachmentContext); @@ -228,9 +231,11 @@ const LiveQueryFormComponent: React.FC = ({ endDate={singleQueryDetails?.expiration} agentIds={singleQueryDetails?.agents} liveQueryActionId={liveQueryActionId} + addToTimeline={addToTimeline} /> ) : null, [ + addToTimeline, singleQueryDetails?.action_id, singleQueryDetails?.expiration, singleQueryDetails?.agents, @@ -316,6 +321,7 @@ const LiveQueryFormComponent: React.FC = ({ liveQueryDetails={liveQueryDetails} submitButtonContent={submitButtonContent} showResultsHeader + addToTimeline={addToTimeline} /> ) : ( <> diff --git a/x-pack/platform/plugins/shared/osquery/public/live_queries/form/pack_queries_status_table.tsx b/x-pack/platform/plugins/shared/osquery/public/live_queries/form/pack_queries_status_table.tsx index fdc2c1b25df68..48559e02ef587 100644 --- a/x-pack/platform/plugins/shared/osquery/public/live_queries/form/pack_queries_status_table.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/live_queries/form/pack_queries_status_table.tsx @@ -32,6 +32,7 @@ import { PackViewInLensAction } from '../../lens/pack_view_in_lens'; import { PackViewInDiscoverAction } from '../../discover/pack_view_in_discover'; import { AddToCaseWrapper } from '../../cases/add_to_cases'; import { AddToTimelineButton } from '../../timelines/add_to_timeline_button'; +import type { AddToTimelineHandler } from '../../types'; const truncateTooltipTextCss = { width: '100%', @@ -140,6 +141,7 @@ interface PackQueriesStatusTableProps { startDate?: string; expirationDate?: string; showResultsHeader?: boolean; + addToTimeline?: AddToTimelineHandler; } const PackQueriesStatusTableComponent: React.FC = ({ @@ -150,6 +152,7 @@ const PackQueriesStatusTableComponent: React.FC = ( startDate, expirationDate, showResultsHeader, + addToTimeline, }) => { const [queryDetailsFlyoutOpen, setQueryDetailsFlyoutOpen] = useState<{ id: string; @@ -242,6 +245,7 @@ const PackQueriesStatusTableComponent: React.FC = ( agentIds={agentIds} failedAgentsCount={item?.failed ?? 0} error={item.error} + addToTimeline={addToTimeline} /> @@ -251,7 +255,7 @@ const PackQueriesStatusTableComponent: React.FC = ( return itemIdToExpandedRowMapValues; }); }, - [actionId, startDate, expirationDate, agentIds] + [actionId, startDate, expirationDate, agentIds, addToTimeline] ); const renderToggleResultsAction = useCallback( @@ -285,7 +289,12 @@ const PackQueriesStatusTableComponent: React.FC = ( { render: (item: { action_id: string }) => item.action_id && ( - + ), }, { @@ -317,6 +326,7 @@ const PackQueriesStatusTableComponent: React.FC = ( }, [ actionId, + addToTimeline, agentIds, handleQueryFlyoutOpen, renderDiscoverResultsAction, @@ -422,6 +432,7 @@ const PackQueriesStatusTableComponent: React.FC = ( queryIds={queryIds as string[]} actionId={actionId} agentIds={agentIds} + addToTimeline={addToTimeline} /> )} ({ @@ -29,8 +31,8 @@ const iconsListCss = { }; export const PackResultsHeader = React.memo( - ({ actionId, agentIds, queryIds }) => { - const iconProps = useMemo(() => ({ color: 'text', size: 'xs', iconSize: 'l' }), []); + ({ actionId, agentIds, queryIds, addToTimeline }) => { + const iconProps = useMemo(() => ({ color: 'text', size: 'xs', iconSize: 'l' } as const), []); return ( <> @@ -64,6 +66,7 @@ export const PackResultsHeader = React.memo( value={queryIds} isIcon={true} iconProps={iconProps} + addToTimeline={addToTimeline} /> diff --git a/x-pack/platform/plugins/shared/osquery/public/live_queries/index.tsx b/x-pack/platform/plugins/shared/osquery/public/live_queries/index.tsx index c6738cc99b3df..6146bddeb2ae7 100644 --- a/x-pack/platform/plugins/shared/osquery/public/live_queries/index.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/live_queries/index.tsx @@ -13,6 +13,7 @@ import { replaceParamsQuery } from '../../common/utils/replace_params_query'; import { AlertAttachmentContext } from '../common/contexts'; import { LiveQueryForm } from './form'; import type { AgentSelection } from '../agents/types'; +import type { AddToTimelineHandler } from '../types'; interface LiveQueryProps { agentId?: string; @@ -32,6 +33,7 @@ interface LiveQueryProps { hideAgentsField?: boolean; packId?: string; agentSelection?: AgentSelection; + addToTimeline?: AddToTimelineHandler; } const LiveQueryComponent: React.FC = ({ @@ -51,6 +53,7 @@ const LiveQueryComponent: React.FC = ({ packId, agentSelection, timeout, + addToTimeline, }) => { const initialAgentSelection = useMemo(() => { if (agentSelection) { @@ -103,6 +106,7 @@ const LiveQueryComponent: React.FC = ({ formType={formType} enabled={enabled} hideAgentsField={hideAgentsField} + addToTimeline={addToTimeline} /> ); }; diff --git a/x-pack/platform/plugins/shared/osquery/public/results/results_table.tsx b/x-pack/platform/plugins/shared/osquery/public/results/results_table.tsx index 56fe0e12da113..c01da5edeff55 100644 --- a/x-pack/platform/plugins/shared/osquery/public/results/results_table.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/results/results_table.tsx @@ -30,6 +30,7 @@ import React, { createContext, useEffect, useState, useCallback, useContext, use import type { ECSMapping } from '@kbn/osquery-io-ts-types'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; import { AddToTimelineButton } from '../timelines/add_to_timeline_button'; +import type { AddToTimelineHandler } from '../types'; import { useAllResults } from './use_all_results'; import type { ResultEdges } from '../../common/search_strategy'; import { Direction } from '../../common/search_strategy'; @@ -99,6 +100,7 @@ export interface ResultsTableComponentProps { startDate?: string; liveQueryActionId?: string; error?: string; + addToTimeline?: AddToTimelineHandler; } const ResultsTableComponent: React.FC = ({ @@ -109,6 +111,7 @@ const ResultsTableComponent: React.FC = ({ endDate, liveQueryActionId, error, + addToTimeline, }) => { const [isLive, setIsLive] = useState(true); @@ -126,7 +129,6 @@ const ResultsTableComponent: React.FC = ({ const { application: { getUrlForApp }, appName, - timelines, notifications: { toasts }, i18n: i18nStart, theme, @@ -369,7 +371,7 @@ const ResultsTableComponent: React.FC = ({ const leadingControlColumns: EuiDataGridControlColumn[] = useMemo(() => { const edges = allResultsData?.edges; - if (timelines && edges) { + if (addToTimeline && edges) { return [ { id: 'timeline', @@ -381,14 +383,21 @@ const ResultsTableComponent: React.FC = ({ }; const eventId = edges[visibleRowIndex]?._id; - return ; + return ( + + ); }, }, ]; } return []; - }, [allResultsData?.edges, timelines]); + }, [addToTimeline, allResultsData?.edges]); const toolbarVisibility = useMemo( () => ({ @@ -408,14 +417,14 @@ const ResultsTableComponent: React.FC = ({ endDate={endDate} startDate={startDate} /> - + {liveQueryActionId && ( )} ), }), - [actionId, agentIds, appName, endDate, liveQueryActionId, startDate] + [actionId, addToTimeline, agentIds, appName, endDate, liveQueryActionId, startDate] ); useEffect( diff --git a/x-pack/platform/plugins/shared/osquery/public/routes/saved_queries/edit/tabs.tsx b/x-pack/platform/plugins/shared/osquery/public/routes/saved_queries/edit/tabs.tsx index c55bebcd9056b..c19511fc94efa 100644 --- a/x-pack/platform/plugins/shared/osquery/public/routes/saved_queries/edit/tabs.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/routes/saved_queries/edit/tabs.tsx @@ -11,6 +11,7 @@ import type { ECSMapping } from '@kbn/osquery-io-ts-types'; import { ResultsTable } from '../../../results/results_table'; import { ActionResultsSummary } from '../../../action_results/action_results_summary'; +import type { AddToTimelineHandler } from '../../../types'; const euiTabbedContentCss = { 'div.euiTabs': { @@ -27,6 +28,7 @@ interface ResultTabsProps { endDate?: string; liveQueryActionId?: string; error?: string; + addToTimeline?: AddToTimelineHandler; } const ResultTabsComponent: React.FC = ({ @@ -38,6 +40,7 @@ const ResultTabsComponent: React.FC = ({ startDate, liveQueryActionId, error, + addToTimeline, }) => { const tabs = useMemo( () => [ @@ -54,6 +57,7 @@ const ResultTabsComponent: React.FC = ({ endDate={endDate} liveQueryActionId={liveQueryActionId} error={error} + addToTimeline={addToTimeline} /> ), }, @@ -86,6 +90,7 @@ const ResultTabsComponent: React.FC = ({ liveQueryActionId, error, failedAgentsCount, + addToTimeline, ] ); diff --git a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_action/index.tsx index 1d4a8137efe73..1099925dc0ba9 100644 --- a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_action/index.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_action/index.tsx @@ -15,6 +15,7 @@ import { useKibana } from '../../common/lib/kibana'; import { LiveQuery } from '../../live_queries'; import { OsqueryIcon } from '../../components/osquery_icon'; import { useIsOsqueryAvailable } from '../use_is_osquery_available'; +import type { AddToTimelineHandler } from '../../types'; export interface OsqueryActionProps { agentId?: string; @@ -22,6 +23,7 @@ export interface OsqueryActionProps { formType: 'steps' | 'simple'; hideAgentsField?: boolean; onSuccess?: () => void; + addToTimeline?: AddToTimelineHandler; } const OsqueryActionComponent: React.FC = ({ @@ -30,6 +32,7 @@ const OsqueryActionComponent: React.FC = ({ defaultValues, hideAgentsField, onSuccess, + addToTimeline, }) => { const permissions = useKibana().services.application.capabilities.osquery; @@ -94,6 +97,7 @@ const OsqueryActionComponent: React.FC = ({ agentId={agentId} hideAgentsField={hideAgentsField} onSuccess={onSuccess} + addToTimeline={addToTimeline} {...defaultValues} /> ); diff --git a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_response_action_type/pack_field_wrapper.tsx b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_response_action_type/pack_field_wrapper.tsx index 4942439844bf7..a5c3a259c4170 100644 --- a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_response_action_type/pack_field_wrapper.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_response_action_type/pack_field_wrapper.tsx @@ -13,6 +13,7 @@ import type { ECSMapping } from '@kbn/osquery-io-ts-types'; import { PackQueriesStatusTable } from '../../live_queries/form/pack_queries_status_table'; import { usePacks } from '../../packs/use_packs'; import { PacksComboBoxField } from '../../live_queries/form/packs_combobox_field'; +import type { AddToTimelineHandler } from '../../types'; interface PackFieldWrapperProps { liveQueryDetails?: { @@ -26,12 +27,14 @@ interface PackFieldWrapperProps { }; submitButtonContent?: React.ReactNode; showResultsHeader?: boolean; + addToTimeline?: AddToTimelineHandler; } export const PackFieldWrapper = ({ liveQueryDetails, submitButtonContent, showResultsHeader, + addToTimeline, }: PackFieldWrapperProps) => { const { data: packsData } = usePacks({}); const { packId } = useWatch<{ packId: string[] }>(); @@ -64,6 +67,7 @@ export const PackFieldWrapper = ({ // @ts-expect-error update types data={liveQueryDetails?.queries ?? selectedPackData?.queries} showResultsHeader={showResultsHeader} + addToTimeline={addToTimeline} /> ) : null} diff --git a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_result.tsx b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_result.tsx index 30e166ae67ad9..8abafd24bc2bd 100644 --- a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_result.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_result.tsx @@ -23,7 +23,7 @@ interface OsqueryResultProps extends OsqueryActionResultsProps { // eslint-disable-next-line react/display-name export const OsqueryResult = React.memo( - ({ actionId, ruleName, startDate, ecsData }) => { + ({ actionId, ruleName, startDate, ecsData, addToTimeline }) => { const [isLive, setIsLive] = useState(false); const { data } = useLiveQueryDetails({ actionId, @@ -49,6 +49,7 @@ export const OsqueryResult = React.memo( startDate={data?.['@timestamp']} expirationDate={data?.expiration} agentIds={data?.agents} + addToTimeline={addToTimeline} /> diff --git a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_result_wrapper.tsx b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_result_wrapper.tsx index 7edf003324a60..49c02fdcdc187 100644 --- a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_result_wrapper.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_result_wrapper.tsx @@ -25,7 +25,7 @@ import type { OsqueryActionResultProps } from './types'; // eslint-disable-next-line react/display-name const OsqueryResultComponent = React.memo( - ({ actionId, ruleName, startDate, ecsData }) => { + ({ actionId, ruleName, startDate, ecsData, addToTimeline }) => { const { read } = useKibana().services.application.capabilities.osquery; const [isLive, setIsLive] = useState(false); @@ -56,6 +56,7 @@ const OsqueryResultComponent = React.memo( startDate={data?.['@timestamp']} expirationDate={data?.expiration} agentIds={data?.agents} + addToTimeline={addToTimeline} /> )} diff --git a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_results.tsx b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_results.tsx index b0f4996bd0b0a..6eeceb9c53abe 100644 --- a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_results.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/osquery_results.tsx @@ -23,6 +23,7 @@ const OsqueryActionResultsComponent: React.FC = ({ ruleName, actionItems, ecsData, + addToTimeline, }) => { const { read } = useKibana().services.application.capabilities.osquery; @@ -43,6 +44,7 @@ const OsqueryActionResultsComponent: React.FC = ({ startDate={startDate} ruleName={ruleName} ecsData={ecsData} + addToTimeline={addToTimeline} /> ); })} diff --git a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/types.ts b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/types.ts index 41f6fbb0e6e23..846017a117c39 100644 --- a/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/types.ts +++ b/x-pack/platform/plugins/shared/osquery/public/shared_components/osquery_results/types.ts @@ -7,11 +7,13 @@ import type { Ecs } from '@kbn/cases-plugin/common'; import type { ActionEdges } from '../../../common/search_strategy'; +import type { AddToTimelineHandler } from '../../types'; export interface OsqueryActionResultsProps { ruleName?: string; ecsData?: Ecs | null; actionItems?: ActionEdges; + addToTimeline?: AddToTimelineHandler; } export interface OsqueryActionResultProps { @@ -19,4 +21,5 @@ export interface OsqueryActionResultProps { ecsData?: Ecs | null; actionId: string; startDate: string; + addToTimeline?: AddToTimelineHandler; } diff --git a/x-pack/platform/plugins/shared/osquery/public/timelines/add_to_timeline_button.tsx b/x-pack/platform/plugins/shared/osquery/public/timelines/add_to_timeline_button.tsx index f59ccfee00abb..d5dd0c5b5222d 100644 --- a/x-pack/platform/plugins/shared/osquery/public/timelines/add_to_timeline_button.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/timelines/add_to_timeline_button.tsx @@ -5,62 +5,79 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { isArray } from 'lodash'; -import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonEmpty, EuiButtonIcon, type EuiButtonIconProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; - -const TimelineComponent = React.memo((props) => ); -TimelineComponent.displayName = 'TimelineComponent'; +import type { AddToTimelineHandler, DataProvider } from '../types'; export interface AddToTimelineButtonProps { field: string; value: string | string[]; + addToTimeline?: AddToTimelineHandler; isIcon?: true; - iconProps?: Record; + iconProps?: Partial; } export const SECURITY_APP_NAME = 'Security'; + export const AddToTimelineButton = (props: AddToTimelineButtonProps) => { - const { timelines, appName, analytics, i18n, theme } = useKibana().services; - const startServices = { analytics, i18n, theme }; - const { field, value, isIcon, iconProps } = props; + const { appName } = useKibana().services; + const { field, value, isIcon, iconProps, addToTimeline } = props; + const addToTimelineLabel = i18n.translate('xpack.osquery.addToTimelineButtonLabel', { + defaultMessage: 'Add to Timeline investigation', + }); - const queryIds = isArray(value) ? value : [value]; - const TimelineIconComponent = useCallback( - (timelineComponentProps: any) => ( - - ), - [iconProps] + const queryIds = useMemo(() => (isArray(value) ? value : [value]), [value]); + + const providers: DataProvider[] = useMemo( + () => + queryIds.map((queryId) => ({ + and: [], + enabled: true, + excluded: false, + id: queryId, + kqlQuery: '', + name: queryId, + queryMatch: { + field, + value: queryId, + operator: ':' as const, + }, + })), + [field, queryIds] ); - if (!timelines || appName !== SECURITY_APP_NAME || !queryIds.length) { + const handleClick = useCallback(() => { + addToTimeline?.(providers); + }, [addToTimeline, providers]); + + if (appName !== SECURITY_APP_NAME || !queryIds.length || !addToTimeline) { return null; } - const { getAddToTimelineButton } = timelines.getHoverActions(); - - const providers = queryIds.map((queryId) => ({ - and: [], - enabled: true, - excluded: false, - id: queryId, - kqlQuery: '', - name: queryId, - queryMatch: { - field, - value: queryId, - operator: ':' as const, - }, - })); + if (isIcon) { + return ( + + ); + } - return getAddToTimelineButton({ - dataProvider: providers, - field: queryIds[0], - ownFocus: false, - ...(isIcon - ? { showTooltip: true, Component: TimelineIconComponent } - : { Component: TimelineComponent }), - startServices, - }); + return ( + + {addToTimelineLabel} + + ); }; diff --git a/x-pack/platform/plugins/shared/osquery/public/types.ts b/x-pack/platform/plugins/shared/osquery/public/types.ts index e2f88c2a8f3dc..b1633d0d90358 100644 --- a/x-pack/platform/plugins/shared/osquery/public/types.ts +++ b/x-pack/platform/plugins/shared/osquery/public/types.ts @@ -17,7 +17,6 @@ import type { TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; import type { CasesPublicStart, CasesPublicSetup } from '@kbn/cases-plugin/public'; -import type { TimelinesUIStart } from '@kbn/timelines-plugin/public'; import type { getLazyLiveQueryField, getLazyOsqueryAction, @@ -27,6 +26,25 @@ import type { import type { useAllLiveQueries, UseAllLiveQueriesConfig } from './actions/use_all_live_queries'; import type { getLazyOsqueryResults } from './shared_components/lazy_osquery_results'; +/** + * Minimal DataProvider type for timeline integration. + * This is a local definition to avoid direct dependency on @kbn/timelines-plugin. + * The structure is compatible with the timelines plugin's DataProvider type. + */ +export interface DataProvider { + id: string; + name: string; + enabled: boolean; + excluded: boolean; + kqlQuery: string; + queryMatch: { + field: string; + value: string | string[]; + operator: ':' | ':*' | 'includes'; + }; + and: DataProvider[]; +} + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface OsqueryPluginSetup {} @@ -53,7 +71,6 @@ export interface StartPlugins { security: SecurityPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; cases: CasesPublicStart; - timelines: TimelinesUIStart; appName?: string; } @@ -63,3 +80,5 @@ export interface SetupPlugins { } export type StartServices = CoreStart & StartPlugins; + +export type AddToTimelineHandler = (dataProviders: DataProvider[]) => void; diff --git a/x-pack/platform/plugins/shared/osquery/tsconfig.json b/x-pack/platform/plugins/shared/osquery/tsconfig.json index d6482a783b2a4..fd995a60d79da 100644 --- a/x-pack/platform/plugins/shared/osquery/tsconfig.json +++ b/x-pack/platform/plugins/shared/osquery/tsconfig.json @@ -43,7 +43,6 @@ "@kbn/lens-plugin", "@kbn/security-plugin", "@kbn/triggers-actions-ui-plugin", - "@kbn/timelines-plugin", "@kbn/spaces-plugin", "@kbn/i18n", "@kbn/rison", diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/response_actions/response_actions_results.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/response_actions/response_actions_results.tsx index 9202af136b59f..2f37e87e6c40d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/response_actions/response_actions_results.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/response_actions/response_actions_results.tsx @@ -15,6 +15,7 @@ import type { LogsEndpointActionWithHosts, } from '../../../../common/endpoint/types'; import { useKibana } from '../../lib/kibana'; +import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; interface ResponseActionsResultsProps { actions: Array; @@ -28,6 +29,7 @@ export const ResponseActionsResults = React.memo( services: { osquery }, } = useKibana(); const { OsqueryResult } = osquery; + const addToTimeline = useAddToTimeline(); const getAction = useCallback( (action: LogsEndpointActionWithHosts | LogsOsqueryAction) => { @@ -42,6 +44,7 @@ export const ResponseActionsResults = React.memo( startDate={startDate} ruleName={ruleName} ecsData={ecsData} + addToTimeline={addToTimeline} /> ); } @@ -56,7 +59,7 @@ export const ResponseActionsResults = React.memo( } return null; }, - [OsqueryResult, ecsData, ruleName] + [OsqueryResult, addToTimeline, ecsData, ruleName] ); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_to_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_to_timeline.ts new file mode 100644 index 0000000000000..ae914f593092b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_to_timeline.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../lib/kibana'; +import type { DataProvider } from '../../timelines/components/timeline/data_providers/data_provider'; +import { addProvider } from '../../timelines/store/actions'; +import { TimelineId } from '../../../common/types/timeline'; + +/** + * Hook that returns a callback to add data providers to the active timeline. + * Shows a success toast notification after adding. + */ +export const useAddToTimeline = () => { + const dispatch = useDispatch(); + const { notifications } = useKibana().services; + + return useCallback( + (dataProviders: DataProvider[]) => { + if (!dataProviders.length) { + return; + } + + dispatch( + addProvider({ + id: TimelineId.active, + providers: dataProviders, + }) + ); + + const title = + dataProviders.length === 1 + ? i18n.translate('xpack.securitySolution.timeline.addedToTimelineTitle', { + values: { fieldOrValue: dataProviders[0].name }, + defaultMessage: 'Added {fieldOrValue} to Timeline', + }) + : i18n.translate('xpack.securitySolution.timeline.addedMultipleToTimelineTitle', { + values: { count: dataProviders.length }, + defaultMessage: 'Added {count} items to Timeline', + }); + + notifications.toasts.addSuccess({ title }); + }, + [dispatch, notifications.toasts] + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx index 8e3c7c078fdcb..3b06b235aa54d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx @@ -18,6 +18,7 @@ import { import { useQueryClient } from '@kbn/react-query'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { useKibana } from '../../../common/lib/kibana'; +import { useAddToTimeline } from '../../../common/hooks/use_add_to_timeline'; import { OsqueryEventDetailsFooter } from './osquery_flyout_footer'; import { ACTION_OSQUERY } from './translations'; @@ -50,6 +51,7 @@ const OsqueryFlyoutComponent: React.FC = ({ services: { osquery }, } = useKibana(); const queryClient = useQueryClient(); + const addToTimeline = useAddToTimeline(); const invalidateQueries = useCallback(() => { queryClient.invalidateQueries({ @@ -77,6 +79,7 @@ const OsqueryFlyoutComponent: React.FC = ({ defaultValues={defaultValues} ecsData={ecsData} onSuccess={invalidateQueries} + addToTimeline={addToTimeline} /> From f9bf1247d58693641402d239ec2edddf9b13d43c Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Fri, 23 Jan 2026 08:46:40 +0100 Subject: [PATCH 2/4] Fix tests --- .../plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts | 1 - .../shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts | 1 - .../plugins/shared/osquery/cypress/e2e/all/cases.cy.ts | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts index c3934c2ba0605..45e0228edaa10 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_cases.cy.ts @@ -25,7 +25,6 @@ import { } from '../../tasks/live_query'; import { generateRandomStringName, interceptCaseId } from '../../tasks/integrations'; -// Failing: See https://github.com/elastic/kibana/issues/197151 describe( 'Alert Event Details - Cases', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts index 3aad10806d1bb..973ec2f2f4308 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts @@ -17,7 +17,6 @@ import { } from '../../tasks/live_query'; import { closeModalIfVisible, closeToastIfVisible } from '../../tasks/integrations'; -// FLAKY: https://github.com/elastic/kibana/issues/218206 describe( 'Alert Event Details', { diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/cases.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/cases.cy.ts index 416151caac342..c527311c29a06 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/cases.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/cases.cy.ts @@ -78,7 +78,7 @@ describe('Add to Cases', () => { cleanupCase(caseId); }); - it('should add result a case and have add to timeline in result', () => { + it('should add result a case and not have add to timeline in result', () => { addLiveQueryToCase(liveQueryId, caseId); cy.contains(`Case ${caseTitle} updated`); viewRecentCaseAndCheckResults(); @@ -88,7 +88,7 @@ describe('Add to Cases', () => { lens: true, discover: true, cases: false, - timeline: true, + timeline: false, }); }); }); From c97d372a60700959a75affdf0060f5af3cea6a23 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Thu, 29 Jan 2026 09:58:30 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=20Address=20PR=20review=20feedback=20=20?= =?UTF-8?q?=20-=20Fix=20test=20description=20grammar=20in=20cases.cy.ts=20?= =?UTF-8?q?=20=20-=20Add=20return=20type=20to=20useAddToTimeline=20hook=20?= =?UTF-8?q?=20=20-=20Rename=20DataProvider=20=E2=86=92=20OsqueryDataProvid?= =?UTF-8?q?er=20to=20avoid=20import=20confusion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shared/osquery/cypress/e2e/all/cases.cy.ts | 4 ++-- .../public/timelines/add_to_timeline_button.tsx | 4 ++-- .../platform/plugins/shared/osquery/public/types.ts | 11 +++++++---- .../public/common/hooks/use_add_to_timeline.ts | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/cases.cy.ts b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/cases.cy.ts index c527311c29a06..00047913118c6 100644 --- a/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/cases.cy.ts +++ b/x-pack/platform/plugins/shared/osquery/cypress/e2e/all/cases.cy.ts @@ -46,7 +46,7 @@ describe('Add to Cases', () => { cleanupCase(caseId); }); - it('should add result a case and not have add to timeline in result', () => { + it('should add result to a case without showing add to timeline button', () => { addLiveQueryToCase(liveQueryId, caseId); cy.contains(`Case ${caseTitle} updated`); viewRecentCaseAndCheckResults(); @@ -78,7 +78,7 @@ describe('Add to Cases', () => { cleanupCase(caseId); }); - it('should add result a case and not have add to timeline in result', () => { + it('should add result to a case without showing add to timeline button', () => { addLiveQueryToCase(liveQueryId, caseId); cy.contains(`Case ${caseTitle} updated`); viewRecentCaseAndCheckResults(); diff --git a/x-pack/platform/plugins/shared/osquery/public/timelines/add_to_timeline_button.tsx b/x-pack/platform/plugins/shared/osquery/public/timelines/add_to_timeline_button.tsx index d5dd0c5b5222d..dd281fa811f6f 100644 --- a/x-pack/platform/plugins/shared/osquery/public/timelines/add_to_timeline_button.tsx +++ b/x-pack/platform/plugins/shared/osquery/public/timelines/add_to_timeline_button.tsx @@ -10,7 +10,7 @@ import { isArray } from 'lodash'; import { EuiButtonEmpty, EuiButtonIcon, type EuiButtonIconProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import type { AddToTimelineHandler, DataProvider } from '../types'; +import type { AddToTimelineHandler, OsqueryDataProvider } from '../types'; export interface AddToTimelineButtonProps { field: string; @@ -31,7 +31,7 @@ export const AddToTimelineButton = (props: AddToTimelineButtonProps) => { const queryIds = useMemo(() => (isArray(value) ? value : [value]), [value]); - const providers: DataProvider[] = useMemo( + const providers: OsqueryDataProvider[] = useMemo( () => queryIds.map((queryId) => ({ and: [], diff --git a/x-pack/platform/plugins/shared/osquery/public/types.ts b/x-pack/platform/plugins/shared/osquery/public/types.ts index b1633d0d90358..b7f78db04061c 100644 --- a/x-pack/platform/plugins/shared/osquery/public/types.ts +++ b/x-pack/platform/plugins/shared/osquery/public/types.ts @@ -27,11 +27,14 @@ import type { useAllLiveQueries, UseAllLiveQueriesConfig } from './actions/use_a import type { getLazyOsqueryResults } from './shared_components/lazy_osquery_results'; /** - * Minimal DataProvider type for timeline integration. + * Minimal DataProvider type for timeline integration within the osquery plugin. * This is a local definition to avoid direct dependency on @kbn/timelines-plugin. * The structure is compatible with the timelines plugin's DataProvider type. + * + * @internal Use this only within the osquery plugin. For security_solution, + * import DataProvider from 'timelines/components/timeline/data_providers/data_provider'. */ -export interface DataProvider { +export interface OsqueryDataProvider { id: string; name: string; enabled: boolean; @@ -42,7 +45,7 @@ export interface DataProvider { value: string | string[]; operator: ':' | ':*' | 'includes'; }; - and: DataProvider[]; + and: OsqueryDataProvider[]; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -81,4 +84,4 @@ export interface SetupPlugins { export type StartServices = CoreStart & StartPlugins; -export type AddToTimelineHandler = (dataProviders: DataProvider[]) => void; +export type AddToTimelineHandler = (dataProviders: OsqueryDataProvider[]) => void; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_to_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_to_timeline.ts index ae914f593092b..ab4e2ffdf42ba 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_to_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_to_timeline.ts @@ -17,7 +17,7 @@ import { TimelineId } from '../../../common/types/timeline'; * Hook that returns a callback to add data providers to the active timeline. * Shows a success toast notification after adding. */ -export const useAddToTimeline = () => { +export const useAddToTimeline = (): ((dataProviders: DataProvider[]) => void) => { const dispatch = useDispatch(); const { notifications } = useKibana().services; From 89de89904da6d78a0b928fb49ea4cb41d8120a48 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Thu, 29 Jan 2026 13:16:51 +0100 Subject: [PATCH 4/4] Use Omit[] to match timelines plugin pattern and prevent unnecessary recursion --- x-pack/platform/plugins/shared/osquery/public/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/osquery/public/types.ts b/x-pack/platform/plugins/shared/osquery/public/types.ts index b7f78db04061c..5a70e7f27ec22 100644 --- a/x-pack/platform/plugins/shared/osquery/public/types.ts +++ b/x-pack/platform/plugins/shared/osquery/public/types.ts @@ -45,7 +45,7 @@ export interface OsqueryDataProvider { value: string | string[]; operator: ':' | ':*' | 'includes'; }; - and: OsqueryDataProvider[]; + and: Omit[]; } // eslint-disable-next-line @typescript-eslint/no-empty-interface