Add golang.org/x/tools/go/analysis based linting tool: - fetchml-vet: Custom go vet tool for security invariants Add analyzers for critical security patterns: - noBareDetector: Ensures CreateDetector always captures DetectionInfo (prevents silent metadata loss in GPU detection) - manifestEnv: Validates functions returning Artifacts populate Environment (ensures reproducibility metadata capture) - noInlineCredentials: Detects inline credential patterns in config structs (enforces environment variable references) - hipaaComplete: Validates HIPAA mode configs have all required fields (structural check for compliance completeness) Integration with make lint-custom: - Builds bin/fetchml-vet from tools/fetchml-vet/cmd/fetchml-vet/ - Runs with: go vet -vettool=bin/fetchml-vet ./internal/... Part of: V.4 custom linting from security plan
96 lines
2.2 KiB
Go
96 lines
2.2 KiB
Go
package analyzers
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/types"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
)
|
|
|
|
// ManifestEnvironmentAnalyzer flags any function that returns manifest.Artifacts
|
|
// without explicitly setting the Environment field. This enforces the V.1 requirement
|
|
// that Artifacts must always include Environment information for provenance.
|
|
var ManifestEnvironmentAnalyzer = &analysis.Analyzer{
|
|
Name: "manifestenv",
|
|
Doc: "flags functions returning Artifacts without Environment field set",
|
|
Run: runManifestEnvironment,
|
|
}
|
|
|
|
func runManifestEnvironment(pass *analysis.Pass) (interface{}, error) {
|
|
for _, file := range pass.Files {
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
// Look for return statements
|
|
ret, ok := n.(*ast.ReturnStmt)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// Check each returned value
|
|
for _, result := range ret.Results {
|
|
// Check if it's a struct literal
|
|
composite, ok := result.(*ast.CompositeLit)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Check if the type is manifest.Artifacts
|
|
typeInfo := pass.TypesInfo.TypeOf(composite)
|
|
if typeInfo == nil {
|
|
continue
|
|
}
|
|
|
|
typeStr := typeInfo.String()
|
|
if !strings.Contains(typeStr, "manifest.Artifacts") && !strings.Contains(typeStr, "Artifacts") {
|
|
continue
|
|
}
|
|
|
|
// Check if Environment field is set
|
|
hasEnv := false
|
|
for _, elt := range composite.Elts {
|
|
kv, ok := elt.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key, ok := kv.Key.(*ast.Ident)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if key.Name == "Environment" {
|
|
hasEnv = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasEnv {
|
|
pass.Reportf(composite.Pos(),
|
|
"returning Artifacts without Environment field set - Environment is required for provenance (V.1)")
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// isArtifactsType checks if a type is manifest.Artifacts
|
|
func isArtifactsType(t types.Type) bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
named, ok := t.(*types.Named)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return named.Obj().Name() == "Artifacts"
|
|
}
|
|
|
|
// getPackagePath returns the package path of a named type
|
|
func getPackagePath(t types.Type) string {
|
|
named, ok := t.(*types.Named)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return named.Obj().Pkg().Path()
|
|
}
|