mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
package analyzers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"strings"
|
||||
|
||||
"github.com/yourorg/devour/internal/quality"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
type ControlFlowAnalyzer struct {
|
||||
maxComplexity int
|
||||
maxNesting int
|
||||
maxFunctionLength int
|
||||
}
|
||||
|
||||
func NewControlFlowAnalyzer() *ControlFlowAnalyzer {
|
||||
return &ControlFlowAnalyzer{
|
||||
maxComplexity: 15,
|
||||
maxNesting: 4,
|
||||
maxFunctionLength: 50,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) Name() string {
|
||||
return "controlflow"
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) Severity() quality.Severity {
|
||||
return quality.SeverityT3
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedName | packages.NeedFiles | packages.NeedSyntax,
|
||||
Dir: path,
|
||||
}
|
||||
|
||||
pkgs, err := packages.Load(cfg, "./...")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load packages: %w", err)
|
||||
}
|
||||
|
||||
var findings []quality.Finding
|
||||
|
||||
for _, pkg := range pkgs {
|
||||
for _, file := range pkg.Syntax {
|
||||
pos := pkg.Fset.Position(file.Pos())
|
||||
findings = append(findings, a.analyzeFile(pkg.Fset, file, pos.Filename)...)
|
||||
}
|
||||
}
|
||||
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) analyzeFile(fset *token.FileSet, file *ast.File, filename string) []quality.Finding {
|
||||
var findings []quality.Finding
|
||||
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
switch node := n.(type) {
|
||||
case *ast.FuncDecl:
|
||||
findings = append(findings, a.analyzeFunction(fset, node, filename)...)
|
||||
case *ast.IfStmt:
|
||||
findings = append(findings, a.checkUnreachableCode(fset, node, filename)...)
|
||||
case *ast.SwitchStmt:
|
||||
findings = append(findings, a.analyzeSwitch(fset, node, filename)...)
|
||||
case *ast.ForStmt:
|
||||
findings = append(findings, a.analyzeLoop(fset, node, filename)...)
|
||||
case *ast.RangeStmt:
|
||||
findings = append(findings, a.analyzeRange(fset, node, filename)...)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) analyzeFunction(fset *token.FileSet, fn *ast.FuncDecl, filename string) []quality.Finding {
|
||||
var findings []quality.Finding
|
||||
|
||||
complexity := a.calculateCyclomaticComplexity(fn.Body)
|
||||
startPos := fset.Position(fn.Pos())
|
||||
endPos := fset.Position(fn.End())
|
||||
loc := endPos.Line - startPos.Line + 1
|
||||
|
||||
if complexity > a.maxComplexity {
|
||||
severity := quality.SeverityT3
|
||||
score := complexity - a.maxComplexity
|
||||
if complexity > a.maxComplexity*2 {
|
||||
severity = quality.SeverityT4
|
||||
score = (complexity - a.maxComplexity) * 2
|
||||
}
|
||||
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("cyclomatic-complexity::%s::%d", filename, startPos.Line),
|
||||
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, a.maxComplexity),
|
||||
File: filename,
|
||||
Line: startPos.Line,
|
||||
EndLine: endPos.Line,
|
||||
Severity: severity,
|
||||
Score: score,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"function": fn.Name.Name,
|
||||
"complexity": fmt.Sprintf("%d", complexity),
|
||||
"max": fmt.Sprintf("%d", a.maxComplexity),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if loc > a.maxFunctionLength {
|
||||
severity := quality.SeverityT2
|
||||
if loc > a.maxFunctionLength*2 {
|
||||
severity = quality.SeverityT3
|
||||
}
|
||||
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("function-length::%s::%d", filename, startPos.Line),
|
||||
Type: "complexity",
|
||||
Title: fmt.Sprintf("Function too long: %s", fn.Name.Name),
|
||||
Description: fmt.Sprintf("Function '%s' is %d lines (max: %d). Consider breaking it into smaller functions.", fn.Name.Name, loc, a.maxFunctionLength),
|
||||
File: filename,
|
||||
Line: startPos.Line,
|
||||
Severity: severity,
|
||||
Score: (loc - a.maxFunctionLength) / 10,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"function": fn.Name.Name,
|
||||
"loc": fmt.Sprintf("%d", loc),
|
||||
"max": fmt.Sprintf("%d", a.maxFunctionLength),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
maxNesting := a.calculateMaxNesting(fn.Body)
|
||||
if maxNesting > a.maxNesting {
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("deep-nesting::%s::%d", filename, startPos.Line),
|
||||
Type: "complexity",
|
||||
Title: fmt.Sprintf("Deep nesting in %s", fn.Name.Name),
|
||||
Description: fmt.Sprintf("Function '%s' has nesting depth of %d (max: %d). Extract nested code into separate functions.", fn.Name.Name, maxNesting, a.maxNesting),
|
||||
File: filename,
|
||||
Line: startPos.Line,
|
||||
Severity: quality.SeverityT3,
|
||||
Score: maxNesting - a.maxNesting,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"function": fn.Name.Name,
|
||||
"nesting": fmt.Sprintf("%d", maxNesting),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
findings = append(findings, a.checkEarlyReturn(fset, fn, filename)...)
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) calculateCyclomaticComplexity(node ast.Node) int {
|
||||
complexity := 1
|
||||
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
switch n.(type) {
|
||||
case *ast.IfStmt:
|
||||
complexity++
|
||||
case *ast.ForStmt:
|
||||
complexity++
|
||||
case *ast.RangeStmt:
|
||||
complexity++
|
||||
case *ast.CaseClause:
|
||||
complexity++
|
||||
case *ast.BinaryExpr:
|
||||
if e, ok := n.(*ast.BinaryExpr); ok {
|
||||
if e.Op == token.LAND || e.Op == token.LOR {
|
||||
complexity++
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return complexity
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) calculateMaxNesting(node ast.Node) int {
|
||||
return a.nestingDepth(node, 0)
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) nestingDepth(node ast.Node, current int) int {
|
||||
maxDepth := current
|
||||
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
var childNode ast.Node
|
||||
|
||||
switch stmt := n.(type) {
|
||||
case *ast.IfStmt:
|
||||
childNode = stmt.Body
|
||||
case *ast.ForStmt:
|
||||
childNode = stmt.Body
|
||||
case *ast.RangeStmt:
|
||||
childNode = stmt.Body
|
||||
case *ast.SelectStmt:
|
||||
childNode = stmt.Body
|
||||
case *ast.SwitchStmt:
|
||||
childNode = stmt.Body
|
||||
case *ast.TypeSwitchStmt:
|
||||
childNode = stmt.Body
|
||||
case *ast.BlockStmt:
|
||||
childNode = nil
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
if childNode != nil {
|
||||
depth := a.nestingDepth(childNode, current+1)
|
||||
if depth > maxDepth {
|
||||
maxDepth = depth
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return maxDepth
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) checkEarlyReturn(fset *token.FileSet, fn *ast.FuncDecl, filename string) []quality.Finding {
|
||||
var findings []quality.Finding
|
||||
|
||||
if fn.Body == nil || len(fn.Body.List) < 2 {
|
||||
return findings
|
||||
}
|
||||
|
||||
ifStmt, ok := fn.Body.List[0].(*ast.IfStmt)
|
||||
if !ok || ifStmt.Else == nil {
|
||||
return findings
|
||||
}
|
||||
|
||||
if _, ok := ifStmt.Else.(*ast.BlockStmt); ok && len(fn.Body.List) > 1 {
|
||||
startPos := fset.Position(ifStmt.Pos())
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("early-return::%s::%d", filename, startPos.Line),
|
||||
Type: "quality",
|
||||
Title: fmt.Sprintf("Use early return pattern in %s", fn.Name.Name),
|
||||
Description: "Consider using early return instead of if-else to reduce nesting and improve readability.",
|
||||
File: filename,
|
||||
Line: startPos.Line,
|
||||
Severity: quality.SeverityT1,
|
||||
Score: 1,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"function": fn.Name.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) checkUnreachableCode(fset *token.FileSet, stmt *ast.IfStmt, filename string) []quality.Finding {
|
||||
var findings []quality.Finding
|
||||
|
||||
a.checkUnreachableInBranch(fset, stmt.Body, filename, &findings)
|
||||
if stmt.Else != nil {
|
||||
if elseBlock, ok := stmt.Else.(*ast.BlockStmt); ok {
|
||||
a.checkUnreachableInBranch(fset, elseBlock, filename, &findings)
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) checkUnreachableInBranch(fset *token.FileSet, block *ast.BlockStmt, filename string, findings *[]quality.Finding) {
|
||||
hasReturn := false
|
||||
for _, stmt := range block.List {
|
||||
if hasReturn {
|
||||
pos := fset.Position(stmt.Pos())
|
||||
*findings = append(*findings, quality.Finding{
|
||||
ID: fmt.Sprintf("unreachable::%s::%d", filename, pos.Line),
|
||||
Type: "dead_code",
|
||||
Title: "Unreachable code after return",
|
||||
Description: "Code after return statement will never be executed.",
|
||||
File: filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT2,
|
||||
Score: 3,
|
||||
Status: quality.StatusOpen,
|
||||
})
|
||||
break
|
||||
}
|
||||
if _, ok := stmt.(*ast.ReturnStmt); ok {
|
||||
hasReturn = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) analyzeSwitch(fset *token.FileSet, stmt *ast.SwitchStmt, filename string) []quality.Finding {
|
||||
var findings []quality.Finding
|
||||
|
||||
pos := fset.Position(stmt.Pos())
|
||||
|
||||
hasDefault := false
|
||||
caseCount := 0
|
||||
|
||||
for _, s := range stmt.Body.List {
|
||||
if clause, ok := s.(*ast.CaseClause); ok {
|
||||
caseCount++
|
||||
if clause.List == nil {
|
||||
hasDefault = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasDefault && caseCount > 0 {
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("switch-no-default::%s::%d", filename, pos.Line),
|
||||
Type: "quality",
|
||||
Title: "Switch without default case",
|
||||
Description: "Switch statement lacks a default case. Consider handling unexpected values explicitly.",
|
||||
File: filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT1,
|
||||
Score: 1,
|
||||
Status: quality.StatusOpen,
|
||||
})
|
||||
}
|
||||
|
||||
if caseCount > 10 {
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("switch-too-many-cases::%s::%d", filename, pos.Line),
|
||||
Type: "complexity",
|
||||
Title: "Switch with too many cases",
|
||||
Description: fmt.Sprintf("Switch has %d cases. Consider using a map or polymorphism instead.", caseCount),
|
||||
File: filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT2,
|
||||
Score: caseCount / 5,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"case_count": fmt.Sprintf("%d", caseCount),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) analyzeLoop(fset *token.FileSet, stmt *ast.ForStmt, filename string) []quality.Finding {
|
||||
var findings []quality.Finding
|
||||
|
||||
pos := fset.Position(stmt.Pos())
|
||||
|
||||
if stmt.Cond == nil && stmt.Post == nil {
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("infinite-loop::%s::%d", filename, pos.Line),
|
||||
Type: "quality",
|
||||
Title: "Potential infinite loop",
|
||||
Description: "For loop has no condition and no post statement. Ensure there's a break inside.",
|
||||
File: filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT3,
|
||||
Score: 4,
|
||||
Status: quality.StatusOpen,
|
||||
})
|
||||
}
|
||||
|
||||
if strings.Contains(fmt.Sprintf("%v", stmt.Cond), "== true") {
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("redundant-bool-compare::%s::%d", filename, pos.Line),
|
||||
Type: "quality",
|
||||
Title: "Redundant boolean comparison",
|
||||
Description: "Comparing to 'true' is redundant. Use the boolean value directly.",
|
||||
File: filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT1,
|
||||
Score: 1,
|
||||
Status: quality.StatusOpen,
|
||||
})
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
func (a *ControlFlowAnalyzer) analyzeRange(fset *token.FileSet, stmt *ast.RangeStmt, filename string) []quality.Finding {
|
||||
var findings []quality.Finding
|
||||
|
||||
pos := fset.Position(stmt.Pos())
|
||||
|
||||
if stmt.Key != nil {
|
||||
if ident, ok := stmt.Key.(*ast.Ident); ok && ident.Name == "_" {
|
||||
} else if stmt.Body != nil {
|
||||
used := false
|
||||
keyName := ""
|
||||
if ident, ok := stmt.Key.(*ast.Ident); ok {
|
||||
keyName = ident.Name
|
||||
}
|
||||
ast.Inspect(stmt.Body, func(n ast.Node) bool {
|
||||
if ident, ok := n.(*ast.Ident); ok && ident.Name == keyName {
|
||||
used = true
|
||||
}
|
||||
return true
|
||||
})
|
||||
if !used && keyName != "" {
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("unused-range-key::%s::%d", filename, pos.Line),
|
||||
Type: "quality",
|
||||
Title: "Unused range key",
|
||||
Description: fmt.Sprintf("Range key '%s' is not used. Use '_' to ignore it explicitly.", keyName),
|
||||
File: filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT1,
|
||||
Score: 1,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"variable": keyName,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package analyzers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/yourorg/devour/internal/quality"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
type DataFlowAnalyzer struct {
|
||||
fset *token.FileSet
|
||||
pkgs []*packages.Package
|
||||
findings []quality.Finding
|
||||
taintSrcs map[string][]TaintSource
|
||||
sanitizeFn map[string]bool
|
||||
}
|
||||
|
||||
type TaintSource struct {
|
||||
Name string
|
||||
Package string
|
||||
Category string
|
||||
Description string
|
||||
}
|
||||
|
||||
type DataFlowFinding struct {
|
||||
Source string
|
||||
Sink string
|
||||
Path []string
|
||||
Line int
|
||||
File string
|
||||
Severity quality.Severity
|
||||
Description string
|
||||
}
|
||||
|
||||
func NewDataFlowAnalyzer() *DataFlowAnalyzer {
|
||||
d := &DataFlowAnalyzer{
|
||||
findings: make([]quality.Finding, 0),
|
||||
taintSrcs: make(map[string][]TaintSource),
|
||||
}
|
||||
d.initTaintSources()
|
||||
d.initSanitizers()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) initTaintSources() {
|
||||
d.taintSrcs["net/http"] = []TaintSource{
|
||||
{Name: "FormValue", Package: "net/http", Category: "http-input", Description: "HTTP form value - user controlled"},
|
||||
{Name: "PostFormValue", Package: "net/http", Category: "http-input", Description: "HTTP POST form value - user controlled"},
|
||||
{Name: "FormFile", Package: "net/http", Category: "http-input", Description: "HTTP uploaded file - user controlled"},
|
||||
{Name: "Cookie", Package: "net/http", Category: "http-input", Description: "HTTP cookie - user controlled"},
|
||||
{Name: "Header", Package: "net/http", Category: "http-input", Description: "HTTP header - user controlled"},
|
||||
{Name: "URL", Package: "net/http", Category: "http-input", Description: "Request URL - user controlled"},
|
||||
{Name: "Body", Package: "net/http", Category: "http-input", Description: "Request body - user controlled"},
|
||||
}
|
||||
d.taintSrcs["os"] = []TaintSource{
|
||||
{Name: "Getenv", Package: "os", Category: "env", Description: "Environment variable - environment controlled"},
|
||||
{Name: "Args", Package: "os", Category: "cli", Description: "Command line arguments - user controlled"},
|
||||
{Name: "Stdin", Package: "os", Category: "io", Description: "Standard input - user controlled"},
|
||||
}
|
||||
d.taintSrcs["bufio"] = []TaintSource{
|
||||
{Name: "ReadString", Package: "bufio", Category: "io", Description: "Reader input - potentially user controlled"},
|
||||
{Name: "ReadBytes", Package: "bufio", Category: "io", Description: "Reader input - potentially user controlled"},
|
||||
{Name: "ReadLine", Package: "bufio", Category: "io", Description: "Reader input - potentially user controlled"},
|
||||
}
|
||||
d.taintSrcs["io"] = []TaintSource{
|
||||
{Name: "ReadAll", Package: "io", Category: "io", Description: "Read all from reader - potentially user controlled"},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) initSanitizers() {
|
||||
d.sanitizeFn = map[string]bool{
|
||||
"html.EscapeString": true,
|
||||
"template.HTMLEscape": true,
|
||||
"template.JSEscape": true,
|
||||
"url.QueryEscape": true,
|
||||
"url.PathEscape": true,
|
||||
"sql.Named": true,
|
||||
"regexp.QuoteMeta": true,
|
||||
"strconv.Quote": true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) Name() string {
|
||||
return "dataflow"
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) Severity() quality.Severity {
|
||||
return quality.SeverityT3
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedSyntax,
|
||||
Dir: path,
|
||||
}
|
||||
|
||||
pkgs, err := packages.Load(cfg, "./...")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load packages: %w", err)
|
||||
}
|
||||
|
||||
d.pkgs = pkgs
|
||||
d.fset = pkgs[0].Fset
|
||||
|
||||
for _, pkg := range pkgs {
|
||||
d.analyzePackage(pkg)
|
||||
}
|
||||
|
||||
return d.findings, nil
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) analyzePackage(pkg *packages.Package) {
|
||||
for _, file := range pkg.Syntax {
|
||||
d.analyzeFile(pkg, file)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) analyzeFile(pkg *packages.Package, file *ast.File) {
|
||||
tainted := make(map[string]TaintSource)
|
||||
propagations := make(map[string][]string)
|
||||
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
switch node := n.(type) {
|
||||
case *ast.AssignStmt:
|
||||
d.analyzeAssignment(pkg, node, tainted, propagations)
|
||||
case *ast.CallExpr:
|
||||
d.analyzeCall(pkg, node, tainted, file)
|
||||
case *ast.ValueSpec:
|
||||
d.analyzeValueSpec(pkg, node, tainted)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) analyzeAssignment(pkg *packages.Package, node *ast.AssignStmt, tainted map[string]TaintSource, propagations map[string][]string) {
|
||||
for i, expr := range node.Lhs {
|
||||
if ident, ok := expr.(*ast.Ident); ok {
|
||||
if i < len(node.Rhs) {
|
||||
if source := d.getTaintSource(pkg, node.Rhs[i]); source != nil {
|
||||
tainted[ident.Name] = *source
|
||||
}
|
||||
if rhsIdent, ok := node.Rhs[i].(*ast.Ident); ok {
|
||||
if t, exists := tainted[rhsIdent.Name]; exists {
|
||||
tainted[ident.Name] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) analyzeCall(pkg *packages.Package, node *ast.CallExpr, tainted map[string]TaintSource, file *ast.File) {
|
||||
fnName := d.getCallName(node)
|
||||
|
||||
if d.isDangerousSink(fnName) {
|
||||
for _, arg := range node.Args {
|
||||
if ident, ok := arg.(*ast.Ident); ok {
|
||||
if source, exists := tainted[ident.Name]; exists {
|
||||
pos := d.fset.Position(node.Pos())
|
||||
d.findings = append(d.findings, quality.Finding{
|
||||
ID: fmt.Sprintf("taint-flow::%s::%d", pos.Filename, pos.Line),
|
||||
Type: "security",
|
||||
Title: fmt.Sprintf("Tainted data flows to dangerous sink: %s", fnName),
|
||||
Description: fmt.Sprintf("User-controlled input from %s flows to %s without sanitization. This may lead to injection vulnerabilities.", source.Description, fnName),
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT4,
|
||||
Score: 8,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"source": source.Name,
|
||||
"source_type": source.Category,
|
||||
"sink": fnName,
|
||||
"variable": ident.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range node.Args {
|
||||
d.checkSQLInjection(pkg, arg, tainted, node)
|
||||
d.checkCommandInjection(pkg, arg, tainted, node)
|
||||
d.checkPathTraversal(pkg, arg, tainted, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) getTaintSource(pkg *packages.Package, expr ast.Expr) *TaintSource {
|
||||
call, ok := expr.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkgIdent, ok := sel.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkgName := pkgIdent.Name
|
||||
fnName := sel.Sel.Name
|
||||
|
||||
if sources, exists := d.taintSrcs[pkgName]; exists {
|
||||
for _, src := range sources {
|
||||
if src.Name == fnName {
|
||||
return &src
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if obj := pkg.TypesInfo.Uses[pkgIdent]; obj != nil {
|
||||
if pkgObj, ok := obj.(*types.PkgName); ok {
|
||||
if sources, exists := d.taintSrcs[pkgObj.Imported().Path()]; exists {
|
||||
for _, src := range sources {
|
||||
if src.Name == fnName {
|
||||
return &src
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) getCallName(node *ast.CallExpr) string {
|
||||
switch fn := node.Fun.(type) {
|
||||
case *ast.SelectorExpr:
|
||||
if ident, ok := fn.X.(*ast.Ident); ok {
|
||||
return ident.Name + "." + fn.Sel.Name
|
||||
}
|
||||
return fn.Sel.Name
|
||||
case *ast.Ident:
|
||||
return fn.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) isDangerousSink(fnName string) bool {
|
||||
dangerousSinks := map[string]bool{
|
||||
"exec.Command": true,
|
||||
"exec.CommandContext": true,
|
||||
"os/exec.Command": true,
|
||||
"db.Exec": true,
|
||||
"db.Query": true,
|
||||
"db.QueryRow": true,
|
||||
"sql.DB.Exec": true,
|
||||
"sql.DB.Query": true,
|
||||
"os.WriteFile": true,
|
||||
"os.Create": true,
|
||||
"os.OpenFile": true,
|
||||
"ioutil.WriteFile": true,
|
||||
"template.Parse": true,
|
||||
"html.template.Parse": true,
|
||||
"fmt.Fprintf": true,
|
||||
"fmt.Printf": true,
|
||||
"fmt.Sprintf": true,
|
||||
}
|
||||
return dangerousSinks[fnName]
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) checkSQLInjection(pkg *packages.Package, arg ast.Expr, tainted map[string]TaintSource, node *ast.CallExpr) {
|
||||
fnName := d.getCallName(node)
|
||||
if !strings.Contains(fnName, "Exec") && !strings.Contains(fnName, "Query") {
|
||||
return
|
||||
}
|
||||
|
||||
if basic, ok := arg.(*ast.BasicLit); ok {
|
||||
query := strings.Trim(basic.Value, "`\"")
|
||||
if strings.Contains(query, "%s") || strings.Contains(query, "%v") || strings.Contains(query, "+") {
|
||||
pos := d.fset.Position(node.Pos())
|
||||
d.findings = append(d.findings, quality.Finding{
|
||||
ID: fmt.Sprintf("sql-injection::%s::%d", pos.Filename, pos.Line),
|
||||
Type: "security",
|
||||
Title: "Potential SQL injection vulnerability",
|
||||
Description: "SQL query constructed with string formatting. Use parameterized queries instead.",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT4,
|
||||
Score: 10,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"vulnerability": "sql-injection",
|
||||
"pattern": "string-formatting-in-query",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) checkCommandInjection(pkg *packages.Package, arg ast.Expr, tainted map[string]TaintSource, node *ast.CallExpr) {
|
||||
fnName := d.getCallName(node)
|
||||
if !strings.Contains(fnName, "exec.Command") {
|
||||
return
|
||||
}
|
||||
|
||||
if ident, ok := arg.(*ast.Ident); ok {
|
||||
if _, exists := tainted[ident.Name]; exists {
|
||||
pos := d.fset.Position(node.Pos())
|
||||
d.findings = append(d.findings, quality.Finding{
|
||||
ID: fmt.Sprintf("command-injection::%s::%d", pos.Filename, pos.Line),
|
||||
Type: "security",
|
||||
Title: "Potential command injection vulnerability",
|
||||
Description: "User-controlled input flows to exec.Command. Sanitize or validate input before use.",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT4,
|
||||
Score: 10,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"vulnerability": "command-injection",
|
||||
"variable": ident.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) checkPathTraversal(pkg *packages.Package, arg ast.Expr, tainted map[string]TaintSource, node *ast.CallExpr) {
|
||||
fnName := d.getCallName(node)
|
||||
pathFunctions := map[string]bool{
|
||||
"os.Open": true,
|
||||
"os.OpenFile": true,
|
||||
"os.Create": true,
|
||||
"os.WriteFile": true,
|
||||
"os.ReadFile": true,
|
||||
"ioutil.ReadFile": true,
|
||||
"ioutil.WriteFile": true,
|
||||
"filepath.Join": true,
|
||||
"filepath.Walk": true,
|
||||
}
|
||||
|
||||
if !pathFunctions[fnName] {
|
||||
return
|
||||
}
|
||||
|
||||
if ident, ok := arg.(*ast.Ident); ok {
|
||||
if _, exists := tainted[ident.Name]; exists {
|
||||
pos := d.fset.Position(node.Pos())
|
||||
d.findings = append(d.findings, quality.Finding{
|
||||
ID: fmt.Sprintf("path-traversal::%s::%d", pos.Filename, pos.Line),
|
||||
Type: "security",
|
||||
Title: "Potential path traversal vulnerability",
|
||||
Description: "User-controlled input used in file path operation. Validate and sanitize paths.",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Severity: quality.SeverityT4,
|
||||
Score: 8,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"vulnerability": "path-traversal",
|
||||
"variable": ident.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataFlowAnalyzer) analyzeValueSpec(pkg *packages.Package, node *ast.ValueSpec, tainted map[string]TaintSource) {
|
||||
for i, name := range node.Names {
|
||||
if i < len(node.Values) {
|
||||
if source := d.getTaintSource(pkg, node.Values[i]); source != nil {
|
||||
tainted[name.Name] = *source
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SecretsDetector struct {
|
||||
patterns []SecretPattern
|
||||
}
|
||||
|
||||
type SecretPattern struct {
|
||||
Name string
|
||||
Pattern *regexp.Regexp
|
||||
Severity quality.Severity
|
||||
}
|
||||
|
||||
func NewSecretsDetector() *SecretsDetector {
|
||||
d := &SecretsDetector{
|
||||
patterns: []SecretPattern{
|
||||
{Name: "AWS Access Key", Pattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`), Severity: quality.SeverityT4},
|
||||
{Name: "AWS Secret Key", Pattern: regexp.MustCompile(`(?i)aws(.{0,20})?['\"][0-9a-zA-Z/+=]{40}['\"]`), Severity: quality.SeverityT4},
|
||||
{Name: "GitHub Token", Pattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`), Severity: quality.SeverityT4},
|
||||
{Name: "GitHub OAuth", Pattern: regexp.MustCompile(`gho_[0-9a-zA-Z]{36}`), Severity: quality.SeverityT4},
|
||||
{Name: "GitHub App Token", Pattern: regexp.MustCompile(`(ghu|ghs)_[0-9a-zA-Z]{36}`), Severity: quality.SeverityT4},
|
||||
{Name: "Slack Token", Pattern: regexp.MustCompile(`xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9]{24}`), Severity: quality.SeverityT4},
|
||||
{Name: "RSA Private Key", Pattern: regexp.MustCompile(`-----BEGIN RSA PRIVATE KEY-----`), Severity: quality.SeverityT4},
|
||||
{Name: "Private Key", Pattern: regexp.MustCompile(`-----BEGIN PRIVATE KEY-----`), Severity: quality.SeverityT4},
|
||||
{Name: "JWT", Pattern: regexp.MustCompile(`eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*`), Severity: quality.SeverityT3},
|
||||
{Name: "Generic API Key", Pattern: regexp.MustCompile(`(?i)(api_key|apikey|secret|password|token)\s*[=:]\s*['"][^'"]{8,}['"]`), Severity: quality.SeverityT3},
|
||||
{Name: "DB Connection String", Pattern: regexp.MustCompile(`(?i)(mysql|postgres|mongodb)://[^:]+:[^@]+@[^/]+`), Severity: quality.SeverityT4},
|
||||
},
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SecretsDetector) Name() string {
|
||||
return "secrets"
|
||||
}
|
||||
|
||||
func (d *SecretsDetector) Severity() quality.Severity {
|
||||
return quality.SeverityT4
|
||||
}
|
||||
|
||||
func (d *SecretsDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
|
||||
var findings []quality.Finding
|
||||
|
||||
err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filePath)
|
||||
if ext != ".go" && ext != ".ts" && ext != ".js" && ext != ".py" && ext != ".java" && ext != ".yaml" && ext != ".yml" && ext != ".json" && ext != ".env" && ext != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(filePath, "_test.go") || strings.Contains(filePath, "vendor/") || strings.Contains(filePath, "node_modules/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
for _, pattern := range d.patterns {
|
||||
matches := pattern.Pattern.FindAllStringIndex(content, -1)
|
||||
for _, match := range matches {
|
||||
line := strings.Count(content[:match[0]], "\n") + 1
|
||||
findings = append(findings, quality.Finding{
|
||||
ID: fmt.Sprintf("secret::%s::%d::%s", filePath, line, pattern.Name),
|
||||
Type: "security",
|
||||
Title: fmt.Sprintf("Potential %s detected", pattern.Name),
|
||||
Description: fmt.Sprintf("A potential %s was found in source code. Remove it and use environment variables or secret management.", pattern.Name),
|
||||
File: filePath,
|
||||
Line: line,
|
||||
Severity: pattern.Severity,
|
||||
Score: 10,
|
||||
Status: quality.StatusOpen,
|
||||
Metadata: map[string]string{
|
||||
"secret_type": pattern.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return findings, nil
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user