diff --git a/packages/validation/src/__tests__/MonokleValidator.kbp.test.ts b/packages/validation/src/__tests__/MonokleValidator.kbp.test.ts new file mode 100644 index 000000000..e914fc042 --- /dev/null +++ b/packages/validation/src/__tests__/MonokleValidator.kbp.test.ts @@ -0,0 +1,70 @@ +import {expect, it} from 'vitest'; +import {MonokleValidator} from '../MonokleValidator.js'; +import {processRefs} from '../references/index.js'; + +// Usage note: This library relies on fetch being on global scope! +import 'isomorphic-fetch'; +import {extractK8sResources} from '@monokle/parser'; +import {ValidationConfig} from '@monokle/types'; +import {ResourceParser} from '../common/resourceParser.js'; +import {Config, RuleMap} from '../config/parse.js'; +import {readDirectory} from './testUtils.js'; +import {DefaultPluginLoader} from '../pluginLoaders/PluginLoader.js'; +import {SchemaLoader} from '../validators/index.js'; +import {DisabledFixer} from '../sarif/index.js'; + +it('should detect rules which allow creation of pods', async () => { + const {response} = await processResourcesInFolder('src/__tests__/resources/kbp', { + 'practices/no-pod-create': true, + 'practices/no-pod-execute': false, + }); + + const errorCount = response.runs.reduce((sum, r) => sum + r.results.length, 0); + expect(errorCount).toBe(2); +}); + +it('should detect rules which allow execution of pods', async () => { + const {response} = await processResourcesInFolder('src/__tests__/resources/kbp', { + 'practices/no-pod-create': false, + 'practices/no-pod-execute': true, + }); + + const errorCount = response.runs.reduce((sum, r) => sum + r.results.length, 0); + expect(errorCount).toBe(1); +}); + +async function processResourcesInFolder(path: string, rules?: RuleMap) { + const files = await readDirectory(path); + const resources = extractK8sResources(files); + + const parser = new ResourceParser(); + const validator = createTestValidator(parser, rules); + const response = await validator.validate({resources}); + return {response, resources}; +} + +function createTestValidator(parser: ResourceParser, rules?: ValidationConfig['rules']) { + const config: Config = { + plugins: { + practices: true, + }, + settings: { + debug: true, + }, + }; + + if (rules) { + config.rules = rules; + } + + return new MonokleValidator( + { + loader: new DefaultPluginLoader(), + parser, + schemaLoader: new SchemaLoader(), + suppressors: [], + fixer: new DisabledFixer(), + }, + config + ); +} diff --git a/packages/validation/src/__tests__/resources/kbp/KBP107-role.yaml b/packages/validation/src/__tests__/resources/kbp/KBP107-role.yaml new file mode 100644 index 000000000..c13decc55 --- /dev/null +++ b/packages/validation/src/__tests__/resources/kbp/KBP107-role.yaml @@ -0,0 +1,88 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: argocd-applicationset-controller + namespace: argocd + labels: + app.kubernetes.io/component: applicationset-controller + app.kubernetes.io/name: argocd-applicationset-controller + app.kubernetes.io/part-of: argocd +rules: + - apiGroups: + - argoproj.io + resources: + - applications + - applicationsets + - applicationsets/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - argoproj.io + resources: + - appprojects + verbs: + - get + - apiGroups: + - argoproj.io + resources: + - applicationsets/status + verbs: + - get + - patch + - update + - apiGroups: + - argoproj.io + resources: + - applicationsets/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - events + verbs: + - create + - get + - list + - patch + - watch + - apiGroups: + - "" + resources: + - secrets + - configmaps + verbs: + - get + - list + - watch + - apiGroups: + - apps + - extensions + resources: + - deployments + verbs: + - get + - create + - list + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - create + - apiGroups: + - "" + resources: + - pods/exec + verbs: + - create diff --git a/packages/validation/src/validators/practices/rules/KBP107-noPodCreate.ts b/packages/validation/src/validators/practices/rules/KBP107-noPodCreate.ts index f73bfa51b..0164785f8 100644 --- a/packages/validation/src/validators/practices/rules/KBP107-noPodCreate.ts +++ b/packages/validation/src/validators/practices/rules/KBP107-noPodCreate.ts @@ -4,6 +4,8 @@ import {isClusterRole} from '../../custom/schemas/clusterrole.rbac.authorization import {isRole} from '../../custom/schemas/role.rbac.authorization.k8s.io.v1.js'; import {NSA_RELATIONS} from '../../../taxonomies/nsa.js'; +const KIND_CREATES_PODS = ['pods', 'deployments', 'statefulsets', 'jobs', 'cronjobs']; + export const noPodCreate = defineRule({ id: 107, description: 'Disallow permissions to create pods', @@ -15,8 +17,10 @@ export const noPodCreate = defineRule({ validate({resources}, {report}) { resources.filter(isTarget).forEach((role, index) => { role.rules?.forEach(rule => { - const isValid = rule.verbs?.includes('create'); - if (isValid) return; + const createPermission = rule.verbs?.includes('create'); + const resourceCanCreatePod = rule.resources?.some(r => KIND_CREATES_PODS.includes(r.toLowerCase())); + const isInvalid = resourceCanCreatePod && createPermission; + if (!isInvalid) return; report(role, {path: `spec.rules.${index}.verbs`}); }); }); diff --git a/packages/validation/src/validators/practices/rules/KBP108-noPodExecute.ts b/packages/validation/src/validators/practices/rules/KBP108-noPodExecute.ts index 7f4b09bd2..f2d1b7fbe 100644 --- a/packages/validation/src/validators/practices/rules/KBP108-noPodExecute.ts +++ b/packages/validation/src/validators/practices/rules/KBP108-noPodExecute.ts @@ -7,7 +7,7 @@ import {NSA_RELATIONS} from '../../../taxonomies/nsa.js'; export const noPodExecute = defineRule({ id: 108, description: 'Disallow permissions to exec on pods', - help: "Do not include 'pods/exec' in 'spec.rules[x].resourcess", + help: "Do not include 'pods/exec' in 'spec.rules[x].resources", advanced: { severity: 5, relationships: [NSA_RELATIONS['kubernetes-pod-security']], @@ -15,7 +15,7 @@ export const noPodExecute = defineRule({ validate({resources}, {report}) { resources.filter(isTarget).forEach((role, index) => { role.rules?.forEach(rule => { - const isValid = rule.resources?.includes('pods/exec'); + const isValid = rule.resources?.every(r => r.toLowerCase() !== 'pods/exec'); if (isValid) return; report(role, {path: `spec.rules.${index}.resources`}); });