297 lines
8.3 KiB
Go
297 lines
8.3 KiB
Go
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
|
||
|
|
}
|