blob: 23dc1b4afbb33bae15b67f2b1ef207d5933cd207 [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,
Josh Bleecher Snyderdbfd36a2025-05-23 20:57:50 +000012 noBlindGitAdd,
Earl Lee2e463fb2025-04-17 11:22:22 -070013}
14
15// Check inspects bashScript and returns an error if it ought not be executed.
16// Check DOES NOT PROVIDE SECURITY against malicious actors.
17// It is intended to catch straightforward mistakes in which a model
18// does things despite having been instructed not to do them.
19func Check(bashScript string) error {
20 r := strings.NewReader(bashScript)
21 parser := syntax.NewParser()
22 file, err := parser.Parse(r, "")
23 if err != nil {
24 // Execution will fail, but we'll get a better error message from bash.
25 // Note that if this were security load bearing, this would be a terrible idea:
26 // You could smuggle stuff past Check by exploiting differences in what is considered syntactically valid.
27 // But it is not.
28 return nil
29 }
30
31 syntax.Walk(file, func(node syntax.Node) bool {
32 if err != nil {
33 return false
34 }
35 callExpr, ok := node.(*syntax.CallExpr)
36 if !ok {
37 return true
38 }
39 for _, check := range checks {
40 err = check(callExpr)
41 if err != nil {
42 return false
43 }
44 }
45 return true
46 })
47
48 return err
49}
50
51// noGitConfigUsernameEmailChanges checks for git config username/email changes.
52// It uses simple heuristics, and has both false positives and false negatives.
53func noGitConfigUsernameEmailChanges(cmd *syntax.CallExpr) error {
54 if hasGitConfigUsernameEmailChanges(cmd) {
55 return fmt.Errorf("permission denied: changing git config username/email is not allowed, use env vars instead")
56 }
57 return nil
58}
59
60func hasGitConfigUsernameEmailChanges(cmd *syntax.CallExpr) bool {
61 if len(cmd.Args) < 3 {
62 return false
63 }
64 if cmd.Args[0].Lit() != "git" {
65 return false
66 }
67
68 configIndex := -1
69 for i, arg := range cmd.Args {
70 if arg.Lit() == "config" {
71 configIndex = i
72 break
73 }
74 }
75
76 if configIndex < 0 || configIndex == len(cmd.Args)-1 {
77 return false
78 }
79
80 // check for user.name or user.email
81 keyIndex := -1
82 for i, arg := range cmd.Args {
83 if i < configIndex {
84 continue
85 }
86 if arg.Lit() == "user.name" || arg.Lit() == "user.email" {
87 keyIndex = i
88 break
89 }
90 }
91
92 if keyIndex < 0 || keyIndex == len(cmd.Args)-1 {
93 return false
94 }
95
96 // user.name/user.email is followed by a value
97 return true
98}
Josh Bleecher Snyderdae19072025-04-30 01:08:57 +000099
100// WillRunGitCommit checks if the provided bash script will run 'git commit'.
101// It returns true if any command in the script is a git commit command.
102func WillRunGitCommit(bashScript string) (bool, error) {
103 r := strings.NewReader(bashScript)
104 parser := syntax.NewParser()
105 file, err := parser.Parse(r, "")
106 if err != nil {
107 // Parsing failed, but let's not consider this an error for the same reasons as in Check
108 return false, nil
109 }
110
111 willCommit := false
112
113 syntax.Walk(file, func(node syntax.Node) bool {
114 callExpr, ok := node.(*syntax.CallExpr)
115 if !ok {
116 return true
117 }
118 if isGitCommitCommand(callExpr) {
119 willCommit = true
120 return false
121 }
122 return true
123 })
124
125 return willCommit, nil
126}
127
Josh Bleecher Snyderdbfd36a2025-05-23 20:57:50 +0000128// noBlindGitAdd checks for git add commands that blindly add all files.
129// It rejects patterns like 'git add -A', 'git add .', 'git add --all', 'git add *'.
130func noBlindGitAdd(cmd *syntax.CallExpr) error {
131 if hasBlindGitAdd(cmd) {
132 return fmt.Errorf("permission denied: blind git add commands (git add -A, git add ., git add --all, git add *) are not allowed, specify files explicitly")
133 }
134 return nil
135}
136
137func hasBlindGitAdd(cmd *syntax.CallExpr) bool {
138 if len(cmd.Args) < 2 {
139 return false
140 }
141 if cmd.Args[0].Lit() != "git" {
142 return false
143 }
144
145 // Find the 'add' subcommand
146 addIndex := -1
147 for i, arg := range cmd.Args {
148 if arg.Lit() == "add" {
149 addIndex = i
150 break
151 }
152 }
153
154 if addIndex < 0 {
155 return false
156 }
157
158 // Check arguments after 'add' for blind patterns
159 for i := addIndex + 1; i < len(cmd.Args); i++ {
160 arg := cmd.Args[i].Lit()
161 // Check for blind add patterns
162 if arg == "-A" || arg == "--all" || arg == "." || arg == "*" {
163 return true
164 }
165 }
166
167 return false
168}
169
Josh Bleecher Snyderdae19072025-04-30 01:08:57 +0000170// isGitCommitCommand checks if a command is 'git commit'.
171func isGitCommitCommand(cmd *syntax.CallExpr) bool {
172 if len(cmd.Args) < 2 {
173 return false
174 }
175
176 // First argument must be 'git'
177 if cmd.Args[0].Lit() != "git" {
178 return false
179 }
180
181 // Look for 'commit' in any position after 'git'
182 for i := 1; i < len(cmd.Args); i++ {
183 if cmd.Args[i].Lit() == "commit" {
184 return true
185 }
186 }
187
188 return false
189}