github.com/yogeshkumararora/slsa-github-generator@v1.10.1-0.20240520161934-11278bd5afb4/internal/builders/go/pkg/build.go (about) 1 // Copyright 2022 SLSA Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package pkg 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "os" 22 "path/filepath" 23 "regexp" 24 "strings" 25 26 "github.com/yogeshkumararora/slsa-github-generator/github" 27 "github.com/yogeshkumararora/slsa-github-generator/internal/runner" 28 "github.com/yogeshkumararora/slsa-github-generator/internal/utils" 29 ) 30 31 var unknownTag = "unknown" 32 33 // See `go build help`. 34 // `-asmflags`, `-n`, `-mod`, `-installsuffix`, `-modfile`, 35 // `-workfile`, `-overlay`, `-pkgdir`, `-toolexec`, `-o`, 36 // `-modcacherw`, `-work` not supported for now. 37 38 var allowedBuildArgs = map[string]bool{ 39 "-a": true, "-race": true, "-msan": true, "-asan": true, 40 "-v": true, "-x": true, "-buildinfo": true, 41 "-buildmode": true, "-buildvcs": true, "-compiler": true, 42 "-gccgoflags": true, "-gcflags": true, 43 "-ldflags": true, "-linkshared": true, 44 "-tags": true, "-trimpath": true, 45 } 46 47 var allowedEnvVariablePrefix = map[string]bool{ 48 "GO": true, "CGO_": true, 49 } 50 51 var ( 52 errEnvVariableNameEmpty = errors.New("variable name empty") 53 errUnsupportedArguments = errors.New("unsupported arguments") 54 errInvalidEnvArgument = errors.New("invalid env argument") 55 errEnvVariableNameNotAllowed = errors.New("invalid variable name") 56 errInvalidFilename = errors.New("invalid filename") 57 ) 58 59 // GoBuild implements building a Go application. 60 type GoBuild struct { 61 cfg *GoReleaserConfig 62 // Note: static env variables are contained in cfg.Env. 63 argEnv map[string]string 64 goc string 65 } 66 67 // GoBuildNew returns a new GoBuild. 68 func GoBuildNew(goc string, cfg *GoReleaserConfig) *GoBuild { 69 c := GoBuild{ 70 cfg: cfg, 71 goc: goc, 72 argEnv: make(map[string]string), 73 } 74 75 return &c 76 } 77 78 // Run executes the build. 79 func (b *GoBuild) Run(dry bool) error { 80 // Get directory. 81 dir, err := b.getDir() 82 if err != nil { 83 return err 84 } 85 // Set flags. 86 flags, err := b.generateFlags() 87 if err != nil { 88 return err 89 } 90 91 // Generate env variables. 92 envs, err := b.generateCommandEnvVariables() 93 if err != nil { 94 return err 95 } 96 97 // Generate ldflags. 98 ldflags, err := b.generateLdflags() 99 if err != nil { 100 return err 101 } 102 103 // Add ldflags. 104 if ldflags != "" { 105 flags = append(flags, fmt.Sprintf("-ldflags=%s", ldflags)) 106 } 107 108 // A dry run prints the information that is trusted, before 109 // the compiler is invoked. 110 if dry { 111 // Generate filename. 112 // Note: the filename uses the config file and is resolved if it contains env variables. 113 // `OUTPUT_BINARY` is only used during the actual compilation, an is a trusted 114 // variable hardcoded in the reusable workflow, to avoid weird looking name 115 // that may interfere with the compilation. 116 filename, err := b.generateOutputFilename() 117 if err != nil { 118 return err 119 } 120 121 // Generate the command. 122 com := b.generateCommand(flags, filename) 123 124 env, err := b.generateCommandEnvVariables() 125 if err != nil { 126 return err 127 } 128 129 r := runner.CommandRunner{ 130 Steps: []*runner.CommandStep{ 131 { 132 Command: com, 133 Env: env, 134 WorkingDir: dir, 135 }, 136 }, 137 } 138 139 steps, err := r.Dry() 140 if err != nil { 141 return err 142 } 143 144 // There is a single command in steps given to the runner so we are 145 // assured to have only one step. 146 menv, err := utils.MarshalToString(steps[0].Env) 147 if err != nil { 148 return err 149 } 150 command, err := utils.MarshalToString(steps[0].Command) 151 if err != nil { 152 return err 153 } 154 155 // Share the resolved name of the binary. 156 if err := github.SetOutput("go-binary-name", filename); err != nil { 157 return err 158 } 159 160 // Share the command used. 161 if err := github.SetOutput("go-command", command); err != nil { 162 return err 163 } 164 165 // Share the env variables used. 166 if err := github.SetOutput("go-env", menv); err != nil { 167 return err 168 } 169 170 // Share working directory necessary for issuing the vendoring command. 171 return github.SetOutput("go-working-dir", dir) 172 } 173 174 binary, err := getOutputBinaryPath(os.Getenv("OUTPUT_BINARY")) 175 if err != nil { 176 return err 177 } 178 179 // Generate the command. 180 command := b.generateCommand(flags, binary) 181 182 fmt.Println("dir", dir) 183 fmt.Println("binary", binary) 184 fmt.Println("command", command) 185 fmt.Println("env", envs) 186 187 r := runner.CommandRunner{ 188 Steps: []*runner.CommandStep{ 189 { 190 Command: command, 191 Env: envs, 192 WorkingDir: dir, 193 }, 194 }, 195 } 196 197 // TODO: Add a timeout? 198 _, err = r.Run(context.Background()) 199 return err 200 } 201 202 func getOutputBinaryPath(binary string) (string, error) { 203 // Use the name provider via env variable for the compilation. 204 // This variable is trusted and defined by the re-usable workflow. 205 // It should be set to an absolute path value. 206 abinary, err := filepath.Abs(binary) 207 if err != nil { 208 return "", fmt.Errorf("filepath.Abs: %w", err) 209 } 210 211 if binary == "" { 212 return "", fmt.Errorf("%w: OUTPUT_BINARY not defined", errInvalidFilename) 213 } 214 215 if binary != abinary { 216 return "", fmt.Errorf("%w: %v is not an absolute path", errInvalidFilename, binary) 217 } 218 219 return binary, nil 220 } 221 222 func (b *GoBuild) getDir() (string, error) { 223 if b.cfg.Dir == nil { 224 return os.Getenv("PWD"), nil 225 } 226 227 // Note: validation of the dir is done in config.go 228 fp, err := filepath.Abs(*b.cfg.Dir) 229 if err != nil { 230 return "", err 231 } 232 233 return fp, nil 234 } 235 236 func (b *GoBuild) generateCommand(flags []string, binary string) []string { 237 var command []string 238 command = append(command, flags...) 239 command = append(command, "-o", binary) 240 241 // Add the entry point. 242 if b.cfg.Main != nil { 243 command = append(command, *b.cfg.Main) 244 } 245 return command 246 } 247 248 func (b *GoBuild) generateCommandEnvVariables() ([]string, error) { 249 var env []string 250 251 if b.cfg.Goos == "" { 252 return nil, fmt.Errorf("%w: %s", errEnvVariableNameEmpty, "GOOS") 253 } 254 env = append(env, fmt.Sprintf("GOOS=%s", b.cfg.Goos)) 255 256 if b.cfg.Goarch == "" { 257 return nil, fmt.Errorf("%w: %s", errEnvVariableNameEmpty, "GOARCH") 258 } 259 env = append(env, fmt.Sprintf("GOARCH=%s", b.cfg.Goarch)) 260 261 // Set env variables from config file. 262 for k, v := range b.cfg.Env { 263 if !isAllowedEnvVariable(k) { 264 return env, fmt.Errorf("%w: %s", errEnvVariableNameNotAllowed, v) 265 } 266 267 env = append(env, fmt.Sprintf("%s=%s", k, v)) 268 } 269 270 return env, nil 271 } 272 273 // SetArgEnvVariables sets static environment variables. 274 func (b *GoBuild) SetArgEnvVariables(envs string) error { 275 // Notes: 276 // - I've tried running the re-usable workflow in a step 277 // and set the env variable in a previous step, but found that a re-usable workflow is not 278 // allowed to run in a step; they have to run as `job.uses`. Using `job.env` with `job.uses` 279 // is not allowed. 280 // - We don't want to allow env variables set in the workflow because of injections 281 // e.g. LD_PRELOAD, etc. 282 if envs == "" { 283 return nil 284 } 285 286 for _, e := range strings.Split(envs, ",") { 287 s := strings.Trim(e, " ") 288 289 sp := strings.Split(s, ":") 290 if len(sp) != 2 { 291 return fmt.Errorf("%w: %s", errInvalidEnvArgument, s) 292 } 293 name := strings.Trim(sp[0], " ") 294 value := strings.Trim(sp[1], " ") 295 296 fmt.Printf("arg env: %s:%s\n", name, value) 297 b.argEnv[name] = value 298 } 299 return nil 300 } 301 302 func (b *GoBuild) generateOutputFilename() (string, error) { 303 // Note: the `.` is needed to accommodate the semantic version 304 // as part of the name. 305 const alpha = ".abcdefghijklmnopqrstuvwxyz1234567890-_" 306 307 var name string 308 309 // Special variables. 310 name, err := b.resolveSpecialVariables(b.cfg.Binary) 311 if err != nil { 312 return "", err 313 } 314 315 // Dynamic env variables provided by caller. 316 name, err = b.resolveEnvVariables(name) 317 if err != nil { 318 return "", err 319 } 320 321 for _, char := range name { 322 if !strings.Contains(alpha, strings.ToLower(string(char))) { 323 return "", fmt.Errorf("%w: found character '%c'", errInvalidFilename, char) 324 } 325 } 326 327 if name == "" { 328 return "", fmt.Errorf("%w: filename is empty", errInvalidFilename) 329 } 330 331 // Validate the path. 332 if err := validatePath(name); err != nil { 333 return "", err 334 } 335 336 return name, nil 337 } 338 339 func (b *GoBuild) generateFlags() ([]string, error) { 340 // -x 341 flags := []string{b.goc, "build", "-mod=vendor"} 342 343 for _, v := range b.cfg.Flags { 344 if !isAllowedArg(v) { 345 return nil, fmt.Errorf("%w: %s", errUnsupportedArguments, v) 346 } 347 flags = append(flags, v) 348 } 349 return flags, nil 350 } 351 352 func isAllowedArg(arg string) bool { 353 for k := range allowedBuildArgs { 354 if strings.HasPrefix(arg, k) { 355 return true 356 } 357 } 358 return false 359 } 360 361 // Check if the env variable is allowed. We want to avoid 362 // variable injection, e.g. LD_PRELOAD, etc. 363 // See an overview in https://www.hale-legacy.com/class/security/s20/handout/slides-env-vars.pdf. 364 func isAllowedEnvVariable(name string) bool { 365 for k := range allowedEnvVariablePrefix { 366 if strings.HasPrefix(name, k) { 367 return true 368 } 369 } 370 return false 371 } 372 373 // TODO: maybe not needed if handled directly by go compiler. 374 func (b *GoBuild) generateLdflags() (string, error) { 375 var a []string 376 377 // Resolve variables. 378 for _, v := range b.cfg.Ldflags { 379 // Special variables. 380 v, err := b.resolveSpecialVariables(v) 381 if err != nil { 382 return "", err 383 } 384 385 // Dynamic env variables provided by caller. 386 v, err = b.resolveEnvVariables(v) 387 if err != nil { 388 return "", err 389 } 390 a = append(a, v) 391 } 392 393 if len(a) > 0 { 394 return strings.Join(a, " "), nil 395 } 396 397 return "", nil 398 } 399 400 func (b *GoBuild) resolveSpecialVariables(s string) (string, error) { 401 reVar := regexp.MustCompile(`{{ \.([A-Z][a-z]*) }}`) 402 names := reVar.FindAllString(s, -1) 403 for _, n := range names { 404 name := strings.ReplaceAll(n, "{{ .", "") 405 name = strings.ReplaceAll(name, " }}", "") 406 407 switch name { 408 case "Os": 409 if b.cfg.Goos == "" { 410 return "", fmt.Errorf("%w: {{ .Os }}", errEnvVariableNameEmpty) 411 } 412 s = strings.ReplaceAll(s, n, b.cfg.Goos) 413 414 case "Arch": 415 if b.cfg.Goarch == "" { 416 return "", fmt.Errorf("%w: {{ .Arch }}", errEnvVariableNameEmpty) 417 } 418 s = strings.ReplaceAll(s, n, b.cfg.Goarch) 419 420 case "Tag": 421 tag := getTag() 422 s = strings.ReplaceAll(s, n, tag) 423 default: 424 return "", fmt.Errorf("%w: %s", errInvalidEnvArgument, n) 425 } 426 } 427 return s, nil 428 } 429 430 func (b *GoBuild) resolveEnvVariables(s string) (string, error) { 431 reDyn := regexp.MustCompile(`{{ \.Env\.(\w+) }}`) 432 names := reDyn.FindAllString(s, -1) 433 for _, n := range names { 434 name := strings.ReplaceAll(n, "{{ .Env.", "") 435 name = strings.ReplaceAll(name, " }}", "") 436 437 val, exists := b.argEnv[name] 438 if !exists { 439 return "", fmt.Errorf("%w: %s", errEnvVariableNameEmpty, n) 440 } 441 s = strings.ReplaceAll(s, n, val) 442 } 443 return s, nil 444 } 445 446 func getTag() string { 447 tag := os.Getenv("GITHUB_REF_NAME") 448 if tag == "" { 449 return unknownTag 450 } 451 return tag 452 }