diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/calculate_risk_scores.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/calculate_risk_scores.ts index fbe580d9e9a47..e6edfb854c09e 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/calculate_risk_scores.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/calculate_risk_scores.ts @@ -20,12 +20,11 @@ import { } from './category_weights'; import type { CalculateRiskScoreAggregations, - FullRiskScore, GetScoresParams, GetScoresResponse, IdentifierType, + RiskScore, RiskScoreBucket, - SimpleRiskScore, } from './types'; const getFieldForIdentifierAgg = (identifierType: IdentifierType): string => @@ -41,7 +40,7 @@ const bucketToResponse = ({ enrichInputs?: boolean; now: string; identifierField: string; -}): SimpleRiskScore | FullRiskScore => ({ +}): RiskScore => ({ '@timestamp': now, identifierField, identifierValue: bucket.key[identifierField], diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_score_service.mock.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_score_service.mock.ts new file mode 100644 index 0000000000000..642e71c155e46 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_score_service.mock.ts @@ -0,0 +1,32 @@ +/* + * 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 type { RiskScoreService } from './risk_score_service'; +import type { RiskScore } from './types'; + +const createRiskScoreMock = (overrides: Partial = {}): RiskScore => ({ + '@timestamp': '2023-02-15T00:15:19.231Z', + identifierField: 'host.name', + identifierValue: 'hostname', + level: 'High', + totalScore: 149, + totalScoreNormalized: 85.332, + alertsScore: 85, + otherScore: 0, + notes: [], + riskiestInputs: [], + ...overrides, +}); + +const createRiskScoreServiceMock = (): jest.Mocked => ({ + getScores: jest.fn(), +}); + +export const riskScoreServiceMock = { + create: createRiskScoreServiceMock, + createRiskScore: createRiskScoreMock, +}; diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_score_service.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_score_service.ts index d7f06f4ab318e..c60ea852f5603 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/risk_score_service.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/risk_score_service.ts @@ -13,7 +13,7 @@ export interface RiskScoreService { getScores: (params: GetScoresParams) => Promise; } -export const buildRiskScoreService = ({ +export const riskScoreService = ({ esClient, logger, }: { diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/routes/risk_score_route.test.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/routes/risk_score_route.test.ts new file mode 100644 index 0000000000000..f1f40a37e29ab --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/routes/risk_score_route.test.ts @@ -0,0 +1,154 @@ +/* + * 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 { loggerMock } from '@kbn/logging-mocks'; + +import { RISK_SCORES_URL } from '../../../../common/constants'; +import { + serverMock, + requestContextMock, + requestMock, +} from '../../detection_engine/routes/__mocks__'; +import { riskScoreService } from '../risk_score_service'; +import { riskScoreServiceMock } from '../risk_score_service.mock'; +import { riskScoringRoute } from './risk_scoring_route'; + +jest.mock('../risk_score_service'); + +describe('GET risk_engine/scores route', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + let logger: ReturnType; + let mockRiskScoreService: ReturnType; + + beforeEach(() => { + jest.resetAllMocks(); + + server = serverMock.create(); + logger = loggerMock.create(); + ({ clients, context } = requestContextMock.createTools()); + mockRiskScoreService = riskScoreServiceMock.create(); + + clients.appClient.getAlertsIndex.mockReturnValue('default-alerts-index'); + (riskScoreService as jest.Mock).mockReturnValue(mockRiskScoreService); + + riskScoringRoute(server.router, logger); + }); + + const buildRequest = (body: object = {}) => + requestMock.create({ + method: 'get', + path: RISK_SCORES_URL, + body, + }); + + describe('parameters', () => { + describe('index / dataview', () => { + it('defaults to scoring the alerts index if no dataview is provided', async () => { + const request = buildRequest(); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(mockRiskScoreService.getScores).toHaveBeenCalledWith( + expect.objectContaining({ index: 'default-alerts-index' }) + ); + }); + + it('respects the provided dataview', async () => { + const request = buildRequest({ data_view_id: 'custom-dataview-id' }); + + // mock call to get dataview title + clients.savedObjectsClient.get.mockResolvedValueOnce({ + id: '', + type: '', + references: [], + attributes: { title: 'custom-dataview-index' }, + }); + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(mockRiskScoreService.getScores).toHaveBeenCalledWith( + expect.objectContaining({ index: 'custom-dataview-index' }) + ); + }); + + it('defaults to the alerts index if dataview is not found', async () => { + const request = buildRequest({ data_view_id: 'custom-dataview-id' }); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(mockRiskScoreService.getScores).toHaveBeenCalledWith( + expect.objectContaining({ index: 'default-alerts-index' }) + ); + }); + }); + + describe('date range', () => { + it('defaults to the last 15 days of data', async () => { + const request = buildRequest(); + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(mockRiskScoreService.getScores).toHaveBeenCalledWith( + expect.objectContaining({ range: { start: 'now-15d', end: 'now' } }) + ); + }); + + it('respects the provided range if provided', async () => { + const request = buildRequest({ range: { start: 'now-30d', end: 'now-20d' } }); + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(mockRiskScoreService.getScores).toHaveBeenCalledWith( + expect.objectContaining({ range: { start: 'now-30d', end: 'now-20d' } }) + ); + }); + + it.todo('rejects if date range is invalid'); + }); + + describe('data filter', () => { + it('respects the provided filter if provided', async () => { + const request = buildRequest({ + filter: { + bool: { + filter: [ + { + ids: { + values: '1', + }, + }, + ], + }, + }, + }); + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(mockRiskScoreService.getScores).toHaveBeenCalledWith( + expect.objectContaining({ + filter: { + bool: { + filter: [ + { + ids: { + values: '1', + }, + }, + ], + }, + }, + }) + ); + }); + + it.todo('rejects if filter is invalid'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/routes/risk_scoring_route.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/routes/risk_scoring_route.ts index cb63b299fe282..15677dc4adf82 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/routes/risk_scoring_route.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/routes/risk_scoring_route.ts @@ -13,7 +13,7 @@ import { RISK_SCORES_URL } from '../../../../common/constants'; import { riskScoresRequestSchema } from '../../../../common/risk_engine/risk_scoring/risk_scores_request_schema'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; -import { buildRiskScoreService } from '../risk_score_service'; +import { riskScoreService } from '../risk_score_service'; import { getRiskInputsIndex } from '../helpers'; export const riskScoringRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => { @@ -30,7 +30,7 @@ export const riskScoringRoute = (router: SecuritySolutionPluginRouter, logger: L const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; const siemClient = (await context.securitySolution).getAppClient(); - const riskScoreService = buildRiskScoreService({ + const riskScore = riskScoreService({ esClient, logger, }); @@ -56,7 +56,7 @@ export const riskScoringRoute = (router: SecuritySolutionPluginRouter, logger: L siemClient.getAlertsIndex(); const range = userRange ?? { start: 'now-15d', end: 'now' }; - const result = await riskScoreService.getScores({ + const result = await riskScore.getScores({ debug, enrichInputs, index, diff --git a/x-pack/plugins/security_solution/server/lib/risk_engine/types.ts b/x-pack/plugins/security_solution/server/lib/risk_engine/types.ts index c8874b5440b5c..5a0392a9eb193 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_engine/types.ts @@ -33,7 +33,7 @@ export interface GetScoresResponse { request: unknown; response: unknown; }; - scores: SimpleRiskScore[] | FullRiskScore[]; + scores: RiskScore[]; } export interface SimpleRiskInput { @@ -44,7 +44,7 @@ export interface SimpleRiskInput { export type RiskInput = Ecs; -export interface BaseRiskScore { +export interface RiskScore { '@timestamp': string; identifierField: string; identifierValue: string; @@ -54,14 +54,7 @@ export interface BaseRiskScore { alertsScore: number; otherScore: number; notes: string[]; -} - -export interface SimpleRiskScore extends BaseRiskScore { - riskiestInputs: SimpleRiskInput[]; -} - -export interface FullRiskScore extends BaseRiskScore { - riskiestInputs: RiskInput[]; + riskiestInputs: SimpleRiskInput[] | RiskInput[]; } export interface CalculateRiskScoreAggregations {