Files
kandev/apps/backend/internal/common/subproc/raw_git_audit_test.go
T
2026-08-03 11:04:34 +01:00

127 lines
3.8 KiB
Go

package subproc
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"testing"
)
// TestProductionGitCommandsUseTheAdmissionSeam is a repository guard for the
// global Git cap. Production code may construct Git commands only in the
// subproc seam; every other package must invoke a classified helper instead.
// This catches raw command construction, executable lookup, and direct exec
// additions that a search for the legacy subproc.Git accessor would miss.
func TestProductionGitCommandsUseTheAdmissionSeam(t *testing.T) {
_, sourceFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
backendRoot := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "../../.."))
var violations []string
err := filepath.WalkDir(backendRoot, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
rel, err := filepath.Rel(backendRoot, path)
if err != nil {
return err
}
// The common/subproc package is the sole Git execution seam. It may
// contain the platform-specific command/exec primitives, while every
// production caller outside it must use a classified runner.
if !strings.HasPrefix(filepath.ToSlash(rel), "internal/common/subproc/") {
violations = append(violations, scanGoFile(t, path)...)
}
return nil
})
if err != nil {
t.Fatalf("walk production Go files: %v", err)
}
sort.Strings(violations)
if len(violations) != 0 {
t.Fatalf("raw Git execution bypasses the admission seam:\n%s", strings.Join(violations, "\n"))
}
}
func scanGoFile(t *testing.T, path string) []string {
t.Helper()
fileSet := token.NewFileSet()
file, err := parser.ParseFile(fileSet, path, nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", path, err)
}
var violations []string
ast.Inspect(file, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok || len(call.Args) == 0 {
return true
}
selector, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
execPkg, ok := selector.X.(*ast.Ident)
if !ok {
return true
}
if (execPkg.Name == "unix" || execPkg.Name == "syscall") && selector.Sel.Name == "Exec" {
position := fileSet.Position(call.Pos())
violations = append(violations, fmt.Sprintf("%s:%d: direct exec", filepath.ToSlash(path), position.Line))
return true
}
if execPkg.Name != "exec" {
return true
}
if selector.Sel.Name == "LookPath" {
if len(call.Args) == 0 || !isGitString(call.Args[0]) {
return true
}
position := fileSet.Position(call.Pos())
violations = append(violations, fmt.Sprintf("%s:%d: Git lookup", filepath.ToSlash(path), position.Line))
return true
}
if selector.Sel.Name != "Command" && selector.Sel.Name != "CommandContext" {
return true
}
// The command argument is the first argument for Command and the
// second for CommandContext. Inspect both positions explicitly.
commandIndex := 0
if selector.Sel.Name == "CommandContext" {
commandIndex = 1
}
if len(call.Args) <= commandIndex {
return true
}
if !isGitString(call.Args[commandIndex]) {
return true
}
position := fileSet.Position(call.Pos())
violations = append(violations, fmt.Sprintf("%s:%d", filepath.ToSlash(path), position.Line))
return true
})
return violations
}
func isGitString(expr ast.Expr) bool {
// NOTE: this guard intentionally recognizes direct string literals only;
// keep production Git construction on the shared subproc seam rather than
// relying on constant aliases that this lightweight AST walk cannot resolve.
literal, ok := expr.(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
return false
}
value, err := strconv.Unquote(literal.Value)
return err == nil && value == "git"
}