blob: 8e67a3b26471e25ad12da145691a79975b4b3df8 [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}
Josh Bleecher Snyderdae19072025-04-30 01:08:57 +000098
99// WillRunGitCommit checks if the provided bash script will run 'git commit'.
100// It returns true if any command in the script is a git commit command.
101func WillRunGitCommit(bashScript string) (bool, error) {
102 r := strings.NewReader(bashScript)
103 parser := syntax.NewParser()
104 file, err := parser.Parse(r, "")
105 if err != nil {
106 // Parsing failed, but let's not consider this an error for the same reasons as in Check
107 return false, nil
108 }
109
110 willCommit := false
111
112 syntax.Walk(file, func(node syntax.Node) bool {
113 callExpr, ok := node.(*syntax.CallExpr)
114 if !ok {
115 return true
116 }
117 if isGitCommitCommand(callExpr) {
118 willCommit = true
119 return false
120 }
121 return true
122 })
123
124 return willCommit, nil
125}
126
127// isGitCommitCommand checks if a command is 'git commit'.
128func isGitCommitCommand(cmd *syntax.CallExpr) bool {
129 if len(cmd.Args) < 2 {
130 return false
131 }
132
133 // First argument must be 'git'
134 if cmd.Args[0].Lit() != "git" {
135 return false
136 }
137
138 // Look for 'commit' in any position after 'git'
139 for i := 1; i < len(cmd.Args); i++ {
140 if cmd.Args[i].Lit() == "commit" {
141 return true
142 }
143 }
144
145 return false
146}