-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Conversation
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.
packages/kbn-test/src/kbn_client/kbn_client_requester.ts
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.
kbn-test changes LGTM
/** | ||
* 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); | ||
}; |
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.
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).
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.
I'll remove one of the concurrency implementations depending on which one reviewers think is better DX-wise.
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.
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
) => {
// ...
}
Added the whole @elastic/security-detection-rule-management team to the list of reviewers so people get reminded about the PR over slack. |
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.
@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'}}'; |
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.
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 }>; |
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.
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.
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.
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.
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.
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.
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.
I think compileBundleTemplate
is much clearer 👍
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.
@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)); |
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.
Does it make sense to sort by sourcePath
here while contexts are sorted by operationId
on line 98?
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.
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.
/** | ||
* 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); | ||
}; |
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.
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 }>; |
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.
I think compileBundleTemplate
is much clearer 👍
💛 Build succeeded, but was flaky
Failed CI StepsTest Failures
Metrics [docs]Module Count
Public APIs missing comments
History
To update your PR or re-run it, just comment with: |
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.
@marshallmain thanks for addressing my comments 🙏
Pinging @elastic/security-detection-engine (Team:Detection Engine) |
…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.
…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)
… 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>
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.