Skip to content

Commit 19a2a69

Browse files
authored
[Security Solution] Implement prebuilt rules package generation tool (#237561)
**Partially addresses:** elastic/security-team#12849, #188090 ## Summary This PR adds a toolkit to generate mock Prebuilt Rule Assets Fleet package a.k.a. `security_detection_engine` for performance testing purposes. ## Details The toolkit lib was added to `@kbn/security-solution-test-api-clients` security solution package. It allows to generate packages of arbitrary size and composition. The only limitation that it uses a single mock "thick" prebuilt rule asset template to produce prebuilt rule assets and their historical versions. To mimic the real package behavior the template contains large setup and investigation guides along the other fields having values copied from the real rules in the real recent package version. The toolkit could be run with the following command ```bash node ./x-pack/solutions/security/packages/test-api-clients/scripts/prebuilt_rules/generate_package.js --packageSemver 99.0.0 --numOfRules 3000 --numOfHistoricalVersions 2 ``` and it produces the following logs ```bash info 🪄 Generating prebuilt rules package... debg Total 9000 prebuilt rules assets will be generated (3000 latest version prebuilt rules assets + 6000 historical prebuilt rules assets) succ 📦 Generated package has been written to <home path>/security_detection_engine-99.0.0.zip (173.93 MB) ``` and it generates a package `security_detection_engine-99.0.0.zip` in the current user's home folder.
1 parent 783c6c6 commit 19a2a69

File tree

21 files changed

+734
-108
lines changed

21 files changed

+734
-108
lines changed

.eslintrc.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,15 @@ module.exports = {
12751275
],
12761276
},
12771277
},
1278+
// Allow node.js imports only for the prebuilt rules package generation scripts
1279+
{
1280+
files: [
1281+
'x-pack/solutions/security/packages/test-api-clients/prebuilt_rules_package_generation/*.{js,mjs,ts,tsx}',
1282+
],
1283+
rules: {
1284+
'import/no-nodejs-modules': 'off',
1285+
},
1286+
},
12781287
{
12791288
// typescript only for front and back end, but excludes the test files.
12801289
// We use this section to add rules in which we do not want to apply to test files.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,6 @@
940940
"@kbn/security-solution-serverless": "link:x-pack/solutions/security/plugins/security_solution_serverless",
941941
"@kbn/security-solution-side-nav": "link:x-pack/solutions/security/packages/side-nav",
942942
"@kbn/security-solution-storybook-config": "link:x-pack/solutions/security/packages/storybook/config",
943-
"@kbn/security-solution-test-api-clients": "link:x-pack/solutions/security/packages/test-api-clients",
944943
"@kbn/security-solution-upselling": "link:x-pack/solutions/security/packages/upselling",
945944
"@kbn/security-test-endpoints-plugin": "link:x-pack/platform/test/security_functional/plugins/test_endpoints",
946945
"@kbn/security-ui-components": "link:x-pack/platform/packages/private/security/ui_components",
@@ -1642,6 +1641,7 @@
16421641
"@kbn/scout-reporting": "link:src/platform/packages/private/kbn-scout-reporting",
16431642
"@kbn/scout-security": "link:x-pack/solutions/security/packages/kbn-scout-security",
16441643
"@kbn/security-api-integration-helpers": "link:x-pack/platform/test/security_api_integration/packages/helpers",
1644+
"@kbn/security-solution-test-api-clients": "link:x-pack/solutions/security/packages/test-api-clients",
16451645
"@kbn/serverless-storybook-config": "link:src/platform/packages/shared/serverless/storybook/config",
16461646
"@kbn/set-map": "link:packages/kbn-set-map",
16471647
"@kbn/shared-ux-card-no-data-mocks": "link:src/platform/packages/shared/shared-ux/card/no_data/mocks",
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
{
22
"id": "@kbn/security-solution-test-api-clients",
33
"type": "shared-common",
4-
"owner": [
5-
"@elastic/security-detection-rule-management"
6-
],
4+
"owner": ["@elastic/security-detection-rule-management"],
75
"group": "security",
8-
"visibility": "private"
9-
}
6+
"visibility": "private",
7+
"devOnly": true
8+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Prebuilt Rules Package Generation
2+
3+
This package provides a small toolkit to generate mock prebuilt detection rule assets packed in a Fleet package compatible with the Security prebuilt rules workflow. It could be used to generate payload for stress and performance testing.
4+
5+
# Details
6+
7+
The toolkit uses a mock prebuilt rule asset mimicking a quite heavy real one with large setup and investigation guides. This prebuilt rule asset is multiplied by the desired N with an ability to add M historical prebuilt rule assets.
8+
9+
# Running the script
10+
11+
The script requires the following parameters
12+
13+
- `--packageSemver` - package version in a semver format. e.g. `9.2.0`
14+
- `--numOfRules` - the total number of prebuilt rule assets with the recent version, e.g. `3000`
15+
16+
and the following optional parameters could be specified
17+
18+
- `--packageName` - a desired package name, by default `security_detection_engine` is used
19+
- `--numOfHistoricalVersions` - a number of historical prebuilt rule assets per each recent version prebuilt rule asset, zero by default
20+
- `--output` or `-o` - output directory path, by default current user's home directory is used
21+
22+
For example the following command
23+
24+
```bash
25+
node ./x-pack/solutions/security/packages/test-api-clients/scripts/prebuilt_rules/generate_package.js --packageSemver 99.0.0 --numOfRules 3000 --numOfHistoricalVersions 2
26+
```
27+
28+
would output the following logs
29+
30+
```bash
31+
info 🪄 Generating prebuilt rules package...
32+
debg Total 9000 prebuilt rules assets will be generated (3000 latest version prebuilt rules assets + 6000 historical prebuilt rules assets)
33+
succ 📦 Generated package has been written to <home path>/security_detection_engine-99.0.0.zip (173.93 MB)
34+
```
35+
36+
and it'd generate a package `security_detection_engine-99.0.0.zip` in the current user's home folder.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { normalize } from 'path';
9+
import { homedir } from 'os';
10+
import yargs from 'yargs/yargs';
11+
import { ToolingLog } from '@kbn/tooling-log';
12+
import chalk from 'chalk';
13+
import { statSync } from 'fs';
14+
import { generatePrebuiltRuleAssets } from './generate_prebuilt_rule_assets';
15+
import { generatePrebuiltRulesPackageZipFile } from './generate_prebuilt_rules_package_zip_file';
16+
17+
const DEFAULT_PREBUILT_RULES_PACKAGE_NAME = 'security_detection_engine';
18+
19+
export function runCli() {
20+
yargs(process.argv.slice(2))
21+
.command(
22+
'*',
23+
'Generate Prebuilt Rules mock Fleet package',
24+
(y) =>
25+
y
26+
.option('packageName', {
27+
describe: `Package name (default: ${DEFAULT_PREBUILT_RULES_PACKAGE_NAME})`,
28+
default: DEFAULT_PREBUILT_RULES_PACKAGE_NAME,
29+
string: true,
30+
})
31+
.option('packageSemver', {
32+
describe: 'Package version in semver format (e.g. 9.2.0)',
33+
demandOption: true,
34+
string: true,
35+
})
36+
.option('numOfRules', {
37+
describe: 'A number of mock prebuilt rule assets to generate',
38+
demandOption: true,
39+
number: true,
40+
})
41+
.option('numOfHistoricalVersions', {
42+
describe:
43+
'A number of historical mock prebuilt rule assets per each rule to generate. TotalNumOfRules = numOfRules * (1 + numOfHistoricalVersions)',
44+
default: 0,
45+
number: true,
46+
})
47+
.option('output', {
48+
describe: 'Output directory path',
49+
default: homedir(),
50+
string: true,
51+
alias: 'o',
52+
})
53+
.showHelpOnFail(true),
54+
async ({ packageName, packageSemver, numOfRules, numOfHistoricalVersions, output }) => {
55+
const logger = new ToolingLog({
56+
level: 'debug',
57+
writeTo: process.stdout,
58+
});
59+
const outputFilePath = normalize(`${output}/${packageName}-${packageSemver}.zip`);
60+
const totalNumOfPrebuiltRulesAssets = numOfRules * (1 + numOfHistoricalVersions);
61+
62+
logger.info('🪄 Generating prebuilt rules package...');
63+
64+
logger.debug(
65+
`Total ${chalk.blue(
66+
totalNumOfPrebuiltRulesAssets
67+
)} prebuilt rules assets will be generated (${chalk.bold(
68+
numOfRules
69+
)} latest version prebuilt rules assets + ${chalk.bold(
70+
numOfRules * numOfHistoricalVersions
71+
)} historical prebuilt rules assets)`
72+
);
73+
74+
const prebuiltRuleAssets = generatePrebuiltRuleAssets({
75+
numOfRules,
76+
numOfHistoricalVersions,
77+
packageVersion: packageSemver,
78+
});
79+
80+
await generatePrebuiltRulesPackageZipFile({
81+
packageName,
82+
packageSemver,
83+
prebuiltRuleAssets,
84+
filePath: outputFilePath,
85+
});
86+
87+
const packageStats = statSync(outputFilePath);
88+
const packageSizeInMB = Math.ceil((packageStats.size / (1024 * 1024)) * 100) / 100;
89+
90+
logger.success(
91+
`📦 Generated package has been written to ${chalk.bold(outputFilePath)} (${chalk.magenta(
92+
packageSizeInMB
93+
)} ${chalk.magenta('MB')})`
94+
);
95+
}
96+
)
97+
.parse();
98+
}

x-pack/solutions/security/packages/test-api-clients/prebuilt_rules_package_generation/generate_prebuilt_rule_assets.ts

Lines changed: 390 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,74 @@
55
* 2.0.
66
*/
77

8-
import AdmZip from 'adm-zip';
8+
import archiver from 'archiver';
99
import { dump } from 'js-yaml';
1010
import semver from 'semver';
1111
import type { PackageSpecManifest } from '@kbn/fleet-plugin/common';
1212
import type { PrebuiltRuleAsset } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules';
1313

14-
interface CreatePrebuiltRulesPackageParams {
14+
export interface GeneratePrebuiltRulesPackageBaseParams {
1515
packageName: string;
1616
packageSemver: string;
1717
prebuiltRuleAssets: PrebuiltRuleAsset[];
1818
}
1919

20-
export function createPrebuiltRulesPackage({
20+
interface GeneratePrebuiltRulesPackageParams extends GeneratePrebuiltRulesPackageBaseParams {
21+
output: NodeJS.WritableStream;
22+
}
23+
24+
/**
25+
* Generates a prebuilt rules package with the provided name, version, and rule assets.
26+
*
27+
* `output` allows to use any writable stream, e.g. a file stream or a PassThrough stream to get a Buffer.
28+
*/
29+
export function generatePrebuiltRulesPackage({
2130
packageName,
2231
packageSemver,
2332
prebuiltRuleAssets,
24-
}: CreatePrebuiltRulesPackageParams): AdmZip {
33+
output,
34+
}: GeneratePrebuiltRulesPackageParams): void {
2535
validateSemver(packageSemver);
2636

37+
const prebuiltRulesPackage = archiver('zip', {
38+
zlib: { level: 9 },
39+
forceZip64: true,
40+
});
41+
42+
prebuiltRulesPackage.pipe(output);
43+
2744
const packageRootFolder = `${packageName}-${packageSemver}`;
28-
const prebuiltRulesPackage = new AdmZip();
2945

30-
prebuiltRulesPackage.addFile(
31-
`${packageRootFolder}/manifest.yml`,
32-
createPackageManifest(packageName, packageSemver)
33-
);
46+
prebuiltRulesPackage.append(createPackageManifest(packageName, packageSemver), {
47+
name: `${packageRootFolder}/manifest.yml`,
48+
});
49+
50+
prebuiltRulesPackage.append(createReadmeFileContent(packageName, packageSemver), {
51+
name: `${packageRootFolder}/docs/README.md`,
52+
});
3453

3554
for (const prebuiltRuleAsset of prebuiltRuleAssets) {
3655
const assetContent = JSON.stringify({
3756
attributes: prebuiltRuleAsset,
38-
id: prebuiltRuleAsset.rule_id,
57+
id: `${prebuiltRuleAsset.rule_id}_${prebuiltRuleAsset.version}`,
3958
type: 'security-rule',
4059
});
4160
const assetFileName = `${packageRootFolder}/kibana/security_rule/rules/${prebuiltRuleAsset.rule_id}_${prebuiltRuleAsset.version}.json`;
4261

43-
prebuiltRulesPackage.addFile(assetFileName, Buffer.from(assetContent, 'utf8'));
62+
prebuiltRulesPackage.append(Buffer.from(assetContent, 'utf8'), { name: assetFileName });
4463
}
4564

46-
return prebuiltRulesPackage;
65+
prebuiltRulesPackage.finalize();
4766
}
4867

4968
function createPackageManifest(packageName: string, packageSemver: string): Buffer {
5069
const packageManifest: PackageSpecManifest = {
5170
name: packageName,
71+
description: 'Prebuilt detection rules for Elastic Security',
5272
title: 'Prebuilt Security Detection Rules',
5373
version: packageSemver,
5474
owner: { github: 'elastic/protections' },
75+
format_version: '3.0.0',
5576
};
5677

5778
const yamlContent = dump(packageManifest, {
@@ -62,6 +83,12 @@ function createPackageManifest(packageName: string, packageSemver: string): Buff
6283
return Buffer.from(yamlContent, 'utf8');
6384
}
6485

86+
function createReadmeFileContent(packageName: string, packageSemver: string): Buffer {
87+
const readmeContent = `# Mock ${packageName} - ${packageSemver}`;
88+
89+
return Buffer.from(readmeContent, 'utf8');
90+
}
91+
6592
function validateSemver(version: string): void {
6693
if (!semver.valid(version)) {
6794
throw new Error(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { PassThrough } from 'stream';
9+
import type { GeneratePrebuiltRulesPackageBaseParams } from './generate_prebuilt_rules_package';
10+
import { generatePrebuiltRulesPackage } from './generate_prebuilt_rules_package';
11+
12+
export async function generatePrebuiltRulesPackageBuffer({
13+
packageName,
14+
packageSemver,
15+
prebuiltRuleAssets,
16+
}: GeneratePrebuiltRulesPackageBaseParams): Promise<Buffer> {
17+
return new Promise((resolve, reject) => {
18+
const output = new PassThrough();
19+
const chunks: Uint8Array[] = [];
20+
21+
output.on('data', (chunk) => chunks.push(chunk));
22+
output.on('end', () => resolve(Buffer.concat(chunks)));
23+
output.on('error', reject);
24+
25+
generatePrebuiltRulesPackage({
26+
packageName,
27+
packageSemver,
28+
prebuiltRuleAssets,
29+
output,
30+
});
31+
});
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { createWriteStream, mkdirSync } from 'fs';
9+
import { dirname } from 'path';
10+
import type { GeneratePrebuiltRulesPackageBaseParams } from './generate_prebuilt_rules_package';
11+
import { generatePrebuiltRulesPackage } from './generate_prebuilt_rules_package';
12+
13+
interface GeneratePrebuiltRulesPackageZipFileParams extends GeneratePrebuiltRulesPackageBaseParams {
14+
filePath: string;
15+
}
16+
17+
export async function generatePrebuiltRulesPackageZipFile({
18+
packageName,
19+
packageSemver,
20+
prebuiltRuleAssets,
21+
filePath,
22+
}: GeneratePrebuiltRulesPackageZipFileParams): Promise<void> {
23+
return new Promise((resolve, reject) => {
24+
// Make sure the path exists
25+
mkdirSync(dirname(filePath), { recursive: true });
26+
27+
const fileStream = createWriteStream(filePath);
28+
29+
fileStream.on('close', resolve);
30+
fileStream.on('error', reject);
31+
32+
generatePrebuiltRulesPackage({
33+
packageName,
34+
packageSemver,
35+
prebuiltRuleAssets,
36+
output: fileStream,
37+
});
38+
});
39+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
export * from './cli';
9+
export * from './generate_prebuilt_rules_package_buffer';
10+
export * from './generate_prebuilt_rules_package_zip_file';

0 commit comments

Comments
 (0)