diff --git a/package-lock.json b/package-lock.json index 15c3b0ea4..9ece3f196 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28032,7 +28032,7 @@ }, "packages/components": { "name": "@monokle/components", - "version": "1.8.1", + "version": "2.0.0", "license": "MIT", "dependencies": { "react-fast-compare": "^3.2.1" @@ -28040,7 +28040,7 @@ "devDependencies": { "@ant-design/icons": "4.7.0", "@babel/core": "7.17.8", - "@monokle/validation": "0.25.4", + "@monokle/validation": "0.26.0", "@rjsf/antd": "5.0.0-beta.11", "@storybook/addon-actions": "6.5.16", "@storybook/addon-essentials": "6.5.16", @@ -28124,10 +28124,10 @@ "license": "MIT" }, "packages/monaco-kubernetes": { - "version": "0.2.12", + "version": "0.2.13", "license": "MIT", "dependencies": { - "@monokle/validation": "^0.25.0", + "@monokle/validation": "^0.26.0", "@types/json-schema": "^7.0.0", "jsonc-parser": "^3.0.0", "monaco-marker-data-provider": "^1.0.0", @@ -28245,7 +28245,7 @@ }, "packages/parser": { "name": "@monokle/parser", - "version": "0.0.1", + "version": "0.1.0", "license": "MIT", "dependencies": { "lodash": "4.17.21", @@ -28317,7 +28317,7 @@ }, "packages/synchronizer": { "name": "@monokle/synchronizer", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "dependencies": { "@monokle/types": "*", @@ -28957,7 +28957,7 @@ }, "packages/validation": { "name": "@monokle/validation", - "version": "0.25.4", + "version": "0.26.0", "license": "MIT", "dependencies": { "@monokle/types": "*", @@ -31847,7 +31847,7 @@ "requires": { "@ant-design/icons": "4.7.0", "@babel/core": "7.17.8", - "@monokle/validation": "0.25.4", + "@monokle/validation": "0.26.0", "@rjsf/antd": "5.0.0-beta.11", "@storybook/addon-actions": "6.5.16", "@storybook/addon-essentials": "6.5.16", @@ -43862,7 +43862,7 @@ "monaco-kubernetes": { "version": "file:packages/monaco-kubernetes", "requires": { - "@monokle/validation": "^0.25.0", + "@monokle/validation": "^0.26.0", "@types/json-schema": "^7.0.0", "@types/uuid": "9.0.0", "esbuild": "^0.15.0", diff --git a/packages/validation/src/MonokleValidator.ts b/packages/validation/src/MonokleValidator.ts index 3c23c50eb..11b11fa06 100644 --- a/packages/validation/src/MonokleValidator.ts +++ b/packages/validation/src/MonokleValidator.ts @@ -4,71 +4,36 @@ import difference from 'lodash/difference.js'; import isEqual from 'lodash/isEqual.js'; import {ResourceParser} from './common/resourceParser.js'; import type {Suppression, Tool, ValidationResponse, ValidationResult, ValidationRun} from './common/sarif.js'; -import type {CustomSchema, Incremental, Plugin, Resource} from './common/types.js'; +import type {CustomSchema, Plugin, Resource} from './common/types.js'; import {Config} from './config/parse.js'; -import {CIS_TAXONOMY} from './taxonomies/cis.js'; -import {NSA_TAXONOMY} from './taxonomies/nsa.js'; -import {PluginMetadataWithConfig, PluginName, RuleMetadataWithConfig, Validator} from './types.js'; +import {CIS_TAXONOMY, NSA_TAXONOMY} from './taxonomies'; +import {PluginMetadataWithConfig, PluginName, RuleMetadataWithConfig, ValidateParams, Validator} from './types.js'; import {nextTick, throwIfAborted} from './utils/abort.js'; import {extractSchema, findDefaultVersion} from './utils/customResourceDefinitions.js'; import {PluginLoadError} from './utils/error.js'; import invariant from './utils/invariant.js'; import {isDefined} from './utils/isDefined.js'; -import {AnnotationSuppressor, FingerprintSuppressor, Suppressor} from './sarif/suppressions/index.js'; +import {Fixer, Suppressor} from './sarif'; import {SuppressEngine} from './sarif/suppressions/engine.js'; -import {Fixer} from './sarif/fix/index.js'; -import {SchemaLoader} from './validators/kubernetes-schema/schemaLoader.js'; +import {SchemaLoader} from './validators'; +import {PluginLoader} from './pluginLoaders/PluginLoader.js'; +import {ValidationConfig} from '@monokle/types'; +import {noop} from "lodash"; +import {PluginContext} from "./pluginLoaders/types"; -export type PluginLoader = (name: string, settings?: Record) => Promise; -export type CustomPluginLoader = (name: string, parser: ResourceParser, fixer?: Fixer) => Promise; - -type MonokleInit = { +export type ValidatorInit = { loader: PluginLoader; - parser?: ResourceParser; - suppressors?: Suppressor[]; - fixer?: Fixer; - schemaLoader?: SchemaLoader; -}; - -export function createMonokleValidator(init: MonokleInit) { - return new MonokleValidator(init); -} - -const DEFAULT_SUPPRESSORS = [new AnnotationSuppressor(), new FingerprintSuppressor()]; - -type ValidateParams = { - /** - * The resources that will be validated. - */ - resources: Resource[]; - - /** - * The list of resources that recently got updated. - * - * @remarks Validators can use this information to skip non-modified resources. - */ - incremental?: Incremental; - - /** - * A previous run which acts as the baseline for detected problems. - * - * @remark Providing a baseline will set run.baselineGuid and result.baselineStatus. - * @remark Newly fixed problems will be added as 'absent' results. - * When using baseline, it is important to properly filter or - * indicate absent results or they appear as false positives. - */ - baseline?: ValidationResponse; - - /** - * A signal that can be used to abort processing. - */ - abortSignal?: AbortSignal; + parser: ResourceParser; + suppressors: Suppressor[]; + fixer: Fixer; + schemaLoader: SchemaLoader; }; export class MonokleValidator implements Validator { _config: Config = {}; _abortController: AbortController = new AbortController(); _loading?: Promise; + _pluginContext: PluginContext; _loader: PluginLoader; _previousPluginsInit?: Record; _plugins: Plugin[] = []; @@ -77,9 +42,15 @@ export class MonokleValidator implements Validator { _suppressions: Suppression[] = []; private _suppressor: SuppressEngine; - constructor(init: MonokleInit) { + constructor(init: ValidatorInit, config?: ValidationConfig) { this._loader = init.loader; - this._suppressor = new SuppressEngine(init.suppressors ?? DEFAULT_SUPPRESSORS); + this._pluginContext = { + parser: init.parser, + fixer: init.fixer, + schemaLoader: init.schemaLoader, + } + this._suppressor = new SuppressEngine(init.suppressors ?? []); + if (config) this.preload(config).catch(noop); } get config(): Config { @@ -133,8 +104,9 @@ export class MonokleValidator implements Validator { * Eagerly load and configure the validation plugins. * * @param config - the new configuration of the validator. + * @param suppressions - a list with suppression requests. */ - async preload(config: Config): Promise { + async preload(config: Config, suppressions?: Suppression[]): Promise { this._config = config; this._suppressions = suppressions || []; return this.load(); @@ -142,7 +114,7 @@ export class MonokleValidator implements Validator { /** * Load the plugin and prepare it. - * Afterwards plugin and rule metadata are available. + * Afterward plugin and rule metadata are available. * * @see config.plugins */ @@ -181,8 +153,7 @@ export class MonokleValidator implements Validator { const loading = await Promise.allSettled( missingPlugins.map(async p => { try { - const validator = await this._loader(p, config.settings?.[p]); - return validator; + return await this._loader.load(p, this._pluginContext, config.settings?.[p]); } catch (err) { const msg = err instanceof Error ? err.message : 'reason unknown'; throw new PluginLoadError(p, msg); @@ -223,6 +194,7 @@ export class MonokleValidator implements Validator { settings: config.settings, }) ), + this._suppressor.preload(this._suppressions), ]); } @@ -435,7 +407,7 @@ export class MonokleValidator implements Validator { } /** - * Unloads the Monokle Validator so it can be used as new. + * Unloads the Monokle Validator, so it can be used as new. */ async unload(): Promise { this.cancelLoad('unload'); diff --git a/packages/validation/src/__tests__/MonokleConfiguration.test.ts b/packages/validation/src/__tests__/MonokleConfiguration.test.ts index 3c2e163e3..e0934d3ba 100644 --- a/packages/validation/src/__tests__/MonokleConfiguration.test.ts +++ b/packages/validation/src/__tests__/MonokleConfiguration.test.ts @@ -1,14 +1,15 @@ import {expect, it} from 'vitest'; -import {processRefs, ResourceParser, createDefaultMonokleValidator} from '../index.js'; +import {processRefs, ResourceParser, DisabledFixer, SchemaLoader, MonokleValidator} from '../index.js'; // Usage note: This library relies on fetch being on global scope! import 'isomorphic-fetch'; import {BAD_DEPLOYMENT, BAD_SERVICE, RESOURCES} from './badResources.js'; +import {DefaultPluginLoader} from '../pluginLoaders/PluginLoader.js'; it('should work with monokle.validation.yaml', async () => { // Step 1: Create the validator const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); + const validator = createTestValidator(parser); // Step 2: Configure validator with monokle.validation.yaml await validator.preload({ @@ -16,7 +17,6 @@ it('should work with monokle.validation.yaml', async () => { plugins: { 'open-policy-agent': true, 'yaml-syntax': true, - labels: false, 'kubernetes-schema': false, 'resource-links': false, }, @@ -49,13 +49,12 @@ it('should work with monokle.validation.yaml', async () => { it('should handle race conditions', async () => { for (let i = 0; i < 3; i++) { - const validator = createDefaultMonokleValidator(); + const validator = createTestValidator(); validator.preload({ plugins: { 'open-policy-agent': true, 'yaml-syntax': true, - labels: true, 'kubernetes-schema': true, 'resource-links': true, }, @@ -69,7 +68,6 @@ it('should handle race conditions', async () => { plugins: { 'open-policy-agent': false, 'yaml-syntax': true, - labels: false, 'kubernetes-schema': false, 'resource-links': false, }, @@ -84,7 +82,6 @@ it('should handle race conditions', async () => { plugins: { 'open-policy-agent': true, 'yaml-syntax': false, - labels: false, 'kubernetes-schema': false, 'resource-links': false, }, @@ -112,3 +109,23 @@ it('should handle race conditions', async () => { expect(hasErrors).toMatchInlineSnapshot('11'); } }); + +function createTestValidator(parser?: ResourceParser) { + return new MonokleValidator( + { + parser: parser ?? new ResourceParser(), + schemaLoader: new SchemaLoader(), + loader: new DefaultPluginLoader(), + suppressors: [], + fixer: new DisabledFixer(), + }, + { + plugins: { + 'kubernetes-schema': true, + 'yaml-syntax': true, + 'pod-security-standards': true, + 'resource-links': true, + }, + } + ); +} diff --git a/packages/validation/src/__tests__/MonokleCustom.test.ts b/packages/validation/src/__tests__/MonokleCustom.test.ts deleted file mode 100644 index 14ceb56fc..000000000 --- a/packages/validation/src/__tests__/MonokleCustom.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import {expect, it} from 'vitest'; -import {ResourceParser, createMonokleValidator, SimpleCustomValidator} from '../index.js'; -import vm from 'vm'; - -// Usage note: This library relies on fetch being on global scope! -import 'isomorphic-fetch'; -import {noop} from 'lodash'; -import {RESOURCES} from './badResources.js'; - -// This will only work if the plugin is served. -it.skip('supports dynamic custom validators', async () => { - const parser = new ResourceParser(); - - const validator = createMonokleValidator(async pluginName => { - if (pluginName !== 'annotations') { - throw new Error('validator_not_found'); - } - - const annotationPlugin = await importWithDataUrl('http://localhost:4111/plugin.js'); - return new SimpleCustomValidator(annotationPlugin.default, parser); - }); - - await validator.preload({ - plugins: {annotations: true}, - settings: { - debug: true, - whoosh: { - teams: ['dreamers', 'dancers'], - }, - }, - }); - - const response = await validator.validate({resources: RESOURCES}); - console.log(JSON.stringify(response, null, 2)); - const hasErrors = response.runs.reduce((sum, r) => sum + r.results.length, 0); - expect(hasErrors).toMatchInlineSnapshot('2'); -}); - -async function importWithDataUrl(url: string) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Error fetching ${url}: ${response.statusText}`); - } - const source = await response.text(); - const buff = Buffer.from(source); - const encodedSource = buff.toString('base64'); - const dataUrl = `data:text/javascript;base64,${encodedSource}`; - - const module = await import(dataUrl); - return module; -} - -/** - * This requires the "--experimental-vm-modules" flag to be set. - * - * You can do this with an env variable: - * @config export NODE_OPTIONS="--experimental-vm-modules" - */ -async function importWithVm(url: string) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Error fetching ${url}: ${response.statusText}`); - } - const source = await response.text(); - const context = vm.createContext({}); - const module = new (vm as any).SourceTextModule(source, { - identifier: url, - context, - }); - await module.link(noop); - await module.evaluate(); - return module.namespace; -} diff --git a/packages/validation/src/__tests__/MonokleValidator.kubernetes-schema.test.ts b/packages/validation/src/__tests__/MonokleValidator.kubernetes-schema.test.ts index f6a32681a..9f199d2a7 100644 --- a/packages/validation/src/__tests__/MonokleValidator.kubernetes-schema.test.ts +++ b/packages/validation/src/__tests__/MonokleValidator.kubernetes-schema.test.ts @@ -1,13 +1,14 @@ import {expect, it} from 'vitest'; import {MonokleValidator} from '../MonokleValidator.js'; -import {processRefs} from '../references/process.js'; +import {processRefs} from '../references'; // Usage note: This library relies on fetch being on global scope! import 'isomorphic-fetch'; import {extractK8sResources} from '@monokle/parser'; import {readDirectory, expectResult} from './testUtils.js'; import {ResourceParser} from '../common/resourceParser.js'; -import {createDefaultMonokleValidator} from '../createDefaultMonokleValidator.node.js'; +import {DisabledFixer, SchemaLoader} from '../commonExports.js'; +import {DefaultPluginLoader} from '../pluginLoaders/PluginLoader.js'; it('should detect deprecation error - single resource, removal', async () => { const {response} = await processResourcesInFolder('src/__tests__/resources/deprecations-1'); @@ -105,9 +106,7 @@ async function processResourcesInFolder(path: string, schemaVersion?: string) { }); const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); - - await configureValidator(validator, schemaVersion); + const validator = createTestValidator(parser, schemaVersion); processRefs( resources, @@ -119,19 +118,28 @@ async function processResourcesInFolder(path: string, schemaVersion?: string) { return {response, resources}; } -async function configureValidator(validator: MonokleValidator, schemaVersion = '1.24.2') { - return validator.preload({ - plugins: { - 'kubernetes-schema': true, - }, - rules: { - 'kubernetes-schema/strict-mode-violated': true, +function createTestValidator(parser: ResourceParser, schemaVersion = '1.24.2') { + return new MonokleValidator( + { + loader: new DefaultPluginLoader(), + parser, + schemaLoader: new SchemaLoader(), + suppressors: [], + fixer: new DisabledFixer(), }, - settings: { - 'kubernetes-schema': { - schemaVersion, + { + plugins: { + 'kubernetes-schema': true, }, - debug: true, - }, - }); + rules: { + 'kubernetes-schema/strict-mode-violated': true, + }, + settings: { + 'kubernetes-schema': { + schemaVersion, + }, + debug: true, + }, + } + ); } diff --git a/packages/validation/src/__tests__/MonokleValidator.metadata.test.ts b/packages/validation/src/__tests__/MonokleValidator.metadata.test.ts index 010011fd8..2885f3020 100644 --- a/packages/validation/src/__tests__/MonokleValidator.metadata.test.ts +++ b/packages/validation/src/__tests__/MonokleValidator.metadata.test.ts @@ -1,14 +1,17 @@ import {expect, it} from 'vitest'; import {MonokleValidator} from '../MonokleValidator.js'; -import {processRefs} from '../references/process.js'; +import {processRefs} from '../references'; // Usage note: This library relies on fetch being on global scope! import 'isomorphic-fetch'; import {extractK8sResources} from '@monokle/parser'; import {readDirectory, expectResult} from './testUtils.js'; import {ResourceParser} from '../common/resourceParser.js'; -import {createDefaultMonokleValidator} from '../createDefaultMonokleValidator.node.js'; import {Config, RuleMap} from '../config/parse.js'; +import {DefaultPluginLoader} from "../pluginLoaders/PluginLoader"; +import {SchemaLoader} from "../validators"; +import {DisabledFixer} from "../sarif"; +import {ValidationConfig} from "@monokle/types"; it('should detect missing recommended labels (MTD-recommended-labels)', async () => { const {response} = await processResourcesInFolder('src/__tests__/resources/metadata'); @@ -183,9 +186,7 @@ it('should detect missing recommended labels even with no metadata at all (MTD-r it('should have custom-* configurable rules', async () => { const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); - - await configureValidator(validator, { + const validator = await createTestValidator(parser, { 'metadata/recommended-labels': false, 'metadata/custom-labels': 'err', 'metadata/custom-annotations': 'err', @@ -201,9 +202,7 @@ it('should have custom-* configurable rules', async () => { it('should generate dynamic configurable rules', async () => { const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); - - await configureValidator(validator, { + const validator = await createTestValidator(parser, { 'metadata/recommended-labels': false, 'metadata/custom-labels': false, 'metadata/custom-annotations': false, @@ -225,9 +224,7 @@ async function processResourcesInFolder(path: string, rules?: RuleMap) { const resources = extractK8sResources(files); const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); - - await configureValidator(validator, rules); + const validator = await createTestValidator(parser, rules); processRefs( resources, @@ -239,7 +236,15 @@ async function processResourcesInFolder(path: string, rules?: RuleMap) { return {response, resources}; } -async function configureValidator(validator: MonokleValidator, rules?: RuleMap) { +function expectMatchList(message: string, expected: string[]) { + expected.forEach(e => expect(message).toMatch(e)); +} + +function expectNotMatchList(message: string, expected: string[]) { + expected.forEach(e => expect(message).not.toMatch(e)); +} + +async function createTestValidator(parser: ResourceParser, rules?: ValidationConfig['rules']) { const config: Config = { plugins: { metadata: true, @@ -253,13 +258,17 @@ async function configureValidator(validator: MonokleValidator, rules?: RuleMap) config.rules = rules; } - return validator.preload(config); -} + const validator = new MonokleValidator( + { + loader: new DefaultPluginLoader(), + parser, + schemaLoader: new SchemaLoader(), + suppressors: [], + fixer: new DisabledFixer(), + } + ); -function expectMatchList(message: string, expected: string[]) { - expected.forEach(e => expect(message).toMatch(e)); -} + await validator.preload(config); -function expectNotMatchList(message: string, expected: string[]) { - expected.forEach(e => expect(message).not.toMatch(e)); + return validator; } diff --git a/packages/validation/src/__tests__/MonokleValidator.pss.test.ts b/packages/validation/src/__tests__/MonokleValidator.pss.test.ts index 051287bd8..24c59ec06 100644 --- a/packages/validation/src/__tests__/MonokleValidator.pss.test.ts +++ b/packages/validation/src/__tests__/MonokleValidator.pss.test.ts @@ -1,14 +1,17 @@ import {expect, it} from 'vitest'; import {MonokleValidator} from '../MonokleValidator.js'; -import {processRefs} from '../references/process.js'; +import {processRefs} from '../references'; // Usage note: This library relies on fetch being on global scope! import 'isomorphic-fetch'; import {ResourceParser} from '../common/resourceParser.js'; import {Config, RuleMap} from '../config/parse.js'; -import {createDefaultMonokleValidator} from '../createDefaultMonokleValidator.node.js'; import {extractK8sResources} from '@monokle/parser'; import {readDirectory} from './testUtils.js'; +import {ValidationConfig} from "@monokle/types"; +import {DefaultPluginLoader} from "../pluginLoaders/PluginLoader"; +import {SchemaLoader} from "../validators"; +import {DisabledFixer} from "../sarif"; it('should detect invalid volume types', async () => { const {response} = await processResourcesInFolder('src/__tests__/resources/pss-1', { @@ -25,9 +28,7 @@ async function processResourcesInFolder(path: string, rules?: RuleMap) { const resources = extractK8sResources(files); const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); - - await configureValidator(validator, rules); + const validator = createTestValidator(parser, rules); processRefs( resources, @@ -39,7 +40,7 @@ async function processResourcesInFolder(path: string, rules?: RuleMap) { return {response, resources}; } -async function configureValidator(validator: MonokleValidator, rules?: RuleMap) { +function createTestValidator(parser: ResourceParser, rules?: ValidationConfig['rules']) { const config: Config = { plugins: { 'pod-security-standards': true, @@ -53,5 +54,14 @@ async function configureValidator(validator: MonokleValidator, rules?: RuleMap) config.rules = rules; } - return validator.preload(config); + return new MonokleValidator( + { + loader: new DefaultPluginLoader(), + parser, + schemaLoader: new SchemaLoader(), + suppressors: [], + fixer: new DisabledFixer(), + }, + config + ); } diff --git a/packages/validation/src/__tests__/MonokleValidator.test.ts b/packages/validation/src/__tests__/MonokleValidator.test.ts index b6c5c8881..432dbcdb3 100644 --- a/packages/validation/src/__tests__/MonokleValidator.test.ts +++ b/packages/validation/src/__tests__/MonokleValidator.test.ts @@ -1,7 +1,7 @@ import Ajv from 'ajv'; import {expect, it} from 'vitest'; import {MonokleValidator} from '../MonokleValidator.js'; -import {processRefs} from '../references/process.js'; +import {processRefs} from '../references'; // Usage note: This library relies on fetch being on global scope! import 'isomorphic-fetch'; @@ -10,15 +10,23 @@ import {extractK8sResources} from '@monokle/parser'; import {readDirectory} from './testUtils.js'; import {ResourceRefType} from '../common/types.js'; import {ResourceParser} from '../common/resourceParser.js'; -import {createDefaultMonokleValidator} from '../createDefaultMonokleValidator.node.js'; -import {RuleConfigMetadataType, SimpleCustomValidator, readConfig} from '../node.js'; +import {DisabledFixer, readConfig, RuleConfigMetadataType, SchemaLoader, SimpleCustomValidator} from '../node.js'; import {defineRule} from '../custom.js'; import {isDeployment} from '../validators/custom/schemas/deployment.apps.v1.js'; +import {DefaultPluginLoader} from "../pluginLoaders/PluginLoader"; +import {ValidationConfig} from "@monokle/types"; it('should be simple to configure', async () => { const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); + const validator = createTestValidator(parser, { + plugins: { + 'kubernetes-schema': true, + 'yaml-syntax': true, + 'pod-security-standards': true, + 'resource-links': true, + }, + }); processRefs(RESOURCES, parser); const response = await validator.validate({resources: RESOURCES}); @@ -30,10 +38,9 @@ it('should be simple to configure', async () => { it('should fail if optional refs are not allowed', async () => { const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); + const validator = createOptionalResourceLinksValidator(parser); processRefs(RESOURCES, parser); - await configureOptionalResourceLinksValidator(validator); const response = await validator.validate({resources: RESOURCES}); const hasErrors = response.runs.reduce((sum, r) => sum + r.results.length, 0); @@ -45,7 +52,7 @@ async function processResourcesInFolder(path: string) { const resources = extractK8sResources(files); const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); + const validator = createTestValidator(parser); processRefs( resources, @@ -68,7 +75,7 @@ it('should support relative folder paths in kustomizations', async () => { const {response} = await processResourcesInFolder('src/__tests__/resources/kustomize-with-relative-path-resources'); const hasErrors = response.runs.reduce((sum, r) => sum + r.results.length, 0); - expect(hasErrors).toBe(6); + expect(hasErrors).toBe(16); }); it('should support patches and additionalValuesFiles', async () => { @@ -79,7 +86,7 @@ it('should support patches and additionalValuesFiles', async () => { }); it('should support Kustomize Components', async () => { - const {resources, response} = await processResourcesInFolder('src/__tests__/resources/kustomize-components'); + const {resources} = await processResourcesInFolder('src/__tests__/resources/kustomize-components'); expect(resources.length).toBe(3); }); @@ -102,7 +109,7 @@ it('should support ownerRefs', async () => { it('should be flexible to configure', async () => { const parser = new ResourceParser(); - const validator = createDefaultMonokleValidator(parser); + const validator = createTestValidator(parser); processRefs(RESOURCES, parser); await configureValidator(validator); @@ -121,43 +128,54 @@ it('should be flexible to configure', async () => { it('should allow rules to be configurable', async () => { const parser = new ResourceParser(); + const pluginLoader = new DefaultPluginLoader(); - const validator = new MonokleValidator(async () => { + pluginLoader.register("practices", ({parser, fixer}) => { return new SimpleCustomValidator( - { - id: 'KBP', - name: 'practices', - description: 'debug-validator', - rules: { - highAvailable: defineRule({ - id: 6, - description: 'Require at least two replicas', - fullDescription: 'High availability avoids downtimes when a pod crashes.', - help: "Set your deployment's replicas to two or higher.", - advanced: { - enabled: false, - severity: 3, - configMetadata: { - type: RuleConfigMetadataType.Number, - name: 'Required replicas', - defaultValue: 1, + { + id: 'KBP', + name: 'practices', + description: 'debug-validator', + rules: { + highAvailable: defineRule({ + id: 6, + description: 'Require at least two replicas', + fullDescription: 'High availability avoids downtimes when a pod crashes.', + help: "Set your deployment's replicas to two or higher.", + advanced: { + enabled: false, + severity: 3, + configMetadata: { + type: RuleConfigMetadataType.Number, + name: 'Required replicas', + defaultValue: 1, + }, }, - }, - validate({resources, params}, {report}) { - resources.filter(isDeployment).forEach(deployment => { - const replicaCount = deployment.spec?.replicas ?? 1; - const replicaThreshold = params; - const valid = replicaCount > replicaThreshold; - if (valid) return; - report(deployment, {path: 'spec.replicas'}); - }); - }, - }), + validate({resources, params}, {report}) { + resources.filter(isDeployment).forEach(deployment => { + const replicaCount = deployment.spec?.replicas ?? 1; + const valid = replicaCount > params; + if (valid) return; + report(deployment, {path: 'spec.replicas'}); + }); + }, + }), + }, }, - }, - parser + parser, + fixer ); - }); + }) + + const validator = new MonokleValidator( + { + loader: pluginLoader, + parser, + schemaLoader: new SchemaLoader(), + suppressors: [], + fixer: new DisabledFixer(), + } + ); processRefs(RESOURCES, parser); @@ -214,7 +232,7 @@ it('should be valid SARIF', async () => { const parser = new ResourceParser(); const resources = RESOURCES; - const validator = createDefaultMonokleValidator(parser); + const validator = createTestValidator(parser); processRefs(resources, parser); await configureValidator(validator, {metadata: true}); const response = await validator.validate({resources}); @@ -263,6 +281,8 @@ it('should correctly read config file #2', async () => { expect(rule2).toBe(true); }); + + function configureValidator(validator: MonokleValidator, additionalPlugins: {[key: string]: boolean} = {}) { return validator.preload({ plugins: { @@ -281,13 +301,50 @@ function configureValidator(validator: MonokleValidator, additionalPlugins: {[ke }); } -function configureOptionalResourceLinksValidator(validator: MonokleValidator) { - return validator.preload({ - plugins: { - 'resource-links': true, - }, - rules: { - 'resource-links/no-missing-optional-links': 'warn', - }, - }); + +function createTestValidator(parser: ResourceParser, config?: ValidationConfig) { + return new MonokleValidator( + { + loader: new DefaultPluginLoader(), + parser, + schemaLoader: new SchemaLoader(), + suppressors: [], + fixer: new DisabledFixer(), + }, + config ?? { + plugins: { + 'yaml-syntax': true, + 'resource-links': true, + 'kubernetes-schema': true, + 'open-policy-agent': true + }, + settings: { + 'kubernetes-schema': { + schemaVersion: '1.24.2', + }, + debug: true, + }, + } + ); +} + + +function createOptionalResourceLinksValidator(parser: ResourceParser) { + return new MonokleValidator( + { + loader: new DefaultPluginLoader(), + parser, + schemaLoader: new SchemaLoader(), + suppressors: [], + fixer: new DisabledFixer(), + }, + { + plugins: { + 'resource-links': true, + }, + rules: { + 'resource-links/no-missing-optional-links': 'warn', + }, + } + ); } diff --git a/packages/validation/src/__tests__/sarif/suppression.test.ts b/packages/validation/src/__tests__/sarif/suppression.test.ts index acad37ad3..7b7da36be 100644 --- a/packages/validation/src/__tests__/sarif/suppression.test.ts +++ b/packages/validation/src/__tests__/sarif/suppression.test.ts @@ -1,26 +1,24 @@ import 'isomorphic-fetch'; import {expect, it} from 'vitest'; -import {createDefaultMonokleValidator} from '../../index.js'; +import { + AnnotationSuppressor, + createDefaultMonokleValidator, + DisabledFixer, + MonokleValidator, + ResourceParser, + SchemaLoader, Suppressor +} from '../../index.js'; import {extractK8sResources} from '@monokle/parser'; import {PRACTICES_ALL_DISABLED, readDirectory} from '../testUtils.js'; import {FakeSuppressor} from '../../sarif/suppressions/plugins/FakeSuppressor.js'; import YAML from 'yaml'; import {set} from 'lodash'; +import {DefaultPluginLoader} from "../../pluginLoaders/PluginLoader"; it('supports suppress requests', async () => { const suppressor = new FakeSuppressor(); - const validator = createDefaultMonokleValidator(undefined, undefined, [suppressor]); - - await validator.preload({ - plugins: { - practices: true, - }, - rules: { - ...PRACTICES_ALL_DISABLED, - 'practices/no-latest-image': 'err', - }, - }); + const validator = createTestValidator(suppressor); // Given a problem const files = await readDirectory('./src/__tests__/resources/basic-deployment'); @@ -48,18 +46,8 @@ it('supports suppress requests', async () => { expect(problem.suppressions?.length).toBe(1); }); -it('in-line annotation suppressions by default', async () => { - const validator = createDefaultMonokleValidator(); - - await validator.preload({ - plugins: { - practices: true, - }, - rules: { - ...PRACTICES_ALL_DISABLED, - 'practices/no-latest-image': 'err', - }, - }); +it('supports annotation suppressions', async () => { + const validator = createTestValidator(new AnnotationSuppressor()); // Given a problem const files = await readDirectory('./src/__tests__/resources/basic-deployment'); @@ -82,3 +70,25 @@ it('in-line annotation suppressions by default', async () => { const problem = editedResponse.runs[0].results[0]; expect(problem.suppressions?.length).toBe(1); }); + + +function createTestValidator(suppressor: Suppressor) { + return new MonokleValidator( + { + loader: new DefaultPluginLoader(), + parser: new ResourceParser(), + schemaLoader: new SchemaLoader(), + suppressors: [suppressor], + fixer: new DisabledFixer(), + }, + { + plugins: { + practices: true, + }, + rules: { + ...PRACTICES_ALL_DISABLED, + 'practices/no-latest-image': 'err', + }, + } + ); +} \ No newline at end of file diff --git a/packages/validation/src/__tests__/testUtils.ts b/packages/validation/src/__tests__/testUtils.ts index 1a65c5d70..ccba36be6 100644 --- a/packages/validation/src/__tests__/testUtils.ts +++ b/packages/validation/src/__tests__/testUtils.ts @@ -1,10 +1,9 @@ - -import glob from 'tiny-glob'; +import type {BaseFile} from '@monokle/parser'; import {readFile as readFileFromFs} from 'fs/promises'; import chunkArray from 'lodash/chunk.js'; -import {ValidationResult} from '../index.js'; +import glob from 'tiny-glob'; import {expect} from 'vitest'; -import type {BaseFile} from '@monokle/parser'; +import {ValidationResult} from '../index.js'; export async function readDirectory(directoryPath: string): Promise { const filePaths = await glob(`${directoryPath}/**/*.{yaml,yml}`); diff --git a/packages/validation/src/common/AbstractPlugin.ts b/packages/validation/src/common/AbstractPlugin.ts index 579c2fdf3..630739df6 100644 --- a/packages/validation/src/common/AbstractPlugin.ts +++ b/packages/validation/src/common/AbstractPlugin.ts @@ -1,7 +1,7 @@ import keyBy from 'lodash/keyBy.js'; import {PrimitiveRuleValue, RuleMap} from '../config/parse.js'; import {NOT_CONFIGURED_ERR_MSG} from '../constants.js'; -import {PluginMetadataWithConfig, RuleMetadataWithConfig} from '../types.js'; +import {PluginMetadataWithConfig, RuleMetadataWithConfig} from '../core/types.js'; import invariant from '../utils/invariant.js'; import {getResourceId} from '../utils/sarif.js'; import {ValidationResult, RuleMetadata, RuleConfig, ToolPlugin} from './sarif.js'; diff --git a/packages/validation/src/common/types.ts b/packages/validation/src/common/types.ts index 73af866ae..68aa0c271 100644 --- a/packages/validation/src/common/types.ts +++ b/packages/validation/src/common/types.ts @@ -4,7 +4,6 @@ import {PluginMetadataWithConfig, RuleMetadataWithConfig} from '../types.js'; import {ResourceSchema} from '../validators/kubernetes-schema/schemaLoader.js'; import {ResourceParser} from './resourceParser.js'; import {ToolPlugin, ValidationPolicy, ValidationResult} from './sarif.js'; -import {Suppressor} from '../sarif/suppressions/types.js'; export type YamlPath = Array; diff --git a/packages/validation/src/commonExports.ts b/packages/validation/src/commonExports.ts index de5d20cc9..ca9eb8ebe 100644 --- a/packages/validation/src/commonExports.ts +++ b/packages/validation/src/commonExports.ts @@ -3,23 +3,16 @@ export * from './common/sarif.js'; export * from './common/types.js'; export * from './common/NodeWrapper.js'; -export * from './references/index.js'; +export * from './common/resourceParser.js'; + export * from './types.js'; export * from './utils/getRule.js'; export * from './utils/sarif.js'; export {CORE_PLUGINS} from './constants.js'; export * from './MonokleValidator.js'; -export * from './common/resourceParser.js'; - -export * from './validators/custom/simpleValidator.js'; -export * from './validators/custom/devValidator.js'; -export * from './validators/custom/constants.js'; -export * from './validators/labels/plugin.js'; -export * from './validators/open-policy-agent/index.js'; -export * from './validators/kubernetes-schema/index.js'; -export * from './validators/yaml-syntax/index.js'; -export * from './validators/resource-links/index.js'; -export * from './validators/metadata/index.js'; -export * from './references/process.js'; +export * from './pluginLoaders/index.js'; +export * from './references/index.js'; +export * from './sarif/index.js'; +export * from './validators/index.js'; diff --git a/packages/validation/src/createDefaultMonokleValidator.browser.ts b/packages/validation/src/createDefaultMonokleValidator.browser.ts deleted file mode 100644 index 4e17494f2..000000000 --- a/packages/validation/src/createDefaultMonokleValidator.browser.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {ResourceParser} from './common/resourceParser.js'; -import {SimpleCustomValidator} from './validators/custom/simpleValidator.js'; -import {SchemaLoader} from './validators/kubernetes-schema/schemaLoader.js'; -import {KubernetesSchemaValidator} from './validators/kubernetes-schema/validator.js'; -import {RemoteWasmLoader} from './wasmLoader/RemoteWasmLoader.browser.js'; -import {OpenPolicyAgentValidator} from './validators/open-policy-agent/validator.js'; -import {ResourceLinksValidator} from './validators/resource-links/validator.js'; -import {YamlValidator} from './validators/yaml-syntax/validator.js'; -import {MetadataValidator} from './validators/metadata/validator.js'; -import {MonokleValidator} from './MonokleValidator.js'; -import kbpPlugin from './validators/practices/plugin.js'; -import pssPlugin from './validators/pod-security-standards/plugin.js'; -import {Suppressor} from './sarif/suppressions/index.js'; -import {Fixer} from './sarif/fix/index.js'; - -export function createDefaultMonokleValidator( - parser: ResourceParser = new ResourceParser(), - schemaLoader: SchemaLoader = new SchemaLoader(), - suppressors?: Suppressor[], - fixer?: Fixer -) { - return new MonokleValidator({ - fixer, - parser, - schemaLoader, - suppressors, - loader: createDefaultPluginLoader(parser, schemaLoader), - }); -} - -export function createDefaultPluginLoader( - parser: ResourceParser = new ResourceParser(), - schemaLoader: SchemaLoader = new SchemaLoader(), - fixer?: Fixer -) { - return async (pluginName: string) => { - switch (pluginName) { - case 'pod-security-standards': - return new SimpleCustomValidator(pssPlugin, parser, fixer); - case 'practices': - return new SimpleCustomValidator(kbpPlugin, parser, fixer); - case 'open-policy-agent': - const wasmLoader = new RemoteWasmLoader(); - return new OpenPolicyAgentValidator(parser, wasmLoader); - case 'resource-links': - return new ResourceLinksValidator(); - case 'yaml-syntax': - return new YamlValidator(parser); - case 'labels': - const labelPlugin = await import('./validators/labels/plugin.js'); - return new SimpleCustomValidator(labelPlugin.default, parser, fixer); - case 'kubernetes-schema': - return new KubernetesSchemaValidator(parser, schemaLoader); - case 'metadata': - return new MetadataValidator(parser); - default: - throw new Error('plugin_not_found'); - } - }; -} diff --git a/packages/validation/src/createDefaultMonokleValidator.node.ts b/packages/validation/src/createDefaultMonokleValidator.node.ts deleted file mode 100644 index a86094fa2..000000000 --- a/packages/validation/src/createDefaultMonokleValidator.node.ts +++ /dev/null @@ -1,73 +0,0 @@ -import fs from 'fs'; -import requireFromString from 'require-from-string'; - -import {ResourceParser} from './common/resourceParser.js'; -import {SimpleCustomValidator} from './validators/custom/simpleValidator.js'; -import {SchemaLoader} from './validators/kubernetes-schema/schemaLoader.js'; -import {KubernetesSchemaValidator} from './validators/kubernetes-schema/validator.js'; -import {RemoteWasmLoader} from './wasmLoader/RemoteWasmLoader.browser.js'; -import {OpenPolicyAgentValidator} from './validators/open-policy-agent/validator.js'; -import {ResourceLinksValidator} from './validators/resource-links/validator.js'; -import {YamlValidator} from './validators/yaml-syntax/validator.js'; -import {MetadataValidator} from './validators/metadata/validator.js'; -import {MonokleValidator} from './MonokleValidator.js'; -import {bundlePluginCode} from './utils/loadCustomPlugin.node.js'; -import practicesPlugin from './validators/practices/plugin.js'; -import pssPlugin from './validators/pod-security-standards/plugin.js'; -import {Suppressor} from './sarif/suppressions/index.js'; -import {Fixer} from './sarif/fix/index.js'; - -export function createDefaultMonokleValidator( - parser: ResourceParser = new ResourceParser(), - schemaLoader: SchemaLoader = new SchemaLoader(), - suppressors?: Suppressor[], - fixer?: Fixer -) { - return new MonokleValidator({ - loader: createDefaultPluginLoader(parser, schemaLoader, fixer), - suppressors, - }); -} - -export function createDefaultPluginLoader( - parser: ResourceParser = new ResourceParser(), - schemaLoader: SchemaLoader = new SchemaLoader(), - fixer?: Fixer -) { - return async (pluginName: string) => { - switch (pluginName) { - case 'pod-security-standards': - return new SimpleCustomValidator(pssPlugin, parser, fixer); - case 'practices': - return new SimpleCustomValidator(practicesPlugin, parser, fixer); - case 'labels': - const lblPlugin = await getPlugin('./validators/labels/plugin.js'); - return new SimpleCustomValidator(lblPlugin, parser, fixer); - case 'open-policy-agent': - const wasmLoader = new RemoteWasmLoader(); - return new OpenPolicyAgentValidator(parser, wasmLoader); - case 'resource-links': - return new ResourceLinksValidator(); - case 'yaml-syntax': - return new YamlValidator(parser); - case 'kubernetes-schema': - return new KubernetesSchemaValidator(parser, schemaLoader); - case 'metadata': - return new MetadataValidator(parser); - default: - throw new Error('plugin_not_found'); - } - }; -} - -async function getPlugin(path: string) { - try { - const code = fs.readFileSync(path, {encoding: 'utf-8'}); - const bundle = await bundlePluginCode(code); - const plugin = requireFromString(bundle, path); - return plugin; - } catch (err) { - const msg = err instanceof Error ? err.message : 'reason unknown'; - throw new Error(`plugin_not_found: ${msg}`); - } -} diff --git a/packages/validation/src/createExtensibleMonokleValidator.browser.ts b/packages/validation/src/createExtensibleMonokleValidator.browser.ts deleted file mode 100644 index d53bdfe4a..000000000 --- a/packages/validation/src/createExtensibleMonokleValidator.browser.ts +++ /dev/null @@ -1,79 +0,0 @@ -import {ResourceParser} from './common/resourceParser.js'; -import {CustomPluginLoader, MonokleValidator} from './MonokleValidator.js'; -import {DEV_MODE_TOKEN} from './validators/custom/constants.js'; -import {DevCustomValidator} from './validators/custom/devValidator.js'; -import {SimpleCustomValidator} from './validators/custom/simpleValidator.js'; -import {SchemaLoader} from './validators/kubernetes-schema/schemaLoader.js'; -import {KubernetesSchemaValidator} from './validators/kubernetes-schema/validator.js'; -import {RemoteWasmLoader} from './wasmLoader/RemoteWasmLoader.browser.js'; -import {OpenPolicyAgentValidator} from './validators/open-policy-agent/validator.js'; -import {ResourceLinksValidator} from './validators/resource-links/validator.js'; -import {YamlValidator} from './validators/yaml-syntax/validator.js'; -import {MetadataValidator} from './validators/metadata/validator.js'; -import kbpPlugin from './validators/practices/plugin.js'; -import pssPlugin from './validators/pod-security-standards/plugin.js'; -import {dynamicImportCustomPluginLoader} from './pluginLoaders/dynamicImportLoader.js'; -import {CUSTOM_PLUGINS_URL_BASE} from './constants.js'; -import {Suppressor} from './sarif/suppressions/types.js'; -import {Fixer} from './sarif/fix/index.js'; - -/** - * Creates a Monokle validator that can dynamically fetch custom plugins. - */ -export function createExtensibleMonokleValidator( - parser: ResourceParser = new ResourceParser(), - schemaLoader: SchemaLoader = new SchemaLoader(), - suppressors: Suppressor[] | undefined = undefined, - fixer?: Fixer, - customPluginLoader: CustomPluginLoader = dynamicImportCustomPluginLoader -) { - return new MonokleValidator({ - suppressors, - fixer, - parser, - schemaLoader, - loader: async (pluginName: string, settings?: Record) => { - switch (pluginName) { - case 'pod-security-standards': - return new SimpleCustomValidator(pssPlugin, parser, fixer); - case 'practices': - return new SimpleCustomValidator(kbpPlugin, parser, fixer); - case 'open-policy-agent': - const wasmLoader = new RemoteWasmLoader(); - return new OpenPolicyAgentValidator(parser, wasmLoader); - case 'resource-links': - return new ResourceLinksValidator(); - case 'yaml-syntax': - return new YamlValidator(parser); - case 'labels': - const labelPlugin = await import('./validators/labels/plugin.js'); - return new SimpleCustomValidator(labelPlugin.default, parser, fixer); - case 'kubernetes-schema': - return new KubernetesSchemaValidator(parser, schemaLoader); - case 'metadata': - return new MetadataValidator(parser); - case DEV_MODE_TOKEN: - return new DevCustomValidator(parser, fixer); - default: - try { - if (settings?.pluginUrl) { - const customPlugin = await import(/* @vite-ignore */ settings.pluginUrl); - return new SimpleCustomValidator(customPlugin.default, parser, fixer); - } - if (settings?.ref) { - const customPlugin = await import( - /* @vite-ignore */ `${CUSTOM_PLUGINS_URL_BASE}/${settings.ref}/plugin.js` - ); - return new SimpleCustomValidator(customPlugin.default, parser, fixer); - } - const validator = await customPluginLoader(pluginName, parser, fixer); - return validator; - } catch (err) { - throw new Error( - err instanceof Error ? `plugin_not_found: ${err.message}` : `plugin_not_found: ${String(err)}` - ); - } - } - }, - }); -} diff --git a/packages/validation/src/createExtensibleMonokleValidator.node.ts b/packages/validation/src/createExtensibleMonokleValidator.node.ts deleted file mode 100644 index e8a6c65a1..000000000 --- a/packages/validation/src/createExtensibleMonokleValidator.node.ts +++ /dev/null @@ -1,95 +0,0 @@ -import fs from 'fs'; -import requireFromString from 'require-from-string'; - -import {ResourceParser} from './common/resourceParser.js'; -import {CustomPluginLoader, MonokleValidator} from './MonokleValidator.js'; -import {SimpleCustomValidator} from './validators/custom/simpleValidator.js'; -import {SchemaLoader} from './validators/kubernetes-schema/schemaLoader.js'; -import {KubernetesSchemaValidator} from './validators/kubernetes-schema/validator.js'; -import {RemoteWasmLoader} from './wasmLoader/RemoteWasmLoader.node.js'; -import {OpenPolicyAgentValidator} from './validators/open-policy-agent/validator.js'; -import {ResourceLinksValidator} from './validators/resource-links/validator.js'; -import {YamlValidator} from './validators/yaml-syntax/validator.js'; -import {MetadataValidator} from './validators/metadata/validator.js'; -import {bundlePluginCode} from './utils/loadCustomPlugin.node.js'; -import kbpPlugin from './validators/practices/plugin.js'; -import pssPlugin from './validators/pod-security-standards/plugin.js'; -import {requireFromStringCustomPluginLoader} from './pluginLoaders/requireFromStringLoader.node.js'; -import {CUSTOM_PLUGINS_URL_BASE} from './constants.js'; -import {AnnotationSuppressor, Suppressor} from './sarif/suppressions/index.js'; -import {Fixer} from './sarif/fix/index.js'; - -type Init = { - parser?: ResourceParser; - suppressors?: Suppressor[]; - fixer?: Fixer; - schemaLoader?: SchemaLoader; - customPluginLoader?: CustomPluginLoader; -}; - -/** - * Creates a Monokle validator that can dynamically fetch custom plugins. - */ -export function createExtensibleMonokleValidator({ - parser = new ResourceParser(), - schemaLoader = new SchemaLoader(), - suppressors = [new AnnotationSuppressor()], - fixer, - customPluginLoader = requireFromStringCustomPluginLoader, -}: Init) { - return new MonokleValidator({ - parser, - schemaLoader, - suppressors, - fixer, - loader: async (pluginNameOrUrl: string, settings?: Record) => { - switch (pluginNameOrUrl) { - case 'pod-security-standards': - return new SimpleCustomValidator(pssPlugin, parser, fixer); - case 'practices': - return new SimpleCustomValidator(kbpPlugin, parser, fixer); - case 'labels': - const lblPlugin = await getPlugin('./validators/labels/plugin.js'); - return new SimpleCustomValidator(lblPlugin, parser, fixer); - case 'open-policy-agent': - const wasmLoader = new RemoteWasmLoader(); - return new OpenPolicyAgentValidator(parser, wasmLoader); - case 'resource-links': - return new ResourceLinksValidator(); - case 'yaml-syntax': - return new YamlValidator(parser); - case 'kubernetes-schema': - return new KubernetesSchemaValidator(parser, schemaLoader); - case 'metadata': - return new MetadataValidator(parser); - default: - try { - let nameOrUrl = pluginNameOrUrl; - if (settings?.pluginUrl) { - nameOrUrl = settings.pluginUrl; - } else if (settings?.ref) { - nameOrUrl = `${CUSTOM_PLUGINS_URL_BASE}/${settings.ref}/plugin.js`; - } - const validator = await customPluginLoader(nameOrUrl, parser); - return validator; - } catch (err) { - throw new Error( - err instanceof Error ? `plugin_not_found: ${err.message}` : `plugin_not_found: ${String(err)}` - ); - } - } - }, - }); -} - -async function getPlugin(path: string) { - try { - const code = fs.readFileSync(path, {encoding: 'utf-8'}); - const bundle = await bundlePluginCode(code); - const plugin = requireFromString(bundle, path); - return plugin; - } catch (err) { - const msg = err instanceof Error ? err.message : 'reason unknown'; - throw new Error(`plugin_not_found: ${msg}`); - } -} diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 6ca0081c4..682944a32 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -1,17 +1,37 @@ +import {MonokleValidator} from './MonokleValidator.js'; +import {ResourceParser, SchemaLoader} from './node.js'; +import {RemotePluginLoader} from './pluginLoaders/PluginLoader.js'; +import {DisabledFixer} from './sarif/fix/plugins/DisabledSuppressor.js'; +import {AnnotationSuppressor, FingerprintSuppressor} from './sarif/suppressions/index.js'; + /** * Modify the commonExports.ts file if you want to export something for both node and browser environments. */ export * from './commonExports.js'; - -export * from './sarif/fix/index.js'; -export * from './sarif/suppressions/index.js'; export * from './pluginLoaders/index.js'; - -export * from './createExtensibleMonokleValidator.browser.js'; -export * from './createDefaultMonokleValidator.browser.js'; export * from './config/index.browser.js'; export * from './config/read.browser.js'; -export * from './wasmLoader/RemoteWasmLoader.browser.js'; +export * from './validators/open-policy-agent/wasmLoader/RemoteWasmLoader.browser.js'; // This function is used in Node environments only, but it cannot be imported in Monokle Desktop if it's not defined here as well export const fetchBundleRequireCustomPlugin = async (pluginName: string): Promise => {}; + +export function createDefaultMonokleValidator() { + return new MonokleValidator( + { + parser: new ResourceParser(), + schemaLoader: new SchemaLoader(), + loader: new RemotePluginLoader(), + suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()], + fixer: new DisabledFixer(), + }, + { + plugins: { + 'kubernetes-schema': true, + 'yaml-syntax': true, + 'pod-security-standards': true, + 'resource-links': true, + }, + } + ); +} diff --git a/packages/validation/src/node.ts b/packages/validation/src/node.ts index 348583c67..24dda9f11 100644 --- a/packages/validation/src/node.ts +++ b/packages/validation/src/node.ts @@ -1,3 +1,14 @@ +import { + AnnotationSuppressor, + DisabledFixer, + FingerprintSuppressor, + MonokleValidator, + ResourceParser, + SchemaLoader, +} from './commonExports.js'; +import {RemotePluginLoader} from './pluginLoaders/PluginLoader.js'; +import {requireFromStringCustomPluginLoader} from './pluginLoaders/requireFromStringLoader.node.js'; + /** * Modify the commonExports.ts file if you want to export something for both node and browser environments. */ @@ -5,9 +16,26 @@ export * from './commonExports.js'; export * from './pluginLoaders/index.node.js'; export * from './utils/loadCustomPlugin.node.js'; - -export * from './createExtensibleMonokleValidator.node.js'; -export * from './createDefaultMonokleValidator.node.js'; export * from './config/index.node.js'; export * from './config/read.node.js'; -export * from './wasmLoader/RemoteWasmLoader.node.js'; +export * from './validators/open-policy-agent/wasmLoader/RemoteWasmLoader.node.js'; + +export function createDefaultNodeMonokleValidator() { + return new MonokleValidator( + { + loader: new RemotePluginLoader(requireFromStringCustomPluginLoader), + parser: new ResourceParser(), + schemaLoader: new SchemaLoader(), + suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()], + fixer: new DisabledFixer(), + }, + { + plugins: { + 'kubernetes-schema': true, + 'yaml-syntax': true, + 'pod-security-standards': true, + 'resource-links': true, + }, + } + ); +} diff --git a/packages/validation/src/pluginLoaders/PluginLoader.ts b/packages/validation/src/pluginLoaders/PluginLoader.ts new file mode 100644 index 000000000..055d40692 --- /dev/null +++ b/packages/validation/src/pluginLoaders/PluginLoader.ts @@ -0,0 +1,120 @@ +import {Plugin} from '../common/types.js'; +import kbpPlugin from '../validators/practices/plugin.js'; +import pssPlugin from '../validators/pod-security-standards/plugin.js'; +import {CUSTOM_PLUGINS_URL_BASE} from '../constants.js'; +import {dynamicImportCustomPluginLoader} from './dynamicImportLoader.js'; +import { + DEV_MODE_TOKEN, + DevCustomValidator, + KubernetesSchemaValidator, + MetadataValidator, OpenPolicyAgentValidator, + ResourceLinksValidator, + SchemaLoader, + SimpleCustomValidator, + YamlValidator +} from '../validators'; +import {PluginContext} from './types.js'; +import {RemoteWasmLoader} from "../validators/open-policy-agent/wasmLoader/RemoteWasmLoader.node"; + +export interface PluginLoader { + load(plugin: string, ctx: PluginContext, settings?: Record): Plugin | Promise; +} + +type PluginLoadFn = (ctx: PluginContext, settings?: Record) => Plugin | Promise; + +export class DefaultPluginLoader implements PluginLoader { + private _registry: Record = {}; + + constructor() { + this.registerCorePlugins(); + } + + protected registerCorePlugins() { + this.register('pod-security-standards', ({parser, fixer}) => { + return new SimpleCustomValidator(pssPlugin, parser, fixer); + }); + + this.register('practices', ({parser, fixer}) => { + return new SimpleCustomValidator(kbpPlugin, parser, fixer); + }); + + this.register('resource-links', () => { + return new ResourceLinksValidator(); + }); + + this.register('yaml-syntax', ({parser}) => { + return new YamlValidator(parser); + }); + + this.register('kubernetes-schema', ({parser, schemaLoader}) => { + return new KubernetesSchemaValidator(parser, schemaLoader ?? new SchemaLoader()); + }); + + this.register('metadata', ({parser}) => { + return new MetadataValidator(parser); + }); + + this.register('open-policy-agent', ({parser}) => { + const wasmLoader = new RemoteWasmLoader(); + return new OpenPolicyAgentValidator(parser, wasmLoader); + }); + + this.register(DEV_MODE_TOKEN, ({parser, fixer}) => { + return new DevCustomValidator(parser, fixer); + }); + } + + register(plugin: string, callback: PluginLoadFn) { + this._registry[plugin] = callback; + } + + unregister(plugin: string) { + delete this._registry[plugin]; + } + + async load(plugin: string, ctx: PluginContext, settings?: Record) { + const loader = this._registry[plugin]; + + if (!loader) { + throw new Error('plugin_not_found'); + } + + return loader(ctx, settings); + } +} + +export class RemotePluginLoader extends DefaultPluginLoader { + constructor(private bundleLoader = dynamicImportCustomPluginLoader) { + super(); + } + + async load(plugin: string, ctx: PluginContext, settings?: Record) { + try { + return super.load(plugin, ctx, settings); + } catch (err) { + if (err instanceof Error && err.message !== 'plugin_not_found') { + throw err; + } + + return this.loadRemote(plugin, ctx, settings); + } + } + + private async loadRemote(plugin: string, {parser, fixer}: PluginContext, settings?: Record) { + try { + let nameOrUrl; + + if (settings?.pluginUrl) { + nameOrUrl = settings.pluginUrl; + } else if (settings?.ref) { + nameOrUrl = `${CUSTOM_PLUGINS_URL_BASE}/${settings.ref}/plugin.js`; + } else { + nameOrUrl = `${CUSTOM_PLUGINS_URL_BASE}/${plugin}/latest.js`; + } + + return await this.bundleLoader(nameOrUrl, parser, fixer); + } catch (err) { + throw new Error(err instanceof Error ? `plugin_not_found: ${err.message}` : `plugin_not_found: ${String(err)}`); + } + } +} diff --git a/packages/validation/src/pluginLoaders/dynamicImportLoader.ts b/packages/validation/src/pluginLoaders/dynamicImportLoader.ts index 1b7795dd4..b317898d4 100644 --- a/packages/validation/src/pluginLoaders/dynamicImportLoader.ts +++ b/packages/validation/src/pluginLoaders/dynamicImportLoader.ts @@ -1,7 +1,8 @@ -import {CustomPluginLoader} from '../MonokleValidator.js'; +import {CustomPluginLoader} from './types.js'; import {CUSTOM_PLUGINS_URL_BASE} from '../constants.js'; import {SimpleCustomValidator} from '../validators/custom/simpleValidator.js'; +// TODO: Lift up URL and import the "bundleLoader" always expected a URL. export const dynamicImportCustomPluginLoader: CustomPluginLoader = async (pluginName, parser, fixer) => { const url = `${CUSTOM_PLUGINS_URL_BASE}/${pluginName}/latest.js`; const customPlugin = await import(/* @vite-ignore */ url); diff --git a/packages/validation/src/pluginLoaders/requireFromStringLoader.node.ts b/packages/validation/src/pluginLoaders/requireFromStringLoader.node.ts index acec38241..2156b350d 100644 --- a/packages/validation/src/pluginLoaders/requireFromStringLoader.node.ts +++ b/packages/validation/src/pluginLoaders/requireFromStringLoader.node.ts @@ -1,4 +1,4 @@ -import {CustomPluginLoader} from '../MonokleValidator.js'; +import type {CustomPluginLoader} from './types.js'; import {loadCustomPlugin} from '../utils/loadCustomPlugin.node.js'; import {SimpleCustomValidator} from '../validators/custom/simpleValidator.js'; diff --git a/packages/validation/src/pluginLoaders/types.ts b/packages/validation/src/pluginLoaders/types.ts new file mode 100644 index 000000000..43763519a --- /dev/null +++ b/packages/validation/src/pluginLoaders/types.ts @@ -0,0 +1,12 @@ +import {ResourceParser} from '../common/resourceParser.js'; +import type {Plugin} from '../common/types.js'; +import {Fixer} from '../sarif/fix/index.js'; +import {SchemaLoader} from '../validators/kubernetes-schema/index.js'; + +export type PluginContext = { + parser: ResourceParser; + fixer?: Fixer; + schemaLoader?: SchemaLoader; +}; + +export type CustomPluginLoader = (name: string, parser: ResourceParser, fixer?: Fixer) => Promise; diff --git a/packages/validation/src/references/index.ts b/packages/validation/src/references/index.ts index 8bc5a74cb..bc0da6588 100644 --- a/packages/validation/src/references/index.ts +++ b/packages/validation/src/references/index.ts @@ -1 +1,2 @@ export * from './utils/helpers.js'; +export * from './process.js'; diff --git a/packages/validation/src/sarif/fix/index.ts b/packages/validation/src/sarif/fix/index.ts index 629931773..47d79d864 100644 --- a/packages/validation/src/sarif/fix/index.ts +++ b/packages/validation/src/sarif/fix/index.ts @@ -2,6 +2,8 @@ import {ResourceParser} from '../../common/resourceParser.js'; import type {Resource} from '../../common/types.js'; import type {Fix} from '../../common/sarif.js'; +export * from './plugins/DisabledSuppressor.js'; + export interface Fixer { createFix(resource: Resource, fixedContent: any, parser: ResourceParser): Fix[]; } diff --git a/packages/validation/src/sarif/fix/plugins/DisabledSuppressor.ts b/packages/validation/src/sarif/fix/plugins/DisabledSuppressor.ts new file mode 100644 index 000000000..743dea021 --- /dev/null +++ b/packages/validation/src/sarif/fix/plugins/DisabledSuppressor.ts @@ -0,0 +1,8 @@ +import type {Fixer} from '../index.js'; +import type {Fix} from '../../../common/sarif.js'; + +export class DisabledFixer implements Fixer { + createFix(): Fix[] { + return []; + } +} diff --git a/packages/validation/src/sarif/index.ts b/packages/validation/src/sarif/index.ts new file mode 100644 index 000000000..fbd1a88e4 --- /dev/null +++ b/packages/validation/src/sarif/index.ts @@ -0,0 +1,2 @@ +export * from './fix/index.js'; +export * from './suppressions/index.js'; diff --git a/packages/validation/src/types.ts b/packages/validation/src/types.ts index 34fe005a5..ff92bc19e 100644 --- a/packages/validation/src/types.ts +++ b/packages/validation/src/types.ts @@ -14,6 +14,35 @@ export type RuleMetadataWithConfig = RuleMetadata & { export type PluginName = string; +export type ValidateParams = { + /** + * The resources that will be validated. + */ + resources: Resource[]; + + /** + * The list of resources that recently got updated. + * + * @remarks Validators can use this information to skip non-modified resources. + */ + incremental?: Incremental; + + /** + * A previous run which acts as the baseline for detected problems. + * + * @remark Providing a baseline will set run.baselineGuid and result.baselineStatus. + * @remark Newly fixed problems will be added as 'absent' results. + * When using baseline, it is important to properly filter or + * indicate absent results or they appear as false positives. + */ + baseline?: ValidationResponse; + + /** + * A signal that can be used to abort processing. + */ + abortSignal?: AbortSignal; +}; + export interface Validator { /** * The user configuration. @@ -39,11 +68,7 @@ export interface Validator { * @remark Plugins must be loaded and configured before you can validate. * this is more text */ - validate(args: { - resources: Resource[]; - incremental?: Incremental; - abortSignal?: AbortSignal; - }): Promise; + validate(args: ValidateParams): Promise; /** * Unloads all plugins. diff --git a/packages/validation/src/validators/custom/devValidator.ts b/packages/validation/src/validators/custom/devValidator.ts index b3777943a..df5041aa2 100644 --- a/packages/validation/src/validators/custom/devValidator.ts +++ b/packages/validation/src/validators/custom/devValidator.ts @@ -4,7 +4,7 @@ import {CustomSchema, Incremental, Plugin, PluginMetadata, Resource, ValidateOpt import {RuleMap} from '../../config/parse.js'; import {Fixer} from '../../sarif/fix/index.js'; import {Suppressor} from '../../sarif/suppressions/index.js'; -import {PluginMetadataWithConfig, RuleMetadataWithConfig} from '../../types.js'; +import {PluginMetadataWithConfig, RuleMetadataWithConfig} from '../../core/types.js'; import {DEV_MODE_TOKEN} from './constants.js'; import {SimpleCustomValidator} from './simpleValidator.js'; diff --git a/packages/validation/src/validators/index.ts b/packages/validation/src/validators/index.ts new file mode 100644 index 000000000..f844216f0 --- /dev/null +++ b/packages/validation/src/validators/index.ts @@ -0,0 +1,8 @@ +export * from './custom/simpleValidator.js'; +export * from './custom/devValidator.js'; +export * from './custom/constants.js'; +export * from './open-policy-agent/index.js'; +export * from './kubernetes-schema/index.js'; +export * from './yaml-syntax/index.js'; +export * from './resource-links/index.js'; +export * from './metadata/index.js'; diff --git a/packages/validation/src/validators/kubernetes-schema/rules.ts b/packages/validation/src/validators/kubernetes-schema/rules.ts index 7aefb6d5d..ec3d4b0e5 100644 --- a/packages/validation/src/validators/kubernetes-schema/rules.ts +++ b/packages/validation/src/validators/kubernetes-schema/rules.ts @@ -57,6 +57,6 @@ export const KUBERNETES_SCHEMA_RULES: RuleMetadata[] = [ }, defaultConfiguration: { enabled: false, - } + }, }, ]; diff --git a/packages/validation/src/validators/labels/noEmptyLabels.ts b/packages/validation/src/validators/labels/noEmptyLabels.ts deleted file mode 100644 index c8f8ef97f..000000000 --- a/packages/validation/src/validators/labels/noEmptyLabels.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {defineRule} from '../custom/config.js'; - -export const noEmptyLabels = defineRule({ - id: 1, - description: 'Require labels as metadata.', - help: 'Add any label to the Kubernetes resource.', - validate({resources}, {report}) { - resources.forEach(resource => { - const labels = Object.entries(resource.metadata?.labels ?? {}); - const hasLabels = labels.length > 0; - - if (!hasLabels) { - report(resource, {path: 'metadata.labels'}); - } - }); - }, -}); diff --git a/packages/validation/src/validators/labels/plugin.ts b/packages/validation/src/validators/labels/plugin.ts deleted file mode 100644 index 0bb677ffd..000000000 --- a/packages/validation/src/validators/labels/plugin.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {definePlugin} from '../custom/config.js'; -import {noEmptyLabels} from './noEmptyLabels.js'; - -export default definePlugin({ - id: 'LBL', - name: 'labels', - description: 'Validates your labels', - rules: { - noEmptyLabels, - }, -}); diff --git a/packages/validation/src/validators/metadata/rules.ts b/packages/validation/src/validators/metadata/rules.ts index 8cada8f16..b5803c848 100644 --- a/packages/validation/src/validators/metadata/rules.ts +++ b/packages/validation/src/validators/metadata/rules.ts @@ -24,7 +24,7 @@ export const METADATA_RULES: RuleMetadata[] = [ 'app.kubernetes.io/component', 'app.kubernetes.io/part-of', 'app.kubernetes.io/managed', - ] + ], }, }, }, diff --git a/packages/validation/src/validators/metadata/validator.ts b/packages/validation/src/validators/metadata/validator.ts index 92cb2de0e..d41eceb1a 100644 --- a/packages/validation/src/validators/metadata/validator.ts +++ b/packages/validation/src/validators/metadata/validator.ts @@ -6,7 +6,7 @@ import {RuleConfigMetadataType, ValidationResult} from '../../common/sarif.js'; import {Resource, ValidateOptions} from '../../common/types.js'; import {METADATA_RULES} from './rules.js'; import {createLocations} from '../../utils/createLocations.js'; -import {RuleMetadataWithConfig} from '../../types.js'; +import {RuleMetadataWithConfig} from '../../core/types.js'; import {findJsonPointerNode} from '../../utils/findJsonPointerNode.js'; import {RuleMap} from '../../config/parse.js'; import {isDefined} from '../../utils/isDefined.js'; diff --git a/packages/validation/src/validators/open-policy-agent/validator.ts b/packages/validation/src/validators/open-policy-agent/validator.ts index cb379d3e0..6a9d529c5 100644 --- a/packages/validation/src/validators/open-policy-agent/validator.ts +++ b/packages/validation/src/validators/open-policy-agent/validator.ts @@ -13,7 +13,7 @@ import {createLocations} from '../../utils/createLocations.js'; import {isDefined} from '../../utils/isDefined.js'; import {OPEN_POLICY_AGENT_RULES} from './rules.js'; import {LoadedPolicy, OpaProperties, PolicyError} from './types.js'; -import {WasmLoader} from '../../wasmLoader/WasmLoader.js'; +import {WasmLoader} from './wasmLoader/WasmLoader.js'; import {isKustomizationResource} from '../../references/utils/kustomizeRefs.js'; import invariant from '../../utils/invariant.js'; import {CUSTOM_PLUGINS_URL_BASE} from '../../constants.js'; diff --git a/packages/validation/src/wasmLoader/FileWasmLoader.ts b/packages/validation/src/validators/open-policy-agent/wasmLoader/FileWasmLoader.ts similarity index 100% rename from packages/validation/src/wasmLoader/FileWasmLoader.ts rename to packages/validation/src/validators/open-policy-agent/wasmLoader/FileWasmLoader.ts diff --git a/packages/validation/src/wasmLoader/RemoteWasmLoader.browser.ts b/packages/validation/src/validators/open-policy-agent/wasmLoader/RemoteWasmLoader.browser.ts similarity index 100% rename from packages/validation/src/wasmLoader/RemoteWasmLoader.browser.ts rename to packages/validation/src/validators/open-policy-agent/wasmLoader/RemoteWasmLoader.browser.ts diff --git a/packages/validation/src/wasmLoader/RemoteWasmLoader.node.ts b/packages/validation/src/validators/open-policy-agent/wasmLoader/RemoteWasmLoader.node.ts similarity index 100% rename from packages/validation/src/wasmLoader/RemoteWasmLoader.node.ts rename to packages/validation/src/validators/open-policy-agent/wasmLoader/RemoteWasmLoader.node.ts diff --git a/packages/validation/src/wasmLoader/WasmLoader.ts b/packages/validation/src/validators/open-policy-agent/wasmLoader/WasmLoader.ts similarity index 100% rename from packages/validation/src/wasmLoader/WasmLoader.ts rename to packages/validation/src/validators/open-policy-agent/wasmLoader/WasmLoader.ts diff --git a/packages/validation/src/validators/practices/rules/KBP106-noAutomountServiceAccountToken.ts b/packages/validation/src/validators/practices/rules/KBP106-noAutomountServiceAccountToken.ts index 5ad679aad..09432589b 100644 --- a/packages/validation/src/validators/practices/rules/KBP106-noAutomountServiceAccountToken.ts +++ b/packages/validation/src/validators/practices/rules/KBP106-noAutomountServiceAccountToken.ts @@ -1,5 +1,6 @@ import {NSA_RELATIONS} from '../../../taxonomies/nsa.js'; import {defineRule} from '../../custom/config.js'; +import {isDeployment} from '../../custom/schemas/deployment.apps.v1.js'; import {isServiceAccount} from '../../custom/schemas/serviceaccount.v1.js'; import {validatePodSpec} from '../../custom/utils.js';