Skip to content

Commit ed9f4e9

Browse files
angorayckibanamachineflorent-leborgneviduni94
authored
[Security Assistant] EIS usage callout (#221566)
## Summary 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" /> ### Checklist 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]>
1 parent 28c230d commit ed9f4e9

File tree

54 files changed

+1464
-213
lines changed

Some content is hidden

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

54 files changed

+1464
-213
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
435435
avcResults: `https://www.elastic.co/blog/elastic-security-av-comparatives-business-test`,
436436
bidirectionalIntegrations: `${ELASTIC_DOCS}solutions/security/endpoint-response-actions/third-party-response-actions`,
437437
trustedApps: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/trusted-applications`,
438+
elasticAiFeatures: `${ELASTIC_DOCS}solutions/security/ai`,
438439
eventFilters: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/event-filters`,
439440
blocklist: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/blocklist`,
440441
threatIntelInt: `${ELASTIC_DOCS}solutions/security/get-started/enable-threat-intelligence-integrations`,
@@ -462,6 +463,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
462463
: `https://www.elastic.co/blog/security-prebuilt-rules-editing`,
463464
createEsqlRuleType: `${ELASTIC_DOCS}solutions/security/detect-and-alert/create-detection-rule#create-esql-rule`,
464465
ruleUiAdvancedParams: `${ELASTIC_DOCS}solutions/security/detect-and-alert/create-detection-rule#rule-ui-advanced-params`,
466+
thirdPartyLlmProviders: `${ELASTIC_DOCS}solutions/security/ai/set-up-connectors-for-large-language-models-llm`,
465467
entityAnalytics: {
466468
riskScorePrerequisites: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/entity-risk-scoring-requirements`,
467469
entityRiskScoring: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/entity-risk-scoring`,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,9 @@ export interface DocLinks {
295295
readonly artifactControl: string;
296296
readonly avcResults: string;
297297
readonly bidirectionalIntegrations: string;
298+
readonly thirdPartyLlmProviders: string;
298299
readonly trustedApps: string;
300+
readonly elasticAiFeatures: string;
299301
readonly eventFilters: string;
300302
readonly eventMerging: string;
301303
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+
});
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+
};
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+
});
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
@@ -27,6 +27,8 @@ import { AssistantSettingsModal } from '../settings/assistant_settings_modal';
2727
import { AIConnector } from '../../connectorland/connector_selector';
2828
import { SettingsContextMenu } from '../settings/settings_context_menu/settings_context_menu';
2929
import * as i18n from './translations';
30+
import { ElasticLLMCostAwarenessTour } from '../../tour/elastic_llm';
31+
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../tour/const';
3032

3133
interface OwnProps {
3234
selectedConversation: Conversation | undefined;
@@ -177,12 +179,18 @@ export const AssistantHeader: React.FC<Props> = ({
177179
<EuiFlexItem grow={false}>
178180
<EuiFlexGroup gutterSize="xs" alignItems={'center'}>
179181
<EuiFlexItem>
180-
<ConnectorSelectorInline
181-
isDisabled={isDisabled || selectedConversation === undefined}
182+
<ElasticLLMCostAwarenessTour
183+
isDisabled={isDisabled}
182184
selectedConnectorId={selectedConnectorId}
183-
selectedConversation={selectedConversation}
184-
onConnectorSelected={onConversationChange}
185-
/>
185+
storageKey={NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ASSISTANT_HEADER}
186+
>
187+
<ConnectorSelectorInline
188+
isDisabled={isDisabled || selectedConversation === undefined}
189+
selectedConnectorId={selectedConnectorId}
190+
selectedConversation={selectedConversation}
191+
onConnectorSelected={onConversationChange}
192+
/>
193+
</ElasticLLMCostAwarenessTour>
186194
</EuiFlexItem>
187195
<EuiFlexItem id={AI_ASSISTANT_SETTINGS_MENU_CONTAINER_ID}>
188196
<SettingsContextMenu

0 commit comments

Comments
 (0)