Skip to content

Commit

Permalink
[SecuritySolution] - Create API that calculates the risk score for an…
Browse files Browse the repository at this point in the history
… Entity (#181715)

## Summary

Create the API that calculates and stores the risk score for one entity.

### API
```
POST /api/risk_scores/calculation/entity
{
    identifier_type: "host", // host || user
    identifier: "Host-mbl1642qhk",
}
```

### Example Response
```
{
  success: true, // a calculation can be successful but have no score, I didnt like the idea of sending an empty object in that case
  score : { // optional if a score is calculated
    "calculated_level": "Low",
    "calculated_score": 48.94973536147405,
    "calculated_score_norm": 31.565227875770386,
    "category_1_score": 18.74032747376495,
    "category_1_count": 50,
    "notes": [],
    "inputs": [...inputs],
    "category_2_score": 12.824900402005436,
    "category_2_count": 1,
    "criticality_level": "low_impact",
    "criticality_modifier": 2
  }
}
````

### How to test it?
```
 KIBANA_URL="http://localhost:5601/pablo"

curl "$KIBANA_URL/api/risk_scores/calculation/entity" \
  -H 'kbn-xsrf:hello' \
  --user elastic:changeme \
  -X 'POST' \
  -H 'elastic-api-version: 1' \
  -H "Content-Type: application/json" \
  -d '{"identifier_type": "host", "identifier": "Test-Host.name"}'
```


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
machadoum authored Apr 29, 2024
1 parent 4d35334 commit ce70c34
Show file tree
Hide file tree
Showing 19 changed files with 917 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* 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 { z } from 'zod';

/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Risk Engine Common Schema
* version: 1.0.0
*/

export type AfterKeys = z.infer<typeof AfterKeys>;
export const AfterKeys = z.object({
host: z.object({}).catchall(z.string()).optional(),
user: z.object({}).catchall(z.string()).optional(),
});

/**
* The identifier of the Kibana data view to be used when generating risk scores.
*/
export type DataViewId = z.infer<typeof DataViewId>;
export const DataViewId = z.string();

/**
* An elasticsearch DSL filter object. Used to filter the risk inputs involved, which implicitly filters the risk scores themselves.
*/
export type Filter = z.infer<typeof Filter>;
export const Filter = z.unknown();

/**
* Specifies how many scores will be involved in a given calculation. Note that this value is per `identifier_type`, i.e. a value of 10 will calculate 10 host scores and 10 user scores, if available. To avoid missed data, keep this value consistent while paginating through scores.
*/
export type PageSize = z.infer<typeof PageSize>;
export const PageSize = z.number().default(1000);

export type KibanaDate = z.infer<typeof KibanaDate>;
export const KibanaDate = z.union([z.string(), z.string().datetime(), z.string()]);

/**
* Defines the time period on which risk inputs will be filtered.
*/
export type DateRange = z.infer<typeof DateRange>;
export const DateRange = z.object({
start: KibanaDate,
end: KibanaDate,
});

export type IdentifierType = z.infer<typeof IdentifierType>;
export const IdentifierType = z.enum(['host', 'user']);
export type IdentifierTypeEnum = typeof IdentifierType.enum;
export const IdentifierTypeEnum = IdentifierType.enum;

/**
* A generic representation of a document contributing to a Risk Score.
*/
export type RiskScoreInput = z.infer<typeof RiskScoreInput>;
export const RiskScoreInput = z.object({
/**
* The unique identifier (`_id`) of the original source document
*/
id: z.string().optional(),
/**
* The unique index (`_index`) of the original source document
*/
index: z.string().optional(),
/**
* The risk category of the risk input document.
*/
category: z.string().optional(),
/**
* A human-readable description of the risk input document.
*/
description: z.string().optional(),
/**
* The weighted risk score of the risk input document.
*/
risk_score: z.number().min(0).max(100).optional(),
/**
* The @timestamp of the risk input document.
*/
timestamp: z.string().optional(),
});

export type RiskScore = z.infer<typeof RiskScore>;
export const RiskScore = z.object({
/**
* The time at which the risk score was calculated.
*/
'@timestamp': z.string().datetime(),
/**
* The identifier field defining this risk score. Coupled with `id_value`, uniquely identifies the entity being scored.
*/
id_field: z.string(),
/**
* The identifier value defining this risk score. Coupled with `id_field`, uniquely identifies the entity being scored.
*/
id_value: z.string(),
/**
* Lexical description of the entity's risk.
*/
calculated_level: z.string(),
/**
* The raw numeric value of the given entity's risk score.
*/
calculated_score: z.number(),
/**
* The normalized numeric value of the given entity's risk score. Useful for comparing with other entities.
*/
calculated_score_norm: z.number().min(0).max(100),
/**
* The contribution of Category 1 to the overall risk score (`calculated_score_norm`). Category 1 contains Detection Engine Alerts.
*/
category_1_score: z.number(),
/**
* The number of risk input documents that contributed to the Category 1 score (`category_1_score`).
*/
category_1_count: z.number(),
/**
* The contribution of Category 2 to the overall risk score (`calculated_score_norm`). Category 2 contains context from external sources.
*/
category_2_score: z.number().optional(),
/**
* The number of risk input documents that contributed to the Category 2 score (`category_2_score`).
*/
category_2_count: z.number().optional(),
/**
* The designated criticality level of the entity. Possible values are `low_impact`, `medium_impact`, `high_impact`, and `extreme_impact`.
*/
criticality_level: z.string().optional(),
/**
* The numeric modifier corresponding to the criticality level of the entity, which is used as an input to the risk score calculation.
*/
criticality_modifier: z.number().optional(),
/**
* A list of the highest-risk documents contributing to this risk score. Useful for investigative purposes.
*/
inputs: z.array(RiskScoreInput),
});

/**
* Configuration used to tune risk scoring. Weights can be used to change the score contribution of risk inputs for hosts and users at both a global level and also for Risk Input categories (e.g. 'category_1').
*/
export type RiskScoreWeight = z.infer<typeof RiskScoreWeight>;
export const RiskScoreWeight = z.object({
type: z.string(),
value: z.string().optional(),
host: z.number().min(0).max(1).optional(),
user: z.number().min(0).max(1).optional(),
});

/**
* A list of weights to be applied to the scoring calculation.
*/
export type RiskScoreWeights = z.infer<typeof RiskScoreWeights>;
export const RiskScoreWeights = z.array(RiskScoreWeight);

export type RiskEngineInitStep = z.infer<typeof RiskEngineInitStep>;
export const RiskEngineInitStep = z.object({
type: z.string(),
success: z.boolean(),
error: z.string().optional(),
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,25 @@ components:

Filter:
description: An elasticsearch DSL filter object. Used to filter the risk inputs involved, which implicitly filters the risk scores themselves.
$ref: 'https://cloud.elastic.co/api/v1/api-docs/spec.json#/definitions/QueryContainer'
# TODO Fix the following line reference. Issue: https://github.com/elastic/kibana/issues/181948
# $ref: 'https://cloud.elastic.co/api/v1/api-docs/spec.json#/definitions/QueryContainer'


PageSize:
description: Specifies how many scores will be involved in a given calculation. Note that this value is per `identifier_type`, i.e. a value of 10 will calculate 10 host scores and 10 user scores, if available. To avoid missed data, keep this value consistent while paginating through scores.
default: 1000
type: number

KibanaDate:
oneOf:
- type: string
format: date
- type: string
format: date-time
- type: string
format: datemath
example: '2017-07-21T17:32:28Z'

DateRange:
description: Defines the time period on which risk inputs will be filtered.
type: object
Expand All @@ -52,20 +64,44 @@ components:
end:
$ref: '#/components/schemas/KibanaDate'

KibanaDate:
type: string
oneOf:
- format: date
- format: date-time
- format: datemath
example: '2017-07-21T17:32:28Z'

IdentifierType:
type: string
enum:
- host
- user

RiskScoreInput:
description: A generic representation of a document contributing to a Risk Score.
type: object
properties:
id:
type: string
example: 91a93376a507e86cfbf282166275b89f9dbdb1f0be6c8103c6ff2909ca8e1a1c
description: The unique identifier (`_id`) of the original source document
index:
type: string
example: .internal.alerts-security.alerts-default-000001
description: The unique index (`_index`) of the original source document
category:
type: string
example: category_1
description: The risk category of the risk input document.
description:
type: string
example: 'Generated from Detection Engine Rule: Malware Prevention Alert'
description: A human-readable description of the risk input document.
risk_score:
type: number
format: double
minimum: 0
maximum: 100
description: The weighted risk score of the risk input document.
timestamp:
type: string
example: '2017-07-21T17:32:28Z'
description: The @timestamp of the risk input document.


RiskScore:
type: object
required:
Expand Down Expand Up @@ -136,37 +172,6 @@ components:
items:
$ref: '#/components/schemas/RiskScoreInput'

RiskScoreInput:
description: A generic representation of a document contributing to a Risk Score.
type: object
properties:
id:
type: string
example: 91a93376a507e86cfbf282166275b89f9dbdb1f0be6c8103c6ff2909ca8e1a1c
description: The unique identifier (`_id`) of the original source document
index:
type: string
example: .internal.alerts-security.alerts-default-000001
description: The unique index (`_index`) of the original source document
category:
type: string
example: category_1
description: The risk category of the risk input document.
description:
type: string
example: 'Generated from Detection Engine Rule: Malware Prevention Alert'
description: A human-readable description of the risk input document.
risk_score:
type: number
format: double
minimum: 0
maximum: 100
description: The weighted risk score of the risk input document.
timestamp:
type: string
example: '2017-07-21T17:32:28Z'
description: The @timestamp of the risk input document.

RiskScoreWeight:
description: "Configuration used to tune risk scoring. Weights can be used to change the score contribution of risk inputs for hosts and users at both a global level and also for Risk Input categories (e.g. 'category_1')."
type: object
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { z } from 'zod';

/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Risk Scoring API
* version: 1.0.0
*/

import { IdentifierType, RiskScore } from './common.gen';

export type RiskScoresEntityCalculationRequest = z.infer<typeof RiskScoresEntityCalculationRequest>;
export const RiskScoresEntityCalculationRequest = z.object({
/**
* Used to identify the entity.
*/
identifier: z.string(),
/**
* Used to define the type of entity.
*/
identifier_type: IdentifierType,
});

export type RiskScoresEntityCalculationResponse = z.infer<
typeof RiskScoresEntityCalculationResponse
>;
export const RiskScoresEntityCalculationResponse = z.object({
success: z.boolean(),
score: RiskScore.optional(),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
openapi: 3.0.0

info:
version: 1.0.0
title: Risk Scoring API
description: These APIs allow the consumer to manage Entity Risk Scores within Entity Analytics.

servers:
- url: 'http://{kibana_host}:{port}'
variables:
kibana_host:
default: localhost
port:
default: '5601'

paths:
/api/risk_scores/calculation/entity:
post:
summary: Trigger calculation of Risk Scores for an entity
description: Calculates and persists Risk Scores for an entity, returning the calculated risk score.
requestBody:
description: The entity type and identifier
content:
application/json:
schema:
$ref: '#/components/schemas/RiskScoresEntityCalculationRequest'
required: true
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/RiskScoresEntityCalculationResponse'
'400':
description: Invalid request

components:
schemas:
RiskScoresEntityCalculationRequest:
type: object
required:
- identifier
- identifier_type
properties:
identifier:
description: Used to identify the entity.
type: string
example: 'my.host'
identifier_type:
description: Used to define the type of entity.
$ref: './common.schema.yaml#/components/schemas/IdentifierType'

RiskScoresEntityCalculationResponse:
type: object
required:
- success
properties:
success:
type: boolean
score:
$ref: './common.schema.yaml#/components/schemas/RiskScore'
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
*/
export const RISK_ENGINE_PUBLIC_PREFIX = '/api/risk_scores' as const;
export const RISK_SCORE_CALCULATION_URL = `${RISK_ENGINE_PUBLIC_PREFIX}/calculation` as const;
export const RISK_SCORE_ENTITY_CALCULATION_URL =
`${RISK_ENGINE_PUBLIC_PREFIX}/calculation/entity` as const;

/**
* Internal Risk Score routes
Expand Down
Loading

0 comments on commit ce70c34

Please sign in to comment.