Skip to content

Commit b5d8b82

Browse files
[9.0] [EDR Workflows] Workflow Insights - Proper Windows Signer field handling (elastic#209117) (elastic#210137)
# Backport This will backport the following commits from `main` to `9.0`: - [[EDR Workflows] Workflow Insights - Proper Windows Signer field handling (elastic#209117)](elastic#209117) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Konrad Szwarc","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-02-07T08:26:10Z","message":"[EDR Workflows] Workflow Insights - Proper Windows Signer field handling (elastic#209117)\n\nThis PR fixes an issue where the Signer was not properly propagated\nduring Trusted Apps creation from Insights. With these changes, we\nexpect process.Ext.code_signature on Windows to be an array (ESS, ESS\nCloud) containing signatures, or a single object (Serverless). On macOS,\nit will continue to be an object.\n\nPlease refer to the corresponding GitHub issue for the recordings.","sha":"b750d46c8bc043dbc4d50e01f7ece6fa8ec48e39","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Defend Workflows","backport:prev-minor","ci:cloud-deploy","ci:cloud-redeploy","ci:project-deploy-security","ci:project-redeploy","v9.1.0"],"title":"[EDR Workflows] Workflow Insights - Proper Windows Signer field handling","number":209117,"url":"https://github.com/elastic/kibana/pull/209117","mergeCommit":{"message":"[EDR Workflows] Workflow Insights - Proper Windows Signer field handling (elastic#209117)\n\nThis PR fixes an issue where the Signer was not properly propagated\nduring Trusted Apps creation from Insights. With these changes, we\nexpect process.Ext.code_signature on Windows to be an array (ESS, ESS\nCloud) containing signatures, or a single object (Serverless). On macOS,\nit will continue to be an object.\n\nPlease refer to the corresponding GitHub issue for the recordings.","sha":"b750d46c8bc043dbc4d50e01f7ece6fa8ec48e39"}},"sourceBranch":"main","suggestedTargetBranches":["9.0"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/209117","number":209117,"mergeCommit":{"message":"[EDR Workflows] Workflow Insights - Proper Windows Signer field handling (elastic#209117)\n\nThis PR fixes an issue where the Signer was not properly propagated\nduring Trusted Apps creation from Insights. With these changes, we\nexpect process.Ext.code_signature on Windows to be an array (ESS, ESS\nCloud) containing signatures, or a single object (Serverless). On macOS,\nit will continue to be an object.\n\nPlease refer to the corresponding GitHub issue for the recordings.","sha":"b750d46c8bc043dbc4d50e01f7ece6fa8ec48e39"}}]}] BACKPORT--> Co-authored-by: Konrad Szwarc <[email protected]>
1 parent 4211698 commit b5d8b82

File tree

4 files changed

+357
-28
lines changed

4 files changed

+357
-28
lines changed

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/builders/incompatible_antivirus.test.ts

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ import type { EndpointMetadataService } from '../../metadata';
2525
import { groupEndpointIdsByOS } from '../helpers';
2626
import { buildIncompatibleAntivirusWorkflowInsights } from './incompatible_antivirus';
2727

28-
jest.mock('../helpers', () => ({
29-
groupEndpointIdsByOS: jest.fn(),
30-
}));
28+
jest.mock('../helpers', () => {
29+
const actualHelpers = jest.requireActual('../helpers');
30+
return {
31+
...actualHelpers,
32+
groupEndpointIdsByOS: jest.fn(),
33+
};
34+
});
3135

3236
describe('buildIncompatibleAntivirusWorkflowInsights', () => {
3337
const mockEndpointAppContextService = createMockEndpointAppContext().service;
@@ -154,6 +158,45 @@ describe('buildIncompatibleAntivirusWorkflowInsights', () => {
154158
});
155159
const params = generateParams('test.com');
156160

161+
params.esClient.search = jest.fn().mockResolvedValue({
162+
hits: {
163+
hits: [
164+
{
165+
_id: 'lqw5opMB9Ke6SNgnxRSZ',
166+
_source: {
167+
process: {
168+
Ext: {
169+
code_signature: [
170+
{
171+
trusted: true,
172+
subject_name: 'test.com',
173+
},
174+
],
175+
},
176+
},
177+
},
178+
},
179+
],
180+
},
181+
});
182+
183+
const result = await buildIncompatibleAntivirusWorkflowInsights(params);
184+
185+
expect(result).toEqual([
186+
buildExpectedInsight('windows', 'process.Ext.code_signature', 'test.com'),
187+
]);
188+
expect(groupEndpointIdsByOS).toHaveBeenCalledWith(
189+
['endpoint-1'],
190+
params.endpointMetadataService
191+
);
192+
});
193+
194+
it('should correctly build workflow insights for Windows with signerId provided as object', async () => {
195+
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
196+
windows: ['endpoint-1'],
197+
});
198+
const params = generateParams('test.com');
199+
157200
params.esClient.search = jest.fn().mockResolvedValue({
158201
hits: {
159202
hits: [
@@ -185,6 +228,68 @@ describe('buildIncompatibleAntivirusWorkflowInsights', () => {
185228
);
186229
});
187230

231+
it('should fallback to createRemediation without signer field when no valid signatures exist for Windows', async () => {
232+
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
233+
windows: ['endpoint-1'],
234+
});
235+
236+
const params = generateParams('test.com');
237+
params.esClient.search = jest.fn().mockResolvedValue({
238+
hits: {
239+
hits: [
240+
{
241+
_id: 'lqw5opMB9Ke6SNgnxRSZ',
242+
_source: {
243+
process: {
244+
Ext: {
245+
code_signature: [{ trusted: false, subject_name: 'Untrusted Publisher' }],
246+
},
247+
},
248+
},
249+
},
250+
],
251+
},
252+
});
253+
254+
const result = await buildIncompatibleAntivirusWorkflowInsights(params);
255+
expect(result).toEqual([buildExpectedInsight('windows')]);
256+
});
257+
258+
it('should skip Microsoft Windows Hardware Compatibility Publisher and use the next trusted signature for Windows', async () => {
259+
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
260+
windows: ['endpoint-1'],
261+
});
262+
263+
const params = generateParams();
264+
params.esClient.search = jest.fn().mockResolvedValue({
265+
hits: {
266+
hits: [
267+
{
268+
_id: 'lqw5opMB9Ke6SNgnxRSZ',
269+
_source: {
270+
process: {
271+
Ext: {
272+
code_signature: [
273+
{
274+
trusted: true,
275+
subject_name: 'Microsoft Windows Hardware Compatibility Publisher',
276+
},
277+
{ trusted: true, subject_name: 'Next Trusted Publisher' },
278+
],
279+
},
280+
},
281+
},
282+
},
283+
],
284+
},
285+
});
286+
287+
const result = await buildIncompatibleAntivirusWorkflowInsights(params);
288+
expect(result).toEqual([
289+
buildExpectedInsight('windows', 'process.Ext.code_signature', 'Next Trusted Publisher'),
290+
]);
291+
});
292+
188293
it('should correctly build workflow insights for MacOS with signerId provided', async () => {
189294
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
190295
macos: ['endpoint-1'],
@@ -218,4 +323,27 @@ describe('buildIncompatibleAntivirusWorkflowInsights', () => {
218323
params.endpointMetadataService
219324
);
220325
});
326+
327+
it('should fallback to createRemediation without signer field for macOS when no code_signature exists', async () => {
328+
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
329+
macos: ['endpoint-1'],
330+
});
331+
332+
const params = generateParams();
333+
params.esClient.search = jest.fn().mockResolvedValue({
334+
hits: {
335+
hits: [
336+
{
337+
_id: 'lqw5opMB9Ke6SNgnxRSZ',
338+
_source: {
339+
process: {},
340+
},
341+
},
342+
],
343+
},
344+
});
345+
346+
const result = await buildIncompatibleAntivirusWorkflowInsights(params);
347+
expect(result).toEqual([buildExpectedInsight('macos')]);
348+
});
221349
});

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/builders/incompatible_antivirus.ts

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import moment from 'moment';
9-
import { get as _get, uniqBy } from 'lodash';
9+
import { uniqBy } from 'lodash';
1010

