-
Notifications
You must be signed in to change notification settings - Fork 5
/
exec.go
173 lines (141 loc) · 5.03 KB
/
exec.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package dreck
import (
"context"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"syscall"
"github.com/miekg/dreck/log"
"github.com/google/go-github/v28/github"
)
// sanitize checks the exec command s to see if a respects our white list.
// It is also check for a maximum length of 64, allow what isExec matches, but disallow ..
func sanitize(s string) bool {
if len(s) > 64 {
return false
}
ok := isExec(s)
if !ok {
return false
}
// Extra check for .. because the regexp doesn't catch that.
if strings.Contains(s, "..") {
return false
}
return true
}
func (d Dreck) exec(ctx context.Context, client *github.Client, req IssueCommentOuter, conf *DreckConfig, c *Action) (*github.Response, error) {
// Due to $reasons c.Value may be prefixed with spaces and a :, strip those off, c.Value should
// then start with a slash.
run, err := stripValue(c.Value)
if err != nil {
return nil, fmt.Errorf("illegal exec command %s", run)
}
log.Infof("%s wants to execute %s for #%d", req.Comment.User.Login, run, req.Issue.Number)
parts := strings.Fields(run) // simple split
if len(parts) == 0 {
return nil, fmt.Errorf("illegal exec command %s", run)
}
if !isValidExec(conf, parts, run) {
return nil, fmt.Errorf("The command %s is not defined in any alias", run)
}
typ := "pull"
pull, _, err := client.PullRequests.Get(ctx, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number)
// 404 error when not found
if err != nil {
typ = "issue"
}
log.Infof("Assembling command '%s %s' for #%d", parts[0], strings.Join(parts[1:], " "), req.Issue.Number)
// Add pull:<NUM> or issue:<NUM> as the first arg.
trigger := fmt.Sprintf("%s/%d", typ, req.Issue.Number)
cmd, err := d.execCmd(parts, trigger)
if err != nil {
return nil, err
}
if typ == "pull" {
stat := newStatus(statusPending, "In progress", cmd)
client.Repositories.CreateStatus(ctx, req.Repository.Owner.Login, req.Repository.Name, pull.Head.GetSHA(), stat)
}
log.Infof("Executing '%s %s' for #%d", parts[0], strings.Join(parts[1:], " "), req.Issue.Number)
// Get all output
buf, err := cmd.CombinedOutput()
if err != nil {
if typ == "pull" {
stat := newStatus(statusFail, fmt.Sprintf("Failed: %s", err), cmd)
client.Repositories.CreateStatus(ctx, req.Repository.Owner.Login, req.Repository.Name, pull.Head.GetSHA(), stat)
}
body := fmt.Sprintf("The command `%s` did **not** run **successfully**. The status returned is `%s`\n\n", run, err.Error())
if len(buf) > 0 {
body += "Its standard and error output is"
body += "\n~~~\n" + string(buf) + "\n~~~\n"
}
comment := githubIssueComment(body)
_, resp, err := client.Issues.CreateComment(ctx, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number, comment)
return resp, err
}
body := fmt.Sprintf("The command `%s` ran **successfully**. Its standard and error output is", run)
body += "\n~~~\n" + string(buf) + "\n~~~\n"
comment := githubIssueComment(body)
_, resp, err := client.Issues.CreateComment(ctx, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number, comment)
if typ == "pull" {
stat := newStatus(statusOK, "Successful", cmd)
client.Repositories.CreateStatus(ctx, req.Repository.Owner.Login, req.Repository.Name, pull.Head.GetSHA(), stat)
}
return resp, err
}
// execCmd creates an exec.Cmd with the right attributes such as the environment and user to run as.
func (d Dreck) execCmd(parts []string, trigger string) (*exec.Cmd, error) {
cmd := exec.Command(parts[0], parts[1:]...)
for i := range parts {
if !sanitize(parts[i]) {
return nil, fmt.Errorf("exec: %q doesn't adhere to the sanitize checks", parts[i])
}
}
// run as d.user, if not empty
if d.user != "" {
uid, gid, err := userID(d.user)
if err != nil {
return nil, err
}
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
}
// extend environment
env := os.Environ()
env = append(env, fmt.Sprintf("GITHUB_TRIGGER=%s", trigger))
for e, v := range d.env {
env = append(env, fmt.Sprintf("%s=%s", e, v))
}
cmd.Env = env
return cmd, nil
}
func newStatus(s, desc string, cmd *exec.Cmd) *github.RepoStatus {
context := fmt.Sprintf("/exec %s", strings.Join(cmd.Args, " "))
return &github.RepoStatus{State: &s, Description: &desc, Context: &context}
}
func stripValue(s string) (string, error) {
pos := strings.Index(s, "/")
if pos < 0 {
return "", fmt.Errorf("illegal exec command %s", s)
}
return s[pos:], nil
}
func isValidExec(conf *DreckConfig, parts []string, run string) bool {
// Ok so run needs to come about from an expanded alias, that means it must be a prefix from one of those.
for _, a := range conf.Aliases {
r, err := NewAlias(a)
if err != nil {
log.Warningf("Failed to parse alias: %s, %v", a, err)
continue
}
if strings.HasPrefix(r.replace, Trigger+execConst+" "+parts[0]) {
log.Infof("Executing %s, because it is defined in alias expansion %s", run, r.replace)
return true
}
}
return false
}
// isExec checks our whitelist.
var isExec = regexp.MustCompile(`^[-a-zA-Z0-9 ./]+$`).MatchString