feat: validate rules while loading
This commit is contained in:
parent
1b15952c05
commit
dbfe0429fa
|
@ -27,6 +27,7 @@ const bundle = {
|
||||||
external,
|
external,
|
||||||
input: {
|
input: {
|
||||||
include: [
|
include: [
|
||||||
|
// join(rootPath, 'rules', '*.yml'),
|
||||||
join(rootPath, 'src', 'index.ts'),
|
join(rootPath, 'src', 'index.ts'),
|
||||||
join(rootPath, 'test', 'harness.ts'),
|
join(rootPath, 'test', 'harness.ts'),
|
||||||
join(rootPath, 'test', '**', 'Test*.ts'),
|
join(rootPath, 'test', '**', 'Test*.ts'),
|
||||||
|
@ -49,7 +50,7 @@ const bundle = {
|
||||||
return 'index';
|
return 'index';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.includes(`${sep}src${sep}`)) {
|
if (id.includes(`${sep}src${sep}`) || id.includes(`${sep}rules${sep}`)) {
|
||||||
return 'main';
|
return 'main';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
## v0.9
|
||||||
|
|
||||||
|
- architecture: split up main into commands
|
||||||
|
- architecture: wire up dependency injection with noicejs
|
||||||
|
- feature: visitor events (#21)
|
||||||
|
- feature: improve diff messages (#133)
|
||||||
|
- feature: interactive diffs (#9, requires #21 and #133)
|
||||||
|
- feature: instantiate rules based on type (#113, requires #?)
|
||||||
|
- fix: pass structured errors
|
||||||
|
- fix: type or eliminate flash data (#137)
|
||||||
|
|
||||||
|
## v1.0
|
||||||
|
|
||||||
|
- fix: all the bugs
|
|
@ -1,5 +1,13 @@
|
||||||
name: salty-dog-meta
|
name: salty-dog-meta
|
||||||
definitions:
|
definitions:
|
||||||
|
name-safe:
|
||||||
|
type: string
|
||||||
|
pattern: "[-a-z0-9]+"
|
||||||
|
|
||||||
|
name-tag:
|
||||||
|
type: string
|
||||||
|
pattern: "[-:a-z0-9]+"
|
||||||
|
|
||||||
rule:
|
rule:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
@ -13,8 +21,7 @@ definitions:
|
||||||
- check
|
- check
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
$ref: "salty-dog-meta#/definitions/name-safe"
|
||||||
pattern: "[-a-z0-9]+"
|
|
||||||
desc:
|
desc:
|
||||||
type: string
|
type: string
|
||||||
minLength: 8
|
minLength: 8
|
||||||
|
@ -29,8 +36,7 @@ definitions:
|
||||||
tags:
|
tags:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
$ref: "salty-dog-meta#/definitions/name-tag"
|
||||||
pattern: "[-:a-z0-9]+"
|
|
||||||
select:
|
select:
|
||||||
type: string
|
type: string
|
||||||
default: '$'
|
default: '$'
|
||||||
|
@ -40,6 +46,25 @@ definitions:
|
||||||
check:
|
check:
|
||||||
$ref: "http://json-schema.org/draft-07/schema#"
|
$ref: "http://json-schema.org/draft-07/schema#"
|
||||||
|
|
||||||
|
source:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- rules
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
$ref: "salty-dog-meta#/definitions/name-safe"
|
||||||
|
definitions:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
patternProperties:
|
||||||
|
"[-a-z]+":
|
||||||
|
type: object
|
||||||
|
rules:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "salty-dog-meta#/definitions/rule"
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
- name: salty-dog-rule
|
- name: salty-dog-rule
|
||||||
desc: rules must be complete
|
desc: rules must be complete
|
||||||
|
@ -60,21 +85,10 @@ rules:
|
||||||
- salty-dog
|
- salty-dog
|
||||||
|
|
||||||
check:
|
check:
|
||||||
type: object
|
allOf:
|
||||||
additionalProperties: false
|
- $ref: "salty-dog-meta#/definitions/source"
|
||||||
required: [name, rules]
|
- type: object
|
||||||
properties:
|
properties:
|
||||||
definitions:
|
rules:
|
||||||
type: object
|
type: array
|
||||||
additionalProperties: false
|
minItems: 1
|
||||||
patternProperties:
|
|
||||||
"[-a-z]+":
|
|
||||||
type: object
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
pattern: "[-a-z]+"
|
|
||||||
rules:
|
|
||||||
type: array
|
|
||||||
minItems: 1
|
|
||||||
items:
|
|
||||||
$ref: "salty-dog-meta#/definitions/rule"
|
|
|
@ -4,6 +4,7 @@ import { Minimatch } from 'minimatch';
|
||||||
import { LogLevel } from 'noicejs';
|
import { LogLevel } from 'noicejs';
|
||||||
import recursive from 'recursive-readdir';
|
import recursive from 'recursive-readdir';
|
||||||
|
|
||||||
|
import ruleSchemaData from '../../rules/salty-dog.yml';
|
||||||
import { YamlParser } from '../parser/YamlParser';
|
import { YamlParser } from '../parser/YamlParser';
|
||||||
import { readFile } from '../source';
|
import { readFile } from '../source';
|
||||||
import { ensureArray } from '../utils';
|
import { ensureArray } from '../utils';
|
||||||
|
@ -107,6 +108,14 @@ export async function loadRuleFiles(paths: Array<string>, ctx: VisitorContext):
|
||||||
const docs = parser.parse(contents) as Array<RuleSourceData>;
|
const docs = parser.parse(contents) as Array<RuleSourceData>;
|
||||||
|
|
||||||
for (const data of docs) {
|
for (const data of docs) {
|
||||||
|
if (!validateRules(ctx, data)) {
|
||||||
|
ctx.logger.error({
|
||||||
|
file: data,
|
||||||
|
path,
|
||||||
|
}, 'error loading rule file');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isNil(data.definitions)) {
|
if (!isNil(data.definitions)) {
|
||||||
ctx.addSchema(data.name, data.definitions);
|
ctx.addSchema(data.name, data.definitions);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +156,12 @@ export async function loadRuleModules(modules: Array<string>, ctx: VisitorContex
|
||||||
try {
|
try {
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
|
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
|
||||||
const module: RuleSourceModule = r(name);
|
const module: RuleSourceModule = r(name);
|
||||||
// TODO: ensure module has definitions, name, and rules
|
if (!validateRules(ctx, module)) {
|
||||||
|
ctx.logger.error({
|
||||||
|
module: name,
|
||||||
|
}, 'error loading rule module');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isNil(module.definitions)) {
|
if (!isNil(module.definitions)) {
|
||||||
ctx.addSchema(module.name, module.definitions);
|
ctx.addSchema(module.name, module.definitions);
|
||||||
|
@ -193,3 +207,18 @@ export async function resolveRules(rules: Array<Rule>, selector: RuleSelector):
|
||||||
|
|
||||||
return Array.from(activeRules);
|
return Array.from(activeRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateRules(ctx: VisitorContext, root: any): boolean {
|
||||||
|
const { definitions, name } = ruleSchemaData as any;
|
||||||
|
|
||||||
|
const validCtx = new VisitorContext(ctx);
|
||||||
|
validCtx.addSchema(name, definitions);
|
||||||
|
const ruleSchema = validCtx.compile(definitions.source);
|
||||||
|
|
||||||
|
if (ruleSchema(root) === true) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
ctx.logger.error({ errors: ruleSchema.errors }, 'error validating rules');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ const TEST_FILES = {
|
||||||
name: test,
|
name: test,
|
||||||
rules: [{
|
rules: [{
|
||||||
name: test,
|
name: test,
|
||||||
desc: test,
|
desc: test-rule,
|
||||||
level: info,
|
level: info,
|
||||||
tags: [test],
|
tags: [test],
|
||||||
check: {
|
check: {
|
||||||
|
|
|
@ -13,6 +13,7 @@ const EXAMPLE_RULES = `{
|
||||||
definitions: {},
|
definitions: {},
|
||||||
rules: [{
|
rules: [{
|
||||||
name: test,
|
name: test,
|
||||||
|
desc: test-rule,
|
||||||
level: info,
|
level: info,
|
||||||
tags: []
|
tags: []
|
||||||
}]
|
}]
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { LogLevel } from 'noicejs';
|
import { ConsoleLogger, LogLevel, NullLogger } from 'noicejs';
|
||||||
|
|
||||||
import { createRuleSelector, createRuleSources, resolveRules } from '../../src/rule';
|
import { createRuleSelector, createRuleSources, resolveRules, validateRules } from '../../src/rule';
|
||||||
import { SchemaRule } from '../../src/rule/SchemaRule';
|
import { SchemaRule } from '../../src/rule/SchemaRule';
|
||||||
|
import { VisitorContext } from '../../src/visitor/VisitorContext';
|
||||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||||
|
|
||||||
const TEST_RULES = [new SchemaRule({
|
const TEST_RULES = [new SchemaRule({
|
||||||
|
@ -133,3 +134,37 @@ describe('create rule selector helper', () => {
|
||||||
expect(sources).to.have.deep.property('includeTag', []);
|
expect(sources).to.have.deep.property('includeTag', []);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describeLeaks('validate rule helper', async () => {
|
||||||
|
itLeaks('should accept valid modules', async () => {
|
||||||
|
const ctx = new VisitorContext({
|
||||||
|
logger: ConsoleLogger.global,
|
||||||
|
schemaOptions: {
|
||||||
|
coerce: false,
|
||||||
|
defaults: false,
|
||||||
|
mutate: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(validateRules(ctx, {
|
||||||
|
name: 'test',
|
||||||
|
rules: [],
|
||||||
|
})).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
itLeaks('should reject partial modules', async () => {
|
||||||
|
const ctx = new VisitorContext({
|
||||||
|
logger: NullLogger.global,
|
||||||
|
schemaOptions: {
|
||||||
|
coerce: false,
|
||||||
|
defaults: false,
|
||||||
|
mutate: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(validateRules(ctx, {})).to.equal(false);
|
||||||
|
expect(validateRules(ctx, {
|
||||||
|
name: '',
|
||||||
|
})).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue