Skip to content

Commit

Permalink
[Security Solution] Implement normalization of ruleSource for API res…
Browse files Browse the repository at this point in the history
…ponses (#188631)

Fixes: #180140

## Summary

- Implements normalization of`rule_source` for API responses
- `rule_source` field in API responses is calculated out of the
`immutable` and `ruleSource` fields.

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
  • Loading branch information
jpdjere authored Jul 22, 2024
1 parent 013276e commit 232a166
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({
from: 'now-6m',
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
immutable: false,
rule_source: {
type: 'internal',
},
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
interval: '5m',
risk_score: 50,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ const reported = {
from: 'now-6m',
id: 'rule-id',
immutable: false,
rule_source: {
type: 'internal',
},
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
investigation_fields: undefined,
language: 'kuery',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '../../../normalization/rule_actions';
import { typeSpecificCamelToSnake } from './type_specific_camel_to_snake';
import { commonParamsCamelToSnake } from './common_params_camel_to_snake';
import { normalizeRuleParams } from './normalize_rule_params';

export const internalRuleToAPIResponse = (
rule: SanitizedRule<RuleParams> | ResolvedSanitizedRule<RuleParams>
Expand All @@ -31,6 +32,7 @@ export const internalRuleToAPIResponse = (
const alertActions = rule.actions.map(transformAlertToRuleAction);
const throttle = transformFromAlertThrottle(rule);
const actions = transformToActionFrequency(alertActions, throttle);
const normalizedRuleParams = normalizeRuleParams(rule.params);

return {
// saved object properties
Expand All @@ -49,7 +51,7 @@ export const internalRuleToAPIResponse = (
enabled: rule.enabled,
revision: rule.revision,
// Security solution shared rule params
...commonParamsCamelToSnake(rule.params),
...commonParamsCamelToSnake(normalizedRuleParams),
// Type specific security solution rule params
...typeSpecificCamelToSnake(rule.params),
// Actions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 { normalizeRuleSource } from './normalize_rule_params';
import type { BaseRuleParams } from '../../../../rule_schema';

describe('normalizeRuleSource', () => {
it('should return rule_source of type `internal` when immutable is false and ruleSource is undefined', () => {
const result = normalizeRuleSource({
immutable: false,
ruleSource: undefined,
});
expect(result).toEqual({
type: 'internal',
});
});

it('should return rule_source of type `external` and `isCustomized: false` when immutable is true and ruleSource is undefined', () => {
const result = normalizeRuleSource({
immutable: true,
ruleSource: undefined,
});
expect(result).toEqual({
type: 'external',
isCustomized: false,
});
});

it('should return existing value when ruleSource is present', () => {
const externalRuleSource: BaseRuleParams['ruleSource'] = {
type: 'external',
isCustomized: true,
};
const externalResult = normalizeRuleSource({ immutable: true, ruleSource: externalRuleSource });
expect(externalResult).toEqual({
type: externalRuleSource.type,
isCustomized: externalRuleSource.isCustomized,
});

const internalRuleSource: BaseRuleParams['ruleSource'] = {
type: 'internal',
};
const internalResult = normalizeRuleSource({
immutable: false,
ruleSource: internalRuleSource,
});
expect(internalResult).toEqual({
type: internalRuleSource.type,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { BaseRuleParams, RuleSourceCamelCased } from '../../../../rule_schema';

interface NormalizeRuleSourceParams {
immutable: BaseRuleParams['immutable'];
ruleSource: BaseRuleParams['ruleSource'];
}

/*
* Since there's no mechanism to migrate all rules at the same time,
* we cannot guarantee that the ruleSource params is present in all rules.
* This function will normalize the ruleSource param, creating it if does
* not exist in ES, based on the immutable param.
*/
export const normalizeRuleSource = ({
immutable,
ruleSource,
}: NormalizeRuleSourceParams): RuleSourceCamelCased => {
if (!ruleSource) {
const normalizedRuleSource: RuleSourceCamelCased = immutable
? {
type: 'external',
isCustomized: false,
}
: {
type: 'internal',
};

return normalizedRuleSource;
}
return ruleSource;
};

export const normalizeRuleParams = (params: BaseRuleParams) => {
return {
...params,
// Fields to normalize
ruleSource: normalizeRuleSource({
immutable: params.immutable,
ruleSource: params.ruleSource,
}),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ describe('getExportAll', () => {
from: 'now-6m',
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
immutable: false,
rule_source: {
type: 'internal',
},
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
interval: '5m',
rule_id: 'rule-1',
Expand Down Expand Up @@ -280,6 +283,9 @@ describe('getExportAll', () => {
from: 'now-6m',
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
immutable: false,
rule_source: {
type: 'internal',
},
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
interval: '5m',
rule_id: 'rule-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,85 +8,15 @@
import { transformValidateBulkError } from './validate';
import type { BulkError } from '../../routes/utils';
import { getRuleMock } from '../../routes/__mocks__/request_responses';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock';
import { getQueryRuleParams } from '../../rule_schema/mocks';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';

export const ruleOutput = (): RuleResponse => ({
actions: [],
author: ['Elastic'],
building_block_type: 'default',
created_at: '2019-12-13T16:40:33.400Z',
updated_at: '2019-12-13T16:40:33.400Z',
created_by: 'elastic',
description: 'Detecting root and admin users',
enabled: true,
false_positives: [],
from: 'now-6m',
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
immutable: false,
interval: '5m',
rule_id: 'rule-1',
language: 'kuery',
license: 'Elastic License',
output_index: '.siem-signals',
max_signals: 10000,
risk_score: 50,
risk_score_mapping: [],
name: 'Detect Root/Admin Users',
query: 'user.name: root or user.name: admin',
references: ['http://example.com', 'https://example.com'],
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
tags: [],
to: 'now',
type: 'query',
throttle: undefined,
threat: getThreatMock(),
version: 1,
revision: 0,
filters: [
{
query: {
match_phrase: {
'host.name': 'some-host',
},
},
},
],
exceptions_list: getListArrayMock(),
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
meta: {
someMeta: 'someField',
},
note: '# Investigative notes',
timeline_title: 'some-timeline-title',
timeline_id: 'some-timeline-id',
related_integrations: [],
required_fields: [],
response_actions: undefined,
setup: '',
outcome: undefined,
alias_target_id: undefined,
alias_purpose: undefined,
rule_name_override: undefined,
timestamp_override: undefined,
timestamp_override_fallback_disabled: undefined,
namespace: undefined,
data_view_id: undefined,
saved_id: undefined,
alert_suppression: undefined,
investigation_fields: undefined,
});
import { getOutputRuleAlertForRest } from '../../routes/__mocks__/utils';

describe('validate', () => {
describe('transformValidateBulkError', () => {
test('it should do a validation correctly of a rule id', () => {
const ruleAlert = getRuleMock(getQueryRuleParams());
const validatedOrError = transformValidateBulkError('rule-1', ruleAlert);
expect(validatedOrError).toEqual(ruleOutput());
expect(validatedOrError).toEqual(getOutputRuleAlertForRest());
});

test('it should do an in-validation correctly of a rule id', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export const getBaseRuleParams = (): BaseRuleParams => {
description: 'Detecting root and admin users',
falsePositives: [],
immutable: false,
ruleSource: {
type: 'internal',
},
from: 'now-6m',
to: 'now',
severity: 'high',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ describe('buildAlert', () => {
},
],
immutable: false,
rule_source: {
type: 'internal',
},
type: 'query',
language: 'kuery',
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
Expand Down Expand Up @@ -357,6 +360,9 @@ describe('buildAlert', () => {
},
],
immutable: false,
rule_source: {
type: 'internal',
},
type: 'query',
language: 'kuery',
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
Expand Down

0 comments on commit 232a166

Please sign in to comment.