1111
import type { DefendInsight } from '@kbn/elastic-assistant-common';
1212

@@ -23,22 +23,8 @@ import {
2323
SourceType,
2424
TargetType,
2525
} from '../../../../../common/endpoint/types/workflow_insights';
26-
import { groupEndpointIdsByOS } from '../helpers';
27-
28-
interface FileEventDoc {
29-
process: {
30-
code_signature?: {
31-
subject_name: string;
32-
trusted: boolean;
33-
};
34-
Ext?: {
35-
code_signature?: {
36-
subject_name: string;
37-
trusted: boolean;
38-
};
39-
};
40-
};
41-
}
26+
import type { FileEventDoc } from '../helpers';
27+
import { getValidCodeSignature, groupEndpointIdsByOS } from '../helpers';
4228

4329
export async function buildIncompatibleAntivirusWorkflowInsights(
4430
params: BuildWorkflowInsightParams
@@ -163,14 +149,10 @@ export async function buildIncompatibleAntivirusWorkflowInsights(
163149
const codeSignatureSearchHit = codeSignaturesHits.find((hit) => hit._id === id);
164150

165151
if (codeSignatureSearchHit) {
166-
const extPath = os === 'windows' ? '.Ext' : '';
167-
const field = `process${extPath}.code_signature`;
168-
const value = _get(
169-
codeSignatureSearchHit,
170-
`_source.${field}.subject_name`,
171-
'invalid subject name'
172-
);
173-
return createRemediation(filePath, os, field, value);
152+
const signature = getValidCodeSignature(os, codeSignatureSearchHit._source);
153+
if (signature) {
154+
return createRemediation(filePath, os, signature.field, signature.value);
155+
}
174156
}
175157
}
176158

x-pack/solutions/security/plugins/security_solution/server/endpoint/services/workflow_insights/helpers.test.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ import {
2828
TargetType,
2929
} from '../../../../common/endpoint/types/workflow_insights';
3030
import type { EndpointMetadataService } from '../metadata';
31+
import type { FileEventDoc } from './helpers';
3132
import {
3233
buildEsQueryParams,
3334
checkIfRemediationExists,
3435
createDatastream,
3536
createPipeline,
3637
generateInsightId,
3738
generateTrustedAppsFilter,
39+
getValidCodeSignature,
3840
groupEndpointIdsByOS,
3941
} from './helpers';
4042
import {
@@ -369,6 +371,7 @@ describe('helpers', () => {
369371
expect(filter).toBe('');
370372
});
371373
});
374+
372375
describe('checkIfRemediationExists', () => {
373376
it('should return false for non-incompatible_antivirus types', async () => {
374377
const insight = getDefaultInsight({
@@ -421,4 +424,155 @@ describe('helpers', () => {
421424
expect(result).toBe(true);
422425
});
423426
});
427+
428+
describe('getValidCodeSignature', () => {
429+
it('should return the first trusted signature for Windows', () => {
430+
const os = 'windows';
431+
const codeSignatureSearchHit = {
432+
process: {
433+
Ext: {
434+
code_signature: [{ subject_name: 'Valid Cert', trusted: true }],
435+
},
436+
},
437+
};
438+
439+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
440+
expect(result).toEqual({
441+
field: 'process.Ext.code_signature',
442+
value: 'Valid Cert',
443+
});
444+
});
445+
446+
it('should return null if no trusted signatures', () => {
447+
const os = 'windows';
448+
const codeSignatureSearchHit = {
449+
process: {
450+
Ext: {
451+
code_signature: [{ subject_name: 'Valid Cert', trusted: false }],
452+
},
453+
},
454+
};
455+
456+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
457+
expect(result).toBeNull();
458+
});
459+
460+
it('should return null if all Windows code signatures are untrusted', () => {
461+
const os = 'windows';
462+
const codeSignatureSearchHit = {
463+
process: {
464+
Ext: {
465+
code_signature: [
466+
{ subject_name: 'Cert 1', trusted: false },
467+
{ subject_name: 'Cert 2', trusted: false },
468+
],
469+
},
470+
},
471+
};
472+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
473+
expect(result).toBeNull();
474+
});
475+
476+
it('should correctly process a single object code signature for Windows', () => {
477+
const os = 'windows';
478+
const codeSignatureSearchHit = {
479+
process: {
480+
Ext: {
481+
code_signature: { subject_name: 'Valid Cert', trusted: true },
482+
},
483+
},
484+
};
485+
486+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
487+
expect(result).toEqual({
488+
field: 'process.Ext.code_signature',
489+
value: 'Valid Cert',
490+
});
491+
});
492+
493+
it('should return the first trusted signature for Windows, skipping Microsoft Windows Hardware Compatibility Publisher', () => {
494+
const os = 'windows';
495+
const codeSignatureSearchHit = {
496+
process: {
497+
Ext: {
498+
code_signature: [
499+
{ subject_name: 'Microsoft Windows Hardware Compatibility Publisher', trusted: true },
500+
{ subject_name: 'Valid Cert', trusted: false },
501+
{ subject_name: 'Valid Cert2', trusted: true },
502+
],
503+
},
504+
},
505+
};
506+
507+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
508+
expect(result).toEqual({
509+
field: 'process.Ext.code_signature',
510+
value: 'Valid Cert2',
511+
});
512+
});
513+
514+
it('should return Windows publisher if this is the only signer', () => {
515+
const os = 'windows';
516+
const codeSignatureSearchHit = {
517+
process: {
518+
Ext: {
519+
code_signature: [
520+
{ subject_name: 'Microsoft Windows Hardware Compatibility Publisher', trusted: true },
521+
],
522+
},
523+
},
524+
};
525+
526+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
527+
expect(result).toEqual({
528+
field: 'process.Ext.code_signature',
529+
value: 'Microsoft Windows Hardware Compatibility Publisher',
530+
});
531+
});
532+
533+
it('should return the subject name for macOS when code signature is present', () => {
534+
const os = 'macos';
535+
const codeSignatureSearchHit = {
536+
process: {
537+
code_signature: { subject_name: 'Apple Inc.', trusted: true },
538+
},
539+
};
540+
541+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
542+
expect(result).toEqual({ field: 'process.code_signature', value: 'Apple Inc.' });
543+
});
544+
545+
it('should return null if no code signature is present for macOS', () => {
546+
const os = 'macos';
547+
const codeSignatureSearchHit = {
548+
process: {},
549+
};
550+
551+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
552+
expect(result).toBeNull();
553+
});
554+
555+
it('should return null if code_signature field is empty for macOS', () => {
556+
const os = 'macos';
557+
const codeSignatureSearchHit = {
558+
process: {
559+
code_signature: {},
560+
},
561+
} as FileEventDoc;
562+
563+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
564+
expect(result).toBeNull();
565+
});
566+
567+
it('should return null for non-Windows when code signature is untrusted', () => {
568+
const os = 'macos';
569+
const codeSignatureSearchHit = {
570+
process: {
571+
code_signature: { subject_name: 'Apple Inc.', trusted: false },
572+
},
573+
};
574+
const result = getValidCodeSignature(os, codeSignatureSearchHit);
575+
expect(result).toBeNull();
576+
});
577+
});
424578
});

0 commit comments

Comments
 (0)