Skip to content

Commit

Permalink
[SIEM][Detection Engine] Output schema validations (#58317)
Browse files Browse the repository at this point in the history
## Summary

* Adds an exact function which allows us to do exact validation rather than allowing arbitrary keys to go through
* Adds a type dependency for parts of validation (gives us feature parity with joi.when())
* Adds several hand spun validations/codecs
* Adds several utilities such as a printer for error validations

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
  • Loading branch information
FrankHassanabad authored Feb 26, 2020
1 parent 4122705 commit d5eb175
Show file tree
Hide file tree
Showing 66 changed files with 3,313 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export const mockPrepackagedRule = (): PrepackagedRules => ({
tags: [],
version: 1,
false_positives: [],
saved_id: 'some-id',
max_signals: 100,
timeline_id: 'timeline-id',
timeline_title: 'timeline-title',
Expand Down Expand Up @@ -312,7 +311,6 @@ export const getResult = (): RuleAlertType => ({
query: 'user.name: root or user.name: admin',
language: 'kuery',
outputIndex: '.siem-signals',
savedId: 'some-id',
timelineId: 'some-timeline-id',
timelineTitle: 'some-timeline-title',
meta: { someMeta: 'someField' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import { updatePrepackagedRules } from '../../rules/update_prepacked_rules';
import { getRulesToInstall } from '../../rules/get_rules_to_install';
import { getRulesToUpdate } from '../../rules/get_rules_to_update';
import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules';
import {
PrePackagedRulesSchema,
prePackagedRulesSchema,
} from '../schemas/response/prepackaged_rules_schema';
import { validate } from './validate';

export const createAddPrepackedRulesRoute = (
config: LegacyServices['config'],
Expand Down Expand Up @@ -78,10 +83,21 @@ export const createAddPrepackedRulesRoute = (
rulesToUpdate,
spaceIndex
);
return {
const prepackagedRulesOutput: PrePackagedRulesSchema = {
rules_installed: rulesToInstall.length,
rules_updated: rulesToUpdate.length,
};
const [validated, errors] = validate(prepackagedRulesOutput, prePackagedRulesSchema);
if (errors != null) {
return headers
.response({
message: errors,
status_code: 500,
})
.code(500);
} else {
return validated;
}
} catch (err) {
const error = transformError(err);
return headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import { LegacyServices } from '../../../../types';
import { createRules } from '../../rules/create_rules';
import { BulkRulesRequest } from '../../rules/types';
import { readRules } from '../../rules/read_rules';
import { transformOrBulkError, getDuplicates } from './utils';
import { getDuplicates } from './utils';
import { transformValidateBulkError, validate } from './validate';
import { getIndexExists } from '../../index/get_index_exists';
import { getIndex, transformBulkError, createBulkErrorObject } from '../utils';
import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema';
import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema';

export const createCreateRulesBulkRoute = (
config: LegacyServices['config'],
Expand Down Expand Up @@ -128,13 +130,13 @@ export const createCreateRulesBulkRoute = (
references,
version,
});
return transformOrBulkError(ruleIdOrUuid, createdRule);
return transformValidateBulkError(ruleIdOrUuid, createdRule);
} catch (err) {
return transformBulkError(ruleIdOrUuid, err);
}
})
);
return [
const rulesBulk = [
...rules,
...dupes.map(ruleId =>
createBulkErrorObject({
Expand All @@ -144,6 +146,17 @@ export const createCreateRulesBulkRoute = (
})
),
];
const [validated, errors] = validate(rulesBulk, rulesBulkSchema);
if (errors != null) {
return headers
.response({
message: errors,
status_code: 500,
})
.code(500);
} else {
return validated;
}
},
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ describe('create_rules', () => {
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
jest.spyOn(utils, 'transform').mockReturnValue(null);
const { payload, statusCode } = await server.inject(getCreateRequest());
expect(JSON.parse(payload).message).toBe('Internal error transforming rules');
expect(JSON.parse(payload).message).toBe('Internal error transforming');
expect(statusCode).toBe(500);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { RulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../r
import { createRulesSchema } from '../schemas/create_rules_schema';
import { readRules } from '../../rules/read_rules';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { transform } from './utils';
import { transformValidate } from './validate';

import { getIndexExists } from '../../index/get_index_exists';
import { getIndex, transformError } from '../utils';

Expand Down Expand Up @@ -136,16 +137,16 @@ export const createCreateRulesRoute = (
search: `${createdRule.id}`,
searchFields: ['alertId'],
});
const transformed = transform(createdRule, ruleStatuses.saved_objects[0]);
if (transformed == null) {
const [validated, errors] = transformValidate(createdRule, ruleStatuses.saved_objects[0]);
if (errors != null) {
return headers
.response({
message: 'Internal error transforming rules',
message: errors,
status_code: 500,
})
.code(500);
} else {
return transformed;
return validated;
}
} catch (err) {
const error = transformError(err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { LegacyServices } from '../../../../types';
import { GetScopedClients } from '../../../../services';
import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema';
import { transformOrBulkError, getIdBulkError } from './utils';
import { getIdBulkError } from './utils';
import { transformValidateBulkError, validate } from './validate';
import { transformBulkError } from '../utils';
import { QueryBulkRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
import { deleteRules } from '../../rules/delete_rules';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema';

export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => {
return {
Expand Down Expand Up @@ -58,7 +60,7 @@ export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.S
ruleStatuses.saved_objects.forEach(async obj =>
savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id)
);
return transformOrBulkError(idOrRuleIdOrUnknown, rule, ruleStatuses);
return transformValidateBulkError(idOrRuleIdOrUnknown, rule, ruleStatuses);
} else {
return getIdBulkError({ id, ruleId });
}
Expand All @@ -67,7 +69,17 @@ export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.S
}
})
);
return rules;
const [validated, errors] = validate(rules, rulesBulkSchema);
if (errors != null) {
return headers
.response({
message: errors,
status_code: 500,
})
.code(500);
} else {
return validated;
}
},
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('delete_rules', () => {
clients.savedObjectsClient.delete.mockResolvedValue({});
jest.spyOn(utils, 'transform').mockReturnValue(null);
const { payload, statusCode } = await server.inject(getDeleteRequest());
expect(JSON.parse(payload).message).toBe('Internal error transforming rules');
expect(JSON.parse(payload).message).toBe('Internal error transforming');
expect(statusCode).toBe(500);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { deleteRules } from '../../rules/delete_rules';
import { LegacyServices, LegacyRequest } from '../../../../types';
import { GetScopedClients } from '../../../../services';
import { queryRulesSchema } from '../schemas/query_rules_schema';
import { getIdError, transform } from './utils';
import { getIdError } from './utils';
import { transformValidate } from './validate';

import { transformError } from '../utils';
import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
Expand Down Expand Up @@ -57,16 +59,16 @@ export const createDeleteRulesRoute = (getClients: GetScopedClients): Hapi.Serve
ruleStatuses.saved_objects.forEach(async obj =>
savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id)
);
const transformed = transform(rule, ruleStatuses.saved_objects[0]);
if (transformed == null) {
const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]);
if (errors != null) {
return headers
.response({
message: 'Internal error transforming rules',
message: errors,
status_code: 500,
})
.code(500);
} else {
return transformed;
return validated;
}
} else {
const error = getIdError({ id, ruleId });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('find_rules', () => {
jest.spyOn(utils, 'transformFindAlerts').mockReturnValue(null);
const { payload, statusCode } = await server.inject(getFindRequest());
expect(statusCode).toBe(500);
expect(JSON.parse(payload).message).toBe('unknown data type, error transforming alert');
expect(JSON.parse(payload).message).toBe('Internal error transforming');
});

test('catch error when findRules function throws error', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { GetScopedClients } from '../../../../services';
import { findRules } from '../../rules/find_rules';
import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
import { findRulesSchema } from '../schemas/find_rules_schema';
import { transformFindAlerts } from './utils';
import { transformValidateFindAlerts } from './validate';

import { transformError } from '../utils';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';

Expand Down Expand Up @@ -59,16 +60,16 @@ export const createFindRulesRoute = (getClients: GetScopedClients): Hapi.ServerR
return results;
})
);
const transformed = transformFindAlerts(rules, ruleStatuses);
if (transformed == null) {
const [validated, errors] = transformValidateFindAlerts(rules, ruleStatuses);
if (errors != null) {
return headers
.response({
message: 'unknown data type, error transforming alert',
message: errors,
status_code: 500,
})
.code(500);
} else {
return transformed;
return validated;
}
} catch (err) {
const error = transformError(err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import { getRulesToInstall } from '../../rules/get_rules_to_install';
import { getRulesToUpdate } from '../../rules/get_rules_to_update';
import { findRules } from '../../rules/find_rules';
import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules';
import {
PrePackagedRulesStatusSchema,
prePackagedRulesStatusSchema,
} from '../schemas/response/prepackaged_rules_status_schema';
import { validate } from './validate';

export const createGetPrepackagedRulesStatusRoute = (
getClients: GetScopedClients
Expand Down Expand Up @@ -50,12 +55,23 @@ export const createGetPrepackagedRulesStatusRoute = (
const prepackagedRules = await getExistingPrepackagedRules({ alertsClient });
const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules);
const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules);
return {
const prepackagedRulesStatus: PrePackagedRulesStatusSchema = {
rules_custom_installed: customRules.total,
rules_installed: prepackagedRules.length,
rules_not_installed: rulesToInstall.length,
rules_not_updated: rulesToUpdate.length,
};
const [validated, errors] = validate(prepackagedRulesStatus, prePackagedRulesStatusSchema);
if (errors != null) {
return headers
.response({
message: errors,
status_code: 500,
})
.code(500);
} else {
return validated;
}
} catch (err) {
const error = transformError(err);
return headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import Hapi from 'hapi';
import { chunk, isEmpty } from 'lodash/fp';
import { chunk } from 'lodash/fp';
import { extname } from 'path';
import { Readable } from 'stream';

Expand All @@ -16,13 +16,23 @@ import { createRules } from '../../rules/create_rules';
import { ImportRulesRequest } from '../../rules/types';
import { readRules } from '../../rules/read_rules';
import { getIndexExists } from '../../index/get_index_exists';
import { getIndex, transformError, createBulkErrorObject, ImportRuleResponse } from '../utils';
import {
getIndex,
createBulkErrorObject,
ImportRuleResponse,
BulkError,
isBulkError,
isImportRegular,
transformError,
} from '../utils';
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
import { ImportRuleAlertRest } from '../../types';
import { patchRules } from '../../rules/patch_rules';
import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema';
import { getTupleDuplicateErrorsAndUniqueRules } from './utils';
import { validate } from './validate';
import { GetScopedClients } from '../../../../services';
import { ImportRulesSchema, importRulesSchema } from '../schemas/response/import_rules_schema';

type PromiseFromStreams = ImportRuleAlertRest | Error;

Expand Down Expand Up @@ -246,12 +256,30 @@ export const createImportRulesRoute = (
];
}

const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error));
return {
const errorsResp = importRuleResponse.filter(resp => isBulkError(resp)) as BulkError[];
const successes = importRuleResponse.filter(resp => {
if (isImportRegular(resp)) {
return resp.status_code === 200;
} else {
return false;
}
});
const importRules: ImportRulesSchema = {
success: errorsResp.length === 0,
success_count: importRuleResponse.filter(resp => resp.status_code === 200).length,
success_count: successes.length,
errors: errorsResp,
};
const [validated, errors] = validate(importRules, importRulesSchema);
if (errors != null) {
return headers
.response({
message: errors,
status_code: 500,
})
.code(500);
} else {
return validated;
}
} catch (exc) {
const error = transformError(exc);
return headers
Expand Down
Loading

0 comments on commit d5eb175

Please sign in to comment.