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] Quickstart script tooling for Detections and Response #190634

Merged
merged 28 commits into from
Sep 6, 2024

Conversation

marshallmain
Copy link
Contributor

@marshallmain marshallmain commented Aug 15, 2024

Summary

Creates CLI script tooling for building data, rules, exceptions, and lists in any (local, cloud, serverless) environment for manual testing. The initial commits here add generated clients for accessing security solution, exceptions, and lists APIs and a placeholder script where those clients are set up for use. See README for more details.

Much of the code in this PR is auto-generated clients. The hand written code is intended to be primarily in quickstart/modules/, where we can add wrapper code to simplify the process for common test environment setup. For example, createValueListException takes an array of items and some metadata and automatically creates a new value list and an exception that references that value list. /modules/data/ contains functions to generate documents of arbitrary size, and we can add more functions to create various other types of documents.

@marshallmain marshallmain marked this pull request as ready for review August 15, 2024 19:30
@marshallmain marshallmain requested review from a team as code owners August 15, 2024 19:30
@marshallmain marshallmain added the release_note:skip Skip the PR/issue when compiling release notes label Aug 15, 2024
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.

packages/kbn-test/src/kbn_client/kbn_client_requester.ts

Copy link
Member

@pheyos pheyos left a comment

Choose a reason for hiding this comment

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

kbn-test changes LGTM

Comment on lines 11 to 35
/**
* Helper function to call an API multiple times concurrently, basically like a client-side bulk API.
*
* const responses = await clientConcurrency(
detectionsClient.createRule.bind(detectionsClient),
ruleCopies.map((rule) => ({
body: rule,
}))
);
* Note the `.bind` is crucial in `detectionsClient.createRule.bind(detectionsClient)` - without binding, `createRule` is called without
access to the detectionsClient object and it will fail at runtime - even though the code typechecks fine.
* @param clientFunction The API function to call multiple times
* @param inputs An array of input values. Each value in the array is passed to a separate invocation of the clientFunction.
* @param concurrency Number of simultaneous in-progress API calls that are allowed.
* @returns An array of the responses from the API calls.
*/
export const clientConcurrency = async <Input, Output>(
clientFunction: (input: Input) => Promise<Output>,
inputs: Input[],
concurrency: number = 10
) => {
const limiter = limit(concurrency);
const promises = inputs.map((input) => limiter(() => clientFunction(input)));
return Promise.all(promises);
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This function is an alternative way of implementing concurrency compared to the one below (concurrentlyExec). IMO concurrentlyExec is a bit better because it's easy to forget the .bind in this implementation, but concurrentlyExec does require creating a wrapper function for each request which makes the code look a bit messier (but the compiler will help you out, unlike with .bind where the compiler does not help you).

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'll remove one of the concurrency implementations depending on which one reviewers think is better DX-wise.

Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO concurrentlyExec has much more straightforward API. It's quite easy to forget adding .bind(). Though I think params should be renamed for better clarity. In fact concurrentlyExec accepts factories (factory is a well known and convenient pattern) so it should be reflected clearly like in the example below

type ActionFactory = () => Promise<Output>;

export const concurrentlyExec = async <Output>(
  actionFactories: ActionFactor[],
  concurrency: number = 10
) => {
  // ...
}

@banderror
Copy link
Contributor

Added the whole @elastic/security-detection-rule-management team to the list of reviewers so people get reminded about the PR over slack.

Copy link
Contributor

@maximpn maximpn left a comment

Choose a reason for hiding this comment

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

@marshallmain thank you for extending code generation with quickstart scripts 🙏

I have concerns regarding generation context typing since it could decrease code readability. Technically sources and imports are almost the same and could be reused. As minimum it's better to get rid of ad hoc types.

Feel free reach me to discuss it in more details if something is unclear.

{{operationId}}RequestBodyInput,
{{operationId}}Response,
{{/each}}
} from '{{replace sourcePath 'schema.yaml' 'gen'}}';
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 better to make such processing when context is constructed. It will help to simplify the template. There is already an utility function getGeneratedFilePath() for that purpose.

compileTemplate: (
templateName: TemplateName,
context: GenerationContext & {
sources: Array<{ sourcePath: string; generationContext: GenerationContext }>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Ad hoc typing only adds confusion here. context is a GenerationContext and also has sources with generationContext. Anyone reading this later on will need to spend time to inspect the upstream code to see what it actually means. I'd recommend to extend GenerationContext with sources.

If going deeper we already have imports field in GenerationContext. It looks like it could be reused after adjustments like checking generationContext and filling importing symbols.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My intent here was to avoid making GenerationContext into a recursive type, but yeah it was too confusing to have the context + generationContext properties and type mixing. I replaced this bit by separating the bundle generation context from the single file generation context and also creating a compileBundleTemplate function separate from compileTemplate - lmk if it looks better to you now. We had the imports field in the generation context for the bundles, but it didn't actually contain any data so it wasn't usable.

Copy link
Contributor

Choose a reason for hiding this comment

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

My idea regarding imports is that it's quite simple. Context could have necessary import entries when constructed. Looking at zod_operation_schema.handlebars using imports we can see that this approach will add imports for any import entry presented in imports.

{{#each imports}}
import {
  {{#each this}}{{.}},{{/each}}
} from "{{@key}}"
{{/each}}

There is no obstacle to add there necessary entries like ${operationId}RequestQueryInputand ${operationId}}RequestParamsInput`. Of course it'll require reworking generation logic a big to support multiple contexts.

I'd investigate this option but this is just a simplification idea and isn't a blocker for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think compileBundleTemplate is much clearer 👍

@xcrzx xcrzx requested review from maximpn and removed request for xcrzx August 30, 2024 14:13
Copy link
Contributor

@maximpn maximpn left a comment

Choose a reason for hiding this comment

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

@marshallmain thanks for addressing my comments 👍

The only concern I have is double sorting in openapi_generator.ts.

@@ -68,6 +69,10 @@ export const generate = async (config: GeneratorConfig) => {
({ generationContext }) =>
generationContext.operations.length > 0 || generationContext.components !== undefined
);
parsedSources.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to sort by sourcePath here while contexts are sorted by operationId on line 98?

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 so, we could sort the operations within each source first then sort the sources by the first operationId in each source but I thought sourcePath felt like a more natural way to sort the sources. The sources and operations are closely related (and often 1-to-1) but I think it makes sense to keep some differentiation between them and treat the sourcePath as a unique identifier for sources and operationId as a unique identifier for operations.

Comment on lines 11 to 35
/**
* Helper function to call an API multiple times concurrently, basically like a client-side bulk API.
*
* const responses = await clientConcurrency(
detectionsClient.createRule.bind(detectionsClient),
ruleCopies.map((rule) => ({
body: rule,
}))
);
* Note the `.bind` is crucial in `detectionsClient.createRule.bind(detectionsClient)` - without binding, `createRule` is called without
access to the detectionsClient object and it will fail at runtime - even though the code typechecks fine.
* @param clientFunction The API function to call multiple times
* @param inputs An array of input values. Each value in the array is passed to a separate invocation of the clientFunction.
* @param concurrency Number of simultaneous in-progress API calls that are allowed.
* @returns An array of the responses from the API calls.
*/
export const clientConcurrency = async <Input, Output>(
clientFunction: (input: Input) => Promise<Output>,
inputs: Input[],
concurrency: number = 10
) => {
const limiter = limit(concurrency);
const promises = inputs.map((input) => limiter(() => clientFunction(input)));
return Promise.all(promises);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO concurrentlyExec has much more straightforward API. It's quite easy to forget adding .bind(). Though I think params should be renamed for better clarity. In fact concurrentlyExec accepts factories (factory is a well known and convenient pattern) so it should be reflected clearly like in the example below

type ActionFactory = () => Promise<Output>;

export const concurrentlyExec = async <Output>(
  actionFactories: ActionFactor[],
  concurrency: number = 10
) => {
  // ...
}

compileTemplate: (
templateName: TemplateName,
context: GenerationContext & {
sources: Array<{ sourcePath: string; generationContext: GenerationContext }>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think compileBundleTemplate is much clearer 👍

@kibana-ci
Copy link
Collaborator

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] FTR Configs #5 / Saved Objects Management saved objects management with hidden types Delete modal should not delete the hidden objects when performing the operation

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
lists 289 340 +51
securitySolution 5675 5726 +51
total +102

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/securitysolution-utils 46 54 +8
Unknown metric groups

API count

id before after diff
@kbn/securitysolution-utils 51 60 +9

History

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

Copy link
Contributor

@maximpn maximpn left a comment

Choose a reason for hiding this comment

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

@marshallmain thanks for addressing my comments 🙏

@marshallmain marshallmain merged commit 3cc7029 into elastic:main Sep 6, 2024
41 checks passed
@marshallmain marshallmain deleted the secsol-quickstart branch September 6, 2024 13:42
@kibanamachine kibanamachine added v8.16.0 backport:skip This commit does not require backporting labels Sep 6, 2024
@marshallmain marshallmain added Team:Detection Rule Management Security Detection Rule Management Team Team:Detection Engine Security Solution Detection Engine Area and removed backport:skip This commit does not require backporting labels Sep 6, 2024
@elasticmachine
Copy link
Contributor

Pinging @elastic/security-detection-engine (Team:Detection Engine)

@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label Sep 6, 2024
marshallmain added a commit that referenced this pull request Sep 30, 2024
…mpty lines in import list API (#192681)

## Summary

The quickstart tooling introduced in
#190634 uses axios under the hood
to make requests to Kibana. When attaching file data to the axios
request with `FormData`, axios adds an extra empty line after the end
content boundary. The logic in `buffer_lines.ts` assumes that there are
no more lines after the end content boundary line, so importing a list
with the quickstart tooling would create a list with an extra empty
item. This empty item fails validation when retrieved through other
APIs.

This PR prevents lines after the end content boundary from being turned
into list items in the import list API.
kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Sep 30, 2024
…mpty lines in import list API (elastic#192681)

## Summary

The quickstart tooling introduced in
elastic#190634 uses axios under the hood
to make requests to Kibana. When attaching file data to the axios
request with `FormData`, axios adds an extra empty line after the end
content boundary. The logic in `buffer_lines.ts` assumes that there are
no more lines after the end content boundary line, so importing a list
with the quickstart tooling would create a list with an extra empty
item. This empty item fails validation when retrieved through other
APIs.

This PR prevents lines after the end content boundary from being turned
into list items in the import list API.

(cherry picked from commit 5f83ac0)
kibanamachine added a commit that referenced this pull request Sep 30, 2024
… for empty lines in import list API (#192681) (#194470)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution][Detection Engine] Avoid creating list items for
empty lines in import list API
(#192681)](#192681)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Marshall
Main","email":"55718608+marshallmain@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-09-30T18:07:39Z","message":"[Security
Solution][Detection Engine] Avoid creating list items for empty lines in
import list API (#192681)\n\n## Summary\r\n\r\nThe quickstart tooling
introduced in\r\nhttps://github.com//pull/190634 uses
axios under the hood\r\nto make requests to Kibana. When attaching file
data to the axios\r\nrequest with `FormData`, axios adds an extra empty
line after the end\r\ncontent boundary. The logic in `buffer_lines.ts`
assumes that there are\r\nno more lines after the end content boundary
line, so importing a list\r\nwith the quickstart tooling would create a
list with an extra empty\r\nitem. This empty item fails validation when
retrieved through other\r\nAPIs.\r\n\r\nThis PR prevents lines after the
end content boundary from being turned\r\ninto list items in the import
list
API.","sha":"5f83ac05991cd980ef5b205acd19c997b60045a3","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","Team:Detection
Engine","v8.16.0"],"title":"[Security Solution][Detection Engine] Avoid
creating list items for empty lines in import list
API","number":192681,"url":"https://github.com/elastic/kibana/pull/192681","mergeCommit":{"message":"[Security
Solution][Detection Engine] Avoid creating list items for empty lines in
import list API (#192681)\n\n## Summary\r\n\r\nThe quickstart tooling
introduced in\r\nhttps://github.com//pull/190634 uses
axios under the hood\r\nto make requests to Kibana. When attaching file
data to the axios\r\nrequest with `FormData`, axios adds an extra empty
line after the end\r\ncontent boundary. The logic in `buffer_lines.ts`
assumes that there are\r\nno more lines after the end content boundary
line, so importing a list\r\nwith the quickstart tooling would create a
list with an extra empty\r\nitem. This empty item fails validation when
retrieved through other\r\nAPIs.\r\n\r\nThis PR prevents lines after the
end content boundary from being turned\r\ninto list items in the import
list
API.","sha":"5f83ac05991cd980ef5b205acd19c997b60045a3"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/192681","number":192681,"mergeCommit":{"message":"[Security
Solution][Detection Engine] Avoid creating list items for empty lines in
import list API (#192681)\n\n## Summary\r\n\r\nThe quickstart tooling
introduced in\r\nhttps://github.com//pull/190634 uses
axios under the hood\r\nto make requests to Kibana. When attaching file
data to the axios\r\nrequest with `FormData`, axios adds an extra empty
line after the end\r\ncontent boundary. The logic in `buffer_lines.ts`
assumes that there are\r\nno more lines after the end content boundary
line, so importing a list\r\nwith the quickstart tooling would create a
list with an extra empty\r\nitem. This empty item fails validation when
retrieved through other\r\nAPIs.\r\n\r\nThis PR prevents lines after the
end content boundary from being turned\r\ninto list items in the import
list
API.","sha":"5f83ac05991cd980ef5b205acd19c997b60045a3"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com>
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 release_note:skip Skip the PR/issue when compiling release notes Team:Detection Engine Security Solution Detection Engine Area Team:Detection Rule Management Security Detection Rule Management Team v8.16.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants