import { Options, Severity, toSeverity } from "./options"; import ComputeUsage from "./rules/compute-usage"; import GroupName from "./rules/group-name"; import GroupMembers from "./rules/group-members"; import LockWithoutDescription from "./rules/lock-without-description"; import ResourceCount from "./rules/resource-count"; import ResourceGroupName from "./rules/resource-group-name"; import ResourceGroupTag from "./rules/resource-group-tag"; import ResourceName from "./rules/resource-name"; import VmCpuMetrics from "./rules/vm-cpu-metrics"; import ApplicationName from "./rules/application-name"; import UserEmail from "./rules/user-email"; import TestApplication from "./rules/test-application"; import { Rule } from "./rule"; import { getClassName, getFileName, readFile, toArray } from "./utils"; import { Client, createClient } from "./client"; import { Context, Problem } from "./context"; import { resolve } from "path"; import TestResourceGroup from "./rules/test-resource-group"; import { logger } from "./common/logger"; const messagesPath = resolve(__dirname, "../messages"); export type ConfigOption = { severity?: Severity; [key: string]: any; }; export type Config = { appId: string; secret: string; tenant: string; extends?: string | string[]; rules?: { [key: string]: string | ConfigOption | (string | ConfigOption)[]; }; }; const ruleTypes = [ ApplicationName, ComputeUsage, GroupName, GroupMembers, LockWithoutDescription, ResourceCount, ResourceGroupName, ResourceGroupTag, ResourceName, TestApplication, TestResourceGroup, UserEmail, VmCpuMetrics ]; function getRuleType(name: string) { const className = getClassName(name); for (const type of ruleTypes) { if (type.name === className) { return type; } } throw new Error(`Rule not found: ${name}`); } class Messages { private readonly locale; private readonly messages = {}; constructor(locale: string) { this.locale = locale; } async load() { for (const type of ruleTypes) { const fileName = getFileName(type.name); const str = await readFile(resolve(messagesPath, `${fileName}.json`)); this.messages[type.name] = JSON.parse(str); } } getMessage(rule: string, message: string, data: any) { if (!this.messages[rule]) { throw new Error(`${rule}: Missing messages!`); } const msg: string = this.messages[rule][message]; if (!msg) { throw new Error(`${rule}: Missing message: ${message}`); } return msg.replace(/{{([a-zA-Z]+)}}/g, (m, key) => { if (data == null) { throw new Error(`${rule}: Missing data for ${message}!`); } else if (data[key] == null) { throw new Error(`${rule}: Missing data for ${message}: ${key}`); } const value = data[key]; if (typeof value === "number") { return value.toLocaleString(this.locale); } else { return value.toString(); } }); } } function toConfigOption(obj: string | ConfigOption): ConfigOption { if (typeof obj === "string") { return { severity: toSeverity(obj) }; } else if (obj && typeof obj === "object") { return obj; } else { throw new Error(`Invalid rule configuration: ${obj}`); } } function getRules( context: Context, config: Config, defaultSeverity: Severity ): Rule[] { const rules: Rule[] = []; if (!config.rules) { return rules; } for (const key of Object.keys(config.rules)) { const RuleType = getRuleType(key); const configOptions = toArray(config.rules[key]).map(toConfigOption); for (const configOption of configOptions) { let rule; try { rule = new RuleType( context, new Options(configOption.severity || defaultSeverity, configOption) ); } catch (err) { throw new Error(`Cannot parse ${key}: ${err.message}`); } rules.push(rule); } } return rules; } type ClientProvider = (config: Config) => Promise; export class Linter { private readonly config: Config; private readonly clientProvider: ClientProvider; constructor(config: Config, clientProvider: ClientProvider = createClient) { this.config = config; this.clientProvider = clientProvider; } async run(): Promise { const context = new Context(); const rules: Rule[] = getRules(context, this.config, "warning"); const messages = new Messages("en"); await messages.load(); const client = await this.clientProvider(this.config); for (const rule of rules) { if (rule.options.severity !== "off") { logger.info("Running rule:", rule.name); await rule.run(client); } } logger.info("All rules ran successfully!"); for (const problem of context.problems) { problem.message = messages.getMessage( problem.rule, problem.messageId, problem.data ); } return context.problems; } }