mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Exceptions][Value Lists] Add file type and size constraints to value list uploads (#8507) (#176074)
## Summary Addresses https://github.com/elastic/security-team/issues/8507 With these changes we address the issue where users can upload any file to be imported as a value list. The restrictions are: * Users should be limited to uploading .txt or .csv. All other file types should return a 415. * Users should be limited to uploading files of 9K bytes size. Files larger than that should return a 413. ### Checklist Delete any items that are not applicable to this PR. - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ESS 97 times](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5019) - [Serverless 97 times](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5020)
This commit is contained in:
parent
e90c2098f2
commit
c38410affe
7 changed files with 102 additions and 8 deletions
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { extname } from 'path';
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { validate } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
@ -17,6 +19,8 @@ import { buildRouteValidation, buildSiemResponse } from '../utils';
|
|||
import { createStreamFromBuffer } from '../utils/create_stream_from_buffer';
|
||||
import { getListClient } from '..';
|
||||
|
||||
const validFileExtensions = ['.csv', '.txt'];
|
||||
|
||||
export const importListItemRoute = (router: ListsPluginRouter, config: ConfigType): void => {
|
||||
router.versioned
|
||||
.post({
|
||||
|
@ -47,10 +51,29 @@ export const importListItemRoute = (router: ListsPluginRouter, config: ConfigTyp
|
|||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
try {
|
||||
const stream = createStreamFromBuffer(request.body);
|
||||
const { deserializer, list_id: listId, serializer, type } = request.query;
|
||||
const lists = await getListClient(context);
|
||||
|
||||
const filename = await lists.getImportFilename({
|
||||
stream: createStreamFromBuffer(request.body),
|
||||
});
|
||||
if (!filename) {
|
||||
return siemResponse.error({
|
||||
body: 'To import a list item, the file name must be specified',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
const fileExtension = extname(filename).toLowerCase();
|
||||
if (!validFileExtensions.includes(fileExtension)) {
|
||||
return siemResponse.error({
|
||||
body: `Unsupported media type. File must be one of the following types: [${validFileExtensions.join(
|
||||
', '
|
||||
)}]`,
|
||||
statusCode: 415,
|
||||
});
|
||||
}
|
||||
|
||||
const stream = createStreamFromBuffer(request.body);
|
||||
const listDataExists = await lists.getListDataStreamExists();
|
||||
if (!listDataExists) {
|
||||
const listIndexExists = await lists.getListIndexExists();
|
||||
|
|
|
@ -37,6 +37,7 @@ import type {
|
|||
|
||||
import type { ConfigType } from '../../config';
|
||||
import {
|
||||
BufferLines,
|
||||
createListItem,
|
||||
deleteListItem,
|
||||
deleteListItemByValue,
|
||||
|
@ -69,6 +70,7 @@ import type {
|
|||
FindAllListItemsOptions,
|
||||
FindListItemOptions,
|
||||
FindListOptions,
|
||||
GetImportFilename,
|
||||
GetListItemByValueOptions,
|
||||
GetListItemOptions,
|
||||
GetListItemsByValueOptions,
|
||||
|
@ -715,6 +717,33 @@ export class ListClient {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the filename of the imported file
|
||||
* @param options
|
||||
* @param options.stream The stream to pull the import from
|
||||
* @returns
|
||||
*/
|
||||
public getImportFilename = ({ stream }: GetImportFilename): Promise<string | undefined> => {
|
||||
return new Promise<string | undefined>((resolve, reject) => {
|
||||
const { config } = this;
|
||||
const readBuffer = new BufferLines({ bufferSize: config.importBufferSize, input: stream });
|
||||
let fileName: string | undefined;
|
||||
readBuffer.on('fileName', async (fileNameEmitted: string) => {
|
||||
try {
|
||||
readBuffer.pause();
|
||||
fileName = decodeURIComponent(fileNameEmitted);
|
||||
readBuffer.resume();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
readBuffer.on('close', () => {
|
||||
resolve(fileName);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Imports list items to a stream. If the list already exists, this will append the list items to the existing list.
|
||||
* If the list does not exist, this will auto-create the list and then add the items to that list.
|
||||
|
|
|
@ -335,3 +335,12 @@ export interface SearchListItemByValuesOptions {
|
|||
/** The value to search for list items based off. */
|
||||
value: unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
* ListClient.getImportFilename
|
||||
* {@link ListClient.getImportFilename}
|
||||
*/
|
||||
export interface GetImportFilename {
|
||||
/** The stream to pull the import from */
|
||||
stream: Readable;
|
||||
}
|
||||
|
|
|
@ -803,7 +803,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
it('generates no alerts when a value list exception is added for a query rule', async () => {
|
||||
const valueListId = 'value-list-id';
|
||||
const valueListId = 'value-list-id.txt';
|
||||
await importFile(supertest, log, 'keyword', ['suricata-sensor-amsterdam'], valueListId);
|
||||
const rule: QueryRuleCreateProps = {
|
||||
name: 'Simple Rule Query',
|
||||
|
@ -835,7 +835,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
it('generates no alerts when a value list exception is added for a threat match rule', async () => {
|
||||
const valueListId = 'value-list-id';
|
||||
const valueListId = 'value-list-id.txt';
|
||||
await importFile(supertest, log, 'keyword', ['zeek-sensor-amsterdam'], valueListId);
|
||||
const rule: ThreatMatchRuleCreateProps = {
|
||||
description: 'Detecting root and admin users',
|
||||
|
@ -883,7 +883,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
it('generates no alerts when a value list exception is added for a threshold rule', async () => {
|
||||
const valueListId = 'value-list-id';
|
||||
const valueListId = 'value-list-id.txt';
|
||||
await importFile(supertest, log, 'keyword', ['zeek-sensor-amsterdam'], valueListId);
|
||||
const rule: ThresholdRuleCreateProps = {
|
||||
description: 'Detecting root and admin users',
|
||||
|
@ -920,7 +920,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
it('generates no alerts when a value list exception is added for an EQL rule', async () => {
|
||||
const valueListId = 'value-list-id';
|
||||
const valueListId = 'value-list-id.txt';
|
||||
await importFile(supertest, log, 'keyword', ['zeek-sensor-amsterdam'], valueListId);
|
||||
const rule: EqlRuleCreateProps = {
|
||||
...getEqlRuleForAlertTesting(['auditbeat-*']),
|
||||
|
@ -944,7 +944,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
expect(alertsOpen.hits.hits.length).toEqual(0);
|
||||
});
|
||||
it('should Not allow deleting value list when there are references and ignoreReferences is false', async () => {
|
||||
const valueListId = 'value-list-id';
|
||||
const valueListId = 'value-list-id.txt';
|
||||
await importFile(supertest, log, 'keyword', ['suricata-sensor-amsterdam'], valueListId);
|
||||
const rule: QueryRuleCreateProps = {
|
||||
...getSimpleRule(),
|
||||
|
|
|
@ -107,7 +107,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
it('should Not allow editing an Exception with deleted ValueList', async () => {
|
||||
await createListsIndex(supertest, log);
|
||||
|
||||
const valueListId = 'value-list-id';
|
||||
const valueListId = 'value-list-id.txt';
|
||||
await importFile(supertest, log, 'keyword', ['suricata-sensor-amsterdam'], valueListId);
|
||||
const rule: QueryRuleCreateProps = {
|
||||
...getSimpleRule(),
|
||||
|
|
|
@ -229,7 +229,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
it('generates no alerts when a value list exception is added for an ML rule', async () => {
|
||||
const valueListId = 'value-list-id';
|
||||
const valueListId = 'value-list-id.txt';
|
||||
await importFile(supertest, log, 'keyword', ['mothra'], valueListId);
|
||||
const { previewId } = await previewRuleWithExceptionEntries({
|
||||
supertest,
|
||||
|
|
|
@ -54,6 +54,39 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
await deleteListsIndex(supertest, log);
|
||||
});
|
||||
|
||||
it('should not import a list item if the imported file is not .txt or .csv', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${LIST_ITEM_URL}/_import?type=ip`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getImportListItemAsBuffer(['127.0.0.1', '127.0.0.2']), 'list_items.pdf')
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(415);
|
||||
|
||||
expect(body).to.eql({
|
||||
status_code: 415,
|
||||
message: 'Unsupported media type. File must be one of the following types: [.csv, .txt]',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not import a list item if the imported file exceed the file size limit', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${LIST_ITEM_URL}/_import?type=ip`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach(
|
||||
'file',
|
||||
getImportListItemAsBuffer(Array(1000000).fill('127.0.0.1')),
|
||||
'list_items.txt'
|
||||
)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(413);
|
||||
|
||||
expect(body).to.eql({
|
||||
statusCode: 413,
|
||||
error: 'Request Entity Too Large',
|
||||
message: 'Payload content length greater than maximum allowed: 9000000',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the response content types to be expected when importing two items', async () => {
|
||||
await supertest
|
||||
.post(`${LIST_ITEM_URL}/_import?type=ip`)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue