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
223 lines
5.4 KiB
Go
223 lines
5.4 KiB
Go
package analyzers
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/token"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
)
|
|
|
|
// HIPAACompletenessAnalyzer flags any switch or if-else statement that checks
|
|
// compliance_mode == "hipaa" but doesn't verify all six hard-required fields.
|
|
// This prevents partial HIPAA enforcement from silently passing.
|
|
var HIPAACompletenessAnalyzer = &analysis.Analyzer{
|
|
Name: "hippacomplete",
|
|
Doc: "flags incomplete HIPAA compliance mode checks",
|
|
Run: runHIPAACompleteness,
|
|
}
|
|
|
|
// hipaaRequiredFields are the six fields that must be checked in HIPAA mode
|
|
var hipaaRequiredFields = []string{
|
|
"ConfigHash",
|
|
"SandboxSeccomp",
|
|
"SandboxNoNewPrivs",
|
|
"SandboxNetworkMode",
|
|
"MaxWorkers",
|
|
"ComplianceMode",
|
|
}
|
|
|
|
func runHIPAACompleteness(pass *analysis.Pass) (interface{}, error) {
|
|
for _, file := range pass.Files {
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
// Look for if statements
|
|
ifStmt, ok := n.(*ast.IfStmt)
|
|
if ok {
|
|
checkHIPAACondition(pass, ifStmt.Cond, ifStmt.Body)
|
|
return true
|
|
}
|
|
|
|
// Look for switch statements
|
|
switchStmt, ok := n.(*ast.SwitchStmt)
|
|
if ok {
|
|
checkHIPAASwitch(pass, switchStmt)
|
|
return true
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// checkHIPAACondition checks if an if condition is checking for HIPAA mode
|
|
func checkHIPAACondition(pass *analysis.Pass, cond ast.Expr, body *ast.BlockStmt) {
|
|
// Check if condition checks for "hipaa" string
|
|
if !containsHIPAACheck(cond) {
|
|
return
|
|
}
|
|
|
|
// Check what fields are accessed in the body
|
|
checkedFields := extractCheckedFields(body)
|
|
|
|
// Report missing fields
|
|
var missing []string
|
|
for _, required := range hipaaRequiredFields {
|
|
found := false
|
|
for _, checked := range checkedFields {
|
|
if strings.EqualFold(checked, required) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
missing = append(missing, required)
|
|
}
|
|
}
|
|
|
|
// If checking HIPAA mode but not all required fields, report it
|
|
if len(missing) > 0 && len(missing) < len(hipaaRequiredFields) {
|
|
// Partial check detected - this is the problematic case
|
|
pass.Reportf(body.Pos(),
|
|
"HIPAA compliance mode check is incomplete - missing required fields: %v",
|
|
missing)
|
|
}
|
|
}
|
|
|
|
// checkHIPAASwitch checks switch statements for HIPAA mode handling
|
|
func checkHIPAASwitch(pass *analysis.Pass, switchStmt *ast.SwitchStmt) {
|
|
// Check if the switch tag checks compliance mode
|
|
if switchStmt.Tag == nil {
|
|
return
|
|
}
|
|
|
|
if !isComplianceModeCheck(switchStmt.Tag) {
|
|
return
|
|
}
|
|
|
|
// Find the "hipaa" case
|
|
var hipaaCase *ast.CaseClause
|
|
for _, stmt := range switchStmt.Body.List {
|
|
caseClause, ok := stmt.(*ast.CaseClause)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Check if this case is for "hipaa"
|
|
for _, val := range caseClause.List {
|
|
if isHipaaString(val) {
|
|
hipaaCase = caseClause
|
|
break
|
|
}
|
|
}
|
|
if hipaaCase != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
if hipaaCase == nil {
|
|
return
|
|
}
|
|
|
|
// Check what fields are accessed in the hipaa case body
|
|
checkedFields := extractCheckedFieldsFromStmts(hipaaCase.Body)
|
|
|
|
// Report missing fields
|
|
var missing []string
|
|
for _, required := range hipaaRequiredFields {
|
|
found := false
|
|
for _, checked := range checkedFields {
|
|
if strings.EqualFold(checked, required) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
missing = append(missing, required)
|
|
}
|
|
}
|
|
|
|
if len(missing) > 0 && len(missing) < len(hipaaRequiredFields) {
|
|
pass.Reportf(hipaaCase.Pos(),
|
|
"HIPAA case is incomplete - missing required field checks: %v",
|
|
missing)
|
|
}
|
|
}
|
|
|
|
// containsHIPAACheck checks if an expression contains a check for "hipaa"
|
|
func containsHIPAACheck(expr ast.Expr) bool {
|
|
switch e := expr.(type) {
|
|
case *ast.BinaryExpr:
|
|
// Check for == "hipaa" or != "hipaa"
|
|
if isHipaaString(e.X) || isHipaaString(e.Y) {
|
|
return true
|
|
}
|
|
// Recursively check both sides
|
|
return containsHIPAACheck(e.X) || containsHIPAACheck(e.Y)
|
|
case *ast.CallExpr:
|
|
// Check for strings.EqualFold(x, "hipaa") or similar
|
|
return containsHipaaInCall(e)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isHipaaString checks if an expression is the string literal "hipaa"
|
|
func isHipaaString(expr ast.Expr) bool {
|
|
lit, ok := expr.(*ast.BasicLit)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return lit.Kind == token.STRING && (lit.Value == `"hipaa"` || lit.Value == `"HIPAA"`)
|
|
}
|
|
|
|
// isComplianceModeCheck checks if an expression is accessing compliance_mode
|
|
func isComplianceModeCheck(expr ast.Expr) bool {
|
|
switch e := expr.(type) {
|
|
case *ast.SelectorExpr:
|
|
return strings.EqualFold(e.Sel.Name, "ComplianceMode") ||
|
|
strings.EqualFold(e.Sel.Name, "compliance_mode")
|
|
case *ast.Ident:
|
|
return strings.EqualFold(e.Name, "complianceMode") ||
|
|
strings.EqualFold(e.Name, "compliance_mode")
|
|
}
|
|
return false
|
|
}
|
|
|
|
// containsHipaaInCall checks if a function call contains "hipaa" as an argument
|
|
func containsHipaaInCall(call *ast.CallExpr) bool {
|
|
for _, arg := range call.Args {
|
|
if isHipaaString(arg) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// extractCheckedFields extracts field names that are accessed in a block
|
|
func extractCheckedFields(block *ast.BlockStmt) []string {
|
|
if block == nil {
|
|
return nil
|
|
}
|
|
return extractCheckedFieldsFromStmts(block.List)
|
|
}
|
|
|
|
// extractCheckedFieldsFromStmts extracts field names from a list of statements
|
|
func extractCheckedFieldsFromStmts(stmts []ast.Stmt) []string {
|
|
var fields []string
|
|
|
|
for _, stmt := range stmts {
|
|
ast.Inspect(stmt, func(n ast.Node) bool {
|
|
// Look for selector expressions (field access)
|
|
sel, ok := n.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
fieldName := sel.Sel.Name
|
|
fields = append(fields, fieldName)
|
|
return true
|
|
})
|
|
}
|
|
|
|
return fields
|
|
}
|