package action import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) // AnalyzeCodeInput is input for AnalyzeCodeActivity type AnalyzeCodeInput struct { Path string `json:"path"` Language string `json:"language,omitempty"` Depth int `json:"depth,omitempty"` } // AnalyzeCodeOutput is output from AnalyzeCodeActivity type AnalyzeCodeOutput struct { Quality float64 `json:"quality"` Metrics map[string]interface{} `json:"metrics"` Issues []string `json:"issues"` Summary string `json:"summary"` } // AnalyzeCodeActivity analyzes code quality using available tools func AnalyzeCodeActivity(ctx context.Context, in AnalyzeCodeInput) (AnalyzeCodeOutput, error) { logger := newActivityLogger(ctx) logger.Info("AnalyzeCodeActivity started", "path", in.Path) output := AnalyzeCodeOutput{ Quality: 0.0, Metrics: make(map[string]interface{}), Issues: []string{}, } // Verify path exists if _, err := os.Stat(in.Path); os.IsNotExist(err) { return output, fmt.Errorf("path does not exist: %s", in.Path) } // Count files and lines var totalFiles, totalLines int err := filepath.Walk(in.Path, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // Skip errors } if info.IsDir() { // Skip hidden and vendor directories if strings.HasPrefix(info.Name(), ".") || info.Name() == "vendor" || info.Name() == "node_modules" { return filepath.SkipDir } return nil } ext := filepath.Ext(path) if isCodeFile(ext) { totalFiles++ if lines, err := countLines(path); err == nil { totalLines += lines } } return nil }) if err != nil { logger.Warn("Error walking path", "error", err) } output.Metrics["totalFiles"] = totalFiles output.Metrics["totalLines"] = totalLines // Try to run go vet if it's a Go project if _, err := os.Stat(filepath.Join(in.Path, "go.mod")); err == nil { cmd := exec.CommandContext(ctx, "go", "vet", "./...") cmd.Dir = in.Path vetOutput, err := cmd.CombinedOutput() if err != nil { issues := strings.Split(string(vetOutput), "\n") for _, issue := range issues { if strings.TrimSpace(issue) != "" { output.Issues = append(output.Issues, issue) } } } output.Metrics["goVetRan"] = true } // Calculate quality score (simple heuristic) issueCount := len(output.Issues) if totalFiles > 0 { issuesPerFile := float64(issueCount) / float64(totalFiles) output.Quality = max(0, 1.0 - (issuesPerFile * 0.1)) } else { output.Quality = 0.5 } output.Summary = fmt.Sprintf("Analyzed %d files (%d lines). Found %d issues. Quality score: %.2f", totalFiles, totalLines, issueCount, output.Quality) logger.Info("AnalyzeCodeActivity completed", "quality", output.Quality, "issues", issueCount) return output, nil } // SecurityScanInput is input for SecurityScanActivity type SecurityScanInput struct { Path string `json:"path"` Severity string `json:"severity,omitempty"` // low, medium, high, critical } // SecurityScanOutput is output from SecurityScanActivity type SecurityScanOutput struct { Vulnerabilities []Vulnerability `json:"vulnerabilities"` SecurityScore float64 `json:"securityScore"` RiskLevel string `json:"riskLevel"` } // Vulnerability represents a security issue type Vulnerability struct { ID string `json:"id"` Severity string `json:"severity"` Description string `json:"description"` File string `json:"file,omitempty"` Line int `json:"line,omitempty"` } // SecurityScanActivity scans code for security vulnerabilities func SecurityScanActivity(ctx context.Context, in SecurityScanInput) (SecurityScanOutput, error) { logger := newActivityLogger(ctx) logger.Info("SecurityScanActivity started", "path", in.Path) output := SecurityScanOutput{ Vulnerabilities: []Vulnerability{}, SecurityScore: 100.0, RiskLevel: "low", } // Verify path exists if _, err := os.Stat(in.Path); os.IsNotExist(err) { return output, fmt.Errorf("path does not exist: %s", in.Path) } // Check for common security issues // 1. Check for hardcoded secrets secretPatterns := []string{ "password=", "secret=", "api_key=", "apikey=", "private_key", "AWS_SECRET", } err := filepath.Walk(in.Path, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } if !isCodeFile(filepath.Ext(path)) { return nil } content, err := os.ReadFile(path) if err != nil { return nil } contentStr := strings.ToLower(string(content)) for _, pattern := range secretPatterns { if strings.Contains(contentStr, pattern) { output.Vulnerabilities = append(output.Vulnerabilities, Vulnerability{ ID: fmt.Sprintf("SEC-%d", len(output.Vulnerabilities)+1), Severity: "high", Description: fmt.Sprintf("Possible hardcoded secret: %s", pattern), File: path, }) } } return nil }) if err != nil { logger.Warn("Error scanning", "error", err) } // Try gosec if available and it's a Go project if _, err := os.Stat(filepath.Join(in.Path, "go.mod")); err == nil { if _, err := exec.LookPath("gosec"); err == nil { cmd := exec.CommandContext(ctx, "gosec", "-fmt=json", "-quiet", "./...") cmd.Dir = in.Path // gosec returns non-zero if issues found, so we ignore the error cmd.CombinedOutput() } } // Calculate score vulnCount := len(output.Vulnerabilities) if vulnCount == 0 { output.SecurityScore = 100.0 output.RiskLevel = "low" } else if vulnCount < 3 { output.SecurityScore = 80.0 output.RiskLevel = "medium" } else if vulnCount < 10 { output.SecurityScore = 50.0 output.RiskLevel = "high" } else { output.SecurityScore = 20.0 output.RiskLevel = "critical" } logger.Info("SecurityScanActivity completed", "vulnerabilities", vulnCount, "riskLevel", output.RiskLevel) return output, nil } // GenerateReportInput is input for GenerateReportActivity type GenerateReportInput struct { AnalysisResult interface{} `json:"analysisResult"` SecurityResult interface{} `json:"securityResult"` Format string `json:"format,omitempty"` // markdown, html, json } // GenerateReportOutput is output from GenerateReportActivity type GenerateReportOutput struct { Report string `json:"report"` ReportPath string `json:"reportPath"` } // GenerateReportActivity generates a combined report func GenerateReportActivity(ctx context.Context, in GenerateReportInput) (GenerateReportOutput, error) { logger := newActivityLogger(ctx) logger.Info("GenerateReportActivity started") format := in.Format if format == "" { format = "markdown" } var report strings.Builder timestamp := time.Now().Format(time.RFC3339) switch format { case "markdown": report.WriteString("# Analysis Report\n\n") report.WriteString(fmt.Sprintf("Generated: %s\n\n", timestamp)) report.WriteString("## Code Analysis\n\n") report.WriteString(fmt.Sprintf("```\n%v\n```\n\n", in.AnalysisResult)) report.WriteString("## Security Scan\n\n") report.WriteString(fmt.Sprintf("```\n%v\n```\n\n", in.SecurityResult)) case "json": report.WriteString(fmt.Sprintf(`{"timestamp":"%s","analysis":%v,"security":%v}`, timestamp, in.AnalysisResult, in.SecurityResult)) default: report.WriteString(fmt.Sprintf("Report generated at %s\n", timestamp)) report.WriteString(fmt.Sprintf("Analysis: %v\n", in.AnalysisResult)) report.WriteString(fmt.Sprintf("Security: %v\n", in.SecurityResult)) } // Save to temp file reportPath := filepath.Join(os.TempDir(), fmt.Sprintf("report-%d.%s", time.Now().UnixNano(), format)) if err := os.WriteFile(reportPath, []byte(report.String()), 0644); err != nil { logger.Warn("Failed to save report", "error", err) } logger.Info("GenerateReportActivity completed", "format", format) return GenerateReportOutput{ Report: report.String(), ReportPath: reportPath, }, nil } // Helper functions func isCodeFile(ext string) bool { codeExts := map[string]bool{ ".go": true, ".py": true, ".js": true, ".ts": true, ".java": true, ".c": true, ".cpp": true, ".h": true, ".rs": true, ".rb": true, ".php": true, ".swift": true, } return codeExts[ext] } func countLines(path string) (int, error) { content, err := os.ReadFile(path) if err != nil { return 0, err } return len(strings.Split(string(content), "\n")), nil } func max(a, b float64) float64 { if a > b { return a } return b }