import { JSONPath } from 'jsonpath-plus'; import { cloneDeep, intersection, isNil } from 'lodash'; import { LogLevel } from 'noicejs'; import { YamlParser } from 'src/parser/YamlParser'; import { readFileSync } from 'src/source'; import { Visitor } from 'src/visitor'; import { VisitorContext } from 'src/visitor/context'; export interface RuleData { // metadata desc: string; level: LogLevel; name: string; tags: Array; // data check: any; filter?: any; select: string; } export interface RuleSelector { excludeLevel: Array; excludeName: Array; excludeTag: Array; includeLevel: Array; includeName: Array; includeTag: Array; } export async function loadRules(paths: Array): Promise> { const parser = new YamlParser(); const rules = []; for (const path of paths) { const contents = await readFileSync(path, { encoding: 'utf-8', }); const data = parser.parse(contents); rules.push(...data.rules.map((data: any) => new Rule(data))); } return rules; } export async function resolveRules(rules: Array, selector: RuleSelector): Promise> { const activeRules = new Set(); for (const r of rules) { if (selector.excludeLevel.includes(r.level)) { continue; } if (selector.excludeName.includes(r.name)) { continue; } const excludedTags = intersection(selector.excludeTag, r.tags); if (excludedTags.length > 0) { continue; } if (selector.includeLevel.includes(r.level)) { activeRules.add(r); } if (selector.includeName.includes(r.name)) { activeRules.add(r); } const includedTags = intersection(selector.includeTag, r.tags); if (includedTags.length > 0) { activeRules.add(r); } } return Array.from(activeRules); } export class Rule implements RuleData, Visitor { public readonly check: any; public readonly desc: string; public readonly filter?: any; public readonly level: LogLevel; public readonly name: string; public readonly select: string; public readonly tags: string[]; constructor(data: RuleData) { this.desc = data.desc; this.level = data.level; this.name = data.name; this.select = data.select; this.tags = Array.from(data.tags); // copy schema objects this.check = cloneDeep(data.check); if (!isNil(data.filter)) { this.filter = cloneDeep(data.filter); } } public async visit(ctx: VisitorContext, node: any): Promise { const check = ctx.ajv.compile(this.check); const filter = this.compileFilter(ctx); const scopes = JSONPath({ json: node, path: this.select, }); if (isNil(scopes) || scopes.length === 0) { ctx.logger.debug('no data selected'); return ctx; } for (const item of scopes) { ctx.logger.debug({ item }, 'filtering item'); if (filter(item)) { ctx.logger.debug({ item }, 'checking item') if (!check(item)) { ctx.logger.warn({ name: this.name, item, }, 'rule failed on item'); ctx.errors.push(...check.errors); return ctx; } } else { ctx.logger.debug({ errors: filter.errors, item }, 'skipping item'); } } return ctx; } protected compileFilter(ctx: VisitorContext): any { if (isNil(this.filter)) { return () => true; } else { return ctx.ajv.compile(this.filter); } } }