mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
first commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user