Skip to content

Commit

Permalink
feat: add sarif suppressions
Browse files Browse the repository at this point in the history
  • Loading branch information
WitoDelnat committed Jul 27, 2023
1 parent 8fbeea7 commit 6e660f0
Show file tree
Hide file tree
Showing 29 changed files with 485 additions and 93 deletions.
29 changes: 23 additions & 6 deletions packages/validation/src/MonokleValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import difference from 'lodash/difference.js';
import uniqueId from 'lodash/uniqueId.js';
import isEqual from 'react-fast-compare';
import {ResourceParser} from './common/resourceParser.js';
import type {BaseLineState, Tool, ValidationResponse, ValidationResult, ValidationRun} from './common/sarif.js';
import type {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 {CIS_TAXONOMY} from './taxonomies/cis.js';
Expand All @@ -14,12 +14,14 @@ import {extractSchema, findDefaultVersion} from './utils/customResourceDefinitio
import {PluginLoadError} from './utils/error.js';
import invariant from './utils/invariant.js';
import {isDefined} from './utils/isDefined.js';
import {AnnotationSuppressor, Suppressor} from './sarif/suppressions/index.js';
import {SuppressEngine} from './sarif/suppressions/engine.js';

export type PluginLoader = (name: string, settings?: Record<string, any>) => Promise<Plugin>;
export type CustomPluginLoader = (name: string, parser: ResourceParser) => Promise<Plugin>;
export type CustomPluginLoader = (name: string, parser: ResourceParser, suppressor?: Suppressor) => Promise<Plugin>;

export function createMonokleValidator(loader: PluginLoader, fallback?: PluginMap) {
return new MonokleValidator(loader, fallback);
export function createMonokleValidator(loader: PluginLoader, suppressors: Suppressor[], fallback?: PluginMap) {
return new MonokleValidator(loader, suppressors, fallback);
}

/**
Expand All @@ -32,6 +34,8 @@ const DEFAULT_PLUGIN_MAP = {
'kubernetes-schema': true,
};

const DEFAULT_SUPPRESSORS = [new AnnotationSuppressor()];

type ValidateParams = {
/**
* The resources that will be validated.
Expand Down Expand Up @@ -83,9 +87,15 @@ export class MonokleValidator implements Validator {
_plugins: Plugin[] = [];
_failedPlugins: string[] = [];
_customSchemas: Set<string> = new Set();
private _suppressor: SuppressEngine;

constructor(loader: PluginLoader, fallback: PluginMap = DEFAULT_PLUGIN_MAP) {
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;
}
Expand Down Expand Up @@ -291,7 +301,9 @@ export class MonokleValidator implements Validator {

this.preprocessCustomResourceDefinitions(resources);

const allRuns = await Promise.allSettled(validators.map(v => v.validate(resources, incremental)));
new SuppressEngine().preload();

const allRuns = await Promise.allSettled(validators.map(v => v.validate(resources, {incremental})));
throwIfAborted(loadAbortSignal, externalAbortSignal);

const results = allRuns
Expand Down Expand Up @@ -321,6 +333,11 @@ export class MonokleValidator implements Validator {
this.compareWithBaseline(result, baseline);
}

await this._suppressor.suppress(result, resources, {
noInSourceSuppressions: this._config?.settings?.noInSourceSuppressions,
noExternalSuppressions: this._config?.settings?.noExternalSuppressions,
});

return result;
}

Expand Down
8 changes: 1 addition & 7 deletions packages/validation/src/__tests__/sarif/fingerprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ it('should have fingerprints & baseline', async () => {
},
});

const files = await readDirectory('./src/__tests__/sarif/assets');
const files = await readDirectory('./src/__tests__/resources/basic-deployment');
const resourceBad = extractK8sResources(files);
const badResponse = await validator.validate({resources: resourceBad});

Expand All @@ -44,12 +44,6 @@ it('should have fingerprints & baseline', async () => {
baseline: badResponse,
});

editedResponse.runs.forEach(r =>
r.results.forEach(result => {
console.error(result.ruleId, result.fingerprints?.['monokleHash/v1']);
})
);

const run = editedResponse.runs[0];
expect(run.baselineGuid).toBeDefined();
const unchangedCount = run.results.reduce((sum, r) => sum + (r.baselineState === 'unchanged' ? 1 : 0), 0);
Expand Down
83 changes: 83 additions & 0 deletions packages/validation/src/__tests__/sarif/suppression.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'isomorphic-fetch';
import {expect, it} from 'vitest';
import {createDefaultMonokleValidator} from '../../index.js';

import {PRACTICES_ALL_DISABLED, extractK8sResources, readDirectory} from '../testUtils.js';
import {FakeSuppressor} from '../../sarif/suppressions/plugins/FakeSuppressor.js';
import YAML from 'yaml';
import {set} from 'lodash';

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',
},
});

// Given a problem
const files = await readDirectory('./src/__tests__/resources/basic-deployment');
const badResponse = await validator.validate({
resources: extractK8sResources(files),
});
const problemCount = badResponse.runs[0].results.length;
expect(problemCount).toBe(1);

// When it is suppressed
suppressor.addSuppressionRequest(badResponse.runs[0].results[0]);

// Then the next validate response will mark the suppress request
const editedResponse = await validator.validate({
resources: extractK8sResources(files),
});

editedResponse.runs.forEach(r =>
r.results.forEach(result => {
console.error(result.ruleId, result.suppressions);
})
);

const problem = editedResponse.runs[0].results[0];
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',
},
});

// Given a problem
const files = await readDirectory('./src/__tests__/resources/basic-deployment');
const badResponse = await validator.validate({
resources: extractK8sResources(files),
});
const problemCount = badResponse.runs[0].results.length;
expect(problemCount).toBe(1);

// When an annotation is added
const content = YAML.parse(files[0].content);
set(content, 'metadata.annotations["monokle.io/suppress.kbp.no-latest-image"]', true);
files[0].content = YAML.stringify(content);

// Then the next validate response will mark the suppress request
const editedResponse = await validator.validate({
resources: extractK8sResources(files),
});

const problem = editedResponse.runs[0].results[0];
expect(problem.suppressions?.length).toBe(1);
});
15 changes: 8 additions & 7 deletions packages/validation/src/common/AbstractPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {PluginMetadataWithConfig, RuleMetadataWithConfig} from '../types.js';
import invariant from '../utils/invariant.js';
import {getResourceId} from '../utils/sarif.js';
import {ValidationResult, RuleMetadata, RuleConfig, ToolPlugin} from './sarif.js';
import {Incremental, Resource, Plugin, PluginMetadata, CustomSchema} from './types.js';
import {Incremental, Resource, Plugin, PluginMetadata, CustomSchema, ValidateOptions} from './types.js';
import {fingerprint} from '../utils/fingerprint.js';

const DEFAULT_RULE_CONFIG: RuleConfig = {
Expand Down Expand Up @@ -123,7 +123,7 @@ export abstract class AbstractPlugin implements Plugin {

protected createValidationResult(
ruleId: string,
args: Omit<ValidationResult, 'ruleId' | 'rule'>
args: Omit<ValidationResult, 'ruleId' | 'rule' | 'suppressions'>
): ValidationResult | undefined {
const index = this._ruleReverseLookup.get(ruleId);
invariant(index !== undefined, 'rules misconfigured');
Expand All @@ -145,6 +145,7 @@ export abstract class AbstractPlugin implements Plugin {
index: this.toolComponentIndex,
},
},
suppressions: [],
taxa,
level: ruleConfig.level,
...args,
Expand Down Expand Up @@ -235,21 +236,21 @@ export abstract class AbstractPlugin implements Plugin {
return;
}

async validate(resources: Resource[], incremental?: Incremental): Promise<ValidationResult[]> {
async validate(resources: Resource[], options: ValidateOptions = {}): Promise<ValidationResult[]> {
invariant(this.configured, NOT_CONFIGURED_ERR_MSG(this.name));

let results = await this.doValidate(resources, incremental);
let results = await this.doValidate(resources, options);

if (incremental) {
results = this.merge(this._previous, results, incremental);
if (options.incremental) {
results = this.merge(this._previous, results, options.incremental);
}

this._previous = results;

return results;
}

protected abstract doValidate(resources: Resource[], incremental?: Incremental): Promise<ValidationResult[]>;
protected abstract doValidate(resources: Resource[], options: ValidateOptions): Promise<ValidationResult[]>;

protected getRuleConfig(ruleId: string): RuleConfig {
const ruleConfig = this._ruleConfig.get(ruleId);
Expand Down
15 changes: 15 additions & 0 deletions packages/validation/src/common/sarif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ export type RunAutomationDetails = {
*/
export type Artifact = ConfigurationArtifact;

/**
* A request to suppress a result.
*
* @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#_Toc10541171
*/
export type Suppression = {
guid?: string;
kind: SuppressionKind;
status: SuppressionStatus;
justification?: string;
};
export type SuppressionKind = 'inSource' | 'external';
export type SuppressionStatus = 'underReview' | 'accept' | 'rejected'; // todo accepted!?

/**
* A Monokle Validation configuration file.
*
Expand Down Expand Up @@ -298,6 +312,7 @@ export type ValidationResult = {
};
fingerprints?: FingerPrints;
baselineState?: BaseLineState;
suppressions?: Suppression[];

/**
* The location of the error.
Expand Down
7 changes: 6 additions & 1 deletion packages/validation/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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<string | number>;

Expand Down Expand Up @@ -173,6 +174,10 @@ export type PluginConfig = {
enabled?: boolean;
};

export type ValidateOptions = {
incremental?: Incremental;
};

export interface Plugin {
/**
* The name of this plugin.
Expand Down Expand Up @@ -232,7 +237,7 @@ export interface Plugin {
registerCustomSchema(schema: CustomSchema): Promise<void> | void;
unregisterCustomSchema(schema: Omit<CustomSchema, 'schema'>): Promise<void> | void;

validate(resources: Resource[], incremental?: Incremental): Promise<ValidationResult[]>;
validate(resources: Resource[], options: ValidateOptions): Promise<ValidationResult[]>;

clear(): Promise<void>;
unload(): Promise<void>;
Expand Down
2 changes: 2 additions & 0 deletions packages/validation/src/config/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type ArrayRuleValue = [PrimitiveRuleValue] | [PrimitiveRuleValue, any];
export type ObjectRuleValue = {severity: PrimitiveRuleValue; config: any};
export type Settings = Record<string, any> & {
debug?: boolean;
noInSourceSuppressions?: boolean;
noExternalSuppressions?: boolean;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ 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 {AnnotationSuppressor, Suppressor} from './sarif/suppressions/index.js';

export function createDefaultMonokleValidator(
parser: ResourceParser = new ResourceParser(),
schemaLoader: SchemaLoader = new SchemaLoader()
schemaLoader: SchemaLoader = new SchemaLoader(),
suppressors: Suppressor[] = [new AnnotationSuppressor()]
) {
return new MonokleValidator(createDefaultPluginLoader(parser, schemaLoader));
return new MonokleValidator(createDefaultPluginLoader(parser, schemaLoader), suppressors);
}

export function createDefaultPluginLoader(
Expand Down
6 changes: 4 additions & 2 deletions packages/validation/src/createDefaultMonokleValidator.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ 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 {AnnotationSuppressor, Suppressor} from './sarif/suppressions/index.js';

export function createDefaultMonokleValidator(
parser: ResourceParser = new ResourceParser(),
schemaLoader: SchemaLoader = new SchemaLoader()
schemaLoader: SchemaLoader = new SchemaLoader(),
suppressors: Suppressor[] = [new AnnotationSuppressor()]
) {
return new MonokleValidator(createDefaultPluginLoader(parser, schemaLoader));
return new MonokleValidator(createDefaultPluginLoader(parser, schemaLoader), suppressors);
}

export function createDefaultPluginLoader(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ 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 {AnnotationSuppressor} from './sarif/suppressions/plugins/AnnotationSuppressor.js';

/**
* Creates a Monokle validator that can dynamically fetch custom plugins.
*/
export function createExtensibleMonokleValidator(
parser: ResourceParser = new ResourceParser(),
schemaLoader: SchemaLoader = new SchemaLoader(),
suppressors: Suppressor[] = [new AnnotationSuppressor()],
customPluginLoader: CustomPluginLoader = dynamicImportCustomPluginLoader
) {
return new MonokleValidator(async (pluginName: string, settings?: Record<string, any>) => {
Expand Down Expand Up @@ -65,5 +68,5 @@ export function createExtensibleMonokleValidator(
);
}
}
});
}, suppressors);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ 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';

/**
* Creates a Monokle validator that can dynamically fetch custom plugins.
*/
export function createExtensibleMonokleValidator(
parser: ResourceParser = new ResourceParser(),
schemaLoader: SchemaLoader = new SchemaLoader(),
suppressors: Suppressor[] = [new AnnotationSuppressor()],
customPluginLoader: CustomPluginLoader = requireFromStringCustomPluginLoader
) {
return new MonokleValidator(async (pluginNameOrUrl: string, settings?: Record<string, any>) => {
Expand Down Expand Up @@ -61,7 +63,7 @@ export function createExtensibleMonokleValidator(
);
}
}
});
}, suppressors);
}

async function getPlugin(path: string) {
Expand Down
1 change: 1 addition & 0 deletions packages/validation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
export * from './commonExports.js';

export * from './sarif/suppressions/index.js';
export * from './pluginLoaders/index.js';

export * from './createExtensibleMonokleValidator.browser.js';
Expand Down
Loading

0 comments on commit 6e660f0

Please sign in to comment.