Files
trufflehog/hack/checksecretparts/check.go
Miccah ab5dd03ee0 Make detector Result.SecretParts initialization stricter (#4948)
* Update linter to disallow assigning SecretParts later

This means all detectors.Result objects must be created with the
SecretParts field set.

* Update documentation

* Migrate existing detectors to always initialize SecretParts
2026-05-05 08:30:41 -07:00

143 lines
3.4 KiB
Go

package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
)
// Finding describes a single detectors.Result{} construction in a package that
// never references the SecretParts field.
type Finding struct {
// Position is the source location of the offending composite literal.
Position token.Position
// Package is the directory containing the finding.
Package string
}
// CheckPackageDir runs the SecretParts check on a single directory. It returns
// one Finding per detectors.Result{} construction site in the directory.
func CheckPackageDir(dir string) ([]Finding, error) {
fset := token.NewFileSet()
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var files []*ast.File
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
path := filepath.Join(dir, name)
f, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err)
}
files = append(files, f)
}
if len(files) == 0 {
return nil, nil
}
return checkFiles(fset, dir, files), nil
}
// checkFiles inspects a set of parsed, non-test files from a single directory
// and returns findings. It is separated from CheckPackageDir so that tests can
// drive it with synthetic ASTs.
func checkFiles(fset *token.FileSet, dir string, files []*ast.File) []Finding {
var constructions []token.Position
for _, f := range files {
constructions = append(constructions, findResultConstructions(fset, f)...)
}
sort.Slice(constructions, func(i, j int) bool {
if constructions[i].Filename != constructions[j].Filename {
return constructions[i].Filename < constructions[j].Filename
}
return constructions[i].Offset < constructions[j].Offset
})
out := make([]Finding, len(constructions))
for i, pos := range constructions {
out[i] = Finding{Position: pos, Package: dir}
}
return out
}
// findResultConstructions returns positions of composite literals with the
// type detectors.Result (selector expression with identifier "detectors" and
// selector "Result"). It covers both bare literals and pointer literals
// (&detectors.Result{}).
func findResultConstructions(fset *token.FileSet, f *ast.File) []token.Position {
var positions []token.Position
ast.Inspect(f, func(n ast.Node) bool {
lit, ok := n.(*ast.CompositeLit)
if !ok {
return true
}
if !isDetectorsResultType(lit.Type) {
return true
}
if hasSecretPartsKey(lit) {
return true
}
positions = append(positions, fset.Position(lit.Lbrace))
return true
})
return positions
}
func isDetectorsResultType(expr ast.Expr) bool {
// Unwrap *detectors.Result which won't appear for composite literal Type,
// but handle parenthesized forms defensively.
for {
switch e := expr.(type) {
case *ast.ParenExpr:
expr = e.X
continue
case *ast.StarExpr:
expr = e.X
continue
}
break
}
sel, ok := expr.(*ast.SelectorExpr)
if !ok {
return false
}
pkg, ok := sel.X.(*ast.Ident)
if !ok {
return false
}
return pkg.Name == "detectors" && sel.Sel.Name == "Result"
}
func hasSecretPartsKey(lit *ast.CompositeLit) bool {
for _, elt := range lit.Elts {
kv, ok := elt.(*ast.KeyValueExpr)
if !ok {
continue
}
key, ok := kv.Key.(*ast.Ident)
if !ok {
continue
}
if key.Name == "SecretParts" {
return true
}
}
return false
}