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

[Security Solution] Recalculate isCustomized when bulk editing rules #190041

Merged
merged 1 commit into from
Aug 29, 2024

Conversation

xcrzx
Copy link
Contributor

@xcrzx xcrzx commented Aug 7, 2024

Resolves: #187706

Summary

Added the isCustomized field recalculation after a bulk edit operation on rules as part of the rules customization epic.

Background
The isCustomized field is a rule parameter indicating if a prebuilt Elastic rule has been modified by a user. This field is extensively used in the prebuilt rule upgrade workflow. It's essential to ensure any rule modification operation recalculates this field to keep its value in sync with the rule content. Most of the rule CRUD operations were already covered in a previous PR: Calculate and save ruleSource.isCustomized in API endpoint handlers. This PR addresses the remaining bulk rule modification operations performed using the rulesClient.bulkEdit method.

rulesClient.bulkEdit changes

The isCustomized calculation is based on the entire rule object (i.e., rule params and attributes) and should be performed after all bulk operations have been applied to the rule - after operations and paramsModifier. To support this, I changed the paramsModifier to accept entire rule object:

export type ParamsModifier<Params extends RuleParams> = (
-  params: Params
+  rule: Rule<Params>
) => Promise<ParamsModifierResult<Params>>;

Security Solution Bulk Endpoint changes

The /api/detection_engine/rules/_bulk_action endpoint now handles bulk edit actions a bit differently. Previously, most of the bulk action was delegated to the rules client. Now, we need to do some preparatory work:

  1. Fetch the affected rules in memory first, regardless of whether we received a query or rule IDs as input (previously delegated to Alerting).
  2. Identify all prebuilt rules among the fetched rules.
  3. Fetch base versions of the prebuilt rules.
  4. Provide base versions to ruleModifier for the isCustomized calculation.

These changes add a few extra roundtrips to Elasticsearch and make the bulk edit endpoint less efficient than before. However, this seems justified given the added functionality of the customization epic. In the future, we might consider optimizing to reduce the number of database requests. Ideally, for Security Solution use cases, we would need a more generic method than bulkEdit, such as bulkUpdate, allowing us to implement any required rule update logic fully on the solution side.

@xcrzx xcrzx added release_note:skip Skip the PR/issue when compiling release notes Team:Endpoint Response Endpoint Response Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. v6.8.16 Team:Detection Rule Management Security Detection Rule Management Team Feature:Prebuilt Detection Rules Security Solution Prebuilt Detection Rules area labels Aug 7, 2024
@xcrzx xcrzx self-assigned this Aug 7, 2024
@banderror banderror added v8.16.0 and removed v6.8.16 labels Aug 9, 2024
@pmuellr
Copy link
Member

pmuellr commented Aug 12, 2024

Just a note that this seems to be another case of an asset type (rule in this case) wanting some influence in API calls (verification, modification of data) that they are not involved in. We have a more general issue open for this, here: allow connector/rule types to provide hooks/feedback during create/update/delete calls #106724

