Skip to content

Commit

Permalink
[Security Solution][Detections] Fix GET /api/detection_engine/rules?i…
Browse files Browse the repository at this point in the history
…d={ruleId} endpoint (#122024)

**Ticket:** #120872 (this is a quick fix that [partially](#120872 (comment)) addresses issues described in there)

## Summary

When a Security rule fails on the Alerting Framework level (but not on the Detection Engine level), we return 500 error result from the Detections API which breaks the Rule Details page. This PR fixes that: instead, the API now returns 200 with the rule and its failed execution status.

## Details

When a rule fails on the Alerting Framework level, its `rule.executionStatus` might look something like that:

```
{
  status: 'error',
  lastExecutionDate: new Date('2021-12-24T04:44:44.961Z'),
  error: {
    reason: AlertExecutionStatusErrorReasons.Read,
    message:
      'security_exception: [security_exception] Reason: missing authentication credentials for REST request [/_security/user/_has_privileges], caused by: ""',
  },
}
```

We merge the Framework's `rule.executionStatus` with our custom status based on the legacy `siem-detection-engine-rule-status` saved object, and return the resulting status from multiple endpoints, like `/api/detection_engine/rules/_find`, `/internal/detection_engine/rules/_find_status` and `/api/detection_engine/rules?id={ruleId}`.

The `/api/detection_engine/rules?id={ruleId}` route handler contained incorrect merging logic which, in the case of `rule.executionStatus.status === 'error'`, has been leading to an exception and 500 error result returned from it. This logic has been removed:

```ts
if (currentStatus != null && rule.executionStatus.status === 'error') {
  currentStatus.attributes.lastFailureMessage = `Reason: ${rule.executionStatus.error?.reason} Message: ${rule.executionStatus.error?.message}`;
  currentStatus.attributes.lastFailureAt =
    rule.executionStatus.lastExecutionDate.toISOString();
  currentStatus.attributes.statusDate =
    rule.executionStatus.lastExecutionDate.toISOString();
  currentStatus.attributes.status = RuleExecutionStatus.failed;
}
```

The proper logic of merging rule statuses is still there. Check `transform` -> `transformAlertToRule` -> `internalRuleToAPIResponse` -> `mergeAlertWithSidecarStatus`.

## Screenshots

**Before:** (this is how the page looks like if `/api/detection_engine/rules?id={ruleId}` returns 500)

![](https://puu.sh/IyOuI/878484c991.png)

**After:**

![](https://puu.sh/IyOtY/3db04cecd7.png)

## How to test

I wasn't able to reproduce the original error described in #120872:

```
security_exception: [security_exception] Reason: missing authentication credentials for REST request [/_security/user/_has_privileges], caused by: ""
```

One way of getting it or a similar error from the Framework is by simulating it in the code.

Add this to `x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts`:

```ts
import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common';

rule.executionStatus = {
  status: 'error',
  lastExecutionDate: new Date('2021-12-24T04:44:44.961Z'),
  error: {
    reason: AlertExecutionStatusErrorReasons.Read,
    message:
      'security_exception: [security_exception] Reason: missing authentication credentials for REST request [/_security/user/_has_privileges], caused by: ""',
  },
};
```

Modify `getFailingRules` in `x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts`:

```ts
export const getFailingRules = async (
  ids: string[],
  rulesClient: RulesClient
): Promise<GetFailingRulesResult> => {
  try {
    const errorRules = await Promise.all(
      ids.map(async (id) =>
        rulesClient.resolve({
          id,
        })
      )
    );
    return errorRules
      .map((rule) => {
        rule.executionStatus = {
          status: 'error',
          lastExecutionDate: new Date('2021-12-25T10:44:44.961Z'),
          error: {
            reason: AlertExecutionStatusErrorReasons.Read,
            message:
              'security_exception: [security_exception] Reason: missing authentication credentials for REST request [/_security/user/_has_privileges], caused by: ""',
          },
        };
        return rule;
      })
      .filter((rule) => rule.executionStatus.status === 'error')
      .reduce<GetFailingRulesResult>((acc, failingRule) => {
        return {
          [failingRule.id]: {
            ...failingRule,
          },
          ...acc,
        };
      }, {});
  } catch (exc) {
    if (exc instanceof CustomHttpRequestError) {
      throw exc;
    }
    throw new Error(`Failed to get executionStatus with RulesClient: ${(exc as Error).message}`);
  }
};
```

Please note that `lastExecutionDate` should be greater than the date of the current legacy rule status SO.

### Checklist

- [ ] [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
  • Loading branch information
banderror authored and spong committed Jan 6, 2022
1 parent 3d5383c commit 20a5555
Show file tree
Hide file tree
Showing 5 changed files with 9 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@ import {
DETECTION_ENGINE_RULES_BULK_ACTION,
INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL,
} from '../../../../../common/constants';
import {
RuleAlertType,
IRuleSavedAttributesSavedObjectAttributes,
HapiReadableStream,
IRuleStatusSOAttributes,
} from '../../rules/types';
import { RuleAlertType, HapiReadableStream, IRuleStatusSOAttributes } from '../../rules/types';
import { requestMock } from './request';
import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema';
import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema';
Expand Down Expand Up @@ -469,7 +464,7 @@ export const getMockPrivilegesResult = () => ({
});

export const getEmptySavedObjectsResponse =
(): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({
(): SavedObjectsFindResponse<IRuleStatusSOAttributes> => ({
page: 1,
per_page: 1,
total: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { getIdError, transform } from './utils';
import { buildSiemResponse } from '../utils';

import { readRules } from '../../rules/read_rules';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
// eslint-disable-next-line no-restricted-imports
import { legacyGetRuleActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object';

Expand Down Expand Up @@ -74,14 +73,7 @@ export const readRulesRoute = (
ruleId: rule.id,
spaceId: context.securitySolution.getSpaceId(),
});
if (currentStatus != null && rule.executionStatus.status === 'error') {
currentStatus.attributes.lastFailureMessage = `Reason: ${rule.executionStatus.error?.reason} Message: ${rule.executionStatus.error?.message}`;
currentStatus.attributes.lastFailureAt =
rule.executionStatus.lastExecutionDate.toISOString();
currentStatus.attributes.statusDate =
rule.executionStatus.lastExecutionDate.toISOString();
currentStatus.attributes.status = RuleExecutionStatus.failed;
}

const transformed = transform(
rule,
currentStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { SavedObjectsFindResponse } from 'kibana/server';

import { rulesClientMock } from '../../../../../alerting/server/mocks';
import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusSOAttributes } from '../rules/types';
import { IRuleStatusSOAttributes } from '../rules/types';
import { BadRequestError } from '@kbn/securitysolution-es-utils';
import {
transformBulkError,
Expand Down Expand Up @@ -94,7 +94,7 @@ describe.each([
// Array accessors can result in undefined but
// this is not represented in typescript for some reason,
// https://github.com/Microsoft/TypeScript/issues/11122
const values: SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> = {
const values: SavedObjectsFindResponse<IRuleStatusSOAttributes> = {
page: 0,
per_page: 5,
total: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from 'kibana/server';
import { isString } from 'lodash/fp';
import { truncateMessage } from '../../rule_execution_log';
import { IRuleSavedAttributesSavedObjectAttributes } from '../types';
import { IRuleStatusSOAttributes } from '../types';
// eslint-disable-next-line no-restricted-imports
import { legacyGetRuleReference } from './legacy_utils';

Expand Down Expand Up @@ -45,8 +45,8 @@ export const truncateMessageFields: SavedObjectMigrationFn<Record<string, unknow
* @returns The document migrated with saved object references
*/
export const legacyMigrateRuleAlertIdSOReferences = (
doc: SavedObjectUnsanitizedDoc<IRuleSavedAttributesSavedObjectAttributes>
): SavedObjectSanitizedDoc<IRuleSavedAttributesSavedObjectAttributes> => {
doc: SavedObjectUnsanitizedDoc<IRuleStatusSOAttributes>
): SavedObjectSanitizedDoc<IRuleStatusSOAttributes> => {
const { alertId, ...otherAttributes } = doc.attributes;
const existingReferences = doc.references ?? [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ import { IRuleExecutionLogClient } from '../rule_execution_log/types';

export type RuleAlertType = SanitizedAlert<RuleParams>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IRuleStatusSOAttributes extends Record<string, any> {
export interface IRuleStatusSOAttributes extends SavedObjectAttributes {
statusDate: StatusDate;
lastFailureAt: LastFailureAt | null | undefined;
lastFailureMessage: LastFailureMessage | null | undefined;
Expand Down Expand Up @@ -142,10 +141,6 @@ export interface RuleStatusResponse {
};
}

export interface IRuleSavedAttributesSavedObjectAttributes
extends IRuleStatusSOAttributes,
SavedObjectAttributes {}

export interface IRuleStatusSavedObject {
type: string;
id: string;
Expand Down

0 comments on commit 20a5555

Please sign in to comment.