mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
602 lines
18 KiB
Go
602 lines
18 KiB
Go
package analyzers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type BestPractice struct {
|
|
ID string
|
|
Category string // security, architecture, performance, quality
|
|
Title string
|
|
Description string
|
|
Pattern string
|
|
Language string
|
|
Framework string
|
|
Severity string
|
|
Reference string
|
|
CodeExample string
|
|
}
|
|
|
|
type PracticesFetcher struct {
|
|
cache map[string][]BestPractice
|
|
cacheMu sync.RWMutex
|
|
docsPath string
|
|
language string
|
|
frameworks []string
|
|
}
|
|
|
|
func NewPracticesFetcher() *PracticesFetcher {
|
|
return &PracticesFetcher{
|
|
cache: make(map[string][]BestPractice),
|
|
}
|
|
}
|
|
|
|
func (f *PracticesFetcher) DetectLanguage(path string) string {
|
|
markers := map[string]string{
|
|
"go.mod": "go",
|
|
"go.sum": "go",
|
|
"package.json": "javascript",
|
|
"tsconfig.json": "typescript",
|
|
"requirements.txt": "python",
|
|
"pyproject.toml": "python",
|
|
"setup.py": "python",
|
|
"Cargo.toml": "rust",
|
|
"pom.xml": "java",
|
|
"build.gradle": "java",
|
|
"composer.json": "php",
|
|
"Gemfile": "ruby",
|
|
}
|
|
|
|
for file, lang := range markers {
|
|
if _, err := os.Stat(filepath.Join(path, file)); err == nil {
|
|
f.language = lang
|
|
return lang
|
|
}
|
|
}
|
|
return "go"
|
|
}
|
|
|
|
func (f *PracticesFetcher) DetectFrameworks(path, language string) []string {
|
|
frameworks := []string{}
|
|
|
|
switch language {
|
|
case "go":
|
|
if f.hasImport(path, "github.com/gin-gonic") {
|
|
frameworks = append(frameworks, "gin")
|
|
}
|
|
if f.hasImport(path, "github.com/labstack/echo") {
|
|
frameworks = append(frameworks, "echo")
|
|
}
|
|
if f.hasImport(path, "github.com/gofiber/fiber") {
|
|
frameworks = append(frameworks, "fiber")
|
|
}
|
|
if f.hasImport(path, "gorm.io") {
|
|
frameworks = append(frameworks, "gorm")
|
|
}
|
|
if f.hasImport(path, "github.com/spf13/cobra") {
|
|
frameworks = append(frameworks, "cobra")
|
|
}
|
|
if f.hasImport(path, "k8s.io/client-go") {
|
|
frameworks = append(frameworks, "kubernetes")
|
|
}
|
|
|
|
case "typescript", "javascript":
|
|
pkgPath := filepath.Join(path, "package.json")
|
|
if data, err := os.ReadFile(pkgPath); err == nil {
|
|
content := string(data)
|
|
if strings.Contains(content, `"react"`) || strings.Contains(content, `"next"`) {
|
|
frameworks = append(frameworks, "react")
|
|
}
|
|
if strings.Contains(content, `"vue"`) {
|
|
frameworks = append(frameworks, "vue")
|
|
}
|
|
if strings.Contains(content, `"express"`) {
|
|
frameworks = append(frameworks, "express")
|
|
}
|
|
if strings.Contains(content, `"nestjs"`) || strings.Contains(content, `"@nestjs"`) {
|
|
frameworks = append(frameworks, "nestjs")
|
|
}
|
|
}
|
|
|
|
case "python":
|
|
reqPath := filepath.Join(path, "requirements.txt")
|
|
if data, err := os.ReadFile(reqPath); err == nil {
|
|
content := strings.ToLower(string(data))
|
|
if strings.Contains(content, "django") {
|
|
frameworks = append(frameworks, "django")
|
|
}
|
|
if strings.Contains(content, "flask") {
|
|
frameworks = append(frameworks, "flask")
|
|
}
|
|
if strings.Contains(content, "fastapi") {
|
|
frameworks = append(frameworks, "fastapi")
|
|
}
|
|
}
|
|
}
|
|
|
|
f.frameworks = frameworks
|
|
return frameworks
|
|
}
|
|
|
|
func (f *PracticesFetcher) hasImport(path, importPath string) bool {
|
|
err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || !strings.HasSuffix(filePath, ".go") {
|
|
return nil
|
|
}
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if strings.Contains(string(data), importPath) {
|
|
return fmt.Errorf("found")
|
|
}
|
|
return nil
|
|
})
|
|
return err != nil
|
|
}
|
|
|
|
func (f *PracticesFetcher) FetchPractices(ctx context.Context, language string, frameworks []string) ([]BestPractice, error) {
|
|
cacheKey := language + ":" + strings.Join(frameworks, ",")
|
|
|
|
f.cacheMu.RLock()
|
|
if practices, ok := f.cache[cacheKey]; ok {
|
|
f.cacheMu.RUnlock()
|
|
return practices, nil
|
|
}
|
|
f.cacheMu.RUnlock()
|
|
|
|
practices := f.getBuiltInPractices(language, frameworks)
|
|
|
|
f.cacheMu.Lock()
|
|
f.cache[cacheKey] = practices
|
|
f.cacheMu.Unlock()
|
|
|
|
return practices, nil
|
|
}
|
|
|
|
func (f *PracticesFetcher) getBuiltInPractices(language string, frameworks []string) []BestPractice {
|
|
var practices []BestPractice
|
|
|
|
practices = append(practices, f.getLanguagePractices(language)...)
|
|
|
|
for _, fw := range frameworks {
|
|
practices = append(practices, f.getFrameworkPractices(fw)...)
|
|
}
|
|
|
|
practices = append(practices, f.getSecurityPractices(language)...)
|
|
practices = append(practices, f.getArchitecturePractices()...)
|
|
practices = append(practices, f.getPerformancePractices(language)...)
|
|
|
|
return practices
|
|
}
|
|
|
|
func (f *PracticesFetcher) getLanguagePractices(lang string) []BestPractice {
|
|
var practices []BestPractice
|
|
|
|
switch lang {
|
|
case "go":
|
|
practices = append(practices, []BestPractice{
|
|
{
|
|
ID: "go:error-handling",
|
|
Category: "quality",
|
|
Title: "Always handle errors explicitly",
|
|
Description: "Never ignore errors. Each error should be handled, wrapped with context, or explicitly logged.",
|
|
Pattern: `if err != nil`,
|
|
Language: "go",
|
|
Severity: "high",
|
|
Reference: "https://go.dev/blog/error-handling-and-go",
|
|
},
|
|
{
|
|
ID: "go:defer-in-loop",
|
|
Category: "performance",
|
|
Title: "Avoid defer in loops",
|
|
Description: "defer in loops causes resources to be held until function returns. Move loop body to a separate function.",
|
|
Pattern: `for.*\{[\s\S]*defer`,
|
|
Language: "go",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "go:context-first",
|
|
Category: "architecture",
|
|
Title: "context.Context should be first parameter",
|
|
Description: "Functions that use context should accept it as the first parameter.",
|
|
Pattern: `func\s+\w+\([^)]*context\.Context`,
|
|
Language: "go",
|
|
Severity: "low",
|
|
},
|
|
{
|
|
ID: "go:interface-location",
|
|
Category: "architecture",
|
|
Title: "Define interfaces where they are used",
|
|
Description: "Interfaces should be defined by the consumer, not the implementer. This promotes loose coupling.",
|
|
Language: "go",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "go:exported-comments",
|
|
Category: "quality",
|
|
Title: "Exported symbols must have documentation comments",
|
|
Description: "All exported functions, types, and variables should have doc comments starting with their name.",
|
|
Language: "go",
|
|
Severity: "low",
|
|
Reference: "https://go.dev/doc/comment",
|
|
},
|
|
{
|
|
ID: "go:receiver-type",
|
|
Category: "architecture",
|
|
Title: "Use pointer receivers consistently",
|
|
Description: "If any method has a pointer receiver, all methods should have pointer receivers. Use value receivers for small immutable types.",
|
|
Language: "go",
|
|
Severity: "low",
|
|
},
|
|
{
|
|
ID: "go:goroutine-leak",
|
|
Category: "performance",
|
|
Title: "Goroutines must have a termination path",
|
|
Description: "Every goroutine should have a clear termination condition, typically via context cancellation or a done channel.",
|
|
Language: "go",
|
|
Severity: "high",
|
|
},
|
|
}...)
|
|
|
|
case "typescript", "javascript":
|
|
practices = append(practices, []BestPractice{
|
|
{
|
|
ID: "ts:async-await",
|
|
Category: "quality",
|
|
Title: "Prefer async/await over raw Promises",
|
|
Description: "async/await provides better readability and error handling than .then() chains.",
|
|
Language: "typescript",
|
|
Severity: "low",
|
|
},
|
|
{
|
|
ID: "ts:any-type",
|
|
Category: "quality",
|
|
Title: "Avoid the any type",
|
|
Description: "Use specific types or unknown instead of any to maintain type safety.",
|
|
Pattern: `:\s*any\b`,
|
|
Language: "typescript",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "ts:null-check",
|
|
Category: "quality",
|
|
Title: "Use strict null checks",
|
|
Description: "Enable strictNullChecks in tsconfig.json and handle null/undefined explicitly.",
|
|
Language: "typescript",
|
|
Severity: "medium",
|
|
},
|
|
}...)
|
|
|
|
case "python":
|
|
practices = append(practices, []BestPractice{
|
|
{
|
|
ID: "py:type-hints",
|
|
Category: "quality",
|
|
Title: "Use type hints for function signatures",
|
|
Description: "Add type annotations to function parameters and return values for better documentation and tooling.",
|
|
Language: "python",
|
|
Severity: "low",
|
|
},
|
|
{
|
|
ID: "py:context-manager",
|
|
Category: "quality",
|
|
Title: "Use context managers for resource handling",
|
|
Description: "Always use 'with' statements for files, connections, and other resources.",
|
|
Pattern: `with\s+\w+`,
|
|
Language: "python",
|
|
Severity: "medium",
|
|
},
|
|
}...)
|
|
}
|
|
|
|
return practices
|
|
}
|
|
|
|
func (f *PracticesFetcher) getFrameworkPractices(framework string) []BestPractice {
|
|
var practices []BestPractice
|
|
|
|
switch framework {
|
|
case "gin", "echo", "fiber", "express":
|
|
practices = append(practices, []BestPractice{
|
|
{
|
|
ID: "web:input-validation",
|
|
Category: "security",
|
|
Title: "Validate all user input",
|
|
Description: "Never trust user input. Validate and sanitize all request parameters, body, and headers.",
|
|
Severity: "critical",
|
|
Framework: framework,
|
|
},
|
|
{
|
|
ID: "web:error-exposure",
|
|
Category: "security",
|
|
Title: "Don't expose internal errors to users",
|
|
Description: "Log detailed errors internally but return generic error messages to users.",
|
|
Severity: "high",
|
|
Framework: framework,
|
|
},
|
|
{
|
|
ID: "web:rate-limiting",
|
|
Category: "security",
|
|
Title: "Implement rate limiting",
|
|
Description: "Protect endpoints with rate limiting to prevent abuse and DoS attacks.",
|
|
Severity: "high",
|
|
Framework: framework,
|
|
},
|
|
{
|
|
ID: "web:security-headers",
|
|
Category: "security",
|
|
Title: "Set security headers",
|
|
Description: "Include X-Content-Type-Options, X-Frame-Options, Content-Security-Policy headers.",
|
|
Severity: "medium",
|
|
Framework: framework,
|
|
},
|
|
}...)
|
|
|
|
case "react", "vue":
|
|
practices = append(practices, []BestPractice{
|
|
{
|
|
ID: "frontend:xss-prevention",
|
|
Category: "security",
|
|
Title: "Prevent XSS vulnerabilities",
|
|
Description: "Never use dangerouslySetInnerHTML/v-html with user content. Sanitize all user input.",
|
|
Severity: "critical",
|
|
Framework: framework,
|
|
},
|
|
{
|
|
ID: "frontend:dependency-audit",
|
|
Category: "security",
|
|
Title: "Audit dependencies regularly",
|
|
Description: "Run npm audit or yarn audit regularly and update vulnerable packages.",
|
|
Severity: "high",
|
|
Framework: framework,
|
|
},
|
|
}...)
|
|
|
|
case "django", "fastapi", "flask":
|
|
practices = append(practices, []BestPractice{
|
|
{
|
|
ID: "django:sql-injection",
|
|
Category: "security",
|
|
Title: "Use ORM to prevent SQL injection",
|
|
Description: "Never use raw string formatting in SQL queries. Always use parameterized queries or ORM methods.",
|
|
Severity: "critical",
|
|
Framework: framework,
|
|
},
|
|
{
|
|
ID: "django:csrf-protection",
|
|
Category: "security",
|
|
Title: "Enable CSRF protection",
|
|
Description: "Ensure CSRF middleware is enabled for all state-changing operations.",
|
|
Severity: "high",
|
|
Framework: framework,
|
|
},
|
|
}...)
|
|
}
|
|
|
|
return practices
|
|
}
|
|
|
|
func (f *PracticesFetcher) getSecurityPractices(lang string) []BestPractice {
|
|
return []BestPractice{
|
|
{
|
|
ID: "sec:hardcoded-secrets",
|
|
Category: "security",
|
|
Title: "No hardcoded secrets",
|
|
Description: "Never commit secrets, API keys, passwords, or tokens in source code. Use environment variables or secret management.",
|
|
Pattern: `(password|secret|api_key|apikey|token)\s*[=:]\s*['"][^'"]+['"]`,
|
|
Severity: "critical",
|
|
Reference: "https://owasp.org/www-project-web-security-testing-guide/",
|
|
},
|
|
{
|
|
ID: "sec:sql-injection",
|
|
Category: "security",
|
|
Title: "Prevent SQL injection",
|
|
Description: "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
|
|
Severity: "critical",
|
|
Reference: "https://owasp.org/www-community/attacks/SQL_Injection",
|
|
},
|
|
{
|
|
ID: "sec:xss-prevention",
|
|
Category: "security",
|
|
Title: "Prevent Cross-Site Scripting (XSS)",
|
|
Description: "Encode output, validate input, use Content-Security-Policy headers.",
|
|
Severity: "critical",
|
|
Reference: "https://owasp.org/www-community/attacks/xss/",
|
|
},
|
|
{
|
|
ID: "sec:insecure-deserialization",
|
|
Category: "security",
|
|
Title: "Avoid insecure deserialization",
|
|
Description: "Don't deserialize untrusted data. Validate and sanitize all serialized input.",
|
|
Severity: "critical",
|
|
Reference: "https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data",
|
|
},
|
|
{
|
|
ID: "sec:weak-crypto",
|
|
Category: "security",
|
|
Title: "Use strong cryptography",
|
|
Description: "Use modern algorithms (AES-256-GCM, SHA-256+, RSA-2048+). Never use MD5, SHA1 for security purposes.",
|
|
Pattern: `(md5|sha1)\s*\(`,
|
|
Severity: "high",
|
|
},
|
|
{
|
|
ID: "sec:logging-sensitive",
|
|
Category: "security",
|
|
Title: "Don't log sensitive data",
|
|
Description: "Never log passwords, tokens, credit cards, or PII. Mask or redact sensitive fields.",
|
|
Severity: "high",
|
|
},
|
|
{
|
|
ID: "sec:auth-checks",
|
|
Category: "security",
|
|
Title: "Implement proper authentication checks",
|
|
Description: "Verify authentication on every protected endpoint. Don't rely on client-side checks.",
|
|
Severity: "critical",
|
|
},
|
|
{
|
|
ID: "sec:input-validation",
|
|
Category: "security",
|
|
Title: "Validate all input on the server",
|
|
Description: "Client-side validation is for UX. Server-side validation is for security.",
|
|
Severity: "critical",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (f *PracticesFetcher) getArchitecturePractices() []BestPractice {
|
|
return []BestPractice{
|
|
{
|
|
ID: "arch:single-responsibility",
|
|
Category: "architecture",
|
|
Title: "Single Responsibility Principle",
|
|
Description: "Each module/class should have one reason to change. Split large modules into focused ones.",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "arch:dependency-injection",
|
|
Category: "architecture",
|
|
Title: "Use dependency injection",
|
|
Description: "Inject dependencies rather than creating them internally. This improves testability and flexibility.",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "arch:layer-separation",
|
|
Category: "architecture",
|
|
Title: "Separate concerns by layer",
|
|
Description: "Keep presentation, business logic, and data access layers separate.",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "arch:interface-segregation",
|
|
Category: "architecture",
|
|
Title: "Prefer small, focused interfaces",
|
|
Description: "Clients shouldn't depend on methods they don't use. Split large interfaces.",
|
|
Severity: "low",
|
|
},
|
|
{
|
|
ID: "arch:avoid-god-classes",
|
|
Category: "architecture",
|
|
Title: "Avoid god classes/modules",
|
|
Description: "Classes with too many responsibilities should be split. Watch for high method/field counts.",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "arch:circular-dependencies",
|
|
Category: "architecture",
|
|
Title: "Eliminate circular dependencies",
|
|
Description: "Circular dependencies indicate tight coupling. Refactor to use dependency inversion.",
|
|
Severity: "high",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (f *PracticesFetcher) getPerformancePractices(lang string) []BestPractice {
|
|
practices := []BestPractice{
|
|
{
|
|
ID: "perf:n-plus-one",
|
|
Category: "performance",
|
|
Title: "Avoid N+1 query patterns",
|
|
Description: "When iterating over results, avoid making separate queries for each item. Use JOINs or batch loading.",
|
|
Severity: "high",
|
|
},
|
|
{
|
|
ID: "perf:unbounded-results",
|
|
Category: "performance",
|
|
Title: "Limit query results",
|
|
Description: "Always paginate or limit query results to prevent memory exhaustion.",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "perf:connection-pooling",
|
|
Category: "performance",
|
|
Title: "Use connection pooling",
|
|
Description: "Don't create new connections per request. Use connection pools for databases and HTTP clients.",
|
|
Severity: "high",
|
|
},
|
|
{
|
|
ID: "perf:caching",
|
|
Category: "performance",
|
|
Title: "Cache expensive operations",
|
|
Description: "Cache frequently accessed, rarely changing data. Consider memoization for expensive computations.",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "perf:blocking-in-hot-path",
|
|
Category: "performance",
|
|
Title: "Avoid blocking operations in hot paths",
|
|
Description: "Move I/O, network calls, and heavy computations out of request handlers when possible.",
|
|
Severity: "medium",
|
|
},
|
|
}
|
|
|
|
if lang == "go" {
|
|
practices = append(practices, []BestPractice{
|
|
{
|
|
ID: "go:perf:string-concat",
|
|
Category: "performance",
|
|
Title: "Use strings.Builder for string concatenation",
|
|
Description: "In loops, use strings.Builder instead of += for efficient string concatenation.",
|
|
Pattern: `for[\s\S]*\+=.*["` + "`" + `]`,
|
|
Language: "go",
|
|
Severity: "medium",
|
|
},
|
|
{
|
|
ID: "go:perf:slice-prealloc",
|
|
Category: "performance",
|
|
Title: "Pre-allocate slices when size is known",
|
|
Description: "Use make([]T, 0, capacity) when you know the final size to avoid reallocations.",
|
|
Language: "go",
|
|
Severity: "low",
|
|
},
|
|
{
|
|
ID: "go:perf:json-marshal",
|
|
Category: "performance",
|
|
Title: "Consider streaming JSON for large payloads",
|
|
Description: "For large JSON, use json.Encoder/Decoder instead of Marshal/Unmarshal to reduce allocations.",
|
|
Language: "go",
|
|
Severity: "low",
|
|
},
|
|
}...)
|
|
}
|
|
|
|
return practices
|
|
}
|
|
|
|
func (f *PracticesFetcher) GetPracticesByCategory(category string) []BestPractice {
|
|
f.cacheMu.RLock()
|
|
defer f.cacheMu.RUnlock()
|
|
|
|
var result []BestPractice
|
|
for _, practices := range f.cache {
|
|
for _, p := range practices {
|
|
if p.Category == category {
|
|
result = append(result, p)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (f *PracticesFetcher) GetAllPractices() []BestPractice {
|
|
f.cacheMu.RLock()
|
|
defer f.cacheMu.RUnlock()
|
|
|
|
var result []BestPractice
|
|
seen := make(map[string]bool)
|
|
for _, practices := range f.cache {
|
|
for _, p := range practices {
|
|
if !seen[p.ID] {
|
|
result = append(result, p)
|
|
seen[p.ID] = true
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|