[Security Solution] Change risk scoring sum max and simplify risk score calculations (#184638)

## Summary

* Update the `RISK_SCORING_SUM_MAX` to the appropriate value based
10.000 alerts (read more on the original issue)
* The following risk scoring engine lines can be simplified by no longer
multiplying by 100, and instead using the value above directly. I also
renamed the constants to improve reliability,


I rounded `2.592375848672986` up to `2.5924` so the calculated score
won't go above `100`.

For `10.000` alerts with a risk score of `100` each the calculated risk
score is `99.99906837960884`

Risk score calculation for 10_00 alerts with 100 risk score
![Screenshot 2024-06-03 at 11 56
48](00c876ea-388b-4322-b8f8-19fc65f9f833)

Risk score calculation for 1_000 alerts with 100 risk score
![Screenshot 2024-06-03 at 11 57
29](929746c2-19e9-4da1-b4b1-c6e56edfc77c)



### User Impact
The entity's calculated risk score will slightly increase because we
update the normalisation divisor from 261.2 to 2.5924.




### Checklist

Delete any items that are not applicable to this PR.

- [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
This commit is contained in:
Pablo Machado 2024-06-06 14:30:38 +02:00 committed by GitHub
parent 22155aefdc
commit b4561e7c3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 79 additions and 123 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { applyCriticalityToScore, normalize } from './helpers';
import { applyCriticalityToScore } from './helpers';
describe('applyCriticalityToScore', () => {
describe('integer scores', () => {
@ -61,42 +61,3 @@ describe('applyCriticalityToScore', () => {
});
});
});
describe('normalize', () => {
it('returns 0 if the number is equal to the min', () => {
const result = normalize({ number: 0, min: 0, max: 100 });
expect(result).toEqual(0);
});
it('returns 100 if the number is equal to the max', () => {
const result = normalize({ number: 100, min: 0, max: 100 });
expect(result).toEqual(100);
});
it('returns 50 if the number is halfway between the min and max', () => {
const result = normalize({ number: 50, min: 0, max: 100 });
expect(result).toEqual(50);
});
it('defaults to a min of 0', () => {
const result = normalize({ number: 50, max: 100 });
expect(result).toEqual(50);
});
describe('when the domain is diffrent than the range', () => {
it('returns 0 if the number is equal to the min', () => {
const result = normalize({ number: 20, min: 20, max: 200 });
expect(result).toEqual(0);
});
it('returns 100 if the number is equal to the max', () => {
const result = normalize({ number: 40, min: 30, max: 40 });
expect(result).toEqual(100);
});
it('returns 50 if the number is halfway between the min and max', () => {
const result = normalize({ number: 20, min: 0, max: 40 });
expect(result).toEqual(50);
});
});
});

View file

@ -65,22 +65,3 @@ export const bayesianUpdate = ({
const newProbability = priorProbability * modifier;
return (max * newProbability) / (1 + newProbability);
};
/**
* Normalizes a number to the range [0, 100]
*
* @param number - The number to be normalized
* @param min - The minimum possible value of the number. Defaults to 0.
* @param max - The maximum possible value of the number
*
* @returns The updated score with modifiers applied
*/
export const normalize = ({
number,
min = 0,
max,
}: {
number: number;
min?: number;
max: number;
}) => ((number - min) / (max - min)) * 100;

View file

@ -30,18 +30,14 @@ import {
import { withSecuritySpan } from '../../../utils/with_security_span';
import type { AssetCriticalityRecord } from '../../../../common/api/entity_analytics';
import type { AssetCriticalityService } from '../asset_criticality/asset_criticality_service';
import {
applyCriticalityToScore,
getCriticalityModifier,
normalize,
} from '../asset_criticality/helpers';
import { applyCriticalityToScore, getCriticalityModifier } from '../asset_criticality/helpers';
import { getAfterKeyForIdentifierType, getFieldForIdentifier } from './helpers';
import type {
CalculateRiskScoreAggregations,
CalculateScoresParams,
RiskScoreBucket,
} from '../types';
import { RISK_SCORING_SUM_MAX, RISK_SCORING_SUM_VALUE } from './constants';
import { RIEMANN_ZETA_VALUE, RIEMANN_ZETA_S_VALUE } from './constants';
import { getPainlessScripts, type PainlessScripts } from './painless';
const formatForResponse = ({
@ -82,10 +78,7 @@ const formatForResponse = ({
calculated_level: calculatedLevel,
calculated_score: riskDetails.value.score,
calculated_score_norm: normalizedScoreWithCriticality,
category_1_score: normalize({
number: riskDetails.value.category_1_score,
max: RISK_SCORING_SUM_MAX,
}),
category_1_score: riskDetails.value.category_1_score / RIEMANN_ZETA_VALUE, // normalize value to be between 0-100
category_1_count: riskDetails.value.category_1_count,
notes: riskDetails.value.notes,
inputs: riskDetails.value.risk_inputs.map((riskInput) => ({
@ -150,8 +143,8 @@ const buildIdentifierTypeAggregation = ({
map_script: scriptedMetricPainless.map,
combine_script: scriptedMetricPainless.combine,
params: {
p: RISK_SCORING_SUM_VALUE,
risk_cap: RISK_SCORING_SUM_MAX,
p: RIEMANN_ZETA_S_VALUE,
risk_cap: RIEMANN_ZETA_VALUE,
global_identifier_type_weight: globalIdentifierTypeWeight || 1,
},
reduce_script: scriptedMetricPainless.reduce,

View file

@ -6,15 +6,36 @@
*/
/**
* The risk scoring algorithm uses a Riemann zeta function to sum an entity's risk inputs to a known, finite value (@see RISK_SCORING_SUM_MAX). It does so by assigning each input a weight based on its position in the list (ordered by score) of inputs. This value represents the complex variable s of Re(s) in traditional Riemann zeta function notation.
* The risk scoring algorithm uses a Riemann zeta function to sum an entity's risk inputs to a known, finite value (@see RIEMANN_ZETA_VALUE).
* It does so by assigning each input a weight based on its position in the list (ordered by score) of inputs.
* This value represents the complex variable s of Re(s) in traditional Riemann zeta function notation.
*
* Read more: https://en.wikipedia.org/wiki/Riemann_zeta_function
*/
export const RISK_SCORING_SUM_VALUE = 1.5;
export const RIEMANN_ZETA_S_VALUE = 1.5;
/**
* Represents the maximum possible risk score sum. This value is derived from RISK_SCORING_SUM_VALUE, but we store the precomputed value here to be used more conveniently in normalization.
* @see RISK_SCORING_SUM_VALUE
* Represents the value calculated by Riemann Zeta function for RIEMANN_ZETA_S_VALUE with 10.000 iterations (inputs) which is the default alertSampleSizePerShard.
* The maximum unnormalized risk score value is calculated by multiplying RIEMANN_ZETA_S_VALUE by the maximum alert risk_score (100).
*
* This value is derived from RIEMANN_ZETA_S_VALUE, but we store the precomputed value here to be used more conveniently in normalization. @see RIEMANN_ZETA_S_VALUE
*
* The Riemann Zeta value for different number of inputs is:
* | 𝑍(s,inputs) |
* | :---------------------------------------|
* | 𝑍(1.5,10)1.9953364933456017 |
* | 𝑍(1.5,100)2.412874098703719 |
* | 𝑍(1.5,1000)2.5491456029175756 |
* | 𝑍(1.5,10_000)2.5923758486729866 |
* | 𝑍(1.5,100_000)2.6060508091764736 |
* | 𝑍(1.5,1_000_000)2.6103753491852295 |
* | 𝑍(1.5,10_000_000)2.611742893169012 |
* | 𝑍(1.5,100_000_000)2.6121753486854478 |
* | 𝑍(1.5,1_000_000_000)2.6123121030481857 |
*
* Read more: https://en.wikipedia.org/wiki/Riemann_zeta_function
*/
export const RISK_SCORING_SUM_MAX = 261.2;
export const RIEMANN_ZETA_VALUE = 2.5924;
/**
* This value represents the maximum possible risk score after normalization.

View file

@ -17,7 +17,7 @@ describe('getPainlessScripts', () => {
"combine": "return state;",
"init": "state.inputs = []",
"map": "Map fields = new HashMap();fields.put('id', doc['kibana.alert.uuid'].value);fields.put('index', doc['_index'].value);fields.put('time', doc['@timestamp'].value);fields.put('rule_name', doc['kibana.alert.rule.name'].value);fields.put('category', doc['event.kind'].value);fields.put('score', doc['kibana.alert.risk_score'].value);state.inputs.add(fields); ",
"reduce": "Map results = new HashMap();results['notes'] = [];results['category_1_score'] = 0.0;results['category_1_count'] = 0;results['risk_inputs'] = [];results['score'] = 0.0;def inputs = states[0].inputs;Collections.sort(inputs, (a, b) -> b.get('score').compareTo(a.get('score')));for (int i = 0; i < inputs.length; i++) { double current_score = inputs[i].score / Math.pow(i + 1, params.p); if (i < 10) { inputs[i][\\"contribution\\"] = 100 * current_score / params.risk_cap; results['risk_inputs'].add(inputs[i]); } results['category_1_score'] += current_score; results['category_1_count'] += 1; results['score'] += current_score;}results['score'] *= params.global_identifier_type_weight;results['normalized_score'] = 100 * results['score'] / params.risk_cap;return results;",
"reduce": "Map results = new HashMap();results['notes'] = [];results['category_1_score'] = 0.0;results['category_1_count'] = 0;results['risk_inputs'] = [];results['score'] = 0.0;def inputs = states[0].inputs;Collections.sort(inputs, (a, b) -> b.get('score').compareTo(a.get('score')));for (int i = 0; i < inputs.length; i++) { double current_score = inputs[i].score / Math.pow(i + 1, params.p); if (i < 10) { inputs[i]['contribution'] = current_score / params.risk_cap; results['risk_inputs'].add(inputs[i]); } results['category_1_score'] += current_score; results['category_1_count'] += 1; results['score'] += current_score;}results['score'] *= params.global_identifier_type_weight;results['normalized_score'] = results['score'] / params.risk_cap;return results;",
}
`);
});

View file

@ -22,7 +22,7 @@ for (int i = 0; i < inputs.length; i++) {
double current_score = inputs[i].score / Math.pow(i + 1, params.p);
if (i < 10) {
inputs[i]["contribution"] = 100 * current_score / params.risk_cap;
inputs[i]['contribution'] = current_score / params.risk_cap;
results['risk_inputs'].add(inputs[i]);
}
@ -36,6 +36,6 @@ for (int i = 0; i < inputs.length; i++) {
}
results['score'] *= params.global_identifier_type_weight;
results['normalized_score'] = 100 * results['score'] / params.risk_cap;
results['normalized_score'] = results['score'] / params.risk_cap;
return results;

View file

@ -138,8 +138,8 @@ export default ({ getService }: FtrProviderContext): void => {
expect(score).to.eql({
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 8.039816232771823,
category_1_score: 8.039816232771821,
calculated_score_norm: 8.10060175898781,
category_1_score: 8.10060175898781,
category_1_count: 1,
id_field: 'host.name',
id_value: 'host-1',
@ -353,8 +353,8 @@ export default ({ getService }: FtrProviderContext): void => {
criticality_modifier: 1.5,
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 11.59366948840633,
category_1_score: 8.039816232771821,
calculated_score_norm: 11.677912063468526,
category_1_score: 8.10060175898781,
category_1_count: 1,
id_field: 'host.name',
id_value: 'host-1',

View file

@ -126,8 +126,8 @@ export default ({ getService }: FtrProviderContext): void => {
const expectedScore = {
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 8.039816232771823,
category_1_score: 8.039816232771821,
calculated_score_norm: 8.10060175898781,
category_1_score: 8.10060175898781,
category_1_count: 1,
id_field: 'host.name',
id_value: 'host-1',
@ -176,8 +176,8 @@ export default ({ getService }: FtrProviderContext): void => {
criticality_modifier: 1.5,
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 11.59366948840633,
category_1_score: 8.039816232771821,
calculated_score_norm: 11.677912063468526,
category_1_score: 8.10060175898781,
category_1_count: 1,
id_field: 'host.name',
id_value: 'host-1',

View file

@ -116,9 +116,9 @@ export default ({ getService }: FtrProviderContext): void => {
expect(score).to.eql({
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 8.039816232771823,
calculated_score_norm: 8.10060175898781,
category_1_count: 1,
category_1_score: 8.039816232771821,
category_1_score: 8.10060175898781,
id_field: 'host.name',
id_value: 'host-1',
});
@ -144,18 +144,18 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 8.039816232771823,
calculated_score_norm: 8.10060175898781,
category_1_count: 1,
category_1_score: 8.039816232771821,
category_1_score: 8.10060175898781,
id_field: 'host.name',
id_value: 'host-1',
},
{
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 8.039816232771823,
calculated_score_norm: 8.10060175898781,
category_1_count: 1,
category_1_score: 8.039816232771821,
category_1_score: 8.10060175898781,
id_field: 'host.name',
id_value: 'host-2',
},
@ -177,9 +177,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Unknown',
calculated_score: 28.42462120245875,
calculated_score_norm: 10.88232052161514,
calculated_score_norm: 10.964596976723788,
category_1_count: 2,
category_1_score: 10.882320521615142,
category_1_score: 10.964596976723788,
id_field: 'host.name',
id_value: 'host-1',
},
@ -199,9 +199,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Unknown',
calculated_score: 47.25513506055279,
calculated_score_norm: 18.091552473412246,
calculated_score_norm: 18.228334771081926,
category_1_count: 30,
category_1_score: 18.091552473412246,
category_1_score: 18.228334771081926,
id_field: 'host.name',
id_value: 'host-1',
},
@ -224,18 +224,18 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Unknown',
calculated_score: 47.25513506055279,
calculated_score_norm: 18.091552473412246,
calculated_score_norm: 18.228334771081926,
category_1_count: 30,
category_1_score: 18.091552473412246,
category_1_score: 18.228334771081926,
id_field: 'host.name',
id_value: 'host-1',
},
{
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 8.039816232771823,
calculated_score_norm: 8.10060175898781,
category_1_count: 1,
category_1_score: 8.039816232771821,
category_1_score: 8.10060175898781,
id_field: 'host.name',
id_value: 'host-2',
},
@ -255,9 +255,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Unknown',
calculated_score: 50.67035607277805,
calculated_score_norm: 19.399064346392823,
calculated_score_norm: 19.545732168175455,
category_1_count: 100,
category_1_score: 19.399064346392823,
category_1_score: 19.545732168175455,
id_field: 'host.name',
id_value: 'host-1',
},
@ -280,9 +280,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Critical',
calculated_score: 241.2874098703716,
calculated_score_norm: 92.37649688758484,
calculated_score_norm: 93.07491508654975,
category_1_count: 100,
category_1_score: 92.37649688758484,
category_1_score: 93.07491508654975,
id_field: 'host.name',
id_value: 'host-1',
},
@ -311,9 +311,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Critical',
calculated_score: 254.91456029175757,
calculated_score_norm: 97.59362951445543,
calculated_score_norm: 98.33149216623883,
category_1_count: 1000,
category_1_score: 97.59362951445543,
category_1_score: 98.33149216623883,
id_field: 'host.name',
id_value: 'host-1',
},
@ -407,9 +407,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'High',
calculated_score: 225.1106801442913,
calculated_score_norm: 86.18326192354185,
calculated_score_norm: 86.83485578779946,
category_1_count: 100,
category_1_score: 86.18326192354185,
category_1_score: 86.83485578779946,
id_field: 'host.name',
id_value: 'host-1',
},
@ -436,9 +436,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Moderate',
calculated_score: 120.6437049351858,
calculated_score_norm: 46.18824844379242,
calculated_score_norm: 46.537457543274876,
category_1_count: 100,
category_1_score: 92.37649688758484,
category_1_score: 93.07491508654975,
id_field: 'host.name',
id_value: 'host-1',
},
@ -463,9 +463,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Moderate',
calculated_score: 168.9011869092601,
calculated_score_norm: 64.66354782130938,
calculated_score_norm: 65.15244056058482,
category_1_count: 100,
category_1_score: 92.37649688758484,
category_1_score: 93.07491508654975,
id_field: 'user.name',
id_value: 'user-1',
},
@ -492,9 +492,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'Low',
calculated_score: 93.23759116471251,
calculated_score_norm: 35.695861854790394,
calculated_score_norm: 35.96574261869793,
category_1_count: 50,
category_1_score: 89.23965463697598,
category_1_score: 89.91435654674481,
id_field: 'host.name',
id_value: 'host-1',
},
@ -504,9 +504,9 @@ export default ({ getService }: FtrProviderContext): void => {
{
calculated_level: 'High',
calculated_score: 186.47518232942502,
calculated_score_norm: 71.39172370958079,
calculated_score_norm: 71.93148523739586,
category_1_count: 50,
category_1_score: 89.23965463697598,
category_1_score: 89.91435654674481,
id_field: 'user.name',
id_value: 'user-1',
},
@ -547,18 +547,18 @@ export default ({ getService }: FtrProviderContext): void => {
criticality_modifier: 2.0,
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 14.8830616583983,
calculated_score_norm: 14.987153868113044,
category_1_count: 1,
category_1_score: 8.039816232771821,
category_1_score: 8.10060175898781,
id_field: 'host.name',
id_value: 'host-1',
},
{
calculated_level: 'Unknown',
calculated_score: 21,
calculated_score_norm: 8.039816232771823,
calculated_score_norm: 8.10060175898781,
category_1_count: 1,
category_1_score: 8.039816232771821,
category_1_score: 8.10060175898781,
id_field: 'host.name',
id_value: 'host-2',
},

View file

@ -274,9 +274,9 @@ export default ({ getService }: FtrProviderContext): void => {
criticality_modifier: 2,
calculated_level: 'Moderate',
calculated_score: 79.81345973382406,
calculated_score_norm: 46.809565696393314,
calculated_score_norm: 47.08016240063269,
category_1_count: 10,
category_1_score: 30.55645472198471,
category_1_score: 30.787478681462762,
},
]);
});