mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security solution] Attack Discovery "View in AI Assistant" button fix (#192416)
This commit is contained in:
parent
b02e1f3987
commit
ea6bb9e0b7
4 changed files with 154 additions and 7 deletions
|
@ -12,6 +12,7 @@ import { useAssistantOverlay } from '.';
|
|||
import { waitFor } from '@testing-library/react';
|
||||
import { useFetchCurrentUserConversations } from '../api';
|
||||
import { Conversation } from '../../assistant_context/types';
|
||||
import { mockConnectors } from '../../mock/connectors';
|
||||
|
||||
const mockUseAssistantContext = {
|
||||
registerPromptContext: jest.fn(),
|
||||
|
@ -27,19 +28,21 @@ jest.mock('../../assistant_context', () => {
|
|||
};
|
||||
});
|
||||
jest.mock('../api/conversations/use_fetch_current_user_conversations');
|
||||
const mockCreateConversation = jest.fn().mockResolvedValue({ id: 'conversation-id' });
|
||||
jest.mock('../use_conversation', () => {
|
||||
return {
|
||||
useConversation: jest.fn(() => ({
|
||||
createConversation: mockCreateConversation,
|
||||
currentConversation: { id: 'conversation-id' },
|
||||
})),
|
||||
};
|
||||
});
|
||||
jest.mock('../helpers');
|
||||
|
||||
jest.mock('../../connectorland/helpers');
|
||||
jest.mock('../../connectorland/use_load_connectors', () => {
|
||||
return {
|
||||
useLoadConnectors: jest.fn(() => ({
|
||||
data: [],
|
||||
data: mockConnectors,
|
||||
error: null,
|
||||
isSuccess: true,
|
||||
})),
|
||||
|
@ -158,10 +161,78 @@ describe('useAssistantOverlay', () => {
|
|||
result.current.showAssistantOverlay(true);
|
||||
});
|
||||
|
||||
expect(mockCreateConversation).not.toHaveBeenCalled();
|
||||
expect(mockUseAssistantContext.showAssistantOverlay).toHaveBeenCalledWith({
|
||||
showOverlay: true,
|
||||
promptContextId: 'id',
|
||||
conversationTitle: 'conversation-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `showAssistantOverlay` and creates a new conversation when shouldCreateConversation: true and the conversation does not exist', async () => {
|
||||
const isAssistantAvailable = true;
|
||||
const { result } = renderHook(() =>
|
||||
useAssistantOverlay(
|
||||
'event',
|
||||
'conversation-id',
|
||||
'description',
|
||||
() => Promise.resolve('data'),
|
||||
'id',
|
||||
null,
|
||||
'tooltip',
|
||||
isAssistantAvailable
|
||||
)
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.showAssistantOverlay(true, true);
|
||||
});
|
||||
|
||||
expect(mockCreateConversation).toHaveBeenCalledWith({
|
||||
title: 'conversation-id',
|
||||
apiConfig: {
|
||||
actionTypeId: '.gen-ai',
|
||||
connectorId: 'connectorId',
|
||||
},
|
||||
category: 'assistant',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseAssistantContext.showAssistantOverlay).toHaveBeenCalledWith({
|
||||
showOverlay: true,
|
||||
promptContextId: 'id',
|
||||
conversationTitle: 'conversation-id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `showAssistantOverlay` and does not create a new conversation when shouldCreateConversation: true and the conversation exists', async () => {
|
||||
const isAssistantAvailable = true;
|
||||
const { result } = renderHook(() =>
|
||||
useAssistantOverlay(
|
||||
'event',
|
||||
'electric sheep',
|
||||
'description',
|
||||
() => Promise.resolve('data'),
|
||||
'id',
|
||||
null,
|
||||
'tooltip',
|
||||
isAssistantAvailable
|
||||
)
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.showAssistantOverlay(true, true);
|
||||
});
|
||||
|
||||
expect(mockCreateConversation).not.toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseAssistantContext.showAssistantOverlay).toHaveBeenCalledWith({
|
||||
showOverlay: true,
|
||||
promptContextId: 'id',
|
||||
conversationTitle: 'electric sheep',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,12 @@ import { useCallback, useEffect, useMemo } from 'react';
|
|||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { getUniquePromptContextId } from '../../assistant_context/helpers';
|
||||
import type { PromptContext } from '../prompt_context/types';
|
||||
import { useConversation } from '../use_conversation';
|
||||
import { getDefaultConnector, mergeBaseWithPersistedConversations } from '../helpers';
|
||||
import { getGenAiConfig } from '../../connectorland/helpers';
|
||||
import { useLoadConnectors } from '../../connectorland/use_load_connectors';
|
||||
import { FetchConversationsResponse, useFetchCurrentUserConversations } from '../api';
|
||||
import { Conversation } from '../../assistant_context/types';
|
||||
|
||||
interface UseAssistantOverlay {
|
||||
showAssistantOverlay: (show: boolean, silent?: boolean) => void;
|
||||
|
@ -76,6 +82,26 @@ export const useAssistantOverlay = (
|
|||
*/
|
||||
replacements?: Replacements | null
|
||||
): UseAssistantOverlay => {
|
||||
const { http } = useAssistantContext();
|
||||
const { data: connectors } = useLoadConnectors({
|
||||
http,
|
||||
});
|
||||
|
||||
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
|
||||
const apiConfig = useMemo(() => getGenAiConfig(defaultConnector), [defaultConnector]);
|
||||
|
||||
const { createConversation } = useConversation();
|
||||
|
||||
const onFetchedConversations = useCallback(
|
||||
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
|
||||
mergeBaseWithPersistedConversations({}, conversationsData),
|
||||
[]
|
||||
);
|
||||
const { data: conversations, isLoading } = useFetchCurrentUserConversations({
|
||||
http,
|
||||
onFetch: onFetchedConversations,
|
||||
isAssistantEnabled,
|
||||
});
|
||||
// memoize the props so that we can use them in the effect below:
|
||||
const _category: PromptContext['category'] = useMemo(() => category, [category]);
|
||||
const _description: PromptContext['description'] = useMemo(() => description, [description]);
|
||||
|
@ -104,8 +130,34 @@ export const useAssistantOverlay = (
|
|||
// proxy show / hide calls to assistant context, using our internal prompt context id:
|
||||
// silent:boolean doesn't show the toast notification if the conversation is not found
|
||||
const showAssistantOverlay = useCallback(
|
||||
async (showOverlay: boolean) => {
|
||||
// shouldCreateConversation should only be passed for
|
||||
// non-default conversations that may need to be initialized
|
||||
async (showOverlay: boolean, shouldCreateConversation: boolean = false) => {
|
||||
if (promptContextId != null) {
|
||||
if (shouldCreateConversation) {
|
||||
let conversation;
|
||||
if (!isLoading) {
|
||||
conversation = conversationTitle
|
||||
? Object.values(conversations).find((conv) => conv.title === conversationTitle)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (isAssistantEnabled && !conversation && defaultConnector && !isLoading) {
|
||||
try {
|
||||
await createConversation({
|
||||
apiConfig: {
|
||||
...apiConfig,
|
||||
actionTypeId: defaultConnector?.actionTypeId,
|
||||
connectorId: defaultConnector?.id,
|
||||
},
|
||||
category: 'assistant',
|
||||
title: conversationTitle ?? '',
|
||||
});
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
}
|
||||
assistantContextShowOverlay({
|
||||
showOverlay,
|
||||
promptContextId,
|
||||
|
@ -113,7 +165,17 @@ export const useAssistantOverlay = (
|
|||
});
|
||||
}
|
||||
},
|
||||
[assistantContextShowOverlay, conversationTitle, promptContextId]
|
||||
[
|
||||
apiConfig,
|
||||
assistantContextShowOverlay,
|
||||
conversationTitle,
|
||||
conversations,
|
||||
createConversation,
|
||||
defaultConnector,
|
||||
isAssistantEnabled,
|
||||
isLoading,
|
||||
promptContextId,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -16,12 +16,12 @@ import { useViewInAiAssistant } from './use_view_in_ai_assistant';
|
|||
jest.mock('@kbn/elastic-assistant');
|
||||
jest.mock('../../../assistant/use_assistant_availability');
|
||||
jest.mock('../../get_attack_discovery_markdown/get_attack_discovery_markdown');
|
||||
|
||||
const mockUseAssistantOverlay = useAssistantOverlay as jest.Mock;
|
||||
describe('useViewInAiAssistant', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useAssistantOverlay as jest.Mock).mockReturnValue({
|
||||
mockUseAssistantOverlay.mockReturnValue({
|
||||
promptContextId: 'prompt-context-id',
|
||||
showAssistantOverlay: jest.fn(),
|
||||
});
|
||||
|
@ -83,4 +83,16 @@ describe('useViewInAiAssistant', () => {
|
|||
|
||||
expect(result.current.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('uses the title + last 5 of the attack discovery id as the conversation title', () => {
|
||||
renderHook(() =>
|
||||
useViewInAiAssistant({
|
||||
attackDiscovery: mockAttackDiscovery,
|
||||
})
|
||||
);
|
||||
|
||||
expect(mockUseAssistantOverlay.mock.calls[0][1]).toEqual(
|
||||
'Malware Attack With Credential Theft Attempt - b72b1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,9 +33,11 @@ export const useViewInAiAssistant = ({
|
|||
}),
|
||||
[attackDiscovery]
|
||||
);
|
||||
|
||||
const lastFive = attackDiscovery.id ? ` - ${attackDiscovery.id.slice(-5)}` : '';
|
||||
const { promptContextId, showAssistantOverlay: showOverlay } = useAssistantOverlay(
|
||||
category,
|
||||
attackDiscovery.title, // conversation title
|
||||
attackDiscovery.title + lastFive, // conversation title
|
||||
attackDiscovery.title, // description used in context pill
|
||||
getPromptContext,
|
||||
attackDiscovery.id ?? null, // accept the UUID default for this prompt context
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue