1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-10 03:47:03 +02:00
goreleaser/internal/pipe/sbom/sbom.go
Carlos Alexandro Becker cd2feb3c81
fix(sbom): syft might fail on windows (#4301)
the paths of the artifacts always use forward slashes, and the logic to
handle the relative path stuff inside the sbom pipe did not account for
that.

running the paths through `filepath.Clean` beforehand fixes it.

also improved yamlschema a little bit :) 

closes #4289
2023-09-17 14:23:04 -03:00

327 lines
8.8 KiB
Go

package sbom
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/caarlos0/log"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/gio"
"github.com/goreleaser/goreleaser/internal/ids"
"github.com/goreleaser/goreleaser/internal/logext"
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/internal/skips"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)
// Environment variables to pass through to exec
var passthroughEnvVars = []string{"HOME", "USER", "USERPROFILE", "TMPDIR", "TMP", "TEMP", "PATH", "LOCALAPPDATA"}
// Pipe that catalogs common artifacts as an SBOM.
type Pipe struct{}
func (Pipe) String() string { return "cataloging artifacts" }
func (Pipe) Skip(ctx *context.Context) bool {
return skips.Any(ctx, skips.SBOM) || len(ctx.Config.SBOMs) == 0
}
func (Pipe) Dependencies(ctx *context.Context) []string {
var cmds []string
for _, s := range ctx.Config.SBOMs {
cmds = append(cmds, s.Cmd)
}
return cmds
}
// Default sets the Pipes defaults.
func (Pipe) Default(ctx *context.Context) error {
ids := ids.New("sboms")
for i := range ctx.Config.SBOMs {
cfg := &ctx.Config.SBOMs[i]
if err := setConfigDefaults(cfg); err != nil {
return err
}
ids.Inc(cfg.ID)
}
return ids.Validate()
}
func setConfigDefaults(cfg *config.SBOM) error {
if cfg.Cmd == "" {
cfg.Cmd = "syft"
}
if cfg.Artifacts == "" {
cfg.Artifacts = "archive"
}
if len(cfg.Documents) == 0 {
switch cfg.Artifacts {
case "binary":
cfg.Documents = []string{"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"}
case "any":
cfg.Documents = []string{}
default:
cfg.Documents = []string{"{{ .ArtifactName }}.sbom"}
}
}
if cfg.Cmd == "syft" {
if len(cfg.Args) == 0 {
cfg.Args = []string{"$artifact", "--file", "$document", "--output", "spdx-json"}
}
if len(cfg.Env) == 0 && (cfg.Artifacts == "source" || cfg.Artifacts == "archive") {
cfg.Env = []string{
"SYFT_FILE_METADATA_CATALOGER_ENABLED=true",
}
}
}
if cfg.ID == "" {
cfg.ID = "default"
}
if cfg.Artifacts != "any" && len(cfg.Documents) > 1 {
return fmt.Errorf("multiple SBOM outputs when artifacts=%q is unsupported", cfg.Artifacts)
}
return nil
}
// Run executes the Pipe.
func (Pipe) Run(ctx *context.Context) error {
g := semerrgroup.New(ctx.Parallelism)
for _, cfg := range ctx.Config.SBOMs {
g.Go(catalogTask(ctx, cfg))
}
return g.Wait()
}
func catalogTask(ctx *context.Context, cfg config.SBOM) func() error {
return func() error {
var filters []artifact.Filter
switch cfg.Artifacts {
case "source":
filters = append(filters, artifact.ByType(artifact.UploadableSourceArchive))
if len(cfg.IDs) > 0 {
log.Warn("when artifacts is `source`, `ids` has no effect. ignoring")
}
case "archive":
filters = append(filters, artifact.ByType(artifact.UploadableArchive))
case "binary":
filters = append(filters, artifact.ByBinaryLikeArtifacts(ctx.Artifacts))
case "package":
filters = append(filters, artifact.ByType(artifact.LinuxPackage))
case "any":
newArtifacts, err := catalogArtifact(ctx, cfg, nil)
if err != nil {
return err
}
for _, newArtifact := range newArtifacts {
ctx.Artifacts.Add(newArtifact)
}
return nil
default:
return fmt.Errorf("invalid list of artifacts to catalog: %s", cfg.Artifacts)
}
if len(cfg.IDs) > 0 {
filters = append(filters, artifact.ByIDs(cfg.IDs...))
}
artifacts := ctx.Artifacts.Filter(artifact.And(filters...)).List()
return catalog(ctx, cfg, artifacts)
}
}
func catalog(ctx *context.Context, cfg config.SBOM, artifacts []*artifact.Artifact) error {
for _, a := range artifacts {
newArtifacts, err := catalogArtifact(ctx, cfg, a)
if err != nil {
return err
}
for _, newArtifact := range newArtifacts {
ctx.Artifacts.Add(newArtifact)
}
}
return nil
}
func subprocessDistPath(distDir string, pathRelativeToCwd string) (string, error) {
distDir = filepath.Clean(distDir)
pathRelativeToCwd = filepath.Clean(pathRelativeToCwd)
cwd, err := os.Getwd()
if err != nil {
return "", err
}
if !filepath.IsAbs(distDir) {
distDir, err = filepath.Abs(distDir)
if err != nil {
return "", err
}
}
relativePath, err := filepath.Rel(cwd, distDir)
if err != nil {
return "", err
}
return strings.TrimPrefix(pathRelativeToCwd, relativePath+string(filepath.Separator)), nil
}
func catalogArtifact(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact) ([]*artifact.Artifact, error) {
artifactDisplayName := "(any)"
args, envs, paths, err := applyTemplate(ctx, cfg, a)
if err != nil {
return nil, fmt.Errorf("cataloging artifacts failed: %w", err)
}
if a != nil {
artifactDisplayName = a.Path
}
var names []string
for _, p := range paths {
names = append(names, filepath.Base(p))
}
// The GoASTScanner flags this as a security risk.
// However, this works as intended. The nosec annotation
// tells the scanner to ignore this.
// #nosec
cmd := exec.CommandContext(ctx, cfg.Cmd, args...)
cmd.Env = []string{}
for _, key := range passthroughEnvVars {
if value := os.Getenv(key); value != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
}
cmd.Env = append(cmd.Env, envs...)
cmd.Dir = ctx.Config.Dist
log.WithField("env", cmd.Env).
WithField("dir", cmd.Dir).
WithField("cmd", cmd.Args).
Debug("running")
var b bytes.Buffer
w := gio.Safe(&b)
cmd.Stderr = io.MultiWriter(logext.NewWriter(), w)
cmd.Stdout = io.MultiWriter(logext.NewWriter(), w)
log.WithField("cmd", cfg.Cmd).
WithField("artifact", artifactDisplayName).
WithField("sbom", names).
Info("cataloging")
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("cataloging artifacts: %s failed: %w: %s", cfg.Cmd, err, b.String())
}
var artifacts []*artifact.Artifact
for _, path := range paths {
if !filepath.IsAbs(path) {
path = filepath.Join(ctx.Config.Dist, path)
}
matches, err := filepath.Glob(path)
if err != nil {
return nil, fmt.Errorf("cataloging artifacts: failed to find SBOM artifact %q: %w", path, err)
}
for _, match := range matches {
artifacts = append(artifacts, &artifact.Artifact{
Type: artifact.SBOM,
Name: filepath.Base(path),
Path: match,
Extra: map[string]interface{}{
artifact.ExtraID: cfg.ID,
},
})
}
}
return artifacts, nil
}
func applyTemplate(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact) ([]string, []string, []string, error) {
env := ctx.Env.Copy()
var extraEnvs []string
templater := tmpl.New(ctx).WithEnv(env)
if a != nil {
procPath, err := subprocessDistPath(ctx.Config.Dist, a.Path)
if err != nil {
return nil, nil, nil, fmt.Errorf("cataloging artifacts failed: cannot determine artifact path for %q: %w", a.Path, err)
}
extraEnvs = appendExtraEnv("artifact", procPath, extraEnvs, env)
extraEnvs = appendExtraEnv("artifactID", a.ID(), extraEnvs, env)
templater = templater.WithArtifact(a)
}
for _, keyValue := range cfg.Env {
renderedKeyValue, err := templater.Apply(expand(keyValue, env))
if err != nil {
return nil, nil, nil, fmt.Errorf("env %q: invalid template: %w", keyValue, err)
}
extraEnvs = append(extraEnvs, renderedKeyValue)
k, v, _ := strings.Cut(renderedKeyValue, "=")
env[k] = v
}
var paths []string
for idx, sbom := range cfg.Documents {
input := expand(sbom, env)
if !filepath.IsAbs(input) {
// assume any absolute path is handled correctly and assume that any relative path is not already
// adjusted to reference the dist path
input = filepath.Join(ctx.Config.Dist, input)
}
path, err := templater.Apply(input)
if err != nil {
return nil, nil, nil, fmt.Errorf("input %q: invalid template: %w", input, err)
}
path, err = filepath.Abs(path)
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to create artifact path %q: %w", sbom, err)
}
procPath, err := subprocessDistPath(ctx.Config.Dist, path)
if err != nil {
return nil, nil, nil, fmt.Errorf("cannot determine document path for %q: %w", path, err)
}
extraEnvs = appendExtraEnv(fmt.Sprintf("document%d", idx), procPath, extraEnvs, env)
if idx == 0 {
extraEnvs = appendExtraEnv("document", procPath, extraEnvs, env)
}
paths = append(paths, procPath)
}
// nolint:prealloc
var args []string
for _, arg := range cfg.Args {
renderedArg, err := templater.Apply(expand(arg, env))
if err != nil {
return nil, nil, nil, fmt.Errorf("arg %q: invalid template: %w", arg, err)
}
args = append(args, renderedArg)
}
return args, extraEnvs, paths, nil
}
func appendExtraEnv(key, value string, envs []string, env map[string]string) []string {
env[key] = value
return append(envs, fmt.Sprintf("%s=%s", key, value))
}
func expand(s string, env map[string]string) string {
return os.Expand(s, func(key string) string {
return env[key]
})
}