mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[EDR Workflows] Workflow Insights - Cypress (#204562)
This PR adds Cypress test coverage for the Defend Insights component and enables RBAC and tier validation tests. It should be merged after the feature flag is enabled - https://github.com/elastic/kibana/pull/204242 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f19636ea2a
commit
ac67d91021
12 changed files with 405 additions and 13 deletions
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 {
|
||||
createBedrockAIConnector,
|
||||
deleteConnectors,
|
||||
expectDefendInsightsApiToBeCalled,
|
||||
expectPostDefendInsightsApiToBeCalled,
|
||||
expectWorkflowInsightsApiToBeCalled,
|
||||
interceptGetDefendInsightsApiCall,
|
||||
interceptGetWorkflowInsightsApiCall,
|
||||
interceptPostDefendInsightsApiCall,
|
||||
setConnectorIdInLocalStorage,
|
||||
stubDefendInsightsApiResponse,
|
||||
stubPutWorkflowInsightsApiResponse,
|
||||
stubWorkflowInsightsApiResponse,
|
||||
validateUserGotRedirectedToEndpointDetails,
|
||||
validateUserGotRedirectedToTrustedApps,
|
||||
} from '../../tasks/insights';
|
||||
import { loadEndpointDetailsFlyout, workflowInsightsSelectors } from '../../screens/insights';
|
||||
import { indexEndpointHosts, type CyIndexEndpointHosts } from '../../tasks/index_endpoint_hosts';
|
||||
import { login } from '../../tasks/login';
|
||||
|
||||
const {
|
||||
addConnectorButtonExists,
|
||||
insightsComponentExists,
|
||||
chooseConnectorButtonExistsWithLabel,
|
||||
selectConnector,
|
||||
clickScanButton,
|
||||
insightsResultExists,
|
||||
insightsEmptyResultsCalloutDoesNotExist,
|
||||
clickInsightsResultRemediationButton,
|
||||
scanButtonShouldBe,
|
||||
clickTrustedAppFormSubmissionButton,
|
||||
} = workflowInsightsSelectors;
|
||||
|
||||
describe(
|
||||
'Workflow Insights',
|
||||
{
|
||||
tags: [
|
||||
'@ess',
|
||||
'@serverless',
|
||||
// skipped on MKI since feature flags are not supported there
|
||||
'@skipInServerlessMKI',
|
||||
],
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['defendInsights'])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
const connectorName = 'TEST-CONNECTOR';
|
||||
let loadedEndpoint: CyIndexEndpointHosts;
|
||||
let endpointId: string;
|
||||
|
||||
// Since the endpoint is used only for displaying details flyout, we can use the same endpoint for all tests
|
||||
before(() => {
|
||||
indexEndpointHosts({ count: 1 }).then((indexedEndpoint) => {
|
||||
loadedEndpoint = indexedEndpoint;
|
||||
endpointId = indexedEndpoint.data.hosts[0].agent.id;
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (loadedEndpoint) {
|
||||
loadedEndpoint.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
it('should render Insights section on endpoint flyout with option to define connectors', () => {
|
||||
loadEndpointDetailsFlyout(endpointId);
|
||||
insightsComponentExists();
|
||||
addConnectorButtonExists();
|
||||
});
|
||||
|
||||
describe('Workflow Insights first visit', () => {
|
||||
let connectorId: string | undefined;
|
||||
beforeEach(() => {
|
||||
createBedrockAIConnector(connectorName).then((response) => {
|
||||
connectorId = response.body.id;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
deleteConnectors();
|
||||
connectorId = undefined;
|
||||
});
|
||||
|
||||
it('should properly initialize workflow insights for the first time', () => {
|
||||
interceptGetWorkflowInsightsApiCall();
|
||||
interceptGetDefendInsightsApiCall();
|
||||
|
||||
loadEndpointDetailsFlyout(endpointId);
|
||||
|
||||
expectWorkflowInsightsApiToBeCalled();
|
||||
expectDefendInsightsApiToBeCalled();
|
||||
|
||||
chooseConnectorButtonExistsWithLabel('Select a connector');
|
||||
selectConnector(connectorId);
|
||||
chooseConnectorButtonExistsWithLabel(connectorName);
|
||||
|
||||
scanButtonShouldBe('enabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workflow Insights consequent visit', () => {
|
||||
beforeEach(() => {
|
||||
createBedrockAIConnector(connectorName).then(setConnectorIdInLocalStorage);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
deleteConnectors();
|
||||
});
|
||||
|
||||
it('should properly initialize workflow insights with a connector already defined', () => {
|
||||
loadEndpointDetailsFlyout(endpointId);
|
||||
chooseConnectorButtonExistsWithLabel(connectorName);
|
||||
scanButtonShouldBe('enabled');
|
||||
});
|
||||
|
||||
it('should disable Scan button if there is an ongoing scan', () => {
|
||||
stubDefendInsightsApiResponse();
|
||||
loadEndpointDetailsFlyout(endpointId);
|
||||
scanButtonShouldBe('disabled');
|
||||
});
|
||||
|
||||
it('should trigger insight generation on Scan button click', () => {
|
||||
interceptPostDefendInsightsApiCall();
|
||||
interceptGetWorkflowInsightsApiCall();
|
||||
interceptGetDefendInsightsApiCall();
|
||||
|
||||
loadEndpointDetailsFlyout(endpointId);
|
||||
clickScanButton();
|
||||
|
||||
expectPostDefendInsightsApiToBeCalled();
|
||||
expectWorkflowInsightsApiToBeCalled();
|
||||
expectDefendInsightsApiToBeCalled();
|
||||
});
|
||||
|
||||
it('should render existing Insights', () => {
|
||||
stubWorkflowInsightsApiResponse(endpointId);
|
||||
|
||||
loadEndpointDetailsFlyout(endpointId);
|
||||
|
||||
insightsResultExists();
|
||||
insightsEmptyResultsCalloutDoesNotExist();
|
||||
clickInsightsResultRemediationButton();
|
||||
|
||||
validateUserGotRedirectedToTrustedApps();
|
||||
stubPutWorkflowInsightsApiResponse();
|
||||
clickTrustedAppFormSubmissionButton();
|
||||
validateUserGotRedirectedToEndpointDetails(endpointId);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -15,8 +15,7 @@ import {
|
|||
import { closeAllToasts } from '../../tasks/toasts';
|
||||
import { login, ROLE } from '../../tasks/login';
|
||||
|
||||
// Unskip when defendInsights assistant feature is enabled by default
|
||||
describe.skip(
|
||||
describe(
|
||||
'When defining a kibana role for Endpoint security access',
|
||||
{
|
||||
env: {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { fetchRunningDefendInsights, fetchWorkflowInsights } from '../../../../t
|
|||
import { login, ROLE } from '../../../../tasks/login';
|
||||
|
||||
// Unskip when defendInsights assistant feature is enabled by default
|
||||
describe.skip(
|
||||
describe(
|
||||
'Workflow Insights APIs',
|
||||
{
|
||||
tags: ['@serverless', '@skipInServerlessMKI'], // remove @skipInServerlessMKI once changes are merged
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { login, ROLE } from '../../../../tasks/login';
|
||||
|
||||
// Unskip when defendInsights assistant feature is enabled by default
|
||||
describe.skip(
|
||||
describe(
|
||||
'Workflow Insights APIs',
|
||||
{
|
||||
tags: ['@serverless', '@skipInServerlessMKI'], // remove @skipInServerlessMKI once changes are merged
|
||||
|
|
|
@ -12,8 +12,7 @@ import { login, ROLE } from '../../../../tasks/login';
|
|||
|
||||
const { insightsComponentExists, addConnectorButtonExists } = workflowInsightsSelectors;
|
||||
|
||||
// Unskip when defendInsights assistant feature is enabled by default
|
||||
describe.skip(
|
||||
describe(
|
||||
'Endpoint details',
|
||||
{
|
||||
tags: [
|
||||
|
|
|
@ -12,8 +12,7 @@ import { login, ROLE } from '../../../../tasks/login';
|
|||
|
||||
const { insightsComponentDoesntExist } = workflowInsightsSelectors;
|
||||
|
||||
// Unskip when defendInsights assistant feature is enabled by default
|
||||
describe.skip(
|
||||
describe(
|
||||
'Endpoint details',
|
||||
{
|
||||
tags: [
|
||||
|
|
|
@ -14,7 +14,25 @@ export const loadEndpointDetailsFlyout = (endpointId: string) =>
|
|||
|
||||
export const workflowInsightsSelectors = {
|
||||
insightsComponentExists: () => cy.getByTestSubj('endpointDetailsInsightsWrapper').should('exist'),
|
||||
addConnectorButtonExists: () => cy.getByTestSubj('addNewConnectorButton').should('exist'),
|
||||
chooseConnectorButtonExistsWithLabel: (label: string) =>
|
||||
cy.getByTestSubj('connector-selector').contains(label),
|
||||
selectConnector: (connectorId?: string) => {
|
||||
cy.getByTestSubj('connector-selector').click();
|
||||
if (connectorId) return cy.getByTestSubj(connectorId).click();
|
||||
},
|
||||
selectScanButton: () => cy.getByTestSubj('workflowInsightsScanButton'),
|
||||
scanButtonShouldBe: (state: 'enabled' | 'disabled') =>
|
||||
workflowInsightsSelectors.selectScanButton().should(`be.${state}`),
|
||||
clickScanButton: () => workflowInsightsSelectors.selectScanButton().click(),
|
||||
insightsResultExists: (index = 0) =>
|
||||
cy.getByTestSubj(`workflowInsightsResult-${index}`).should('exist'),
|
||||
clickInsightsResultRemediationButton: (index = 0) =>
|
||||
cy.getByTestSubj(`workflowInsightsResult-${index}-remediation`).click(),
|
||||
insightsEmptyResultsCalloutDoesNotExist: () =>
|
||||
cy.getByTestSubj('workflowInsightsEmptyResultsCallout').should('not.exist'),
|
||||
clickTrustedAppFormSubmissionButton: () =>
|
||||
cy.getByTestSubj('trustedAppsListPage-flyout-submitButton').click(),
|
||||
insightsComponentDoesntExist: () =>
|
||||
cy.getByTestSubj('endpointDetailsInsightsWrapper').should('not.exist'),
|
||||
addConnectorButtonExists: () => cy.getByTestSubj('addNewConnectorButton').should('exist'),
|
||||
};
|
||||
|
|
|
@ -4,15 +4,211 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
AllConnectorsResponse,
|
||||
ConnectorResponse,
|
||||
} from '@kbn/actions-plugin/common/routes/connector/response';
|
||||
import { DEFEND_INSIGHTS } from '@kbn/elastic-assistant-common';
|
||||
import { ActionType } from '../../../../common/endpoint/types/workflow_insights';
|
||||
import { request } from './common';
|
||||
import { ActionType } from '../../../../common/endpoint/types/workflow_insights';
|
||||
import {
|
||||
WORKFLOW_INSIGHTS_ROUTE,
|
||||
WORKFLOW_INSIGHTS_UPDATE_ROUTE,
|
||||
} from '../../../../common/endpoint/constants';
|
||||
|
||||
const INTERNAL_CLOUD_CONNECTORS = ['Elastic-Cloud-SMTP'];
|
||||
|
||||
export const createBedrockAIConnector = (connectorName?: string) =>
|
||||
request<ConnectorResponse>({
|
||||
method: 'POST',
|
||||
url: '/api/actions/connector',
|
||||
body: {
|
||||
connector_type_id: '.bedrock',
|
||||
secrets: {
|
||||
accessKey: '123',
|
||||
secret: '123',
|
||||
},
|
||||
config: {
|
||||
apiUrl: 'https://bedrock.com',
|
||||
},
|
||||
name: connectorName || 'Bedrock cypress test e2e connector',
|
||||
},
|
||||
});
|
||||
|
||||
export const getConnectors = () =>
|
||||
request<AllConnectorsResponse[]>({
|
||||
method: 'GET',
|
||||
url: 'api/actions/connectors',
|
||||
});
|
||||
|
||||
export const deleteConnectors = () => {
|
||||
getConnectors().then(($response) => {
|
||||
if ($response.body.length > 0) {
|
||||
const ids = $response.body.map((connector) => {
|
||||
return connector.id;
|
||||
});
|
||||
ids.forEach((id) => {
|
||||
if (!INTERNAL_CLOUD_CONNECTORS.includes(id)) {
|
||||
request({
|
||||
method: 'DELETE',
|
||||
url: `api/actions/connector/${id}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const setConnectorIdInLocalStorage = (res: { body: { id: string } }) => {
|
||||
window.localStorage.setItem(
|
||||
`elasticAssistantDefault.defendInsights.default.connectorId`,
|
||||
`"${res.body.id}"`
|
||||
);
|
||||
};
|
||||
|
||||
export const validateUserGotRedirectedToTrustedApps = () => {
|
||||
cy.url().should('include', '/app/security/administration/trusted_apps');
|
||||
};
|
||||
|
||||
export const validateUserGotRedirectedToEndpointDetails = (endpointId: string) => {
|
||||
cy.url().should(
|
||||
'include',
|
||||
`/app/security/administration/endpoints?selected_endpoint=${endpointId}&show=details`
|
||||
);
|
||||
};
|
||||
|
||||
export const interceptGetWorkflowInsightsApiCall = () => {
|
||||
cy.intercept({ method: 'GET', url: '**/internal/api/endpoint/workflow_insights**' }).as(
|
||||
'getWorkflowInsights'
|
||||
);
|
||||
};
|
||||
|
||||
export const expectWorkflowInsightsApiToBeCalled = () => {
|
||||
cy.wait('@getWorkflowInsights', { timeout: 30 * 1000 });
|
||||
};
|
||||
|
||||
export const interceptGetDefendInsightsApiCall = () => {
|
||||
cy.intercept({ method: 'GET', url: '**/internal/elastic_assistant/defend_insights**' }).as(
|
||||
'getDefendInsights'
|
||||
);
|
||||
};
|
||||
|
||||
export const expectDefendInsightsApiToBeCalled = () => {
|
||||
cy.wait('@getDefendInsights', { timeout: 30 * 1000 });
|
||||
};
|
||||
|
||||
export const interceptPostDefendInsightsApiCall = () => {
|
||||
cy.intercept({ method: 'POST', url: '**/internal/elastic_assistant/defend_insights**' }).as(
|
||||
'createInsights'
|
||||
);
|
||||
};
|
||||
|
||||
export const expectPostDefendInsightsApiToBeCalled = () => {
|
||||
cy.wait('@createInsights', { timeout: 30 * 1000 });
|
||||
};
|
||||
|
||||
export const stubPutWorkflowInsightsApiResponse = () => {
|
||||
cy.intercept('PUT', '**/internal/api/endpoint/workflow_insights/**', (req) => {
|
||||
req.continue((res) => {
|
||||
return res.send(200, {
|
||||
status: 'ok',
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const stubDefendInsightsApiResponse = () => {
|
||||
cy.intercept('GET', '**/internal/elastic_assistant/defend_insights?status=running**', (req) => {
|
||||
req.continue((res) => {
|
||||
return res.send(200, {
|
||||
data: [
|
||||
{
|
||||
timestamp: '2024-12-16T13:44:52.633Z',
|
||||
id: 'd95561cb-1f75-4a6c-8be4-cb7529ddd5e0',
|
||||
backingIndex:
|
||||
'.ds-.kibana-elastic-ai-assistant-defend-insights-default-2024.12.16-000001',
|
||||
createdAt: '2024-12-16T13:44:52.633Z',
|
||||
updatedAt: '2024-12-16T13:44:52.633Z',
|
||||
lastViewedAt: '2024-12-16T13:44:53.866Z',
|
||||
users: [
|
||||
{
|
||||
id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
|
||||
name: 'elastic',
|
||||
},
|
||||
],
|
||||
namespace: 'default',
|
||||
status: 'running',
|
||||
apiConfig: {
|
||||
connectorId: 'db760d65-6722-4646-955f-fbdc9851df86',
|
||||
actionTypeId: '.bedrock',
|
||||
},
|
||||
endpointIds: ['33581c4f-bef1-4162-9809-4c208e2e1991'],
|
||||
insightType: 'incompatible_antivirus',
|
||||
insights: [],
|
||||
generationIntervals: [],
|
||||
averageIntervalMs: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const stubWorkflowInsightsApiResponse = (endpointId: string) => {
|
||||
cy.intercept('GET', '**/internal/api/endpoint/workflow_insights**', (req) => {
|
||||
req.continue((res) => {
|
||||
return res.send(200, [
|
||||
{
|
||||
remediation: {
|
||||
exception_list_items: [
|
||||
{
|
||||
entries: [
|
||||
{
|
||||
field: 'process.executable.caseless',
|
||||
type: 'match',
|
||||
value: '/usr/bin/clamscan',
|
||||
operator: 'included',
|
||||
},
|
||||
],
|
||||
list_id: 'endpoint_trusted_apps',
|
||||
name: 'ClamAV',
|
||||
os_types: ['linux'],
|
||||
description: 'Suggested by Security Workflow Insights',
|
||||
tags: ['policy:all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
metadata: {
|
||||
notes: {
|
||||
llm_model: '',
|
||||
},
|
||||
},
|
||||
'@timestamp': '2024-12-16T13:45:03.055Z',
|
||||
action: {
|
||||
type: 'refreshed',
|
||||
timestamp: '2024-12-16T13:45:03.055Z',
|
||||
},
|
||||
source: {
|
||||
data_range_end: '2024-12-17T13:45:03.055Z',
|
||||
id: 'db760d65-6722-4646-955f-fbdc9851df86',
|
||||
type: 'llm-connector',
|
||||
data_range_start: '2024-12-16T13:45:03.055Z',
|
||||
},
|
||||
message: 'Incompatible antiviruses detected',
|
||||
category: 'endpoint',
|
||||
type: 'incompatible_antivirus',
|
||||
value: 'ClamAV',
|
||||
target: {
|
||||
ids: [endpointId],
|
||||
type: 'endpoint',
|
||||
},
|
||||
id: 'CMm3z5MBPx3JiizjFx5g',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const triggerRunningDefendInsights = () => {
|
||||
return request({
|
||||
method: 'POST',
|
||||
|
|
|
@ -36,5 +36,6 @@
|
|||
"@kbn/spaces-plugin",
|
||||
"@kbn/test-suites-xpack/security_solution_cypress/cypress",
|
||||
"@kbn/elastic-assistant-common",
|
||||
"@kbn/actions-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -109,7 +109,11 @@ export const WorkflowInsightsResults = ({
|
|||
const insights = useMemo(() => {
|
||||
if (showEmptyResultsCallout) {
|
||||
return (
|
||||
<CustomEuiCallOut onDismiss={hideEmptyStateCallout} color={'success'}>
|
||||
<CustomEuiCallOut
|
||||
onDismiss={hideEmptyStateCallout}
|
||||
color={'success'}
|
||||
data-test-subj={'workflowInsightsEmptyResultsCallout'}
|
||||
>
|
||||
{WORKFLOW_INSIGHTS.issues.emptyResults}
|
||||
</CustomEuiCallOut>
|
||||
);
|
||||
|
@ -120,7 +124,13 @@ export const WorkflowInsightsResults = ({
|
|||
WORKFLOW_INSIGHTS.issues.remediationButton;
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="m" hasShadow={false} hasBorder key={index}>
|
||||
<EuiPanel
|
||||
paddingSize="m"
|
||||
hasShadow={false}
|
||||
hasBorder
|
||||
key={index}
|
||||
data-test-subj={`workflowInsightsResult-${index}`}
|
||||
>
|
||||
<EuiFlexGroup alignItems={'center'} gutterSize={'m'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="warning" size="l" color="warning" />
|
||||
|
@ -148,6 +158,7 @@ export const WorkflowInsightsResults = ({
|
|||
position={'top'}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`workflowInsightsResult-${index}-remediation`}
|
||||
isDisabled={!canWriteTrustedApplications}
|
||||
aria-label={ariaLabel}
|
||||
iconType="popout"
|
||||
|
|
|
@ -84,6 +84,7 @@ export const WorkflowInsightsScanSection = ({
|
|||
const button = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="workflowInsightsScanButton"
|
||||
size="s"
|
||||
isLoading={isScanButtonDisabled}
|
||||
isDisabled={!canWriteWorkflowInsights}
|
||||
|
|
|
@ -33,6 +33,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
// API Keys is enabled at the top level
|
||||
'xpack.security.enabled=true',
|
||||
'http.host=0.0.0.0',
|
||||
'xpack.ml.enabled=false',
|
||||
],
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue