diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/insights/workflow_insights_results.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/insights/workflow_insights_results.tsx index 764b482a66826..035e7e3936a88 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/insights/workflow_insights_results.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/insights/workflow_insights_results.tsx @@ -42,6 +42,12 @@ const CustomEuiCallOut = styled(EuiCallOut)` } `; +const ScrollableContainer = styled(EuiPanel)` + max-height: 500px; + overflow-y: auto; + padding: 0; +`; + export const WorkflowInsightsResults = ({ results, scanCompleted, @@ -127,7 +133,7 @@ export const WorkflowInsightsResults = ({ {insight.message} - + {item.entries[0].type === 'match' && item.entries[0].field === 'process.executable.caseless' && item.entries[0].value} @@ -173,7 +179,7 @@ export const WorkflowInsightsResults = ({ ) : null} - {insights} + {insights} ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/insights/use_fetch_insights.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/insights/use_fetch_insights.ts index 4b70bbc3dade9..d4eee941200f4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/insights/use_fetch_insights.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/insights/use_fetch_insights.ts @@ -31,6 +31,7 @@ export const useFetchInsights = ({ endpointId, onSuccess }: UseFetchInsightsConf query: { actionTypes: JSON.stringify([ActionType.Refreshed]), targetIds: JSON.stringify([endpointId]), + size: 100, }, }); onSuccess(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/builders/incompatible_antivirus.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/builders/incompatible_antivirus.ts index b53c2d44555bd..434d9356e2f7b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/builders/incompatible_antivirus.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/builders/incompatible_antivirus.ts @@ -57,6 +57,7 @@ export async function buildIncompatibleAntivirusWorkflowInsights( const codeSignaturesHits = ( await esClient.search({ index: FILE_EVENTS_INDEX_PATTERN, + size: eventIds.length, query: { bool: { must: [ diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.test.ts index 50184413063cf..5a37507517ddd 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.test.ts @@ -30,9 +30,11 @@ import { import type { EndpointMetadataService } from '../metadata'; import { buildEsQueryParams, + checkIfRemediationExists, createDatastream, createPipeline, generateInsightId, + generateTrustedAppsFilter, groupEndpointIdsByOS, } from './helpers'; import { @@ -44,6 +46,7 @@ import { } from './constants'; import { securityWorkflowInsightsFieldMap } from './field_map_configurations'; import { createMockEndpointAppContext } from '../../mocks'; +import type { ExceptionListClient } from '@kbn/lists-plugin/server'; jest.mock('@kbn/data-stream-adapter', () => ({ DataStreamSpacesAdapter: jest.fn().mockImplementation(() => ({ @@ -262,4 +265,160 @@ describe('helpers', () => { expect(result).toBe(expectedHash); }); }); + + describe('generateTrustedAppsFilter', () => { + it('should generate a filter for process.executable.caseless entries', () => { + const insight = getDefaultInsight({ + remediation: { + exception_list_items: [ + { + entries: [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'match', + value: 'example-value', + }, + ], + }, + ], + }, + } as Partial); + + const filter = generateTrustedAppsFilter(insight); + + expect(filter).toBe('exception-list-agnostic.attributes.entries.value:"example-value"'); + }); + + it('should generate a filter for process.code_signature entries', () => { + const insight = getDefaultInsight({ + remediation: { + exception_list_items: [ + { + entries: [ + { + field: 'process.code_signature', + operator: 'included', + type: 'match', + value: 'Example, Inc.', + }, + ], + }, + ], + }, + } as Partial); + + const filter = generateTrustedAppsFilter(insight); + + expect(filter).toContain( + 'exception-list-agnostic.attributes.entries.entries.value:(*Example,*Inc.*)' + ); + }); + + it('should generate a filter for combined process.Ext.code_signature and process.executable.caseless', () => { + const insight = getDefaultInsight({ + remediation: { + exception_list_items: [ + { + entries: [ + { + field: 'process.Ext.code_signature', + operator: 'included', + type: 'match', + value: 'Example, (Inc.) http://example.com [example]', + }, + { + field: 'process.executable.caseless', + operator: 'included', + type: 'match', + value: 'example-value', + }, + ], + }, + ], + }, + } as Partial); + + const filter = generateTrustedAppsFilter(insight); + + expect(filter).toContain( + 'exception-list-agnostic.attributes.entries.entries.value:(*Example,*\\(Inc.\\)*http\\://example.com*[example]*) AND exception-list-agnostic.attributes.entries.value:"example-value"' + ); + }); + + it('should return empty string if no valid entries are present', () => { + const insight = getDefaultInsight({ + remediation: { + exception_list_items: [ + { + entries: [ + { + field: 'unknown-field', + operator: 'included', + type: 'match', + value: 'example-value', + }, + ], + }, + ], + }, + } as Partial); + + const filter = generateTrustedAppsFilter(insight); + + expect(filter).toBe(''); + }); + }); + describe('checkIfRemediationExists', () => { + it('should return false for non-incompatible_antivirus types', async () => { + const insight = getDefaultInsight({ + type: 'other-type' as DefendInsightType, + }); + + const result = await checkIfRemediationExists({ + insight, + exceptionListsClient: jest.fn() as unknown as ExceptionListClient, + }); + + expect(result).toBe(false); + }); + + it('should call exceptionListsClient with the correct filter', async () => { + const findExceptionListItemMock = jest.fn().mockResolvedValue({ total: 1 }); + const insight = getDefaultInsight({ + remediation: { + exception_list_items: [ + { + entries: [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'match', + value: 'example-value', + }, + ], + }, + ], + }, + } as Partial); + + const result = await checkIfRemediationExists({ + insight, + exceptionListsClient: { + findExceptionListItem: findExceptionListItemMock, + } as unknown as ExceptionListClient, + }); + + expect(findExceptionListItemMock).toHaveBeenCalledWith({ + listId: 'endpoint_trusted_apps', + page: 1, + perPage: 1, + namespaceType: 'agnostic', + filter: expect.any(String), + sortField: 'created_at', + sortOrder: 'desc', + }); + expect(result).toBe(true); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.ts index 0e508c5bcfed7..e356d0ed123a7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.ts @@ -13,6 +13,8 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import { DataStreamSpacesAdapter } from '@kbn/data-stream-adapter'; +import type { ExceptionListClient } from '@kbn/lists-plugin/server'; +import { DefendInsightType } from '@kbn/elastic-assistant-common'; import type { SearchParams, SecurityWorkflowInsight, @@ -160,3 +162,58 @@ export function getUniqueInsights(insights: SecurityWorkflowInsight[]): Security } return Object.values(uniqueInsights); } + +export const generateTrustedAppsFilter = (insight: SecurityWorkflowInsight): string | undefined => { + return insight.remediation.exception_list_items + ?.flatMap((item) => + item.entries.map((entry) => { + if (!('value' in entry)) return ''; + + if (entry.field === 'process.executable.caseless') { + return `exception-list-agnostic.attributes.entries.value:"${entry.value}"`; + } + + if ( + entry.field === 'process.code_signature' || + (entry.field === 'process.Ext.code_signature' && typeof entry.value === 'string') + ) { + const sanitizedValue = (entry.value as string) + .replace(/[)(<>}{":\\]/gm, '\\$&') + .replace(/\s/gm, '*'); + return `exception-list-agnostic.attributes.entries.entries.value:(*${sanitizedValue}*)`; + } + + return ''; + }) + ) + .filter(Boolean) + .join(' AND '); +}; + +export const checkIfRemediationExists = async ({ + insight, + exceptionListsClient, +}: { + insight: SecurityWorkflowInsight; + exceptionListsClient: ExceptionListClient; +}): Promise => { + if (insight.type !== DefendInsightType.Enum.incompatible_antivirus) { + return false; + } + + const filter = generateTrustedAppsFilter(insight); + + if (!filter) return false; + + const response = await exceptionListsClient.findExceptionListItem({ + listId: 'endpoint_trusted_apps', + page: 1, + perPage: 1, + namespaceType: 'agnostic', + filter, + sortField: 'created_at', + sortOrder: 'desc', + }); + + return !!response?.total && response.total > 0; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/index.test.ts index cca105152d3fe..a9b71a3017209 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/index.test.ts @@ -32,7 +32,12 @@ import { ActionType, } from '../../../../common/endpoint/types/workflow_insights'; import { createMockEndpointAppContext } from '../../mocks'; -import { createDatastream, createPipeline, generateInsightId } from './helpers'; +import { + checkIfRemediationExists, + createDatastream, + createPipeline, + generateInsightId, +} from './helpers'; import { securityWorkflowInsightsService } from '.'; import { DATA_STREAM_NAME } from './constants'; import { buildWorkflowInsights } from './builders'; @@ -43,6 +48,7 @@ jest.mock('./helpers', () => { ...original, createDatastream: jest.fn(), createPipeline: jest.fn(), + checkIfRemediationExists: jest.fn(), }; }); @@ -293,6 +299,22 @@ describe('SecurityWorkflowInsightsService', () => { }); }); + it('should not index the doc if remediation exists', async () => { + await securityWorkflowInsightsService.start({ esClient }); + const insight = getDefaultInsight(); + + const remediationExistsMock = checkIfRemediationExists as jest.Mock; + remediationExistsMock.mockResolvedValueOnce(true); + + await securityWorkflowInsightsService.create(insight); + + expect(remediationExistsMock).toHaveBeenCalledTimes(1); + + // two since it calls fetch as well + expect(isInitializedSpy).toHaveBeenCalledTimes(1); + expect(esClient.index).toHaveBeenCalledTimes(0); + }); + it('should call update instead if insight already exists', async () => { const indexName = 'backing-index'; const fetchSpy = jest diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/index.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/index.ts index 5714f25c92be2..c9acf9c42204f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/index.ts @@ -25,6 +25,7 @@ import type { EndpointAppContextService } from '../../endpoint_app_context_servi import { SecurityWorkflowInsightsFailedInitialized } from './errors'; import { buildEsQueryParams, + checkIfRemediationExists, createDatastream, createPipeline, generateInsightId, @@ -127,7 +128,7 @@ class SecurityWorkflowInsightsService { public async createFromDefendInsights( defendInsights: DefendInsight[], request: KibanaRequest - ): Promise { + ): Promise>> { await this.isInitialized; const workflowInsights = await buildWorkflowInsights({ @@ -137,14 +138,24 @@ class SecurityWorkflowInsightsService { esClient: this.esClient, }); const uniqueInsights = getUniqueInsights(workflowInsights); + return Promise.all(uniqueInsights.map((insight) => this.create(insight))); } - public async create(insight: SecurityWorkflowInsight): Promise { + public async create(insight: SecurityWorkflowInsight): Promise { await this.isInitialized; const id = generateInsightId(insight); + const remediationExists = await checkIfRemediationExists({ + insight, + exceptionListsClient: this.endpointContext.getExceptionListsClient(), + }); + + if (remediationExists) { + return; + } + // if insight already exists, update instead const existingInsights = await this.fetch({ ids: [id] }); if (existingInsights.length) {