Skip to content

Commit

Permalink
[EDR Workflows] Workflow Insights - migrate to Signature field (#205323)
Browse files Browse the repository at this point in the history
This PR adds checks to verify whether the signer_id is present in file
events stored in the ES, which serve as the foundation for generating
endpoint insights. Previously, we relied solely on the executable path,
which caused issues when a single AV generated multiple paths.

With these changes:
* If the `signer_id` exists in the file event, it will be used for
generating insights alongside the path
* For cases where the `signer_id` is unavailable (e.g., Linux, which
lacks signers), the executable path will still be used as an only value.




https://github.com/user-attachments/assets/8965efef-e962-485a-b20f-d2730cffcf10

---------

Co-authored-by: Joey F. Poon <[email protected]>
  • Loading branch information
szwarckonrad and joeypoon authored Jan 17, 2025
1 parent 739e8cc commit 7bafc0b
Show file tree
Hide file tree
Showing 8 changed files with 361 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface SecurityWorkflowInsight {
metadata: {
notes?: Record<string, string>;
message_variables?: string[];
display_name?: string;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,15 @@ export const WorkflowInsightsResults = ({
<EuiFlexItem>
<EuiText size="s">
<EuiText size={'s'}>
<strong>{insight.value}</strong>
<strong>{insight.metadata.display_name || insight.value}</strong>
</EuiText>
<EuiText size={'s'} color={'subdued'}>
{insight.message}
</EuiText>
<EuiText size={'xs'} color={'subdued'}>
{item.entries[0].type === 'match' && item.entries[0].value}
{item.entries[0].type === 'match' &&
item.entries[0].field === 'process.executable.caseless' &&
item.entries[0].value}
</EuiText>
</EuiText>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import moment from 'moment';

import type { KibanaRequest } from '@kbn/core/server';
import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server';
import type { DefendInsightsPostRequestBody } from '@kbn/elastic-assistant-common';

import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants';
Expand All @@ -30,101 +30,189 @@ jest.mock('../helpers', () => ({
}));

describe('buildIncompatibleAntivirusWorkflowInsights', () => {
let params: BuildWorkflowInsightParams;
const mockEndpointAppContextService = createMockEndpointAppContext().service;
mockEndpointAppContextService.getEndpointMetadataService = jest.fn().mockReturnValue({
getMetadataForEndpoints: jest.fn(),
});
const endpointMetadataService =
mockEndpointAppContextService.getEndpointMetadataService() as jest.Mocked<EndpointMetadataService>;

beforeEach(() => {
const mockEndpointAppContextService = createMockEndpointAppContext().service;
mockEndpointAppContextService.getEndpointMetadataService = jest.fn().mockReturnValue({
getMetadataForEndpoints: jest.fn(),
});
const endpointMetadataService =
mockEndpointAppContextService.getEndpointMetadataService() as jest.Mocked<EndpointMetadataService>;

params = {
defendInsights: [
{
group: 'AVGAntivirus',
events: [
{
id: 'lqw5opMB9Ke6SNgnxRSZ',
endpointId: 'f6e2f338-6fb7-4c85-9c23-d20e9f96a051',
value: '/Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity',
},
],
const generateParams = (signerId?: string): BuildWorkflowInsightParams => ({
defendInsights: [
{
group: 'AVGAntivirus',
events: [
{
id: 'lqw5opMB9Ke6SNgnxRSZ',
endpointId: 'f6e2f338-6fb7-4c85-9c23-d20e9f96a051',
value: '/Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity',
...(signerId ? { signerId } : {}),
},
],
},
],
request: {
body: {
insightType: 'incompatible_antivirus',
endpointIds: ['endpoint-1'],
apiConfig: {
connectorId: 'connector-id-1',
actionTypeId: 'action-type-id-1',
model: 'model-1',
},
anonymizationFields: [],
subAction: 'invokeAI',
},
} as unknown as KibanaRequest<unknown, unknown, DefendInsightsPostRequestBody>,
endpointMetadataService,
esClient: {
search: jest.fn().mockResolvedValue({
hits: {
hits: [],
},
],
request: {
body: {
insightType: 'incompatible_antivirus',
endpointIds: ['endpoint-1'],
apiConfig: {
connectorId: 'connector-id-1',
actionTypeId: 'action-type-id-1',
model: 'model-1',
}),
} as unknown as ElasticsearchClient,
});

const buildExpectedInsight = (os: string, signerField?: string, signerValue?: string) =>
expect.objectContaining({
'@timestamp': expect.any(moment),
message: 'Incompatible antiviruses detected',
category: Category.Endpoint,
type: 'incompatible_antivirus',
source: {
type: SourceType.LlmConnector,
id: 'connector-id-1',
data_range_start: expect.any(moment),
data_range_end: expect.any(moment),
},
target: {
type: TargetType.Endpoint,
ids: ['endpoint-1'],
},
action: {
type: ActionType.Refreshed,
timestamp: expect.any(moment),
},
value: `AVGAntivirus /Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity${
signerValue ? ` ${signerValue}` : ''
}`,
remediation: {
exception_list_items: [
{
list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id,
name: 'AVGAntivirus',
description: 'Suggested by Security Workflow Insights',
entries: [
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: '/Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity',
},
...(signerField && signerValue
? [
{
field: signerField,
operator: 'included',
type: 'match',
value: signerValue,
},
]
: []),
],
tags: ['policy:all'],
os_types: [os],
},
anonymizationFields: [],
subAction: 'invokeAI',
],
},
metadata: {
notes: {
llm_model: 'model-1',
},
} as unknown as KibanaRequest<unknown, unknown, DefendInsightsPostRequestBody>,
endpointMetadataService,
};
display_name: 'AVGAntivirus',
},
});

it('should correctly build workflow insights', async () => {
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
windows: ['endpoint-1'],
});
const params = generateParams();
const result = await buildIncompatibleAntivirusWorkflowInsights(params);

expect(result).toEqual([buildExpectedInsight('windows')]);
expect(groupEndpointIdsByOS).toHaveBeenCalledWith(
['endpoint-1'],
params.endpointMetadataService
);
});

it('should correctly build workflow insights', async () => {
it('should correctly build workflow insights for Windows with signerId provided', async () => {
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
windows: ['endpoint-1'],
});
const params = generateParams('test.com');

params.esClient.search = jest.fn().mockResolvedValue({
hits: {
hits: [
{
_id: 'lqw5opMB9Ke6SNgnxRSZ',
_source: {
process: {
Ext: {
code_signature: {
trusted: true,
subject_name: 'test.com',
},
},
},
},
},
],
},
});

const result = await buildIncompatibleAntivirusWorkflowInsights(params);

expect(result).toEqual([
expect.objectContaining({
'@timestamp': expect.any(moment),
message: 'Incompatible antiviruses detected',
category: Category.Endpoint,
type: 'incompatible_antivirus',
source: {
type: SourceType.LlmConnector,
id: 'connector-id-1',
data_range_start: expect.any(moment),
data_range_end: expect.any(moment),
},
target: {
type: TargetType.Endpoint,
ids: ['endpoint-1'],
},
action: {
type: ActionType.Refreshed,
timestamp: expect.any(moment),
},
value: 'AVGAntivirus',
remediation: {
exception_list_items: [
{
list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id,
name: 'AVGAntivirus',
description: 'Suggested by Security Workflow Insights',
entries: [
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value:
'/Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity',
buildExpectedInsight('windows', 'process.Ext.code_signature', 'test.com'),
]);
expect(groupEndpointIdsByOS).toHaveBeenCalledWith(
['endpoint-1'],
params.endpointMetadataService
);
});

it('should correctly build workflow insights for MacOS with signerId provided', async () => {
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
macos: ['endpoint-1'],
});

const params = generateParams('test.com');

params.esClient.search = jest.fn().mockResolvedValue({
hits: {
hits: [
{
_id: 'lqw5opMB9Ke6SNgnxRSZ',
_source: {
process: {
code_signature: {
trusted: true,
subject_name: 'test.com',
},
],
tags: ['policy:all'],
os_types: ['windows'],
},
},
],
},
metadata: {
notes: {
llm_model: 'model-1',
},
},
}),
]);
],
},
});

const result = await buildIncompatibleAntivirusWorkflowInsights(params);

expect(result).toEqual([buildExpectedInsight('macos', 'process.code_signature', 'test.com')]);
expect(groupEndpointIdsByOS).toHaveBeenCalledWith(
['endpoint-1'],
params.endpointMetadataService
Expand Down
Loading

0 comments on commit 7bafc0b

Please sign in to comment.