@banderror banderror requested review from mikecote and JiaweiWu August 13, 2024 10:25
@xcrzx xcrzx force-pushed the is-customized-bulk branch from 54ab88d to 5287557 Compare August 15, 2024 12:57
@@ -505,7 +505,7 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>(
validateScheduleInterval(context, updatedRule.schedule.interval, ruleType.id, rule.id);

const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier
? await paramsModifier(updatedRule.params)
? await paramsModifier(updatedRule as Rule<Params>)
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'm not sure how to implement that without type casting. There are several similar Rule types that aren't assignable to each other due to subtle differences.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine to cast here since we're in the process of migrating and consolidating our rule types. But I would just put a TODO comment here

@xcrzx xcrzx force-pushed the is-customized-bulk branch 3 times, most recently from 695d101 to ded507e Compare August 27, 2024 12:33
@xcrzx xcrzx marked this pull request as ready for review August 27, 2024 12:36
@xcrzx xcrzx requested review from a team as code owners August 27, 2024 12:36
@xcrzx xcrzx requested a review from maximpn August 27, 2024 12:36
@elasticmachine
Copy link
Contributor

Pinging @elastic/security-solution (Team: SecuritySolution)

@elasticmachine
Copy link
Contributor

Pinging @elastic/security-detection-rule-management (Team:Detection Rule Management)

Copy link
Member

@jbudz jbudz left a comment

Choose a reason for hiding this comment

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

ftr_configs.yml

@xcrzx xcrzx requested review from jpdjere and banderror and removed request for maximpn August 27, 2024 13:17
Copy link
Contributor

@JiaweiWu JiaweiWu left a comment

Choose a reason for hiding this comment

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

Management experiences team LGTM!

@@ -505,7 +505,7 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>(
validateScheduleInterval(context, updatedRule.schedule.interval, ruleType.id, rule.id);

const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier
? await paramsModifier(updatedRule.params)
? await paramsModifier(updatedRule as Rule<Params>)
Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine to cast here since we're in the process of migrating and consolidating our rule types. But I would just put a TODO comment here

Comment on lines +23 to +31
testConfig.kbnTestServer.serverArgs = testConfig.kbnTestServer.serverArgs.map((arg: string) => {
// Override the default value of `--xpack.securitySolution.enableExperimental` to enable the prebuilt rules customization feature
if (arg.includes('--xpack.securitySolution.enableExperimental')) {
return `--xpack.securitySolution.enableExperimental=${JSON.stringify([
'prebuiltRulesCustomizationEnabled',
])}`;
}
return arg;
});
Copy link
Contributor

@jpdjere jpdjere Aug 28, 2024

Choose a reason for hiding this comment

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

This worked for me:

export default async function ({ readConfigFile }: FtrConfigProviderContext) {
  const functionalConfig = await readConfigFile(
    require.resolve('../../../../../../../config/ess/config.base.trial')
  );

  const testConfig = {
    ...functionalConfig.getAll(),
    testFiles: [require.resolve('..')],
    junit: {
      reportName:
        'Rules Management - Prebuilt Rule Customization Integration Tests - ESS Env - Trial License',
    },
    kbnTestServer: {
      ...functionalConfig.get('kbnTestServer'),
      serverArgs: [
        ...functionalConfig.get('kbnTestServer.serverArgs'),
        `--xpack.securitySolution.enableExperimental=${JSON.stringify([
          'prebuiltRulesCustomizationEnabled',
        ])}`,
      ],
    },
  };

  return testConfig;
}

I would remove the additional check to simplify this config file, unless you still see it as necessary for some other reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With this implementation, a new --xpack.securitySolution.enableExperimental flag is added to the config. I'm not entirely sure how this will be interpreted by the Kibana server since the same flag from the base config will also be present. The server will be started with something like this:

node path/to/kibana --xpack.securitySolution.enableExperimental=valuesFromBase --xpack.securitySolution.enableExperimental=prebuiltRulesCustomizationEnabled

Therefore, I'd prefer to have the flag entirely replaced for more transparency.

Copy link
Contributor

@jpdjere jpdjere left a comment

Choose a reason for hiding this comment

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

Overall LGTM ✅ Thanks for finding a solution for this!

Just an observation + question. I stressed-tested bulk editing:

  • all 1195 Elastic rules = ✅ good performance
  • all 1195 Elastic rules + their 1195 duplicates = ✅ good performance

When I duplicated the Elastic rules again for a total of 3585 rules, that's when the endpoint started to fail.

First failed with an internal ES error:

 "failed_shards": [
        {
            "shard": 0,
            "index": ".kibana_alerting_cases_8.16.0_001",
            "node": "2n4sV2RASoa28WZrMu8vaw",
            "reason": {
                "type": "too_many_nested_clauses",
                "reason": "Query contains too many nested clauses; maxClauseCount is set to 6144"
            }
        }
    ],

So, I add a promise pool to the edit request:

                const bulkEditOutcome = await initPromisePool({
                  concurrency: 1000,
                  items: rules,
                  executor: async (rule) => {
                    const bulkEditResult = await bulkEditRules({
                      actionsClient,
                      rulesClient,
                      prebuiltRuleAssetClient,
                      rules,
                      actions: body.edit,
                      mlAuthz,
                      experimentalFeatures: config.experimentalFeatures,
                    });
                    return bulkEditResult;
                  },
                  abortSignal: abortController.signal,
                });

But that just caused the requests to eventually time out. Which I kind of expected. What do you think? I mean, this is the same scenario we're having for importing large amounts of rules, and it seems that the only "proper" way out of this is replacing our endpoints with async endpoints.

@xcrzx xcrzx force-pushed the is-customized-bulk branch 2 times, most recently from d9c9859 to 778a215 Compare August 28, 2024 13:59
@xcrzx
Copy link
Contributor Author

xcrzx commented Aug 28, 2024

But that just caused the requests to eventually time out. Which I kind of expected. What do you think? I mean, this is the same scenario we're having for importing large amounts of rules, and it seems that the only "proper" way out of this is replacing our endpoints with async endpoints.

Thanks for catching that, @jpdjere! For now, we can add a more human-readable error message instead of the cryptic 500 Internal Server Error. I've added extra validation to the API handler to return a 400 error when there are more than 2,000 rules matching the query. This number seems to perform okay without hitting a timeout or triggering the "too many nested clauses" exception.

In the longer term, turning this API into an async process might be the best approach, but that will require more thorough exploration.

@xcrzx xcrzx force-pushed the is-customized-bulk branch from 778a215 to efc32ab Compare August 28, 2024 14:42
@xcrzx xcrzx force-pushed the is-customized-bulk branch from efc32ab to f39b7c3 Compare August 28, 2024 15:55
@kibana-ci
Copy link
Collaborator

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

✅ unchanged

History

  • 💔 Build #230484 failed efc32ab83a37f23f9ff3cc6b7aa6bc233e011f49
  • 💔 Build #230469 failed 778a21522bfa26ee47b9beeabd6a4cfd59eb31be
  • 💔 Build #230462 failed 778a21522bfa26ee47b9beeabd6a4cfd59eb31be
  • 💔 Build #230455 failed 778a21522bfa26ee47b9beeabd6a4cfd59eb31be
  • 💚 Build #230403 succeeded d9c98599c9a5dee2478676dff51a366a2a1faa0e
  • 💛 Build #230089 was flaky ded507e8f63e4070ba9534218543231eddb5683b

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

cc @xcrzx

@xcrzx xcrzx merged commit 5cf9522 into elastic:main Aug 29, 2024
41 checks passed
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label Aug 29, 2024
@xcrzx xcrzx deleted the is-customized-bulk branch August 29, 2024 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:Prebuilt Detection Rules Security Solution Prebuilt Detection Rules area release_note:skip Skip the PR/issue when compiling release notes Team:Detection Rule Management Security Detection Rule Management Team Team:Endpoint Response Endpoint Response Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. v8.16.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Security Solution] Calculate and save ruleSource.isCustomized in bulk edit API
9 participants