package analyzers import ( "context" "fmt" "go/ast" "go/parser" "go/token" "regexp" "strings" "github.com/yourorg/devour/internal/quality" ) type SecurityDetector struct { *quality.BaseDetector patterns []SecurityPattern } type SecurityPattern struct { Name string Description string Pattern *regexp.Regexp Severity quality.Severity Score int } func NewSecurityDetector(finder quality.FileFinder) *SecurityDetector { d := &SecurityDetector{ BaseDetector: quality.NewBaseDetector("security", quality.SeverityT3, finder), patterns: []SecurityPattern{ { Name: "hardcoded_password", Description: "Hardcoded password or secret detected", Pattern: regexp.MustCompile(`(?i)(password|passwd|pwd|secret|api_key|apikey|token)\s*[:=]\s*["'][^"']+["']`), Severity: quality.SeverityT4, Score: 30, }, { Name: "sql_injection_risk", Description: "Potential SQL injection - string concatenation in query", Pattern: regexp.MustCompile(`fmt\.Sprintf.*SELECT|fmt\.Sprintf.*INSERT|fmt\.Sprintf.*UPDATE|fmt\.Sprintf.*DELETE`), Severity: quality.SeverityT4, Score: 25, }, { Name: "unsafe_sql_exec", Description: "Direct string interpolation in SQL execution", Pattern: regexp.MustCompile(`db\.(Exec|Query).*\+|db\.(Exec|Query).*fmt\.Sprintf`), Severity: quality.SeverityT4, Score: 25, }, { Name: "weak_random", Description: "Using math/rand for security-sensitive operations", Pattern: regexp.MustCompile(`math/rand.*token|math/rand.*password|math/rand.*secret|math/rand.*key`), Severity: quality.SeverityT3, Score: 15, }, { Name: "todo_security", Description: "TODO/FIXME related to security", Pattern: regexp.MustCompile(`(?i)(TODO|FIXME|XXX).*security|(?i)(TODO|FIXME|XXX).*auth|(?i)(TODO|FIXME|XXX).*password`), Severity: quality.SeverityT2, Score: 5, }, { Name: "os_exec_shell", Description: "Command execution with potential shell injection", Pattern: regexp.MustCompile(`exec\.Command.*sh.*-c|exec\.Command.*bash.*-c`), Severity: quality.SeverityT4, Score: 30, }, }, } return d } func (d *SecurityDetector) Name() string { return "security" } func (d *SecurityDetector) Severity() quality.Severity { return quality.SeverityT3 } func (d *SecurityDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { files, err := d.FindFiles(path, "go") if err != nil { return nil, err } var findings []quality.Finding for _, file := range files { fileFindings, err := d.analyzeFile(file) if err != nil { continue } findings = append(findings, fileFindings...) } return findings, nil } func (d *SecurityDetector) analyzeFile(filePath string) ([]quality.Finding, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) if err != nil { return nil, err } var findings []quality.Finding ast.Inspect(node, func(n ast.Node) bool { switch x := n.(type) { case *ast.CallExpr: d.checkCallExpr(x, fset, filePath, &findings) case *ast.AssignStmt: d.checkAssignStmt(x, fset, filePath, &findings) case *ast.ValueSpec: d.checkValueSpec(x, fset, filePath, &findings) } return true }) d.checkComments(node, fset, filePath, &findings) return findings, nil } func (d *SecurityDetector) checkCallExpr(expr *ast.CallExpr, fset *token.FileSet, file string, findings *[]quality.Finding) { exprStr := d.nodeToString(expr) pos := fset.Position(expr.Pos()) for _, pattern := range d.patterns { if pattern.Pattern.MatchString(exprStr) { finding := quality.Finding{ ID: fmt.Sprintf("security::%s::%d", file, pos.Line), Type: "security", Title: pattern.Name, Description: pattern.Description, File: file, Line: pos.Line, Severity: pattern.Severity, Score: pattern.Score, Status: quality.StatusOpen, Metadata: map[string]string{ "pattern": pattern.Name, "match": exprStr, "severity": fmt.Sprintf("%d", pattern.Severity), }, } *findings = append(*findings, finding) break } } } func (d *SecurityDetector) checkAssignStmt(stmt *ast.AssignStmt, fset *token.FileSet, file string, findings *[]quality.Finding) { for _, expr := range stmt.Lhs { if ident, ok := expr.(*ast.Ident); ok { if strings.Contains(strings.ToLower(ident.Name), "password") || strings.Contains(strings.ToLower(ident.Name), "secret") || strings.Contains(strings.ToLower(ident.Name), "token") { for _, val := range stmt.Rhs { if basicLit, ok := val.(*ast.BasicLit); ok && basicLit.Kind == token.STRING { pos := fset.Position(stmt.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("security::%s::%d", file, pos.Line), Type: "security", Title: "hardcoded_credential", Description: fmt.Sprintf("Hardcoded credential in variable '%s'", ident.Name), File: file, Line: pos.Line, Severity: quality.SeverityT4, Score: 30, Status: quality.StatusOpen, Metadata: map[string]string{ "variable": ident.Name, }, } *findings = append(*findings, finding) } } } } } } func (d *SecurityDetector) checkValueSpec(spec *ast.ValueSpec, fset *token.FileSet, file string, findings *[]quality.Finding) { for i, name := range spec.Names { lowerName := strings.ToLower(name.Name) if strings.Contains(lowerName, "password") || strings.Contains(lowerName, "secret") || strings.Contains(lowerName, "apikey") || strings.Contains(lowerName, "token") { if len(spec.Values) > i { if basicLit, ok := spec.Values[i].(*ast.BasicLit); ok && basicLit.Kind == token.STRING { pos := fset.Position(spec.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("security::%s::%d", file, pos.Line), Type: "security", Title: "hardcoded_credential", Description: fmt.Sprintf("Hardcoded credential in variable '%s'", name.Name), File: file, Line: pos.Line, Severity: quality.SeverityT4, Score: 30, Status: quality.StatusOpen, Metadata: map[string]string{ "variable": name.Name, }, } *findings = append(*findings, finding) } } } } } func (d *SecurityDetector) checkComments(node *ast.File, fset *token.FileSet, file string, findings *[]quality.Finding) { for _, group := range node.Comments { for _, comment := range group.List { text := comment.Text for _, pattern := range d.patterns { if pattern.Pattern.MatchString(text) { pos := fset.Position(comment.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("security::%s::%d", file, pos.Line), Type: "security", Title: pattern.Name, Description: pattern.Description, File: file, Line: pos.Line, Severity: pattern.Severity, Score: pattern.Score, Status: quality.StatusOpen, Metadata: map[string]string{ "pattern": pattern.Name, "in_comment": "true", }, } *findings = append(*findings, finding) break } } } } } func (d *SecurityDetector) nodeToString(node ast.Node) string { var b strings.Builder fmt.Fprint(&b, node) return b.String() } type ComplexityASTDetector struct { *quality.BaseDetector maxComplexity int maxNesting int } func NewComplexityASTDetector(finder quality.FileFinder) *ComplexityASTDetector { return &ComplexityASTDetector{ BaseDetector: quality.NewBaseDetector("complexity_ast", quality.SeverityT2, finder), maxComplexity: 15, maxNesting: 4, } } func (d *ComplexityASTDetector) Name() string { return "complexity_ast" } func (d *ComplexityASTDetector) Severity() quality.Severity { return quality.SeverityT2 } func (d *ComplexityASTDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) { files, err := d.FindFiles(path, "go") if err != nil { return nil, err } var findings []quality.Finding for _, file := range files { fileFindings, err := d.analyzeFile(file) if err != nil { continue } findings = append(findings, fileFindings...) } return findings, nil } func (d *ComplexityASTDetector) analyzeFile(filePath string) ([]quality.Finding, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, filePath, nil, 0) if err != nil { return nil, err } var findings []quality.Finding for _, decl := range node.Decls { if fn, ok := decl.(*ast.FuncDecl); ok { complexity := d.calculateCyclomaticComplexity(fn) nesting := d.calculateNestingDepth(fn) if complexity > d.maxComplexity { pos := fset.Position(fn.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("complexity::%s::%s", filePath, fn.Name.Name), Type: "complexity", Title: fmt.Sprintf("High cyclomatic complexity in %s", fn.Name.Name), Description: fmt.Sprintf("Function '%s' has cyclomatic complexity of %d (max: %d). Consider breaking it into smaller functions.", fn.Name.Name, complexity, d.maxComplexity), File: filePath, Line: pos.Line, Severity: quality.SeverityT2, Score: complexity - d.maxComplexity, Status: quality.StatusOpen, Metadata: map[string]string{ "function": fn.Name.Name, "complexity": fmt.Sprintf("%d", complexity), "max_complexity": fmt.Sprintf("%d", d.maxComplexity), }, } findings = append(findings, finding) } if nesting > d.maxNesting { pos := fset.Position(fn.Pos()) finding := quality.Finding{ ID: fmt.Sprintf("nesting::%s::%s", filePath, fn.Name.Name), Type: "complexity", Title: fmt.Sprintf("Deep nesting in %s", fn.Name.Name), Description: fmt.Sprintf("Function '%s' has nesting depth of %d (max: %d). Consider extracting logic into helper functions.", fn.Name.Name, nesting, d.maxNesting), File: filePath, Line: pos.Line, Severity: quality.SeverityT3, Score: (nesting - d.maxNesting) * 3, Status: quality.StatusOpen, Metadata: map[string]string{ "function": fn.Name.Name, "nesting": fmt.Sprintf("%d", nesting), "max_nesting": fmt.Sprintf("%d", d.maxNesting), }, } findings = append(findings, finding) } } } return findings, nil } func (d *ComplexityASTDetector) calculateCyclomaticComplexity(fn *ast.FuncDecl) int { complexity := 1 ast.Inspect(fn, func(n ast.Node) bool { switch n.(type) { case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt: complexity++ case *ast.CaseClause: complexity++ case *ast.BinaryExpr: complexity++ } return true }) return complexity } func (d *ComplexityASTDetector) calculateNestingDepth(fn *ast.FuncDecl) int { maxDepth := 0 var visit func(n ast.Node, depth int) visit = func(n ast.Node, depth int) { if depth > maxDepth { maxDepth = depth } switch stmt := n.(type) { case *ast.IfStmt: visit(stmt.Body, depth+1) if stmt.Else != nil { visit(stmt.Else, depth+1) } case *ast.ForStmt: visit(stmt.Body, depth+1) case *ast.RangeStmt: visit(stmt.Body, depth+1) case *ast.SwitchStmt: visit(stmt.Body, depth+1) case *ast.SelectStmt: visit(stmt.Body, depth+1) case *ast.BlockStmt: for _, s := range stmt.List { visit(s, depth) } } } visit(fn.Body, 0) return maxDepth }