Skip to content

Commit

Permalink
[SECURITY SOLUTION] Get case ids from alert ids (#98702)
Browse files Browse the repository at this point in the history
* wip to get caseIds with alertId

* make work the API with the latest aggs from SO :)

* review

* change logic to re-do aggregation if we think that we are missing data

* Integration tests

Co-authored-by: Jonathan Buttner <[email protected]>
  • Loading branch information
XavierM and jonathan-buttner authored May 5, 2021
1 parent 669be33 commit 5de608c
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { schema as s, ObjectType } from '@kbn/config-schema';
* Currently supported:
* - filter
* - histogram
* - nested
* - terms
*
* Not implemented:
Expand All @@ -32,7 +33,6 @@ import { schema as s, ObjectType } from '@kbn/config-schema';
* - ip_range
* - missing
* - multi_terms
* - nested
* - parent
* - range
* - rare_terms
Expand All @@ -42,6 +42,7 @@ import { schema as s, ObjectType } from '@kbn/config-schema';
* - significant_text
* - variable_width_histogram
*/

export const bucketAggsSchemas: Record<string, ObjectType> = {
filter: s.object({
term: s.recordOf(s.string(), s.oneOf([s.string(), s.boolean(), s.number()])),
Expand Down Expand Up @@ -71,6 +72,9 @@ export const bucketAggsSchemas: Record<string, ObjectType> = {
})
),
}),
nested: s.object({
path: s.string(),
}),
terms: s.object({
field: s.maybe(s.string()),
collect_mode: s.maybe(s.string()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ const mockMappings = {
updated_at: {
type: 'date',
},
references: {
type: 'nested',
properties: {
id: 'keyword',
},
},
foo: {
properties: {
title: {
Expand Down Expand Up @@ -182,6 +188,40 @@ describe('validateAndConvertAggregations', () => {
});
});

it('validates a nested root aggregations', () => {
expect(
validateAndConvertAggregations(
['alert'],
{
aggName: {
nested: {
path: 'alert.references',
},
aggregations: {
aggName2: {
terms: { field: 'alert.references.id' },
},
},
},
},
mockMappings
)
).toEqual({
aggName: {
nested: {
path: 'references',
},
aggregations: {
aggName2: {
terms: {
field: 'references.id',
},
},
},
},
});
});

it('rewrites type attributes when valid', () => {
const aggregations: AggsMap = {
average: {
Expand Down Expand Up @@ -428,4 +468,50 @@ describe('validateAndConvertAggregations', () => {
`"[someAgg.aggs.nested.max.script]: definition for this key is missing"`
);
});

it('throws an error when trying to access a property via {type}.{type}.attributes.{attr}', () => {
expect(() => {
validateAndConvertAggregations(
['alert'],
{
aggName: {
cardinality: {
field: 'alert.alert.attributes.actions.group',
},
aggs: {
aggName: {
max: { field: 'alert.alert.attributes.actions.group' },
},
},
},
},
mockMappings
);
}).toThrowErrorMatchingInlineSnapshot(
'"[aggName.cardinality.field] Invalid attribute path: alert.alert.attributes.actions.group"'
);
});

it('throws an error when trying to access a property via {type}.{type}.{attr}', () => {
expect(() => {
validateAndConvertAggregations(
['alert'],
{
aggName: {
cardinality: {
field: 'alert.alert.actions.group',
},
aggs: {
aggName: {
max: { field: 'alert.alert.actions.group' },
},
},
},
},
mockMappings
);
}).toThrowErrorMatchingInlineSnapshot(
'"[aggName.cardinality.field] Invalid attribute path: alert.alert.actions.group"'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,13 @@ const validateAggregations = (
aggregations: Record<string, estypes.AggregationContainer>,
context: ValidationContext
) => {
return Object.entries(aggregations).reduce((memo, [aggrName, aggrContainer]) => {
memo[aggrName] = validateAggregation(aggrContainer, childContext(context, aggrName));
return memo;
}, {} as Record<string, estypes.AggregationContainer>);
return Object.entries(aggregations).reduce<Record<string, estypes.AggregationContainer>>(
(memo, [aggrName, aggrContainer]) => {
memo[aggrName] = validateAggregation(aggrContainer, childContext(context, aggrName));
return memo;
},
{}
);
};

/**
Expand Down Expand Up @@ -93,15 +96,18 @@ const validateAggregationContainer = (
container: estypes.AggregationContainer,
context: ValidationContext
) => {
return Object.entries(container).reduce((memo, [aggName, aggregation]) => {
if (aggregationKeys.includes(aggName)) {
return memo;
}
return {
...memo,
[aggName]: validateAggregationType(aggName, aggregation, childContext(context, aggName)),
};
}, {} as estypes.AggregationContainer);
return Object.entries(container).reduce<estypes.AggregationContainer>(
(memo, [aggName, aggregation]) => {
if (aggregationKeys.includes(aggName)) {
return memo;
}
return {
...memo,
[aggName]: validateAggregationType(aggName, aggregation, childContext(context, aggName)),
};
},
{}
);
};

const validateAggregationType = (
Expand Down Expand Up @@ -143,7 +149,7 @@ const validateAggregationStructure = (
* },
* ```
*/
const attributeFields = ['field'];
const attributeFields = ['field', 'path'];
/**
* List of fields that have a Record<attribute path, value> as value
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ export const isRootLevelAttribute = (
allowedTypes: string[]
): boolean => {
const splits = attributePath.split('.');
if (splits.length !== 2) {
if (splits.length <= 1) {
return false;
}

const [type, fieldName] = splits;
if (allowedTypes.includes(fieldName)) {
const [type, firstPath, ...otherPaths] = splits;
if (allowedTypes.includes(firstPath)) {
return false;
}
return allowedTypes.includes(type) && fieldDefined(indexMapping, fieldName);
return (
allowedTypes.includes(type) && fieldDefined(indexMapping, [firstPath, ...otherPaths].join('.'))
);
};

/**
Expand All @@ -45,7 +47,8 @@ export const isRootLevelAttribute = (
* ```
*/
export const rewriteRootLevelAttribute = (attributePath: string) => {
return attributePath.split('.')[1];
const [, ...attributes] = attributePath.split('.');
return attributes.join('.');
};

/**
Expand Down
16 changes: 16 additions & 0 deletions x-pack/plugins/cases/common/api/cases/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ import * as rt from 'io-ts';

import { UserRT } from '../user';

const BucketsAggs = rt.array(
rt.type({
key: rt.string,
})
);

export const GetCaseIdsByAlertIdAggsRt = rt.type({
references: rt.type({
doc_count: rt.number,
caseIds: rt.type({
buckets: BucketsAggs,
}),
}),
});

/**
* this is used to differentiate between an alert attached to a top-level case and a group of alerts that should only
* be attached to a sub case. The reason we need this is because an alert group comment will have references to both a case and
Expand Down Expand Up @@ -123,3 +138,4 @@ export type CommentPatchRequest = rt.TypeOf<typeof CommentPatchRequestRt>;
export type CommentPatchAttributes = rt.TypeOf<typeof CommentPatchAttributesRt>;
export type CommentRequestUserType = rt.TypeOf<typeof ContextTypeUserRt>;
export type CommentRequestAlertType = rt.TypeOf<typeof AlertCommentRequestRt>;
export type GetCaseIdsByAlertIdAggs = rt.TypeOf<typeof GetCaseIdsByAlertIdAggsRt>;
2 changes: 2 additions & 0 deletions x-pack/plugins/cases/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const CASE_STATUS_URL = `${CASES_URL}/status`;
export const CASE_TAGS_URL = `${CASES_URL}/tags`;
export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`;

export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}`;

/**
* Action routes
*/
Expand Down
48 changes: 48 additions & 0 deletions x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts
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 { schema } from '@kbn/config-schema';
import Boom from '@hapi/boom';

import { RouteDeps } from '../../types';
import { wrapError } from '../../utils';
import { CASE_ALERTS_URL } from '../../../../../common/constants';

export function initGetCaseIdsByAlertIdApi({ caseService, router, logger }: RouteDeps) {
router.get(
{
path: CASE_ALERTS_URL,
validate: {
params: schema.object({
alert_id: schema.string(),
}),
},
},
async (context, request, response) => {
try {
const alertId = request.params.alert_id;
if (alertId == null || alertId === '') {
throw Boom.badRequest('The `alertId` is not valid');
}
const client = context.core.savedObjects.client;
const caseIds = await caseService.getCaseIdsByAlertId({
client,
alertId,
});

return response.ok({
body: caseIds,
});
} catch (error) {
logger.error(
`Failed to retrieve case ids for this alert id: ${request.params.alert_id}: ${error}`
);
return response.customError(wrapError(error));
}
}
);
}
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/server/routes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { initPatchSubCasesApi } from './cases/sub_case/patch_sub_cases';
import { initFindSubCasesApi } from './cases/sub_case/find_sub_cases';
import { initDeleteSubCasesApi } from './cases/sub_case/delete_sub_cases';
import { ENABLE_CASE_CONNECTOR } from '../../../common/constants';
import { initGetCaseIdsByAlertIdApi } from './cases/alerts/get_cases';

/**
* Default page number when interacting with the saved objects API.
Expand Down Expand Up @@ -86,4 +87,6 @@ export function initCaseApi(deps: RouteDeps) {
initGetCasesStatusApi(deps);
// Tags
initGetTagsApi(deps);
// Alerts
initGetCaseIdsByAlertIdApi(deps);
}
Loading

0 comments on commit 5de608c

Please sign in to comment.