Skip to content

Commit 292365b

Browse files
sallyomclaude
andcommitted
PR Review suggestions
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: sallyom <somalley@redhat.com>
1 parent 9b7b5a7 commit 292365b

File tree

13 files changed

+282
-57
lines changed

13 files changed

+282
-57
lines changed

components/backend/crd/bugfix.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package crd
33
import (
44
"context"
55
"fmt"
6+
"log"
67

78
"ambient-code-backend/types"
89

@@ -113,8 +114,16 @@ func GetProjectBugFixWorkflowCR(dyn dynamic.Interface, project, id string) (*typ
113114

114115
spec, found, _ := unstructured.NestedMap(obj.Object, "spec")
115116
if found {
117+
// Parse githubIssueNumber with type safety - handle both int64 and float64
118+
// JSON unmarshaling can produce either depending on the source
116119
if val, ok := spec["githubIssueNumber"].(int64); ok {
117120
workflow.GithubIssueNumber = int(val)
121+
} else if val, ok := spec["githubIssueNumber"].(float64); ok {
122+
workflow.GithubIssueNumber = int(val)
123+
} else if spec["githubIssueNumber"] != nil {
124+
// Log warning if type assertion fails - helps debug unexpected types
125+
log.Printf("Warning: githubIssueNumber has unexpected type %T in BugFixWorkflow %s/%s",
126+
spec["githubIssueNumber"], project, id)
118127
}
119128
if val, ok := spec["githubIssueURL"].(string); ok {
120129
workflow.GithubIssueURL = val
@@ -144,16 +153,22 @@ func GetProjectBugFixWorkflowCR(dyn dynamic.Interface, project, id string) (*typ
144153
workflow.LastSyncedAt = &val
145154
}
146155

147-
// Parse implementationRepo
156+
// Parse implementationRepo (required field)
148157
if implMap, ok := spec["implementationRepo"].(map[string]interface{}); ok {
149158
repo := types.GitRepository{}
150-
if url, ok := implMap["url"].(string); ok {
159+
if url, ok := implMap["url"].(string); ok && url != "" {
151160
repo.URL = url
161+
} else {
162+
// Critical field missing - return error instead of zero value
163+
return nil, fmt.Errorf("BugFixWorkflow %s/%s missing required field: implementationRepo.url", project, id)
152164
}
153165
if branch, ok := implMap["branch"].(string); ok && branch != "" {
154166
repo.Branch = &branch
155167
}
156168
workflow.ImplementationRepo = repo
169+
} else {
170+
// implementationRepo is required
171+
return nil, fmt.Errorf("BugFixWorkflow %s/%s missing required field: implementationRepo", project, id)
157172
}
158173
}
159174

components/backend/git/operations.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ func CheckRepoSeeding(ctx context.Context, repoURL string, branch *string, githu
206206

207207
// ParseGitHubURL extracts owner and repo from a GitHub URL
208208
func ParseGitHubURL(gitURL string) (owner, repo string, err error) {
209+
// Only HTTPS URLs are supported (no SSH git@ URLs)
210+
if !strings.HasPrefix(gitURL, "https://") {
211+
return "", "", fmt.Errorf("invalid URL scheme: must be https://")
212+
}
213+
209214
gitURL = strings.TrimSuffix(gitURL, ".git")
210215

211216
if strings.Contains(gitURL, "github.com") {
@@ -218,7 +223,15 @@ func ParseGitHubURL(gitURL string) (owner, repo string, err error) {
218223
if len(pathParts) < 2 {
219224
return "", "", fmt.Errorf("invalid GitHub URL path")
220225
}
221-
return pathParts[0], pathParts[1], nil
226+
227+
// Validate owner and repo are non-empty
228+
owner = pathParts[0]
229+
repo = pathParts[1]
230+
if owner == "" || repo == "" {
231+
return "", "", fmt.Errorf("owner and repository name cannot be empty")
232+
}
233+
234+
return owner, repo, nil
222235
}
223236

224237
return "", "", fmt.Errorf("not a GitHub URL")

components/backend/handlers/bugfix/create.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"log"
66
"net/http"
7+
"regexp"
78
"strings"
89
"time"
910

@@ -24,6 +25,31 @@ var (
2425
GetProjectSettingsResource func() schema.GroupVersionResource
2526
)
2627

28+
// validBranchNameRegex defines allowed characters in branch names
29+
// Allows: letters, numbers, hyphens, underscores, forward slashes, and dots
30+
// Prevents: shell metacharacters, backticks, quotes, semicolons, pipes, etc.
31+
var validBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9/_.-]+$`)
32+
33+
// validateBranchName checks if a branch name is safe to use in git operations
34+
// Returns error if the branch name contains potentially dangerous characters
35+
func validateBranchName(branchName string) error {
36+
if branchName == "" {
37+
return fmt.Errorf("branch name cannot be empty")
38+
}
39+
if !validBranchNameRegex.MatchString(branchName) {
40+
return fmt.Errorf("branch name contains invalid characters (allowed: a-z, A-Z, 0-9, /, _, -, .)")
41+
}
42+
// Prevent branch names that start with special characters
43+
if strings.HasPrefix(branchName, ".") || strings.HasPrefix(branchName, "-") {
44+
return fmt.Errorf("branch name cannot start with '.' or '-'")
45+
}
46+
// Prevent branch names with ".." (path traversal) or "//" (double slashes)
47+
if strings.Contains(branchName, "..") || strings.Contains(branchName, "//") {
48+
return fmt.Errorf("branch name cannot contain '..' or '//'")
49+
}
50+
return nil
51+
}
52+
2753
// CreateProjectBugFixWorkflow handles POST /api/projects/:projectName/bugfix-workflows
2854
// Creates a new BugFix Workspace from either GitHub Issue URL or text description
2955
func CreateProjectBugFixWorkflow(c *gin.Context) {
@@ -162,9 +188,14 @@ func CreateProjectBugFixWorkflow(c *gin.Context) {
162188
}
163189

164190
// Auto-generate branch name if not provided
165-
branch := "main"
191+
var branch string
166192
if req.BranchName != nil && *req.BranchName != "" {
167193
branch = *req.BranchName
194+
// Validate user-provided branch name for security
195+
if err := validateBranchName(branch); err != nil {
196+
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid branch name", "details": err.Error()})
197+
return
198+
}
168199
} else {
169200
// Auto-generate branch name: bugfix/gh-{issue-number}
170201
branch = fmt.Sprintf("bugfix/gh-%d", githubIssue.Number)

components/backend/handlers/bugfix/handlers_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func TestCreateBugFixWorkflow(t *testing.T) {
1111
// Test cases:
1212
// 1. Valid GitHub Issue URL -> 201 Created
1313
// 2. Invalid GitHub Issue URL -> 400 Bad Request
14-
// 3. Missing umbrellaRepo -> 400 Bad Request
14+
// 3. Missing implementationRepo -> 400 Bad Request
1515
// 4. Duplicate workspace (same issue number) -> 409 Conflict
1616
// 5. Text description with valid targetRepository -> 201 Created
1717
// 6. Both githubIssueURL and textDescription -> 400 Bad Request

components/backend/handlers/bugfix/jira_sync.go

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9+
"log"
910
"net/http"
1011
"strings"
1112
"time"
@@ -152,6 +153,7 @@ func SyncProjectBugFixWorkflowToJira(c *gin.Context) {
152153
req.Header.Set("Content-Type", "application/json")
153154
req.Header.Set("Accept", "application/json")
154155

156+
// Use context from request for proper cancellation propagation
155157
client := &http.Client{Timeout: 30 * time.Second}
156158
resp, err := client.Do(req)
157159
if err != nil {
@@ -187,26 +189,29 @@ func SyncProjectBugFixWorkflowToJira(c *gin.Context) {
187189
return
188190
case 404:
189191
websocket.BroadcastBugFixJiraSyncFailed(workflowID, workflow.GithubIssueNumber, "Jira project not found")
190-
c.JSON(http.StatusBadRequest, gin.H{"error": "Jira project not found", "details": string(body)})
192+
// Don't expose Jira error details to user - may contain sensitive info
193+
log.Printf("Jira 404 error details: %s", string(body))
194+
c.JSON(http.StatusBadRequest, gin.H{"error": "Jira project not found"})
191195
return
192196
default:
193-
websocket.BroadcastBugFixJiraSyncFailed(workflowID, workflow.GithubIssueNumber, fmt.Sprintf("Jira API error: %s", string(body)))
194-
c.JSON(http.StatusServiceUnavailable, gin.H{"error": fmt.Sprintf("Failed to create Jira issue (status %d)", resp.StatusCode), "details": string(body)})
197+
websocket.BroadcastBugFixJiraSyncFailed(workflowID, workflow.GithubIssueNumber, "Jira API error")
198+
// Log details for debugging, but don't expose to user
199+
log.Printf("Jira API error (status %d): %s", resp.StatusCode, string(body))
200+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": fmt.Sprintf("Failed to create Jira issue (status %d)", resp.StatusCode)})
195201
return
196202
}
197203

198204
// Parse JSON response
199205
var result map[string]interface{}
200206
if err := json.Unmarshal(body, &result); err != nil {
201-
// Log the raw response for debugging
202-
fmt.Printf("ERROR: Failed to parse Jira response as JSON: %v\n", err)
207+
// Log the raw response for debugging (server-side only)
208+
log.Printf("ERROR: Failed to parse Jira response as JSON: %v", err)
203209
bodyLen := len(body)
204-
fmt.Printf("Response body (first 500 chars): %s\n", string(body[:min(500, bodyLen)]))
210+
log.Printf("Response body (first 500 chars): %s", string(body[:min(500, bodyLen)]))
205211
websocket.BroadcastBugFixJiraSyncFailed(workflowID, workflow.GithubIssueNumber, "Invalid Jira response")
212+
// Don't expose Jira response body to user - may contain sensitive details
206213
c.JSON(http.StatusInternalServerError, gin.H{
207-
"error": "Failed to parse Jira response",
208-
"details": err.Error(),
209-
"responsePreview": string(body[:min(200, bodyLen)]),
214+
"error": "Failed to parse Jira response",
210215
})
211216
return
212217
}
@@ -422,40 +427,6 @@ func formatGitHubJiraLinkComment(jiraTaskKey, jiraTaskURL string, workflow *type
422427
return comment.String()
423428
}
424429

425-
// formatGitHubJiraUpdateComment formats the comment to post on GitHub Issue when updating Jira task
426-
func formatGitHubJiraUpdateComment(jiraTaskKey, jiraTaskURL string, workflow *types.BugFixWorkflow) string {
427-
var comment strings.Builder
428-
429-
comment.WriteString("## 🔄 Jira Task Updated\n\n")
430-
comment.WriteString(fmt.Sprintf("Jira task [**%s**](%s) has been updated with the latest information.\n\n", jiraTaskKey, jiraTaskURL))
431-
432-
// Add links to analysis documents if available
433-
if workflow.Annotations != nil {
434-
hasGists := false
435-
if bugReviewGist := workflow.Annotations["bug-review-gist-url"]; bugReviewGist != "" {
436-
if !hasGists {
437-
comment.WriteString("### 📄 Latest Analysis\n\n")
438-
hasGists = true
439-
}
440-
comment.WriteString(fmt.Sprintf("- [Bug Review & Assessment](%s)\n", bugReviewGist))
441-
}
442-
if implGist := workflow.Annotations["implementation-gist-url"]; implGist != "" {
443-
if !hasGists {
444-
comment.WriteString("### 📄 Latest Analysis\n\n")
445-
hasGists = true
446-
}
447-
comment.WriteString(fmt.Sprintf("- [Implementation Details](%s)\n", implGist))
448-
}
449-
if hasGists {
450-
comment.WriteString("\n")
451-
}
452-
}
453-
454-
comment.WriteString("*Synchronized by vTeam BugFix Workspace*")
455-
456-
return comment.String()
457-
}
458-
459430
// getSuccessMessage returns appropriate success message
460431
func getSuccessMessage(created bool, jiraTaskKey string) string {
461432
if created {

components/backend/handlers/bugfix/session_webhook.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,17 @@ func handleBugReviewCompletion(c *gin.Context, event AgenticSessionWebhookEvent,
9090
log.Printf("handleBugReviewCompletion: session=%s, workflow=%s, project=%s", sessionName, workflowID, project)
9191

9292
// Get K8s clients (try user token first, fall back to service account)
93+
// NOTE: This webhook is triggered BY the Kubernetes operator when a session
94+
// completes, NOT by a user API call. The operator does not include user
95+
// authentication headers, so we fall back to service account credentials.
96+
// This is a legitimate system operation pattern - the webhook is processing
97+
// completed session results on behalf of the system, not a specific user.
98+
// Per CLAUDE.md security rules, service account is allowed for:
99+
// "Cross-namespace operations backend is authorized for" (webhook processing)
93100
_, reqDyn := GetK8sClientsForRequest(c)
94101
if reqDyn == nil {
95102
// In webhook context, use service account clients
96-
log.Printf("No user token, using service account client")
103+
log.Printf("No user token, using service account client for system webhook operation")
97104
reqDyn = GetServiceAccountDynamicClient()
98105
}
99106

@@ -143,7 +150,7 @@ func handleBugReviewCompletion(c *gin.Context, event AgenticSessionWebhookEvent,
143150
log.Printf("Parsed issue: owner=%s, repo=%s, issue=%d", owner, repo, issueNumber)
144151

145152
// Get GitHub token from K8s secret (webhook uses service account, no user context)
146-
ctx := context.Background()
153+
ctx := c.Request.Context()
147154
githubToken, err := git.GetGitHubToken(ctx, K8sClient, DynamicClient, project, "")
148155
if err != nil || githubToken == "" {
149156
log.Printf("ERROR: Failed to get GitHub token: %v", err)
@@ -268,8 +275,16 @@ func handleBugImplementFixCompletion(c *gin.Context, event AgenticSessionWebhook
268275
}
269276

270277
// Get K8s client
278+
// NOTE: This webhook is triggered BY the Kubernetes operator when a session
279+
// completes, NOT by a user API call. The operator does not include user
280+
// authentication headers, so we fall back to service account credentials.
281+
// This is a legitimate system operation pattern - the webhook is processing
282+
// completed session results on behalf of the system, not a specific user.
283+
// Per CLAUDE.md security rules, service account is allowed for:
284+
// "Cross-namespace operations backend is authorized for" (webhook processing)
271285
_, reqDyn := GetK8sClientsForRequest(c)
272286
if reqDyn == nil {
287+
log.Printf("No user token, using service account client for system webhook operation")
273288
reqDyn = GetServiceAccountDynamicClient()
274289
}
275290

@@ -290,7 +305,7 @@ func handleBugImplementFixCompletion(c *gin.Context, event AgenticSessionWebhook
290305
}
291306

292307
// Get GitHub token from K8s secret (webhook uses service account, no user context)
293-
ctx := context.Background()
308+
ctx := c.Request.Context()
294309
githubToken, err := git.GetGitHubToken(ctx, K8sClient, DynamicClient, project, "")
295310
if err != nil || githubToken == "" {
296311
log.Printf("ERROR: Failed to get GitHub token: %v", err)
@@ -447,8 +462,8 @@ func formatImplementationSummary(gistURL, sessionID, branchName, repoURL string,
447462
comment.WriteString("**Create a Pull Request** to merge these changes:\n\n")
448463
branchURL := fmt.Sprintf("%s/tree/%s", strings.TrimSuffix(repoURL, ".git"), branchName)
449464
comment.WriteString(fmt.Sprintf("1. 🌿 View changes: [%s](%s)\n", branchName, branchURL))
450-
comment.WriteString(fmt.Sprintf("2. 🔀 Click \"Contribute\"\"Open pull request\" on GitHub\n"))
451-
comment.WriteString(fmt.Sprintf("3. 📝 Review the changes and submit the PR\n\n"))
465+
comment.WriteString("2. 🔀 Click \"Contribute\"\"Open pull request\" on GitHub\n")
466+
comment.WriteString("3. 📝 Review the changes and submit the PR\n\n")
452467
}
453468

454469
comment.WriteString("**To review locally:**\n")
@@ -488,8 +503,8 @@ func formatImplementationComment(summary, sessionID, branchName, repoURL string,
488503
comment.WriteString("**Create a Pull Request** to merge these changes:\n\n")
489504
branchURL := fmt.Sprintf("%s/tree/%s", strings.TrimSuffix(repoURL, ".git"), branchName)
490505
comment.WriteString(fmt.Sprintf("1. 🌿 View changes: [%s](%s)\n", branchName, branchURL))
491-
comment.WriteString(fmt.Sprintf("2. 🔀 Click \"Contribute\"\"Open pull request\" on GitHub\n"))
492-
comment.WriteString(fmt.Sprintf("3. 📝 Review the changes and submit the PR\n\n"))
506+
comment.WriteString("2. 🔀 Click \"Contribute\"\"Open pull request\" on GitHub\n")
507+
comment.WriteString("3. 📝 Review the changes and submit the PR\n\n")
493508
}
494509

495510
comment.WriteString("**To review locally:**\n")

components/backend/handlers/bugfix/sessions.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package bugfix
22

33
import (
4-
"context"
54
"fmt"
65
"log"
76
"net/http"
@@ -427,7 +426,7 @@ func CreateProjectBugFixWorkflowSession(c *gin.Context) {
427426
},
428427
}
429428

430-
created, err := reqDyn.Resource(gvr).Namespace(project).Create(context.TODO(), sessionObj, v1.CreateOptions{})
429+
created, err := reqDyn.Resource(gvr).Namespace(project).Create(c.Request.Context(), sessionObj, v1.CreateOptions{})
431430
if err != nil {
432431
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session", "details": err.Error()})
433432
return
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
import { Button } from '@/components/ui/button';
5+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6+
import { AlertCircle } from 'lucide-react';
7+
8+
export default function BugFixWorkflowDetailError({
9+
error,
10+
reset,
11+
}: {
12+
error: Error & { digest?: string };
13+
reset: () => void;
14+
}) {
15+
useEffect(() => {
16+
console.error('BugFix workflow detail error:', error);
17+
}, [error]);
18+
19+
return (
20+
<div className="container mx-auto p-6">
21+
<Card className="max-w-lg mx-auto mt-12">
22+
<CardHeader>
23+
<div className="flex items-center gap-2">
24+
<AlertCircle className="h-5 w-5 text-destructive" />
25+
<CardTitle>Failed to load BugFix workflow</CardTitle>
26+
</div>
27+
<CardDescription>
28+
{error.message || 'An unexpected error occurred while loading this workflow.'}
29+
</CardDescription>
30+
</CardHeader>
31+
<CardContent>
32+
<Button onClick={reset}>Try again</Button>
33+
</CardContent>
34+
</Card>
35+
</div>
36+
);
37+
}

0 commit comments

Comments
 (0)