From fcd2fc04a440ed7edda60614467334aa40a867cf Mon Sep 17 00:00:00 2001 From: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> Date: Tue, 22 Jul 2025 23:15:17 +0530 Subject: [PATCH 1/5] Add Rook Ceph MCP Server This MCP server enables AI assistants to manage Rook Ceph storage clusters in Kubernetes environments. It provides tools for cluster management, storage resource operations, and pre-configured YAML templates. Features: - Cluster and resource management tools - Kubernetes integration - Production-ready YAML templates - Both stdio and HTTP transport support Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> --- servers/rook-ceph-mcp-server/server.yaml | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 servers/rook-ceph-mcp-server/server.yaml diff --git a/servers/rook-ceph-mcp-server/server.yaml b/servers/rook-ceph-mcp-server/server.yaml new file mode 100644 index 0000000..266c50c --- /dev/null +++ b/servers/rook-ceph-mcp-server/server.yaml @@ -0,0 +1,34 @@ +name: rook-ceph-mcp-server +image: rook-ceph-mcp-server +type: server +metadata: + category: Infrastructure + tags: + - kubernetes + - storage + - ceph + - rook + - infrastructure + - devops +about: + title: Rook Ceph MCP Server + description: A Model Context Protocol server for managing Rook Ceph storage clusters in Kubernetes environments. Provides tools for cluster management, storage resource operations, and pre-configured YAML templates. + icon: 🗂️ +source: https://github.com/sunny/ceph-mcp +config: + variables: + - name: KUBECONFIG + description: Path to Kubernetes configuration file + required: false + default: ~/.kube/config + - name: PORT + description: HTTP server port (when using HTTP transport) + required: false + default: "3000" + - name: NODE_ENV + description: Node.js environment (development/production) + required: false + default: production + secrets: [] + dockerfile: Dockerfile + entry_point: ["npm", "run", "start:stdio"] \ No newline at end of file From f4c08926b90bbbb3985951927038d53adc2cf933 Mon Sep 17 00:00:00 2001 From: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> Date: Tue, 22 Jul 2025 23:16:59 +0530 Subject: [PATCH 2/5] Update repository URL in server configuration Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> --- servers/rook-ceph-mcp-server/server.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servers/rook-ceph-mcp-server/server.yaml b/servers/rook-ceph-mcp-server/server.yaml index 266c50c..dd4f2d2 100644 --- a/servers/rook-ceph-mcp-server/server.yaml +++ b/servers/rook-ceph-mcp-server/server.yaml @@ -14,7 +14,7 @@ about: title: Rook Ceph MCP Server description: A Model Context Protocol server for managing Rook Ceph storage clusters in Kubernetes environments. Provides tools for cluster management, storage resource operations, and pre-configured YAML templates. icon: 🗂️ -source: https://github.com/sunny/ceph-mcp +source: https://github.com/shreyanshjain7174/ceph-mcp config: variables: - name: KUBECONFIG From b3a8cb77d28a80b0f5e019ae9297cbf0b277d30f Mon Sep 17 00:00:00 2001 From: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:34:29 +0530 Subject: [PATCH 3/5] Fix rook-ceph-mcp-server configuration issues - Fix source field format to proper YAML structure - Update repository URL to correct GitHub location - Fix image namespace to use mcp/ prefix - Replace emoji icon with proper PNG URL using Rook organization avatar - All validation and build tests now pass successfully Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> --- servers/rook-ceph-mcp-server/server.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/servers/rook-ceph-mcp-server/server.yaml b/servers/rook-ceph-mcp-server/server.yaml index dd4f2d2..1877d8d 100644 --- a/servers/rook-ceph-mcp-server/server.yaml +++ b/servers/rook-ceph-mcp-server/server.yaml @@ -1,5 +1,5 @@ name: rook-ceph-mcp-server -image: rook-ceph-mcp-server +image: mcp/rook-ceph-mcp-server type: server metadata: category: Infrastructure @@ -13,8 +13,9 @@ metadata: about: title: Rook Ceph MCP Server description: A Model Context Protocol server for managing Rook Ceph storage clusters in Kubernetes environments. Provides tools for cluster management, storage resource operations, and pre-configured YAML templates. - icon: 🗂️ -source: https://github.com/shreyanshjain7174/ceph-mcp + icon: https://avatars.githubusercontent.com/u/35940573?s=200&v=4 +source: + project: https://github.com/shreyanshjain7174/rook-ceph-mcp config: variables: - name: KUBECONFIG From 15d7ccba5076d2abc76984a76e07ef9172c0003d Mon Sep 17 00:00:00 2001 From: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:56:56 +0530 Subject: [PATCH 4/5] Add Dockerfile validation to task validate command - Add GetFileContent method to GitHub client for fetching file content - Implement isDockerfileValid function with comprehensive validation: - Path format validation (relative path, no directory traversal) - Dockerfile content validation (FROM instruction, ENTRYPOINT/CMD, security checks) - Graceful handling of servers without source projects - Update license validation to skip servers without source projects - Add proper error handling and user-friendly messaging Fixes #121 Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> --- cmd/validate/main.go | 142 ++++++++++++++++++++++++++++++++++++++++++- pkg/github/github.go | 39 ++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) diff --git a/cmd/validate/main.go b/cmd/validate/main.go index 7304827..0a47f90 100644 --- a/cmd/validate/main.go +++ b/cmd/validate/main.go @@ -50,6 +50,10 @@ func run(name string) error { return err } + if err := isDockerfileValid(name); err != nil { + return err + } + return nil } @@ -110,12 +114,19 @@ func areSecretsValid(name string) error { // check if the license is valid // the license must be valid func IsLicenseValid(name string) error { - ctx := context.Background() - client := github.New() server, err := readServerYaml(name) if err != nil { return err } + + // Skip validation for servers without source project + if server.Source.Project == "" { + fmt.Println("✅ License validation skipped (no source project)") + return nil + } + + ctx := context.Background() + client := github.New() repository, err := client.GetProjectRepository(ctx, server.Source.Project) if err != nil { return err @@ -173,6 +184,133 @@ func isIconValid(name string) error { return nil } +func isDockerfileValid(name string) error { + server, err := readServerYaml(name) + if err != nil { + return err + } + + // Skip validation for servers without source project + if server.Source.Project == "" { + fmt.Println("✅ Dockerfile validation skipped (no source project)") + return nil + } + + // Determine Dockerfile path - default to "Dockerfile" if not specified + dockerfilePath := server.Source.Dockerfile + if dockerfilePath == "" { + dockerfilePath = "Dockerfile" + } + + // Validate Dockerfile path format + if err := validateDockerfilePath(dockerfilePath); err != nil { + return fmt.Errorf("invalid Dockerfile path: %w", err) + } + + // Skip GitHub API validation for local development or if GITHUB_TOKEN is not set + if os.Getenv("GITHUB_TOKEN") == "" { + fmt.Println("🛑 Dockerfile content validation skipped (no GITHUB_TOKEN)") + return nil + } + + // Fetch and validate Dockerfile content from GitHub + ctx := context.Background() + client := github.NewFromServer(server) + + // Construct full path including directory if specified + fullPath := dockerfilePath + if server.Source.Directory != "" { + fullPath = filepath.Join(server.Source.Directory, dockerfilePath) + } + + content, err := client.GetFileContent(ctx, server.Source.Project, server.Source.Branch, fullPath) + if err != nil { + return fmt.Errorf("failed to fetch Dockerfile: %w", err) + } + + // Validate Dockerfile content + if err := validateDockerfileContent(content); err != nil { + return fmt.Errorf("invalid Dockerfile content: %w", err) + } + + fmt.Println("✅ Dockerfile is valid") + return nil +} + +func validateDockerfilePath(path string) error { + // Must be relative path + if strings.HasPrefix(path, "/") { + return fmt.Errorf("Dockerfile path must be relative, not absolute") + } + + // Should not contain directory traversal + if strings.Contains(path, "..") { + return fmt.Errorf("Dockerfile path should not contain directory traversal (..) components") + } + + // Should be a valid filename + if strings.Contains(path, "\x00") { + return fmt.Errorf("Dockerfile path contains invalid characters") + } + + return nil +} + +func validateDockerfileContent(content string) error { + lines := strings.Split(content, "\n") + + // Remove empty lines and comments for validation + var validLines []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" && !strings.HasPrefix(trimmed, "#") { + validLines = append(validLines, trimmed) + } + } + + if len(validLines) == 0 { + return fmt.Errorf("Dockerfile is empty or contains only comments") + } + + // First instruction must be FROM + firstLine := strings.ToUpper(strings.TrimSpace(validLines[0])) + if !strings.HasPrefix(firstLine, "FROM ") { + return fmt.Errorf("Dockerfile must start with FROM instruction") + } + + // Check for basic security issues + for _, line := range validLines { + upperLine := strings.ToUpper(line) + + // Check for potential security issues + if strings.Contains(upperLine, "PASSWORD=") || + strings.Contains(upperLine, "SECRET=") || + strings.Contains(upperLine, "API_KEY=") || + strings.Contains(upperLine, "TOKEN=") { + return fmt.Errorf("Dockerfile should not contain hardcoded credentials") + } + } + + // Should have an ENTRYPOINT or CMD instruction + hasEntrypoint := false + hasCmd := false + for _, line := range validLines { + upperLine := strings.ToUpper(strings.TrimSpace(line)) + if strings.HasPrefix(upperLine, "ENTRYPOINT ") { + hasEntrypoint = true + } + if strings.HasPrefix(upperLine, "CMD ") { + hasCmd = true + } + } + + if !hasEntrypoint && !hasCmd { + return fmt.Errorf("Dockerfile must contain either ENTRYPOINT or CMD instruction") + } + + return nil +} + func readServerYaml(name string) (servers.Server, error) { serverYaml, err := os.ReadFile(filepath.Join("servers", name, "server.yaml")) if err != nil { diff --git a/pkg/github/github.go b/pkg/github/github.go index 3c33bdf..6a8015c 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -106,6 +106,45 @@ func (c *Client) FindIcon(ctx context.Context, projectURL string) (string, error return repository.Owner.GetAvatarURL(), nil } +func (c *Client) GetFileContent(ctx context.Context, projectURL, branch, filePath string) (string, error) { + owner, repo, err := extractOrgAndProject(projectURL) + if err != nil { + return "", err + } + + if branch == "" { + repository, err := c.GetProjectRepository(ctx, projectURL) + if err != nil { + return "", err + } + branch = repository.GetDefaultBranch() + } + + for { + fileContent, _, _, err := c.gh.Repositories.GetContents(ctx, owner, repo, filePath, &github.RepositoryContentGetOptions{ + Ref: branch, + }) + if sleepOnRateLimitError(ctx, err) { + continue + } + + if err != nil { + return "", err + } + + if fileContent == nil { + return "", fmt.Errorf("file not found: %s", filePath) + } + + content, err := fileContent.GetContent() + if err != nil { + return "", err + } + + return content, nil + } +} + func sleepOnRateLimitError(ctx context.Context, err error) bool { var rateLimitErr *github.RateLimitError if !errors.As(err, &rateLimitErr) { From 2af14ac8e82edd0bdc57db1aeafe8937c5a8f2c4 Mon Sep 17 00:00:00 2001 From: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:05:35 +0530 Subject: [PATCH 5/5] Add comprehensive tests for Dockerfile validation functions - Add TestValidateDockerfilePath to test path validation logic - Add TestValidateDockerfileContent to test Dockerfile content validation - Tests cover various edge cases including security validations - All new Dockerfile validation tests pass successfully Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> --- cmd/validate/main_test.go | 98 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/cmd/validate/main_test.go b/cmd/validate/main_test.go index a66369b..6bffbae 100644 --- a/cmd/validate/main_test.go +++ b/cmd/validate/main_test.go @@ -104,3 +104,101 @@ func Test_areSecretsValid(t *testing.T) { }) } } + +func TestValidateDockerfilePath(t *testing.T) { + tests := []struct { + path string + wantErr bool + desc string + }{ + {"Dockerfile", false, "simple Dockerfile"}, + {"src/postgres/Dockerfile", false, "relative path with subdirectory"}, + {"Dockerfile.local", false, "Dockerfile with suffix"}, + {"/Dockerfile", true, "absolute path should be rejected"}, + {"../Dockerfile", true, "directory traversal should be rejected"}, + {"src/../Dockerfile", true, "directory traversal in middle should be rejected"}, + {"Dockerfile\x00", true, "null byte should be rejected"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + err := validateDockerfilePath(tt.path) + if (err != nil) != tt.wantErr { + t.Errorf("validateDockerfilePath(%q) error = %v, wantErr %v", tt.path, err, tt.wantErr) + } + }) + } +} + +func TestValidateDockerfileContent(t *testing.T) { + tests := []struct { + content string + wantErr bool + desc string + }{ + { + content: "FROM node:18-alpine\nWORKDIR /app\nCMD [\"node\", \"index.js\"]", + wantErr: false, + desc: "valid simple Dockerfile", + }, + { + content: "FROM alpine\nRUN apk add --no-cache curl\nENTRYPOINT [\"curl\"]", + wantErr: false, + desc: "valid Dockerfile with ENTRYPOINT", + }, + { + content: "# Comment only Dockerfile\n# Another comment", + wantErr: true, + desc: "Dockerfile with only comments should fail", + }, + { + content: "", + wantErr: true, + desc: "empty Dockerfile should fail", + }, + { + content: "RUN echo hello\nFROM alpine", + wantErr: true, + desc: "Dockerfile not starting with FROM should fail", + }, + { + content: "FROM node:18\nWORKDIR /app", + wantErr: true, + desc: "Dockerfile without CMD or ENTRYPOINT should fail", + }, + { + content: "FROM alpine\nENV PASSWORD=secret123\nCMD [\"sh\"]", + wantErr: true, + desc: "Dockerfile with hardcoded password should fail", + }, + { + content: "FROM alpine\nENV API_KEY=abc123\nCMD [\"sh\"]", + wantErr: true, + desc: "Dockerfile with hardcoded API key should fail", + }, + { + content: "FROM alpine\nENV SECRET=mysecret\nENTRYPOINT [\"sh\"]", + wantErr: true, + desc: "Dockerfile with hardcoded secret should fail", + }, + { + content: "FROM alpine\nENV TOKEN=mytoken\nCMD [\"sh\"]", + wantErr: true, + desc: "Dockerfile with hardcoded token should fail", + }, + { + content: "FROM node:18\n# This is a comment\nWORKDIR /app\n# Another comment\nCMD [\"node\", \"app.js\"]", + wantErr: false, + desc: "valid Dockerfile with comments should pass", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + err := validateDockerfileContent(tt.content) + if (err != nil) != tt.wantErr { + t.Errorf("validateDockerfileContent() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}