diff --git a/packages/components/src/molecules/ValidationOverview/ProblemRenderer.tsx b/packages/components/src/molecules/ValidationOverview/ProblemRenderer.tsx index a2516b065..98c38c33b 100644 --- a/packages/components/src/molecules/ValidationOverview/ProblemRenderer.tsx +++ b/packages/components/src/molecules/ValidationOverview/ProblemRenderer.tsx @@ -31,6 +31,9 @@ const ProblemRenderer: React.FC = props => { [selectedProblem, node.problem, groupByFilterValue] ); + const absent = useMemo(() => node.problem.baselineState === 'absent', [node]); + const hasFix = useMemo(() => node.problem.fixes && node.problem.fixes.length > 0, [node]); + const isUnderReview = useMemo(() => { return Boolean( typeof suppressionBindings?.isUnderReview == 'function' && suppressionBindings.isUnderReview(node.problem) @@ -51,7 +54,7 @@ const ProblemRenderer: React.FC = props => { return ( = props => { )} - - - { - node.problem.locations[groupByFilterValue === 'group-by-file' ? 0 : 1].physicalLocation?.region - ?.startLine - } - - - - - {node.problem.message.text} - + {node.problem.message.text} + {suppressed ? '(suppressed)' : ''} {node.problem.taxa?.length ? ( {node.problem.taxa.map(framework => ( @@ -119,11 +109,11 @@ const ProblemRenderer: React.FC = props => { )} e.stopPropagation()}> - {onAutofixHandler && ( - + {onAutofixHandler && hasFix && !absent ? ( + onAutofixHandler(node.problem)} /> - )} + ) : null} {showSuppressionCTA ? ( = props onToggle={() => setActive(!active)} > + + onFiltersChange({...filtersValue, showAbsent: !Boolean(filtersValue.showAbsent)})} + > + Show absent misconfigurations + + Hide suppressed misconfigurations diff --git a/packages/components/src/molecules/ValidationOverview/types.ts b/packages/components/src/molecules/ValidationOverview/types.ts index f86b6b555..695cf0cf1 100644 --- a/packages/components/src/molecules/ValidationOverview/types.ts +++ b/packages/components/src/molecules/ValidationOverview/types.ts @@ -46,6 +46,7 @@ export type ValidationFiltersValueType = { type?: 'error' | 'warning'; showSuppressed?: boolean; showUnsuppressed?: boolean; + showAbsent?: boolean; }; export type ProblemsType = { diff --git a/packages/components/src/molecules/ValidationOverview/utils.tsx b/packages/components/src/molecules/ValidationOverview/utils.tsx index c6baa6be0..f844d2c69 100644 --- a/packages/components/src/molecules/ValidationOverview/utils.tsx +++ b/packages/components/src/molecules/ValidationOverview/utils.tsx @@ -5,6 +5,7 @@ import { getFileLocation, getResourceLocation, getRuleForResultV2, + isSuppressed, ValidationResponse, ValidationResult, } from '@monokle/validation'; @@ -103,29 +104,33 @@ export const filterProblems = ( filters: ValidationFiltersValueType, securityFrameworkFilter: string ) => { - const suppressedFilter = filters.showSuppressed - ? () => true - : (problem: ValidationResult) => !problem.suppressions?.length; - const unsuppressedFilter = filters.showUnsuppressed - ? () => true - : (problem: ValidationResult) => problem.suppressions?.length; - return Object.fromEntries( Object.entries(problems || {}) .map(([filePath, validationResults]) => { - let filteredValidationResults = validationResults - .filter(suppressedFilter) - .filter(unsuppressedFilter) - .filter( - el => - (filters['type'] ? el.level === filters['type'] : true) && - (filters['tool-component']?.length - ? filters['tool-component'].includes(el.rule.toolComponent.name) - : true) && - (securityFrameworkFilter !== 'all' - ? el.taxa?.find(t => t.toolComponent.name === securityFrameworkFilter) - : true) - ); + let filteredValidationResults = validationResults.filter( + el => + (filters['type'] ? el.level === filters['type'] : true) && + (filters['tool-component']?.length + ? filters['tool-component'].includes(el.rule.toolComponent.name) + : true) && + (securityFrameworkFilter !== 'all' + ? el.taxa?.find(t => t.toolComponent.name === securityFrameworkFilter) + : true) + ); + + if (filters.showSuppressed && !filters.showUnsuppressed) { + filteredValidationResults = filteredValidationResults.filter(p => isSuppressed(p)); + } + if (!filters.showSuppressed && filters.showUnsuppressed) { + filteredValidationResults = filteredValidationResults.filter(p => !isSuppressed(p)); + } + if (!filters.showSuppressed && !filters.showUnsuppressed) { + return []; + } + + if (filters.showAbsent) { + filteredValidationResults = filteredValidationResults.filter(p => p.baselineState !== 'absent'); + } if (filteredValidationResults.length > 0) { return [filePath, filteredValidationResults]; diff --git a/packages/validation/src/MonokleValidator.ts b/packages/validation/src/MonokleValidator.ts index b52e43f38..3c23c50eb 100644 --- a/packages/validation/src/MonokleValidator.ts +++ b/packages/validation/src/MonokleValidator.ts @@ -5,7 +5,7 @@ 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 {Config, PluginMap} from './config/parse.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'; @@ -16,24 +16,24 @@ import invariant from './utils/invariant.js'; import {isDefined} from './utils/isDefined.js'; import {AnnotationSuppressor, FingerprintSuppressor, Suppressor} from './sarif/suppressions/index.js'; import {SuppressEngine} from './sarif/suppressions/engine.js'; +import {Fixer} from './sarif/fix/index.js'; +import {SchemaLoader} from './validators/kubernetes-schema/schemaLoader.js'; export type PluginLoader = (name: string, settings?: Record) => Promise; -export type CustomPluginLoader = (name: string, parser: ResourceParser, suppressor?: Suppressor) => Promise; +export type CustomPluginLoader = (name: string, parser: ResourceParser, fixer?: Fixer) => Promise; + +type MonokleInit = { + loader: PluginLoader; + parser?: ResourceParser; + suppressors?: Suppressor[]; + fixer?: Fixer; + schemaLoader?: SchemaLoader; +}; -export function createMonokleValidator(loader: PluginLoader, suppressors?: Suppressor[], fallback?: PluginMap) { - return new MonokleValidator(loader, suppressors, fallback); +export function createMonokleValidator(init: MonokleInit) { + return new MonokleValidator(init); } -/** - * The plugins that will be loaded by default. - */ -const DEFAULT_PLUGIN_MAP = { - 'kubernetes-schema': true, - 'yaml-syntax': true, - 'pod-security-standards': true, - 'resource-links': true, -}; - const DEFAULT_SUPPRESSORS = [new AnnotationSuppressor(), new FingerprintSuppressor()]; type ValidateParams = { @@ -66,20 +66,7 @@ type ValidateParams = { }; export class MonokleValidator implements Validator { - /** - * The user configuration of this validator. - */ - _config?: Config; - - /** - * The fallback configuration of this validator. - * - * This is here for sane defaults, but the moment you - * set configuration it's not taken into consideration - * to make it easier to reason about what you get. - */ - _fallback: Config; - + _config: Config = {}; _abortController: AbortController = new AbortController(); _loading?: Promise; _loader: PluginLoader; @@ -90,19 +77,13 @@ export class MonokleValidator implements Validator { _suppressions: Suppression[] = []; private _suppressor: SuppressEngine; - constructor( - loader: PluginLoader, - suppressors: Suppressor[] = DEFAULT_SUPPRESSORS, - fallback: PluginMap = DEFAULT_PLUGIN_MAP - ) { - this._loader = loader; - this._suppressor = new SuppressEngine(suppressors); - this._fallback = {plugins: fallback}; - this._config = this._fallback; + constructor(init: MonokleInit) { + this._loader = init.loader; + this._suppressor = new SuppressEngine(init.suppressors ?? DEFAULT_SUPPRESSORS); } get config(): Config { - return this._config ?? this._fallback; + return this._config; } get metadata(): Record { @@ -115,33 +96,13 @@ export class MonokleValidator implements Validator { return Object.fromEntries(entries); } - /** - * Whether the rule exists in some plugin. - * - * @params - Either the rule identifier or display name. - * @example "KSV013" or "open-policy-agent/no-latest-image" - */ - hasRule(rule: string): boolean { - return this._plugins.some(p => p.hasRule(rule)); - } - - /** - * Whether the rule is enabled in some plugin. - * - * @params rule - Either the rule identifier or display name. - * @example "KSV013" or "open-policy-agent/no-latest-image" - */ - isRuleEnabled(rule: string) { - return this._plugins.some(p => p.isRuleEnabled(rule)); - } - /** * Whether the plugin is loaded. * * @params name - The plugin name. * @example "open-policy-agent" */ - isPluginLoaded(name: string): boolean { + private isPluginLoaded(name: string): boolean { return this._plugins.some(p => p.metadata.name === name); } @@ -173,7 +134,7 @@ export class MonokleValidator implements Validator { * * @param config - the new configuration of the validator. */ - async preload(config?: Config, suppressions?: Suppression[]): Promise { + async preload(config: Config): Promise { this._config = config; this._suppressions = suppressions || []; return this.load(); @@ -262,7 +223,6 @@ export class MonokleValidator implements Validator { settings: config.settings, }) ), - this._suppressor.preload(this._suppressions), ]); } @@ -486,7 +446,7 @@ export class MonokleValidator implements Validator { const pluginNames = this.getPlugins().map(p => p.metadata.name); await this.doUnload(pluginNames); - this._config = undefined; + this._config = {}; this._failedPlugins = []; } diff --git a/packages/validation/src/common/sarif.ts b/packages/validation/src/common/sarif.ts index 6f036d523..1bd248932 100644 --- a/packages/validation/src/common/sarif.ts +++ b/packages/validation/src/common/sarif.ts @@ -75,6 +75,47 @@ export type ExternalSuppression = Suppression & { export type SuppressionKind = 'inSource' | 'external'; export type SuppressionStatus = 'underReview' | 'accepted' | 'rejected'; +/** + * A suggestion to fix a problem. + * + * @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#_Toc10541319 + */ +export type Fix = { + description: string; + artifactChanges: ArtifactChange[]; +}; + +export type ArtifactChange = { + artifactLocation: { + uri: string; + }; + replacements: Replacement[]; +}; + +export type Replacement = TextualReplacement | BinaryReplacement; + +export type TextualReplacement = { + deletedRegion: { + startLine: number; + startColumn: number; + endLine: number; + endColumn: number; + }; + insertedContent?: { + text: string; + }; +}; + +export type BinaryReplacement = { + deletedRegion: { + byteOffset: number; + byteLength: number; + }; + insertedContent?: { + binary: string; + }; +}; + /** * A Monokle Validation configuration file. * @@ -317,6 +358,7 @@ export type ValidationResult = { fingerprints?: FingerPrints; baselineState?: BaseLineState; suppressions?: Suppression[]; + fixes?: Fix[]; /** * The location of the error. diff --git a/packages/validation/src/createDefaultMonokleValidator.browser.ts b/packages/validation/src/createDefaultMonokleValidator.browser.ts index b33d21dd4..4e17494f2 100644 --- a/packages/validation/src/createDefaultMonokleValidator.browser.ts +++ b/packages/validation/src/createDefaultMonokleValidator.browser.ts @@ -11,25 +11,34 @@ 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[] + suppressors?: Suppressor[], + fixer?: Fixer ) { - return new MonokleValidator(createDefaultPluginLoader(parser, schemaLoader), suppressors); + return new MonokleValidator({ + fixer, + parser, + schemaLoader, + suppressors, + loader: createDefaultPluginLoader(parser, schemaLoader), + }); } export function createDefaultPluginLoader( parser: ResourceParser = new ResourceParser(), - schemaLoader: SchemaLoader = new SchemaLoader() + schemaLoader: SchemaLoader = new SchemaLoader(), + fixer?: Fixer ) { return async (pluginName: string) => { switch (pluginName) { case 'pod-security-standards': - return new SimpleCustomValidator(pssPlugin, parser); + return new SimpleCustomValidator(pssPlugin, parser, fixer); case 'practices': - return new SimpleCustomValidator(kbpPlugin, parser); + return new SimpleCustomValidator(kbpPlugin, parser, fixer); case 'open-policy-agent': const wasmLoader = new RemoteWasmLoader(); return new OpenPolicyAgentValidator(parser, wasmLoader); @@ -39,7 +48,7 @@ export function createDefaultPluginLoader( return new YamlValidator(parser); case 'labels': const labelPlugin = await import('./validators/labels/plugin.js'); - return new SimpleCustomValidator(labelPlugin.default, parser); + return new SimpleCustomValidator(labelPlugin.default, parser, fixer); case 'kubernetes-schema': return new KubernetesSchemaValidator(parser, schemaLoader); case 'metadata': diff --git a/packages/validation/src/createDefaultMonokleValidator.node.ts b/packages/validation/src/createDefaultMonokleValidator.node.ts index 3fbf59e2a..a86094fa2 100644 --- a/packages/validation/src/createDefaultMonokleValidator.node.ts +++ b/packages/validation/src/createDefaultMonokleValidator.node.ts @@ -15,28 +15,34 @@ 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[] + suppressors?: Suppressor[], + fixer?: Fixer ) { - return new MonokleValidator(createDefaultPluginLoader(parser, schemaLoader), suppressors); + return new MonokleValidator({ + loader: createDefaultPluginLoader(parser, schemaLoader, fixer), + suppressors, + }); } export function createDefaultPluginLoader( parser: ResourceParser = new ResourceParser(), - schemaLoader: SchemaLoader = new SchemaLoader() + schemaLoader: SchemaLoader = new SchemaLoader(), + fixer?: Fixer ) { return async (pluginName: string) => { switch (pluginName) { case 'pod-security-standards': - return new SimpleCustomValidator(pssPlugin, parser); + return new SimpleCustomValidator(pssPlugin, parser, fixer); case 'practices': - return new SimpleCustomValidator(practicesPlugin, parser); + return new SimpleCustomValidator(practicesPlugin, parser, fixer); case 'labels': const lblPlugin = await getPlugin('./validators/labels/plugin.js'); - return new SimpleCustomValidator(lblPlugin, parser); + return new SimpleCustomValidator(lblPlugin, parser, fixer); case 'open-policy-agent': const wasmLoader = new RemoteWasmLoader(); return new OpenPolicyAgentValidator(parser, wasmLoader); diff --git a/packages/validation/src/createExtensibleMonokleValidator.browser.ts b/packages/validation/src/createExtensibleMonokleValidator.browser.ts index a7d7d3500..d53bdfe4a 100644 --- a/packages/validation/src/createExtensibleMonokleValidator.browser.ts +++ b/packages/validation/src/createExtensibleMonokleValidator.browser.ts @@ -15,6 +15,7 @@ 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. @@ -23,49 +24,56 @@ export function createExtensibleMonokleValidator( parser: ResourceParser = new ResourceParser(), schemaLoader: SchemaLoader = new SchemaLoader(), suppressors: Suppressor[] | undefined = undefined, + fixer?: Fixer, customPluginLoader: CustomPluginLoader = dynamicImportCustomPluginLoader ) { - return new MonokleValidator(async (pluginName: string, settings?: Record) => { - switch (pluginName) { - case 'pod-security-standards': - return new SimpleCustomValidator(pssPlugin, parser); - case 'practices': - return new SimpleCustomValidator(kbpPlugin, parser); - 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); - case 'kubernetes-schema': - return new KubernetesSchemaValidator(parser, schemaLoader); - case 'metadata': - return new MetadataValidator(parser); - case DEV_MODE_TOKEN: - return new DevCustomValidator(parser); - default: - try { - if (settings?.pluginUrl) { - const customPlugin = await import(/* @vite-ignore */ settings.pluginUrl); - return new SimpleCustomValidator(customPlugin.default, parser); - } - if (settings?.ref) { - const customPlugin = await import( - /* @vite-ignore */ `${CUSTOM_PLUGINS_URL_BASE}/${settings.ref}/plugin.js` + 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)}` ); - return new SimpleCustomValidator(customPlugin.default, parser); } - const validator = await customPluginLoader(pluginName, parser); - return validator; - } catch (err) { - throw new Error( - err instanceof Error ? `plugin_not_found: ${err.message}` : `plugin_not_found: ${String(err)}` - ); - } - } - }, suppressors); + } + }, + }); } diff --git a/packages/validation/src/createExtensibleMonokleValidator.node.ts b/packages/validation/src/createExtensibleMonokleValidator.node.ts index 9265af590..e8a6c65a1 100644 --- a/packages/validation/src/createExtensibleMonokleValidator.node.ts +++ b/packages/validation/src/createExtensibleMonokleValidator.node.ts @@ -16,54 +16,70 @@ 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 {Suppressor} from './sarif/suppressions/index.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: ResourceParser = new ResourceParser(), - schemaLoader: SchemaLoader = new SchemaLoader(), - suppressors: Suppressor[] | undefined = undefined, - customPluginLoader: CustomPluginLoader = requireFromStringCustomPluginLoader -) { - return new MonokleValidator(async (pluginNameOrUrl: string, settings?: Record) => { - switch (pluginNameOrUrl) { - case 'pod-security-standards': - return new SimpleCustomValidator(pssPlugin, parser); - case 'practices': - return new SimpleCustomValidator(kbpPlugin, parser); - case 'labels': - const lblPlugin = await getPlugin('./validators/labels/plugin.js'); - return new SimpleCustomValidator(lblPlugin, parser); - 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`; +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)}` + ); } - 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)}` - ); - } - } - }, suppressors); + } + }, + }); } async function getPlugin(path: string) { diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 560f7e282..6ca0081c4 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -3,6 +3,7 @@ */ export * from './commonExports.js'; +export * from './sarif/fix/index.js'; export * from './sarif/suppressions/index.js'; export * from './pluginLoaders/index.js'; diff --git a/packages/validation/src/pluginLoaders/dynamicImportLoader.ts b/packages/validation/src/pluginLoaders/dynamicImportLoader.ts index 6550ecdc5..1b7795dd4 100644 --- a/packages/validation/src/pluginLoaders/dynamicImportLoader.ts +++ b/packages/validation/src/pluginLoaders/dynamicImportLoader.ts @@ -2,8 +2,8 @@ import {CustomPluginLoader} from '../MonokleValidator.js'; import {CUSTOM_PLUGINS_URL_BASE} from '../constants.js'; import {SimpleCustomValidator} from '../validators/custom/simpleValidator.js'; -export const dynamicImportCustomPluginLoader: CustomPluginLoader = async (pluginName, parser) => { +export const dynamicImportCustomPluginLoader: CustomPluginLoader = async (pluginName, parser, fixer) => { const url = `${CUSTOM_PLUGINS_URL_BASE}/${pluginName}/latest.js`; const customPlugin = await import(/* @vite-ignore */ url); - return new SimpleCustomValidator(customPlugin.default, parser); + return new SimpleCustomValidator(customPlugin.default, parser, fixer); }; diff --git a/packages/validation/src/pluginLoaders/requireFromStringLoader.node.ts b/packages/validation/src/pluginLoaders/requireFromStringLoader.node.ts index 889823a17..acec38241 100644 --- a/packages/validation/src/pluginLoaders/requireFromStringLoader.node.ts +++ b/packages/validation/src/pluginLoaders/requireFromStringLoader.node.ts @@ -2,7 +2,7 @@ import {CustomPluginLoader} from '../MonokleValidator.js'; import {loadCustomPlugin} from '../utils/loadCustomPlugin.node.js'; import {SimpleCustomValidator} from '../validators/custom/simpleValidator.js'; -export const requireFromStringCustomPluginLoader: CustomPluginLoader = async (pluginNameOrUrl, parser) => { +export const requireFromStringCustomPluginLoader: CustomPluginLoader = async (pluginNameOrUrl, parser, fixer) => { const customPlugin = await loadCustomPlugin(pluginNameOrUrl); - return new SimpleCustomValidator(customPlugin, parser); + return new SimpleCustomValidator(customPlugin, parser, fixer); }; diff --git a/packages/validation/src/sarif/fix/index.ts b/packages/validation/src/sarif/fix/index.ts new file mode 100644 index 000000000..629931773 --- /dev/null +++ b/packages/validation/src/sarif/fix/index.ts @@ -0,0 +1,7 @@ +import {ResourceParser} from '../../common/resourceParser.js'; +import type {Resource} from '../../common/types.js'; +import type {Fix} from '../../common/sarif.js'; + +export interface Fixer { + createFix(resource: Resource, fixedContent: any, parser: ResourceParser): Fix[]; +} diff --git a/packages/validation/src/validators/custom/config.ts b/packages/validation/src/validators/custom/config.ts index bdefa19fe..027cede1d 100644 --- a/packages/validation/src/validators/custom/config.ts +++ b/packages/validation/src/validators/custom/config.ts @@ -1,5 +1,11 @@ import type {Document, ParsedNode} from 'yaml'; -import {RuleConfigMetadataProperties, RuleConfigMetadataAllowedValues, RuleLevel, reportingDescriptorRelationship} from '../../node.js'; +import { + RuleConfigMetadataProperties, + RuleConfigMetadataAllowedValues, + RuleLevel, + reportingDescriptorRelationship, + ValidationResult, +} from '../../node.js'; export type PluginInit = { /** @@ -111,6 +117,13 @@ export type RuleInit = { */ validate(ctx: RuleContext, api: RuleApi): Promise | void; + /** + * The runtime that gives a fix for a reported problem. + * + * @remark you can update the resource as needed. + */ + fix?: (ctx: FixContext, api: FixApi) => void; + /** * Advanced rule settings. */ @@ -154,6 +167,21 @@ export type RuleApi = { parse(resource: Resource): Document.Parsed; }; +export type FixContext = { + resource: Resource; + problem: ValidationResult; + path: string; +}; + +export type FixApi = { + /** + * Utility that sets the given value. + * + * @example a service's label selector relates to a deployment. + */ + set(resource: Resource, path: string, value: any): void; +}; + export type ReportArgs = { /** * A path to the error. diff --git a/packages/validation/src/validators/custom/devValidator.ts b/packages/validation/src/validators/custom/devValidator.ts index 5402dff1a..b3777943a 100644 --- a/packages/validation/src/validators/custom/devValidator.ts +++ b/packages/validation/src/validators/custom/devValidator.ts @@ -2,6 +2,7 @@ import {ResourceParser} from '../../common/resourceParser.js'; import {ToolPlugin, ValidationResult, ValidationRun} from '../../common/sarif.js'; import {CustomSchema, Incremental, Plugin, PluginMetadata, Resource, ValidateOptions} from '../../common/types.js'; 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 {DEV_MODE_TOKEN} from './constants.js'; @@ -32,7 +33,7 @@ export class DevCustomValidator implements Plugin { private _debug: boolean = false; protected _toolComponentIndex: number = -1; - constructor(private parser: ResourceParser) { + constructor(private parser: ResourceParser, private fixer: Fixer | undefined) { this.hmr(); } @@ -63,7 +64,7 @@ export class DevCustomValidator implements Plugin { const dataUrl = `data:text/javascript;base64,${encodedSource}`; import(/* @vite-ignore */ dataUrl).then(module => { const pluginInit = module.default; - const validator = new SimpleCustomValidator(pluginInit, this.parser); + const validator = new SimpleCustomValidator(pluginInit, this.parser, this.fixer); this._currentValidator = validator; if (this._lastConfig) { const entries = Object.entries(this._lastConfig.rules ?? {}).map(([key, value]) => { diff --git a/packages/validation/src/validators/custom/simpleValidator.ts b/packages/validation/src/validators/custom/simpleValidator.ts index 7e637268a..a56dd87c8 100644 --- a/packages/validation/src/validators/custom/simpleValidator.ts +++ b/packages/validation/src/validators/custom/simpleValidator.ts @@ -1,16 +1,19 @@ import {paramCase, sentenceCase} from 'change-case'; import keyBy from 'lodash/keyBy.js'; -import {Document, isNode, Node, ParsedNode} from 'yaml'; +import set from 'lodash/set.js'; +import {Document, Node, ParsedNode, isNode} from 'yaml'; import {AbstractPlugin} from '../../common/AbstractPlugin.js'; import {ResourceParser} from '../../common/resourceParser.js'; -import {ValidationResult, RuleMetadata} from '../../common/sarif.js'; +import {RuleMetadata, ValidationResult} from '../../common/sarif.js'; import {PluginMetadata, Resource, ValidateOptions} from '../../common/types.js'; +import {Fixer} from '../../sarif/fix/index.js'; import {createLocations} from '../../utils/createLocations.js'; import {isDefined} from '../../utils/isDefined.js'; -import {PluginInit, ReportArgs, Resource as PlainResource, RuleInit} from './config.js'; +import {Resource as PlainResource, PluginInit, ReportArgs, RuleInit} from './config.js'; type Runtime = { validate: RuleInit['validate']; + fix: RuleInit['fix']; }; type PlainResourceWithId = PlainResource & {_id: string}; @@ -22,10 +25,12 @@ export class SimpleCustomValidator extends AbstractPlugin { private _parser: ResourceParser; private _settings: any = {}; private _ruleRuntime: Record; + private _fixer: Fixer | undefined; - constructor(plugin: PluginInit, parser: ResourceParser) { + constructor(plugin: PluginInit, parser: ResourceParser, fixer: Fixer | undefined) { super(toPluginMetadata(plugin), toSarifRules(plugin)); this._parser = parser; + this._fixer = fixer; this._ruleRuntime = toRuntime(plugin); } @@ -35,7 +40,6 @@ export class SimpleCustomValidator extends AbstractPlugin { async doValidate(resources: Resource[], options: ValidateOptions): Promise { const results: ValidationResult[] = []; - const resourceMap = keyBy(resources, r => r.id); const clonedResources: PlainResourceWithId[] = resources.map(r => JSON.parse(JSON.stringify({...r.content, _id: r.id})) @@ -44,6 +48,9 @@ export class SimpleCustomValidator extends AbstractPlugin { ? clonedResources.filter(r => options.incremental?.resourceIds.includes(r._id)) : clonedResources; + const resourceMap = keyBy(resources, r => r.id); + const clonedResourceMap = keyBy(resources, r => r.id); + for (const rule of this.rules) { const ruleConfig = this.getRuleConfig(rule.id); @@ -57,7 +64,7 @@ export class SimpleCustomValidator extends AbstractPlugin { await validate( { resources: dirtyResources, - allResources: resources, + allResources: clonedResources, settings: this._settings, params: rule.properties?.configMetadata ? ruleConfig.parameters?.configValue : undefined, }, @@ -67,7 +74,7 @@ export class SimpleCustomValidator extends AbstractPlugin { return this._parser.parse(resource).parsedDoc; }, report: (res, args) => { - const resource = resourceMap[(res as PlainResourceWithId)._id]; + const resource = clonedResourceMap[(res as PlainResourceWithId)._id]; const result = this.adaptToValidationResult(rule, resource, args, options); if (!result) return; results.push(result); @@ -115,12 +122,38 @@ export class SimpleCustomValidator extends AbstractPlugin { const locations = createLocations(resource, region, path); - return this.createValidationResult(rule.id, { + const result = this.createValidationResult(rule.id, { message: { text: args.message ?? rule.shortDescription.text, }, locations, }); + + if (result && this._fixer) { + const {fix} = this._ruleRuntime[rule.id]; + + const fixedResource = JSON.parse(JSON.stringify(resource.content)); + + fix?.( + { + resource: fixedResource, + problem: result, + path: args.path, + }, + { + set: (resource: Resource['content'], path: string, value: any) => { + set(resource, path.split('.'), value); + }, + } + ); + + if (fixedResource) { + delete fixedResource['_id']; + result.fixes = this._fixer?.createFix(resource, fixedResource, this._parser); + } + } + + return result; } } @@ -206,7 +239,7 @@ function toSarifRules(plugin: PluginInit): RuleMetadata[] { function toRuntime(plugin: PluginInit): Record { const entries = Object.entries(plugin.rules).map(([_, rule]) => { - return [toRuleId(plugin.id, rule.id), {validate: rule.validate}]; + return [toRuleId(plugin.id, rule.id), {validate: rule.validate, fix: rule.fix}]; }); return Object.fromEntries(entries); diff --git a/packages/validation/src/validators/practices/rules/KBP106-noAutomountServiceAccountToken.ts b/packages/validation/src/validators/practices/rules/KBP106-noAutomountServiceAccountToken.ts index d1fe66392..5ad679aad 100644 --- a/packages/validation/src/validators/practices/rules/KBP106-noAutomountServiceAccountToken.ts +++ b/packages/validation/src/validators/practices/rules/KBP106-noAutomountServiceAccountToken.ts @@ -28,4 +28,7 @@ export const noAutomountServiceAccountToken = defineRule({ }); }); }, + fix({resource, path}, {set}) { + set(resource, path, false); + }, });