[Cases] - Update case generator (#223609)

## Summary

Updates the logic around the test cases generator to allow for adding
cases to additional environments

example to test:

```
yarn generate:cases  -c 1000 -o securitySolution
```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Michael Olorunnisola 2025-06-24 11:31:13 -04:00 committed by GitHub
parent 26bb220e64
commit 0a07e18442
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 246 additions and 163 deletions

View file

@ -3,5 +3,8 @@
"name": "@kbn/cases-plugin",
"version": "1.0.0",
"private": true,
"license": "Elastic License 2.0"
"license": "Elastic License 2.0",
"scripts": {
"generate:cases": "node scripts/generate_cases.js"
}
}

View file

@ -0,0 +1,231 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/* eslint-disable no-console */
import { KbnClient } from '@kbn/test';
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { ToolingLog } from '@kbn/tooling-log';
import type { KbnClientOptions } from '@kbn/test';
import fs from 'fs';
import pMap from 'p-map';
import yargs from 'yargs';
import { CaseSeverity, type CasePostRequest } from '../common';
const toolingLogger = new ToolingLog({
level: 'info',
writeTo: process.stdout,
});
function updateURL({
url,
user,
protocol,
}: {
url: string;
user?: { username: string; password: string };
protocol?: string;
}): string {
const urlObject = new URL(url);
if (user) {
urlObject.username = user.username;
urlObject.password = user.password;
}
if (protocol) {
urlObject.protocol = protocol;
}
return urlObject.href;
}
const makeRequest = async ({
url,
newCase,
path,
username,
password,
ssl,
}: {
url: string;
newCase: CasePostRequest;
path: string;
username: string;
password: string;
ssl: boolean;
}) => {
let ca: Buffer;
const toolingLogOptions = { log: toolingLogger };
let updatedUrl = updateURL({
url,
user: { username, password },
});
let kbnClientOptions: KbnClientOptions = {
...toolingLogOptions,
url: updatedUrl,
};
if (ssl) {
ca = fs.readFileSync(CA_CERT_PATH);
updatedUrl = updateURL({
url: updatedUrl,
user: { username, password },
protocol: 'https:',
});
kbnClientOptions = {
...kbnClientOptions,
certificateAuthorities: [ca],
url: updatedUrl,
};
}
const kbnClient = new KbnClient({ ...kbnClientOptions });
return kbnClient
.request({
method: 'POST',
path,
body: newCase,
})
.then(({ data }) => data)
.catch(toolingLogger.error.bind(toolingLogger, `Error creating case: ${newCase.title}`));
};
const createCase = (counter: number, owner: string, reqId: string): CasePostRequest => ({
title: `Sample Case: ${reqId} - ${counter}`,
tags: [],
severity: CaseSeverity.LOW,
description: `Auto generated case ${counter}`,
assignees: [],
connector: {
id: 'none',
name: 'none',
// @ts-ignore
type: '.none',
fields: null,
},
settings: {
syncAlerts: false,
},
owner: owner ?? 'cases',
customFields: [],
});
const getRandomString = (length: number) =>
Math.random()
.toString(36)
.substring(2, length + 2);
const generateCases = async ({
cases,
space,
username,
password,
kibana,
ssl,
}: {
cases: CasePostRequest[];
space: string;
username: string;
password: string;
kibana: string;
ssl: boolean;
}) => {
try {
toolingLogger.info(
`Creating ${cases.length} cases in ${space ? `space: ${space}` : 'default space'}`
);
const path = `${space ? `/s/${space}` : ''}/api/cases`;
await pMap(
cases,
(newCase) => {
return makeRequest({ url: kibana, path, newCase, username, password, ssl });
},
{ concurrency: 100 }
);
} catch (error) {
toolingLogger.error(error);
}
};
const main = async () => {
try {
const argv = yargs.help().options({
username: {
alias: 'u',
describe: 'username for kibana',
type: 'string',
default: 'elastic',
},
password: {
alias: 'p',
describe: 'password for kibana',
type: 'string',
default: 'changeme',
},
kibana: {
alias: 'k',
describe: 'kibana url',
default: 'http://127.0.0.1:5601',
type: 'string',
},
count: {
alias: 'c',
describe: 'Number of cases to generate',
type: 'number',
default: 10,
},
owners: {
alias: 'o',
describe:
'solutions where the cases should be created. combination of securitySolution, observability, or cases',
default: ['securitySolution', 'observability', 'cases'],
type: 'array',
},
space: {
alias: 's',
describe: 'space where the cases should be created',
default: '',
type: 'string',
},
ssl: {
alias: 'ssl',
describe: 'Use https for non local environments',
type: 'boolean',
default: false,
},
}).argv;
const { username, password, kibana, count, owners, space, ssl } = argv;
const numCasesToCreate = Number(count);
const potentialOwners = new Set(['securitySolution', 'observability', 'cases']);
const invalidOwnerProvided = owners.some((owner) => !potentialOwners.has(owner));
if (invalidOwnerProvided) {
toolingLogger.error('Only valid owners are securitySolution, observability, and cases');
// eslint-disable-next-line no-process-exit
process.exit(1);
}
const idForThisRequest = getRandomString(6);
const cases = Array(numCasesToCreate)
.fill(null)
.map((_, index) => {
const owner = owners[Math.floor(Math.random() * owners.length)];
return createCase(index + 1, owner, idForThisRequest);
});
await generateCases({ cases, space, username, password, kibana, ssl });
} catch (error) {
console.log(error);
}
};
main();
process.on('uncaughtException', function (err) {
console.log(err);
});

