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) {