blob: a56eef0314ac4ac5bec061e60be59a6d0d246c0e [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package bashkit
2
3import (
4 "fmt"
5 "strings"
6
7 "mvdan.cc/sh/v3/syntax"
8)
9
10var checks = []func(*syntax.CallExpr) error{
11 noGitConfigUsernameEmailChanges,
12}
13
14// Check inspects bashScript and returns an error if it ought not be executed.
15// Check DOES NOT PROVIDE SECURITY against malicious actors.
16// It is intended to catch straightforward mistakes in which a model
17// does things despite having been instructed not to do them.
18func Check(bashScript string) error {
19 r := strings.NewReader(bashScript)
20 parser := syntax.NewParser()
21 file, err := parser.Parse(r, "")
22 if err != nil {
23 // Execution will fail, but we'll get a better error message from bash.
24 // Note that if this were security load bearing, this would be a terrible idea:
25 // You could smuggle stuff past Check by exploiting differences in what is considered syntactically valid.
26 // But it is not.
27 return nil
28 }
29
30 syntax.Walk(file, func(node syntax.Node) bool {
31 if err != nil {
32 return false
33 }
34 callExpr, ok := node.(*syntax.CallExpr)
35 if !ok {
36 return true
37 }
38 for _, check := range checks {
39 err = check(callExpr)
40 if err != nil {
41 return false
42 }
43 }
44 return true
45 })
46
47 return err
48}
49
50// noGitConfigUsernameEmailChanges checks for git config username/email changes.
51// It uses simple heuristics, and has both false positives and false negatives.
52func noGitConfigUsernameEmailChanges(cmd *syntax.CallExpr) error {
53 if hasGitConfigUsernameEmailChanges(cmd) {
54 return fmt.Errorf("permission denied: changing git config username/email is not allowed, use env vars instead")
55 }
56 return nil
57}
58
59func hasGitConfigUsernameEmailChanges(cmd *syntax.CallExpr) bool {
60 if len(cmd.Args) < 3 {
61 return false
62 }
63 if cmd.Args[0].Lit() != "git" {
64 return false
65 }
66
67 configIndex := -1
68 for i, arg := range cmd.Args {
69 if arg.Lit() == "config" {
70 configIndex = i
71 break
72 }
73 }
74
75 if configIndex < 0 || configIndex == len(cmd.Args)-1 {
76 return false
77 }
78
79 // check for user.name or user.email
80 keyIndex := -1
81 for i, arg := range cmd.Args {
82 if i < configIndex {
83 continue
84 }
85 if arg.Lit() == "user.name" || arg.Lit() == "user.email" {
86 keyIndex = i
87 break
88 }
89 }
90
91 if keyIndex < 0 || keyIndex == len(cmd.Args)-1 {
92 return false
93 }
94
95 // user.name/user.email is followed by a value
96 return true
97}