View file

@ -5,161 +5,5 @@
* 2.0.
*/
const http = require('http');
const pMap = require('p-map');
const yargs = require('yargs');
const username = 'elastic';
const password = 'changeme';
const makeRequest = async ({ options, data }) => {
return new Promise((resolve, reject) => {
const reqData = JSON.stringify(data);
const reqOptions = {
...options,
rejectUnauthorized: false,
requestCert: true,
agent: false,
headers: {
...options.headers,
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
'Content-Type': 'application/json',
'Content-Length': reqData.length,
'kbn-xsrf': 'true',
},
};
const req = http.request(reqOptions, (res) => {
const body = [];
res.on('data', (chunk) => body.push(chunk));
res.on('end', () => {
const resString = Buffer.concat(body).toString();
try {
if (resString != null && resString.length > 0) {
const res = JSON.parse(resString);
if (res.statusCode && res.statusCode === 400) {
reject(new Error(res.message));
}
}
} catch (error) {
reject(error);
}
resolve(resString);
});
});
req.on('error', (err) => {
reject(err);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request time out'));
});
req.write(reqData);
req.end();
});
};
const getHostAndPort = () => ({
host: '127.0.0.1',
port: 5601,
});
const createCase = (counter, owner) => ({
title: `Sample Case ${counter}`,
tags: [],
severity: 'low',
description: `Auto generated case ${counter}`,
assignees: [],
connector: {
id: 'none',
name: 'none',
type: '.none',
fields: null,
},
settings: {
syncAlerts: false,
},
owner: owner ?? 'cases',
customFields: [],
});
const generateCases = async (cases, space) => {
try {
console.log(`Creating ${cases.length} cases in ${space ? `space: ${space}` : 'default space'}`);
const path = space ? `/s/${space}/api/cases` : '/api/cases';
await pMap(
cases,
(theCase) => {
const options = {
...getHostAndPort(),
path,
method: 'POST',
};
return makeRequest({ options, data: theCase });
},
{ concurrency: 100 }
);
} catch (error) {
console.log(error);
}
};
const main = async () => {
try {
const argv = yargs.help().options({
count: {
alias: 'c',
describe: 'number of cases to generate',
type: 'number',
default: 10,
},
owners: {
alias: 'o',
describe:
'solutions where the cases should be created. combination of securitySolution, observability, or cases',
default: 'cases',
type: 'array',
},
space: {
alias: 's',
describe: 'space where the cases should be created',
default: '',
type: 'string',
},
}).argv;
const { count, owners, space } = argv;
const numCasesToCreate = Number(count);
const potentialOwners = new Set(['securitySolution', 'observability', 'cases']);
const invalidOwnerProvided = owners.some((owner) => !potentialOwners.has(owner));
if (invalidOwnerProvided) {
console.error('Only valid owners are securitySolution, observability, and cases');
// eslint-disable-next-line no-process-exit
process.exit(1);
}
const cases = Array(numCasesToCreate)
.fill(null)
.map((_, index) => {
const owner = owners[Math.floor(Math.random() * owners.length)];
return createCase(index + 1, owner);
});
await generateCases(cases, space);
} catch (error) {
console.log(error);
}
};
main();
process.on('uncaughtException', function (err) {
console.log(err);
});
require('../../../../../../src/setup_node_env');
require('./cases_generator');

View file

@ -1,16 +1,20 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"outDir": "target/types"
},
"include": [
"common/**/*",
"public/**/*",
"server/**/*",
"scripts/**/*",
"../../../../../typings/**/*"
],
"kbn_references": [
"@kbn/core",
{
"path": "../../../../../src/setup_node_env/tsconfig.json"
},
// optionalPlugins from ./kibana.json
"@kbn/lens-plugin",
"@kbn/security-plugin",
@ -85,8 +89,9 @@
"@kbn/logging",
"@kbn/core-elasticsearch-client-server-mocks",
"@kbn/core-test-helpers-model-versions",
"@kbn/test",
"@kbn/dev-utils",
"@kbn/tooling-log"
],
"exclude": [
"target/**/*",
]
"exclude": ["target/**/*"]
}