Skip to content
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

[Case] Detection rules for case #88726

Merged

Conversation

jonathan-buttner
Copy link
Contributor

@jonathan-buttner jonathan-buttner commented Jan 19, 2021

This PR adds support in the case backend for attaching generated alerts from detection rules to a case.

Notable changes:

  • A case can be either an individual case or a collection
    • Individual cases are the same pre 7.12
    • Collection cases contain multiple sub cases and do not have a status
      • Collections can only be deleted using a force flag which defaults to false
  • Sub cases are attached to a case and share all the case information (title, tags, etc) except for status (they each have a status)
  • An individual case can be converted to a collection if it does not have any alerts attached to it
  • Detection rule alerts can be attached to sub cases and will have the type generated_alert (this allows the UI to distinguish between generated alerts and user added alerts)
  • APIs
    • get sub case by ID
    • find sub cases under a specific collection
    • patch multiple sub cases within multiple collections
    • delete sub cases

@jonathan-buttner jonathan-buttner added v8.0.0 release_note:skip Skip the PR/issue when compiling release notes v7.12.0 Team:Threat Hunting Security Solution Threat Hunting Team Feature:Cases Cases feature labels Jan 19, 2021
};

const requestsUpdatingTypeField = requests.filter((req) => req.type === CaseType.collection);
const casesAlertTotals = await Promise.all(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

});

return subCases.saved_objects.reduce((acc, subCase) => {
// log about the sub cases that we couldn't find
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a Todo to notify the user about situations where there is an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's really a TODO to add logging later

let status: CaseStatuses = CaseStatuses.open;
if (alertComment.attributes.associationType === AssociationType.case) {
const id = getID(alertComment, CASE_SAVED_OBJECT);
// We should log if we can't find the status
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another log comment. I agree, maybe a parameter sent with the response errors: ['We were unable to retrieve x, y, and z']

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll add the logging in a follow up PR. I need to pass the plugin's logger around to do it I think.

/**
* This class is a pass through for common case functionality (like creating, get a case).
*/
export class CaseClientHandler implements CaseClient {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean 👌🏾

});

// An alert cannot be attach to a closed case.
if (query.type === CommentType.alert && myCase.attributes.status === CaseStatuses.closed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to keep this check though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it should be in CommentableCase now haha.

commentReq: query,
});

if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary for this PR, but could be helpful to just have all these checks encapsulated in functions like isAlert() isCollection() isGeneratedAlert etc... especially if the criteria for any of these changes in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. I'll add it to the refactoring list 👍

import { CaseClientGetAlertsResponse } from './alerts/types';

export interface CaseClientCreate {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More of a question. We seem to be passing in the post and patch request types while the others are defined here? Is there a reason they're separated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that might have been from more of a convenience thing where some parameter types were defined already by the io-ts types and we just pass those fields around where as others don't strictly align with an io-ts type.

}): Promise<NewCommentResp> {
if (commentReq.type === CommentType.alert) {
if (this.status === CaseStatuses.closed) {
throw Boom.badRequest('Alert cannot be attached to a closed case');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore my previous comment about the removed check 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha

if (alert.id) {
const totalAlerts = acc.get(alert.id);
if (totalAlerts !== undefined) {
acc.set(alert.id, totalAlerts + countAlerts(alertsInfo));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can cache countAlerts const alertCount = countAlerts(alertsInfo) and use that on 105 maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will update

);
};

export const isCommentAlert = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice 👍🏾, this was the idea for all the other types of checks as well that I mentioned earlier

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah got it.

Copy link
Member

@cnasikas cnasikas Feb 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I see that the type is not used anywhere. Maybe we can remove it.

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
securitySolution 2187 2189 +2

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
securitySolution 7.5MB 7.5MB +796.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
securitySolution 234.0KB 237.2KB +3.1KB

Saved Objects .kibana field count

Every field in each saved object type adds overhead to Elasticsearch. Kibana needs to keep the total field count below Elasticsearch's default limit of 1000 fields. Only specify field mappings for the fields you wish to search on or query. See https://www.elastic.co/guide/en/kibana/master/development-plugin-saved-objects.html#_mappings

id before after diff
cases 40 41 +1
cases-comments 20 21 +1
cases-sub-case - 17 +17
total +19

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

});
}

decodeCommentRequest(comment);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment for another PR: TypeScript thinks that the comment is of type user and not alert | user.

} = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query });

if (
(newComment.attributes.type === CommentType.alert ||
Copy link
Member

@cnasikas cnasikas Feb 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you throw an error before if comment.type !== CommentType.generatedAlert it should be impossible the new comment to be of type anything other than CommentType.generatedAlert. Never hurts to double check though 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha good point.

});

