diff --git a/src/index.ts b/src/index.ts index 7490932..1056f26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + import { installHelmChart, installHelmChartSchema, @@ -54,6 +55,19 @@ import { kubectlGenericSchema, } from "./tools/kubectl-generic.js"; import { kubectlPatch, kubectlPatchSchema } from "./tools/kubectl-patch.js"; +import { kubectlRollout, kubectlRolloutSchema } from "./tools/kubectl-rollout.js"; +import { k8sSecurityCheck } from "./tools/k8s_security_check.js"; + +// Define k8s security check schema +const k8sSecurityCheckSchema = { + name: "k8s_security_check", + description: "Perform comprehensive security checks on Kubernetes cluster including privileged pods, RBAC permissions, exposed secrets, and missing network policies", + inputSchema: { + type: "object", + properties: {}, + required: [], + }, +}; import { kubectlRollout, kubectlRolloutSchema, @@ -108,6 +122,9 @@ const allTools = [ // Generic kubectl command kubectlGenericSchema, + + // Security operations + k8sSecurityCheckSchema, ]; const k8sManager = new KubernetesManager(); @@ -441,6 +458,32 @@ server.setRequestHandler( ); } + case "k8s_security_check": { + const findings = k8sSecurityCheck(); + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + success: true, + findings: findings, + summary: { + total: findings.length, + privilegedPods: findings.filter(f => f.type === 'Privileged Pod').length, + permissiveRBAC: findings.filter(f => f.type === 'Permissive RBAC').length, + exposedSecrets: findings.filter(f => f.type === 'Exposed Secret').length, + missingNetworkPolicies: findings.filter(f => f.type === 'Missing NetworkPolicy').length, + } + }, + null, + 2 + ), + }, + ], + }; + } + default: throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${name}`); } diff --git a/src/tools/k8s_security_check.ts b/src/tools/k8s_security_check.ts new file mode 100644 index 0000000..34f1918 --- /dev/null +++ b/src/tools/k8s_security_check.ts @@ -0,0 +1,181 @@ +import { execSync } from 'child_process'; + +type Finding = { + type: string; + namespace: string; + resource: string; + details: string; +}; + +// Run kubectl and return parsed JSON, or fallback to empty list +function runKubectl(resource: string): any { + try { + const cmd = `kubectl get ${resource} -A -o json`; + const output = execSync(cmd).toString(); + return JSON.parse(output); + } catch (error: any) { + console.error(`Error fetching ${resource}:`, error.message || error); + return { items: [] }; + } +} + +// 1. Privileged or Root Pods +function checkPrivilegedPods(): Finding[] { + const data = runKubectl('pods'); + const findings: Finding[] = []; + + data.items.forEach((item: any) => { + const ns = item.metadata.namespace; + const name = item.metadata.name; + const containers = [ + ...(item.spec.containers || []), + ...(item.spec.initContainers || []) + ]; + + containers.forEach((c: any) => { + const ctx = c.securityContext || {}; + if (ctx.privileged || ctx.runAsNonRoot === false || ctx.runAsUser === 0) { + findings.push({ + type: 'Privileged Pod', + namespace: ns, + resource: name, + details: `Container ${c.name} is privileged or running as root` + }); + } + }); + + // Optional: Check for hostPath volumes + if ((item.spec.volumes || []).some((v: any) => v.hostPath)) { + findings.push({ + type: 'HostPath Volume', + namespace: ns, + resource: name, + details: 'Pod uses hostPath volume' + }); + } + }); + + return findings; +} + +// 2. Overly Permissive RBAC +function checkRbacPermissions(): Finding[] { + const findings: Finding[] = []; + const roles = runKubectl('roles'); + const clusterRoles = runKubectl('clusterroles'); + + const excludedClusterRoles = new Set(['cluster-admin', 'admin', 'edit', 'view']); + + const scanRules = (rules: any[], name: string, kind: string, ns?: string) => { + if (kind === 'ClusterRole' && excludedClusterRoles.has(name)) return; + + rules.forEach(rule => { + if ((rule.verbs || []).includes('*') || + (rule.resources || []).includes('*') || + (rule.apiGroups || []).includes('*')) { + findings.push({ + type: 'Permissive RBAC', + namespace: ns || 'cluster-wide', + resource: `${kind}/${name}`, + details: 'Contains wildcard permissions' + }); + } + }); + }; + + (roles.items || []).forEach((item: any) => { + scanRules(item.rules, item.metadata.name, 'Role', item.metadata.namespace); + }); + + (clusterRoles.items || []).forEach((item: any) => { + scanRules(item.rules, item.metadata.name, 'ClusterRole'); + }); + + return findings; +} + +// 3. Secrets in Env Vars +function checkExposedSecrets(): Finding[] { + const data = runKubectl('pods'); + const findings: Finding[] = []; + + data.items.forEach((item: any) => { + const ns = item.metadata.namespace; + const name = item.metadata.name; + + const containers = [ + ...(item.spec.containers || []), + ...(item.spec.initContainers || []) + ]; + + containers.forEach((c: any) => { + (c.env || []).forEach((envVar: any) => { + if ('value' in envVar && /secret|token|key|password/i.test(envVar.name)) { + findings.push({ + type: 'Exposed Secret', + namespace: ns, + resource: name, + details: `Container ${c.name} env var '${envVar.name}' may contain sensitive data` + }); + } + }); + }); + }); + + return findings; +} + +// 4. Missing or Unrestricted Network Policies +function checkNetworkPolicies(): Finding[] { + const findings: Finding[] = []; + const namespaces = runKubectl('namespaces'); + const netpols = runKubectl('networkpolicies'); + + const nsWithNetpols = new Set(netpols.items.map((np: any) => np.metadata.namespace)); + + // Namespaces without any NetworkPolicy + namespaces.items.forEach((ns: any) => { + const nsName = ns.metadata.name; + if (!nsWithNetpols.has(nsName)) { + findings.push({ + type: 'Missing NetworkPolicy', + namespace: nsName, + resource: 'Namespace', + details: 'No network policy present' + }); + } + }); + + // Network policies with open ingress and egress + netpols.items.forEach((np: any) => { + const ns = np.metadata.namespace; + const name = np.metadata.name; + const spec = np.spec; + + const allowsAllIngress = !spec.ingress || spec.ingress.length === 0; + const allowsAllEgress = !spec.egress || spec.egress.length === 0; + + if (allowsAllIngress && allowsAllEgress) { + findings.push({ + type: 'Unrestricted NetworkPolicy', + namespace: ns, + resource: name, + details: 'Policy allows all ingress and egress traffic' + }); + } + }); + + return findings; +} + +// Aggregate security check results +export function k8sSecurityCheck(): Finding[] { + const results: Finding[] = []; + + results.push(...checkPrivilegedPods()); + results.push(...checkRbacPermissions()); + results.push(...checkExposedSecrets()); + results.push(...checkNetworkPolicies()); + + return results; +}