Skip to content

Commit 0f1665d

Browse files
angorayckibanamachineflorent-leborgneviduni94
committed
[Security Assistant] EIS usage callout (#221566)
elastic/security-team#12656 https://github.com/elastic/kibana/pull/220782/files# To test: 1. https://p.elstc.co/paste/w06HF7Yw#2tr6JjZXmUbjQ6TQdpgdenH4YOjiWdAoHCZ3OpRi5JG 2. locally: ``` export VAULT_ADDR=https://secrets.elastic.co:8200/ vault login --method=oidc node scripts/eis.js ``` Callouts will not appear again once dismissed. Please clear the local storage if you want them to show up again. <img width="2557" alt="Screenshot 2025-05-29 at 15 53 21" src="https://github.com/user-attachments/assets/506925cb-5bce-4a66-918e-cd9e000c7088" /> onboarding hub: <img width="2559" alt="Screenshot 2025-05-29 at 09 32 14" src="https://github.com/user-attachments/assets/4c8b99e5-156e-4062-95a9-fa45c101b858" /> Assistant: <img width="1282" alt="Screenshot 2025-06-11 at 15 16 09" src="https://github.com/user-attachments/assets/30d47a05-ded1-4c3e-9540-6ad97fda0a8b" /> Conversation: <img width="674" alt="452997822-5c0b3933-b253-474e-92a5-d8793ebff819" src="https://github.com/user-attachments/assets/97506996-9a85-45bb-a728-79df37bd592e" /> Integration: <img width="2559" alt="Screenshot 2025-05-28 at 21 28 11" src="https://github.com/user-attachments/assets/ec564dac-2aed-4ac5-ad2c-67728d6f3eda" /> Attack Discovery: <img width="2560" alt="Screenshot 2025-06-11 at 15 35 08" src="https://github.com/user-attachments/assets/9816fc43-0e6e-40b2-862b-82673330c4da" /> ``` feature_flags.overrides: securitySolution.attackDiscoveryAlertsEnabled: true securitySolution.assistantAttackDiscoverySchedulingEnabled: true ``` <img width="2560" alt="Screenshot 2025-06-11 at 15 30 53" src="https://github.com/user-attachments/assets/7089626f-a416-4260-92f0-1be3f06cf5d3" /> Connectors: <img width="2559" alt="Screenshot 2025-06-10 at 11 15 41" src="https://github.com/user-attachments/assets/74773473-ff1c-41c1-bdd5-fe6e64b9a497" /> Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <[email protected]> Co-authored-by: florent-leborgne <[email protected]> Co-authored-by: Viduni Wickramarachchi <[email protected]> (cherry picked from commit ed9f4e9)
1 parent 2ddbb86 commit 0f1665d

File tree

53 files changed

+1462
-212
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1462
-212
lines changed

src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
510510
threatIntelInt: `${SECURITY_SOLUTION_DOCS}es-threat-intel-integrations.html`,
511511
endpointArtifacts: `${SECURITY_SOLUTION_DOCS}endpoint-artifacts.html`,
512512
eventMerging: `${SECURITY_SOLUTION_DOCS}endpoint-data-volume.html`,
513+
elasticAiFeatures: `${DOCS_WEBSITE_URL}solutions/security/ai`,
513514
policyResponseTroubleshooting: {
514515
full_disk_access: `${SECURITY_SOLUTION_DOCS}deploy-elastic-endpoint.html#enable-fda-endpoint`,
515516
macos_system_ext: `${SECURITY_SOLUTION_DOCS}deploy-elastic-endpoint.html#system-extension-endpoint`,

src/platform/packages/shared/kbn-doc-links/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,9 @@ export interface DocLinks {
356356
readonly artifactControl: string;
357357
readonly avcResults: string;
358358
readonly bidirectionalIntegrations: string;
359+
readonly thirdPartyLlmProviders: string;
359360
readonly trustedApps: string;
361+
readonly elasticAiFeatures: string;
360362
readonly eventFilters: string;
361363
readonly eventMerging: string;
362364
readonly blocklist: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import React from 'react';
8+
import { render } from '@testing-library/react';
9+
import useLocalStorage from 'react-use/lib/useLocalStorage';
10+
import { ElasticLlmCallout } from './elastic_llm_callout';
11+
import { TestProviders } from '../../mock/test_providers/test_providers';
12+
13+
jest.mock('react-use/lib/useLocalStorage');
14+
15+
describe('ElasticLlmCallout', () => {
16+
const defaultProps = {
17+
showEISCallout: true,
18+
};
19+
20+
beforeEach(() => {
21+
jest.clearAllMocks();
22+
(useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]);
23+
});
24+
25+
it('should not render when showEISCallout is false', () => {
26+
const { queryByTestId } = render(<ElasticLlmCallout showEISCallout={false} />, {
27+
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
28+
});
29+
expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument();
30+
});
31+
32+
it('should not render when tour is completed', () => {
33+
(useLocalStorage as jest.Mock).mockReturnValue([true, jest.fn()]);
34+
const { queryByTestId } = render(
35+
<TestProviders>
36+
<ElasticLlmCallout {...defaultProps} />
37+
</TestProviders>,
38+
{
39+
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
40+
}
41+
);
42+
43+
expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument();
44+
});
45+
46+
it('should render links', () => {
47+
const { queryByTestId } = render(
48+
<TestProviders>
49+
<ElasticLlmCallout {...defaultProps} />
50+
</TestProviders>,
51+
{ wrapper: ({ children }) => <TestProviders>{children}</TestProviders> }
52+
);
53+
expect(queryByTestId('elasticLlmUsageCostLink')).toHaveTextContent('additional costs incur');
54+
expect(queryByTestId('elasticLlmConnectorLink')).toHaveTextContent('connector');
55+
});
56+
57+
it('should show callout when showEISCallout changes to true', () => {
58+
const { rerender, queryByTestId } = render(
59+
<TestProviders>
60+
<ElasticLlmCallout showEISCallout={false} />
61+
</TestProviders>,
62+
{ wrapper: ({ children }) => <TestProviders>{children}</TestProviders> }
63+
);
64+
expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument();
65+
66+
rerender(<ElasticLlmCallout showEISCallout={true} />);
67+
expect(queryByTestId('elasticLlmCallout')).toBeInTheDocument();
68+
});
69+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import React, { useCallback, useEffect, useState } from 'react';
8+
import useLocalStorage from 'react-use/lib/useLocalStorage';
9+
10+
import { css } from '@emotion/react';
11+
import { i18n } from '@kbn/i18n';
12+
import { FormattedMessage } from '@kbn/i18n-react';
13+
import { EuiCallOut, EuiLink, useEuiTheme } from '@elastic/eui';
14+
import { useAssistantContext } from '../../assistant_context';
15+
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../tour/const';
16+
import { useTourStorageKey } from '../../tour/common/hooks/use_tour_storage_key';
17+
18+
export const ElasticLlmCallout = ({ showEISCallout }: { showEISCallout: boolean }) => {
19+
const {
20+
getUrlForApp,
21+
docLinks: {
22+
links: {
23+
observability: { elasticManagedLlmUsageCost: ELASTIC_LLM_USAGE_COST_LINK },
24+
},
25+
},
26+
} = useAssistantContext();
27+
const { euiTheme } = useEuiTheme();
28+
const tourStorageKey = useTourStorageKey(
29+
NEW_FEATURES_TOUR_STORAGE_KEYS.CONVERSATION_CONNECTOR_ELASTIC_LLM
30+
);
31+
const [tourCompleted, setTourCompleted] = useLocalStorage<boolean>(tourStorageKey, false);
32+
const [showCallOut, setShowCallOut] = useState<boolean>(showEISCallout);
33+
34+
const onDismiss = useCallback(() => {
35+
setShowCallOut(false);
36+
setTourCompleted(true);
37+
}, [setTourCompleted]);
38+
39+
useEffect(() => {
40+
if (showEISCallout && !tourCompleted) {
41+
setShowCallOut(true);
42+
} else {
43+
setShowCallOut(false);
44+
}
45+
}, [showEISCallout, tourCompleted]);
46+
47+
if (!showCallOut) {
48+
return;
49+
}
50+
51+
return (
52+
<EuiCallOut
53+
data-test-subj="elasticLlmCallout"
54+
onDismiss={onDismiss}
55+
iconType="iInCircle"
56+
title={i18n.translate('xpack.elasticAssistant.assistant.connectors.elasticLlmCallout.title', {
57+
defaultMessage: 'You are now using the Elastic Managed LLM connector',
58+
})}
59+
size="s"
60+
css={css`
61+
padding: ${euiTheme.size.s} !important;
62+
`}
63+
>
64+
<p>
65+
<FormattedMessage
66+
id="xpack.elasticAssistant.assistant.connectors.tour.elasticLlmDescription"
67+
defaultMessage="Elastic AI Assistant and other AI features are powered by an LLM. The Elastic Managed LLM connector is used by default ({costLink}) when no custom connectors are available. You can configure a {customConnector} if you prefer."
68+
values={{
69+
costLink: (
70+
<EuiLink
71+
data-test-subj="elasticLlmUsageCostLink"
72+
href={ELASTIC_LLM_USAGE_COST_LINK}
73+
target="_blank"
74+
rel="noopener noreferrer"
75+
external
76+
>
77+
<FormattedMessage
78+
id="xpack.elasticAssistant.assistant.eisCallout.extraCost.label"
79+
defaultMessage="additional costs incur"
80+
/>
81+
</EuiLink>
82+
),
83+
customConnector: (
84+
<EuiLink
85+
data-test-subj="elasticLlmConnectorLink"
86+
href={getUrlForApp('management', {
87+
path: `/insightsAndAlerting/triggersActionsConnectors/connectors`,
88+
})}
89+
target="_blank"
90+
rel="noopener noreferrer"
91+
external
92+
>
93+
<FormattedMessage
94+
id="xpack.elasticAssistant.assistant.eisCallout.connector.label"
95+
defaultMessage="custom connector"
96+
/>
97+
</EuiLink>
98+
),
99+
}}
100+
/>
101+
</p>
102+
</EuiCallOut>
103+
);
104+
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import React from 'react';
8+
import { render, screen } from '@testing-library/react';
9+
import { AssistantConversationBanner } from '.';
10+
import { AIConnector, Conversation, useAssistantContext } from '../../..';
11+
import { customConvo } from '../../mock/conversation';
12+
13+
jest.mock('../../..');
14+
15+
jest.mock('../../connectorland/connector_missing_callout', () => ({
16+
ConnectorMissingCallout: () => <div data-test-subj="connector-missing-callout" />,
17+
}));
18+
19+
jest.mock('./elastic_llm_callout', () => ({
20+
ElasticLlmCallout: () => <div data-test-subj="elastic-llm-callout" />,
21+
}));
22+
23+
describe('AssistantConversationBanner', () => {
24+
const setIsSettingsModalVisible = jest.fn();
25+
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
30+
it('renders ConnectorMissingCallout when shouldShowMissingConnectorCallout is true', () => {
31+
(useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: true });
32+
33+
render(
34+
<AssistantConversationBanner
35+
isSettingsModalVisible={false}
36+
setIsSettingsModalVisible={setIsSettingsModalVisible}
37+
shouldShowMissingConnectorCallout={true}
38+
currentConversation={undefined}
39+
connectors={[]}
40+
/>
41+
);
42+
43+
expect(screen.getByTestId('connector-missing-callout')).toBeInTheDocument();
44+
});
45+
46+
it('renders ElasticLlmCallout when Elastic LLM is enabled', () => {
47+
(useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: true });
48+
const mockConnectors = [
49+
{ id: 'mockLLM', actionTypeId: '.inference', isPreconfigured: true },
50+
] as AIConnector[];
51+
52+
const mockConversation = {
53+
...customConvo,
54+
id: 'mockConversation',
55+
apiConfig: {
56+
connectorId: 'mockLLM',
57+
actionTypeId: '.inference',
58+
},
59+
} as Conversation;
60+
61+
render(
62+
<AssistantConversationBanner
63+
isSettingsModalVisible={false}
64+
setIsSettingsModalVisible={setIsSettingsModalVisible}
65+
shouldShowMissingConnectorCallout={false}
66+
currentConversation={mockConversation}
67+
connectors={mockConnectors}
68+
/>
69+
);
70+
71+
expect(screen.getByTestId('elastic-llm-callout')).toBeInTheDocument();
72+
});
73+
74+
it('renders nothing when no conditions are met', () => {
75+
(useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: false });
76+
77+
const { container } = render(
78+
<AssistantConversationBanner
79+
isSettingsModalVisible={false}
80+
setIsSettingsModalVisible={setIsSettingsModalVisible}
81+
shouldShowMissingConnectorCallout={false}
82+
currentConversation={undefined}
83+
connectors={[]}
84+
/>
85+
);
86+
87+
expect(container).toBeEmptyDOMElement();
88+
});
89+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { useMemo } from 'react';
9+
import { AIConnector, Conversation, useAssistantContext } from '../../..';
10+
import { isElasticManagedLlmConnector } from '../../connectorland/helpers';
11+
import { ConnectorMissingCallout } from '../../connectorland/connector_missing_callout';
12+
import { ElasticLlmCallout } from './elastic_llm_callout';
13+
14+
export const AssistantConversationBanner = React.memo(
15+
({
16+
isSettingsModalVisible,
17+
setIsSettingsModalVisible,
18+
shouldShowMissingConnectorCallout,
19+
currentConversation,
20+
connectors,
21+
}: {
22+
isSettingsModalVisible: boolean;
23+
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
24+
shouldShowMissingConnectorCallout: boolean;
25+
currentConversation: Conversation | undefined;
26+
connectors: AIConnector[] | undefined;
27+
}) => {
28+
const { inferenceEnabled } = useAssistantContext();
29+
const showEISCallout = useMemo(() => {
30+
if (inferenceEnabled && currentConversation && currentConversation.id !== '') {
31+
if (currentConversation?.apiConfig?.connectorId) {
32+
return connectors?.some(
33+
(c) =>
34+
c.id === currentConversation.apiConfig?.connectorId && isElasticManagedLlmConnector(c)
35+
);
36+
}
37+
}
38+
}, [inferenceEnabled, currentConversation, connectors]);
39+
if (shouldShowMissingConnectorCallout) {
40+
return (
41+
<ConnectorMissingCallout
42+
isConnectorConfigured={(connectors?.length ?? 0) > 0}
43+
isSettingsModalVisible={isSettingsModalVisible}
44+
setIsSettingsModalVisible={setIsSettingsModalVisible}
45+
/>
46+
);
47+
}
48+
49+
if (showEISCallout) {
50+
return <ElasticLlmCallout showEISCallout={showEISCallout} />;
51+
}
52+
53+
return null;
54+
}
55+
);
56+
57+
AssistantConversationBanner.displayName = 'AssistantConversationBanner';

x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { AssistantSettingsModal } from '../settings/assistant_settings_modal';
2121
import { AIConnector } from '../../connectorland/connector_selector';
2222
import { SettingsContextMenu } from '../settings/settings_context_menu/settings_context_menu';
2323
import * as i18n from './translations';
24+
import { ElasticLLMCostAwarenessTour } from '../../tour/elastic_llm';
25+
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../tour/const';
2426

2527
interface OwnProps {
2628
selectedConversation: Conversation | undefined;
@@ -169,12 +171,18 @@ export const AssistantHeader: React.FC<Props> = ({
169171
<EuiFlexItem grow={false}>
170172
<EuiFlexGroup gutterSize="xs" alignItems={'center'}>
171173
<EuiFlexItem>
172-
<ConnectorSelectorInline
173-
isDisabled={isDisabled || selectedConversation === undefined}
174+
<ElasticLLMCostAwarenessTour
175+
isDisabled={isDisabled}
174176
selectedConnectorId={selectedConnectorId}
175-
selectedConversation={selectedConversation}
176-
onConnectorSelected={onConversationChange}
177-
/>
177+
storageKey={NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ASSISTANT_HEADER}
178+
>
179+
<ConnectorSelectorInline
180+
isDisabled={isDisabled || selectedConversation === undefined}
181+
selectedConnectorId={selectedConnectorId}
182+
selectedConversation={selectedConversation}
183+
onConnectorSelected={onConversationChange}
184+
/>
185+
</ElasticLLMCostAwarenessTour>
178186
</EuiFlexItem>
179187
<EuiFlexItem id={AI_ASSISTANT_SETTINGS_MENU_CONTAINER_ID}>
180188
<SettingsContextMenu

0 commit comments

Comments
 (0)