return CollectWithSubCaseResponseRt.encode({
subCase: flattenSubCaseSavedObject({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we return subCases: [flattenSubCaseSavedObject(...)]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I talked with Xavier about this and we thought for now we'd keep it as a single entry rather than an array of sub cases for the add comment response.

response: kibanaResponseFactory,
scopedClusterClient,
// we might want the user information to be passed as part of the action request
user: nullUser,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found out that you can scopedClusterClient.security.getUser(). Just for future reference in case we need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh interesting! I'll add it to the list of stuff to refactor 👍

): comment is ContextTypeGeneratedAlertType => {
return (
comment.type === CommentType.generatedAlert &&
'alerts' in comment &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the type check is enough, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just being extra careful since we need to access the alerts field.

const args = query
? {
caseService,
Copy link
Member

@cnasikas cnasikas Feb 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

const args = {caseService, client, id, options: { 
	page: defaultPage,
	perPage: defaultPerPage,
	sortField: 'created_at',
	...(query ? query : {})
}, associationType} 


await Promise.all(
subCasesForCaseIds.saved_objects.map((subCaseSO) =>
caseService.deleteSubCase(client, subCaseSO.id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For another PR: I think maybe is better to first delete the subcase and then the comments if the corresponding subcase has been deleted successfully. This will prevent a use case where all the comments of a subcase have been deleted and the deletion of the subcase faild.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point

/**
* Builds an AlertInfo object accumulating the alert IDs and indices for the passed in alerts.
*/
export const getAlertIndicesAndIDs = (comments: CommentAttributes[] | undefined): AlertInfo => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: getAlertIndicesAndIDs and getAlertIndicesAndIDsFromSO are almost identical. They can be combined in one function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently I don't even use getAlertIndicesAndIDsFromSO anywhere haha 🤦 I'll remove it in the next PR.


/**
* This is used to test if the posted comment is an generated alert. A generated alert will have one or many alerts.
* An alert is essentially an object with a _id field. This differs from a regular attached alert because the _id is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: generated alerts do not have an _id field any more. Fields of alert and generated_alert is the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep thanks.

@@ -5,24 +5,26 @@
* 2.0.
*/

import _ from 'lodash';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Better to use import { isEmpty } from lodash/fp

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member

@cnasikas cnasikas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good PR! The code is very clean and readable. Very important for the complexity of this feature. Thank you for the effort you put on this. Great job!

I left some nit/code enhancement comments. Let's work together on another PR to do them.

Tests I preformed:

  • Add a user comment to a collection ✅
  • Add an alert to a collection: 400 error: Alert cannot be attached to a collection case ✅
  • Add a generated alert to a collection. The generated alert is added to the subcase and not to the collection ✅
  • Attach an alert and a user comment to a subcase ✅
  • Attach a generated alert to a sub case using the cases route: 404 error, case not found ✅.
  • Add an alert and user comment to an individual case ✅
  • Add generated alert to an individual case: 400 error ✅
  • Add connector to an individual case and to a collection case ✅
  • Add connector to a subcase: 406: All update fields are identical to current version. ✅
  • Push individual case to external service with alerts & user comments ✅
  • Push subcase to external service using the case route: 404 error ✅
  • Patching a subcase using the case route: 404 error ✅
  • Push a collection to an external service ✅
  • Update the status of an individual case with sync turned off does not update the alerts attached ✅
  • Updating the status of an individual case with sync turned on updates the alerts correctly ✅
  • Updating the status of a collection is not allowed ✅
  • Update the status of a subcase ✅
  • Case collection: Turn sync off, change the status of a subcase, turn sync on, all alerts attach to the subcase change their status accordingly ✅
  • Attach a generated_alert to a collection creates another sub case if all subcases are closed ✅
  • Attach a generated_alert to a collection does NOT creates another sub case if there is an open subcase ✅
  • Case collection, sync alert is off: two subcases, one in-progress, one closed. Turn sync on. All alerts are updated accordingly ✅.
  • Two subcases: one in-progess, one open. Attaching generated_alerts to the collection is added to the most recent subcase ✅
  • Update collection & individual case fields like title, tags, comments, connector fields etc from the UI ✅
  • Push updates to external service correctly ✅
  • Creates a case correctly from the UI. ✅
  • Converting a collection to individual throws an error ✅
  • Converting an individual case to a collection when having alerts attached throws an error ✅

Notes:

  • If I am working on a collection case (in the UI) and a subcase is generate behind the scene I have to refresh for me to be able to do a change. One day we have to have an observable/websocket to watch for cases updates and update on the fly as Github does.
  • We should use excess in the future when patching subcases & cases.

@jonathan-buttner jonathan-buttner merged commit f8b8d5b into elastic:master Feb 12, 2021
jonathan-buttner added a commit that referenced this pull request Feb 12, 2021
* Adding type field to client

* Removing context and adding association type

* Handle alerts from multiple indices

* Adding flow for adding a sub case

* Making progress on creating alerts from rules

* Refactored add comment to handle case and sub case

* Starting sub case API and refactoring of case client

* Fleshing out find cases

* Finished the find cases api

* Filtering comments by association type

* Fixing tests and types

* Updating snapshots

* Cleaning up comment references

* Working unit tests

* Fixing integration tests and got ES to work

* Unit tests and api integration test working

* Refactoring find and get_status

* Starting patch, and update

* script for sub cases

* Removing converted_by and fixing type errors

* Adding docs for script

* Removing converted_by and fixing integration test

* Adding sub case id to comment routes

* Removing stringify comparison

* Adding delete api and tests

* Updating license

* missed license files

* Integration tests passing

* Adding more tests for sub cases

* Find int tests, scoped client, patch sub user actions

* fixing types and call cluster

* fixing get sub case param issue

* Adding user actions for sub cases

* Preventing alerts on collections and refactoring user

* Allowing type to be updated for ind cases

* Refactoring and writing tests

* Fixing sub case status filtering

* Adding more tests not allowing gen alerts patch

* Working unit tests

* Push to connector gets all sub case comments

* Writing more tests and cleaning up

* Updating push functionality for generated alerts and sub cases

* Adding comment about updating collection sync

* Refactoring update alert status for sub cases and removing request and cleaning up

* Addressing alert service feedback

* Fixing sub case sync bug and cleaning up comment types

* Addressing more feedback

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Kibana Machine <[email protected]>
spalger added a commit to spalger/kibana that referenced this pull request Feb 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Cases Cases feature release_note:feature Makes this part of the condensed release notes Team:Threat Hunting Security Solution Threat Hunting Team v7.12.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants