-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Security Solution][Detections] Optimize rule fetching endpoints [WIP] #90639
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,9 +15,11 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; | |
import { findRules } from '../../rules/find_rules'; | ||
import { transformValidateFindAlerts } from './validate'; | ||
import { transformError, buildSiemResponse } from '../utils'; | ||
import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; | ||
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; | ||
import { bulkFetchRuleActionsOfManyRules, getRuleActionsSavedObject, RulesActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; | ||
import { RuleStatusSavedObjectsClient, ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; | ||
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; | ||
import { IScopedClusterClient, ElasticsearchClient, SavedObjectsFindResult } from '../../../../../../../../src/core/server'; | ||
import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusSOAttributes } from '../../rules/types'; | ||
|
||
export const findRulesRoute = (router: SecuritySolutionPluginRouter) => { | ||
router.get( | ||
|
@@ -43,12 +45,13 @@ export const findRulesRoute = (router: SecuritySolutionPluginRouter) => { | |
const { query } = request; | ||
const alertsClient = context.alerting?.getAlertsClient(); | ||
const savedObjectsClient = context.core.savedObjects.client; | ||
const elasticsearchClient = context.core.elasticsearch.client; | ||
|
||
if (!alertsClient) { | ||
return siemResponse.error({ statusCode: 404 }); | ||
} | ||
|
||
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); | ||
// const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); | ||
const rules = await findRules({ | ||
alertsClient, | ||
perPage: query.per_page, | ||
|
@@ -59,45 +62,94 @@ export const findRulesRoute = (router: SecuritySolutionPluginRouter) => { | |
fields: query.fields, | ||
}); | ||
|
||
if (rules.data.length === 0) { | ||
const [validated, errors] = transformValidateFindAlerts(rules, [], []); | ||
if (errors != null) { | ||
return siemResponse.error({ statusCode: 500, body: errors }); | ||
} else { | ||
return response.ok({ body: validated ?? {} }); | ||
} | ||
} | ||
|
||
const ruleIds = rules.data.map((rule) => rule.id); | ||
|
||
// // if any rules attempted to execute but failed before the rule executor is called, | ||
// // an execution status will be written directly onto the rule via the kibana alerting framework, | ||
// // which we are filtering on and will write a failure status | ||
// // for any rules found to be in a failing state into our rule status saved objects | ||
// const failingRules = rules.data.filter( | ||
// (rule) => rule.executionStatus != null && rule.executionStatus.status === 'error' | ||
// ); | ||
|
||
// const ruleStatuses = await Promise.all( | ||
// rules.data.map(async (rule) => { | ||
// const results = await ruleStatusClient.find({ | ||
// perPage: 1, | ||
// sortField: 'statusDate', | ||
// sortOrder: 'desc', | ||
// search: rule.id, | ||
// searchFields: ['alertId'], | ||
// }); | ||
// const failingRule = failingRules.find((badRule) => badRule.id === rule.id); | ||
// if (failingRule != null) { | ||
// if (results.saved_objects.length > 0) { | ||
// results.saved_objects[0].attributes.status = 'failed'; | ||
// results.saved_objects[0].attributes.lastFailureAt = failingRule.executionStatus.lastExecutionDate.toISOString(); | ||
// } | ||
// } | ||
// return results; | ||
// }) | ||
// ); | ||
|
||
// const ruleActions = await Promise.all( | ||
// rules.data.map(async (rule) => { | ||
// const results = await getRuleActionsSavedObject({ | ||
// savedObjectsClient, | ||
// ruleAlertId: rule.id, | ||
// }); | ||
|
||
// return results; | ||
// }) | ||
// ); | ||
|
||
const [ruleActions, ruleStatuses] = await Promise.all([ | ||
bulkFetchRuleActionsOfManyRules({ savedObjectsClient, ruleIds }), | ||
bulkFetchLatestStatusesOfManyRules(elasticsearchClient.asCurrentUser, ruleIds), | ||
]); | ||
|
||
// if any rules attempted to execute but failed before the rule executor is called, | ||
// an execution status will be written directly onto the rule via the kibana alerting framework, | ||
// which we are filtering on and will write a failure status | ||
// for any rules found to be in a failing state into our rule status saved objects | ||
const failingRules = rules.data.filter( | ||
(rule) => rule.executionStatus != null && rule.executionStatus.status === 'error' | ||
); | ||
rules.data.forEach((rule) => { | ||
const isRuleFailing = | ||
rule.executionStatus != null && rule.executionStatus.status === 'error'; | ||
|
||
const ruleStatuses = await Promise.all( | ||
rules.data.map(async (rule) => { | ||
const results = await ruleStatusClient.find({ | ||
perPage: 1, | ||
sortField: 'statusDate', | ||
sortOrder: 'desc', | ||
search: rule.id, | ||
searchFields: ['alertId'], | ||
}); | ||
const failingRule = failingRules.find((badRule) => badRule.id === rule.id); | ||
if (failingRule != null) { | ||
if (results.saved_objects.length > 0) { | ||
results.saved_objects[0].attributes.status = 'failed'; | ||
results.saved_objects[0].attributes.lastFailureAt = failingRule.executionStatus.lastExecutionDate.toISOString(); | ||
} | ||
if (isRuleFailing) { | ||
const ruleStatus = ruleStatuses.find((status) => status.alertId === rule.id); | ||
if (ruleStatus != null) { | ||
ruleStatus.status = 'failed'; | ||
ruleStatus.lastFailureAt = rule.executionStatus.lastExecutionDate.toISOString(); | ||
} | ||
return results; | ||
}) | ||
); | ||
const ruleActions = await Promise.all( | ||
rules.data.map(async (rule) => { | ||
const results = await getRuleActionsSavedObject({ | ||
savedObjectsClient, | ||
ruleAlertId: rule.id, | ||
}); | ||
|
||
return results; | ||
}) | ||
); | ||
} | ||
}); | ||
|
||
const ruleStatusesFindResultMock = ruleStatuses.map((status) => { | ||
return { | ||
saved_objects: [{ attributes: status }] as Array< | ||
SavedObjectsFindResult<IRuleSavedAttributesSavedObjectAttributes> | ||
>, | ||
total: 0, | ||
per_page: 0, | ||
page: 0, | ||
}; | ||
}); | ||
|
||
const [validated, errors] = transformValidateFindAlerts(rules, ruleActions, ruleStatuses); | ||
const [validated, errors] = transformValidateFindAlerts( | ||
rules, | ||
ruleActions, | ||
ruleStatusesFindResultMock | ||
); | ||
if (errors != null) { | ||
return siemResponse.error({ statusCode: 500, body: errors }); | ||
} else { | ||
|
@@ -113,3 +165,94 @@ export const findRulesRoute = (router: SecuritySolutionPluginRouter) => { | |
} | ||
); | ||
}; | ||
|
||
// const bulkFetchRuleActionsOfManyRules = async ( | ||
// client: ElasticsearchClient, | ||
// ruleIds: string[] | ||
// ): Promise<Array<RulesActionsSavedObject | null>> => { | ||
// if (ruleIds.length === 0) { | ||
// return []; | ||
// } | ||
|
||
// const response = await client.search({ | ||
// index: '.kibana', | ||
// ignore_unavailable: true, | ||
// size: ruleIds.length, | ||
// body: { | ||
// size: ruleIds.length, | ||
// query: { | ||
// bool: { | ||
// filter: [ | ||
// { term: { type: 'siem-detection-engine-rule-actions' } }, | ||
// { terms: { 'siem-detection-engine-rule-actions.ruleAlertId': ruleIds } }, | ||
// ], | ||
// }, | ||
// }, | ||
// }, | ||
// }); | ||
|
||
// const actionsHits = response.body.hits?.hits ?? []; | ||
// const actions = actionsHits.filter(Boolean).map((hit: any) => { | ||
// const id = hit._id; | ||
// const attributes = hit._source?.['siem-detection-engine-rule-actions'] ?? {}; | ||
// return { id, ...attributes }; | ||
// }); | ||
|
||
// return actions.filter(Boolean) as RulesActionsSavedObject[]; | ||
// }; | ||
|
||
const bulkFetchLatestStatusesOfManyRules = async ( | ||
client: ElasticsearchClient, | ||
ruleIds: string[] | ||
): Promise<IRuleSavedAttributesSavedObjectAttributes[]> => { | ||
// This is not only a performance optimization. If ids is empty, the search | ||
// request to Elasticsearch using terms aggregation will throw a 400 error, | ||
// which is not what we want here. | ||
if (ruleIds.length === 0) { | ||
return []; | ||
} | ||
|
||
const response = await client.search({ | ||
index: '.kibana', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: Kibana index name should be retrieved from the config. |
||
ignore_unavailable: true, | ||
size: 0, | ||
body: { | ||
size: 0, | ||
query: { | ||
bool: { | ||
filter: [ | ||
{ term: { type: 'siem-detection-engine-rule-status' } }, | ||
{ terms: { 'siem-detection-engine-rule-status.alertId': ruleIds } }, | ||
], | ||
}, | ||
}, | ||
aggs: { | ||
rules: { | ||
terms: { | ||
field: 'siem-detection-engine-rule-status.alertId', | ||
size: ruleIds.length, | ||
}, | ||
aggs: { | ||
latest_status: { | ||
top_hits: { | ||
size: 1, | ||
sort: [{ 'siem-detection-engine-rule-status.statusDate': { order: 'desc' } }], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
const ruleBuckets = response.body.aggregations?.rules?.buckets ?? []; | ||
const statusHits = ruleBuckets.map((bucket: any) => { | ||
const [firstHit] = bucket?.latest_status?.hits?.hits ?? []; | ||
return firstHit ?? null; | ||
}); | ||
const statuses = statusHits.map((hit: any) => { | ||
return hit?._source?.['siem-detection-engine-rule-status'] ?? null; | ||
}); | ||
|
||
return statuses.filter(Boolean) as IRuleSavedAttributesSavedObjectAttributes[]; | ||
}; | ||
Comment on lines
+204
to
+258
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please review this from the security standpoint (access privileges, isolation (spaces), error handling, potential issues, etc.). Note that:
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[ | ||
null, | ||
{ | ||
"id": "fd61c550-66e1-11eb-89d7-93d66aca027c", | ||
"actions": [ | ||
{ | ||
"group": "default", | ||
"id": "7a8a4990-66e1-11eb-89d7-93d66aca027c", | ||
"params": { | ||
"body": "Rule:\n{{context.rule.name}}\n{{context.rule.id}}\n\nAlert:\n{{alertName}}\n{{alertId}}\n{{alertActionGroupName}}\n\nTags:\n{{tags}}\n\n{{date}}" | ||
}, | ||
"action_type_id": ".webhook" | ||
} | ||
], | ||
"alertThrottle": null, | ||
"ruleThrottle": "rule" | ||
} | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please review this from the security standpoint (access privileges, isolation (spaces), error handling, potential issues, etc.).
.kibana
index viaelasticsearchClient.asCurrentUser
, or privileges to.kibana
are not guaranteed to all users by default?asInternalUser
. Would it be safe in this particular case?