From 223059c0653776f52330acaa2c8d94de00c80d61 Mon Sep 17 00:00:00 2001 From: Vamshi Goud Kesari <120409185+pt-vamshi@users.noreply.github.com> Date: Sun, 25 May 2025 16:37:54 +0530 Subject: [PATCH 1/3] add security feature --- src/index.ts | 10 +++ src/tools/k8s_security_check.ts | 133 ++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/tools/k8s_security_check.ts diff --git a/src/index.ts b/src/index.ts index d2670f3..02c9a28 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, @@ -29,6 +30,15 @@ import { KubernetesManager } from "./types.js"; import { serverConfig } from "./config/server-config.js"; import { cleanupSchema } from "./config/cleanup-config.js"; import { startSSEServer } from "./utils/sse.js"; +import { + k8sSecurityCheck, + k8sSecurityCheckSchema, +} from "./tools/k8s_security_check.js"; +allTools.push(k8sSecurityCheckSchema); + +case "k8s_security_check": { + return await k8sSecurityCheck(k8sManager); +} import { startPortForward, PortForwardSchema, diff --git a/src/tools/k8s_security_check.ts b/src/tools/k8s_security_check.ts new file mode 100644 index 0000000..1f1b308 --- /dev/null +++ b/src/tools/k8s_security_check.ts @@ -0,0 +1,133 @@ +import { execSync } from 'child_process'; + +type Finding = { + type: string; + namespace: string; + resource: string; + details: string; +}; + +function runKubectl(resource: string): any { + const cmd = `kubectl get ${resource} -A -o json`; + const output = execSync(cmd).toString(); + return JSON.parse(output); +} + +// 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; + + 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` + }); + } + }); + }); + + return findings; +} + +// 2. Overly Permissive RBAC +function checkRbacPermissions(): Finding[] { + const findings: Finding[] = []; + + const roles = runKubectl('roles'); + const clusterRoles = runKubectl('clusterroles'); + + const scanRules = (rules: any[], name: string, kind: string, ns?: string) => { + 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; + + item.spec.containers.forEach((c: any) => { + (c.env || []).forEach((envVar: any) => { + if ('value' in envVar) { + findings.push({ + type: 'Exposed Secret', + namespace: ns, + resource: name, + details: `Container ${c.name} env var '${envVar.name}' contains a literal value` + }); + } + }); + }); + }); + + return findings; +} + +// 4. Missing Network Policies +function checkMissingNetworkPolicies(): Finding[] { + const namespaces = runKubectl('namespaces'); + const netpols = runKubectl('networkpolicies'); + + const nsWithNetpols = new Set(netpols.items.map((np: any) => np.metadata.namespace)); + const findings: Finding[] = []; + + 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' + }); + } + }); + + return findings; +} + +export function k8sSecurityCheck(): Finding[] { + const results: Finding[] = []; + + results.push(...checkPrivilegedPods()); + results.push(...checkRbacPermissions()); + results.push(...checkExposedSecrets()); + results.push(...checkMissingNetworkPolicies()); + + return results; +} From 7bf2c1cc3f16f64f420eb04974bc0296b4a6f771 Mon Sep 17 00:00:00 2001 From: pt-vamshi Date: Sun, 25 May 2025 16:58:23 +0530 Subject: [PATCH 2/3] added securtity scan basic --- src/index.ts | 50 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 02c9a28..5f01fd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,15 +30,6 @@ import { KubernetesManager } from "./types.js"; import { serverConfig } from "./config/server-config.js"; import { cleanupSchema } from "./config/cleanup-config.js"; import { startSSEServer } from "./utils/sse.js"; -import { - k8sSecurityCheck, - k8sSecurityCheckSchema, -} from "./tools/k8s_security_check.js"; -allTools.push(k8sSecurityCheckSchema); - -case "k8s_security_check": { - return await k8sSecurityCheck(k8sManager); -} import { startPortForward, PortForwardSchema, @@ -57,6 +48,18 @@ import { kubectlLogs, kubectlLogsSchema } from "./tools/kubectl-logs.js"; import { kubectlGeneric, 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: [], + }, +}; // Check if non-destructive tools only mode is enabled const nonDestructiveTools = @@ -107,6 +110,9 @@ const allTools = [ // Generic kubectl command kubectlGenericSchema, + + // Security operations + k8sSecurityCheckSchema, ]; const k8sManager = new KubernetesManager(); @@ -401,6 +407,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}`); } From 7193aff9b13f2abd336ef87ac4e45f8a586323cd Mon Sep 17 00:00:00 2001 From: Vamshi Goud Kesari <120409185+pt-vamshi@users.noreply.github.com> Date: Sun, 25 May 2025 17:15:33 +0530 Subject: [PATCH 3/3] add more securites --- src/tools/k8s_security_check.ts | 76 +++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/src/tools/k8s_security_check.ts b/src/tools/k8s_security_check.ts index 1f1b308..34f1918 100644 --- a/src/tools/k8s_security_check.ts +++ b/src/tools/k8s_security_check.ts @@ -7,10 +7,16 @@ type Finding = { details: string; }; +// Run kubectl and return parsed JSON, or fallback to empty list function runKubectl(resource: string): any { - const cmd = `kubectl get ${resource} -A -o json`; - const output = execSync(cmd).toString(); - return JSON.parse(output); + 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 @@ -21,7 +27,10 @@ function checkPrivilegedPods(): Finding[] { data.items.forEach((item: any) => { const ns = item.metadata.namespace; const name = item.metadata.name; - const containers = item.spec.containers; + const containers = [ + ...(item.spec.containers || []), + ...(item.spec.initContainers || []) + ]; containers.forEach((c: any) => { const ctx = c.securityContext || {}; @@ -34,6 +43,16 @@ function checkPrivilegedPods(): Finding[] { }); } }); + + // 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; @@ -42,11 +61,14 @@ function checkPrivilegedPods(): Finding[] { // 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('*') || @@ -61,11 +83,11 @@ function checkRbacPermissions(): Finding[] { }); }; - roles.items.forEach((item: any) => { + (roles.items || []).forEach((item: any) => { scanRules(item.rules, item.metadata.name, 'Role', item.metadata.namespace); }); - clusterRoles.items.forEach((item: any) => { + (clusterRoles.items || []).forEach((item: any) => { scanRules(item.rules, item.metadata.name, 'ClusterRole'); }); @@ -81,14 +103,19 @@ function checkExposedSecrets(): Finding[] { const ns = item.metadata.namespace; const name = item.metadata.name; - item.spec.containers.forEach((c: any) => { + const containers = [ + ...(item.spec.containers || []), + ...(item.spec.initContainers || []) + ]; + + containers.forEach((c: any) => { (c.env || []).forEach((envVar: any) => { - if ('value' in envVar) { + 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}' contains a literal value` + details: `Container ${c.name} env var '${envVar.name}' may contain sensitive data` }); } }); @@ -98,14 +125,15 @@ function checkExposedSecrets(): Finding[] { return findings; } -// 4. Missing Network Policies -function checkMissingNetworkPolicies(): Finding[] { +// 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)); - const findings: Finding[] = []; + // Namespaces without any NetworkPolicy namespaces.items.forEach((ns: any) => { const nsName = ns.metadata.name; if (!nsWithNetpols.has(nsName)) { @@ -118,16 +146,36 @@ function checkMissingNetworkPolicies(): Finding[] { } }); + // 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(...checkMissingNetworkPolicies()); + results.push(...checkNetworkPolicies()); return results; }